diff --git a/dist/index.html b/dist/index.html
index 4666506..63cbf0f 100644
--- a/dist/index.html
+++ b/dist/index.html
@@ -5,11 +5,11 @@
设备管理系统
-
+
-
-
+
+
diff --git a/src/api/alert.js b/src/api/alert.js
new file mode 100644
index 0000000..87dcbbc
--- /dev/null
+++ b/src/api/alert.js
@@ -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}`)
diff --git a/src/api/audit.js b/src/api/audit.js
new file mode 100644
index 0000000..527c5ed
--- /dev/null
+++ b/src/api/audit.js
@@ -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 })
diff --git a/src/api/auth.js b/src/api/auth.js
new file mode 100644
index 0000000..8785755
--- /dev/null
+++ b/src/api/auth.js
@@ -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)
diff --git a/src/api/org.js b/src/api/org.js
new file mode 100644
index 0000000..fadfbe5
--- /dev/null
+++ b/src/api/org.js
@@ -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}`)
diff --git a/src/api/role.js b/src/api/role.js
new file mode 100644
index 0000000..426e243
--- /dev/null
+++ b/src/api/role.js
@@ -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'
diff --git a/src/api/user.js b/src/api/user.js
new file mode 100644
index 0000000..8d9b1b0
--- /dev/null
+++ b/src/api/user.js
@@ -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 })
diff --git a/src/directives/permission.js b/src/directives/permission.js
new file mode 100644
index 0000000..2b2b666
--- /dev/null
+++ b/src/directives/permission.js
@@ -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)
+}
diff --git a/src/layout/Header.vue b/src/layout/Header.vue
index b47e620..8dd91c3 100644
--- a/src/layout/Header.vue
+++ b/src/layout/Header.vue
@@ -13,34 +13,113 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 取消
+ 确定
+
+
diff --git a/src/layout/Sidebar.vue b/src/layout/Sidebar.vue
index cd2aa30..8d37936 100644
--- a/src/layout/Sidebar.vue
+++ b/src/layout/Sidebar.vue
@@ -39,8 +39,7 @@
{{ child.meta?.title }}
-
-
+
@@ -49,20 +48,31 @@
import { computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useAppStore } from '@/store/app'
+import { useUserStore } from '@/store/user'
const route = useRoute()
const router = useRouter()
const appStore = useAppStore()
+const userStore = useUserStore()
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(() => {
return router.options.routes
- .filter(r => r.component && r.path !== '/:pathMatch(.*)*')
+ .filter(r => r.component && r.path !== '/:pathMatch(.*)*' && !r.meta?.public)
.map(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) {
diff --git a/src/main.js b/src/main.js
index 93f1d36..cdc4f13 100644
--- a/src/main.js
+++ b/src/main.js
@@ -5,6 +5,7 @@ import 'element-plus/dist/index.css'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import router from './router'
import App from './App.vue'
+import { setupDirectives } from './directives/permission'
const app = createApp(App)
@@ -14,6 +15,7 @@ for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
}
app.use(createPinia())
+setupDirectives(app)
app.use(router)
app.use(ElementPlus)
app.mount('#app')
diff --git a/src/router/index.js b/src/router/index.js
index c895b33..f3947aa 100644
--- a/src/router/index.js
+++ b/src/router/index.js
@@ -2,6 +2,12 @@ import { createRouter, createWebHistory } from 'vue-router'
import Layout from '@/layout/Layout.vue'
const routes = [
+ {
+ path: '/login',
+ name: 'Login',
+ component: () => import('@/views/login/Index.vue'),
+ meta: { title: '登录', public: true }
+ },
{
path: '/',
component: Layout,
@@ -143,7 +149,7 @@ const routes = [
path: 'patrol',
name: 'PatrolManage',
component: () => import('@/views/inspection/patrol/Index.vue'),
- meta: { title: '巡检管理', icon: 'Guide' }
+ meta: { title: '巡检管理', icon: 'Guide', permissions: ['inspection:manage'] }
},
{
path: 'patrol/template',
@@ -200,25 +206,25 @@ const routes = [
path: 'product',
name: 'ProductManage',
component: () => import('@/views/config/product/Index.vue'),
- meta: { title: '产品管理', icon: 'Goods' }
+ meta: { title: '产品管理', icon: 'Goods', permissions: ['device:view'] }
},
{
path: 'product/category',
name: 'ProductCategory',
component: () => import('@/views/config/product/Category.vue'),
- meta: { title: '产品分类', icon: 'Menu' }
+ meta: { title: '产品分类', icon: 'Menu', permissions: ['device:view'] }
},
{
path: 'device',
name: 'DeviceManage',
component: () => import('@/views/config/device/Index.vue'),
- meta: { title: '设备管理', icon: 'Cpu' }
+ meta: { title: '设备管理', icon: 'Cpu', permissions: ['device:view'] }
},
{
path: 'gateway',
name: 'GatewayManage',
component: () => import('@/views/config/gateway/Index.vue'),
- meta: { title: '网关管理', icon: 'Connection' }
+ meta: { title: '网关管理', icon: 'Connection', permissions: ['device:view'] }
},
{
path: 'protocol/certificate',
@@ -287,13 +293,13 @@ const routes = [
path: 'role',
name: 'RoleManage',
component: () => import('@/views/system/role/Index.vue'),
- meta: { title: '角色权限管理', icon: 'Lock' }
+ meta: { title: '角色权限管理', icon: 'Lock', permissions: ['user:manage'] }
},
{
path: 'audit',
name: 'AuditLog',
component: () => import('@/views/system/audit/Index.vue'),
- meta: { title: '操作审计日志', icon: 'Notebook' }
+ meta: { title: '操作审计日志', icon: 'Notebook', permissions: ['user:manage'] }
},
{
path: 'organization',
@@ -305,7 +311,7 @@ const routes = [
path: 'user',
name: 'UserManage',
component: () => import('@/views/system/user/Index.vue'),
- meta: { title: '用户管理', icon: 'User' }
+ meta: { title: '用户管理', icon: 'User', permissions: ['user:manage'] }
},
{
path: 'dict',
@@ -352,4 +358,42 @@ const router = createRouter({
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
diff --git a/src/store/user.js b/src/store/user.js
new file mode 100644
index 0000000..2d35709
--- /dev/null
+++ b/src/store/user.js
@@ -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 }
+})
diff --git a/src/utils/format.js b/src/utils/format.js
index 5a8169d..b3de670 100644
--- a/src/utils/format.js
+++ b/src/utils/format.js
@@ -1,60 +1,42 @@
/**
- * 通用格式化工具(后端返回的日期为 ISO 字符串,如 "2026-08-28T10:30:00")
+ * 通用格式化工具(asset 台账视图与告警页共用)
*/
-// 补零:5 → "05"
-const pad = n => String(n).padStart(2, '0')
-
-/**
- * 格式化为日期:2026-08-28
- * @param {string|Date} value 日期/时间值
- * @returns {string} 空值返回 ''
- */
+// 日期:YYYY-MM-DD(兼容 ISO 字符串 / Date)
export function fmtDate(value) {
- if (!value) return ''
+ if (!value) return '-'
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())}`
}
-/**
- * 格式化为日期时间:2026-08-28 10:30:00
- * @param {string|Date} value 日期/时间值
- * @returns {string} 空值返回 ''
- */
+// 日期时间:YYYY-MM-DD HH:mm:ss
export function fmtDateTime(value) {
- if (!value) return ''
+ if (!value) return '-'
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())}`
}
-/**
- * 格式化文件大小:1024 → "1.00 KB"
- * @param {number} bytes 字节数
- * @returns {string} 空值返回 ''
- */
+// 文件大小(字节 → 可读单位)
export function fmtFileSize(bytes) {
- if (bytes === null || bytes === undefined || isNaN(bytes)) return ''
- if (bytes < 1024) return `${bytes} B`
- const units = ['KB', 'MB', 'GB', 'TB']
- let size = bytes
- let i = -1
- do {
- size /= 1024
- i++
- } while (size >= 1024 && i < units.length - 1)
- return `${size.toFixed(2)} ${units[i]}`
+ if (bytes == null || bytes === '' || Number.isNaN(Number(bytes))) return '-'
+ const n = Number(bytes)
+ if (n < 1024) return `${n} B`
+ if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`
+ if (n < 1024 * 1024 * 1024) return `${(n / 1024 / 1024).toFixed(1)} MB`
+ return `${(n / 1024 / 1024 / 1024).toFixed(2)} GB`
}
-/**
- * 判断日期是否已超期(小于当前日期)
- * @param {string|Date} value 日期值
- * @returns {boolean}
- */
+// 是否已逾期(日期早于今天且非空)
export function isOverdue(value) {
if (!value) return false
const d = new Date(value)
- if (isNaN(d.getTime())) return false
- return d < new Date()
+ if (Number.isNaN(d.getTime())) return false
+ const today = new Date()
+ today.setHours(0, 0, 0, 0)
+ d.setHours(0, 0, 0, 0)
+ return d.getTime() < today.getTime()
}
diff --git a/src/utils/request.js b/src/utils/request.js
index ade014d..289d9bf 100644
--- a/src/utils/request.js
+++ b/src/utils/request.js
@@ -34,7 +34,25 @@ request.interceptors.response.use(
return data
},
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)
}
)
diff --git a/src/views/config/device/Index.vue b/src/views/config/device/Index.vue
index 8580d6d..3efb2d7 100644
--- a/src/views/config/device/Index.vue
+++ b/src/views/config/device/Index.vue
@@ -70,8 +70,8 @@
详情
- 编辑
- onMoreCommand(cmd, row)">
+ 编辑
+ onMoreCommand(cmd, row)">
更多
@@ -96,13 +96,13 @@
-