first commit
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>智慧缫丝系统</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,72 @@
|
||||
{
|
||||
"runtime": "Net80",
|
||||
"defaultVariables": null,
|
||||
"documentGenerator": {
|
||||
"fromDocument": {
|
||||
"url": "http://localhost:3000/swagger/v1/swagger.json",
|
||||
"output": null,
|
||||
"newLineBehavior": "Auto"
|
||||
}
|
||||
},
|
||||
"codeGenerators": {
|
||||
"openApiToTypeScriptClient": {
|
||||
"className": "ApiClient",
|
||||
"moduleName": "",
|
||||
"namespace": "",
|
||||
"typeScriptVersion": 4.3,
|
||||
"template": "Axios",
|
||||
"promiseType": "Promise",
|
||||
"httpClass": "HttpClient",
|
||||
"withCredentials": false,
|
||||
"useSingletonProvider": false,
|
||||
"injectionTokenType": "OpaqueToken",
|
||||
"rxJsVersion": 6.0,
|
||||
"dateTimeType": "Date",
|
||||
"nullValue": "Undefined",
|
||||
"generateClientClasses": true,
|
||||
"generateClientInterfaces": false,
|
||||
"generateOptionalParameters": false,
|
||||
"exportTypes": true,
|
||||
"wrapDtoExceptions": false,
|
||||
"exceptionClass": "ApiException",
|
||||
"clientBaseClass": null,
|
||||
"wrapResponses": false,
|
||||
"wrapResponseMethods": [],
|
||||
"generateResponseClasses": true,
|
||||
"responseClass": "SwaggerResponse",
|
||||
"protectedMethods": [],
|
||||
"configurationClass": null,
|
||||
"useTransformOptionsMethod": false,
|
||||
"useTransformResultMethod": false,
|
||||
"generateDtoTypes": true,
|
||||
"operationGenerationMode": "MultipleClientsFromOperationId",
|
||||
"markOptionalProperties": true,
|
||||
"generateCloneMethod": false,
|
||||
"typeStyle": "Interface",
|
||||
"enumStyle": "Enum",
|
||||
"useLeafType": false,
|
||||
"classTypes": [],
|
||||
"extendedClasses": [],
|
||||
"extensionCode": null,
|
||||
"generateDefaultValues": true,
|
||||
"excludedTypeNames": [],
|
||||
"excludedParameterNames": [],
|
||||
"handleReferences": false,
|
||||
"generateTypeCheckFunctions": false,
|
||||
"generateConstructorInterface": true,
|
||||
"convertConstructorInterfaceData": false,
|
||||
"importRequiredTypes": true,
|
||||
"useGetBaseUrlMethod": false,
|
||||
"baseUrlTokenName": "API_BASE_URL",
|
||||
"queryNullValue": "",
|
||||
"useAbortSignal": false,
|
||||
"inlineNamedDictionaries": false,
|
||||
"inlineNamedAny": false,
|
||||
"includeHttpContext": false,
|
||||
"templateDirectory": null,
|
||||
"serviceHost": null,
|
||||
"serviceSchemes": null,
|
||||
"output": "src/api/client.ts"
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+1862
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "f9-web-ai-test",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"gen:api": "nswag run nswag.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "^2.3.1",
|
||||
"@microsoft/signalr": "^10.0.11",
|
||||
"axios": "^1.7.7",
|
||||
"element-plus": "^2.8.4",
|
||||
"pinia": "^2.2.2",
|
||||
"vue": "^3.4.38",
|
||||
"vue-router": "^4.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.1.3",
|
||||
"vite": "^5.4.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
<template>
|
||||
<router-view />
|
||||
</template>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,24 @@
|
||||
import { useUserStore } from '../store/user';
|
||||
|
||||
// 权限判断工具函数
|
||||
export function hasPermission(value) {
|
||||
const userStore = useUserStore();
|
||||
const permissions = userStore.permissions || [];
|
||||
// admin 角色拥有所有权限
|
||||
const roles = userStore.userInfo?.roles || [];
|
||||
if (roles.includes('admin')) return true;
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
return value.some((p) => permissions.includes(p));
|
||||
}
|
||||
return permissions.includes(value);
|
||||
}
|
||||
|
||||
// 权限指令 v-permission="'system:user:add'"
|
||||
export default {
|
||||
mounted(el, binding) {
|
||||
if (!hasPermission(binding.value)) {
|
||||
el.parentNode && el.parentNode.removeChild(el);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
<template>
|
||||
<router-view />
|
||||
</template>
|
||||
@@ -0,0 +1,299 @@
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onBeforeUnmount } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { useUserStore } from '../store/user';
|
||||
import { resetRoutes } from '../router';
|
||||
import { ElMessageBox, ElNotification } from 'element-plus';
|
||||
import { initNotification, stopNotification, unreadCount, loadUnreadCount, openMessage } from '../utils/notification';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const userStore = useUserStore();
|
||||
|
||||
const isCollapse = ref(false);
|
||||
const sidebarMenus = ref([]);
|
||||
|
||||
// 面包屑
|
||||
const breadcrumbs = computed(() => {
|
||||
const matched = route.matched.filter((item) => item.meta && item.meta.title);
|
||||
return matched;
|
||||
});
|
||||
|
||||
// 构建菜单树(目录 + 菜单)
|
||||
function buildMenuTree(menus) {
|
||||
return menus.filter((m) => m.type === 'directory' || m.type === 'menu');
|
||||
}
|
||||
|
||||
// 实时收到新消息:弹窗提示(未读数量由 notification.js 统一 +1)
|
||||
function handleMessage(msg) {
|
||||
const p = ['普通', '重要', '紧急'][msg.priority] || '普通';
|
||||
const type = msg.priority === 2 ? 'error' : (msg.priority === 1 ? 'warning' : 'info');
|
||||
const isTodo = msg.category === 1;
|
||||
ElNotification({
|
||||
title: `【${p}】${isTodo ? '待办' : ''}${msg.typeName || '新消息'}`,
|
||||
message: msg.title,
|
||||
type,
|
||||
duration: 5000,
|
||||
onClick: () => {
|
||||
// 有业务跳转则跳业务页,否则跳消息中心
|
||||
if (!msg.routePath) {
|
||||
router.push('/notification/messages');
|
||||
return;
|
||||
}
|
||||
openMessage(msg, router);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function goMessages() {
|
||||
router.push('/notification/messages');
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
// 菜单已在路由守卫中加载并缓存到 store,这里直接使用
|
||||
if (userStore.menus.length === 0) {
|
||||
sidebarMenus.value = buildMenuTree(await userStore.fetchMenus());
|
||||
} else {
|
||||
sidebarMenus.value = buildMenuTree(userStore.menus);
|
||||
}
|
||||
|
||||
// 全局初始化 SignalR 通知连接 + 加载未读数量
|
||||
loadUnreadCount();
|
||||
initNotification(handleMessage);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
// 布局不卸载,这里仅作保险(登出时由 logout 触发 stopNotification)
|
||||
});
|
||||
|
||||
function toggleCollapse() {
|
||||
isCollapse.value = !isCollapse.value;
|
||||
}
|
||||
|
||||
async function handleLogout() {
|
||||
await ElMessageBox.confirm('确定退出登录吗?', '提示', { type: 'warning' });
|
||||
stopNotification();
|
||||
userStore.logout();
|
||||
resetRoutes();
|
||||
router.push('/login');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="layout">
|
||||
<!-- 侧边栏 -->
|
||||
<aside class="sidebar" :class="{ collapse: isCollapse }">
|
||||
<div class="logo">
|
||||
<el-icon class="logo-icon"><MagicStick /></el-icon>
|
||||
<span v-if="!isCollapse" class="logo-text">智慧缫丝系统</span>
|
||||
</div>
|
||||
<el-menu
|
||||
:default-active="route.path"
|
||||
:collapse="isCollapse"
|
||||
background-color="#0c3b2e"
|
||||
text-color="#b5cdc2"
|
||||
active-text-color="#e8c877"
|
||||
router
|
||||
class="menu"
|
||||
>
|
||||
<el-menu-item index="/dashboard">
|
||||
<el-icon><HomeFilled /></el-icon>
|
||||
<template #title>首页</template>
|
||||
</el-menu-item>
|
||||
|
||||
<template v-for="menu in sidebarMenus" :key="menu.id">
|
||||
<!-- 有子菜单 -->
|
||||
<el-sub-menu v-if="menu.children && menu.children.length > 0" :index="menu.path">
|
||||
<template #title>
|
||||
<el-icon><component :is="menu.icon || 'Menu'" /></el-icon>
|
||||
<span>{{ menu.name }}</span>
|
||||
</template>
|
||||
<el-menu-item
|
||||
v-for="child in menu.children"
|
||||
:key="child.id"
|
||||
:index="child.path"
|
||||
>
|
||||
<el-icon><component :is="child.icon || 'Document'" /></el-icon>
|
||||
<template #title>{{ child.name }}</template>
|
||||
</el-menu-item>
|
||||
</el-sub-menu>
|
||||
<!-- 无子菜单 -->
|
||||
<el-menu-item v-else :index="menu.path">
|
||||
<el-icon><component :is="menu.icon || 'Menu'" /></el-icon>
|
||||
<template #title>{{ menu.name }}</template>
|
||||
</el-menu-item>
|
||||
</template>
|
||||
</el-menu>
|
||||
</aside>
|
||||
|
||||
<!-- 主区域 -->
|
||||
<div class="main-container">
|
||||
<!-- 顶部栏 -->
|
||||
<header class="header">
|
||||
<div class="header-left">
|
||||
<el-icon class="collapse-btn" @click="toggleCollapse">
|
||||
<Expand v-if="isCollapse" />
|
||||
<Fold v-else />
|
||||
</el-icon>
|
||||
<!-- 面包屑 -->
|
||||
<el-breadcrumb separator="/">
|
||||
<el-breadcrumb-item
|
||||
v-for="item in breadcrumbs"
|
||||
:key="item.path"
|
||||
:to="item.path ? { path: item.path } : undefined"
|
||||
>
|
||||
{{ item.meta.title }}
|
||||
</el-breadcrumb-item>
|
||||
</el-breadcrumb>
|
||||
</div>
|
||||
|
||||
<div class="header-right">
|
||||
<!-- 消息中心铃铛(头像左侧) -->
|
||||
<el-tooltip content="消息中心" placement="bottom">
|
||||
<el-badge :value="unreadCount" :hidden="unreadCount === 0" :max="99" class="notify-badge">
|
||||
<el-icon class="notify-icon" @click="goMessages"><Bell /></el-icon>
|
||||
</el-badge>
|
||||
</el-tooltip>
|
||||
|
||||
<el-dropdown @command="handleLogout">
|
||||
<span class="user-info">
|
||||
<el-avatar :size="30" class="avatar">
|
||||
{{ (userStore.userInfo?.nickname || userStore.userInfo?.realName || 'U').charAt(0) }}
|
||||
</el-avatar>
|
||||
<span class="username">{{ userStore.userInfo?.nickname || userStore.userInfo?.realName }}</span>
|
||||
<el-icon><ArrowDown /></el-icon>
|
||||
</span>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="logout">退出登录</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- 内容区 -->
|
||||
<main class="content">
|
||||
<router-view />
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.layout {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
width: 220px;
|
||||
background: linear-gradient(180deg, #0f4a38 0%, #0c3b2e 100%);
|
||||
transition: width 0.3s;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.sidebar.collapse {
|
||||
width: 64px;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 60px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
color: #fff;
|
||||
font-size: 17px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 1px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
.logo-icon {
|
||||
color: #e8c877;
|
||||
}
|
||||
|
||||
.logo-text {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.menu {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.main-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.header {
|
||||
height: 60px;
|
||||
background: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 20px;
|
||||
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.collapse-btn {
|
||||
font-size: 20px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.notify-badge {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.notify-icon {
|
||||
font-size: 20px;
|
||||
cursor: pointer;
|
||||
color: #555;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.notify-icon:hover {
|
||||
color: #16a34a;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
background: #16a34a;
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.username {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
background: #f0f2f5;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,57 @@
|
||||
import { createApp } from 'vue';
|
||||
import { createPinia } from 'pinia';
|
||||
import ElementPlus from 'element-plus';
|
||||
import 'element-plus/dist/index.css';
|
||||
import zhCn from 'element-plus/es/locale/lang/zh-cn';
|
||||
import * as ElementPlusIconsVue from '@element-plus/icons-vue';
|
||||
import App from './App.vue';
|
||||
import router, { buildRoutes, markRoutesLoaded } from './router';
|
||||
import permissionDirective from './directives/permission';
|
||||
import { getAccessToken } from './utils/request';
|
||||
import request from './utils/request';
|
||||
import './styles/index.css';
|
||||
import './styles/theme.css';
|
||||
|
||||
const app = createApp(App);
|
||||
|
||||
// 注册所有图标
|
||||
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
||||
app.component(key, component);
|
||||
}
|
||||
|
||||
// 注册权限指令
|
||||
app.directive('permission', permissionDirective);
|
||||
|
||||
app.use(createPinia());
|
||||
|
||||
// 在挂载前同步初始化动态路由(避免 "No match" 警告)
|
||||
async function bootstrap() {
|
||||
if (getAccessToken()) {
|
||||
try {
|
||||
const [menusRes, permsRes] = await Promise.all([
|
||||
request.get('/auth/menus'),
|
||||
request.get('/auth/permissions')
|
||||
]);
|
||||
const routes = buildRoutes(menusRes.data);
|
||||
routes.forEach((r) => {
|
||||
if (!router.hasRoute(r.name)) {
|
||||
router.addRoute('Layout', r);
|
||||
}
|
||||
});
|
||||
// 缓存到 store
|
||||
const { useUserStore } = await import('./store/user');
|
||||
const store = useUserStore();
|
||||
store.menus = menusRes.data;
|
||||
store.permissions = permsRes.data;
|
||||
markRoutesLoaded();
|
||||
} catch (e) {
|
||||
// 菜单加载失败,可能是 token 失效,交给路由守卫跳登录
|
||||
}
|
||||
}
|
||||
|
||||
app.use(router);
|
||||
app.use(ElementPlus, { locale: zhCn });
|
||||
app.mount('#app');
|
||||
}
|
||||
|
||||
bootstrap();
|
||||
@@ -0,0 +1,169 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router';
|
||||
import { getAccessToken } from '../utils/request';
|
||||
import request from '../utils/request';
|
||||
|
||||
// 静态路由
|
||||
export const constantRoutes = [
|
||||
{
|
||||
path: '/login',
|
||||
name: 'Login',
|
||||
component: () => import('../views/login/index.vue'),
|
||||
meta: { title: '登录', hidden: true }
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
name: 'Layout',
|
||||
component: () => import('../layout/index.vue'),
|
||||
redirect: '/dashboard',
|
||||
children: [
|
||||
{
|
||||
path: 'dashboard',
|
||||
name: 'Dashboard',
|
||||
component: () => import('../views/dashboard/index.vue'),
|
||||
meta: { title: '首页', icon: 'HomeFilled' }
|
||||
},
|
||||
// 请假审批:静态注册,所有登录用户可访问(待办消息"去处理"跳转入口,不依赖菜单权限)
|
||||
{
|
||||
path: 'attendance/approve',
|
||||
name: 'AttendanceApprove',
|
||||
component: () => import('../views/attendance/approve/index.vue'),
|
||||
meta: { title: '请假审批', icon: 'Checked' }
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
// 已静态注册的路由(不随菜单动态注册,避免重复)
|
||||
const STATIC_ROUTES = ['/attendance/approve'];
|
||||
|
||||
// 组件映射:后端菜单的 component 字段 -> 前端组件
|
||||
export const componentMap = {
|
||||
'system/department/index': () => import('../views/system/department/index.vue'),
|
||||
'system/employee/index': () => import('../views/system/employee/index.vue'),
|
||||
'system/role/index': () => import('../views/system/role/index.vue'),
|
||||
'system/menu/index': () => import('../views/system/menu/index.vue'),
|
||||
'system/log/index': () => import('../views/system/log/index.vue'),
|
||||
'attendance/punch/index': () => import('../views/attendance/punch/index.vue'),
|
||||
'attendance/my/index': () => import('../views/attendance/my/index.vue'),
|
||||
'attendance/leave/index': () => import('../views/attendance/leave/index.vue'),
|
||||
'attendance/records/index': () => import('../views/attendance/records/index.vue'),
|
||||
'attendance/approve/index': () => import('../views/attendance/approve/index.vue'),
|
||||
'base/zhuangkou/index': () => import('../views/base/zhuangkou/index.vue'),
|
||||
'base/guige/index': () => import('../views/base/guige/index.vue'),
|
||||
'base/dingjizhibiao/index': () => import('../views/base/dingjizhibiao/index.vue'),
|
||||
'base/jizuchetai/index': () => import('../views/base/jizuchetai/index.vue'),
|
||||
'prod/lsjl/index': () => import('../views/prod/lsjl/index.vue'),
|
||||
'prod/xdjy/index': () => import('../views/prod/xdjy/index.vue'),
|
||||
'prod/hbjy/index': () => import('../views/prod/hbjy/index.vue'),
|
||||
'notification/messages/index': () => import('../views/notification/messages/index.vue'),
|
||||
'notification/subscribe/index': () => import('../views/notification/subscribe/index.vue'),
|
||||
'notification/type/index': () => import('../views/notification/type/index.vue'),
|
||||
'sys/user/index': () => import('../views/sys/user/index.vue'),
|
||||
'sys/role/index': () => import('../views/sys/role/index.vue'),
|
||||
'sys/menu/index': () => import('../views/sys/menu/index.vue'),
|
||||
'sys/department/index': () => import('../views/sys/department/index.vue')
|
||||
};
|
||||
|
||||
// 空壳布局组件(用于目录节点,透传子路由)
|
||||
const EmptyLayout = () => import('../layout/EmptyLayout.vue');
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes: constantRoutes
|
||||
});
|
||||
|
||||
// 白名单
|
||||
const whiteList = ['/login'];
|
||||
|
||||
// 动态路由是否已注册
|
||||
let routesLoaded = false;
|
||||
|
||||
// 标记路由已加载(main.js bootstrap 调用)
|
||||
export function markRoutesLoaded() {
|
||||
routesLoaded = true;
|
||||
}
|
||||
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
const token = getAccessToken();
|
||||
if (token) {
|
||||
if (to.path === '/login') {
|
||||
next({ path: '/' });
|
||||
return;
|
||||
}
|
||||
// 兜底:如果 bootstrap 未注册路由(比如登录后首次跳转),这里加载
|
||||
if (!routesLoaded) {
|
||||
try {
|
||||
const [menusRes, permsRes] = await Promise.all([
|
||||
request.get('/auth/menus'),
|
||||
request.get('/auth/permissions')
|
||||
]);
|
||||
const menus = menusRes.data;
|
||||
const routes = buildRoutes(menus);
|
||||
routes.forEach((r) => {
|
||||
if (!router.hasRoute(r.name)) {
|
||||
router.addRoute('Layout', r);
|
||||
}
|
||||
});
|
||||
const { useUserStore } = await import('../store/user');
|
||||
const store = useUserStore();
|
||||
store.menus = menus;
|
||||
store.permissions = permsRes.data;
|
||||
routesLoaded = true;
|
||||
// 重新导航到目标路由(此时路由已注册)
|
||||
next({ ...to, replace: true });
|
||||
return;
|
||||
} catch (e) {
|
||||
next('/login');
|
||||
return;
|
||||
}
|
||||
}
|
||||
next();
|
||||
} else {
|
||||
if (whiteList.includes(to.path)) {
|
||||
next();
|
||||
} else {
|
||||
next(`/login?redirect=${to.path}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 重置路由加载状态(退出登录时调用)
|
||||
export function resetRoutes() {
|
||||
routesLoaded = false;
|
||||
}
|
||||
|
||||
// 将后端菜单树转换为 Vue Router 路由(平铺注册到 Layout 下,用绝对路径)
|
||||
export function buildRoutes(menus) {
|
||||
const routes = [];
|
||||
const walk = (list) => {
|
||||
list.forEach((menu) => {
|
||||
// 只处理目录和菜单类型(按钮不生成路由)
|
||||
if (menu.type === 'directory') {
|
||||
routes.push({
|
||||
path: menu.path,
|
||||
name: menu.path.replace(/\//g, '-') + '-' + menu.id,
|
||||
component: EmptyLayout,
|
||||
meta: { title: menu.name, icon: menu.icon }
|
||||
});
|
||||
} else if (menu.type === 'menu') {
|
||||
// 已静态注册的路由跳过,避免重复
|
||||
if (STATIC_ROUTES.includes(menu.path)) return;
|
||||
routes.push({
|
||||
path: menu.path,
|
||||
name: menu.path.replace(/\//g, '-') + '-' + menu.id,
|
||||
component: componentMap[menu.component] || (() => import('../views/system/empty.vue')),
|
||||
meta: { title: menu.name, icon: menu.icon }
|
||||
});
|
||||
}
|
||||
// 递归处理子节点(平铺)
|
||||
if (menu.children && menu.children.length > 0) {
|
||||
walk(menu.children);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
walk(menus);
|
||||
return routes;
|
||||
}
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,43 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import request, { getAccessToken, setTokens, clearTokens } from '../utils/request';
|
||||
|
||||
export const useUserStore = defineStore('user', {
|
||||
state: () => ({
|
||||
token: getAccessToken() || '',
|
||||
userInfo: JSON.parse(localStorage.getItem('user') || 'null'),
|
||||
menus: [],
|
||||
permissions: []
|
||||
}),
|
||||
getters: {
|
||||
isLoggedIn: (state) => !!state.token
|
||||
},
|
||||
actions: {
|
||||
async login(loginForm) {
|
||||
const res = await request.post('/auth/login', loginForm);
|
||||
const { accessToken, refreshToken, user } = res.data;
|
||||
setTokens(accessToken, refreshToken);
|
||||
this.token = accessToken;
|
||||
this.userInfo = user;
|
||||
localStorage.setItem('user', JSON.stringify(user));
|
||||
return res;
|
||||
},
|
||||
async fetchMenus() {
|
||||
const res = await request.get('/auth/menus');
|
||||
this.menus = res.data;
|
||||
return res.data;
|
||||
},
|
||||
async fetchPermissions() {
|
||||
const res = await request.get('/auth/permissions');
|
||||
this.permissions = res.data;
|
||||
return res.data;
|
||||
},
|
||||
logout() {
|
||||
clearTokens();
|
||||
this.token = '';
|
||||
this.userInfo = null;
|
||||
this.menus = [];
|
||||
this.permissions = [];
|
||||
localStorage.removeItem('user');
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html, body, #app {
|
||||
height: 100%;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
background: #f6f4ef;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/* ============================================================
|
||||
智慧缫丝系统主题
|
||||
蚕桑黛绿(沉稳专业)+ 丝绸金(光泽点缀)+ 暖白(丝绸质感)
|
||||
============================================================ */
|
||||
:root {
|
||||
/* Element Plus 主色改为黛绿 */
|
||||
--el-color-primary: #1e6b52;
|
||||
--el-color-primary-light-3: #4d8f74;
|
||||
--el-color-primary-light-5: #82b39d;
|
||||
--el-color-primary-light-7: #bad5c8;
|
||||
--el-color-primary-light-8: #d6e7de;
|
||||
--el-color-primary-light-9: #ecf4f0;
|
||||
--el-color-primary-dark-2: #175544;
|
||||
|
||||
/* 成功色统一为黛绿系 */
|
||||
--el-color-success: #1e6b52;
|
||||
--el-color-success-light-3: #4d8f74;
|
||||
--el-color-success-light-5: #82b39d;
|
||||
--el-color-success-light-7: #bad5c8;
|
||||
--el-color-success-light-8: #d6e7de;
|
||||
--el-color-success-light-9: #ecf4f0;
|
||||
--el-color-success-dark-2: #175544;
|
||||
|
||||
/* 自定义全局变量 */
|
||||
--lc-primary: #1e6b52; /* 主色:黛绿 */
|
||||
--lc-primary-dark: #175544; /* 深黛绿 */
|
||||
--lc-primary-deep: #0c3b2e; /* 墨绿(侧边栏) */
|
||||
--lc-accent: #c9a54a; /* 丝绸金 */
|
||||
--lc-accent-light: #e8c877;
|
||||
--lc-bg: #f6f4ef; /* 暖白背景(丝绸质感) */
|
||||
--lc-text: #2c3a35; /* 主文字 */
|
||||
--lc-text-secondary: #6b7a74; /* 次要文字 */
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// 新部门接口(/sys/department)返回扁平列表,这里转换为 el-tree 所需树结构
|
||||
export function buildDeptTree(list) {
|
||||
const map = {};
|
||||
(list || []).forEach((d) => {
|
||||
map[d.id] = { id: d.id, name: d.name, code: d.code, parentId: d.parentId, children: [] };
|
||||
});
|
||||
const roots = [];
|
||||
(list || []).forEach((d) => {
|
||||
const node = map[d.id];
|
||||
if (d.parentId && map[d.parentId]) map[d.parentId].children.push(node);
|
||||
else roots.push(node);
|
||||
});
|
||||
return roots;
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import { ref } from 'vue';
|
||||
import * as signalR from '@microsoft/signalr';
|
||||
import { getAccessToken } from './request';
|
||||
import request from './request';
|
||||
|
||||
// 全局未读数量(layout 徽章 与 消息页共用)
|
||||
export const unreadCount = ref(0);
|
||||
|
||||
let connection = null;
|
||||
const messageHandlers = [];
|
||||
const businessChangeHandlers = [];
|
||||
|
||||
/** 加载未读数量 */
|
||||
export async function loadUnreadCount() {
|
||||
try {
|
||||
const res = await request.get('/notification/unread-count');
|
||||
unreadCount.value = res.data.count;
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化 SignalR 通知连接(幂等,重复调用返回同一连接)
|
||||
* @param {Function} onMessage 收到新消息时的回调 (msg) => void
|
||||
*/
|
||||
export function initNotification(onMessage) {
|
||||
if (connection) {
|
||||
if (onMessage) messageHandlers.push(onMessage);
|
||||
return connection;
|
||||
}
|
||||
|
||||
if (onMessage) messageHandlers.push(onMessage);
|
||||
|
||||
connection = new signalR.HubConnectionBuilder()
|
||||
.withUrl('/hubs/notification', {
|
||||
accessTokenFactory: () => getAccessToken() || ''
|
||||
})
|
||||
.withAutomaticReconnect()
|
||||
.configureLogging(signalR.LogLevel.Warning)
|
||||
.build();
|
||||
|
||||
connection.on('ReceiveNotification', (msg) => {
|
||||
unreadCount.value++; // 全局未读 +1
|
||||
messageHandlers.forEach((h) => { try { h(msg); } catch (e) {} });
|
||||
});
|
||||
|
||||
// 业务状态变更事件(多端同步:某端操作后,其它端刷新已打开的页面)
|
||||
connection.on('BusinessChanged', (e) => {
|
||||
businessChangeHandlers.forEach((h) => { try { h(e); } catch (err) {} });
|
||||
});
|
||||
|
||||
connection.start().catch(() => {});
|
||||
return connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* 订阅业务状态变更事件(用于多端同步刷新)。
|
||||
* @param {Function} handler (e) => void,e 含 businessType / businessId / action
|
||||
*/
|
||||
export function onBusinessChanged(handler) {
|
||||
if (handler) businessChangeHandlers.push(handler);
|
||||
initNotification(); // 确保连接已建立(幂等)
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开消息:标记已读 + 跳转到业务页面(消息自带 routePath 时)。
|
||||
* @param {Object} msg 消息对象(含 id/readStatus/routePath/category)
|
||||
* @param {Object} router vue-router 实例
|
||||
* @returns {boolean} 是否发生了页面跳转
|
||||
*/
|
||||
export async function openMessage(msg, router) {
|
||||
// 待办消息(category=1)点击"去处理"不标记已读,保持未读提醒;
|
||||
// 只有真正完成待办后(后端完成待办时会标记已读)才消失
|
||||
if (msg.readStatus === 0 && msg.category !== 1) {
|
||||
try {
|
||||
await request.put(`/notification/messages/${msg.id}/read`);
|
||||
msg.readStatus = 1;
|
||||
loadUnreadCount();
|
||||
} catch (e) {}
|
||||
}
|
||||
if (msg.routePath) {
|
||||
router.push(msg.routePath);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** 停止连接(登出时调用) */
|
||||
export function stopNotification() {
|
||||
if (connection) {
|
||||
connection.stop().catch(() => {});
|
||||
connection = null;
|
||||
}
|
||||
messageHandlers.length = 0;
|
||||
unreadCount.value = 0;
|
||||
}
|
||||
|
||||
export function getNotificationConnection() {
|
||||
return connection;
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import axios from 'axios';
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
const request = axios.create({
|
||||
baseURL: '/api',
|
||||
timeout: 15000
|
||||
});
|
||||
|
||||
// 令牌存取
|
||||
const TOKEN_KEY = 'access_token';
|
||||
const REFRESH_KEY = 'refresh_token';
|
||||
|
||||
export function getAccessToken() {
|
||||
return localStorage.getItem(TOKEN_KEY);
|
||||
}
|
||||
export function setTokens(accessToken, refreshToken) {
|
||||
localStorage.setItem(TOKEN_KEY, accessToken);
|
||||
localStorage.setItem(REFRESH_KEY, refreshToken);
|
||||
}
|
||||
export function clearTokens() {
|
||||
localStorage.removeItem(TOKEN_KEY);
|
||||
localStorage.removeItem(REFRESH_KEY);
|
||||
}
|
||||
export function getRefreshToken() {
|
||||
return localStorage.getItem(REFRESH_KEY);
|
||||
}
|
||||
|
||||
// 请求拦截:附加 access token
|
||||
request.interceptors.request.use((config) => {
|
||||
const token = getAccessToken();
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
});
|
||||
|
||||
// 刷新令牌(避免并发重复刷新)
|
||||
let isRefreshing = false;
|
||||
let pendingQueue = [];
|
||||
|
||||
async function refreshAccessToken() {
|
||||
const refreshToken = getRefreshToken();
|
||||
if (!refreshToken) return null;
|
||||
try {
|
||||
const res = await axios.post('/api/auth/refresh', { refreshToken });
|
||||
if (res.data?.code === 200) {
|
||||
setTokens(res.data.data.accessToken, res.data.data.refreshToken);
|
||||
return res.data.data.accessToken;
|
||||
}
|
||||
return null;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 响应拦截
|
||||
request.interceptors.response.use(
|
||||
(res) => {
|
||||
const data = res.data;
|
||||
if (data.code === 200) {
|
||||
return data;
|
||||
}
|
||||
ElMessage.error(data.message || '请求失败');
|
||||
return Promise.reject(new Error(data.message || '请求失败'));
|
||||
},
|
||||
async (err) => {
|
||||
const { response, config } = err;
|
||||
|
||||
// 401:尝试用 refresh token 刷新 access token 后重试
|
||||
if (response?.status === 401 && !config._retry && getRefreshToken()) {
|
||||
if (isRefreshing) {
|
||||
// 等待正在进行的刷新完成
|
||||
return new Promise((resolve) => {
|
||||
pendingQueue.push((newToken) => {
|
||||
if (newToken) {
|
||||
config.headers.Authorization = `Bearer ${newToken}`;
|
||||
resolve(request(config));
|
||||
} else {
|
||||
resolve(Promise.reject(err));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
config._retry = true;
|
||||
isRefreshing = true;
|
||||
const newToken = await refreshAccessToken();
|
||||
isRefreshing = false;
|
||||
|
||||
if (newToken) {
|
||||
pendingQueue.forEach((cb) => cb(newToken));
|
||||
pendingQueue = [];
|
||||
config.headers.Authorization = `Bearer ${newToken}`;
|
||||
return request(config);
|
||||
} else {
|
||||
pendingQueue.forEach((cb) => cb(null));
|
||||
pendingQueue = [];
|
||||
clearTokens();
|
||||
localStorage.removeItem('user');
|
||||
ElMessage.error('登录已过期,请重新登录');
|
||||
window.location.href = '/login';
|
||||
return Promise.reject(err);
|
||||
}
|
||||
}
|
||||
|
||||
const msg = response?.data?.message || '请求失败,请稍后重试';
|
||||
ElMessage.error(msg);
|
||||
return Promise.reject(err);
|
||||
}
|
||||
);
|
||||
|
||||
export default request;
|
||||
@@ -0,0 +1,154 @@
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useUserStore } from '../../../store/user';
|
||||
import request from '../../../utils/request';
|
||||
import { onBusinessChanged, loadUnreadCount } from '../../../utils/notification';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
|
||||
const route = useRoute();
|
||||
const userStore = useUserStore();
|
||||
const leaves = ref([]);
|
||||
const loading = ref(false);
|
||||
// 带 leaveId 跳转时默认显示"全部",确保能看到并高亮目标单
|
||||
const statusFilter = ref(route.query.leaveId ? '全部' : '待审批');
|
||||
// 从消息跳转带来的 leaveId,用于高亮定位对应申请单
|
||||
const highlightId = ref(route.query.leaveId || '');
|
||||
// 当前登录用户ID
|
||||
const currentUserId = userStore.userInfo?.id;
|
||||
|
||||
// 当前登录用户是否为该申请单的当前审批人(按序号定位)
|
||||
function isCurrentApprover(row) {
|
||||
if (!currentUserId) return false;
|
||||
const current = row.approvers?.find(a => a.orderNo === row.currentOrderNo);
|
||||
return current && current.approverId === currentUserId;
|
||||
}
|
||||
|
||||
async function load() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await request.get('/attendance/leave', { params: { status: statusFilter.value } });
|
||||
leaves.value = res.data;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function rowClass({ row }) {
|
||||
return highlightId.value && String(row.id) === String(highlightId.value) ? 'row-highlight' : '';
|
||||
}
|
||||
|
||||
async function approve(row, approved) {
|
||||
const action = approved ? '批准' : '驳回';
|
||||
const { value } = await ElMessageBox.prompt(`确认${action}该请假申请?可填写审批意见`, `${action}确认`, {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputPlaceholder: '审批意见(可选)'
|
||||
});
|
||||
try {
|
||||
await request.put(`/attendance/leave/${row.id}/approve`, { approved, remark: value || '' });
|
||||
ElMessage.success(`${action}成功`);
|
||||
load();
|
||||
loadUnreadCount();
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
function statusType(status) {
|
||||
if (status === '已批准') return 'success';
|
||||
if (status === '已驳回') return 'danger';
|
||||
return 'warning';
|
||||
}
|
||||
|
||||
function approverTagType(a, row) {
|
||||
if (a.status === '已批准') return 'success';
|
||||
if (a.status === '已驳回') return 'danger';
|
||||
return a.orderNo === row.currentOrderNo ? 'warning' : 'info';
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
load();
|
||||
// 多端同步:请假审批状态变化时自动刷新本页
|
||||
onBusinessChanged((e) => {
|
||||
if (e.businessType === 'LeaveApproval') load();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-card shadow="never">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>请假审批</span>
|
||||
<el-radio-group v-model="statusFilter" @change="load">
|
||||
<el-radio-button value="待审批">待审批</el-radio-button>
|
||||
<el-radio-button value="已批准">已批准</el-radio-button>
|
||||
<el-radio-button value="已驳回">已驳回</el-radio-button>
|
||||
<el-radio-button value="全部">全部</el-radio-button>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-table v-loading="loading" :data="leaves" border stripe :row-class-name="rowClass">
|
||||
<el-table-column prop="employeeName" label="申请人" width="120" />
|
||||
<el-table-column prop="departmentName" label="部门" width="120" />
|
||||
<el-table-column prop="type" label="类型" width="90" />
|
||||
<el-table-column label="开始时间" width="160">
|
||||
<template #default="{ row }">{{ row.startTime?.slice(0, 16) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="结束时间" width="160">
|
||||
<template #default="{ row }">{{ row.endTime?.slice(0, 16) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="reason" label="事由" min-width="180" />
|
||||
<el-table-column label="审批流程" min-width="240">
|
||||
<template #default="{ row }">
|
||||
<div v-if="row.approvers && row.approvers.length" class="approver-list">
|
||||
<el-tag v-for="a in row.approvers" :key="a.id" size="small" :type="approverTagType(a, row)" effect="light">
|
||||
{{ a.orderNo }}.{{ a.approverName }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<span v-else class="done-text">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="90">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="statusType(row.status)" size="small">{{ row.status }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="160" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<template v-if="row.status === '待审批' && isCurrentApprover(row)">
|
||||
<el-button size="small" type="success" @click="approve(row, true)">批准</el-button>
|
||||
<el-button size="small" type="danger" @click="approve(row, false)">驳回</el-button>
|
||||
</template>
|
||||
<span v-else-if="row.status === '待审批'" class="done-text">等待他人审批</span>
|
||||
<span v-else class="done-text">已处理</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.done-text {
|
||||
color: #999;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.approver-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
:deep(.row-highlight) {
|
||||
background: #fdf6ec;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,141 @@
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue';
|
||||
import request from '../../../utils/request';
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
const leaves = ref([]);
|
||||
const employees = ref([]);
|
||||
const loading = ref(false);
|
||||
const dialogVisible = ref(false);
|
||||
|
||||
const form = reactive({
|
||||
type: '事假',
|
||||
startTime: '',
|
||||
endTime: '',
|
||||
reason: '',
|
||||
approverIds: []
|
||||
});
|
||||
|
||||
const leaveTypes = ['事假', '病假', '年假', '调休', '婚假', '产假', '陪产假'];
|
||||
|
||||
async function load() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await request.get('/attendance/leave/my');
|
||||
leaves.value = res.data;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 可选审批人(所有在职员工)
|
||||
async function loadEmployees() {
|
||||
try {
|
||||
const res = await request.get('/sys/user', { params: { page: 1, pageSize: 1000 } });
|
||||
employees.value = res.data.items || [];
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
if (!form.startTime || !form.endTime) {
|
||||
ElMessage.warning('请选择请假起止时间');
|
||||
return;
|
||||
}
|
||||
if (!form.approverIds.length) {
|
||||
ElMessage.warning('请选择审批人');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await request.post('/attendance/leave', form);
|
||||
ElMessage.success('申请已提交');
|
||||
dialogVisible.value = false;
|
||||
form.reason = '';
|
||||
form.approverIds = [];
|
||||
load();
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
function statusType(status) {
|
||||
if (status === '已批准') return 'success';
|
||||
if (status === '已驳回') return 'danger';
|
||||
return 'warning';
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
load();
|
||||
loadEmployees();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-card shadow="never">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>请假申请</span>
|
||||
<el-button type="primary" @click="dialogVisible = true">申请请假</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-table v-loading="loading" :data="leaves" border stripe>
|
||||
<el-table-column prop="type" label="类型" width="100" />
|
||||
<el-table-column label="开始时间" width="170">
|
||||
<template #default="{ row }">{{ row.startTime?.slice(0, 16) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="结束时间" width="170">
|
||||
<template #default="{ row }">{{ row.endTime?.slice(0, 16) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="reason" label="事由" min-width="200" />
|
||||
<el-table-column label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="statusType(row.status)" size="small">{{ row.status }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="approverName" label="审批人" width="120" />
|
||||
<el-table-column label="审批意见" min-width="150">
|
||||
<template #default="{ row }">{{ row.approveRemark || '-' }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-dialog v-model="dialogVisible" title="申请请假" width="560px">
|
||||
<el-form :model="form" label-width="90px">
|
||||
<el-form-item label="请假类型">
|
||||
<el-select v-model="form.type" style="width: 100%">
|
||||
<el-option v-for="t in leaveTypes" :key="t" :label="t" :value="t" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="开始时间">
|
||||
<el-date-picker v-model="form.startTime" type="datetime" placeholder="选择开始时间" style="width: 100%" value-format="YYYY-MM-DDTHH:mm:ss" />
|
||||
</el-form-item>
|
||||
<el-form-item label="结束时间">
|
||||
<el-date-picker v-model="form.endTime" type="datetime" placeholder="选择结束时间" style="width: 100%" value-format="YYYY-MM-DDTHH:mm:ss" />
|
||||
</el-form-item>
|
||||
<el-form-item label="请假事由">
|
||||
<el-input v-model="form.reason" type="textarea" :rows="3" placeholder="请填写请假事由" />
|
||||
</el-form-item>
|
||||
<el-form-item label="审批人">
|
||||
<el-select v-model="form.approverIds" multiple filterable placeholder="搜索并选择审批人" style="width: 100%">
|
||||
<el-option v-for="e in employees" :key="e.id" :label="`${e.userName}(${e.departmentName || '无部门'})`" :value="e.id" />
|
||||
</el-select>
|
||||
<div class="form-tip">任意选人、可多选;选择顺序即审批顺序(序号 1、2、3…)</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="submit">提交申请</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.form-tip {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
line-height: 1.4;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,158 @@
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import request from '../../../utils/request';
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
const records = ref([]);
|
||||
const total = ref(0);
|
||||
const summary = ref(null);
|
||||
const loading = ref(false);
|
||||
const pageIndex = ref(1);
|
||||
const pageSize = ref(10);
|
||||
const month = ref(new Date().toISOString().slice(0, 7));
|
||||
|
||||
async function loadRecords() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await request.get('/attendance/my', { params: { pageIndex: pageIndex.value, pageSize: pageSize.value } });
|
||||
records.value = res.data.items;
|
||||
total.value = res.data.total;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSummary() {
|
||||
try {
|
||||
const res = await request.get('/attendance/my/summary', { params: { month: month.value } });
|
||||
summary.value = res.data;
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
function statusType(status) {
|
||||
if (status === '正常') return 'success';
|
||||
if (status === '迟到' || status === '早退') return 'warning';
|
||||
return 'danger';
|
||||
}
|
||||
|
||||
function handlePageChange(page) {
|
||||
pageIndex.value = page;
|
||||
loadRecords();
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadRecords();
|
||||
loadSummary();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<!-- 统计卡片 -->
|
||||
<el-card shadow="never" class="summary-card" v-if="summary">
|
||||
<div class="summary-title">
|
||||
<span>考勤统计({{ month }})</span>
|
||||
<el-date-picker
|
||||
v-model="month"
|
||||
type="month"
|
||||
placeholder="选择月份"
|
||||
value-format="YYYY-MM"
|
||||
@change="loadSummary"
|
||||
/>
|
||||
</div>
|
||||
<div class="summary-grid">
|
||||
<div class="stat-box"><div class="num normal">{{ summary.normalDays }}</div><div class="label">正常出勤</div></div>
|
||||
<div class="stat-box"><div class="num late">{{ summary.lateDays }}</div><div class="label">迟到</div></div>
|
||||
<div class="stat-box"><div class="num early">{{ summary.earlyLeaveDays }}</div><div class="label">早退</div></div>
|
||||
<div class="stat-box"><div class="num absent">{{ summary.absentDays }}</div><div class="label">缺勤</div></div>
|
||||
<div class="stat-box"><div class="num leave">{{ summary.leaveDays }}</div><div class="label">请假</div></div>
|
||||
<div class="stat-box"><div class="num">{{ summary.totalDays }}</div><div class="label">总天数</div></div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 记录列表 -->
|
||||
<el-card shadow="never">
|
||||
<template #header>
|
||||
<span>我的考勤记录</span>
|
||||
</template>
|
||||
<el-table v-loading="loading" :data="records" border stripe>
|
||||
<el-table-column prop="workDate" label="日期" width="120">
|
||||
<template #default="{ row }">{{ row.workDate?.slice(0, 10) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="签到时间" width="120">
|
||||
<template #default="{ row }">{{ row.signInTime ? row.signInTime.slice(11, 19) : '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="签到状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="statusType(row.signInStatus)" size="small">{{ row.signInStatus || '-' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="签退时间" width="120">
|
||||
<template #default="{ row }">{{ row.signOutTime ? row.signOutTime.slice(11, 19) : '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="签退状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="statusType(row.signOutStatus)" size="small">{{ row.signOutStatus || '-' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination
|
||||
class="pagination"
|
||||
:current-page="pageIndex"
|
||||
:page-size="pageSize"
|
||||
:total="total"
|
||||
layout="total, prev, pager, next"
|
||||
@current-change="handlePageChange"
|
||||
/>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.summary-card {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.summary-title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.summary-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(6, 1fr);
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.stat-box {
|
||||
text-align: center;
|
||||
padding: 16px;
|
||||
background: #f8faf9;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.stat-box .num {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.stat-box .num.normal { color: #16a34a; }
|
||||
.stat-box .num.late { color: #f59e0b; }
|
||||
.stat-box .num.early { color: #f59e0b; }
|
||||
.stat-box .num.absent { color: #ef4444; }
|
||||
.stat-box .num.leave { color: #3b82f6; }
|
||||
|
||||
.stat-box .label {
|
||||
font-size: 13px;
|
||||
color: #999;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
margin-top: 16px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,207 @@
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import request from '../../../utils/request';
|
||||
import { useUserStore } from '../../../store/user';
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
const userStore = useUserStore();
|
||||
const user = userStore.userInfo || {};
|
||||
|
||||
const now = ref(new Date());
|
||||
const today = ref('');
|
||||
const punchResult = ref(null);
|
||||
const todayRecord = ref(null);
|
||||
let timer = null;
|
||||
|
||||
function formatDate(d) {
|
||||
const y = d.getFullYear();
|
||||
const m = String(d.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(d.getDate()).padStart(2, '0');
|
||||
return `${y}-${m}-${day}`;
|
||||
}
|
||||
|
||||
function formatTime(d) {
|
||||
return d.toTimeString().slice(0, 8);
|
||||
}
|
||||
|
||||
async function loadToday() {
|
||||
try {
|
||||
const res = await request.get('/attendance/my', { params: { pageIndex: 1, pageSize: 1 } });
|
||||
const list = res.data.items;
|
||||
todayRecord.value = list.find((r) => formatDate(new Date(r.workDate)) === today.value) || null;
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
async function punch() {
|
||||
try {
|
||||
const res = await request.post('/attendance/punch');
|
||||
punchResult.value = res.data;
|
||||
ElMessage.success(res.data.message);
|
||||
await loadToday();
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
now.value = new Date();
|
||||
today.value = formatDate(now.value);
|
||||
loadToday();
|
||||
timer = setInterval(() => {
|
||||
now.value = new Date();
|
||||
}, 1000);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<el-card shadow="never" class="punch-card">
|
||||
<div class="punch-header">
|
||||
<div class="time-display">{{ formatTime(now) }}</div>
|
||||
<div class="date-display">{{ today }} · 星期{{ ['日','一','二','三','四','五','六'][now.getDay()] }}</div>
|
||||
</div>
|
||||
|
||||
<div class="punch-info">
|
||||
<div class="user-avatar">{{ (user.nickname || user.realName || 'U').charAt(0) }}</div>
|
||||
<div class="user-text">
|
||||
<div class="user-name">{{ user.nickname || user.realName }}</div>
|
||||
<div class="user-dept">{{ user.departmentName || '未分配部门' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="punch-status">
|
||||
<div class="status-item">
|
||||
<span class="label">上班签到</span>
|
||||
<span class="value" :class="{ done: todayRecord?.signInTime }">
|
||||
{{ todayRecord?.signInTime ? todayRecord.signInTime.slice(11, 19) : '未签到' }}
|
||||
</span>
|
||||
<el-tag v-if="todayRecord" :type="todayRecord.signInStatus === '正常' ? 'success' : 'warning'" size="small">
|
||||
{{ todayRecord.signInStatus }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="status-item">
|
||||
<span class="label">下班签退</span>
|
||||
<span class="value" :class="{ done: todayRecord?.signOutTime }">
|
||||
{{ todayRecord?.signOutTime ? todayRecord.signOutTime.slice(11, 19) : '未签退' }}
|
||||
</span>
|
||||
<el-tag v-if="todayRecord?.signOutTime" :type="todayRecord.signOutStatus === '正常' ? 'success' : 'warning'" size="small">
|
||||
{{ todayRecord.signOutStatus }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-button
|
||||
type="primary"
|
||||
size="large"
|
||||
class="punch-btn"
|
||||
:disabled="todayRecord?.signOutTime"
|
||||
@click="punch"
|
||||
>
|
||||
{{ !todayRecord ? '上班签到' : !todayRecord.signOutTime ? '下班签退' : '今日已完成' }}
|
||||
</el-button>
|
||||
|
||||
<div v-if="punchResult" class="punch-result">
|
||||
{{ punchResult.message }}
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.punch-card {
|
||||
max-width: 500px;
|
||||
margin: 0 auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.punch-header {
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.time-display {
|
||||
font-size: 48px;
|
||||
font-weight: 700;
|
||||
color: #16a34a;
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
|
||||
.date-display {
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.punch-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
background: #16a34a;
|
||||
color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
font-weight: 600;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.user-dept {
|
||||
font-size: 13px;
|
||||
color: #999;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.punch-status {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
margin: 20px 0;
|
||||
padding: 16px;
|
||||
background: #f8faf9;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.status-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.status-item .label {
|
||||
font-size: 13px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.status-item .value {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.status-item .value.done {
|
||||
color: #16a34a;
|
||||
}
|
||||
|
||||
.punch-btn {
|
||||
width: 200px;
|
||||
height: 50px;
|
||||
font-size: 16px;
|
||||
border-radius: 25px;
|
||||
}
|
||||
|
||||
.punch-result {
|
||||
margin-top: 16px;
|
||||
color: #16a34a;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,131 @@
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue';
|
||||
import request from '../../../utils/request';
|
||||
import { buildDeptTree } from '../../../utils/dept';
|
||||
|
||||
const records = ref([]);
|
||||
const total = ref(0);
|
||||
const loading = ref(false);
|
||||
const deptTree = ref([]);
|
||||
|
||||
const query = reactive({
|
||||
date: '',
|
||||
departmentId: null,
|
||||
keyword: '',
|
||||
pageIndex: 1,
|
||||
pageSize: 10
|
||||
});
|
||||
|
||||
async function load() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await request.get('/attendance', { params: query });
|
||||
records.value = res.data.items;
|
||||
total.value = res.data.total;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadDepts() {
|
||||
const res = await request.get('/sys/department');
|
||||
deptTree.value = buildDeptTree(res.data);
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
query.pageIndex = 1;
|
||||
load();
|
||||
}
|
||||
|
||||
function resetSearch() {
|
||||
Object.assign(query, { date: '', departmentId: null, keyword: '', pageIndex: 1, pageSize: 10 });
|
||||
load();
|
||||
}
|
||||
|
||||
function handlePageChange(page) {
|
||||
query.pageIndex = page;
|
||||
load();
|
||||
}
|
||||
|
||||
function statusType(status) {
|
||||
if (status === '正常') return 'success';
|
||||
if (status === '迟到' || status === '早退') return 'warning';
|
||||
return 'danger';
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
load();
|
||||
loadDepts();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-card shadow="never">
|
||||
<template #header>
|
||||
<span>考勤记录</span>
|
||||
</template>
|
||||
|
||||
<div class="search-bar">
|
||||
<el-date-picker v-model="query.date" type="date" placeholder="选择日期" value-format="YYYY-MM-DD" style="width: 180px" />
|
||||
<el-tree-select
|
||||
v-model="query.departmentId"
|
||||
:data="deptTree"
|
||||
:props="{ label: 'name', value: 'id', children: 'children' }"
|
||||
check-strictly
|
||||
clearable
|
||||
placeholder="选择部门"
|
||||
style="width: 180px"
|
||||
/>
|
||||
<el-input v-model="query.keyword" placeholder="姓名/账号" clearable style="width: 180px" @keyup.enter="handleSearch" />
|
||||
<el-button type="primary" @click="handleSearch">搜索</el-button>
|
||||
<el-button @click="resetSearch">重置</el-button>
|
||||
</div>
|
||||
|
||||
<el-table v-loading="loading" :data="records" border stripe>
|
||||
<el-table-column prop="employeeName" label="姓名" width="120" />
|
||||
<el-table-column prop="departmentName" label="部门" width="120" />
|
||||
<el-table-column label="日期" width="120">
|
||||
<template #default="{ row }">{{ row.workDate?.slice(0, 10) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="签到" width="110">
|
||||
<template #default="{ row }">{{ row.signInTime ? row.signInTime.slice(11, 19) : '缺卡' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="签到状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="statusType(row.signInStatus)" size="small">{{ row.signInStatus || '-' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="签退" width="110">
|
||||
<template #default="{ row }">{{ row.signOutTime ? row.signOutTime.slice(11, 19) : '缺卡' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="签退状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="statusType(row.signOutStatus)" size="small">{{ row.signOutStatus || '-' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-pagination
|
||||
class="pagination"
|
||||
:current-page="query.pageIndex"
|
||||
:page-size="query.pageSize"
|
||||
:total="total"
|
||||
layout="total, prev, pager, next"
|
||||
@current-change="handlePageChange"
|
||||
/>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.search-bar {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
margin-top: 16px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,165 @@
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue';
|
||||
import request from '../../../utils/request';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
|
||||
const list = ref([]);
|
||||
const total = ref(0);
|
||||
const loading = ref(false);
|
||||
const guigeOptions = ref([]);
|
||||
const query = reactive({ pageIndex: 1, pageSize: 20, keyword: '', standard: null, guige: '' });
|
||||
|
||||
const dialogVisible = ref(false);
|
||||
const dialogTitle = ref('新增指标');
|
||||
const isEdit = ref(false);
|
||||
const editId = ref(null);
|
||||
const form = reactive({
|
||||
standard: 0, guige: '', grade: '', gradeScore: 0, piancha: 0, dacha: 0, erdu: 0, sandu: 0, qingjie: 0, jiejing: 0
|
||||
});
|
||||
|
||||
const standardOptions = [
|
||||
{ label: '厂标', value: 0 },
|
||||
{ label: '国标', value: 1 },
|
||||
{ label: '个人', value: 2 }
|
||||
];
|
||||
|
||||
async function load() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await request.get('/shengsidingjizhibiao', { params: query });
|
||||
list.value = res.data.items;
|
||||
total.value = res.data.total;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadGuige() {
|
||||
const res = await request.get('/shengsiguige/all');
|
||||
guigeOptions.value = res.data;
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
isEdit.value = false;
|
||||
dialogTitle.value = '新增指标';
|
||||
Object.assign(form, { standard: 0, guige: guigeOptions.value[0] || '', grade: '', gradeScore: 0, piancha: 0, dacha: 0, erdu: 0, sandu: 0, qingjie: 0, jiejing: 0 });
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
function openEdit(row) {
|
||||
isEdit.value = true;
|
||||
editId.value = row.id;
|
||||
dialogTitle.value = '编辑指标';
|
||||
const { id, standardName, ...rest } = row;
|
||||
Object.assign(form, rest);
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
async function save() {
|
||||
if (!form.grade) { ElMessage.warning('请输入等级'); return; }
|
||||
try {
|
||||
if (isEdit.value) {
|
||||
await request.put(`/shengsidingjizhibiao/${editId.value}`, form);
|
||||
ElMessage.success('更新成功');
|
||||
} else {
|
||||
await request.post('/shengsidingjizhibiao', form);
|
||||
ElMessage.success('创建成功');
|
||||
}
|
||||
dialogVisible.value = false;
|
||||
load();
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
async function del(row) {
|
||||
await ElMessageBox.confirm(`确定删除等级「${row.grade}」吗?`, '提示', { type: 'warning' });
|
||||
try {
|
||||
await request.delete(`/shengsidingjizhibiao/${row.id}`);
|
||||
ElMessage.success('删除成功');
|
||||
load();
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
function handlePageChange(p) { query.pageIndex = p; load(); }
|
||||
onMounted(() => { load(); loadGuige(); });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-card shadow="never">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>生丝定级指标</span>
|
||||
<el-button type="primary" @click="openCreate">新增指标</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="search-bar">
|
||||
<el-select v-model="query.standard" clearable placeholder="标准" style="width: 120px">
|
||||
<el-option v-for="s in standardOptions" :key="s.value" :label="s.label" :value="s.value" />
|
||||
</el-select>
|
||||
<el-select v-model="query.guige" clearable placeholder="规格" style="width: 140px">
|
||||
<el-option v-for="g in guigeOptions" :key="g" :label="g" :value="g" />
|
||||
</el-select>
|
||||
<el-input v-model="query.keyword" placeholder="等级" clearable style="width: 140px" @keyup.enter="query.pageIndex=1;load()" />
|
||||
<el-button type="primary" @click="query.pageIndex=1;load()">搜索</el-button>
|
||||
<el-button @click="Object.assign(query,{keyword:'',standard:null,guige:'',pageIndex:1});load()">重置</el-button>
|
||||
</div>
|
||||
|
||||
<el-table v-loading="loading" :data="list" border stripe>
|
||||
<el-table-column label="标准" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small">{{ row.standardName }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="guige" label="规格" width="90" />
|
||||
<el-table-column prop="grade" label="等级" width="90" />
|
||||
<el-table-column prop="gradeScore" label="等级分值" width="90" />
|
||||
<el-table-column prop="piancha" label="偏差" width="80" />
|
||||
<el-table-column prop="dacha" label="大差" width="80" />
|
||||
<el-table-column prop="erdu" label="二度变化" width="90" />
|
||||
<el-table-column prop="sandu" label="三度变化" width="90" />
|
||||
<el-table-column prop="qingjie" label="清洁" width="80" />
|
||||
<el-table-column prop="jiejing" label="洁净" width="80" />
|
||||
<el-table-column label="操作" width="140" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" type="primary" @click="openEdit(row)">编辑</el-button>
|
||||
<el-button size="small" type="danger" @click="del(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-pagination class="pagination" :current-page="query.pageIndex" :page-size="query.pageSize" :total="total" layout="total, prev, pager, next" @current-change="handlePageChange" />
|
||||
|
||||
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="560px">
|
||||
<el-form :model="form" label-width="100px">
|
||||
<el-form-item label="标准">
|
||||
<el-radio-group v-model="form.standard">
|
||||
<el-radio-button v-for="s in standardOptions" :key="s.value" :value="s.value">{{ s.label }}</el-radio-button>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="规格">
|
||||
<el-select v-model="form.guige" style="width: 100%">
|
||||
<el-option v-for="g in guigeOptions" :key="g" :label="g" :value="g" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="等级"><el-input v-model="form.grade" /></el-form-item>
|
||||
<el-form-item label="等级分值"><el-input-number v-model="form.gradeScore" :precision="1" /></el-form-item>
|
||||
<el-form-item label="偏差"><el-input-number v-model="form.piancha" :precision="2" /></el-form-item>
|
||||
<el-form-item label="大差"><el-input-number v-model="form.dacha" :precision="2" /></el-form-item>
|
||||
<el-form-item label="二度变化"><el-input-number v-model="form.erdu" :precision="1" /></el-form-item>
|
||||
<el-form-item label="三度变化"><el-input-number v-model="form.sandu" :precision="1" /></el-form-item>
|
||||
<el-form-item label="清洁"><el-input-number v-model="form.qingjie" :precision="1" /></el-form-item>
|
||||
<el-form-item label="洁净"><el-input-number v-model="form.jiejing" :precision="1" /></el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="save">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.card-header { display: flex; justify-content: space-between; align-items: center; }
|
||||
.search-bar { display: flex; gap: 12px; margin-bottom: 16px; flex-wrap: wrap; }
|
||||
.pagination { margin-top: 16px; justify-content: flex-end; }
|
||||
</style>
|
||||
@@ -0,0 +1,177 @@
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue';
|
||||
import request from '../../../utils/request';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
|
||||
const list = ref([]);
|
||||
const total = ref(0);
|
||||
const loading = ref(false);
|
||||
const query = reactive({ pageIndex: 1, pageSize: 20, keyword: '' });
|
||||
|
||||
const dialogVisible = ref(false);
|
||||
const dialogTitle = ref('新增规格');
|
||||
const isEdit = ref(false);
|
||||
const editId = ref(null);
|
||||
|
||||
const emptyForm = () => ({
|
||||
guige: '', centerrangeLolimit: 0, centerrangeUplimit: 0, centerXiandu: 0,
|
||||
validLolimit: 0, validUplimit: 0, xiaoyeLolimit: 0, xiaoyeUplimit: 0,
|
||||
dayeLolimit: 0, dayeUplimit: 0, teyeLolimit: 0, teyeUplimit: 0,
|
||||
yexianNkLolimitLeft: 0, yexianNkLolimitRight: 0, yexianNkUplimitLeft: 0, yexianNkUplimitRight: 0,
|
||||
laodongJingsaiYexianLolimit: 0, laodongJingsaiYexianUplimit: 0, sort: 0, memo: ''
|
||||
});
|
||||
const form = reactive(emptyForm());
|
||||
|
||||
async function load() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await request.get('/shengsiguige', { params: query });
|
||||
list.value = res.data.items;
|
||||
total.value = res.data.total;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
isEdit.value = false;
|
||||
dialogTitle.value = '新增规格';
|
||||
Object.assign(form, emptyForm());
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
function openEdit(row) {
|
||||
isEdit.value = true;
|
||||
editId.value = row.id;
|
||||
dialogTitle.value = '编辑规格';
|
||||
const { id, ...rest } = row;
|
||||
Object.assign(form, rest);
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
async function save() {
|
||||
if (!form.guige) { ElMessage.warning('请输入规格'); return; }
|
||||
try {
|
||||
if (isEdit.value) {
|
||||
await request.put(`/shengsiguige/${editId.value}`, form);
|
||||
ElMessage.success('更新成功');
|
||||
} else {
|
||||
await request.post('/shengsiguige', form);
|
||||
ElMessage.success('创建成功');
|
||||
}
|
||||
dialogVisible.value = false;
|
||||
load();
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
async function del(row) {
|
||||
await ElMessageBox.confirm(`确定删除规格「${row.guige}」吗?`, '提示', { type: 'warning' });
|
||||
try {
|
||||
await request.delete(`/shengsiguige/${row.id}`);
|
||||
ElMessage.success('删除成功');
|
||||
load();
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
function handlePageChange(p) { query.pageIndex = p; load(); }
|
||||
onMounted(load);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-card shadow="never">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>生丝规格</span>
|
||||
<el-button type="primary" @click="openCreate">新增规格</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="search-bar">
|
||||
<el-input v-model="query.keyword" placeholder="规格" clearable style="width: 200px" @keyup.enter="query.pageIndex=1;load()" />
|
||||
<el-button type="primary" @click="query.pageIndex=1;load()">搜索</el-button>
|
||||
</div>
|
||||
|
||||
<el-table v-loading="loading" :data="list" border stripe>
|
||||
<el-table-column prop="guige" label="规格" width="90" fixed />
|
||||
<el-table-column prop="centerrangeLolimit" label="中心下限" width="90" />
|
||||
<el-table-column prop="centerrangeUplimit" label="中心上限" width="90" />
|
||||
<el-table-column prop="centerXiandu" label="中心纤度" width="90" />
|
||||
<el-table-column prop="validLolimit" label="有效下限" width="90" />
|
||||
<el-table-column prop="validUplimit" label="有效上限" width="90" />
|
||||
<el-table-column prop="xiaoyeLolimit" label="小野下限" width="90" />
|
||||
<el-table-column prop="xiaoyeUplimit" label="小野上限" width="90" />
|
||||
<el-table-column prop="dayeLolimit" label="大野下限" width="90" />
|
||||
<el-table-column prop="dayeUplimit" label="大野上限" width="90" />
|
||||
<el-table-column prop="teyeLolimit" label="特野下限" width="90" />
|
||||
<el-table-column prop="teyeUplimit" label="特野上限" width="90" />
|
||||
<el-table-column prop="laodongJingsaiYexianLolimit" label="野纤下限" width="90" />
|
||||
<el-table-column prop="laodongJingsaiYexianUplimit" label="野纤上限" width="90" />
|
||||
<el-table-column prop="sort" label="顺序号" width="80" />
|
||||
<el-table-column label="操作" width="140" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" type="primary" @click="openEdit(row)">编辑</el-button>
|
||||
<el-button size="small" type="danger" @click="del(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-pagination class="pagination" :current-page="query.pageIndex" :page-size="query.pageSize" :total="total" layout="total, prev, pager, next" @current-change="handlePageChange" />
|
||||
|
||||
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="700px">
|
||||
<el-form :model="form" label-width="130px">
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="8"><el-form-item label="规格"><el-input v-model="form.guige" /></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="顺序号"><el-input-number v-model="form.sort" :min="0" /></el-form-item></el-col>
|
||||
</el-row>
|
||||
<el-divider content-position="left">中心范围</el-divider>
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="8"><el-form-item label="中心下限"><el-input-number v-model="form.centerrangeLolimit" :precision="1" /></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="中心上限"><el-input-number v-model="form.centerrangeUplimit" :precision="1" /></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="中心纤度"><el-input-number v-model="form.centerXiandu" :precision="1" /></el-form-item></el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="8"><el-form-item label="有效下限"><el-input-number v-model="form.validLolimit" :precision="1" /></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="有效上限"><el-input-number v-model="form.validUplimit" :precision="1" /></el-form-item></el-col>
|
||||
</el-row>
|
||||
<el-divider content-position="left">野纤指标</el-divider>
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="8"><el-form-item label="小野下限"><el-input-number v-model="form.xiaoyeLolimit" :precision="1" /></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="小野上限"><el-input-number v-model="form.xiaoyeUplimit" :precision="1" /></el-form-item></el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="8"><el-form-item label="大野下限"><el-input-number v-model="form.dayeLolimit" :precision="1" /></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="大野上限"><el-input-number v-model="form.dayeUplimit" :precision="1" /></el-form-item></el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="8"><el-form-item label="特野下限"><el-input-number v-model="form.teyeLolimit" :precision="1" /></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="特野上限"><el-input-number v-model="form.teyeUplimit" :precision="1" /></el-form-item></el-col>
|
||||
</el-row>
|
||||
<el-divider content-position="left">内控指标</el-divider>
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="8"><el-form-item label="内控左下"><el-input-number v-model="form.yexianNkLolimitLeft" :precision="1" /></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="内控左上"><el-input-number v-model="form.yexianNkLolimitRight" :precision="1" /></el-form-item></el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="8"><el-form-item label="内控右下"><el-input-number v-model="form.yexianNkUplimitLeft" :precision="1" /></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="内控右上"><el-input-number v-model="form.yexianNkUplimitRight" :precision="1" /></el-form-item></el-col>
|
||||
</el-row>
|
||||
<el-divider content-position="left">劳动竞赛</el-divider>
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="8"><el-form-item label="野纤下限"><el-input-number v-model="form.laodongJingsaiYexianLolimit" :precision="1" /></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="野纤上限"><el-input-number v-model="form.laodongJingsaiYexianUplimit" :precision="1" /></el-form-item></el-col>
|
||||
</el-row>
|
||||
<el-form-item label="备注"><el-input v-model="form.memo" type="textarea" :rows="2" /></el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="save">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.card-header { display: flex; justify-content: space-between; align-items: center; }
|
||||
.search-bar { display: flex; gap: 12px; margin-bottom: 16px; }
|
||||
.pagination { margin-top: 16px; justify-content: flex-end; }
|
||||
</style>
|
||||
@@ -0,0 +1,135 @@
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue';
|
||||
import request from '../../../utils/request';
|
||||
import { buildDeptTree } from '../../../utils/dept';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
|
||||
const list = ref([]);
|
||||
const total = ref(0);
|
||||
const loading = ref(false);
|
||||
const deptTree = ref([]);
|
||||
const query = reactive({ pageIndex: 1, pageSize: 20, keyword: '' });
|
||||
|
||||
const dialogVisible = ref(false);
|
||||
const dialogTitle = ref('新增机组车台');
|
||||
const isEdit = ref(false);
|
||||
const editId = ref(null);
|
||||
const form = reactive({ departmentId: null, jizu: 0, chetaiStart: 0, chetaiEnd: 0 });
|
||||
|
||||
async function load() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await request.get('/jizuchetai', { params: query });
|
||||
list.value = res.data.items;
|
||||
total.value = res.data.total;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadDepts() {
|
||||
const res = await request.get('/sys/department');
|
||||
deptTree.value = buildDeptTree(res.data);
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
isEdit.value = false;
|
||||
dialogTitle.value = '新增机组车台';
|
||||
Object.assign(form, { departmentId: null, jizu: 0, chetaiStart: 0, chetaiEnd: 0 });
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
function openEdit(row) {
|
||||
isEdit.value = true;
|
||||
editId.value = row.id;
|
||||
dialogTitle.value = '编辑机组车台';
|
||||
Object.assign(form, { departmentId: row.departmentId, jizu: row.jizu, chetaiStart: row.chetaiStart, chetaiEnd: row.chetaiEnd });
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
async function save() {
|
||||
if (!form.departmentId) { ElMessage.warning('请选择车间'); return; }
|
||||
try {
|
||||
if (isEdit.value) {
|
||||
await request.put(`/jizuchetai/${editId.value}`, form);
|
||||
ElMessage.success('更新成功');
|
||||
} else {
|
||||
await request.post('/jizuchetai', form);
|
||||
ElMessage.success('创建成功');
|
||||
}
|
||||
dialogVisible.value = false;
|
||||
load();
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
async function del(row) {
|
||||
await ElMessageBox.confirm(`确定删除机组「${row.jizu}」吗?`, '提示', { type: 'warning' });
|
||||
try {
|
||||
await request.delete(`/jizuchetai/${row.id}`);
|
||||
ElMessage.success('删除成功');
|
||||
load();
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
function handlePageChange(p) { query.pageIndex = p; load(); }
|
||||
onMounted(() => { load(); loadDepts(); });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-card shadow="never">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>机组车台</span>
|
||||
<el-button type="primary" @click="openCreate">新增机组车台</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="search-bar">
|
||||
<el-input v-model="query.keyword" placeholder="车间/机组号" clearable style="width: 200px" @keyup.enter="query.pageIndex=1;load()" />
|
||||
<el-button type="primary" @click="query.pageIndex=1;load()">搜索</el-button>
|
||||
</div>
|
||||
|
||||
<el-table v-loading="loading" :data="list" border stripe>
|
||||
<el-table-column prop="departmentName" label="车间" min-width="200" />
|
||||
<el-table-column prop="jizu" label="机组号" width="120" />
|
||||
<el-table-column prop="chetaiStart" label="开始车台号" width="120" />
|
||||
<el-table-column prop="chetaiEnd" label="结束车台号" width="120" />
|
||||
<el-table-column label="操作" width="140" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" type="primary" @click="openEdit(row)">编辑</el-button>
|
||||
<el-button size="small" type="danger" @click="del(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-pagination class="pagination" :current-page="query.pageIndex" :page-size="query.pageSize" :total="total" layout="total, prev, pager, next" @current-change="handlePageChange" />
|
||||
|
||||
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="480px">
|
||||
<el-form :model="form" label-width="100px">
|
||||
<el-form-item label="车间">
|
||||
<el-tree-select
|
||||
v-model="form.departmentId"
|
||||
:data="deptTree"
|
||||
:props="{ label: 'name', value: 'id', children: 'children' }"
|
||||
check-strictly
|
||||
placeholder="选择车间"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="机组号"><el-input-number v-model="form.jizu" :min="0" /></el-form-item>
|
||||
<el-form-item label="开始车台号"><el-input-number v-model="form.chetaiStart" :min="0" /></el-form-item>
|
||||
<el-form-item label="结束车台号"><el-input-number v-model="form.chetaiEnd" :min="0" /></el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="save">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.card-header { display: flex; justify-content: space-between; align-items: center; }
|
||||
.search-bar { display: flex; gap: 12px; margin-bottom: 16px; }
|
||||
.pagination { margin-top: 16px; justify-content: flex-end; }
|
||||
</style>
|
||||
@@ -0,0 +1,164 @@
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue';
|
||||
import request from '../../../utils/request';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
|
||||
const list = ref([]);
|
||||
const total = ref(0);
|
||||
const loading = ref(false);
|
||||
const query = reactive({ pageIndex: 1, pageSize: 20, keyword: '', year: null });
|
||||
|
||||
const dialogVisible = ref(false);
|
||||
const dialogTitle = ref('新增庄口');
|
||||
const isEdit = ref(false);
|
||||
const editId = ref(null);
|
||||
const form = reactive({
|
||||
belongingYear: new Date().getFullYear(),
|
||||
bendiWaidi: 1,
|
||||
jidiWaigou: '基地',
|
||||
zhuangKouCode: '',
|
||||
zhuangKouName: '',
|
||||
productionDisplayName: '',
|
||||
chandi: '',
|
||||
sort: 0,
|
||||
memo: '',
|
||||
isJiezhuang: '0'
|
||||
});
|
||||
|
||||
async function load() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await request.get('/craftzhuangkou', { params: query });
|
||||
list.value = res.data.items;
|
||||
total.value = res.data.total;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
isEdit.value = false;
|
||||
dialogTitle.value = '新增庄口';
|
||||
Object.assign(form, { belongingYear: new Date().getFullYear(), bendiWaidi: 1, jidiWaigou: '基地', zhuangKouCode: '', zhuangKouName: '', productionDisplayName: '', chandi: '', sort: 0, memo: '', isJiezhuang: '0' });
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
function openEdit(row) {
|
||||
isEdit.value = true;
|
||||
editId.value = row.id;
|
||||
dialogTitle.value = '编辑庄口';
|
||||
Object.assign(form, {
|
||||
belongingYear: row.belongingYear, bendiWaidi: row.bendiWaidi, jidiWaigou: row.jidiWaigou,
|
||||
zhuangKouCode: row.zhuangKouCode, zhuangKouName: row.zhuangKouName,
|
||||
productionDisplayName: row.productionDisplayName, chandi: row.chandi,
|
||||
sort: row.sort, memo: row.memo, isJiezhuang: row.isJiezhuang
|
||||
});
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
async function save() {
|
||||
if (!form.zhuangKouName) { ElMessage.warning('请输入庄口名称'); return; }
|
||||
try {
|
||||
if (isEdit.value) {
|
||||
await request.put(`/craftzhuangkou/${editId.value}`, form);
|
||||
ElMessage.success('更新成功');
|
||||
} else {
|
||||
await request.post('/craftzhuangkou', form);
|
||||
ElMessage.success('创建成功');
|
||||
}
|
||||
dialogVisible.value = false;
|
||||
load();
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
async function del(row) {
|
||||
await ElMessageBox.confirm(`确定删除庄口「${row.zhuangKouName}」吗?`, '提示', { type: 'warning' });
|
||||
try {
|
||||
await request.delete(`/craftzhuangkou/${row.id}`);
|
||||
ElMessage.success('删除成功');
|
||||
load();
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
function handlePageChange(p) { query.pageIndex = p; load(); }
|
||||
onMounted(load);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-card shadow="never">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>工艺庄口</span>
|
||||
<el-button type="primary" @click="openCreate">新增庄口</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="search-bar">
|
||||
<el-input v-model="query.keyword" placeholder="庄口名称/编号" clearable style="width: 220px" @keyup.enter="query.pageIndex=1;load()" />
|
||||
<el-input-number v-model="query.year" :min="2000" :max="2100" placeholder="年度" style="width: 130px" />
|
||||
<el-button type="primary" @click="query.pageIndex=1;load()">搜索</el-button>
|
||||
<el-button @click="Object.assign(query,{keyword:'',year:null,pageIndex:1});load()">重置</el-button>
|
||||
</div>
|
||||
|
||||
<el-table v-loading="loading" :data="list" border stripe>
|
||||
<el-table-column prop="belongingYear" label="年度" width="80" />
|
||||
<el-table-column prop="bendiWaidiName" label="本地外地" width="90" />
|
||||
<el-table-column prop="jidiWaigou" label="基地外购" width="90" />
|
||||
<el-table-column prop="zhuangKouCode" label="庄口编号" width="200" />
|
||||
<el-table-column prop="zhuangKouName" label="庄口名称" min-width="240" />
|
||||
<el-table-column prop="productionDisplayName" label="生产显示名称" min-width="240" />
|
||||
<el-table-column prop="chandi" label="产地" width="100" />
|
||||
<el-table-column prop="sort" label="排序" width="70" />
|
||||
<el-table-column label="操作" width="140" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" type="primary" @click="openEdit(row)">编辑</el-button>
|
||||
<el-button size="small" type="danger" @click="del(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-pagination class="pagination" :current-page="query.pageIndex" :page-size="query.pageSize" :total="total" layout="total, prev, pager, next" @current-change="handlePageChange" />
|
||||
|
||||
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="560px">
|
||||
<el-form :model="form" label-width="100px">
|
||||
<el-form-item label="年度">
|
||||
<el-input-number v-model="form.belongingYear" :min="2000" :max="2100" />
|
||||
</el-form-item>
|
||||
<el-form-item label="本地外地">
|
||||
<el-radio-group v-model="form.bendiWaidi">
|
||||
<el-radio :value="1">本地</el-radio>
|
||||
<el-radio :value="2">外地</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="基地外购">
|
||||
<el-select v-model="form.jidiWaigou" style="width: 100%">
|
||||
<el-option label="基地" value="基地" />
|
||||
<el-option label="外购" value="外购" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="庄口编号"><el-input v-model="form.zhuangKouCode" /></el-form-item>
|
||||
<el-form-item label="庄口名称"><el-input v-model="form.zhuangKouName" /></el-form-item>
|
||||
<el-form-item label="生产显示名称"><el-input v-model="form.productionDisplayName" /></el-form-item>
|
||||
<el-form-item label="产地"><el-input v-model="form.chandi" /></el-form-item>
|
||||
<el-form-item label="排序"><el-input-number v-model="form.sort" :min="0" /></el-form-item>
|
||||
<el-form-item label="是否结庄">
|
||||
<el-radio-group v-model="form.isJiezhuang">
|
||||
<el-radio value="0">否</el-radio>
|
||||
<el-radio value="1">是</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注"><el-input v-model="form.memo" type="textarea" :rows="2" /></el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="save">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.card-header { display: flex; justify-content: space-between; align-items: center; }
|
||||
.search-bar { display: flex; gap: 12px; margin-bottom: 16px; flex-wrap: wrap; }
|
||||
.pagination { margin-top: 16px; justify-content: flex-end; }
|
||||
</style>
|
||||
@@ -0,0 +1,125 @@
|
||||
<script setup>
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useUserStore } from '../../store/user';
|
||||
|
||||
const router = useRouter();
|
||||
const userStore = useUserStore();
|
||||
const user = userStore.userInfo || {};
|
||||
|
||||
const quickLinks = [
|
||||
{ title: '落丝记录', desc: '缫丝生产 · 落丝管理', icon: 'DataLine', path: '/prod/lsjl', color: '#1e6b52' },
|
||||
{ title: '纤度检验', desc: '纤度 / 野纤等级判定', icon: 'ScaleToOriginal', path: '/prod/xdjy', color: '#c9a54a' },
|
||||
{ title: '黑板检验', desc: '清洁 / 洁净等级', icon: 'Grid', path: '/prod/hbjy', color: '#3f8f6e' },
|
||||
{ title: '考勤打卡', desc: '上下班签到签退', icon: 'AlarmClock', path: '/attendance/punch', color: '#b8873a' }
|
||||
];
|
||||
|
||||
function go(path) { router.push(path); }
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<el-card shadow="never" class="welcome-card">
|
||||
<div class="welcome">
|
||||
<div class="welcome-text">
|
||||
<h2>欢迎回来,{{ user.nickname || user.realName }}!</h2>
|
||||
<p>智慧缫丝系统 · 蚕丝缫制生产数字化管理平台,覆盖落丝、检验、考勤与消息协同。</p>
|
||||
</div>
|
||||
<div class="welcome-info">
|
||||
<div class="info-item">
|
||||
<span class="label">所属部门</span>
|
||||
<span class="value">{{ user.departmentName || '-' }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">当前角色</span>
|
||||
<span class="value">{{ (user.roles || []).join('、') || '-' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-row :gutter="20" class="stat-row">
|
||||
<el-col v-for="link in quickLinks" :key="link.path" :span="6">
|
||||
<el-card shadow="hover" class="stat-card" @click="go(link.path)">
|
||||
<div class="stat">
|
||||
<el-icon class="stat-icon" :color="link.color"><component :is="link.icon" /></el-icon>
|
||||
<div>
|
||||
<div class="stat-value">{{ link.title }}</div>
|
||||
<div class="stat-label">{{ link.desc }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.welcome-card {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.welcome {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.welcome-text h2 {
|
||||
margin-bottom: 8px;
|
||||
color: #1e3a5f;
|
||||
}
|
||||
|
||||
.welcome-text p {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.welcome-info {
|
||||
display: flex;
|
||||
gap: 40px;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.info-item .label {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.info-item .value {
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.stat {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
font-size: 40px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-top: 4px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,122 @@
|
||||
<script setup>
|
||||
import { ref, reactive } from 'vue';
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
import { useUserStore } from '../../store/user';
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const userStore = useUserStore();
|
||||
|
||||
const loginForm = reactive({ username: '', password: '' });
|
||||
const loading = ref(false);
|
||||
const loginRules = {
|
||||
username: [{ required: true, trigger: 'blur', message: '请输入用户名' }],
|
||||
password: [{ required: true, trigger: 'blur', message: '请输入密码' }]
|
||||
};
|
||||
const formRef = ref();
|
||||
|
||||
async function handleLogin() {
|
||||
await formRef.value.validate();
|
||||
loading.value = true;
|
||||
try {
|
||||
await userStore.login(loginForm);
|
||||
ElMessage.success('登录成功');
|
||||
const redirect = route.query.redirect || '/';
|
||||
router.push(redirect);
|
||||
} catch (e) {
|
||||
// 错误已在拦截器处理
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="login-page">
|
||||
<div class="login-card">
|
||||
<div class="login-header">
|
||||
<div class="logo-icon">
|
||||
<el-icon :size="40"><MagicStick /></el-icon>
|
||||
</div>
|
||||
<h1>智慧缫丝系统</h1>
|
||||
<p>蚕丝缫制 · 智慧工厂管理平台</p>
|
||||
</div>
|
||||
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="loginForm"
|
||||
:rules="loginRules"
|
||||
size="large"
|
||||
@keyup.enter="handleLogin"
|
||||
>
|
||||
<el-form-item prop="username">
|
||||
<el-input v-model="loginForm.username" placeholder="用户名">
|
||||
<template #prefix><el-icon><User /></el-icon></template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="password">
|
||||
<el-input v-model="loginForm.password" type="password" placeholder="密码" show-password>
|
||||
<template #prefix><el-icon><Lock /></el-icon></template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-button type="primary" size="large" :loading="loading" class="login-btn" @click="handleLogin">
|
||||
登 录
|
||||
</el-button>
|
||||
</el-form>
|
||||
|
||||
<div class="login-tip">默认账号:admin / 123456</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.login-page {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #0c3b2e 0%, #1e6b52 55%, #c9a54a 150%);
|
||||
}
|
||||
|
||||
.login-card {
|
||||
width: 400px;
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
padding: 40px;
|
||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.login-header {
|
||||
text-align: center;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.logo-icon {
|
||||
color: #1e6b52;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.login-header h1 {
|
||||
font-size: 26px;
|
||||
color: #175544;
|
||||
letter-spacing: 2px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.login-header p {
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.login-btn {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.login-tip {
|
||||
text-align: center;
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,182 @@
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, onBeforeUnmount } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import request from '../../../utils/request';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { initNotification, unreadCount, loadUnreadCount, openMessage, onBusinessChanged } from '../../../utils/notification';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const list = ref([]);
|
||||
const total = ref(0);
|
||||
const loading = ref(false);
|
||||
const query = reactive({ pageIndex: 1, pageSize: 20, readStatus: null, category: null });
|
||||
|
||||
const priorityMap = {
|
||||
0: { label: '普通', type: 'info' },
|
||||
1: { label: '重要', type: 'warning' },
|
||||
2: { label: '紧急', type: 'danger' }
|
||||
};
|
||||
|
||||
const categoryMap = {
|
||||
0: { label: '通知', type: 'primary' },
|
||||
1: { label: '待办', type: 'warning' },
|
||||
2: { label: '公告', type: 'danger' }
|
||||
};
|
||||
|
||||
async function load() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const params = { pageIndex: query.pageIndex, pageSize: query.pageSize };
|
||||
if (query.readStatus !== null && query.readStatus !== '') params.readStatus = query.readStatus;
|
||||
if (query.category !== null && query.category !== '') params.category = query.category;
|
||||
const res = await request.get('/notification/messages', { params });
|
||||
list.value = res.data.items;
|
||||
total.value = res.data.total;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function markRead(row) {
|
||||
if (row.readStatus === 1) return;
|
||||
try {
|
||||
await request.put(`/notification/messages/${row.id}/read`);
|
||||
row.readStatus = 1;
|
||||
loadUnreadCount();
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
// 点击消息:标记已读 + 跳转业务页(有 routePath 时)
|
||||
async function handleOpen(row) {
|
||||
const jumped = await openMessage(row, router);
|
||||
if (!jumped) {
|
||||
ElMessage.info('该消息无关联业务页面');
|
||||
}
|
||||
}
|
||||
|
||||
async function markAllRead() {
|
||||
try {
|
||||
await request.put('/notification/messages/read-all');
|
||||
ElMessage.success('已全部标记为已读');
|
||||
load();
|
||||
loadUnreadCount();
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
function handlePageChange(p) { query.pageIndex = p; load(); }
|
||||
function resetPage() { query.pageIndex = 1; load(); }
|
||||
|
||||
function rowClass({ row }) {
|
||||
return row.readStatus === 0 ? 'row-unread' : '';
|
||||
}
|
||||
|
||||
function onReceive() {
|
||||
load();
|
||||
loadUnreadCount();
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
load();
|
||||
loadUnreadCount();
|
||||
initNotification(onReceive);
|
||||
// 业务状态变化(如请假审批完成)时,刷新消息列表与未读数
|
||||
onBusinessChanged((e) => {
|
||||
if (e.businessType === 'LeaveApproval') {
|
||||
load();
|
||||
loadUnreadCount();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-card shadow="never">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>
|
||||
我的消息
|
||||
<el-badge v-if="unreadCount > 0" :value="unreadCount" :max="99" class="unread-badge" />
|
||||
</span>
|
||||
<el-button type="primary" plain @click="markAllRead">全部已读</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="search-bar">
|
||||
<el-radio-group v-model="query.category" @change="resetPage">
|
||||
<el-radio-button :value="null">全部</el-radio-button>
|
||||
<el-radio-button :value="0">通知</el-radio-button>
|
||||
<el-radio-button :value="1">待办</el-radio-button>
|
||||
<el-radio-button :value="2">公告</el-radio-button>
|
||||
</el-radio-group>
|
||||
<el-radio-group v-model="query.readStatus" class="read-filter" @change="resetPage">
|
||||
<el-radio-button :value="null">全部</el-radio-button>
|
||||
<el-radio-button :value="0">未读</el-radio-button>
|
||||
<el-radio-button :value="1">已读</el-radio-button>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
|
||||
<el-table v-loading="loading" :data="list" border :row-class-name="rowClass">
|
||||
<el-table-column label="分类" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small" :type="(categoryMap[row.category] || categoryMap[0]).type" effect="light">
|
||||
{{ (categoryMap[row.category] || categoryMap[0]).label }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="类型" width="130">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small" effect="plain">{{ row.typeName || row.messageTypeCode }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="优先级" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small" :type="(priorityMap[row.priority] || priorityMap[0]).type" effect="light">
|
||||
{{ (priorityMap[row.priority] || priorityMap[0]).label }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="标题" min-width="200" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<span class="title-link" :class="{ unread: row.readStatus === 0 }" @click="handleOpen(row)">{{ row.title }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="content" label="内容" min-width="240" show-overflow-tooltip />
|
||||
<el-table-column label="时间" width="160">
|
||||
<template #default="{ row }">{{ row.createdTime?.replace('T', ' ').slice(0, 19) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small" :type="row.readStatus === 1 ? 'info' : 'success'" effect="light">
|
||||
{{ row.readStatus === 1 ? '已读' : '未读' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="110" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button v-if="row.routePath" size="small" type="primary" @click="handleOpen(row)">
|
||||
{{ row.category === 1 ? '去处理' : '查看' }}
|
||||
</el-button>
|
||||
<el-button v-else-if="row.readStatus === 0" size="small" type="primary" link @click="markRead(row)">标记已读</el-button>
|
||||
<span v-else class="done-text">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-pagination class="pagination" :current-page="query.pageIndex" :page-size="query.pageSize" :total="total" layout="total, prev, pager, next" @current-change="handlePageChange" />
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.card-header { display: flex; justify-content: space-between; align-items: center; }
|
||||
.search-bar { margin-bottom: 16px; display: flex; justify-content: space-between; align-items: center; }
|
||||
.pagination { margin-top: 16px; justify-content: flex-end; }
|
||||
.unread-badge { margin-left: 6px; }
|
||||
.unread { font-weight: 600; }
|
||||
.title-link { cursor: pointer; color: #409eff; }
|
||||
.title-link:hover { text-decoration: underline; }
|
||||
.done-text { color: #c0c4cc; }
|
||||
::deep(.row-unread) { background: #f0f9ff; }
|
||||
</style>
|
||||
@@ -0,0 +1,93 @@
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import request from '../../../utils/request';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { Bell, Lock } from '@element-plus/icons-vue';
|
||||
|
||||
const list = ref([]);
|
||||
const loading = ref(false);
|
||||
|
||||
async function load() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await request.get('/notification/subscribe');
|
||||
list.value = res.data;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function toggle(item) {
|
||||
if (!item.canSubscribe) { ElMessage.warning('您没有订阅该消息类型的权限'); return; }
|
||||
try {
|
||||
await request.post('/notification/subscribe', {
|
||||
notificationTypeId: item.id,
|
||||
subscribe: !item.subscribed
|
||||
});
|
||||
ElMessage.success(item.subscribed ? '已取消订阅' : '订阅成功');
|
||||
item.subscribed = !item.subscribed;
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
onMounted(load);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-card shadow="never">
|
||||
<template #header>
|
||||
<div class="card-header"><span>消息订阅</span></div>
|
||||
</template>
|
||||
|
||||
<el-alert
|
||||
title="订阅后,当业务系统触发对应消息时,您将实时收到推送。有权限限制的类型需要对应权限才能订阅。"
|
||||
type="info" :closable="false" show-icon class="tip"
|
||||
/>
|
||||
|
||||
<div v-loading="loading" class="type-list">
|
||||
<el-empty v-if="!loading && list.length === 0" description="暂无可订阅的消息类型" />
|
||||
<el-card
|
||||
v-for="item in list" :key="item.id"
|
||||
class="type-card" shadow="hover"
|
||||
>
|
||||
<div class="type-row">
|
||||
<div class="type-info">
|
||||
<div class="type-name">
|
||||
<el-icon class="bell"><Bell /></el-icon>
|
||||
{{ item.name }}
|
||||
<el-tag v-if="item.requiredPermission" size="small" type="warning" effect="plain" class="perm-tag">
|
||||
<el-icon><Lock /></el-icon> 需权限
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="type-desc">{{ item.description || '暂无描述' }}</div>
|
||||
<div v-if="item.requiredPermission" class="type-perm">权限码:{{ item.requiredPermission }}</div>
|
||||
</div>
|
||||
<div class="type-action">
|
||||
<el-button
|
||||
v-if="item.canSubscribe"
|
||||
:type="item.subscribed ? 'default' : 'primary'"
|
||||
@click="toggle(item)"
|
||||
>
|
||||
{{ item.subscribed ? '取消订阅' : '订阅' }}
|
||||
</el-button>
|
||||
<el-tooltip v-else content="您没有该类型的订阅权限" placement="top">
|
||||
<el-button disabled>无权限</el-button>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.card-header { display: flex; justify-content: space-between; align-items: center; }
|
||||
.tip { margin-bottom: 16px; }
|
||||
.type-list { display: flex; flex-direction: column; gap: 12px; }
|
||||
.type-row { display: flex; justify-content: space-between; align-items: center; }
|
||||
.type-name { display: flex; align-items: center; gap: 6px; font-size: 16px; font-weight: 600; }
|
||||
.bell { color: #409eff; }
|
||||
.perm-tag { margin-left: 4px; }
|
||||
.type-desc { margin-top: 6px; color: #606266; font-size: 13px; }
|
||||
.type-perm { margin-top: 4px; color: #c0c4cc; font-size: 12px; }
|
||||
.type-action { margin-left: 16px; }
|
||||
</style>
|
||||
@@ -0,0 +1,183 @@
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue';
|
||||
import request from '../../../utils/request';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
|
||||
const list = ref([]);
|
||||
const loading = ref(false);
|
||||
|
||||
const dialogVisible = ref(false);
|
||||
const dialogTitle = ref('新增消息类型');
|
||||
const isEdit = ref(false);
|
||||
const editId = ref(null);
|
||||
const categoryMap = [
|
||||
{ value: 0, label: '通知' },
|
||||
{ value: 1, label: '待办' },
|
||||
{ value: 2, label: '公告' }
|
||||
];
|
||||
|
||||
const form = reactive({
|
||||
code: '', name: '', description: '', requiredPermission: '', isActive: true, sort: 0,
|
||||
category: 0, icon: '', routeTemplate: ''
|
||||
});
|
||||
|
||||
async function load() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await request.get('/notification/type');
|
||||
list.value = res.data;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
isEdit.value = false;
|
||||
dialogTitle.value = '新增消息类型';
|
||||
Object.assign(form, { code: '', name: '', description: '', requiredPermission: '', isActive: true, sort: 0, category: 0, icon: '', routeTemplate: '' });
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
function openEdit(row) {
|
||||
isEdit.value = true;
|
||||
editId.value = row.id;
|
||||
dialogTitle.value = '编辑消息类型';
|
||||
Object.assign(form, {
|
||||
code: row.code, name: row.name, description: row.description,
|
||||
requiredPermission: row.requiredPermission, isActive: row.isActive, sort: row.sort,
|
||||
category: row.category ?? 0, icon: row.icon || '', routeTemplate: row.routeTemplate || ''
|
||||
});
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
function categoryLabel(category) {
|
||||
return (categoryMap.find(c => c.value === category) || categoryMap[0]).label;
|
||||
}
|
||||
|
||||
async function save() {
|
||||
if (!form.code || !form.name) { ElMessage.warning('编码和名称不能为空'); return; }
|
||||
try {
|
||||
if (isEdit.value) {
|
||||
await request.put(`/notification/type/${editId.value}`, form);
|
||||
ElMessage.success('更新成功');
|
||||
} else {
|
||||
await request.post('/notification/type', form);
|
||||
ElMessage.success('创建成功');
|
||||
}
|
||||
dialogVisible.value = false;
|
||||
load();
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
async function del(row) {
|
||||
await ElMessageBox.confirm(`确定删除消息类型「${row.name}」吗?`, '提示', { type: 'warning' });
|
||||
try {
|
||||
await request.delete(`/notification/type/${row.id}`);
|
||||
ElMessage.success('删除成功');
|
||||
load();
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
onMounted(load);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-card shadow="never">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>消息类型管理</span>
|
||||
<el-button type="primary" @click="openCreate">新增消息类型</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-alert
|
||||
title="消息类型定义系统中有哪些消息,以及订阅该类型需要什么权限。权限码为空表示所有用户可订阅。"
|
||||
type="info" :closable="false" show-icon class="tip"
|
||||
/>
|
||||
|
||||
<el-table v-loading="loading" :data="list" border stripe>
|
||||
<el-table-column type="index" label="序号" width="60" />
|
||||
<el-table-column prop="code" label="编码" width="160" />
|
||||
<el-table-column prop="name" label="名称" width="130" />
|
||||
<el-table-column label="分类" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small" :type="row.category === 1 ? 'warning' : (row.category === 2 ? 'danger' : 'primary')" effect="light">
|
||||
{{ categoryLabel(row.category) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="description" label="描述" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column label="跳转路由" min-width="200" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.routeTemplate" class="route-text">{{ row.routeTemplate }}</span>
|
||||
<span v-else class="done-text">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="所需权限" min-width="180">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.requiredPermission" size="small" type="warning" effect="plain">{{ row.requiredPermission }}</el-tag>
|
||||
<el-tag v-else size="small" type="success" effect="plain">所有人可订阅</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="开放订阅" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small" :type="row.isActive ? 'success' : 'info'">{{ row.isActive ? '开放' : '关闭' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="sort" label="排序" width="70" align="center" />
|
||||
<el-table-column label="操作" width="140" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" type="primary" @click="openEdit(row)">编辑</el-button>
|
||||
<el-button size="small" type="danger" @click="del(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="560px">
|
||||
<el-form :model="form" label-width="100px">
|
||||
<el-form-item label="编码">
|
||||
<el-input v-model="form.code" placeholder="如 TeyeFound" :disabled="isEdit" />
|
||||
</el-form-item>
|
||||
<el-form-item label="名称">
|
||||
<el-input v-model="form.name" placeholder="如 发现特野" />
|
||||
</el-form-item>
|
||||
<el-form-item label="分类">
|
||||
<el-select v-model="form.category" style="width: 100%">
|
||||
<el-option v-for="c in categoryMap" :key="c.value" :label="c.label" :value="c.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="图标">
|
||||
<el-input v-model="form.icon" placeholder="Element Plus 图标名,如 Bell、Checked" />
|
||||
</el-form-item>
|
||||
<el-form-item label="描述">
|
||||
<el-input v-model="form.description" type="textarea" :rows="2" placeholder="展示在订阅页面的说明" />
|
||||
</el-form-item>
|
||||
<el-form-item label="跳转路由">
|
||||
<el-input v-model="form.routeTemplate" placeholder="如 /attendance/approve?leaveId={businessId},留空则无跳转" />
|
||||
<div class="form-tip">支持 {businessId} 占位符,发送消息时自动替换为业务单据 ID</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="所需权限">
|
||||
<el-input v-model="form.requiredPermission" placeholder="权限码,留空表示所有用户可订阅" />
|
||||
</el-form-item>
|
||||
<el-form-item label="开放订阅">
|
||||
<el-switch v-model="form.isActive" />
|
||||
</el-form-item>
|
||||
<el-form-item label="排序">
|
||||
<el-input-number v-model="form.sort" :min="0" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="save">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.card-header { display: flex; justify-content: space-between; align-items: center; }
|
||||
.tip { margin-bottom: 16px; }
|
||||
.form-tip { font-size: 12px; color: #999; line-height: 1.4; }
|
||||
.route-text { font-size: 12px; color: #409eff; }
|
||||
.done-text { color: #c0c4cc; }
|
||||
</style>
|
||||
@@ -0,0 +1,300 @@
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, computed } from 'vue';
|
||||
import request from '../../../utils/request';
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
// ==================== 列表 ====================
|
||||
const list = ref([]);
|
||||
const total = ref(0);
|
||||
const loading = ref(false);
|
||||
const deptOptions = ref([]);
|
||||
const query = reactive({ pageIndex: 1, pageSize: 20, date: '', deptId: null });
|
||||
|
||||
async function loadDepts() {
|
||||
try {
|
||||
const res = await request.get('/sys/department');
|
||||
deptOptions.value = (res.data || []).filter((d) => d.name.includes('前缫'));
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
async function load() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const params = { pageIndex: query.pageIndex, pageSize: query.pageSize };
|
||||
if (query.date) params.date = query.date;
|
||||
if (query.deptId) params.deptId = query.deptId;
|
||||
const res = await request.get('/hbjy', { params });
|
||||
list.value = res.data.items;
|
||||
total.value = res.data.total;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handlePageChange(p) { query.pageIndex = p; load(); }
|
||||
|
||||
// ==================== 编辑对话框 ====================
|
||||
const dialogVisible = ref(false);
|
||||
const editLoading = ref(false);
|
||||
const saving = ref(false);
|
||||
const editData = ref(null);
|
||||
|
||||
// 一度/二度/三度
|
||||
const yiduColumns = [
|
||||
{ key: 'ydYidu', label: '一度' },
|
||||
{ key: 'ydErdu', label: '二度' },
|
||||
{ key: 'ydSandu', label: '三度' },
|
||||
];
|
||||
|
||||
// 洁净等级
|
||||
const jjColumns = [
|
||||
{ key: 'jj100', label: '100' },
|
||||
{ key: 'jj95', label: '95' },
|
||||
{ key: 'jj90', label: '90' },
|
||||
{ key: 'jj85', label: '85' },
|
||||
{ key: 'jj80', label: '80' },
|
||||
{ key: 'jj75', label: '75' },
|
||||
{ key: 'jj70', label: '70' },
|
||||
{ key: 'jj65', label: '65' },
|
||||
{ key: 'jj60', label: '60' },
|
||||
];
|
||||
|
||||
// 清洁疵点
|
||||
const qjColumns = [
|
||||
{ key: 'qjZycdTdc', label: '特大糙' },
|
||||
{ key: 'qjCycdFs', label: '废丝' },
|
||||
{ key: 'qjCycdDc', label: '大糙' },
|
||||
{ key: 'qjCycdNfc', label: '粘付糙' },
|
||||
{ key: 'qjCycdDcj', label: '大长结' },
|
||||
{ key: 'qjCycdZlx', label: '重螺旋' },
|
||||
{ key: 'qjPtcdXc', label: '小糙' },
|
||||
{ key: 'qjPtcdCj', label: '长结' },
|
||||
{ key: 'qjPtcdLx', label: '螺旋' },
|
||||
{ key: 'qjPtcdHjls', label: '环结及裂丝' },
|
||||
];
|
||||
|
||||
function num(v) {
|
||||
if (typeof v === 'number') return isFinite(v) ? v : 0;
|
||||
if (v === null || v === undefined || v === '') return 0;
|
||||
const n = Number(v);
|
||||
return isFinite(n) ? n : 0;
|
||||
}
|
||||
function round2(v) { return Math.round(v * 100) / 100; }
|
||||
|
||||
// 片数 = 洁净等级各列之和
|
||||
function calcPianshu(row) {
|
||||
return jjColumns.reduce((s, c) => s + num(row[c.key]), 0);
|
||||
}
|
||||
|
||||
function calcRow(row) {
|
||||
row.pianshu = calcPianshu(row);
|
||||
}
|
||||
|
||||
function onInput(row, key, v) {
|
||||
row[key] = num(v);
|
||||
calcRow(row);
|
||||
}
|
||||
|
||||
function selectAll(e) {
|
||||
const el = e?.target;
|
||||
if (el && typeof el.select === 'function') setTimeout(() => el.select(), 0);
|
||||
}
|
||||
|
||||
// 汇总
|
||||
const summary = computed(() => {
|
||||
const rows = editData.value?.records || [];
|
||||
const pianshu = rows.reduce((s, r) => s + (r.pianshu || 0), 0);
|
||||
|
||||
// 清洁扣分
|
||||
let totalDeduction = 0;
|
||||
rows.forEach((r) => {
|
||||
totalDeduction += num(r.qjZycdTdc) * 1.0;
|
||||
totalDeduction += (num(r.qjCycdFs) + num(r.qjCycdDc) + num(r.qjCycdNfc) + num(r.qjCycdDcj) + num(r.qjCycdZlx)) * 0.4;
|
||||
totalDeduction += (num(r.qjPtcdXc) + num(r.qjPtcdCj) + num(r.qjPtcdLx) + num(r.qjPtcdHjls)) * 0.1;
|
||||
});
|
||||
const qingjie = pianshu === 0 ? 0 : round2((pianshu - totalDeduction) * 100 / pianshu);
|
||||
|
||||
// 洁净扣分
|
||||
let jjDeduction = 0;
|
||||
jjDeduction += 1 * rows.reduce((s, r) => s + num(r.jj95), 0);
|
||||
jjDeduction += 2 * rows.reduce((s, r) => s + num(r.jj90), 0);
|
||||
jjDeduction += 3 * rows.reduce((s, r) => s + num(r.jj85), 0);
|
||||
jjDeduction += 4 * rows.reduce((s, r) => s + num(r.jj80), 0);
|
||||
jjDeduction += 5 * rows.reduce((s, r) => s + num(r.jj75), 0);
|
||||
jjDeduction += 6 * rows.reduce((s, r) => s + num(r.jj70), 0);
|
||||
jjDeduction += 7 * rows.reduce((s, r) => s + num(r.jj65), 0);
|
||||
jjDeduction += 8 * rows.reduce((s, r) => s + num(r.jj60), 0);
|
||||
const jiejing = pianshu === 0 ? 0 : round2((pianshu * 100 - jjDeduction * 5) / pianshu);
|
||||
|
||||
return { pianshu, qingjie, jiejing };
|
||||
});
|
||||
|
||||
async function openEdit(row) {
|
||||
editLoading.value = true;
|
||||
dialogVisible.value = true;
|
||||
try {
|
||||
const res = await request.get(`/hbjy/${row.lsjlId}`);
|
||||
editData.value = res.data;
|
||||
editData.value.records.forEach(calcRow);
|
||||
} catch (e) {
|
||||
dialogVisible.value = false;
|
||||
} finally {
|
||||
editLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function save() {
|
||||
if (!editData.value) return;
|
||||
if (summary.value.pianshu === 0) { ElMessage.warning('请至少录入一个洁净等级片数'); return; }
|
||||
saving.value = true;
|
||||
try {
|
||||
await request.post('/hbjy', {
|
||||
lsjlId: editData.value.lsjlId,
|
||||
dengji: editData.value.dengji || '',
|
||||
records: editData.value.records.map((r) => ({
|
||||
chetaihao: r.chetaihao,
|
||||
ydYidu: num(r.ydYidu), ydErdu: num(r.ydErdu), ydSandu: num(r.ydSandu),
|
||||
jj100: num(r.jj100), jj95: num(r.jj95), jj90: num(r.jj90), jj85: num(r.jj85),
|
||||
jj80: num(r.jj80), jj75: num(r.jj75), jj70: num(r.jj70), jj65: num(r.jj65), jj60: num(r.jj60),
|
||||
qjZycdTdc: num(r.qjZycdTdc),
|
||||
qjCycdFs: num(r.qjCycdFs), qjCycdDc: num(r.qjCycdDc), qjCycdNfc: num(r.qjCycdNfc),
|
||||
qjCycdDcj: num(r.qjCycdDcj), qjCycdZlx: num(r.qjCycdZlx),
|
||||
qjPtcdXc: num(r.qjPtcdXc), qjPtcdCj: num(r.qjPtcdCj), qjPtcdLx: num(r.qjPtcdLx), qjPtcdHjls: num(r.qjPtcdHjls)
|
||||
}))
|
||||
});
|
||||
ElMessage.success('保存成功');
|
||||
dialogVisible.value = false;
|
||||
load();
|
||||
} catch (e) {} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => { load(); loadDepts(); });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-card shadow="never">
|
||||
<template #header>
|
||||
<div class="card-header"><span>黑板检验</span></div>
|
||||
</template>
|
||||
|
||||
<div class="search-bar">
|
||||
<el-date-picker v-model="query.date" type="date" placeholder="日期" value-format="YYYY-MM-DD" style="width: 160px" />
|
||||
<el-select v-model="query.deptId" clearable placeholder="车间" style="width: 180px">
|
||||
<el-option v-for="d in deptOptions" :key="d.id" :label="d.name" :value="d.id" />
|
||||
</el-select>
|
||||
<el-button type="primary" @click="query.pageIndex=1;load()">搜索</el-button>
|
||||
<el-button @click="Object.assign(query,{date:'',deptId:null,pageIndex:1});load()">重置</el-button>
|
||||
</div>
|
||||
|
||||
<el-table v-loading="loading" :data="list" border stripe>
|
||||
<el-table-column type="index" label="序号" width="55" />
|
||||
<el-table-column label="日期" width="100">
|
||||
<template #default="{ row }">{{ row.workDate?.slice(0, 10) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="deptName" label="车间" width="110" />
|
||||
<el-table-column prop="jizu" label="机组" width="60" />
|
||||
<el-table-column prop="huishu" label="回数" width="60" />
|
||||
<el-table-column label="操作" width="80" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" :type="row.isChecked ? 'warning' : 'primary'" @click="openEdit(row)">
|
||||
{{ row.isChecked ? '重新录入' : '录入' }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="guige" label="规格" width="75" />
|
||||
<el-table-column prop="zhuangKouName" label="庄口" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column prop="yanse" label="颜色" width="75" />
|
||||
<el-table-column prop="isbancheName" label="半车" width="65" />
|
||||
<el-table-column prop="luosiTime" label="落丝时间" width="90" />
|
||||
<el-table-column prop="chetaishu" label="车台数" width="70" />
|
||||
<el-table-column prop="yingluPianshu" label="应录片数" width="80" />
|
||||
<el-table-column label="已录数" width="70" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.yingluPianshu > row.pianshu ? 'warning' : 'success'" size="small" effect="light">{{ row.pianshu }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="qingjie" label="清洁" width="70" />
|
||||
<el-table-column prop="jiejing" label="洁净" width="70" />
|
||||
<el-table-column prop="dengji" label="等级" width="65" />
|
||||
</el-table>
|
||||
|
||||
<el-pagination class="pagination" :current-page="query.pageIndex" :page-size="query.pageSize" :total="total" layout="total, prev, pager, next" @current-change="handlePageChange" />
|
||||
|
||||
<!-- 编辑对话框 -->
|
||||
<el-dialog v-model="dialogVisible" title="黑板检验" width="95%" top="2vh" destroy-on-close>
|
||||
<div v-loading="editLoading">
|
||||
<template v-if="editData">
|
||||
<!-- 落丝信息 -->
|
||||
<el-descriptions :column="7" border size="small" class="info-block">
|
||||
<el-descriptions-item label="车间">{{ editData.deptName }}</el-descriptions-item>
|
||||
<el-descriptions-item label="机组">{{ editData.jizu }}</el-descriptions-item>
|
||||
<el-descriptions-item label="回数">{{ editData.huishu }}</el-descriptions-item>
|
||||
<el-descriptions-item label="庄口">{{ editData.zhuangKouName }}</el-descriptions-item>
|
||||
<el-descriptions-item label="颜色">{{ editData.yanse }}</el-descriptions-item>
|
||||
<el-descriptions-item label="半车">{{ editData.isbancheName }}</el-descriptions-item>
|
||||
<el-descriptions-item label="规格">{{ editData.guige }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<!-- 车台明细 -->
|
||||
<el-table :data="editData.records" border size="small" max-height="420" class="hbjy-table">
|
||||
<el-table-column prop="chetaihao" label="车台" width="70" align="center" fixed />
|
||||
<el-table-column v-for="c in yiduColumns" :key="c.key" :label="c.label" width="70" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-input-number v-model="row[c.key]" :min="0" :controls="false" class="hb-input" @focus="selectAll" @input="(v) => onInput(row, c.key, v)" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-for="c in jjColumns" :key="c.key" :label="c.label" width="70" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-input-number v-model="row[c.key]" :min="0" :controls="false" class="hb-input" @focus="selectAll" @input="(v) => onInput(row, c.key, v)" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-for="c in qjColumns" :key="c.key" :label="c.label" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-input-number v-model="row[c.key]" :min="0" :controls="false" class="hb-input" @focus="selectAll" @input="(v) => onInput(row, c.key, v)" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="pianshu" label="片数" width="70" align="center" fixed="right" />
|
||||
</el-table>
|
||||
|
||||
<!-- 汇总 -->
|
||||
<div class="summary-bar">
|
||||
<div class="stat"><span class="lbl">已录片数</span><b>{{ summary.pianshu }}</b></div>
|
||||
<div class="stat"><span class="lbl">应录片数</span><b class="c-gray">{{ editData.yingluPianshu }}</b></div>
|
||||
<div class="stat"><span class="lbl">清洁分</span><b class="c-blue">{{ summary.qingjie }}</b></div>
|
||||
<div class="stat"><span class="lbl">洁净分</span><b class="c-green">{{ summary.jiejing }}</b></div>
|
||||
<div class="stat level">
|
||||
<span class="lbl">等级</span>
|
||||
<el-input v-model="editData.dengji" placeholder="等级" style="width: 90px" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="saving" @click="save">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.card-header { display: flex; justify-content: space-between; align-items: center; }
|
||||
.search-bar { display: flex; gap: 12px; margin-bottom: 16px; flex-wrap: wrap; }
|
||||
.pagination { margin-top: 16px; justify-content: flex-end; }
|
||||
.info-block { margin-bottom: 12px; }
|
||||
|
||||
.hb-input :deep(.el-input__wrapper) { padding: 0 4px; }
|
||||
.hb-input :deep(.el-input__inner) { text-align: center; }
|
||||
|
||||
.summary-bar { display: flex; gap: 10px; flex-wrap: wrap; align-items: center; margin-top: 16px; padding: 14px 16px; background: #f5f7fa; border-radius: 8px; }
|
||||
.stat { display: flex; flex-direction: column; align-items: center; min-width: 70px; padding: 6px 14px; background: #fff; border-radius: 6px; box-shadow: 0 1px 3px rgba(0,0,0,.06); }
|
||||
.stat .lbl { font-size: 12px; color: #909399; }
|
||||
.stat b { font-size: 18px; color: #303133; }
|
||||
.stat .c-blue { color: #409eff; }
|
||||
.stat .c-green { color: #67c23a; }
|
||||
.stat .c-gray { color: #909399; }
|
||||
.stat.level { flex-direction: row; gap: 6px; align-items: center; }
|
||||
</style>
|
||||
@@ -0,0 +1,283 @@
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, computed } from 'vue';
|
||||
import request from '../../../utils/request';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
|
||||
const list = ref([]);
|
||||
const total = ref(0);
|
||||
const loading = ref(false);
|
||||
const deptOptions = ref([]); // 前缫车间
|
||||
const zhuangkouOptions = ref([]); // 工艺庄口
|
||||
const guigeOptions = ref([]); // 规格
|
||||
|
||||
const query = reactive({ pageIndex: 1, pageSize: 20, date: '', deptId: null, keyword: '' });
|
||||
|
||||
// 颜色字典(写死)
|
||||
const yanseOptions = ['蓝色', '绿色', '乌红', '深红', '桔红', '蓝方', '黄方', '绿方', '紫方', '红方', '金方', '红圆', '桔圆', '绿圆', '黄圆', '粉圆', '蓝圆', '嫩绿'];
|
||||
const jizuOptions = [1, 2, 3, 4, 5, 6];
|
||||
|
||||
const dialogVisible = ref(false);
|
||||
const dialogTitle = ref('新增落丝记录');
|
||||
const isEdit = ref(false);
|
||||
const editId = ref(null);
|
||||
const form = reactive({
|
||||
workDate: new Date(), deptId: null, gyzkId: null, jizu: 1,
|
||||
luosiTime: '', yanse: '', huishu: 0, chetaiStart: 0, chetaiEnd: 0,
|
||||
chetaishu: 0, isbanche: false, hours: 0, guige: '', chesu: 0
|
||||
});
|
||||
|
||||
async function load() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const params = { pageIndex: query.pageIndex, pageSize: query.pageSize, keyword: query.keyword };
|
||||
if (query.date) params.date = query.date;
|
||||
if (query.deptId) params.deptId = query.deptId;
|
||||
const res = await request.get('/lsjl', { params });
|
||||
list.value = res.data.items;
|
||||
total.value = res.data.total;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 加载前缫车间(名称含"前缫"的部门)
|
||||
async function loadDepts() {
|
||||
const res = await request.get('/sys/department');
|
||||
deptOptions.value = (res.data || []).filter((d) => d.name.includes('前缫'));
|
||||
}
|
||||
|
||||
async function loadZhuangkou() {
|
||||
const res = await request.get('/craftzhuangkou', { params: { pageIndex: 1, pageSize: 1000 } });
|
||||
zhuangkouOptions.value = res.data.items;
|
||||
}
|
||||
|
||||
async function loadGuige() {
|
||||
const res = await request.get('/shengsiguige/all');
|
||||
guigeOptions.value = res.data;
|
||||
}
|
||||
|
||||
// 车间+机组 → 自动带出车台号
|
||||
async function loadChetai() {
|
||||
if (!form.deptId || !form.jizu) return;
|
||||
try {
|
||||
const res = await request.get('/lsjl/chetai', { params: { deptId: form.deptId, jizu: form.jizu } });
|
||||
if (res.data) {
|
||||
form.chetaiStart = res.data.chetaiStart;
|
||||
form.chetaiEnd = res.data.chetaiEnd;
|
||||
calcChetaishu();
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
function calcChetaishu() {
|
||||
if (form.chetaiStart > 0 && form.chetaiEnd > form.chetaiStart) {
|
||||
form.chetaishu = form.chetaiEnd - form.chetaiStart + 1;
|
||||
}
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
isEdit.value = false;
|
||||
dialogTitle.value = '新增落丝记录';
|
||||
Object.assign(form, {
|
||||
workDate: new Date(), deptId: deptOptions.value[0]?.id || null, gyzkId: null, jizu: 1,
|
||||
luosiTime: '', yanse: yanseOptions[0] || '', huishu: 0, chetaiStart: 0, chetaiEnd: 0,
|
||||
chetaishu: 0, isbanche: false, hours: 0, guige: guigeOptions.value[0] || '', chesu: 0
|
||||
});
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
function openEdit(row) {
|
||||
isEdit.value = true;
|
||||
editId.value = row.id;
|
||||
dialogTitle.value = '修改落丝记录';
|
||||
Object.assign(form, {
|
||||
workDate: row.workDate, deptId: row.deptId, gyzkId: row.gyzkId, jizu: row.jizu,
|
||||
luosiTime: row.luosiTime, yanse: row.yanse, huishu: row.huishu,
|
||||
chetaiStart: row.chetaiStart, chetaiEnd: row.chetaiEnd, chetaishu: row.chetaishu,
|
||||
isbanche: row.isbanche, hours: row.hours, guige: row.guige, chesu: row.chesu
|
||||
});
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
async function save() {
|
||||
if (!form.deptId) { ElMessage.warning('请选择车间'); return; }
|
||||
if (!form.gyzkId) { ElMessage.warning('请选择庄口'); return; }
|
||||
try {
|
||||
if (isEdit.value) {
|
||||
await request.put(`/lsjl/${editId.value}`, form);
|
||||
ElMessage.success('更新成功');
|
||||
} else {
|
||||
await request.post('/lsjl', form);
|
||||
ElMessage.success('创建成功');
|
||||
}
|
||||
dialogVisible.value = false;
|
||||
load();
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
async function del(row) {
|
||||
await ElMessageBox.confirm('确定删除这条落丝记录吗?', '提示', { type: 'warning' });
|
||||
try {
|
||||
await request.delete(`/lsjl/${row.id}`);
|
||||
ElMessage.success('删除成功');
|
||||
load();
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
function handlePageChange(p) { query.pageIndex = p; load(); }
|
||||
|
||||
onMounted(() => { load(); loadDepts(); loadZhuangkou(); loadGuige(); });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-card shadow="never">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>落丝记录</span>
|
||||
<el-button type="primary" @click="openCreate">新增落丝记录</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="search-bar">
|
||||
<el-date-picker v-model="query.date" type="date" placeholder="日期" value-format="YYYY-MM-DD" style="width: 160px" />
|
||||
<el-select v-model="query.deptId" clearable placeholder="车间" style="width: 160px">
|
||||
<el-option v-for="d in deptOptions" :key="d.id" :label="d.name" :value="d.id" />
|
||||
</el-select>
|
||||
<el-input v-model="query.keyword" placeholder="庄口/规格" clearable style="width: 180px" @keyup.enter="query.pageIndex=1;load()" />
|
||||
<el-button type="primary" @click="query.pageIndex=1;load()">搜索</el-button>
|
||||
<el-button @click="Object.assign(query,{date:'',deptId:null,keyword:'',pageIndex:1});load()">重置</el-button>
|
||||
</div>
|
||||
|
||||
<el-table v-loading="loading" :data="list" border stripe>
|
||||
<el-table-column label="日期" width="100">
|
||||
<template #default="{ row }">{{ row.workDate?.slice(0, 10) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="deptName" label="车间" width="120" />
|
||||
<el-table-column prop="jizu" label="机组" width="70" />
|
||||
<el-table-column prop="guige" label="规格" width="80" />
|
||||
<el-table-column prop="zhuangKouName" label="庄口" min-width="180" />
|
||||
<el-table-column prop="huishu" label="回数" width="70" />
|
||||
<el-table-column prop="yanse" label="颜色" width="80" />
|
||||
<el-table-column prop="isbancheName" label="半车" width="70" />
|
||||
<el-table-column prop="luosiTime" label="落丝时间" width="90" />
|
||||
<el-table-column prop="hours" label="时长" width="70" />
|
||||
<el-table-column prop="chetaishu" label="车台数" width="80" />
|
||||
<el-table-column prop="chetaiStart" label="开始车台" width="90" />
|
||||
<el-table-column prop="chetaiEnd" label="结束车台" width="90" />
|
||||
<el-table-column prop="chesu" label="车速" width="80" />
|
||||
<el-table-column label="操作" width="140" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" type="primary" @click="openEdit(row)">编辑</el-button>
|
||||
<el-button size="small" type="danger" @click="del(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-pagination class="pagination" :current-page="query.pageIndex" :page-size="query.pageSize" :total="total" layout="total, prev, pager, next" @current-change="handlePageChange" />
|
||||
|
||||
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="640px">
|
||||
<el-form :model="form" label-width="100px">
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="日期">
|
||||
<el-date-picker v-model="form.workDate" type="date" value-format="YYYY-MM-DD" style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="车间">
|
||||
<el-select v-model="form.deptId" style="width: 100%" @change="loadChetai">
|
||||
<el-option v-for="d in deptOptions" :key="d.id" :label="d.name" :value="d.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="机组">
|
||||
<el-select v-model="form.jizu" style="width: 100%" @change="loadChetai">
|
||||
<el-option v-for="j in jizuOptions" :key="j" :label="j" :value="j" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="庄口">
|
||||
<el-select v-model="form.gyzkId" filterable style="width: 100%">
|
||||
<el-option v-for="z in zhuangkouOptions" :key="z.id" :label="z.zhuangKouName" :value="z.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="颜色">
|
||||
<el-select v-model="form.yanse" style="width: 100%">
|
||||
<el-option v-for="y in yanseOptions" :key="y" :label="y" :value="y" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="回数">
|
||||
<el-input-number v-model="form.huishu" :min="0" style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="落丝时间">
|
||||
<el-time-picker v-model="form.luosiTime" value-format="HH:mm:ss" placeholder="落丝时间" style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="缫丝时长">
|
||||
<el-input-number v-model="form.hours" :min="0" :precision="1" style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="8">
|
||||
<el-form-item label="开始车台">
|
||||
<el-input-number v-model="form.chetaiStart" :min="0" style="width: 100%" @change="calcChetaishu" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="结束车台">
|
||||
<el-input-number v-model="form.chetaiEnd" :min="0" style="width: 100%" @change="calcChetaishu" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="车台数">
|
||||
<el-input-number v-model="form.chetaishu" :min="0" style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="规格">
|
||||
<el-select v-model="form.guige" style="width: 100%">
|
||||
<el-option v-for="g in guigeOptions" :key="g" :label="g" :value="g" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="车速">
|
||||
<el-input-number v-model="form.chesu" :min="0" :precision="1" style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-form-item label="半车">
|
||||
<el-switch v-model="form.isbanche" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="save">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.card-header { display: flex; justify-content: space-between; align-items: center; }
|
||||
.search-bar { display: flex; gap: 12px; margin-bottom: 16px; flex-wrap: wrap; }
|
||||
.pagination { margin-top: 16px; justify-content: flex-end; }
|
||||
</style>
|
||||
@@ -0,0 +1,374 @@
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, computed } from 'vue';
|
||||
import request from '../../../utils/request';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { CircleCloseFilled, WarningFilled, InfoFilled } from '@element-plus/icons-vue';
|
||||
|
||||
// ==================== 列表 ====================
|
||||
const list = ref([]);
|
||||
const total = ref(0);
|
||||
const loading = ref(false);
|
||||
const deptOptions = ref([]);
|
||||
const query = reactive({ pageIndex: 1, pageSize: 20, date: '', deptId: null });
|
||||
|
||||
async function loadDepts() {
|
||||
try {
|
||||
const res = await request.get('/sys/department');
|
||||
deptOptions.value = (res.data || []).filter((d) => d.name.includes('前缫'));
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
async function load() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const params = { pageIndex: query.pageIndex, pageSize: query.pageSize };
|
||||
if (query.date) params.date = query.date;
|
||||
if (query.deptId) params.deptId = query.deptId;
|
||||
const res = await request.get('/xdjy', { params });
|
||||
list.value = res.data.items;
|
||||
total.value = res.data.total;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handlePageChange(p) { query.pageIndex = p; load(); }
|
||||
|
||||
// ==================== 编辑对话框 ====================
|
||||
const dialogVisible = ref(false);
|
||||
const editLoading = ref(false);
|
||||
const saving = ref(false);
|
||||
const editData = ref(null);
|
||||
const gg = computed(() => editData.value?.guigeConfig || null);
|
||||
|
||||
// 纤度列配置
|
||||
const xdColumns = [
|
||||
{ key: 'xd1', label: '纤度1' },
|
||||
{ key: 'xd2', label: '纤度2' },
|
||||
{ key: 'xd3', label: '纤度3' },
|
||||
{ key: 'xd4', label: '纤度4' },
|
||||
];
|
||||
|
||||
function num(v) {
|
||||
if (typeof v === 'number') return isFinite(v) ? v : 0;
|
||||
if (v === null || v === undefined || v === '') return 0;
|
||||
const n = Number(v);
|
||||
return isFinite(n) ? n : 0;
|
||||
}
|
||||
function round2(v) { return Math.round(v * 100) / 100; }
|
||||
|
||||
// 野纤等级:teye(特野) / daye(大野) / xiaoye(小野) / null(正常)
|
||||
function xdLevel(xd) {
|
||||
const g = gg.value;
|
||||
const v = num(xd);
|
||||
if (!g || v <= 0) return null;
|
||||
if (v <= g.teyeLolimit || v >= g.teyeUplimit) return 'teye';
|
||||
if (v <= g.dayeLolimit || v >= g.dayeUplimit) return 'daye';
|
||||
if (v <= g.xiaoyeLolimit || v >= g.xiaoyeUplimit) return 'xiaoye';
|
||||
return null;
|
||||
}
|
||||
|
||||
// 范围(在中心范围内)
|
||||
function isInRange(xd) {
|
||||
const g = gg.value;
|
||||
const v = num(xd);
|
||||
return !!g && v > 0 && g.centerrangeLolimit <= v && v <= g.centerrangeUplimit;
|
||||
}
|
||||
|
||||
// 中心(等于中心纤度)
|
||||
function isCenter(xd) {
|
||||
const g = gg.value;
|
||||
const v = num(xd);
|
||||
return !!g && v > 0 && g.centerXiandu === v;
|
||||
}
|
||||
|
||||
// 计算单行指标
|
||||
function calcRow(row) {
|
||||
const xds = [num(row.xd1), num(row.xd2), num(row.xd3), num(row.xd4)];
|
||||
const valid = xds.filter((v) => v > 0);
|
||||
row.xdNum = valid.length;
|
||||
if (valid.length > 0) {
|
||||
row.xdAvg = round2(xds.reduce((a, b) => a + b, 0) / valid.length);
|
||||
row.fanweiNum = xds.filter(isInRange).length;
|
||||
row.xiaoyeNum = xds.filter((v) => xdLevel(v) === 'xiaoye').length;
|
||||
row.dayeNum = xds.filter((v) => xdLevel(v) === 'daye').length;
|
||||
row.teyeNum = xds.filter((v) => xdLevel(v) === 'teye').length;
|
||||
row.centerNum = xds.filter(isCenter).length;
|
||||
} else {
|
||||
row.xdAvg = 0; row.fanweiNum = 0; row.xiaoyeNum = 0; row.dayeNum = 0; row.teyeNum = 0; row.centerNum = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 汇总指标
|
||||
const summary = computed(() => {
|
||||
const items = editData.value?.xdjy || [];
|
||||
let jiaoshu = 0, sum = 0, fanweiNum = 0, xiaoyeNum = 0, dayeNum = 0, teyeNum = 0, centerNum = 0;
|
||||
const xds = [];
|
||||
items.forEach((r) => {
|
||||
[num(r.xd1), num(r.xd2), num(r.xd3), num(r.xd4)].forEach((xd) => {
|
||||
if (xd > 0) { jiaoshu++; sum += xd; xds.push(xd); }
|
||||
});
|
||||
fanweiNum += r.fanweiNum || 0; xiaoyeNum += r.xiaoyeNum || 0; dayeNum += r.dayeNum || 0;
|
||||
teyeNum += r.teyeNum || 0; centerNum += r.centerNum || 0;
|
||||
});
|
||||
const avg = jiaoshu > 0 ? sum / jiaoshu : 0;
|
||||
let junfangcha = 0;
|
||||
if (jiaoshu > 0) {
|
||||
let diff = 0;
|
||||
xds.forEach((x) => { const d = x - avg; diff += d * d; });
|
||||
junfangcha = round2(Math.sqrt(round2(diff / jiaoshu)));
|
||||
}
|
||||
let zongcha = 0;
|
||||
if (jiaoshu > 0) {
|
||||
xds.sort((a, b) => a - b);
|
||||
const n = Math.max(1, Math.ceil(xds.length * 0.02));
|
||||
const minAvg = xds.slice(0, n).reduce((a, b) => a + b, 0) / n;
|
||||
const maxAvg = xds.slice(xds.length - n).reduce((a, b) => a + b, 0) / n;
|
||||
zongcha = round2(Math.max(Math.abs(minAvg - round2(avg)), Math.abs(maxAvg - round2(avg))));
|
||||
}
|
||||
const fanxianLv = jiaoshu > 0 ? Math.round(fanweiNum * 100 / jiaoshu) : 0;
|
||||
return { jiaoshu, avgxiandu: round2(avg), junfangcha, zongcha, fanxianLv, fanweiNum, xiaoyeNum, dayeNum, teyeNum, centerNum };
|
||||
});
|
||||
|
||||
// 纤度输入:显式赋值 + 重算,避免事件时序问题
|
||||
function onXdInput(row, key, v) {
|
||||
row[key] = num(v);
|
||||
calcRow(row);
|
||||
}
|
||||
|
||||
// 单击选中整个单元格(聚焦时全选,方便直接覆盖输入)
|
||||
function selectAll(e) {
|
||||
const el = e?.target;
|
||||
if (el && typeof el.select === 'function') {
|
||||
setTimeout(() => el.select(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
async function openEdit(row) {
|
||||
editLoading.value = true;
|
||||
dialogVisible.value = true;
|
||||
try {
|
||||
const res = await request.get(`/xdjy/${row.lsjlId}`);
|
||||
editData.value = res.data;
|
||||
editData.value.xdjy.forEach(calcRow);
|
||||
} catch (e) {
|
||||
dialogVisible.value = false;
|
||||
} finally {
|
||||
editLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function save() {
|
||||
if (!editData.value) return;
|
||||
if (summary.value.jiaoshu === 0) { ElMessage.warning('请至少录入一个纤度值'); return; }
|
||||
saving.value = true;
|
||||
try {
|
||||
await request.post('/xdjy', {
|
||||
lsjlId: editData.value.lsjlId,
|
||||
dengji: editData.value.dengji || '',
|
||||
items: editData.value.xdjy.map((i) => ({
|
||||
chetaihao: i.chetaihao,
|
||||
xd1: num(i.xd1), xd2: num(i.xd2), xd3: num(i.xd3), xd4: num(i.xd4)
|
||||
}))
|
||||
});
|
||||
ElMessage.success('保存成功');
|
||||
dialogVisible.value = false;
|
||||
load();
|
||||
} catch (e) {} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => { load(); loadDepts(); });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-card shadow="never">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>纤度检验</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="search-bar">
|
||||
<el-date-picker v-model="query.date" type="date" placeholder="日期" value-format="YYYY-MM-DD" style="width: 160px" />
|
||||
<el-select v-model="query.deptId" clearable placeholder="车间" style="width: 180px">
|
||||
<el-option v-for="d in deptOptions" :key="d.id" :label="d.name" :value="d.id" />
|
||||
</el-select>
|
||||
<el-button type="primary" @click="query.pageIndex=1;load()">搜索</el-button>
|
||||
<el-button @click="Object.assign(query,{date:'',deptId:null,pageIndex:1});load()">重置</el-button>
|
||||
</div>
|
||||
|
||||
<el-table v-loading="loading" :data="list" border stripe>
|
||||
<el-table-column type="index" label="序号" width="55" />
|
||||
<el-table-column label="日期" width="100">
|
||||
<template #default="{ row }">{{ row.workDate?.slice(0, 10) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="deptName" label="车间" width="110" />
|
||||
<el-table-column prop="jizu" label="机组" width="60" />
|
||||
<el-table-column prop="huishu" label="回数" width="60" />
|
||||
<el-table-column prop="guige" label="规格" width="75" />
|
||||
<el-table-column prop="zhuangKouName" label="庄口" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column prop="yanse" label="颜色" width="75" />
|
||||
<el-table-column prop="isbancheName" label="半车" width="65" />
|
||||
<el-table-column prop="chetaishu" label="车台数" width="70" />
|
||||
<el-table-column prop="luosiTime" label="落丝时间" width="90" />
|
||||
<el-table-column prop="yingchengnum" label="应秤数" width="70" />
|
||||
<el-table-column label="已秤数" width="70" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.yingchengnum > row.jiaoshu ? 'warning' : 'success'" size="small" effect="light">{{ row.jiaoshu }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="avgxiandu" label="平均纤度" width="85" />
|
||||
<el-table-column prop="junfangcha" label="均方差" width="75" />
|
||||
<el-table-column prop="zongcha" label="总差" width="65" />
|
||||
<el-table-column prop="fanxianLv" label="范纤率" width="70" />
|
||||
<el-table-column prop="centerNum" label="中心" width="60" />
|
||||
<el-table-column prop="xiaoyeNum" label="小野" width="60" />
|
||||
<el-table-column prop="dayeNum" label="大野" width="60" />
|
||||
<el-table-column prop="teyeNum" label="特野" width="60" />
|
||||
<el-table-column prop="dengji" label="等级" width="65" />
|
||||
<el-table-column label="操作" width="90" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" :type="row.isChecked ? 'warning' : 'primary'" @click="openEdit(row)">
|
||||
{{ row.isChecked ? '重新检验' : '检验' }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-pagination class="pagination" :current-page="query.pageIndex" :page-size="query.pageSize" :total="total" layout="total, prev, pager, next" @current-change="handlePageChange" />
|
||||
|
||||
<!-- 检验对话框 -->
|
||||
<el-dialog v-model="dialogVisible" title="纤度检验" width="94%" top="2vh" destroy-on-close>
|
||||
<div v-loading="editLoading">
|
||||
<template v-if="editData">
|
||||
<!-- 落丝信息 -->
|
||||
<el-descriptions :column="6" border size="small" class="info-block">
|
||||
<el-descriptions-item label="车间">{{ editData.deptName }}</el-descriptions-item>
|
||||
<el-descriptions-item label="机组">{{ editData.jizu }}</el-descriptions-item>
|
||||
<el-descriptions-item label="回数">{{ editData.huishu }}</el-descriptions-item>
|
||||
<el-descriptions-item label="庄口">{{ editData.zhuangKouName }}</el-descriptions-item>
|
||||
<el-descriptions-item label="颜色">{{ editData.yanse }}</el-descriptions-item>
|
||||
<el-descriptions-item label="半车">{{ editData.isbancheName }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<!-- 规格范围 -->
|
||||
<el-descriptions v-if="gg" :column="6" border size="small" class="info-block">
|
||||
<el-descriptions-item label="规格">{{ gg.guige }}</el-descriptions-item>
|
||||
<el-descriptions-item label="中心范围">[{{ gg.centerrangeLolimit }} - {{ gg.centerrangeUplimit }}]</el-descriptions-item>
|
||||
<el-descriptions-item label="中心纤度">{{ gg.centerXiandu }}</el-descriptions-item>
|
||||
<el-descriptions-item label="小野范围" class="d-xiaoye">≤{{ gg.xiaoyeLolimit }} / ≥{{ gg.xiaoyeUplimit }}</el-descriptions-item>
|
||||
<el-descriptions-item label="大野范围" class="d-daye">≤{{ gg.dayeLolimit }} / ≥{{ gg.dayeUplimit }}</el-descriptions-item>
|
||||
<el-descriptions-item label="特野范围" class="d-teye">≤{{ gg.teyeLolimit }} / ≥{{ gg.teyeUplimit }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<!-- 车台明细 -->
|
||||
<el-table :data="editData.xdjy" border size="small" max-height="440">
|
||||
<el-table-column prop="chetaihao" label="车台号" width="70" align="center" fixed />
|
||||
<el-table-column v-for="c in xdColumns" :key="c.key" :label="c.label" width="118" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-input-number
|
||||
v-model="row[c.key]"
|
||||
:min="0"
|
||||
:precision="1"
|
||||
:step="0.5"
|
||||
:controls="false"
|
||||
:class="['xd-input', 'xd-' + (xdLevel(row[c.key]) || 'normal')]"
|
||||
@focus="selectAll"
|
||||
@input="(v) => onXdInput(row, c.key, v)"
|
||||
>
|
||||
<template #suffix>
|
||||
<el-icon v-if="xdLevel(row[c.key]) === 'teye'" class="xd-icon icon-teye"><CircleCloseFilled /></el-icon>
|
||||
<el-icon v-else-if="xdLevel(row[c.key]) === 'daye'" class="xd-icon icon-daye"><WarningFilled /></el-icon>
|
||||
<el-icon v-else-if="xdLevel(row[c.key]) === 'xiaoye'" class="xd-icon icon-xiaoye"><InfoFilled /></el-icon>
|
||||
</template>
|
||||
</el-input-number>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="xdNum" label="已称绞数" width="80" align="center" />
|
||||
<el-table-column prop="xdAvg" label="平均纤度" width="85" align="center" />
|
||||
<el-table-column prop="fanweiNum" label="范围" width="60" align="center" />
|
||||
<el-table-column prop="xiaoyeNum" label="小野" width="60" align="center" />
|
||||
<el-table-column prop="dayeNum" label="大野" width="60" align="center" />
|
||||
<el-table-column prop="teyeNum" label="特野" width="60" align="center" />
|
||||
<el-table-column prop="centerNum" label="中心" width="60" align="center" />
|
||||
</el-table>
|
||||
|
||||
<!-- 汇总 -->
|
||||
<div class="summary-bar">
|
||||
<div class="stat"><span class="lbl">已称绞数</span><b>{{ summary.jiaoshu }}</b></div>
|
||||
<div class="stat"><span class="lbl">平均纤度</span><b class="c-primary">{{ summary.avgxiandu }}</b></div>
|
||||
<div class="stat"><span class="lbl">均方差</span><b>{{ summary.junfangcha }}</b></div>
|
||||
<div class="stat"><span class="lbl">总差</span><b>{{ summary.zongcha }}</b></div>
|
||||
<div class="stat"><span class="lbl">范纤率</span><b class="c-primary">{{ summary.fanxianLv }}%</b></div>
|
||||
<div class="stat"><span class="lbl">范围</span><b class="c-blue">{{ summary.fanweiNum }}</b></div>
|
||||
<div class="stat tag-xiaoye"><span class="lbl">小野</span><b>{{ summary.xiaoyeNum }}</b></div>
|
||||
<div class="stat tag-daye"><span class="lbl">大野</span><b>{{ summary.dayeNum }}</b></div>
|
||||
<div class="stat tag-teye"><span class="lbl">特野</span><b>{{ summary.teyeNum }}</b></div>
|
||||
<div class="stat tag-center"><span class="lbl">中心</span><b>{{ summary.centerNum }}</b></div>
|
||||
<div class="stat level">
|
||||
<span class="lbl">等级</span>
|
||||
<el-input v-model="editData.dengji" placeholder="等级" style="width: 90px" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 图例 -->
|
||||
<div class="legend">
|
||||
<span class="lg lg-teye"><el-icon><CircleCloseFilled /></el-icon> 特野</span>
|
||||
<span class="lg lg-daye"><el-icon><WarningFilled /></el-icon> 大野</span>
|
||||
<span class="lg lg-xiaoye"><el-icon><InfoFilled /></el-icon> 小野</span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="saving" @click="save">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.card-header { display: flex; justify-content: space-between; align-items: center; }
|
||||
.search-bar { display: flex; gap: 12px; margin-bottom: 16px; flex-wrap: wrap; }
|
||||
.pagination { margin-top: 16px; justify-content: flex-end; }
|
||||
.info-block { margin-bottom: 12px; }
|
||||
|
||||
/* 规格范围描述着色 */
|
||||
.d-xiaoye :deep(.el-descriptions__content) { color: #b88230; font-weight: 600; }
|
||||
.d-daye :deep(.el-descriptions__content) { color: #e6a23c; font-weight: 600; }
|
||||
.d-teye :deep(.el-descriptions__content) { color: #f56c6c; font-weight: 600; }
|
||||
|
||||
/* 纤度输入框:浅色背景 + 图标 */
|
||||
.xd-input :deep(.el-input__wrapper) { padding: 0 6px; }
|
||||
.xd-input.xd-teye :deep(.el-input__wrapper) { background: #fef0f0; box-shadow: 0 0 0 1px #f56c6c inset; }
|
||||
.xd-input.xd-daye :deep(.el-input__wrapper) { background: #fdf6ec; box-shadow: 0 0 0 1px #e6a23c inset; }
|
||||
.xd-input.xd-xiaoye :deep(.el-input__wrapper) { background: #fef9e7; box-shadow: 0 0 0 1px #e6b23c inset; }
|
||||
|
||||
.xd-icon { font-size: 14px; }
|
||||
.icon-teye { color: #f56c6c; }
|
||||
.icon-daye { color: #e6a23c; }
|
||||
.icon-xiaoye { color: #e6b23c; }
|
||||
|
||||
/* 汇总栏 */
|
||||
.summary-bar { display: flex; gap: 10px; flex-wrap: wrap; align-items: center; margin-top: 16px; padding: 14px 16px; background: #f5f7fa; border-radius: 8px; }
|
||||
.stat { display: flex; flex-direction: column; align-items: center; min-width: 64px; padding: 6px 12px; background: #fff; border-radius: 6px; box-shadow: 0 1px 3px rgba(0,0,0,.06); }
|
||||
.stat .lbl { font-size: 12px; color: #909399; }
|
||||
.stat b { font-size: 18px; color: #303133; }
|
||||
.stat .c-primary { color: #409eff; }
|
||||
.stat .c-blue { color: #409eff; }
|
||||
.stat.tag-xiaoye b { color: #b88230; }
|
||||
.stat.tag-daye b { color: #e6a23c; }
|
||||
.stat.tag-teye b { color: #f56c6c; }
|
||||
.stat.tag-center b { color: #67c23a; }
|
||||
.stat.level { flex-direction: row; gap: 6px; align-items: center; }
|
||||
|
||||
/* 图例 */
|
||||
.legend { display: flex; gap: 18px; margin-top: 12px; color: #606266; font-size: 13px; }
|
||||
.legend .lg { display: flex; align-items: center; gap: 4px; }
|
||||
.lg-teye { color: #f56c6c; }
|
||||
.lg-daye { color: #e6a23c; }
|
||||
.lg-xiaoye { color: #b88230; }
|
||||
</style>
|
||||
@@ -0,0 +1,152 @@
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue';
|
||||
import request from '../../../utils/request';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
|
||||
const list = ref([]);
|
||||
const loading = ref(false);
|
||||
const dialogVisible = ref(false);
|
||||
const dialogTitle = ref('新增部门');
|
||||
const isEdit = ref(false);
|
||||
const editId = ref(null);
|
||||
const form = reactive({ code: '', name: '', parentId: 0, manageType: 5, dutyType: 5, sortOrder: 0, description: '' });
|
||||
|
||||
const manageTypeMap = { 5: '公司', 10: '部门', 15: '车间', 20: '科室', 25: '岗位', 30: '虚拟' };
|
||||
|
||||
async function loadList() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await request.get('/sys/department');
|
||||
list.value = res.data;
|
||||
} finally { loading.value = false; }
|
||||
}
|
||||
|
||||
function parentName(id) {
|
||||
return list.value.find(x => x.id === id)?.name || '-';
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
isEdit.value = false;
|
||||
dialogTitle.value = '新增部门';
|
||||
Object.assign(form, { code: '', name: '', parentId: 0, manageType: 5, dutyType: 5, sortOrder: 0, description: '' });
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
function openEdit(row) {
|
||||
isEdit.value = true;
|
||||
editId.value = row.id;
|
||||
dialogTitle.value = '编辑部门';
|
||||
Object.assign(form, {
|
||||
code: row.code, name: row.name, parentId: row.parentId, manageType: row.manageType,
|
||||
dutyType: row.dutyType, sortOrder: row.sortOrder, description: row.description || ''
|
||||
});
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
if (!form.code || !form.name) { ElMessage.warning('请填写编码和名称'); return; }
|
||||
try {
|
||||
if (isEdit.value) {
|
||||
await request.put(`/sys/department/${editId.value}`, form);
|
||||
ElMessage.success('更新成功');
|
||||
} else {
|
||||
await request.post('/sys/department', form);
|
||||
ElMessage.success('创建成功');
|
||||
}
|
||||
dialogVisible.value = false;
|
||||
loadList();
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
async function handleDelete(row) {
|
||||
await ElMessageBox.confirm(`确定删除部门「${row.name}」吗?`, '提示', { type: 'warning' });
|
||||
try {
|
||||
await request.delete(`/sys/department/${row.id}`);
|
||||
ElMessage.success('删除成功');
|
||||
loadList();
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
async function handleActive(row) {
|
||||
try {
|
||||
await request.put(`/sys/department/${row.id}/active`, { active: !row.isActive });
|
||||
ElMessage.success('操作成功');
|
||||
loadList();
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
onMounted(loadList);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-card shadow="never">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>部门管理</span>
|
||||
<el-button type="primary" @click="openCreate">新增部门</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-table v-loading="loading" :data="list" border stripe>
|
||||
<el-table-column prop="code" label="编码" width="140" />
|
||||
<el-table-column prop="name" label="名称" width="160" />
|
||||
<el-table-column label="上级" width="140">
|
||||
<template #default="{ row }">{{ parentName(row.parentId) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="类型" width="90">
|
||||
<template #default="{ row }">{{ manageTypeMap[row.manageType] || row.manageType }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="sortOrder" label="排序" width="80" />
|
||||
<el-table-column prop="description" label="描述" min-width="160" />
|
||||
<el-table-column label="状态" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.isActive ? 'success' : 'danger'">{{ row.isActive ? '启用' : '禁用' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" type="primary" @click="openEdit(row)">编辑</el-button>
|
||||
<el-button size="small" :type="row.isActive ? 'warning' : 'success'" @click="handleActive(row)">
|
||||
{{ row.isActive ? '禁用' : '启用' }}
|
||||
</el-button>
|
||||
<el-button size="small" type="danger" @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="450px">
|
||||
<el-form :model="form" label-width="80px">
|
||||
<el-form-item label="编码">
|
||||
<el-input v-model="form.code" placeholder="部门编码" />
|
||||
</el-form-item>
|
||||
<el-form-item label="名称">
|
||||
<el-input v-model="form.name" placeholder="部门名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="上级">
|
||||
<el-select v-model="form.parentId" clearable placeholder="无上级" style="width: 100%">
|
||||
<el-option v-for="d in list" :key="d.id" :label="d.name" :value="d.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="类型">
|
||||
<el-select v-model="form.manageType" style="width: 100%">
|
||||
<el-option v-for="(v, k) in manageTypeMap" :key="k" :label="v" :value="Number(k)" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="排序">
|
||||
<el-input-number v-model="form.sortOrder" :min="0" />
|
||||
</el-form-item>
|
||||
<el-form-item label="描述">
|
||||
<el-input v-model="form.description" type="textarea" :rows="2" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleSave">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.card-header { display: flex; justify-content: space-between; align-items: center; }
|
||||
</style>
|
||||
@@ -0,0 +1,163 @@
|
||||
<script setup>
|
||||
import { ref, reactive, computed, onMounted } from 'vue';
|
||||
import request from '../../../utils/request';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
|
||||
const list = ref([]);
|
||||
const loading = ref(false);
|
||||
const dialogVisible = ref(false);
|
||||
const dialogTitle = ref('新增菜单');
|
||||
const isEdit = ref(false);
|
||||
const editId = ref(null);
|
||||
const form = reactive({ name: '', iconSvg: '', menuType: 2, parentId: 0, sortOrder: 0 });
|
||||
|
||||
const menuTypeMap = { 0: '默认', 1: '分组', 2: '页面' };
|
||||
|
||||
// 扁平化树形菜单(用于父菜单选择)
|
||||
const flatMenus = computed(() => {
|
||||
const result = [];
|
||||
const walk = (items) => {
|
||||
items.forEach((i) => {
|
||||
result.push(i);
|
||||
if (i.items && i.items.length) walk(i.items);
|
||||
});
|
||||
};
|
||||
walk(list.value);
|
||||
return result;
|
||||
});
|
||||
|
||||
async function loadList() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await request.get('/sys/menu/tree');
|
||||
list.value = res.data;
|
||||
} finally { loading.value = false; }
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
isEdit.value = false;
|
||||
dialogTitle.value = '新增菜单';
|
||||
Object.assign(form, { name: '', iconSvg: '', menuType: 2, parentId: 0, sortOrder: 0 });
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
function openEdit(row) {
|
||||
isEdit.value = true;
|
||||
editId.value = row.id;
|
||||
dialogTitle.value = '编辑菜单';
|
||||
Object.assign(form, {
|
||||
name: row.name, iconSvg: row.iconSvg, menuType: menuTypeValue(row.menuType),
|
||||
parentId: row.parentId, sortOrder: row.sortOrder
|
||||
});
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
function menuTypeValue(t) {
|
||||
if (t === '分组') return 1;
|
||||
if (t === '页面') return 2;
|
||||
return 0;
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
if (!form.name) { ElMessage.warning('请填写菜单名称'); return; }
|
||||
try {
|
||||
if (isEdit.value) {
|
||||
await request.put(`/sys/menu/${editId.value}`, { ...form, assemblyName: '', typeFullName: '' });
|
||||
ElMessage.success('更新成功');
|
||||
} else {
|
||||
await request.post('/sys/menu', { ...form, assemblyName: '', typeFullName: '' });
|
||||
ElMessage.success('创建成功');
|
||||
}
|
||||
dialogVisible.value = false;
|
||||
loadList();
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
async function handleDelete(row) {
|
||||
await ElMessageBox.confirm(`确定删除菜单「${row.name}」吗?`, '提示', { type: 'warning' });
|
||||
try {
|
||||
await request.delete(`/sys/menu/${row.id}`);
|
||||
ElMessage.success('删除成功');
|
||||
loadList();
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
async function handleActive(row) {
|
||||
try {
|
||||
await request.put(`/sys/menu/${row.id}/active`, { active: row.isActive !== '启用' });
|
||||
ElMessage.success('操作成功');
|
||||
loadList();
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
onMounted(loadList);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-card shadow="never">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>菜单管理</span>
|
||||
<el-button type="primary" @click="openCreate">新增菜单</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-table v-loading="loading" :data="list" border stripe row-key="id" default-expand-all>
|
||||
<el-table-column prop="name" label="名称" width="200" />
|
||||
<el-table-column prop="code" label="编码" width="180" />
|
||||
<el-table-column label="类型" width="90">
|
||||
<template #default="{ row }">{{ row.menuType }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="iconSvg" label="图标" width="100" />
|
||||
<el-table-column prop="parentName" label="上级" width="140" />
|
||||
<el-table-column prop="level" label="层级" width="70" />
|
||||
<el-table-column prop="sortOrder" label="排序" width="80" />
|
||||
<el-table-column label="状态" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.isActive === '启用' ? 'success' : 'danger'">{{ row.isActive }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" type="primary" @click="openEdit(row)">编辑</el-button>
|
||||
<el-button size="small" :type="row.isActive === '启用' ? 'warning' : 'success'" @click="handleActive(row)">
|
||||
{{ row.isActive === '启用' ? '禁用' : '启用' }}
|
||||
</el-button>
|
||||
<el-button size="small" type="danger" @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="450px">
|
||||
<el-form :model="form" label-width="80px">
|
||||
<el-form-item label="名称">
|
||||
<el-input v-model="form.name" placeholder="菜单名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="图标">
|
||||
<el-input v-model="form.iconSvg" placeholder="Element Plus 图标名" />
|
||||
</el-form-item>
|
||||
<el-form-item label="类型">
|
||||
<el-select v-model="form.menuType" style="width: 100%">
|
||||
<el-option v-for="(v, k) in menuTypeMap" :key="k" :label="v" :value="Number(k)" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="上级">
|
||||
<el-select v-model="form.parentId" clearable placeholder="无上级" style="width: 100%">
|
||||
<el-option v-for="m in flatMenus" :key="m.id" :label="m.name" :value="m.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="排序">
|
||||
<el-input-number v-model="form.sortOrder" :min="0" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleSave">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.card-header { display: flex; justify-content: space-between; align-items: center; }
|
||||
</style>
|
||||
@@ -0,0 +1,318 @@
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, nextTick } from 'vue';
|
||||
import request from '../../../utils/request';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
|
||||
const list = ref([]);
|
||||
const loading = ref(false);
|
||||
const dialogVisible = ref(false);
|
||||
const dialogTitle = ref('新增角色');
|
||||
const isEdit = ref(false);
|
||||
const editId = ref(null);
|
||||
const form = reactive({ name: '', code: '', sortOrder: 0, description: '' });
|
||||
|
||||
async function loadList() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await request.get('/sys/role');
|
||||
list.value = res.data;
|
||||
} finally { loading.value = false; }
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
isEdit.value = false;
|
||||
dialogTitle.value = '新增角色';
|
||||
Object.assign(form, { name: '', code: '', sortOrder: 0, description: '' });
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
function openEdit(row) {
|
||||
isEdit.value = true;
|
||||
editId.value = row.id;
|
||||
dialogTitle.value = '编辑角色';
|
||||
Object.assign(form, { name: row.name, code: row.code, sortOrder: row.sortOrder, description: row.description });
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
if (!form.name || !form.code) { ElMessage.warning('请填写名称和编码'); return; }
|
||||
try {
|
||||
if (isEdit.value) {
|
||||
await request.put(`/sys/role/${editId.value}`, form);
|
||||
ElMessage.success('更新成功');
|
||||
} else {
|
||||
await request.post('/sys/role', form);
|
||||
ElMessage.success('创建成功');
|
||||
}
|
||||
dialogVisible.value = false;
|
||||
loadList();
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
async function handleDelete(row) {
|
||||
await ElMessageBox.confirm(`确定删除角色「${row.name}」吗?`, '提示', { type: 'warning' });
|
||||
try {
|
||||
await request.delete(`/sys/role/${row.id}`);
|
||||
ElMessage.success('删除成功');
|
||||
loadList();
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
async function handleActive(row) {
|
||||
try {
|
||||
await request.put(`/sys/role/${row.id}/active`, { active: !row.isActive });
|
||||
ElMessage.success('操作成功');
|
||||
loadList();
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
// ==================== 权限分配(参照 F9 Page_Role_DepPermission 用法) ====================
|
||||
const RESOURCE_TYPE = { menu: 1, department: 4, subscribe: 5 };
|
||||
|
||||
const permDialogVisible = ref(false);
|
||||
const currentRole = ref(null);
|
||||
const activeTab = ref('menu');
|
||||
const loadingPerm = ref(false);
|
||||
const savingPerm = ref(false);
|
||||
|
||||
const menuTree = ref([]);
|
||||
const deptTree = ref([]);
|
||||
const subTree = ref([]);
|
||||
const menuTreeRef = ref();
|
||||
const deptTreeRef = ref();
|
||||
const subTreeRef = ref();
|
||||
|
||||
// 角色已分配权限:ResourceId -> rolePermissionId
|
||||
const rolePerms = reactive({ menu: [], department: [], subscribe: [] });
|
||||
|
||||
function toMenuTree(nodes) {
|
||||
return (nodes || []).map(n => ({
|
||||
id: n.id,
|
||||
label: n.name,
|
||||
code: n.code,
|
||||
children: n.items ? toMenuTree(n.items) : [],
|
||||
}));
|
||||
}
|
||||
|
||||
function buildDeptTreeLocal(listData) {
|
||||
const map = {};
|
||||
(listData || []).forEach(d => {
|
||||
map[d.id] = { id: d.id, label: d.name, code: d.code, children: [] };
|
||||
});
|
||||
const roots = [];
|
||||
(listData || []).forEach(d => {
|
||||
const node = map[d.id];
|
||||
if (d.parentId && map[d.parentId]) map[d.parentId].children.push(node);
|
||||
else roots.push(node);
|
||||
});
|
||||
return roots;
|
||||
}
|
||||
|
||||
async function openPerm(row) {
|
||||
currentRole.value = row;
|
||||
activeTab.value = 'menu';
|
||||
permDialogVisible.value = true;
|
||||
await loadPermData(row.id);
|
||||
}
|
||||
|
||||
async function loadPermData(roleId) {
|
||||
loadingPerm.value = true;
|
||||
try {
|
||||
const [menuRes, deptRes, subRes, menuPerms, deptPerms, subPerms] = await Promise.all([
|
||||
request.get('/sys/menu/tree'),
|
||||
request.get('/sys/department'),
|
||||
request.get('/notification/type'),
|
||||
request.get(`/sys/role/${roleId}/permissions`, { params: { resourceType: RESOURCE_TYPE.menu } }),
|
||||
request.get(`/sys/role/${roleId}/permissions`, { params: { resourceType: RESOURCE_TYPE.department } }),
|
||||
request.get(`/sys/role/${roleId}/permissions`, { params: { resourceType: RESOURCE_TYPE.subscribe } }),
|
||||
]);
|
||||
|
||||
rolePerms.menu = menuPerms.data || [];
|
||||
rolePerms.department = deptPerms.data || [];
|
||||
rolePerms.subscribe = subPerms.data || [];
|
||||
|
||||
menuTree.value = toMenuTree(menuRes.data);
|
||||
deptTree.value = buildDeptTreeLocal(deptRes.data);
|
||||
subTree.value = (subRes.data || []).map(t => ({ id: t.id, label: t.name, code: t.code }));
|
||||
|
||||
// 树渲染后设置勾选(自动勾选角色已有权限)
|
||||
await nextTick();
|
||||
menuTreeRef.value?.setCheckedKeys(rolePerms.menu.map(p => String(p.resourceId)));
|
||||
deptTreeRef.value?.setCheckedKeys(rolePerms.department.map(p => String(p.resourceId)));
|
||||
subTreeRef.value?.setCheckedKeys(rolePerms.subscribe.map(p => String(p.resourceId)));
|
||||
} finally {
|
||||
loadingPerm.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 收集树中所有节点(用于按 id 查 code)
|
||||
function collectNodes(nodes, result) {
|
||||
(nodes || []).forEach(n => { result.push(n); if (n.children) collectNodes(n.children, result); });
|
||||
}
|
||||
|
||||
async function handlePermSave() {
|
||||
const roleId = currentRole.value?.id;
|
||||
if (!roleId) return;
|
||||
savingPerm.value = true;
|
||||
try {
|
||||
const menuKeys = (menuTreeRef.value?.getCheckedKeys() || []).map(String);
|
||||
const deptKeys = (deptTreeRef.value?.getCheckedKeys() || []).map(String);
|
||||
const subKeys = (subTreeRef.value?.getCheckedKeys() || []).map(String);
|
||||
|
||||
await savePermType('menu', RESOURCE_TYPE.menu, menuKeys, menuTree.value);
|
||||
await savePermType('department', RESOURCE_TYPE.department, deptKeys, deptTree.value);
|
||||
await savePermType('subscribe', RESOURCE_TYPE.subscribe, subKeys, subTree.value);
|
||||
|
||||
ElMessage.success('权限保存成功');
|
||||
permDialogVisible.value = false;
|
||||
loadList();
|
||||
} finally {
|
||||
savingPerm.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function savePermType(type, resourceType, checkedKeys, treeData) {
|
||||
const checkedIds = new Set(checkedKeys.map(String));
|
||||
const original = rolePerms[type];
|
||||
|
||||
// 构建 id -> node 映射(用于取 code)
|
||||
const nodes = [];
|
||||
collectNodes(treeData, nodes);
|
||||
const nodeMap = new Map(nodes.map(n => [String(n.id), n]));
|
||||
|
||||
// 新增:当前勾选但原始没有
|
||||
for (const key of checkedKeys) {
|
||||
if (!original.some(p => String(p.resourceId) === key)) {
|
||||
const node = nodeMap.get(key);
|
||||
await request.post(`/sys/role/${currentRole.value.id}/permissions`, {
|
||||
permissionType: 1, // Visble
|
||||
resourceType,
|
||||
resourceIdentity: node?.code || key,
|
||||
resourceId: Number(key),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 删除:原始有但当前未勾选
|
||||
for (const rp of original) {
|
||||
if (!checkedIds.has(String(rp.resourceId))) {
|
||||
await request.delete(`/sys/role/${currentRole.value.id}/permissions/${rp.id}`, {
|
||||
params: { resourceType },
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(loadList);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-card shadow="never">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>角色管理</span>
|
||||
<el-button type="primary" @click="openCreate">新增角色</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-table v-loading="loading" :data="list" border stripe>
|
||||
<el-table-column prop="name" label="名称" width="140" />
|
||||
<el-table-column prop="code" label="编码" width="160" />
|
||||
<el-table-column prop="sortOrder" label="排序" width="80" />
|
||||
<el-table-column prop="menuPermissionCount" label="菜单权限" width="90" align="center" />
|
||||
<el-table-column prop="depPermissionCount" label="部门权限" width="90" align="center" />
|
||||
<el-table-column prop="description" label="描述" min-width="140" />
|
||||
<el-table-column label="状态" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.isActive ? 'success' : 'danger'">{{ row.isActive ? '启用' : '禁用' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="280" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" type="success" @click="openPerm(row)">分配权限</el-button>
|
||||
<el-button size="small" type="primary" @click="openEdit(row)">编辑</el-button>
|
||||
<el-button size="small" :type="row.isActive ? 'warning' : 'success'" @click="handleActive(row)">
|
||||
{{ row.isActive ? '禁用' : '启用' }}
|
||||
</el-button>
|
||||
<el-button size="small" type="danger" @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="450px">
|
||||
<el-form :model="form" label-width="80px">
|
||||
<el-form-item label="名称">
|
||||
<el-input v-model="form.name" placeholder="如 系统管理员" />
|
||||
</el-form-item>
|
||||
<el-form-item label="编码">
|
||||
<el-input v-model="form.code" placeholder="如 admin" />
|
||||
</el-form-item>
|
||||
<el-form-item label="排序">
|
||||
<el-input-number v-model="form.sortOrder" :min="0" />
|
||||
</el-form-item>
|
||||
<el-form-item label="描述">
|
||||
<el-input v-model="form.description" type="textarea" :rows="2" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleSave">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 权限分配对话框(菜单 / 部门 / 消息订阅,带确认按钮) -->
|
||||
<el-dialog v-model="permDialogVisible" :title="`分配权限 - ${currentRole?.name || ''}`" width="520px">
|
||||
<div v-loading="loadingPerm">
|
||||
<el-radio-group v-model="activeTab" class="perm-tabs">
|
||||
<el-radio-button value="menu">菜单权限</el-radio-button>
|
||||
<el-radio-button value="department">部门权限</el-radio-button>
|
||||
<el-radio-button value="subscribe">消息订阅</el-radio-button>
|
||||
</el-radio-group>
|
||||
|
||||
<div v-show="activeTab === 'menu'" class="perm-panel">
|
||||
<el-tree
|
||||
ref="menuTreeRef"
|
||||
:data="menuTree"
|
||||
show-checkbox
|
||||
check-strictly
|
||||
node-key="id"
|
||||
default-expand-all
|
||||
:props="{ label: 'label', children: 'children' }"
|
||||
/>
|
||||
</div>
|
||||
<div v-show="activeTab === 'department'" class="perm-panel">
|
||||
<el-tree
|
||||
ref="deptTreeRef"
|
||||
:data="deptTree"
|
||||
show-checkbox
|
||||
check-strictly
|
||||
node-key="id"
|
||||
default-expand-all
|
||||
:props="{ label: 'label', children: 'children' }"
|
||||
/>
|
||||
</div>
|
||||
<div v-show="activeTab === 'subscribe'" class="perm-panel">
|
||||
<el-tree
|
||||
ref="subTreeRef"
|
||||
:data="subTree"
|
||||
show-checkbox
|
||||
check-strictly
|
||||
node-key="id"
|
||||
default-expand-all
|
||||
:props="{ label: 'label', children: 'children' }"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="permDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="savingPerm" @click="handlePermSave">确认</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.card-header { display: flex; justify-content: space-between; align-items: center; }
|
||||
.perm-tabs { margin-bottom: 12px; }
|
||||
.perm-panel { max-height: 420px; overflow-y: auto; border: 1px solid #ebeef5; border-radius: 4px; padding: 8px; }
|
||||
</style>
|
||||
@@ -0,0 +1,236 @@
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue';
|
||||
import request from '../../../utils/request';
|
||||
import { buildDeptTree } from '../../../utils/dept';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
|
||||
const list = ref([]);
|
||||
const total = ref(0);
|
||||
const loading = ref(false);
|
||||
const deptTree = ref([]);
|
||||
const roles = ref([]);
|
||||
const userRoles = ref([]);
|
||||
|
||||
const query = reactive({ departmentId: 0, like: '', page: 1, pageSize: 10 });
|
||||
|
||||
const dialogVisible = ref(false);
|
||||
const dialogTitle = ref('新增用户');
|
||||
const isEdit = ref(false);
|
||||
const editId = ref(null);
|
||||
const form = reactive({ userName: '', phoneNumber: '', loginName: '', departmentId: null, sortOrder: 0 });
|
||||
|
||||
const roleDialogVisible = ref(false);
|
||||
const roleDialogUserId = ref(null);
|
||||
|
||||
async function loadList() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await request.get('/sys/user', {
|
||||
params: { departmentId: query.departmentId || 0, like: query.like, page: query.page, pageSize: query.pageSize }
|
||||
});
|
||||
list.value = res.data.items;
|
||||
total.value = res.data.totalCount;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadDepts() {
|
||||
const res = await request.get('/sys/department');
|
||||
deptTree.value = buildDeptTree(res.data);
|
||||
}
|
||||
|
||||
async function loadRoles() {
|
||||
const res = await request.get('/sys/role');
|
||||
roles.value = res.data;
|
||||
}
|
||||
|
||||
function handleSearch() { query.page = 1; loadList(); }
|
||||
function resetSearch() { Object.assign(query, { departmentId: 0, like: '', page: 1, pageSize: 10 }); loadList(); }
|
||||
|
||||
function openCreate() {
|
||||
isEdit.value = false;
|
||||
dialogTitle.value = '新增用户';
|
||||
Object.assign(form, { userName: '', phoneNumber: '', loginName: '', departmentId: null, sortOrder: 0 });
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
function openEdit(row) {
|
||||
isEdit.value = true;
|
||||
editId.value = row.id;
|
||||
dialogTitle.value = '编辑用户';
|
||||
Object.assign(form, {
|
||||
userName: row.userName, phoneNumber: row.phoneNumber, loginName: row.loginName,
|
||||
departmentId: row.departmentId ?? null, sortOrder: row.sortOrder
|
||||
});
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
if (!form.userName || !form.loginName) {
|
||||
ElMessage.warning('请填写姓名和登录账号');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (isEdit.value) {
|
||||
await request.put(`/sys/user/${editId.value}`, { ...form, departmentId: form.departmentId });
|
||||
ElMessage.success('更新成功');
|
||||
} else {
|
||||
await request.post('/sys/user', { ...form, departmentId: form.departmentId });
|
||||
ElMessage.success('创建成功');
|
||||
}
|
||||
dialogVisible.value = false;
|
||||
loadList();
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
async function handleDelete(row) {
|
||||
await ElMessageBox.confirm(`确定删除用户「${row.userName}」吗?`, '提示', { type: 'warning' });
|
||||
try {
|
||||
await request.delete(`/sys/user/${row.id}`);
|
||||
ElMessage.success('删除成功');
|
||||
loadList();
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
async function handleActive(row) {
|
||||
try {
|
||||
await request.put(`/sys/user/${row.id}/active`, { active: !row.isActive });
|
||||
ElMessage.success('操作成功');
|
||||
loadList();
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
async function handleResetPwd(row) {
|
||||
await ElMessageBox.confirm(`为用户「${row.userName}」重置为默认密码?`, '重置密码', { type: 'warning' });
|
||||
try {
|
||||
await request.put(`/sys/user/${row.id}/reset-password`);
|
||||
ElMessage.success('密码已重置');
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
async function openAssignRole(row) {
|
||||
roleDialogUserId.value = row.id;
|
||||
const res = await request.get(`/sys/user/${row.id}/roles`);
|
||||
userRoles.value = res.data.map(x => x.roleId);
|
||||
roleDialogVisible.value = true;
|
||||
}
|
||||
|
||||
async function handleSaveRoles() {
|
||||
try {
|
||||
const res = await request.get(`/sys/user/${roleDialogUserId.value}/roles`);
|
||||
const existing = res.data.map(x => ({ roleId: x.roleId, id: x.id }));
|
||||
// 移除未选中的
|
||||
for (const e of existing) {
|
||||
if (!userRoles.value.includes(e.roleId)) {
|
||||
await request.delete(`/sys/user/${roleDialogUserId.value}/roles/${e.roleId}`);
|
||||
}
|
||||
}
|
||||
// 新增选中的
|
||||
for (const roleId of userRoles.value) {
|
||||
if (!existing.some(e => e.roleId === roleId)) {
|
||||
await request.post(`/sys/user/${roleDialogUserId.value}/roles`, { roleId });
|
||||
}
|
||||
}
|
||||
ElMessage.success('角色分配成功');
|
||||
roleDialogVisible.value = false;
|
||||
loadList();
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
function handlePageChange(page) { query.page = page; loadList(); }
|
||||
|
||||
onMounted(() => { loadList(); loadDepts(); loadRoles(); });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-card shadow="never">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>用户管理</span>
|
||||
<el-button type="primary" @click="openCreate">新增用户</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="search-bar">
|
||||
<el-input v-model="query.like" placeholder="姓名/账号/手机号" clearable style="width: 200px" @keyup.enter="handleSearch" />
|
||||
<el-tree-select v-model="query.departmentId" :data="deptTree" :props="{ label: 'name', value: 'id', children: 'children' }" check-strictly clearable placeholder="选择部门" style="width: 220px" />
|
||||
<el-button type="primary" @click="handleSearch">搜索</el-button>
|
||||
<el-button @click="resetSearch">重置</el-button>
|
||||
</div>
|
||||
|
||||
<el-table v-loading="loading" :data="list" border stripe>
|
||||
<el-table-column prop="userName" label="姓名" width="120" />
|
||||
<el-table-column prop="loginName" label="账号" width="120" />
|
||||
<el-table-column prop="phoneNumber" label="手机号" width="130" />
|
||||
<el-table-column prop="departmentName" label="部门" width="120" />
|
||||
<el-table-column prop="roleName" label="角色" min-width="140" />
|
||||
<el-table-column prop="lastLoginAt" label="最后登录" width="160" />
|
||||
<el-table-column label="状态" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.isActive ? 'success' : 'danger'">{{ row.isActive ? '启用' : '禁用' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="260" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" type="primary" @click="openEdit(row)">编辑</el-button>
|
||||
<el-button size="small" @click="openAssignRole(row)">角色</el-button>
|
||||
<el-button size="small" @click="handleResetPwd(row)">重置密码</el-button>
|
||||
<el-button size="small" :type="row.isActive ? 'warning' : 'success'" @click="handleActive(row)">
|
||||
{{ row.isActive ? '禁用' : '启用' }}
|
||||
</el-button>
|
||||
<el-button size="small" type="danger" @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-pagination
|
||||
class="pagination"
|
||||
:current-page="query.page"
|
||||
:page-size="query.pageSize"
|
||||
:total="total"
|
||||
layout="total, prev, pager, next"
|
||||
@current-change="handlePageChange"
|
||||
/>
|
||||
|
||||
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="500px">
|
||||
<el-form :model="form" label-width="90px">
|
||||
<el-form-item label="姓名">
|
||||
<el-input v-model="form.userName" placeholder="真实姓名" />
|
||||
</el-form-item>
|
||||
<el-form-item label="登录账号">
|
||||
<el-input v-model="form.loginName" placeholder="登录账号" />
|
||||
</el-form-item>
|
||||
<el-form-item label="手机号">
|
||||
<el-input v-model="form.phoneNumber" placeholder="手机号" />
|
||||
</el-form-item>
|
||||
<el-form-item label="部门">
|
||||
<el-tree-select v-model="form.departmentId" :data="deptTree" :props="{ label: 'name', value: 'id', children: 'children' }" check-strictly clearable placeholder="选择部门" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="排序">
|
||||
<el-input-number v-model="form.sortOrder" :min="0" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleSave">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="roleDialogVisible" title="分配角色" width="400px">
|
||||
<el-select v-model="userRoles" multiple placeholder="选择角色" style="width: 100%">
|
||||
<el-option v-for="r in roles" :key="r.id" :label="r.name" :value="r.id" />
|
||||
</el-select>
|
||||
<template #footer>
|
||||
<el-button @click="roleDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleSaveRoles">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.card-header { display: flex; justify-content: space-between; align-items: center; }
|
||||
.search-bar { display: flex; gap: 12px; margin-bottom: 16px; flex-wrap: wrap; }
|
||||
.pagination { margin-top: 16px; justify-content: flex-end; }
|
||||
</style>
|
||||
@@ -0,0 +1,175 @@
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue';
|
||||
import request from '../../../utils/request';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
|
||||
const treeData = ref([]);
|
||||
const loading = ref(false);
|
||||
const dialogVisible = ref(false);
|
||||
const dialogTitle = ref('新增部门');
|
||||
const isEdit = ref(false);
|
||||
const editId = ref(null);
|
||||
|
||||
const form = reactive({
|
||||
name: '',
|
||||
parentId: null,
|
||||
leaderId: null,
|
||||
phone: '',
|
||||
sort: 0
|
||||
});
|
||||
|
||||
const employees = ref([]);
|
||||
|
||||
async function loadTree() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await request.get('/department/tree');
|
||||
treeData.value = res.data;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadEmployees() {
|
||||
const res = await request.get('/employee', { params: { pageIndex: 1, pageSize: 1000 } });
|
||||
employees.value = res.data.items;
|
||||
}
|
||||
|
||||
function openCreate(parentId = null) {
|
||||
isEdit.value = false;
|
||||
dialogTitle.value = '新增部门';
|
||||
Object.assign(form, { name: '', parentId, leaderId: null, phone: '', sort: 0 });
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
function openEdit(row) {
|
||||
isEdit.value = true;
|
||||
editId.value = row.id;
|
||||
dialogTitle.value = '编辑部门';
|
||||
Object.assign(form, {
|
||||
name: row.name,
|
||||
parentId: row.parentId,
|
||||
leaderId: row.leaderId,
|
||||
phone: row.phone,
|
||||
sort: row.sort
|
||||
});
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
if (!form.name) {
|
||||
ElMessage.warning('请输入部门名称');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (isEdit.value) {
|
||||
await request.put(`/department/${editId.value}`, { ...form, status: 1 });
|
||||
ElMessage.success('更新成功');
|
||||
} else {
|
||||
await request.post('/department', form);
|
||||
ElMessage.success('创建成功');
|
||||
}
|
||||
dialogVisible.value = false;
|
||||
loadTree();
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
async function handleDelete(row) {
|
||||
await ElMessageBox.confirm(`确定删除部门「${row.name}」吗?`, '提示', { type: 'warning' });
|
||||
try {
|
||||
await request.delete(`/department/${row.id}`);
|
||||
ElMessage.success('删除成功');
|
||||
loadTree();
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadTree();
|
||||
loadEmployees();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-card shadow="never">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>部门管理</span>
|
||||
<el-button v-permission="'system:department:btn'" type="primary" @click="openCreate(null)">新增部门</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="treeData"
|
||||
row-key="id"
|
||||
:tree-props="{ children: 'children' }"
|
||||
default-expand-all
|
||||
>
|
||||
<el-table-column prop="name" label="部门名称" min-width="200" />
|
||||
<el-table-column prop="phone" label="联系电话" width="150" />
|
||||
<el-table-column prop="leaderName" label="负责人" width="120" />
|
||||
<el-table-column prop="sort" label="排序" width="80" />
|
||||
<el-table-column label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 1 ? 'success' : 'danger'">
|
||||
{{ row.status === 1 ? '启用' : '禁用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="220" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button v-permission="'system:department:btn'" size="small" @click="openCreate(row.id)">添加子部门</el-button>
|
||||
<el-button v-permission="'system:department:btn'" size="small" type="primary" @click="openEdit(row)">编辑</el-button>
|
||||
<el-button v-permission="'system:department:btn'" size="small" type="danger" @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="500px">
|
||||
<el-form :model="form" label-width="90px">
|
||||
<el-form-item label="部门名称">
|
||||
<el-input v-model="form.name" placeholder="请输入部门名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="上级部门">
|
||||
<el-tree-select
|
||||
v-model="form.parentId"
|
||||
:data="treeData"
|
||||
:props="{ label: 'name', value: 'id', children: 'children' }"
|
||||
check-strictly
|
||||
clearable
|
||||
placeholder="不选则为顶级部门"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="负责人">
|
||||
<el-select v-model="form.leaderId" clearable placeholder="选择负责人" style="width: 100%">
|
||||
<el-option
|
||||
v-for="e in employees"
|
||||
:key="e.id"
|
||||
:label="e.realName"
|
||||
:value="e.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="联系电话">
|
||||
<el-input v-model="form.phone" placeholder="请输入联系电话" />
|
||||
</el-form-item>
|
||||
<el-form-item label="排序">
|
||||
<el-input-number v-model="form.sort" :min="0" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleSave">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,284 @@
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue';
|
||||
import request from '../../../utils/request';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
|
||||
const list = ref([]);
|
||||
const total = ref(0);
|
||||
const loading = ref(false);
|
||||
const deptTree = ref([]);
|
||||
const roles = ref([]);
|
||||
|
||||
const query = reactive({
|
||||
keyword: '',
|
||||
departmentId: null,
|
||||
status: null,
|
||||
pageIndex: 1,
|
||||
pageSize: 10
|
||||
});
|
||||
|
||||
const dialogVisible = ref(false);
|
||||
const dialogTitle = ref('新增职工');
|
||||
const isEdit = ref(false);
|
||||
const editId = ref(null);
|
||||
const form = reactive({
|
||||
username: '',
|
||||
password: '',
|
||||
realName: '',
|
||||
nickname: '',
|
||||
gender: '男',
|
||||
phone: '',
|
||||
email: '',
|
||||
departmentId: null,
|
||||
remark: '',
|
||||
roleIds: []
|
||||
});
|
||||
|
||||
async function loadList() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await request.get('/employee', { params: query });
|
||||
list.value = res.data.items;
|
||||
total.value = res.data.total;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadDepts() {
|
||||
const res = await request.get('/department/tree');
|
||||
deptTree.value = res.data;
|
||||
}
|
||||
|
||||
async function loadRoles() {
|
||||
const res = await request.get('/role');
|
||||
roles.value = res.data;
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
query.pageIndex = 1;
|
||||
loadList();
|
||||
}
|
||||
|
||||
function resetSearch() {
|
||||
Object.assign(query, { keyword: '', departmentId: null, status: null, pageIndex: 1, pageSize: 10 });
|
||||
loadList();
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
isEdit.value = false;
|
||||
dialogTitle.value = '新增职工';
|
||||
Object.assign(form, {
|
||||
username: '', password: '', realName: '', nickname: '', gender: '男',
|
||||
phone: '', email: '', departmentId: null, remark: '', roleIds: []
|
||||
});
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
function openEdit(row) {
|
||||
isEdit.value = true;
|
||||
editId.value = row.id;
|
||||
dialogTitle.value = '编辑职工';
|
||||
Object.assign(form, {
|
||||
username: row.username, password: '', realName: row.realName, nickname: row.nickname,
|
||||
gender: row.gender, phone: row.phone, email: row.email,
|
||||
departmentId: row.departmentId, remark: row.remark, roleIds: [...row.roleIds]
|
||||
});
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
try {
|
||||
if (isEdit.value) {
|
||||
const { username, password, ...updateData } = form;
|
||||
await request.put(`/employee/${editId.value}`, updateData);
|
||||
ElMessage.success('更新成功');
|
||||
} else {
|
||||
if (!form.username || !form.password || !form.realName) {
|
||||
ElMessage.warning('请填写账号、密码、姓名');
|
||||
return;
|
||||
}
|
||||
await request.post('/employee', form);
|
||||
ElMessage.success('创建成功');
|
||||
}
|
||||
dialogVisible.value = false;
|
||||
loadList();
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
async function handleDelete(row) {
|
||||
await ElMessageBox.confirm(`确定删除职工「${row.realName}」吗?`, '提示', { type: 'warning' });
|
||||
try {
|
||||
await request.delete(`/employee/${row.id}`);
|
||||
ElMessage.success('删除成功');
|
||||
loadList();
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
async function handleResetPwd(row) {
|
||||
const { value } = await ElMessageBox.prompt(`为「${row.realName}」重置密码`, '重置密码', {
|
||||
inputType: 'password',
|
||||
inputPattern: /^.{6,}$/,
|
||||
inputErrorMessage: '密码至少6位'
|
||||
});
|
||||
try {
|
||||
await request.put(`/employee/${row.id}/password`, { newPassword: value });
|
||||
ElMessage.success('密码重置成功');
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
function handlePageChange(page) {
|
||||
query.pageIndex = page;
|
||||
loadList();
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadList();
|
||||
loadDepts();
|
||||
loadRoles();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-card shadow="never">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>职工管理</span>
|
||||
<el-button v-permission="'system:employee:btn'" type="primary" @click="openCreate">新增职工</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 搜索栏 -->
|
||||
<div class="search-bar">
|
||||
<el-input v-model="query.keyword" placeholder="姓名/账号/手机号" clearable style="width: 200px" @keyup.enter="handleSearch" />
|
||||
<el-tree-select
|
||||
v-model="query.departmentId"
|
||||
:data="deptTree"
|
||||
:props="{ label: 'name', value: 'id', children: 'children' }"
|
||||
check-strictly
|
||||
clearable
|
||||
placeholder="选择部门"
|
||||
style="width: 200px"
|
||||
/>
|
||||
<el-select v-model="query.status" clearable placeholder="状态" style="width: 120px">
|
||||
<el-option label="在职" :value="1" />
|
||||
<el-option label="离职" :value="0" />
|
||||
</el-select>
|
||||
<el-button type="primary" @click="handleSearch">搜索</el-button>
|
||||
<el-button @click="resetSearch">重置</el-button>
|
||||
</div>
|
||||
|
||||
<el-table v-loading="loading" :data="list" border stripe>
|
||||
<el-table-column prop="username" label="账号" width="120" />
|
||||
<el-table-column prop="realName" label="姓名" width="120" />
|
||||
<el-table-column prop="nickname" label="昵称" width="120" />
|
||||
<el-table-column prop="gender" label="性别" width="70" />
|
||||
<el-table-column prop="phone" label="手机号" width="130" />
|
||||
<el-table-column prop="email" label="邮箱" min-width="160" />
|
||||
<el-table-column prop="departmentName" label="部门" width="120" />
|
||||
<el-table-column label="角色" min-width="140">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-for="r in row.roleNames" :key="r" size="small" class="role-tag">{{ r }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 1 ? 'success' : 'danger'">
|
||||
{{ row.status === 1 ? '在职' : '离职' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="220" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button v-permission="'system:employee:btn'" size="small" type="primary" @click="openEdit(row)">编辑</el-button>
|
||||
<el-button v-permission="'system:employee:btn'" size="small" @click="handleResetPwd(row)">重置密码</el-button>
|
||||
<el-button v-permission="'system:employee:btn'" size="small" type="danger" @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-pagination
|
||||
class="pagination"
|
||||
:current-page="query.pageIndex"
|
||||
:page-size="query.pageSize"
|
||||
:total="total"
|
||||
layout="total, prev, pager, next"
|
||||
@current-change="handlePageChange"
|
||||
/>
|
||||
|
||||
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="600px">
|
||||
<el-form :model="form" label-width="90px">
|
||||
<el-form-item v-if="!isEdit" label="账号">
|
||||
<el-input v-model="form.username" placeholder="登录账号" />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="!isEdit" label="密码">
|
||||
<el-input v-model="form.password" type="password" placeholder="初始密码(至少6位)" show-password />
|
||||
</el-form-item>
|
||||
<el-form-item label="姓名">
|
||||
<el-input v-model="form.realName" placeholder="真实姓名" />
|
||||
</el-form-item>
|
||||
<el-form-item label="昵称">
|
||||
<el-input v-model="form.nickname" placeholder="昵称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="性别">
|
||||
<el-radio-group v-model="form.gender">
|
||||
<el-radio value="男">男</el-radio>
|
||||
<el-radio value="女">女</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="手机号">
|
||||
<el-input v-model="form.phone" placeholder="手机号" />
|
||||
</el-form-item>
|
||||
<el-form-item label="邮箱">
|
||||
<el-input v-model="form.email" placeholder="邮箱" />
|
||||
</el-form-item>
|
||||
<el-form-item label="部门">
|
||||
<el-tree-select
|
||||
v-model="form.departmentId"
|
||||
:data="deptTree"
|
||||
:props="{ label: 'name', value: 'id', children: 'children' }"
|
||||
check-strictly
|
||||
placeholder="选择部门"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="角色">
|
||||
<el-select v-model="form.roleIds" multiple placeholder="分配角色" style="width: 100%">
|
||||
<el-option v-for="r in roles" :key="r.id" :label="r.name" :value="r.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="form.remark" type="textarea" :rows="2" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleSave">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.role-tag {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
margin-top: 16px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,3 @@
|
||||
<template>
|
||||
<div></div>
|
||||
</template>
|
||||
@@ -0,0 +1,14 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
|
||||
const logs = ref([]);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-card shadow="never">
|
||||
<template #header>
|
||||
<span>操作日志</span>
|
||||
</template>
|
||||
<el-empty description="暂无日志记录" />
|
||||
</el-card>
|
||||
</template>
|
||||
@@ -0,0 +1,187 @@
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue';
|
||||
import request from '../../../utils/request';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
|
||||
const treeData = ref([]);
|
||||
const loading = ref(false);
|
||||
|
||||
const dialogVisible = ref(false);
|
||||
const dialogTitle = ref('新增菜单');
|
||||
const isEdit = ref(false);
|
||||
const editId = ref(null);
|
||||
const form = reactive({
|
||||
parentId: null,
|
||||
name: '',
|
||||
type: 'menu',
|
||||
path: '',
|
||||
component: '',
|
||||
icon: '',
|
||||
permission: '',
|
||||
sort: 0
|
||||
});
|
||||
|
||||
const typeOptions = [
|
||||
{ label: '目录', value: 'directory' },
|
||||
{ label: '菜单', value: 'menu' },
|
||||
{ label: '按钮', value: 'button' }
|
||||
];
|
||||
|
||||
async function loadTree() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await request.get('/menu/tree');
|
||||
treeData.value = res.data;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function openCreate(parentId = null) {
|
||||
isEdit.value = false;
|
||||
dialogTitle.value = '新增菜单';
|
||||
Object.assign(form, {
|
||||
parentId, name: '', type: 'menu', path: '', component: '', icon: '', permission: '', sort: 0
|
||||
});
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
function openEdit(row) {
|
||||
isEdit.value = true;
|
||||
editId.value = row.id;
|
||||
dialogTitle.value = '编辑菜单';
|
||||
Object.assign(form, {
|
||||
parentId: row.parentId,
|
||||
name: row.name,
|
||||
type: row.type,
|
||||
path: row.path,
|
||||
component: row.component,
|
||||
icon: row.icon,
|
||||
permission: row.permission,
|
||||
sort: row.sort
|
||||
});
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
if (!form.name) {
|
||||
ElMessage.warning('请输入菜单名称');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (isEdit.value) {
|
||||
await request.put(`/menu/${editId.value}`, { ...form, status: 1 });
|
||||
ElMessage.success('更新成功');
|
||||
} else {
|
||||
await request.post('/menu', form);
|
||||
ElMessage.success('创建成功');
|
||||
}
|
||||
dialogVisible.value = false;
|
||||
loadTree();
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
async function handleDelete(row) {
|
||||
await ElMessageBox.confirm(`确定删除菜单「${row.name}」吗?`, '提示', { type: 'warning' });
|
||||
try {
|
||||
await request.delete(`/menu/${row.id}`);
|
||||
ElMessage.success('删除成功');
|
||||
loadTree();
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
onMounted(loadTree);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-card shadow="never">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>菜单管理</span>
|
||||
<el-button v-permission="'system:menu:btn'" type="primary" @click="openCreate(null)">新增菜单</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="treeData"
|
||||
row-key="id"
|
||||
:tree-props="{ children: 'children' }"
|
||||
default-expand-all
|
||||
>
|
||||
<el-table-column prop="name" label="菜单名称" min-width="180" />
|
||||
<el-table-column label="类型" width="90">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.type === 'directory' ? 'warning' : row.type === 'menu' ? 'primary' : 'info'">
|
||||
{{ row.type === 'directory' ? '目录' : row.type === 'menu' ? '菜单' : '按钮' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="path" label="路由路径" min-width="150" />
|
||||
<el-table-column prop="component" label="组件" min-width="160" />
|
||||
<el-table-column prop="icon" label="图标" width="90" />
|
||||
<el-table-column prop="permission" label="权限标识" min-width="160" />
|
||||
<el-table-column prop="sort" label="排序" width="80" />
|
||||
<el-table-column label="操作" width="220" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button v-if="row.type !== 'button'" v-permission="'system:menu:btn'" size="small" @click="openCreate(row.id)">添加子菜单</el-button>
|
||||
<el-button v-permission="'system:menu:btn'" size="small" type="primary" @click="openEdit(row)">编辑</el-button>
|
||||
<el-button v-permission="'system:menu:btn'" size="small" type="danger" @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="550px">
|
||||
<el-form :model="form" label-width="90px">
|
||||
<el-form-item label="上级菜单">
|
||||
<el-tree-select
|
||||
v-model="form.parentId"
|
||||
:data="treeData"
|
||||
:props="{ label: 'name', value: 'id', children: 'children' }"
|
||||
check-strictly
|
||||
clearable
|
||||
placeholder="不选则为顶级"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="菜单类型">
|
||||
<el-radio-group v-model="form.type">
|
||||
<el-radio-button v-for="t in typeOptions" :key="t.value" :value="t.value">
|
||||
{{ t.label }}
|
||||
</el-radio-button>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="菜单名称">
|
||||
<el-input v-model="form.name" placeholder="菜单名称" />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="form.type !== 'button'" label="路由路径">
|
||||
<el-input v-model="form.path" placeholder="/system/xxx" />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="form.type === 'menu'" label="组件路径">
|
||||
<el-input v-model="form.component" placeholder="system/xxx/index" />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="form.type !== 'button'" label="图标">
|
||||
<el-input v-model="form.icon" placeholder="Element Plus 图标名,如 User" />
|
||||
</el-form-item>
|
||||
<el-form-item label="权限标识">
|
||||
<el-input v-model="form.permission" placeholder="如 system:user:list" />
|
||||
</el-form-item>
|
||||
<el-form-item label="排序">
|
||||
<el-input-number v-model="form.sort" :min="0" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleSave">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,197 @@
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue';
|
||||
import request from '../../../utils/request';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
|
||||
const list = ref([]);
|
||||
const loading = ref(false);
|
||||
const menuTree = ref([]);
|
||||
|
||||
const dialogVisible = ref(false);
|
||||
const dialogTitle = ref('新增角色');
|
||||
const isEdit = ref(false);
|
||||
const editId = ref(null);
|
||||
const form = reactive({
|
||||
name: '',
|
||||
code: '',
|
||||
description: '',
|
||||
sort: 0,
|
||||
menuIds: []
|
||||
});
|
||||
|
||||
// 菜单树 ref
|
||||
const menuTreeRef = ref();
|
||||
|
||||
async function loadList() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await request.get('/role');
|
||||
list.value = res.data;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadMenuTree() {
|
||||
const res = await request.get('/menu/tree');
|
||||
menuTree.value = res.data;
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
isEdit.value = false;
|
||||
dialogTitle.value = '新增角色';
|
||||
Object.assign(form, { name: '', code: '', description: '', sort: 0, menuIds: [] });
|
||||
dialogVisible.value = true;
|
||||
nextTickClear();
|
||||
}
|
||||
|
||||
function openEdit(row) {
|
||||
isEdit.value = true;
|
||||
editId.value = row.id;
|
||||
dialogTitle.value = '编辑角色';
|
||||
Object.assign(form, {
|
||||
name: row.name,
|
||||
code: row.code,
|
||||
description: row.description,
|
||||
sort: row.sort,
|
||||
menuIds: []
|
||||
});
|
||||
dialogVisible.value = true;
|
||||
// 加载该角色已分配的菜单
|
||||
loadRoleMenus(row.id);
|
||||
}
|
||||
|
||||
function nextTickClear() {
|
||||
setTimeout(() => {
|
||||
menuTreeRef.value?.setCheckedKeys([]);
|
||||
}, 100);
|
||||
}
|
||||
|
||||
async function loadRoleMenus(roleId) {
|
||||
const res = await request.get(`/role/${roleId}/menus`);
|
||||
form.menuIds = res.data;
|
||||
setTimeout(() => {
|
||||
menuTreeRef.value?.setCheckedKeys(res.data);
|
||||
}, 100);
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
if (!form.name || !form.code) {
|
||||
ElMessage.warning('请填写角色名称和编码');
|
||||
return;
|
||||
}
|
||||
const menuIds = menuTreeRef.value?.getCheckedKeys() || [];
|
||||
try {
|
||||
if (isEdit.value) {
|
||||
await request.put(`/role/${editId.value}`, { ...form, menuIds });
|
||||
ElMessage.success('更新成功');
|
||||
} else {
|
||||
await request.post('/role', { ...form, menuIds });
|
||||
ElMessage.success('创建成功');
|
||||
}
|
||||
dialogVisible.value = false;
|
||||
loadList();
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
async function handleDelete(row) {
|
||||
await ElMessageBox.confirm(`确定删除角色「${row.name}」吗?`, '提示', { type: 'warning' });
|
||||
try {
|
||||
await request.delete(`/role/${row.id}`);
|
||||
ElMessage.success('删除成功');
|
||||
loadList();
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadList();
|
||||
loadMenuTree();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-card shadow="never">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>角色管理</span>
|
||||
<el-button v-permission="'system:role:btn'" type="primary" @click="openCreate">新增角色</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-table v-loading="loading" :data="list" border stripe>
|
||||
<el-table-column prop="name" label="角色名称" width="160" />
|
||||
<el-table-column prop="code" label="角色编码" width="140" />
|
||||
<el-table-column prop="description" label="描述" min-width="200" />
|
||||
<el-table-column prop="sort" label="排序" width="80" />
|
||||
<el-table-column prop="employeeCount" label="用户数" width="100" />
|
||||
<el-table-column label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 1 ? 'success' : 'danger'">
|
||||
{{ row.status === 1 ? '启用' : '禁用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="160" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button v-permission="'system:role:btn'" size="small" type="primary" @click="openEdit(row)">编辑</el-button>
|
||||
<el-button
|
||||
v-permission="'system:role:btn'"
|
||||
size="small"
|
||||
type="danger"
|
||||
:disabled="row.code === 'admin'"
|
||||
@click="handleDelete(row)"
|
||||
>删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="550px">
|
||||
<el-form :model="form" label-width="90px">
|
||||
<el-form-item label="角色名称">
|
||||
<el-input v-model="form.name" placeholder="角色名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="角色编码">
|
||||
<el-input v-model="form.code" placeholder="角色编码(唯一)" :disabled="isEdit" />
|
||||
</el-form-item>
|
||||
<el-form-item label="描述">
|
||||
<el-input v-model="form.description" type="textarea" :rows="2" placeholder="角色描述" />
|
||||
</el-form-item>
|
||||
<el-form-item label="排序">
|
||||
<el-input-number v-model="form.sort" :min="0" />
|
||||
</el-form-item>
|
||||
<el-form-item label="菜单权限">
|
||||
<el-tree
|
||||
ref="menuTreeRef"
|
||||
:data="menuTree"
|
||||
show-checkbox
|
||||
node-key="id"
|
||||
:props="{ label: 'name', children: 'children' }"
|
||||
default-expand-all
|
||||
class="menu-tree"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleSave">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.menu-tree {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid #eee;
|
||||
border-radius: 4px;
|
||||
padding: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,21 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import vue from '@vitejs/plugin-vue';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
server: {
|
||||
host: '0.0.0.0',
|
||||
port: 5173,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:3000',
|
||||
changeOrigin: true
|
||||
},
|
||||
'/hubs': {
|
||||
target: 'http://localhost:3000',
|
||||
changeOrigin: true,
|
||||
ws: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
Binary file not shown.
Reference in New Issue
Block a user