feat: 登录认证 + 用户/角色/组织架构/审计日志管理页
- 新增登录页、认证 store、路由守卫(未登录跳转 + meta.permissions 校验 + 401 统一处理) - 新增 v-permission 按钮权限指令,Sidebar 按权限过滤菜单,Header 绑定真实用户+改密+退出 - 用户管理:增删改查、启停、重置密码、分配角色、组织树选择、岗位、技能标签 - 角色管理:预定义角色矩阵、权限分配、数据范围(全部/本组织及下级) - 组织架构:固定层级 公司/实验室/部门/班组(白夜班) 树形管理 + 数据权限摘要展示 - 审计日志页:操作人/类型/对象/时间段筛选 + 前后值查看 - 补齐同事模块缺失文件:api/alert.js、utils/format.js(修复构建) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Vendored
+3
-3
@@ -5,11 +5,11 @@
|
|||||||
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>设备管理系统</title>
|
<title>设备管理系统</title>
|
||||||
<script type="module" crossorigin src="/assets/index-BaJKRrhL.js"></script>
|
<script type="module" crossorigin src="/assets/index-BZChQzGB.js"></script>
|
||||||
<link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-hePW80VL.js">
|
<link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-hePW80VL.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/runtime-core.esm-bundler-DZ3WZ9y9.js">
|
<link rel="modulepreload" crossorigin href="/assets/runtime-core.esm-bundler-DZ3WZ9y9.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/message-ddUl4JIs.js">
|
<link rel="modulepreload" crossorigin href="/assets/request-D1PLBK09.js">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-BpBnxyAz.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-C42Vprib.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|||||||
@@ -0,0 +1,116 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
// 后端路由前缀(AlertMessageController / AlertNotifyController)
|
||||||
|
const msgBase = '/inspection/alert-message'
|
||||||
|
const notifyBase = '/inspection/alert-notify'
|
||||||
|
|
||||||
|
// ============ 枚举常量(与后端 Model.Entity.Inspection.AlertEnums 一一对应) ============
|
||||||
|
|
||||||
|
// 告警级别(AlertLevelEnum)
|
||||||
|
export const levelOptions = [
|
||||||
|
{ value: 1, label: '信息' },
|
||||||
|
{ value: 2, label: '警告' },
|
||||||
|
{ value: 3, label: '紧急' }
|
||||||
|
]
|
||||||
|
export const levelLabel = (v) => levelOptions.find(i => i.value === v)?.label || '-'
|
||||||
|
export const levelTag = (v) => ({ 3: 'danger', 2: 'warning' }[v] || 'info')
|
||||||
|
|
||||||
|
// 告警状态(AlertStatusEnum)
|
||||||
|
export const statusOptions = [
|
||||||
|
{ value: 1, label: '未确认' },
|
||||||
|
{ value: 2, label: '已确认' },
|
||||||
|
{ value: 3, label: '已处理' },
|
||||||
|
{ value: 4, label: '已关闭' },
|
||||||
|
{ value: 5, label: '已忽略' },
|
||||||
|
{ value: 6, label: '已转工单' }
|
||||||
|
]
|
||||||
|
export const statusLabel = (v) => statusOptions.find(i => i.value === v)?.label || '-'
|
||||||
|
export const statusTag = (v) => ({ 1: 'danger', 2: 'warning', 3: 'success', 4: 'info', 5: 'info', 6: 'info' }[v] || 'info')
|
||||||
|
|
||||||
|
// 告警类型(AlertTypeEnum)
|
||||||
|
export const typeOptions = [
|
||||||
|
{ value: 1, label: '阈值超限' },
|
||||||
|
{ value: 2, label: '设备离线' },
|
||||||
|
{ value: 3, label: '通讯故障' },
|
||||||
|
{ value: 4, label: '巡检异常' },
|
||||||
|
{ value: 99, label: '自定义' }
|
||||||
|
]
|
||||||
|
export const typeLabel = (v) => typeOptions.find(i => i.value === v)?.label || '-'
|
||||||
|
|
||||||
|
// 告警来源(AlertSourceEnum)
|
||||||
|
export const sourceOptions = [
|
||||||
|
{ value: 1, label: '规则引擎' },
|
||||||
|
{ value: 2, label: '自动巡检' },
|
||||||
|
{ value: 3, label: '数据看板' },
|
||||||
|
{ value: 4, label: '数据采集' },
|
||||||
|
{ value: 5, label: '手动上报' },
|
||||||
|
{ value: 99, label: '其它' }
|
||||||
|
]
|
||||||
|
export const sourceLabel = (v) => sourceOptions.find(i => i.value === v)?.label || '-'
|
||||||
|
|
||||||
|
// 通知渠道类型(NotifyChannelTypeEnum)
|
||||||
|
export const channelTypeOptions = [
|
||||||
|
{ value: 1, label: '飞书' },
|
||||||
|
{ value: 2, label: '钉钉' },
|
||||||
|
{ value: 3, label: '企业微信' }
|
||||||
|
]
|
||||||
|
export const channelLabel = (v) => channelTypeOptions.find(i => i.value === v)?.label || '-'
|
||||||
|
|
||||||
|
// 推送模式(NotifyPushModeEnum)
|
||||||
|
export const pushModeOptions = [
|
||||||
|
{ value: 1, label: '直连推送' },
|
||||||
|
{ value: 2, label: 'Outbox 中转' }
|
||||||
|
]
|
||||||
|
export const pushModeLabel = (v) => pushModeOptions.find(i => i.value === v)?.label || '-'
|
||||||
|
|
||||||
|
// ============ 告警消息 ============
|
||||||
|
|
||||||
|
// 告警列表(分页;level/status/type/source 筛选 + 时间段)
|
||||||
|
export const getAlertList = (params) => request.get(`${msgBase}/list`, { params })
|
||||||
|
|
||||||
|
// 告警详情
|
||||||
|
export const getAlertDetail = (id) => request.get(`${msgBase}/${id}`)
|
||||||
|
|
||||||
|
// 告警操作(确认/处理/关闭/忽略):body = { Operator, Remark }
|
||||||
|
export const ackAlert = (id, data) => request.post(`${msgBase}/${id}/ack`, data)
|
||||||
|
export const handleAlert = (id, data) => request.post(`${msgBase}/${id}/handle`, data)
|
||||||
|
export const closeAlert = (id, data) => request.post(`${msgBase}/${id}/close`, data)
|
||||||
|
export const ignoreAlert = (id, data) => request.post(`${msgBase}/${id}/ignore`, data)
|
||||||
|
|
||||||
|
// 告警概览(未确认数/今日新增/今日紧急/近7日总数)
|
||||||
|
export const getAlertOverview = () => request.get(`${msgBase}/statistics/overview`)
|
||||||
|
|
||||||
|
// 告警趋势(granularity: day/week/month)
|
||||||
|
export const getAlertTrend = (params) => request.get(`${msgBase}/statistics/trend`, { params })
|
||||||
|
|
||||||
|
// 告警分布(dimension: device/type/level/source)
|
||||||
|
export const getAlertDistribution = (params) => request.get(`${msgBase}/statistics/distribution`, { params })
|
||||||
|
|
||||||
|
// 高频告警 TOP
|
||||||
|
export const getAlertTop = (params) => request.get(`${msgBase}/statistics/top`, { params })
|
||||||
|
|
||||||
|
// 处理及时率
|
||||||
|
export const getAlertTimeliness = (params) => request.get(`${msgBase}/statistics/timeliness`, { params })
|
||||||
|
|
||||||
|
// ============ 通知渠道 ============
|
||||||
|
export const getChannels = () => request.get(`${notifyBase}/channel/list`)
|
||||||
|
export const addChannel = (data) => request.post(`${notifyBase}/channel`, data)
|
||||||
|
export const updateChannel = (id, data) => request.put(`${notifyBase}/channel/${id}`, data)
|
||||||
|
export const deleteChannel = (id) => request.delete(`${notifyBase}/channel/${id}`)
|
||||||
|
export const testChannel = (id) => request.post(`${notifyBase}/channel/${id}/test`)
|
||||||
|
|
||||||
|
// ============ 通知规则 ============
|
||||||
|
export const getNotifyRules = () => request.get(`${notifyBase}/rule/list`)
|
||||||
|
export const addNotifyRule = (data) => request.post(`${notifyBase}/rule`, data)
|
||||||
|
export const updateNotifyRule = (id, data) => request.put(`${notifyBase}/rule/${id}`, data)
|
||||||
|
export const deleteNotifyRule = (id) => request.delete(`${notifyBase}/rule/${id}`)
|
||||||
|
export const setRuleEnabled = (id, enabled) => request.put(`${notifyBase}/rule/${id}/enabled`, null, { params: { enabled } })
|
||||||
|
|
||||||
|
// ============ 通知日志 ============
|
||||||
|
export const getNotifyLogs = (params) => request.get(`${notifyBase}/log/list`, { params })
|
||||||
|
|
||||||
|
// ============ 值班排班 ============
|
||||||
|
export const getDuties = (params) => request.get(`${notifyBase}/duty/list`, { params })
|
||||||
|
export const addDuty = (data) => request.post(`${notifyBase}/duty`, data)
|
||||||
|
export const updateDuty = (id, data) => request.put(`${notifyBase}/duty/${id}`, data)
|
||||||
|
export const deleteDuty = (id) => request.delete(`${notifyBase}/duty/${id}`)
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
// 审计日志接口(AuditController: api/system/audit,需 user:manage 权限)
|
||||||
|
const base = '/system/audit'
|
||||||
|
|
||||||
|
// 审计日志列表(分页;筛选:关键字/操作类型/操作对象/时间段;X-Total-Count)
|
||||||
|
export const getAuditLogList = (params) => request.get(`${base}/list`, { params })
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
// 认证接口(AuthController: api/system/auth)
|
||||||
|
const base = '/system/auth'
|
||||||
|
|
||||||
|
// 登录:{ UserName, Password } → { Token, User, Permissions }
|
||||||
|
export const login = (data) => request.post(`${base}/login`, data)
|
||||||
|
|
||||||
|
// 退出登录(记录审计日志)
|
||||||
|
export const logout = () => request.post(`${base}/logout`)
|
||||||
|
|
||||||
|
// 当前登录用户信息 + 权限列表
|
||||||
|
export const getMe = () => request.get(`${base}/me`)
|
||||||
|
|
||||||
|
// 修改自己的密码:{ OldPassword, NewPassword }
|
||||||
|
export const changePassword = (data) => request.post(`${base}/change-password`, data)
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
// 组织架构接口(OrganizationController: api/system/organization,需 user:manage 权限)
|
||||||
|
const base = '/system/organization'
|
||||||
|
|
||||||
|
// 组织类型(OrgType)
|
||||||
|
export const ORG_TYPES = {
|
||||||
|
1: { label: '公司', tag: 'danger', icon: 'OfficeBuilding' },
|
||||||
|
2: { label: '实验室', tag: 'warning', icon: 'School' },
|
||||||
|
3: { label: '部门', tag: 'primary', icon: 'Files' },
|
||||||
|
4: { label: '班组', tag: 'success', icon: 'UserFilled' }
|
||||||
|
}
|
||||||
|
export const orgTypeLabel = (t) => ORG_TYPES[t]?.label || '-'
|
||||||
|
export const orgTypeTag = (t) => ORG_TYPES[t]?.tag || 'info'
|
||||||
|
export const orgTypeOptions = Object.entries(ORG_TYPES).map(([value, o]) => ({ value: Number(value), label: o.label }))
|
||||||
|
|
||||||
|
// 班组班次(ShiftType)
|
||||||
|
export const SHIFT_TYPES = {
|
||||||
|
0: '非班组',
|
||||||
|
1: '白班',
|
||||||
|
2: '夜班'
|
||||||
|
}
|
||||||
|
export const shiftLabel = (s) => SHIFT_TYPES[s] || '-'
|
||||||
|
export const shiftOptions = [
|
||||||
|
{ value: 1, label: '白班' },
|
||||||
|
{ value: 2, label: '夜班' }
|
||||||
|
]
|
||||||
|
|
||||||
|
// 完整组织树(嵌套子级)
|
||||||
|
export const getOrgTree = () => request.get(`${base}/tree`)
|
||||||
|
|
||||||
|
// 组织下拉选项(平铺)
|
||||||
|
export const getOrgOptions = () => request.get(`${base}/options`)
|
||||||
|
|
||||||
|
// 新增组织节点(ParentId 传父级 Id,根节点传 "0")
|
||||||
|
export const addOrg = (data) => request.post(base, data)
|
||||||
|
|
||||||
|
// 修改组织节点
|
||||||
|
export const updateOrg = (data) => request.put(base, data)
|
||||||
|
|
||||||
|
// 删除组织节点(有子级或挂靠用户时不可删)
|
||||||
|
export const deleteOrg = (id) => request.delete(`${base}/${id}`)
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
// 角色权限管理接口(RoleController: api/system/role,需 user:manage 权限)
|
||||||
|
const base = '/system/role'
|
||||||
|
|
||||||
|
// 角色列表(分页)
|
||||||
|
export const getRoleList = (params) => request.get(`${base}/list`, { params })
|
||||||
|
|
||||||
|
// 角色下拉选项
|
||||||
|
export const getRoleOptions = () => request.get(`${base}/options`)
|
||||||
|
|
||||||
|
// 全部权限列表(分配权限用)
|
||||||
|
export const getPermissionList = () => request.get(`${base}/permissions`)
|
||||||
|
|
||||||
|
// 角色详情(含权限Id列表)
|
||||||
|
export const getRoleDetail = (id) => request.get(`${base}/${id}`)
|
||||||
|
|
||||||
|
// 新增角色
|
||||||
|
export const addRole = (data) => request.post(base, data)
|
||||||
|
|
||||||
|
// 修改角色
|
||||||
|
export const updateRole = (data) => request.put(base, data)
|
||||||
|
|
||||||
|
// 删除角色(系统内置角色不可删)
|
||||||
|
export const deleteRole = (id) => request.delete(`${base}/${id}`)
|
||||||
|
|
||||||
|
// 分配权限(全量覆盖)
|
||||||
|
export const assignRolePermissions = (id, permissionIds) => request.post(`${base}/${id}/permissions`, permissionIds)
|
||||||
|
|
||||||
|
// 数据范围常量(DataScope:1=全部, 2=本组织及下级)
|
||||||
|
export const DATA_SCOPES = {
|
||||||
|
1: { label: '全部', tag: 'danger' },
|
||||||
|
2: { label: '本组织及下级', tag: 'warning' }
|
||||||
|
}
|
||||||
|
export const dataScopeLabel = (s) => DATA_SCOPES[s]?.label || '-'
|
||||||
|
export const dataScopeTag = (s) => DATA_SCOPES[s]?.tag || 'info'
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
// 用户管理接口(UserController: api/system/user,需 user:manage 权限)
|
||||||
|
const base = '/system/user'
|
||||||
|
|
||||||
|
// 用户列表(分页;关键字/角色筛选;X-Total-Count)
|
||||||
|
export const getUserList = (params) => request.get(`${base}/list`, { params })
|
||||||
|
|
||||||
|
// 用户下拉选项(启用中的用户)
|
||||||
|
export const getUserOptions = () => request.get(`${base}/options`)
|
||||||
|
|
||||||
|
// 用户详情
|
||||||
|
export const getUserDetail = (id) => request.get(`${base}/${id}`)
|
||||||
|
|
||||||
|
// 新增用户:{ UserName, RealName, InitialPassword, RoleIds, ... }
|
||||||
|
export const addUser = (data) => request.post(base, data)
|
||||||
|
|
||||||
|
// 修改用户(RoleIds=null 不改角色)
|
||||||
|
export const updateUser = (data) => request.put(base, data)
|
||||||
|
|
||||||
|
// 删除用户(软删除)
|
||||||
|
export const deleteUser = (id) => request.delete(`${base}/${id}`)
|
||||||
|
|
||||||
|
// 分配角色(全量覆盖)
|
||||||
|
export const assignUserRoles = (id, roleIds) => request.post(`${base}/${id}/roles`, roleIds)
|
||||||
|
|
||||||
|
// 重置密码
|
||||||
|
export const resetUserPassword = (id, newPassword) => request.post(`${base}/${id}/reset-password`, { NewPassword: newPassword })
|
||||||
|
|
||||||
|
// 启用/禁用
|
||||||
|
export const setUserEnabled = (id, enabled) => request.post(`${base}/${id}/enabled`, { Enabled: enabled })
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { useUserStore } from '@/store/user'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按钮级权限指令:v-permission="'device:edit'" 或 v-permission="['device:edit','device:control']"
|
||||||
|
* 无权限时移除元素(与 v-if 语义一致)
|
||||||
|
*/
|
||||||
|
export const permission = {
|
||||||
|
mounted(el, binding) {
|
||||||
|
const userStore = useUserStore()
|
||||||
|
const value = binding.value
|
||||||
|
const required = Array.isArray(value) ? value : [value]
|
||||||
|
if (!userStore.hasAnyPermission(required)) {
|
||||||
|
el.parentNode?.removeChild(el)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setupDirectives(app) {
|
||||||
|
app.directive('permission', permission)
|
||||||
|
}
|
||||||
+94
-6
@@ -13,34 +13,113 @@
|
|||||||
</el-breadcrumb>
|
</el-breadcrumb>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-right">
|
<div class="header-right">
|
||||||
<el-dropdown>
|
<el-dropdown @command="onCommand">
|
||||||
<span class="user-info">
|
<span class="user-info">
|
||||||
<el-icon><User /></el-icon>
|
<el-icon><User /></el-icon>
|
||||||
<span>管理员</span>
|
<span>{{ userStore.displayName }}</span>
|
||||||
|
<el-tag v-if="userStore.roleNames" size="small" type="info" class="role-tag">{{ userStore.roleNames }}</el-tag>
|
||||||
</span>
|
</span>
|
||||||
<template #dropdown>
|
<template #dropdown>
|
||||||
<el-dropdown-menu>
|
<el-dropdown-menu>
|
||||||
<el-dropdown-item>个人中心</el-dropdown-item>
|
<el-dropdown-item disabled>
|
||||||
<el-dropdown-item divided>退出登录</el-dropdown-item>
|
<span class="dropdown-username">{{ userStore.userInfo?.UserName }}</span>
|
||||||
|
</el-dropdown-item>
|
||||||
|
<el-dropdown-item command="change-password">修改密码</el-dropdown-item>
|
||||||
|
<el-dropdown-item command="logout" divided>退出登录</el-dropdown-item>
|
||||||
</el-dropdown-menu>
|
</el-dropdown-menu>
|
||||||
</template>
|
</template>
|
||||||
</el-dropdown>
|
</el-dropdown>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 修改密码对话框 -->
|
||||||
|
<el-dialog v-model="pwdVisible" title="修改密码" width="420px" destroy-on-close>
|
||||||
|
<el-form ref="pwdFormRef" :model="pwdForm" :rules="pwdRules" label-width="80px">
|
||||||
|
<el-form-item label="原密码" prop="OldPassword">
|
||||||
|
<el-input v-model="pwdForm.OldPassword" type="password" show-password />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="新密码" prop="NewPassword">
|
||||||
|
<el-input v-model="pwdForm.NewPassword" type="password" show-password />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="确认密码" prop="Confirm">
|
||||||
|
<el-input v-model="pwdForm.Confirm" type="password" show-password />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="pwdVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" :loading="pwdSaving" @click="onChangePassword">确定</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from 'vue'
|
import { ref, reactive, computed } from 'vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
import { Fold, Expand, User } from '@element-plus/icons-vue'
|
import { Fold, Expand, User } from '@element-plus/icons-vue'
|
||||||
import { useAppStore } from '@/store/app'
|
import { useAppStore } from '@/store/app'
|
||||||
|
import { useUserStore } from '@/store/user'
|
||||||
|
import { changePassword } from '@/api/auth'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
const appStore = useAppStore()
|
const appStore = useAppStore()
|
||||||
|
const userStore = useUserStore()
|
||||||
|
|
||||||
const breadcrumbs = computed(() => {
|
const breadcrumbs = computed(() => {
|
||||||
return route.matched.filter(item => item.meta?.title)
|
return route.matched.filter(item => item.meta?.title)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// ---------- 下拉命令 ----------
|
||||||
|
function onCommand(cmd) {
|
||||||
|
if (cmd === 'logout') onLogout()
|
||||||
|
else if (cmd === 'change-password') pwdVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onLogout() {
|
||||||
|
await userStore.logout()
|
||||||
|
router.replace('/login')
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 修改密码 ----------
|
||||||
|
const pwdVisible = ref(false)
|
||||||
|
const pwdSaving = ref(false)
|
||||||
|
const pwdFormRef = ref(null)
|
||||||
|
const pwdForm = reactive({ OldPassword: '', NewPassword: '', Confirm: '' })
|
||||||
|
|
||||||
|
const pwdRules = {
|
||||||
|
OldPassword: [{ required: true, message: '请输入原密码', trigger: 'blur' }],
|
||||||
|
NewPassword: [
|
||||||
|
{ required: true, message: '请输入新密码', trigger: 'blur' },
|
||||||
|
{ min: 6, message: '新密码长度不能少于 6 位', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
Confirm: [
|
||||||
|
{ required: true, message: '请再次输入新密码', trigger: 'blur' },
|
||||||
|
{
|
||||||
|
validator: (rule, value, callback) => {
|
||||||
|
if (value !== pwdForm.NewPassword) callback(new Error('两次输入的密码不一致'))
|
||||||
|
else callback()
|
||||||
|
},
|
||||||
|
trigger: 'blur'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onChangePassword() {
|
||||||
|
await pwdFormRef.value.validate()
|
||||||
|
pwdSaving.value = true
|
||||||
|
try {
|
||||||
|
await changePassword({ OldPassword: pwdForm.OldPassword, NewPassword: pwdForm.NewPassword })
|
||||||
|
ElMessage.success('密码修改成功,请重新登录')
|
||||||
|
pwdVisible.value = false
|
||||||
|
await userStore.logout()
|
||||||
|
router.replace('/login')
|
||||||
|
} catch {
|
||||||
|
/* 拦截器已提示 */
|
||||||
|
} finally {
|
||||||
|
pwdSaving.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -84,4 +163,13 @@ const breadcrumbs = computed(() => {
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #606266;
|
color: #606266;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.role-tag {
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-username {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
+14
-4
@@ -39,8 +39,7 @@
|
|||||||
<template #title>{{ child.meta?.title }}</template>
|
<template #title>{{ child.meta?.title }}</template>
|
||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
</el-sub-menu>
|
</el-sub-menu>
|
||||||
</template>
|
</template> </el-menu>
|
||||||
</el-menu>
|
|
||||||
</el-scrollbar>
|
</el-scrollbar>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -49,20 +48,31 @@
|
|||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { useAppStore } from '@/store/app'
|
import { useAppStore } from '@/store/app'
|
||||||
|
import { useUserStore } from '@/store/user'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const appStore = useAppStore()
|
const appStore = useAppStore()
|
||||||
|
const userStore = useUserStore()
|
||||||
|
|
||||||
const activeMenu = computed(() => route.path)
|
const activeMenu = computed(() => route.path)
|
||||||
|
|
||||||
|
// 按权限过滤菜单:meta.permissions 任一满足才显示;无权限标记的菜单登录即可见
|
||||||
|
function hasPerm(meta) {
|
||||||
|
const required = meta?.permissions
|
||||||
|
if (!required || required.length === 0) return true
|
||||||
|
return userStore.hasAnyPermission(required)
|
||||||
|
}
|
||||||
|
|
||||||
const menuRoutes = computed(() => {
|
const menuRoutes = computed(() => {
|
||||||
return router.options.routes
|
return router.options.routes
|
||||||
.filter(r => r.component && r.path !== '/:pathMatch(.*)*')
|
.filter(r => r.component && r.path !== '/:pathMatch(.*)*' && !r.meta?.public)
|
||||||
.map(r => ({
|
.map(r => ({
|
||||||
...r,
|
...r,
|
||||||
children: (r.children || []).filter(c => !c.path.includes(':'))
|
children: (r.children || []).filter(c => !c.path.includes(':') && hasPerm(c.meta))
|
||||||
}))
|
}))
|
||||||
|
// 父级自身也要过权限;子菜单全被过滤掉的分组隐藏
|
||||||
|
.filter(r => hasPerm(r.meta) && (!r.children || r.children.length > 0))
|
||||||
})
|
})
|
||||||
|
|
||||||
function resolvePath(basePath, childPath) {
|
function resolvePath(basePath, childPath) {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import 'element-plus/dist/index.css'
|
|||||||
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
||||||
import router from './router'
|
import router from './router'
|
||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
|
import { setupDirectives } from './directives/permission'
|
||||||
|
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
|
|
||||||
@@ -14,6 +15,7 @@ for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
app.use(createPinia())
|
app.use(createPinia())
|
||||||
|
setupDirectives(app)
|
||||||
app.use(router)
|
app.use(router)
|
||||||
app.use(ElementPlus)
|
app.use(ElementPlus)
|
||||||
app.mount('#app')
|
app.mount('#app')
|
||||||
|
|||||||
+52
-8
@@ -2,6 +2,12 @@ import { createRouter, createWebHistory } from 'vue-router'
|
|||||||
import Layout from '@/layout/Layout.vue'
|
import Layout from '@/layout/Layout.vue'
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
|
{
|
||||||
|
path: '/login',
|
||||||
|
name: 'Login',
|
||||||
|
component: () => import('@/views/login/Index.vue'),
|
||||||
|
meta: { title: '登录', public: true }
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/',
|
path: '/',
|
||||||
component: Layout,
|
component: Layout,
|
||||||
@@ -143,7 +149,7 @@ const routes = [
|
|||||||
path: 'patrol',
|
path: 'patrol',
|
||||||
name: 'PatrolManage',
|
name: 'PatrolManage',
|
||||||
component: () => import('@/views/inspection/patrol/Index.vue'),
|
component: () => import('@/views/inspection/patrol/Index.vue'),
|
||||||
meta: { title: '巡检管理', icon: 'Guide' }
|
meta: { title: '巡检管理', icon: 'Guide', permissions: ['inspection:manage'] }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'patrol/template',
|
path: 'patrol/template',
|
||||||
@@ -200,25 +206,25 @@ const routes = [
|
|||||||
path: 'product',
|
path: 'product',
|
||||||
name: 'ProductManage',
|
name: 'ProductManage',
|
||||||
component: () => import('@/views/config/product/Index.vue'),
|
component: () => import('@/views/config/product/Index.vue'),
|
||||||
meta: { title: '产品管理', icon: 'Goods' }
|
meta: { title: '产品管理', icon: 'Goods', permissions: ['device:view'] }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'product/category',
|
path: 'product/category',
|
||||||
name: 'ProductCategory',
|
name: 'ProductCategory',
|
||||||
component: () => import('@/views/config/product/Category.vue'),
|
component: () => import('@/views/config/product/Category.vue'),
|
||||||
meta: { title: '产品分类', icon: 'Menu' }
|
meta: { title: '产品分类', icon: 'Menu', permissions: ['device:view'] }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'device',
|
path: 'device',
|
||||||
name: 'DeviceManage',
|
name: 'DeviceManage',
|
||||||
component: () => import('@/views/config/device/Index.vue'),
|
component: () => import('@/views/config/device/Index.vue'),
|
||||||
meta: { title: '设备管理', icon: 'Cpu' }
|
meta: { title: '设备管理', icon: 'Cpu', permissions: ['device:view'] }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'gateway',
|
path: 'gateway',
|
||||||
name: 'GatewayManage',
|
name: 'GatewayManage',
|
||||||
component: () => import('@/views/config/gateway/Index.vue'),
|
component: () => import('@/views/config/gateway/Index.vue'),
|
||||||
meta: { title: '网关管理', icon: 'Connection' }
|
meta: { title: '网关管理', icon: 'Connection', permissions: ['device:view'] }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'protocol/certificate',
|
path: 'protocol/certificate',
|
||||||
@@ -287,13 +293,13 @@ const routes = [
|
|||||||
path: 'role',
|
path: 'role',
|
||||||
name: 'RoleManage',
|
name: 'RoleManage',
|
||||||
component: () => import('@/views/system/role/Index.vue'),
|
component: () => import('@/views/system/role/Index.vue'),
|
||||||
meta: { title: '角色权限管理', icon: 'Lock' }
|
meta: { title: '角色权限管理', icon: 'Lock', permissions: ['user:manage'] }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'audit',
|
path: 'audit',
|
||||||
name: 'AuditLog',
|
name: 'AuditLog',
|
||||||
component: () => import('@/views/system/audit/Index.vue'),
|
component: () => import('@/views/system/audit/Index.vue'),
|
||||||
meta: { title: '操作审计日志', icon: 'Notebook' }
|
meta: { title: '操作审计日志', icon: 'Notebook', permissions: ['user:manage'] }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'organization',
|
path: 'organization',
|
||||||
@@ -305,7 +311,7 @@ const routes = [
|
|||||||
path: 'user',
|
path: 'user',
|
||||||
name: 'UserManage',
|
name: 'UserManage',
|
||||||
component: () => import('@/views/system/user/Index.vue'),
|
component: () => import('@/views/system/user/Index.vue'),
|
||||||
meta: { title: '用户管理', icon: 'User' }
|
meta: { title: '用户管理', icon: 'User', permissions: ['user:manage'] }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'dict',
|
path: 'dict',
|
||||||
@@ -352,4 +358,42 @@ const router = createRouter({
|
|||||||
routes
|
routes
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// ---------- 全局守卫:登录校验 + 权限校验 ----------
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { useUserStore } from '@/store/user'
|
||||||
|
|
||||||
|
router.beforeEach(async (to) => {
|
||||||
|
const userStore = useUserStore()
|
||||||
|
|
||||||
|
// 公开页面(登录页)直接放行;已登录访问登录页则回首页
|
||||||
|
if (to.meta.public) {
|
||||||
|
if (userStore.isLoggedIn && to.path === '/login') return '/'
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 未登录 → 跳登录页(带上回跳地址)
|
||||||
|
if (!userStore.isLoggedIn) {
|
||||||
|
return { path: '/login', query: { redirect: to.fullPath } }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 已登录但权限未加载(页面刷新场景)→ 拉取当前用户
|
||||||
|
if (userStore.permissions.length === 0 && !userStore.userInfo) {
|
||||||
|
try {
|
||||||
|
await userStore.fetchMe()
|
||||||
|
} catch {
|
||||||
|
userStore.reset()
|
||||||
|
return { path: '/login', query: { redirect: to.fullPath } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 路由级权限校验(meta.permissions 任一满足即放行)
|
||||||
|
const required = to.meta.permissions
|
||||||
|
if (required && required.length > 0 && !userStore.hasAnyPermission(required)) {
|
||||||
|
ElMessage.warning('没有访问该页面的权限')
|
||||||
|
return '/'
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
export default router
|
export default router
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { login as loginApi, logout as logoutApi, getMe } from '@/api/auth'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 认证状态:token / 用户信息 / 权限列表(登录成功后由后端签发,权限随 JWT Claims 校验)
|
||||||
|
*/
|
||||||
|
export const useUserStore = defineStore('user', () => {
|
||||||
|
const token = ref(localStorage.getItem('token') || '')
|
||||||
|
const userInfo = ref(null)
|
||||||
|
const permissions = ref([])
|
||||||
|
|
||||||
|
const isLoggedIn = computed(() => !!token.value)
|
||||||
|
const displayName = computed(() => {
|
||||||
|
const u = userInfo.value
|
||||||
|
return u?.RealName || u?.UserName || '未登录'
|
||||||
|
})
|
||||||
|
const roleNames = computed(() => userInfo.value?.RoleNames || '')
|
||||||
|
|
||||||
|
function hasPermission(code) {
|
||||||
|
return permissions.value.includes(code) || permissions.value.includes('*')
|
||||||
|
}
|
||||||
|
function hasAnyPermission(codes) {
|
||||||
|
if (!codes || codes.length === 0) return true
|
||||||
|
return codes.some((c) => hasPermission(c))
|
||||||
|
}
|
||||||
|
|
||||||
|
async function login(form) {
|
||||||
|
const res = await loginApi(form)
|
||||||
|
token.value = res.Token
|
||||||
|
userInfo.value = res.User
|
||||||
|
permissions.value = res.Permissions || []
|
||||||
|
localStorage.setItem('token', res.Token)
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchMe() {
|
||||||
|
const res = await getMe()
|
||||||
|
userInfo.value = res.User
|
||||||
|
permissions.value = res.Permissions || []
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
async function logout() {
|
||||||
|
try { await logoutApi() } catch { /* 审计失败不影响退出 */ }
|
||||||
|
reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
function reset() {
|
||||||
|
token.value = ''
|
||||||
|
userInfo.value = null
|
||||||
|
permissions.value = []
|
||||||
|
localStorage.removeItem('token')
|
||||||
|
}
|
||||||
|
|
||||||
|
return { token, userInfo, permissions, isLoggedIn, displayName, roleNames, hasPermission, hasAnyPermission, login, fetchMe, logout, reset }
|
||||||
|
})
|
||||||
+22
-40
@@ -1,60 +1,42 @@
|
|||||||
/**
|
/**
|
||||||
* 通用格式化工具(后端返回的日期为 ISO 字符串,如 "2026-08-28T10:30:00")
|
* 通用格式化工具(asset 台账视图与告警页共用)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// 补零:5 → "05"
|
// 日期:YYYY-MM-DD(兼容 ISO 字符串 / Date)
|
||||||
const pad = n => String(n).padStart(2, '0')
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 格式化为日期:2026-08-28
|
|
||||||
* @param {string|Date} value 日期/时间值
|
|
||||||
* @returns {string} 空值返回 ''
|
|
||||||
*/
|
|
||||||
export function fmtDate(value) {
|
export function fmtDate(value) {
|
||||||
if (!value) return ''
|
if (!value) return '-'
|
||||||
const d = new Date(value)
|
const d = new Date(value)
|
||||||
if (isNaN(d.getTime())) return String(value).slice(0, 10)
|
if (Number.isNaN(d.getTime())) return String(value)
|
||||||
|
const pad = (n) => String(n).padStart(2, '0')
|
||||||
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`
|
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// 日期时间:YYYY-MM-DD HH:mm:ss
|
||||||
* 格式化为日期时间:2026-08-28 10:30:00
|
|
||||||
* @param {string|Date} value 日期/时间值
|
|
||||||
* @returns {string} 空值返回 ''
|
|
||||||
*/
|
|
||||||
export function fmtDateTime(value) {
|
export function fmtDateTime(value) {
|
||||||
if (!value) return ''
|
if (!value) return '-'
|
||||||
const d = new Date(value)
|
const d = new Date(value)
|
||||||
if (isNaN(d.getTime())) return String(value).replace('T', ' ').slice(0, 19)
|
if (Number.isNaN(d.getTime())) return String(value)
|
||||||
|
const pad = (n) => String(n).padStart(2, '0')
|
||||||
return `${fmtDate(d)} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
|
return `${fmtDate(d)} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// 文件大小(字节 → 可读单位)
|
||||||
* 格式化文件大小:1024 → "1.00 KB"
|
|
||||||
* @param {number} bytes 字节数
|
|
||||||
* @returns {string} 空值返回 ''
|
|
||||||
*/
|
|
||||||
export function fmtFileSize(bytes) {
|
export function fmtFileSize(bytes) {
|
||||||
if (bytes === null || bytes === undefined || isNaN(bytes)) return ''
|
if (bytes == null || bytes === '' || Number.isNaN(Number(bytes))) return '-'
|
||||||
if (bytes < 1024) return `${bytes} B`
|
const n = Number(bytes)
|
||||||
const units = ['KB', 'MB', 'GB', 'TB']
|
if (n < 1024) return `${n} B`
|
||||||
let size = bytes
|
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`
|
||||||
let i = -1
|
if (n < 1024 * 1024 * 1024) return `${(n / 1024 / 1024).toFixed(1)} MB`
|
||||||
do {
|
return `${(n / 1024 / 1024 / 1024).toFixed(2)} GB`
|
||||||
size /= 1024
|
|
||||||
i++
|
|
||||||
} while (size >= 1024 && i < units.length - 1)
|
|
||||||
return `${size.toFixed(2)} ${units[i]}`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// 是否已逾期(日期早于今天且非空)
|
||||||
* 判断日期是否已超期(小于当前日期)
|
|
||||||
* @param {string|Date} value 日期值
|
|
||||||
* @returns {boolean}
|
|
||||||
*/
|
|
||||||
export function isOverdue(value) {
|
export function isOverdue(value) {
|
||||||
if (!value) return false
|
if (!value) return false
|
||||||
const d = new Date(value)
|
const d = new Date(value)
|
||||||
if (isNaN(d.getTime())) return false
|
if (Number.isNaN(d.getTime())) return false
|
||||||
return d < new Date()
|
const today = new Date()
|
||||||
|
today.setHours(0, 0, 0, 0)
|
||||||
|
d.setHours(0, 0, 0, 0)
|
||||||
|
return d.getTime() < today.getTime()
|
||||||
}
|
}
|
||||||
|
|||||||
+19
-1
@@ -34,7 +34,25 @@ request.interceptors.response.use(
|
|||||||
return data
|
return data
|
||||||
},
|
},
|
||||||
error => {
|
error => {
|
||||||
ElMessage.error(error.message || '网络错误')
|
const status = error.response?.status
|
||||||
|
if (status === 401) {
|
||||||
|
// 未登录 / Token 失效:清凭证并跳登录页(避免死循环用 sessionStorage 标记只提示一次)
|
||||||
|
if (!sessionStorage.getItem('authRedirecting')) {
|
||||||
|
sessionStorage.setItem('authRedirecting', '1')
|
||||||
|
ElMessage.error('登录已失效,请重新登录')
|
||||||
|
localStorage.removeItem('token')
|
||||||
|
const redirect = window.location.pathname + window.location.search
|
||||||
|
window.location.href = `/login?redirect=${encodeURIComponent(redirect)}`
|
||||||
|
}
|
||||||
|
return Promise.reject(error)
|
||||||
|
}
|
||||||
|
if (status === 403) {
|
||||||
|
ElMessage.error(error.response?.data?.Msg || '没有操作权限')
|
||||||
|
return Promise.reject(error)
|
||||||
|
}
|
||||||
|
// 后端统一返回 { Code, Msg, Data },HTTP 200 但业务失败时错误体也带 Msg
|
||||||
|
const msg = error.response?.data?.Msg || error.message || '网络错误'
|
||||||
|
ElMessage.error(msg)
|
||||||
return Promise.reject(error)
|
return Promise.reject(error)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -70,8 +70,8 @@
|
|||||||
<el-table-column label="操作" width="150" fixed="right">
|
<el-table-column label="操作" width="150" fixed="right">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-button link type="primary" @click.stop="openDetail(row)">详情</el-button>
|
<el-button link type="primary" @click.stop="openDetail(row)">详情</el-button>
|
||||||
<el-button link type="primary" @click.stop="openEdit(row)">编辑</el-button>
|
<el-button v-permission="'device:edit'" link type="primary" @click.stop="openEdit(row)">编辑</el-button>
|
||||||
<el-dropdown trigger="click" @command="(cmd) => onMoreCommand(cmd, row)">
|
<el-dropdown v-permission="'device:edit'" trigger="click" @command="(cmd) => onMoreCommand(cmd, row)">
|
||||||
<el-button link type="primary" @click.stop>
|
<el-button link type="primary" @click.stop>
|
||||||
更多<el-icon class="more-arrow"><ArrowDown /></el-icon>
|
更多<el-icon class="more-arrow"><ArrowDown /></el-icon>
|
||||||
</el-button>
|
</el-button>
|
||||||
@@ -96,13 +96,13 @@
|
|||||||
<div class="ctx-menu__item" @click="ctxDetail">
|
<div class="ctx-menu__item" @click="ctxDetail">
|
||||||
<el-icon><View /></el-icon><span>设备详情</span>
|
<el-icon><View /></el-icon><span>设备详情</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="ctx-menu__item" @click="ctxEdit">
|
<div class="ctx-menu__item" v-permission="'device:edit'" @click="ctxEdit">
|
||||||
<el-icon><Edit /></el-icon><span>修改设备</span>
|
<el-icon><Edit /></el-icon><span>修改设备</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="ctx-menu__item" @click="ctxLogs">
|
<div class="ctx-menu__item" @click="ctxLogs">
|
||||||
<el-icon><Document /></el-icon><span>设备日志</span>
|
<el-icon><Document /></el-icon><span>设备日志</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="ctx-menu__item danger" @click="ctxDelete">
|
<div class="ctx-menu__item danger" v-permission="'device:edit'" @click="ctxDelete">
|
||||||
<el-icon><Delete /></el-icon><span>删除设备</span>
|
<el-icon><Delete /></el-icon><span>删除设备</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -196,7 +196,7 @@
|
|||||||
<el-tab-pane label="告警" name="alarm">
|
<el-tab-pane label="告警" name="alarm">
|
||||||
<ThingAlarmPanel :owner-type="2" :owner-id="detailDevice.Id" />
|
<ThingAlarmPanel :owner-type="2" :owner-id="detailDevice.Id" />
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
<el-tab-pane label="下发指令" name="command">
|
<el-tab-pane v-permission="'device:control'" label="下发指令" name="command">
|
||||||
<PointSendForm ref="pointSendRef" :device-id="detailDevice.Id" />
|
<PointSendForm ref="pointSendRef" :device-id="detailDevice.Id" />
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
<el-tab-pane label="看板" name="board">
|
<el-tab-pane label="看板" name="board">
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
<template>
|
||||||
|
<div class="login-page">
|
||||||
|
<div class="login-card">
|
||||||
|
<div class="login-title">苏试试验设备管理平台</div>
|
||||||
|
<div class="login-subtitle">Suzhou Testing Equipment Management Platform</div>
|
||||||
|
<el-form ref="formRef" :model="form" :rules="formRules" size="large" @keyup.enter="onSubmit">
|
||||||
|
<el-form-item prop="UserName">
|
||||||
|
<el-input v-model="form.UserName" placeholder="用户名" :prefix-icon="User" clearable />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item prop="Password">
|
||||||
|
<el-input v-model="form.Password" type="password" placeholder="密码" :prefix-icon="Lock" show-password />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" class="login-btn" :loading="loading" @click="onSubmit">登 录</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<div class="login-tip">默认管理员:admin / admin123</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive } from 'vue'
|
||||||
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { User, Lock } from '@element-plus/icons-vue'
|
||||||
|
import { useUserStore } from '@/store/user'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const route = useRoute()
|
||||||
|
const userStore = useUserStore()
|
||||||
|
|
||||||
|
// 清除 401 重定向标记(request.js 里 401 时设置)
|
||||||
|
sessionStorage.removeItem('authRedirecting')
|
||||||
|
|
||||||
|
const formRef = ref(null)
|
||||||
|
const loading = ref(false)
|
||||||
|
const form = reactive({ UserName: '', Password: '' })
|
||||||
|
|
||||||
|
const formRules = {
|
||||||
|
UserName: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
|
||||||
|
Password: [{ required: true, message: '请输入密码', trigger: 'blur' }]
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onSubmit() {
|
||||||
|
await formRef.value.validate()
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
await userStore.login({ UserName: form.UserName, Password: form.Password })
|
||||||
|
ElMessage.success('登录成功')
|
||||||
|
const redirect = route.query.redirect || '/'
|
||||||
|
router.replace(redirect)
|
||||||
|
} catch {
|
||||||
|
/* 拦截器已提示 */
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.login-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: linear-gradient(135deg, #1f2d3d 0%, #2b3a4e 50%, #32475e 100%);
|
||||||
|
}
|
||||||
|
.login-card {
|
||||||
|
width: 380px;
|
||||||
|
padding: 40px 36px 28px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 8px 40px rgba(0, 0, 0, 0.25);
|
||||||
|
}
|
||||||
|
.login-title {
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-align: center;
|
||||||
|
color: #303133;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
.login-subtitle {
|
||||||
|
font-size: 12px;
|
||||||
|
text-align: center;
|
||||||
|
color: #909399;
|
||||||
|
margin-bottom: 28px;
|
||||||
|
}
|
||||||
|
.login-btn {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.login-tip {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #c0c4cc;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -2,13 +2,142 @@
|
|||||||
<div class="page-container">
|
<div class="page-container">
|
||||||
<el-card>
|
<el-card>
|
||||||
<template #header>
|
<template #header>
|
||||||
<span>操作审计日志</span>
|
<div class="card-header">
|
||||||
|
<span>操作审计日志(共 {{ total }} 条)</span>
|
||||||
|
<el-button link type="primary" @click="fetchList">刷新</el-button>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<el-empty description="操作审计日志 - 功能开发中" />
|
|
||||||
|
<div class="search-bar">
|
||||||
|
<el-input v-model="keyword" placeholder="操作人 / 描述" clearable style="width: 200px"
|
||||||
|
@keyup.enter="onSearch" @clear="onSearch">
|
||||||
|
<template #prefix><el-icon><Search /></el-icon></template>
|
||||||
|
</el-input>
|
||||||
|
<el-select v-model="searchType" placeholder="操作类型(全部)" clearable style="width: 140px" @change="onSearch">
|
||||||
|
<el-option v-for="(label, val) in OPERATION_TYPES" :key="val" :label="label" :value="val" />
|
||||||
|
</el-select>
|
||||||
|
<el-select v-model="searchTarget" placeholder="操作对象(全部)" clearable style="width: 140px" @change="onSearch">
|
||||||
|
<el-option v-for="t in OPERATION_TARGETS" :key="t" :label="t" :value="t" />
|
||||||
|
</el-select>
|
||||||
|
<el-date-picker v-model="dateRange" type="datetimerange" start-placeholder="开始时间" end-placeholder="结束时间"
|
||||||
|
value-format="YYYY-MM-DDTHH:mm:ss" style="width: 340px" @change="onSearch" />
|
||||||
|
<el-button type="primary" @click="onSearch">查询</el-button>
|
||||||
|
<el-button @click="onReset">重置</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-table :data="list" v-loading="loading" stripe border row-key="Id">
|
||||||
|
<el-table-column label="操作时间" width="165">
|
||||||
|
<template #default="{ row }">{{ fmtTime(row.OperateTime) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="UserName" label="操作人" width="110" />
|
||||||
|
<el-table-column prop="RoleName" label="角色" width="120" show-overflow-tooltip />
|
||||||
|
<el-table-column label="操作类型" width="100" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="typeTag(row.OperationType)" size="small">{{ typeLabel(row.OperationType) }}</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="OperationTarget" label="操作对象" width="120" />
|
||||||
|
<el-table-column prop="Description" label="描述" min-width="220" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="Ip" label="IP" width="130" />
|
||||||
|
<el-table-column label="操作前值" width="70" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-link v-if="row.OldValue" type="primary" @click="showValue(row, 'OldValue')">查看</el-link>
|
||||||
|
<span v-else>-</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作后值" width="70" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-link v-if="row.NewValue" type="primary" @click="showValue(row, 'NewValue')">查看</el-link>
|
||||||
|
<span v-else>-</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<div class="pagination">
|
||||||
|
<el-pagination v-model:current-page="page" v-model:page-size="pageSize" :total="total"
|
||||||
|
:page-sizes="[20, 50, 100]" layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
@current-change="fetchList" @size-change="onSizeChange" />
|
||||||
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 值查看对话框 -->
|
||||||
|
<el-dialog v-model="valueVisible" :title="valueTitle" width="560px">
|
||||||
|
<pre class="value-pre">{{ valueContent }}</pre>
|
||||||
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
// TODO: 实现操作审计日志功能
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { getAuditLogList } from '@/api/audit'
|
||||||
|
|
||||||
|
const OPERATION_TYPES = { Create: '新增', Update: '修改', Delete: '删除', Command: '指令', Login: '登录', Logout: '登出' }
|
||||||
|
const OPERATION_TARGETS = ['IotDevice', 'Gateway', 'User', 'Role', 'Permission']
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
const list = ref([])
|
||||||
|
const total = ref(0)
|
||||||
|
const page = ref(1)
|
||||||
|
const pageSize = ref(20)
|
||||||
|
const keyword = ref('')
|
||||||
|
const searchType = ref('')
|
||||||
|
const searchTarget = ref('')
|
||||||
|
const dateRange = ref(null)
|
||||||
|
|
||||||
|
async function fetchList() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
list.value = (await getAuditLogList({
|
||||||
|
pageIndex: page.value, pageSize: pageSize.value,
|
||||||
|
keyword: keyword.value || undefined,
|
||||||
|
operationType: searchType.value || undefined,
|
||||||
|
operationTarget: searchTarget.value || undefined,
|
||||||
|
startTime: dateRange.value?.[0] || undefined,
|
||||||
|
endTime: dateRange.value?.[1] || undefined
|
||||||
|
})) || []
|
||||||
|
total.value = list.value.total || 0
|
||||||
|
} catch { list.value = []; total.value = 0 } finally { loading.value = false }
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSearch() { page.value = 1; fetchList() }
|
||||||
|
function onReset() {
|
||||||
|
keyword.value = ''; searchType.value = ''; searchTarget.value = ''; dateRange.value = null
|
||||||
|
page.value = 1; fetchList()
|
||||||
|
}
|
||||||
|
function onSizeChange() { page.value = 1; fetchList() }
|
||||||
|
|
||||||
|
// ---------- 值查看 ----------
|
||||||
|
const valueVisible = ref(false)
|
||||||
|
const valueTitle = ref('')
|
||||||
|
const valueContent = ref('')
|
||||||
|
|
||||||
|
function showValue(row, field) {
|
||||||
|
valueTitle.value = field === 'OldValue' ? '操作前值' : '操作后值'
|
||||||
|
try { valueContent.value = JSON.stringify(JSON.parse(row[field]), null, 2) }
|
||||||
|
catch { valueContent.value = row[field] }
|
||||||
|
valueVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const typeLabel = (t) => OPERATION_TYPES[t] || t
|
||||||
|
const typeTag = (t) => ({ Delete: 'danger', Command: 'warning', Login: 'success', Create: 'success' }[t] || 'info')
|
||||||
|
const fmtTime = (t) => t ? String(t).replace('T', ' ').slice(0, 19) : '-'
|
||||||
|
|
||||||
|
onMounted(() => fetchList())
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.card-header { display: flex; justify-content: space-between; align-items: center; }
|
||||||
|
.search-bar { display: flex; gap: 10px; margin-bottom: 16px; flex-wrap: wrap; }
|
||||||
|
.pagination { margin-top: 16px; display: flex; justify-content: flex-end; }
|
||||||
|
.value-pre {
|
||||||
|
margin: 0;
|
||||||
|
padding: 12px;
|
||||||
|
background: #f5f7fa;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 13px;
|
||||||
|
max-height: 400px;
|
||||||
|
overflow: auto;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -2,13 +2,235 @@
|
|||||||
<div class="page-container">
|
<div class="page-container">
|
||||||
<el-card>
|
<el-card>
|
||||||
<template #header>
|
<template #header>
|
||||||
<span>组织架构管理</span>
|
<div class="card-header">
|
||||||
|
<span>组织架构管理(公司 / 实验室 / 部门 / 班组)</span>
|
||||||
|
<el-button type="primary" icon="Plus" @click="openAddRoot">新增公司</el-button>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<el-empty description="组织架构管理 - 功能开发中" />
|
|
||||||
|
<el-alert type="info" :closable="false" class="org-tip"
|
||||||
|
title="层级固定:公司 → 实验室 → 部门 → 班组(白班/夜班)。用户挂靠任一层级节点,角色的数据范围按「本组织及下级」隔离。下表展示各组织的数据权限覆盖情况。" />
|
||||||
|
|
||||||
|
<div class="tree-toolbar">
|
||||||
|
<el-input v-model="filterText" placeholder="输入名称/编码过滤" clearable style="width: 260px">
|
||||||
|
<template #prefix><el-icon><Search /></el-icon></template>
|
||||||
|
</el-input>
|
||||||
|
<el-button link type="primary" @click="fetchTree">刷新</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-table :data="treeRows" v-loading="loading" row-key="Id" :tree-props="{ children: 'Children' }"
|
||||||
|
border default-expand-all :key="tableKey">
|
||||||
|
<el-table-column prop="Name" label="组织名称" min-width="220">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-icon class="org-icon"><component :is="ORG_TYPES[row.OrgType]?.icon || 'Folder'" /></el-icon>
|
||||||
|
<span>{{ row.Name }}</span>
|
||||||
|
<el-tag v-if="row.OrgType === 4" size="small" :type="row.ShiftType === 2 ? 'info' : 'success'" class="shift-tag">
|
||||||
|
{{ shiftLabel(row.ShiftType) }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="Code" label="编码" width="120" />
|
||||||
|
<el-table-column label="类型" width="90" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="orgTypeTag(row.OrgType)" size="small">{{ orgTypeLabel(row.OrgType) }}</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="用户数" width="90" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span :class="{ 'has-users': row.TotalUserCount > 0 }">{{ row.TotalUserCount }}</span>
|
||||||
|
<span v-if="row.UserCount !== row.TotalUserCount" class="direct-count">(直挂 {{ row.UserCount }})</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="数据权限" min-width="260">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<template v-if="row.ScopeSummary && row.ScopeSummary.length">
|
||||||
|
<el-tag v-for="s in row.ScopeSummary" :key="s" size="small"
|
||||||
|
:type="s.endsWith('·全部') ? 'danger' : 'warning'" class="scope-tag">{{ s }}</el-tag>
|
||||||
|
</template>
|
||||||
|
<span v-else class="no-scope">-</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="Leader" label="负责人" width="90" />
|
||||||
|
<el-table-column prop="Phone" label="联系电话" width="120" />
|
||||||
|
<el-table-column label="操作" width="200" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button v-if="row.OrgType < 4" link type="primary" @click="openAddChild(row)">添加下级</el-button>
|
||||||
|
<el-button link type="primary" @click="openEdit(row)">编辑</el-button>
|
||||||
|
<el-button link type="danger" @click="onDelete(row)">删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 添加/编辑组织节点 -->
|
||||||
|
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="520px" destroy-on-close>
|
||||||
|
<el-form ref="formRef" :model="form" :rules="formRules" label-width="90px">
|
||||||
|
<el-form-item label="上级组织">
|
||||||
|
<el-input :value="parentName || '(根节点)'" disabled />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="组织类型">
|
||||||
|
<el-input :value="orgTypeLabel(form.OrgType)" disabled />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item v-if="form.OrgType === 4" label="班次" prop="ShiftType">
|
||||||
|
<el-radio-group v-model="form.ShiftType">
|
||||||
|
<el-radio v-for="s in shiftOptions" :key="s.value" :value="s.value">{{ s.label }}</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="组织名称" prop="Name">
|
||||||
|
<el-input v-model="form.Name" placeholder="如 一号实验室 / 温控部 / 白班一组" maxlength="100" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="组织编码" prop="Code">
|
||||||
|
<el-input v-model="form.Code" placeholder="唯一编码,如 LAB-01" maxlength="64" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="负责人">
|
||||||
|
<el-input v-model="form.Leader" maxlength="50" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="联系电话">
|
||||||
|
<el-input v-model="form.Phone" maxlength="20" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="排序">
|
||||||
|
<el-input-number v-model="form.Sort" :min="0" :max="999" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="备注">
|
||||||
|
<el-input v-model="form.Remark" type="textarea" :rows="2" maxlength="500" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="dialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" :loading="saving" @click="onSubmit">确定</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
// TODO: 实现组织架构管理功能
|
import { ref, reactive, computed, onMounted } from 'vue'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import {
|
||||||
|
getOrgTree, addOrg, updateOrg, deleteOrg,
|
||||||
|
ORG_TYPES, orgTypeLabel, orgTypeTag, shiftLabel, shiftOptions
|
||||||
|
} from '@/api/org'
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
const tree = ref([])
|
||||||
|
const filterText = ref('')
|
||||||
|
const tableKey = ref(0)
|
||||||
|
|
||||||
|
// 前端过滤树(按名称/编码过滤后保留路径)
|
||||||
|
const treeRows = computed(() => {
|
||||||
|
const kw = filterText.value.trim().toLowerCase()
|
||||||
|
if (!kw) return tree.value
|
||||||
|
const filterNodes = (nodes) => {
|
||||||
|
const result = []
|
||||||
|
for (const n of nodes) {
|
||||||
|
const children = filterNodes(n.Children || [])
|
||||||
|
if (n.Name?.toLowerCase().includes(kw) || n.Code?.toLowerCase().includes(kw) || children.length > 0) {
|
||||||
|
result.push({ ...n, Children: children })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
return filterNodes(tree.value)
|
||||||
|
})
|
||||||
|
|
||||||
|
async function fetchTree() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
tree.value = (await getOrgTree()) || []
|
||||||
|
tableKey.value++
|
||||||
|
} catch { tree.value = [] } finally { loading.value = false }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 新增/编辑 ----------
|
||||||
|
const dialogVisible = ref(false)
|
||||||
|
const saving = ref(false)
|
||||||
|
const formRef = ref(null)
|
||||||
|
const parentName = ref('')
|
||||||
|
|
||||||
|
const defaultForm = () => ({
|
||||||
|
Id: '', ParentId: '0', Name: '', Code: '',
|
||||||
|
OrgType: 1, ShiftType: 0, Leader: '', Phone: '', Sort: 0, Remark: ''
|
||||||
|
})
|
||||||
|
const form = reactive(defaultForm())
|
||||||
|
|
||||||
|
// 层级固定:公司→实验室→部门→班组
|
||||||
|
const CHILD_TYPE_MAP = { 1: [2], 2: [3], 3: [4] }
|
||||||
|
|
||||||
|
const formRules = {
|
||||||
|
Name: [{ required: true, message: '请输入组织名称', trigger: 'blur' }],
|
||||||
|
Code: [{ required: true, message: '请输入组织编码', trigger: 'blur' }],
|
||||||
|
ShiftType: [{ required: true, message: '请选择班次', trigger: 'change' }]
|
||||||
|
}
|
||||||
|
|
||||||
|
const dialogTitle = computed(() => form.Id ? '修改组织' : `新增「${parentName.value || '公司'}」`)
|
||||||
|
|
||||||
|
function openAddRoot() {
|
||||||
|
Object.assign(form, defaultForm(), { OrgType: 1, ParentId: '0' })
|
||||||
|
parentName.value = ''
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function openAddChild(row) {
|
||||||
|
Object.assign(form, defaultForm())
|
||||||
|
form.ParentId = row.Id
|
||||||
|
// 层级固定:自动推断唯一的下级类型
|
||||||
|
const allowed = CHILD_TYPE_MAP[row.OrgType] || []
|
||||||
|
form.OrgType = allowed[0] || 4
|
||||||
|
if (form.OrgType === 4) form.ShiftType = 1
|
||||||
|
parentName.value = `${row.Name}(${orgTypeLabel(row.OrgType)})下级`
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function findNodeById(nodes, id) {
|
||||||
|
for (const n of nodes) {
|
||||||
|
if (n.Id === id) return n
|
||||||
|
const found = findNodeById(n.Children || [], id)
|
||||||
|
if (found) return found
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
function openEdit(row) {
|
||||||
|
Object.assign(form, defaultForm(), {
|
||||||
|
Id: row.Id, ParentId: row.ParentId, Name: row.Name, Code: row.Code,
|
||||||
|
OrgType: row.OrgType, ShiftType: row.OrgType === 4 ? (row.ShiftType || 1) : 0,
|
||||||
|
Leader: row.Leader || '', Phone: row.Phone || '',
|
||||||
|
Sort: row.Sort, Remark: row.Remark || ''
|
||||||
|
})
|
||||||
|
const parent = findNodeById(tree.value, row.ParentId)
|
||||||
|
parentName.value = parent ? `${parent.Name}(${orgTypeLabel(parent.OrgType)})` : ''
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onSubmit() {
|
||||||
|
await formRef.value.validate()
|
||||||
|
saving.value = true
|
||||||
|
try {
|
||||||
|
const payload = { ...form, ShiftType: form.OrgType === 4 ? form.ShiftType : 0 }
|
||||||
|
if (form.Id) { await updateOrg(payload); ElMessage.success('修改成功') }
|
||||||
|
else { await addOrg(payload); ElMessage.success('添加成功') }
|
||||||
|
dialogVisible.value = false
|
||||||
|
fetchTree()
|
||||||
|
} catch { /* 拦截器已提示 */ } finally { saving.value = false }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 删除 ----------
|
||||||
|
async function onDelete(row) {
|
||||||
|
await ElMessageBox.confirm(`确定删除组织「${row.Name}」吗?(有下级组织或挂靠用户时不可删除)`, '提示', { type: 'warning' })
|
||||||
|
try { await deleteOrg(row.Id); ElMessage.success('删除成功'); fetchTree() } catch { /* 拦截器已提示 */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => fetchTree())
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.card-header { display: flex; justify-content: space-between; align-items: center; }
|
||||||
|
.org-tip { margin-bottom: 14px; }
|
||||||
|
.tree-toolbar { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; max-width: 400px; }
|
||||||
|
.org-icon { margin-right: 6px; vertical-align: -2px; }
|
||||||
|
.shift-tag { margin-left: 6px; }
|
||||||
|
.scope-tag { margin-right: 6px; margin-bottom: 2px; }
|
||||||
|
.has-users { font-weight: 600; color: #409eff; }
|
||||||
|
.direct-count { font-size: 12px; color: #909399; }
|
||||||
|
.no-scope { color: #c0c4cc; }
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -2,13 +2,231 @@
|
|||||||
<div class="page-container">
|
<div class="page-container">
|
||||||
<el-card>
|
<el-card>
|
||||||
<template #header>
|
<template #header>
|
||||||
<span>角色权限管理</span>
|
<div class="card-header">
|
||||||
|
<span>角色权限管理({{ total }} 个)</span>
|
||||||
|
<el-button v-permission="'user:manage'" type="primary" icon="Plus" @click="openAdd">添加角色</el-button>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<el-empty description="角色权限管理 - 功能开发中" />
|
|
||||||
|
<div class="search-bar">
|
||||||
|
<el-input v-model="keyword" placeholder="角色编码 / 名称" clearable style="width: 260px"
|
||||||
|
@keyup.enter="onSearch" @clear="onSearch">
|
||||||
|
<template #prefix><el-icon><Search /></el-icon></template>
|
||||||
|
</el-input>
|
||||||
|
<el-button type="primary" @click="onSearch">查询</el-button>
|
||||||
|
<el-button @click="onReset">重置</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-table :data="list" v-loading="loading" stripe border row-key="Id">
|
||||||
|
<el-table-column prop="Code" label="角色编码" width="140" />
|
||||||
|
<el-table-column prop="Name" label="角色名称" width="140" />
|
||||||
|
<el-table-column label="数据范围" width="110" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="dataScopeTag(row.DataScope)" size="small">{{ dataScopeLabel(row.DataScope) }}</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="权限" min-width="280">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag v-for="code in (row.PermissionCodes || [])" :key="code" size="small" class="perm-tag">
|
||||||
|
{{ permName(code) }}
|
||||||
|
</el-tag>
|
||||||
|
<span v-if="!(row.PermissionCodes || []).length">-</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="内置" width="70" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag v-if="row.IsSystem === 1" type="warning" size="small">内置</el-tag>
|
||||||
|
<span v-else>-</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="Remark" label="备注" min-width="140" show-overflow-tooltip />
|
||||||
|
<el-table-column label="操作" width="180" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button link type="primary" @click="openEdit(row)">编辑</el-button>
|
||||||
|
<el-button link type="primary" @click="openPerm(row)">分配权限</el-button>
|
||||||
|
<el-button v-if="row.IsSystem !== 1" link type="danger" @click="onDelete(row)">删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<div class="pagination">
|
||||||
|
<el-pagination v-model:current-page="page" v-model:page-size="pageSize" :total="total"
|
||||||
|
:page-sizes="[10, 20, 50]" layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
@current-change="fetchList" @size-change="onSizeChange" />
|
||||||
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 添加/编辑角色 -->
|
||||||
|
<el-dialog v-model="dialogVisible" :title="form.Id ? '修改角色' : '添加角色'" width="520px" destroy-on-close>
|
||||||
|
<el-form ref="formRef" :model="form" :rules="formRules" label-width="90px">
|
||||||
|
<el-form-item label="角色编码" prop="Code">
|
||||||
|
<el-input v-model="form.Code" placeholder="唯一编码,如 labadmin" maxlength="64"
|
||||||
|
:disabled="!!form.Id && form.IsSystem === 1" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="角色名称" prop="Name">
|
||||||
|
<el-input v-model="form.Name" maxlength="100" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="数据范围" prop="DataScope">
|
||||||
|
<el-select v-model="form.DataScope" style="width: 100%">
|
||||||
|
<el-option label="全部" :value="1" />
|
||||||
|
<el-option label="本组织及下级" :value="2" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="排序">
|
||||||
|
<el-input-number v-model="form.Sort" :min="0" :max="999" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="备注">
|
||||||
|
<el-input v-model="form.Remark" type="textarea" :rows="2" maxlength="500" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="dialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" :loading="saving" @click="onSubmit">确定</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 分配权限 -->
|
||||||
|
<el-dialog v-model="permVisible" :title="`分配权限 - ${permRole?.Name || ''}`" width="440px" destroy-on-close>
|
||||||
|
<el-checkbox-group v-model="permSelectedIds">
|
||||||
|
<div v-for="group in groupedPermissions" :key="group.name" class="perm-group">
|
||||||
|
<div class="perm-group-title">{{ group.name }}</div>
|
||||||
|
<el-checkbox v-for="p in group.items" :key="p.Id" :value="p.Id">{{ p.Name }}({{ p.Code }})</el-checkbox>
|
||||||
|
</div>
|
||||||
|
</el-checkbox-group>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="permVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" :loading="permSaving" @click="onSavePermissions">确定</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
// TODO: 实现角色权限管理功能
|
import { ref, reactive, computed, onMounted } from 'vue'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import {
|
||||||
|
getRoleList, getRoleOptions, getPermissionList, addRole, updateRole, deleteRole, assignRolePermissions,
|
||||||
|
dataScopeLabel, dataScopeTag
|
||||||
|
} from '@/api/role'
|
||||||
|
|
||||||
|
// ---------- 列表 ----------
|
||||||
|
const loading = ref(false)
|
||||||
|
const list = ref([])
|
||||||
|
const total = ref(0)
|
||||||
|
const page = ref(1)
|
||||||
|
const pageSize = ref(10)
|
||||||
|
const keyword = ref('')
|
||||||
|
const allPermissions = ref([])
|
||||||
|
|
||||||
|
const PERM_GROUP_NAMES = {
|
||||||
|
device: '设备', alert: '告警', inspection: '巡检', user: '系统'
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchList() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
list.value = (await getRoleList({
|
||||||
|
pageIndex: page.value, pageSize: pageSize.value,
|
||||||
|
keyword: keyword.value || undefined
|
||||||
|
})) || []
|
||||||
|
total.value = list.value.total || 0
|
||||||
|
} catch { list.value = []; total.value = 0 } finally { loading.value = false }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchPermissions() {
|
||||||
|
try { allPermissions.value = (await getPermissionList()) || [] } catch { allPermissions.value = [] }
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSearch() { page.value = 1; fetchList() }
|
||||||
|
function onReset() { keyword.value = ''; page.value = 1; fetchList() }
|
||||||
|
function onSizeChange() { page.value = 1; fetchList() }
|
||||||
|
|
||||||
|
function permName(code) {
|
||||||
|
const p = allPermissions.value.find(x => x.Code === code)
|
||||||
|
return p?.Name || code
|
||||||
|
}
|
||||||
|
|
||||||
|
const groupedPermissions = computed(() => {
|
||||||
|
const groups = {}
|
||||||
|
for (const p of allPermissions.value) {
|
||||||
|
const g = p.Group || '其他'
|
||||||
|
if (!groups[g]) groups[g] = { name: PERM_GROUP_NAMES[g] || g, items: [] }
|
||||||
|
groups[g].items.push(p)
|
||||||
|
}
|
||||||
|
return Object.values(groups)
|
||||||
|
})
|
||||||
|
|
||||||
|
// ---------- 新增/修改 ----------
|
||||||
|
const dialogVisible = ref(false)
|
||||||
|
const saving = ref(false)
|
||||||
|
const formRef = ref(null)
|
||||||
|
|
||||||
|
const defaultForm = () => ({ Id: '', Code: '', Name: '', DataScope: 2, Sort: 0, IsSystem: 0, Remark: '' })
|
||||||
|
const form = reactive(defaultForm())
|
||||||
|
|
||||||
|
const formRules = {
|
||||||
|
Code: [{ required: true, message: '请输入角色编码', trigger: 'blur' }],
|
||||||
|
Name: [{ required: true, message: '请输入角色名称', trigger: 'blur' }],
|
||||||
|
DataScope: [{ required: true, message: '请选择数据范围', trigger: 'change' }]
|
||||||
|
}
|
||||||
|
|
||||||
|
function openAdd() { Object.assign(form, defaultForm()); dialogVisible.value = true }
|
||||||
|
function openEdit(row) {
|
||||||
|
Object.assign(form, defaultForm(), {
|
||||||
|
Id: row.Id, Code: row.Code, Name: row.Name,
|
||||||
|
DataScope: row.DataScope, Sort: row.Sort, IsSystem: row.IsSystem, Remark: row.Remark || ''
|
||||||
|
})
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onSubmit() {
|
||||||
|
await formRef.value.validate()
|
||||||
|
saving.value = true
|
||||||
|
try {
|
||||||
|
if (form.Id) { await updateRole({ ...form }); ElMessage.success('修改成功') }
|
||||||
|
else { await addRole({ ...form }); ElMessage.success('添加成功') }
|
||||||
|
dialogVisible.value = false
|
||||||
|
fetchList()
|
||||||
|
} catch { /* 拦截器已提示 */ } finally { saving.value = false }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 分配权限 ----------
|
||||||
|
const permVisible = ref(false)
|
||||||
|
const permSaving = ref(false)
|
||||||
|
const permRole = ref(null)
|
||||||
|
const permSelectedIds = ref([])
|
||||||
|
|
||||||
|
function openPerm(row) {
|
||||||
|
permRole.value = row
|
||||||
|
permSelectedIds.value = [...(row.PermissionIds || [])]
|
||||||
|
permVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onSavePermissions() {
|
||||||
|
permSaving.value = true
|
||||||
|
try {
|
||||||
|
await assignRolePermissions(permRole.value.Id, permSelectedIds.value)
|
||||||
|
ElMessage.success('权限已更新')
|
||||||
|
permVisible.value = false
|
||||||
|
fetchList()
|
||||||
|
} catch { /* 拦截器已提示 */ } finally { permSaving.value = false }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 删除 ----------
|
||||||
|
async function onDelete(row) {
|
||||||
|
await ElMessageBox.confirm(`确定删除角色「${row.Name}」吗?`, '提示', { type: 'warning' })
|
||||||
|
try { await deleteRole(row.Id); ElMessage.success('删除成功'); fetchList() } catch { /* 拦截器已提示 */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => { fetchList(); fetchPermissions() })
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.card-header { display: flex; justify-content: space-between; align-items: center; }
|
||||||
|
.search-bar { display: flex; gap: 10px; margin-bottom: 16px; }
|
||||||
|
.pagination { margin-top: 16px; display: flex; justify-content: flex-end; }
|
||||||
|
.perm-tag { margin-right: 4px; margin-bottom: 2px; }
|
||||||
|
.perm-group { margin-bottom: 12px; }
|
||||||
|
.perm-group-title { font-size: 13px; color: #909399; margin-bottom: 4px; }
|
||||||
|
.perm-group .el-checkbox { margin-right: 16px; }
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -2,13 +2,270 @@
|
|||||||
<div class="page-container">
|
<div class="page-container">
|
||||||
<el-card>
|
<el-card>
|
||||||
<template #header>
|
<template #header>
|
||||||
<span>用户管理</span>
|
<div class="card-header">
|
||||||
|
<span>用户管理({{ total }} 人)</span>
|
||||||
|
<el-button v-permission="'user:manage'" type="primary" icon="Plus" @click="openAdd">添加用户</el-button>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<el-empty description="用户管理 - 功能开发中" />
|
|
||||||
|
<div class="search-bar">
|
||||||
|
<el-tree-select v-model="searchOrgId" :data="orgTree" :props="{ label: 'Name', children: 'Children' }"
|
||||||
|
node-key="Id" check-strictly clearable placeholder="所属组织(全部)" style="width: 200px" @change="onSearch" />
|
||||||
|
<el-select v-model="searchRoleId" placeholder="所属角色(全部)" clearable style="width: 180px" @change="onSearch">
|
||||||
|
<el-option v-for="r in roleOptions" :key="r.Id" :label="r.Name" :value="r.Id" />
|
||||||
|
</el-select>
|
||||||
|
<el-input v-model="keyword" placeholder="用户名 / 姓名" clearable style="width: 240px"
|
||||||
|
@keyup.enter="onSearch" @clear="onSearch">
|
||||||
|
<template #prefix><el-icon><Search /></el-icon></template>
|
||||||
|
</el-input>
|
||||||
|
<el-button type="primary" @click="onSearch">查询</el-button>
|
||||||
|
<el-button @click="onReset">重置</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-table :data="list" v-loading="loading" stripe border row-key="Id">
|
||||||
|
<el-table-column prop="UserName" label="用户名" width="120" />
|
||||||
|
<el-table-column prop="RealName" label="姓名" width="100" />
|
||||||
|
<el-table-column label="组织" min-width="180" show-overflow-tooltip>
|
||||||
|
<template #default="{ row }">{{ row.OrgName || '-' }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="Position" label="岗位" width="110" show-overflow-tooltip>
|
||||||
|
<template #default="{ row }">{{ row.Position || '-' }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="技能标签" min-width="160">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag v-for="tag in parseTags(row.SkillTags)" :key="tag" size="small" class="skill-tag">{{ tag }}</el-tag>
|
||||||
|
<span v-if="!parseTags(row.SkillTags).length">-</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="角色" min-width="130" show-overflow-tooltip>
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag v-for="name in (row.RoleNames || '').split(',').filter(Boolean)" :key="name"
|
||||||
|
size="small" type="info" class="skill-tag">{{ name }}</el-tag>
|
||||||
|
<span v-if="!row.RoleNames">-</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="状态" width="80" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="row.IsEnabled === 1 ? 'success' : 'danger'" size="small">
|
||||||
|
{{ row.IsEnabled === 1 ? '启用' : '禁用' }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="最后登录" width="160">
|
||||||
|
<template #default="{ row }">{{ fmtTime(row.LastLoginTime) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="220" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button link type="primary" @click="openEdit(row)">编辑</el-button>
|
||||||
|
<el-button link type="warning" @click="onResetPassword(row)">重置密码</el-button>
|
||||||
|
<el-button v-if="row.Id !== userStore.userInfo?.Id" link :type="row.IsEnabled === 1 ? 'info' : 'success'" @click="onToggleEnabled(row)">
|
||||||
|
{{ row.IsEnabled === 1 ? '禁用' : '启用' }}
|
||||||
|
</el-button>
|
||||||
|
<el-button v-if="row.Id !== userStore.userInfo?.Id" link type="danger" @click="onDelete(row)">删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<div class="pagination">
|
||||||
|
<el-pagination v-model:current-page="page" v-model:page-size="pageSize" :total="total"
|
||||||
|
:page-sizes="[10, 20, 50]" layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
@current-change="fetchList" @size-change="onSizeChange" />
|
||||||
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 添加/编辑用户 -->
|
||||||
|
<el-dialog v-model="dialogVisible" :title="form.Id ? '修改用户' : '添加用户'" width="560px" destroy-on-close>
|
||||||
|
<el-form ref="formRef" :model="form" :rules="formRules" label-width="90px">
|
||||||
|
<el-form-item label="用户名" prop="UserName">
|
||||||
|
<el-input v-model="form.UserName" placeholder="登录账号" maxlength="64" :disabled="!!form.Id" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="姓名">
|
||||||
|
<el-input v-model="form.RealName" maxlength="100" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item v-if="!form.Id" label="初始密码" prop="InitialPassword">
|
||||||
|
<el-input v-model="form.InitialPassword" type="password" placeholder="不少于 6 位" show-password />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="所属组织" prop="OrgId">
|
||||||
|
<el-tree-select v-model="form.OrgId" :data="orgTree" :props="{ label: 'Name', children: 'Children' }"
|
||||||
|
node-key="Id" check-strictly clearable placeholder="选择组织节点(公司/实验室/部门/班组)" style="width: 100%" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="岗位">
|
||||||
|
<el-input v-model="form.Position" placeholder="如 设备工程师 / 操作员" maxlength="64" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="技能标签">
|
||||||
|
<el-select v-model="form.TagList" multiple filterable allow-create default-first-option
|
||||||
|
placeholder="输入或选择技能标签" style="width: 100%">
|
||||||
|
<el-option v-for="tag in presetTags" :key="tag" :label="tag" :value="tag" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="手机号">
|
||||||
|
<el-input v-model="form.Phone" maxlength="20" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="邮箱">
|
||||||
|
<el-input v-model="form.Email" maxlength="128" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="角色" prop="RoleIds">
|
||||||
|
<el-select v-model="form.RoleIds" multiple placeholder="选择角色" style="width: 100%">
|
||||||
|
<el-option v-for="r in roleOptions" :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" maxlength="500" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="dialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" :loading="saving" @click="onSubmit">确定</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
// TODO: 实现用户管理功能
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import { getUserList, addUser, updateUser, deleteUser, resetUserPassword, setUserEnabled } from '@/api/user'
|
||||||
|
import { getRoleOptions } from '@/api/role'
|
||||||
|
import { getOrgTree } from '@/api/org'
|
||||||
|
import { useUserStore } from '@/store/user'
|
||||||
|
|
||||||
|
const userStore = useUserStore()
|
||||||
|
|
||||||
|
// 预置技能标签建议(可自由输入新标签)
|
||||||
|
const presetTags = ['Modbus', 'PLC', 'S7', '温度箱', '充放电柜', '电气维修', '机械维修', '计量校准', '巡检']
|
||||||
|
|
||||||
|
// ---------- 列表 ----------
|
||||||
|
const loading = ref(false)
|
||||||
|
const list = ref([])
|
||||||
|
const total = ref(0)
|
||||||
|
const page = ref(1)
|
||||||
|
const pageSize = ref(10)
|
||||||
|
const keyword = ref('')
|
||||||
|
const searchRoleId = ref('')
|
||||||
|
const searchOrgId = ref(null)
|
||||||
|
const roleOptions = ref([])
|
||||||
|
const orgTree = ref([])
|
||||||
|
|
||||||
|
async function fetchList() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
list.value = (await getUserList({
|
||||||
|
pageIndex: page.value, pageSize: pageSize.value,
|
||||||
|
keyword: keyword.value || undefined,
|
||||||
|
roleId: searchRoleId.value || undefined,
|
||||||
|
orgId: searchOrgId.value || undefined
|
||||||
|
})) || []
|
||||||
|
total.value = list.value.total || 0
|
||||||
|
} catch { list.value = []; total.value = 0 } finally { loading.value = false }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchRoleOptions() {
|
||||||
|
try { roleOptions.value = (await getRoleOptions()) || [] } catch { roleOptions.value = [] }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchOrgTree() {
|
||||||
|
try { orgTree.value = (await getOrgTree()) || [] } catch { orgTree.value = [] }
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSearch() { page.value = 1; fetchList() }
|
||||||
|
function onReset() {
|
||||||
|
keyword.value = ''; searchRoleId.value = ''; searchOrgId.value = null
|
||||||
|
page.value = 1; fetchList()
|
||||||
|
}
|
||||||
|
function onSizeChange() { page.value = 1; fetchList() }
|
||||||
|
|
||||||
|
function parseTags(tags) {
|
||||||
|
return (tags || '').split(',').map(t => t.trim()).filter(Boolean)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 新增/修改 ----------
|
||||||
|
const dialogVisible = ref(false)
|
||||||
|
const saving = ref(false)
|
||||||
|
const formRef = ref(null)
|
||||||
|
|
||||||
|
const defaultForm = () => ({
|
||||||
|
Id: '', UserName: '', RealName: '', InitialPassword: '',
|
||||||
|
OrgId: null, Position: '', TagList: [],
|
||||||
|
Phone: '', Email: '', RoleIds: [], Remark: ''
|
||||||
|
})
|
||||||
|
const form = reactive(defaultForm())
|
||||||
|
|
||||||
|
const formRules = {
|
||||||
|
UserName: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
|
||||||
|
InitialPassword: [
|
||||||
|
{ required: true, message: '请输入初始密码', trigger: 'blur' },
|
||||||
|
{ min: 6, message: '密码长度不能少于 6 位', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
RoleIds: [{ required: true, type: 'array', min: 1, message: '请选择至少一个角色', trigger: 'change' }]
|
||||||
|
}
|
||||||
|
|
||||||
|
function openAdd() { Object.assign(form, defaultForm()); dialogVisible.value = true }
|
||||||
|
function openEdit(row) {
|
||||||
|
Object.assign(form, defaultForm(), {
|
||||||
|
Id: row.Id, UserName: row.UserName, RealName: row.RealName || '',
|
||||||
|
OrgId: row.OrgId && row.OrgId !== '0' ? row.OrgId : null,
|
||||||
|
Position: row.Position || '',
|
||||||
|
TagList: parseTags(row.SkillTags),
|
||||||
|
Phone: row.Phone || '', Email: row.Email || '',
|
||||||
|
RoleIds: row.RoleIds || [], Remark: row.Remark || ''
|
||||||
|
})
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onSubmit() {
|
||||||
|
await formRef.value.validate()
|
||||||
|
saving.value = true
|
||||||
|
try {
|
||||||
|
const payload = {
|
||||||
|
...form,
|
||||||
|
OrgId: form.OrgId || '0',
|
||||||
|
SkillTags: (form.TagList || []).join(',')
|
||||||
|
}
|
||||||
|
if (form.Id) {
|
||||||
|
await updateUser({ ...payload, InitialPassword: undefined, TagList: undefined })
|
||||||
|
ElMessage.success('修改成功')
|
||||||
|
} else {
|
||||||
|
await addUser({ ...payload, TagList: undefined })
|
||||||
|
ElMessage.success('添加成功')
|
||||||
|
}
|
||||||
|
dialogVisible.value = false
|
||||||
|
fetchList()
|
||||||
|
} catch { /* 拦截器已提示 */ } finally { saving.value = false }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 重置密码 ----------
|
||||||
|
async function onResetPassword(row) {
|
||||||
|
const { value } = await ElMessageBox.prompt(`为用户「${row.UserName}」设置新密码:`, '重置密码', {
|
||||||
|
inputType: 'password', inputPattern: /^.{6,}$/, inputErrorMessage: '密码长度不能少于 6 位'
|
||||||
|
})
|
||||||
|
await resetUserPassword(row.Id, value)
|
||||||
|
ElMessage.success('密码已重置')
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 启用/禁用 ----------
|
||||||
|
async function onToggleEnabled(row) {
|
||||||
|
const target = row.IsEnabled === 1 ? 0 : 1
|
||||||
|
await ElMessageBox.confirm(
|
||||||
|
`确定${target === 0 ? '禁用' : '启用'}用户「${row.UserName}」吗?`, '提示', { type: 'warning' })
|
||||||
|
await setUserEnabled(row.Id, target === 1)
|
||||||
|
ElMessage.success(target === 0 ? '已禁用' : '已启用')
|
||||||
|
fetchList()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 删除 ----------
|
||||||
|
async function onDelete(row) {
|
||||||
|
await ElMessageBox.confirm(`确定删除用户「${row.UserName}」吗?`, '提示', { type: 'warning' })
|
||||||
|
try { await deleteUser(row.Id); ElMessage.success('删除成功'); fetchList() } catch { /* 拦截器已提示 */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
const fmtTime = (t) => t ? String(t).replace('T', ' ').slice(0, 19) : '-'
|
||||||
|
|
||||||
|
onMounted(() => { fetchList(); fetchRoleOptions(); fetchOrgTree() })
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.card-header { display: flex; justify-content: space-between; align-items: center; }
|
||||||
|
.search-bar { display: flex; gap: 10px; margin-bottom: 16px; flex-wrap: wrap; }
|
||||||
|
.pagination { margin-top: 16px; display: flex; justify-content: flex-end; }
|
||||||
|
.skill-tag { margin-right: 4px; margin-bottom: 2px; }
|
||||||
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user