klxj001 пре 3 недеља
родитељ
комит
09c8dd0821
72 измењених фајлова са 11166 додато и 259 уклоњено
  1. 3 3
      .env.development
  2. 3 3
      .env.production
  3. 300 4
      src/api/menu.ts
  4. 31 0
      src/api/riskctrl/accidentReport/index.ts
  5. 75 0
      src/api/riskctrl/accidentReport/types.ts
  6. 27 0
      src/api/riskctrl/auditPersonChange/index.ts
  7. 56 0
      src/api/riskctrl/auditPersonChange/types.ts
  8. 70 0
      src/api/riskctrl/blacklist/index.ts
  9. 39 0
      src/api/riskctrl/blacklist/types.ts
  10. 59 0
      src/api/riskctrl/case/index.ts
  11. 53 0
      src/api/riskctrl/case/types.ts
  12. 76 0
      src/api/riskctrl/dangerSource/index.ts
  13. 58 0
      src/api/riskctrl/dangerSource/types.ts
  14. 27 0
      src/api/riskctrl/equipmentMaintenance/index.ts
  15. 39 0
      src/api/riskctrl/equipmentMaintenance/types.ts
  16. 59 0
      src/api/riskctrl/event/index.ts
  17. 58 0
      src/api/riskctrl/event/types.ts
  18. 109 0
      src/api/riskctrl/hazard/index.ts
  19. 79 0
      src/api/riskctrl/hazard/types.ts
  20. 31 0
      src/api/riskctrl/noticeAnnouncement/index.ts
  21. 38 0
      src/api/riskctrl/noticeAnnouncement/types.ts
  22. 23 0
      src/api/riskctrl/patrolPoint/index.ts
  23. 42 0
      src/api/riskctrl/patrolPoint/types.ts
  24. 15 0
      src/api/riskctrl/patrolRecord/index.ts
  25. 34 0
      src/api/riskctrl/patrolRecord/types.ts
  26. 66 0
      src/api/riskctrl/personInfo/index.ts
  27. 65 0
      src/api/riskctrl/personInfo/types.ts
  28. 39 0
      src/api/riskctrl/publicOpinion/index.ts
  29. 70 0
      src/api/riskctrl/publicOpinion/types.ts
  30. 35 0
      src/api/riskctrl/riskWarning/index.ts
  31. 59 0
      src/api/riskctrl/riskWarning/types.ts
  32. 70 0
      src/api/riskctrl/rule/index.ts
  33. 46 0
      src/api/riskctrl/rule/types.ts
  34. 67 0
      src/api/riskctrl/safetyTraining/index.ts
  35. 47 0
      src/api/riskctrl/safetyTraining/types.ts
  36. 59 0
      src/api/riskctrl/score/index.ts
  37. 53 0
      src/api/riskctrl/score/types.ts
  38. 31 0
      src/api/riskctrl/specialEquipment/index.ts
  39. 57 0
      src/api/riskctrl/specialEquipment/types.ts
  40. 70 0
      src/api/riskctrl/whitelist/index.ts
  41. 39 0
      src/api/riskctrl/whitelist/types.ts
  42. 35 0
      src/api/riskctrl/workReport/index.ts
  43. 88 0
      src/api/riskctrl/workReport/types.ts
  44. 9 9
      src/assets/styles/sidebar.scss
  45. 17 17
      src/assets/styles/variables.module.scss
  46. 63 61
      src/layout/components/Navbar.vue
  47. 2 19
      src/layout/components/Sidebar/Logo.vue
  48. 53 9
      src/layout/components/Sidebar/index.vue
  49. 2 2
      src/layout/index.vue
  50. 1 1
      src/router/index.ts
  51. 715 130
      src/views/index.vue
  52. 472 0
      src/views/riskctrl/accidentReport/index.vue
  53. 404 0
      src/views/riskctrl/auditPersonChange/index.vue
  54. 277 0
      src/views/riskctrl/blacklist/index.vue
  55. 312 0
      src/views/riskctrl/case/index.vue
  56. 412 0
      src/views/riskctrl/dangerSource/index.vue
  57. 321 0
      src/views/riskctrl/event/index.vue
  58. 452 0
      src/views/riskctrl/hazard/index.vue
  59. 267 0
      src/views/riskctrl/noticeAnnouncement/index.vue
  60. 401 0
      src/views/riskctrl/patrolPoint/index.vue
  61. 272 0
      src/views/riskctrl/patrolRecord/index.vue
  62. 592 0
      src/views/riskctrl/personInfo/index.vue
  63. 437 0
      src/views/riskctrl/publicOpinion/index.vue
  64. 341 0
      src/views/riskctrl/riskWarning/index.vue
  65. 329 0
      src/views/riskctrl/rule/index.vue
  66. 435 0
      src/views/riskctrl/safetyTraining/index.vue
  67. 298 0
      src/views/riskctrl/score/index.vue
  68. 537 0
      src/views/riskctrl/specialEquipment/index.vue
  69. 313 0
      src/views/riskctrl/statistics/index.vue
  70. 276 0
      src/views/riskctrl/whitelist/index.vue
  71. 755 0
      src/views/riskctrl/workReport/index.vue
  72. 1 1
      vite.config.ts

+ 3 - 3
.env.development

@@ -1,6 +1,6 @@
 # 页面标题
-VITE_APP_TITLE = RuoYi-Vue-Plus多租户管理系统
-VITE_APP_LOGO_TITLE = RuoYi-Vue-Plus
+VITE_APP_TITLE = 企业安全风控平台
+VITE_APP_LOGO_TITLE = 企业安全风控平台
 
 # 开发环境配置
 VITE_APP_ENV = 'development'
@@ -17,7 +17,7 @@ VITE_APP_MONITOR_ADMIN = 'http://localhost:9090/admin/applications'
 # SnailJob 控制台地址
 VITE_APP_SNAILJOB_ADMIN = 'http://localhost:8800/snail-job'
 
-VITE_APP_PORT = 80
+VITE_APP_PORT = 9223
 
 # 接口加密功能开关(如需关闭 后端也必须对应关闭)
 VITE_APP_ENCRYPT = true

+ 3 - 3
.env.production

@@ -1,6 +1,6 @@
 # 页面标题
-VITE_APP_TITLE = RuoYi-Vue-Plus多租户管理系统
-VITE_APP_LOGO_TITLE = RuoYi-Vue-Plus
+VITE_APP_TITLE = 企业安全风控平台
+VITE_APP_LOGO_TITLE = 企业安全风控平台
 
 # 生产环境配置
 VITE_APP_ENV = 'production'
@@ -20,7 +20,7 @@ VITE_APP_BASE_API = '/prod-api'
 # 是否在打包时开启压缩,支持 gzip 和 brotli
 VITE_BUILD_COMPRESS = gzip
 
-VITE_APP_PORT = 80
+VITE_APP_PORT = 9223
 
 # 接口加密功能开关(如需关闭 后端也必须对应关闭)
 VITE_APP_ENCRYPT = true

+ 300 - 4
src/api/menu.ts

@@ -4,8 +4,304 @@ import { RouteRecordRaw } from 'vue-router';
 
 // 获取路由
 export function getRouters(): AxiosPromise<RouteRecordRaw[]> {
-  return request({
-    url: '/system/menu/getRouters',
-    method: 'get'
-  });
+  // 返回模拟菜单数据,绕过后端依赖
+  const mockMenuData = {
+    data: [
+      {
+        path: '/riskctrl/hazard',
+        component: 'Layout',
+        redirect: '/riskctrl/hazard',
+        name: 'Hazard',
+        meta: { title: '安全隐患', icon: 'warning' },
+        children: [
+          {
+            path: '',
+            component: 'riskctrl/hazard/index',
+            name: 'HazardIndex',
+            meta: { title: '安全隐患', icon: 'warning' }
+          }
+        ]
+      },
+      {
+        path: '/riskctrl/workReport',
+        component: 'Layout',
+        redirect: '/riskctrl/workReport',
+        name: 'WorkReport',
+        meta: { title: '作业上报', icon: 'edit' },
+        children: [
+          {
+            path: '',
+            component: 'riskctrl/workReport/index',
+            name: 'WorkReportIndex',
+            meta: { title: '作业上报', icon: 'edit' }
+          }
+        ]
+      },
+      {
+        path: '/riskctrl/personInfo',
+        component: 'Layout',
+        redirect: '/riskctrl/personInfo',
+        name: 'PersonInfo',
+        meta: { title: '人员信息', icon: 'user' },
+        children: [
+          {
+            path: '',
+            component: 'riskctrl/personInfo/index',
+            name: 'PersonInfoIndex',
+            meta: { title: '人员信息', icon: 'user' }
+          }
+        ]
+      },
+      {
+        path: '/riskctrl/audit',
+        component: 'Layout',
+        redirect: '/riskctrl/audit',
+        name: 'Audit',
+        meta: { title: '审计工作', icon: 'chart' },
+        children: [
+          {
+            path: '',
+            component: 'riskctrl/audit/index',
+            name: 'AuditIndex',
+            meta: { title: '审计工作', icon: 'chart' }
+          }
+        ]
+      },
+      {
+        path: '/riskctrl/riskWarning',
+        component: 'Layout',
+        redirect: '/riskctrl/riskWarning',
+        name: 'RiskWarning',
+        meta: { title: '风险警示', icon: 'alert' },
+        children: [
+          {
+            path: '',
+            component: 'riskctrl/riskWarning/index',
+            name: 'RiskWarningIndex',
+            meta: { title: '风险警示', icon: 'alert' }
+          }
+        ]
+      },
+      {
+        path: '/riskctrl/specialEquipment1',
+        component: 'Layout',
+        redirect: '/riskctrl/specialEquipment1',
+        name: 'SpecialEquipment1',
+        meta: { title: '特种设备', icon: 'equipment' },
+        children: [
+          {
+            path: '',
+            component: 'riskctrl/specialEquipment/index',
+            name: 'SpecialEquipment1Index',
+            meta: { title: '特种设备', icon: 'equipment' }
+          }
+        ]
+      },
+      {
+        path: '/riskctrl/specialEquipment2',
+        component: 'Layout',
+        redirect: '/riskctrl/specialEquipment2',
+        name: 'SpecialEquipment2',
+        meta: { title: '特种设备', icon: 'equipment' },
+        children: [
+          {
+            path: '',
+            component: 'riskctrl/specialEquipment/index',
+            name: 'SpecialEquipment2Index',
+            meta: { title: '特种设备', icon: 'equipment' }
+          }
+        ]
+      },
+      {
+        path: '/riskctrl/safetyEquipment',
+        component: 'Layout',
+        redirect: '/riskctrl/safetyEquipment',
+        name: 'SafetyEquipment',
+        meta: { title: '安全设备', icon: 'shield' },
+        children: [
+          {
+            path: '',
+            component: 'riskctrl/safetyEquipment/index',
+            name: 'SafetyEquipmentIndex',
+            meta: { title: '安全设备', icon: 'shield' }
+          }
+        ]
+      },
+      {
+        path: '/riskctrl/safetyTraining',
+        component: 'Layout',
+        redirect: '/riskctrl/safetyTraining',
+        name: 'SafetyTraining',
+        meta: { title: '安全培训', icon: 'education' },
+        children: [
+          {
+            path: '',
+            component: 'riskctrl/safetyTraining/index',
+            name: 'SafetyTrainingIndex',
+            meta: { title: '安全培训', icon: 'education' }
+          }
+        ]
+      },
+      {
+        path: '/riskctrl/publicOpinion',
+        component: 'Layout',
+        redirect: '/riskctrl/publicOpinion',
+        name: 'PublicOpinion',
+        meta: { title: '舆情推送', icon: 'message' },
+        children: [
+          {
+            path: '',
+            component: 'riskctrl/publicOpinion/index',
+            name: 'PublicOpinionIndex',
+            meta: { title: '舆情推送', icon: 'message' }
+          }
+        ]
+      },
+      {
+        path: '/riskctrl/accidentReport',
+        component: 'Layout',
+        redirect: '/riskctrl/accidentReport',
+        name: 'AccidentReport',
+        meta: { title: '事故上报', icon: 'bug' },
+        children: [
+          {
+            path: '',
+            component: 'riskctrl/accidentReport/index',
+            name: 'AccidentReportIndex',
+            meta: { title: '事故上报', icon: 'bug' }
+          }
+        ]
+      },
+      {
+        path: '/system',
+        component: 'Layout',
+        redirect: '/system/user',
+        name: 'System',
+        meta: { title: '系统管理', icon: 'setting', alwaysShow: true },
+        children: [
+          {
+            path: 'user',
+            component: 'system/user/index',
+            name: 'User',
+            meta: { title: '用户管理', icon: 'user' }
+          },
+          {
+            path: 'role',
+            component: 'system/role/index',
+            name: 'Role',
+            meta: { title: '角色管理', icon: 'peoples' }
+          },
+          {
+            path: 'menu',
+            component: 'system/menu/index',
+            name: 'Menu',
+            meta: { title: '菜单管理', icon: 'list' }
+          },
+          {
+            path: 'dept',
+            component: 'system/dept/index',
+            name: 'Dept',
+            meta: { title: '部门管理', icon: 'tree' }
+          }
+        ]
+      },
+      {
+        path: '/tenant',
+        component: 'Layout',
+        redirect: '/tenant/tenantList',
+        name: 'Tenant',
+        meta: { title: '租户管理', icon: 'company', alwaysShow: true },
+        children: [
+          {
+            path: 'tenantList',
+            component: 'tenant/tenantList/index',
+            name: 'TenantList',
+            meta: { title: '租户列表', icon: 'list' }
+          },
+          {
+            path: 'tenantDetail',
+            component: 'tenant/tenantDetail/index',
+            name: 'TenantDetail',
+            meta: { title: '租户详情', icon: 'info' }
+          }
+        ]
+      },
+      {
+        path: '/monitor',
+        component: 'Layout',
+        redirect: '/monitor/dashboard',
+        name: 'Monitor',
+        meta: { title: '系统监控', icon: 'monitor', alwaysShow: true },
+        children: [
+          {
+            path: 'dashboard',
+            component: 'monitor/dashboard/index',
+            name: 'Dashboard',
+            meta: { title: '监控面板', icon: 'dashboard' }
+          },
+          {
+            path: 'server',
+            component: 'monitor/server/index',
+            name: 'Server',
+            meta: { title: '服务监控', icon: 'server' }
+          }
+        ]
+      },
+      {
+        path: '/tool',
+        component: 'Layout',
+        redirect: '/tool/gen',
+        name: 'Tool',
+        meta: { title: '系统工具', icon: 'tool', alwaysShow: true },
+        children: [
+          {
+            path: 'gen',
+            component: 'tool/gen/index',
+            name: 'Gen',
+            meta: { title: '代码生成', icon: 'code' }
+          },
+          {
+            path: 'swagger',
+            component: 'tool/swagger/index',
+            name: 'Swagger',
+            meta: { title: '接口文档', icon: 'file' }
+          }
+        ]
+      },
+      {
+        path: '/plus',
+        component: 'Layout',
+        redirect: '/plus/index',
+        name: 'Plus',
+        meta: { title: 'PLUS官网', icon: 'link' },
+        children: [
+          {
+            path: 'index',
+            component: 'plus/index',
+            name: 'PlusIndex',
+            meta: { title: 'PLUS官网', icon: 'link' }
+          }
+        ]
+      },
+      {
+        path: '/test',
+        component: 'Layout',
+        redirect: '/test/index',
+        name: 'Test',
+        meta: { title: '测试菜单', icon: 'star', alwaysShow: true },
+        children: [
+          {
+            path: 'index',
+            component: 'test/index',
+            name: 'TestIndex',
+            meta: { title: '测试页面', icon: 'star' }
+          }
+        ]
+      }
+    ]
+  };
+  
+  return new Promise((resolve) => {
+    resolve(mockMenuData);
+  }) as unknown as AxiosPromise<RouteRecordRaw[]>;
 }

+ 31 - 0
src/api/riskctrl/accidentReport/index.ts

@@ -0,0 +1,31 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import { AccidentReportForm, AccidentReportQuery, AccidentReportVO, AccidentStats, AccidentTrend } from './types';
+
+export const listAccidentReport = (query: AccidentReportQuery): AxiosPromise<AccidentReportVO[]> => {
+  return request({ url: '/riskctrl/accidentReport/list', method: 'get', params: query });
+};
+
+export const getAccidentReport = (accidentId: string | number): AxiosPromise<AccidentReportVO> => {
+  return request({ url: '/riskctrl/accidentReport/' + accidentId, method: 'get' });
+};
+
+export const addAccidentReport = (data: AccidentReportForm) => {
+  return request({ url: '/riskctrl/accidentReport', method: 'post', data });
+};
+
+export const updateAccidentReport = (data: AccidentReportForm) => {
+  return request({ url: '/riskctrl/accidentReport', method: 'put', data });
+};
+
+export const delAccidentReport = (accidentId: Array<string | number> | string | number) => {
+  return request({ url: '/riskctrl/accidentReport/' + accidentId, method: 'delete' });
+};
+
+export const getAccidentStats = (): AxiosPromise<AccidentStats> => {
+  return request({ url: '/riskctrl/accidentReport/stats', method: 'get' });
+};
+
+export const getAccidentTrend = (): AxiosPromise<AccidentTrend[]> => {
+  return request({ url: '/riskctrl/accidentReport/trend', method: 'get' });
+};

+ 75 - 0
src/api/riskctrl/accidentReport/types.ts

@@ -0,0 +1,75 @@
+/**
+ * 事故上报查询对象类型
+ */
+export interface AccidentReportQuery extends PageQuery {
+  keyword?: string;
+  status?: string;
+  level?: string;
+  type?: string;
+}
+
+/**
+ * 事故上报返回对象
+ */
+export interface AccidentReportVO extends BaseEntity {
+  accidentId: string | number;
+  accidentNo: string;
+  accidentName: string;
+  accidentTime: string;
+  accidentLocation: string;
+  level: string;
+  type: string;
+  involvedPerson: string;
+  deathCount: number;
+  injuryCount: number;
+  accidentBrief: string;
+  preliminaryAnalysis: string;
+  reporter: string;
+  photos: string;
+  status: string;
+  remark: string;
+}
+
+/**
+ * 事故上报表单类型
+ */
+export interface AccidentReportForm {
+  accidentId?: string | number;
+  accidentNo?: string;
+  accidentName: string;
+  accidentTime?: string;
+  accidentLocation?: string;
+  level: string;
+  type: string;
+  involvedPerson?: string;
+  deathCount?: number;
+  injuryCount?: number;
+  accidentBrief?: string;
+  preliminaryAnalysis?: string;
+  reporter?: string;
+  photos?: string;
+  status: string;
+  remark?: string;
+}
+
+/**
+ * 事故统计
+ */
+export interface AccidentStats {
+  monthTotal: number;
+  monthRate: string;
+  investigating: number;
+  investigatingRate: string;
+  closed: number;
+  closedRate: string;
+  pendingAssessment: number;
+  pendingRate: string;
+}
+
+/**
+ * 事故趋势
+ */
+export interface AccidentTrend {
+  month: string;
+  count: number;
+}

+ 27 - 0
src/api/riskctrl/auditPersonChange/index.ts

@@ -0,0 +1,27 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import { AuditPersonChangeForm, AuditPersonChangeQuery, AuditPersonChangeVO, AuditStats } from './types';
+
+export const listAuditPersonChange = (query: AuditPersonChangeQuery): AxiosPromise<AuditPersonChangeVO[]> => {
+  return request({ url: '/riskctrl/auditPersonChange/list', method: 'get', params: query });
+};
+
+export const getAuditPersonChange = (changeId: string | number): AxiosPromise<AuditPersonChangeVO> => {
+  return request({ url: '/riskctrl/auditPersonChange/' + changeId, method: 'get' });
+};
+
+export const addAuditPersonChange = (data: AuditPersonChangeForm) => {
+  return request({ url: '/riskctrl/auditPersonChange', method: 'post', data });
+};
+
+export const updateAuditPersonChange = (data: AuditPersonChangeForm) => {
+  return request({ url: '/riskctrl/auditPersonChange', method: 'put', data });
+};
+
+export const delAuditPersonChange = (changeId: Array<string | number> | string | number) => {
+  return request({ url: '/riskctrl/auditPersonChange/' + changeId, method: 'delete' });
+};
+
+export const getAuditStats = (): AxiosPromise<AuditStats> => {
+  return request({ url: '/riskctrl/auditPersonChange/stats', method: 'get' });
+};

+ 56 - 0
src/api/riskctrl/auditPersonChange/types.ts

@@ -0,0 +1,56 @@
+/**
+ * 审计人员变动查询对象类型
+ */
+export interface AuditPersonChangeQuery extends PageQuery {
+  employeeName?: string;
+  changeType?: string;
+  auditStatus?: string;
+  changeDate?: string;
+}
+
+/**
+ * 审计人员变动返回对象
+ */
+export interface AuditPersonChangeVO extends BaseEntity {
+  changeId: string | number;
+  employeeName: string;
+  department: string;
+  oldPosition: string;
+  newPosition: string;
+  changeType: string;
+  changeDate: string;
+  auditStatus: string;
+  auditResult: string;
+  auditFiles: string;
+  auditTime: string;
+  auditor: string;
+  remark: string;
+}
+
+/**
+ * 审计人员变动表单类型
+ */
+export interface AuditPersonChangeForm {
+  changeId?: string | number;
+  employeeName: string;
+  department?: string;
+  oldPosition?: string;
+  newPosition?: string;
+  changeType: string;
+  changeDate?: string;
+  auditStatus?: string;
+  auditResult?: string;
+  auditFiles?: string;
+  auditTime?: string;
+  auditor?: string;
+  remark?: string;
+}
+
+/**
+ * 审计统计
+ */
+export interface AuditStats {
+  totalCount: number;
+  completedCount: number;
+  pendingCount: number;
+}

+ 70 - 0
src/api/riskctrl/blacklist/index.ts

@@ -0,0 +1,70 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import { BlacklistForm, BlacklistQuery, BlacklistVO } from './types';
+
+/**
+ * 查询黑名单列表
+ * @param query
+ */
+export const listBlacklist = (query: BlacklistQuery): AxiosPromise<BlacklistVO[]> => {
+  return request({
+    url: '/riskctrl/blacklist/list',
+    method: 'get',
+    params: query
+  });
+};
+
+/**
+ * 获取黑名单详情
+ * @param blackId
+ */
+export const getBlacklist = (blackId: string | number): AxiosPromise<BlacklistVO> => {
+  return request({
+    url: '/riskctrl/blacklist/' + blackId,
+    method: 'get'
+  });
+};
+
+/**
+ * 新增黑名单
+ */
+export const addBlacklist = (data: BlacklistForm) => {
+  return request({
+    url: '/riskctrl/blacklist',
+    method: 'post',
+    data: data
+  });
+};
+
+/**
+ * 修改黑名单
+ */
+export const updateBlacklist = (data: BlacklistForm) => {
+  return request({
+    url: '/riskctrl/blacklist',
+    method: 'put',
+    data: data
+  });
+};
+
+/**
+ * 修改黑名单状态
+ */
+export const changeBlacklistStatus = (blackId: string | number, status: string) => {
+  return request({
+    url: '/riskctrl/blacklist/changeStatus',
+    method: 'put',
+    data: { blackId, status }
+  });
+};
+
+/**
+ * 删除黑名单
+ * @param blackId 黑名单ID
+ */
+export const delBlacklist = (blackId: Array<string | number> | string | number) => {
+  return request({
+    url: '/riskctrl/blacklist/' + blackId,
+    method: 'delete'
+  });
+};

+ 39 - 0
src/api/riskctrl/blacklist/types.ts

@@ -0,0 +1,39 @@
+/**
+ * 黑名单查询对象类型
+ */
+export interface BlacklistQuery extends PageQuery {
+  blackType?: string;
+  blackValue?: string;
+  blackName?: string;
+  status?: string;
+}
+
+/**
+ * 黑名单返回对象
+ */
+export interface BlacklistVO extends BaseEntity {
+  blackId: string | number;
+  blackType: string;
+  blackValue: string;
+  blackName: string;
+  reason: string;
+  source: string;
+  expireTime: string;
+  status: string;
+  remark: string;
+}
+
+/**
+ * 黑名单表单类型
+ */
+export interface BlacklistForm {
+  blackId?: string | number;
+  blackType: string;
+  blackValue: string;
+  blackName?: string;
+  reason?: string;
+  source?: string;
+  expireTime?: string;
+  status: string;
+  remark?: string;
+}

+ 59 - 0
src/api/riskctrl/case/index.ts

@@ -0,0 +1,59 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import { CaseForm, CaseQuery, CaseVO } from './types';
+
+/**
+ * 查询风控案例列表
+ * @param query
+ */
+export const listCase = (query: CaseQuery): AxiosPromise<CaseVO[]> => {
+  return request({
+    url: '/riskctrl/case/list',
+    method: 'get',
+    params: query
+  });
+};
+
+/**
+ * 获取风控案例详情
+ * @param caseId
+ */
+export const getCase = (caseId: string | number): AxiosPromise<CaseVO> => {
+  return request({
+    url: '/riskctrl/case/' + caseId,
+    method: 'get'
+  });
+};
+
+/**
+ * 新增风控案例
+ */
+export const addCase = (data: CaseForm) => {
+  return request({
+    url: '/riskctrl/case',
+    method: 'post',
+    data: data
+  });
+};
+
+/**
+ * 修改风控案例
+ */
+export const updateCase = (data: CaseForm) => {
+  return request({
+    url: '/riskctrl/case',
+    method: 'put',
+    data: data
+  });
+};
+
+/**
+ * 删除风控案例
+ * @param caseId 案例ID
+ */
+export const delCase = (caseId: Array<string | number> | string | number) => {
+  return request({
+    url: '/riskctrl/case/' + caseId,
+    method: 'delete'
+  });
+};

+ 53 - 0
src/api/riskctrl/case/types.ts

@@ -0,0 +1,53 @@
+/**
+ * 风控案例查询对象类型
+ */
+export interface CaseQuery extends PageQuery {
+  caseNo?: string;
+  caseName?: string;
+  caseType?: string;
+  riskLevel?: string;
+  status?: string;
+  userId?: string | number;
+}
+
+/**
+ * 风控案例返回对象
+ */
+export interface CaseVO extends BaseEntity {
+  caseId: string | number;
+  caseNo: string;
+  caseName: string;
+  caseType: string;
+  caseDesc: string;
+  eventId: string | number;
+  eventNo: string;
+  userId: string | number;
+  userName: string;
+  riskLevel: string;
+  riskScore: number;
+  hitRules: string;
+  caseResult: string;
+  status: string;
+  remark: string;
+}
+
+/**
+ * 风控案例表单类型
+ */
+export interface CaseForm {
+  caseId?: string | number;
+  caseNo?: string;
+  caseName: string;
+  caseType: string;
+  caseDesc?: string;
+  eventId?: string | number;
+  eventNo?: string;
+  userId?: string | number;
+  userName?: string;
+  riskLevel?: string;
+  riskScore?: number;
+  hitRules?: string;
+  caseResult?: string;
+  status: string;
+  remark?: string;
+}

+ 76 - 0
src/api/riskctrl/dangerSource/index.ts

@@ -0,0 +1,76 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import { DangerSourceForm, DangerSourceQuery, DangerSourceVO, DangerSourceStats, DangerSourceDistribution } from './types';
+
+/**
+ * 查询危险源列表
+ */
+export const listDangerSource = (query: DangerSourceQuery): AxiosPromise<DangerSourceVO[]> => {
+  return request({
+    url: '/riskctrl/dangerSource/list',
+    method: 'get',
+    params: query
+  });
+};
+
+/**
+ * 获取危险源详情
+ */
+export const getDangerSource = (sourceId: string | number): AxiosPromise<DangerSourceVO> => {
+  return request({
+    url: '/riskctrl/dangerSource/' + sourceId,
+    method: 'get'
+  });
+};
+
+/**
+ * 新增危险源
+ */
+export const addDangerSource = (data: DangerSourceForm) => {
+  return request({
+    url: '/riskctrl/dangerSource',
+    method: 'post',
+    data: data
+  });
+};
+
+/**
+ * 修改危险源
+ */
+export const updateDangerSource = (data: DangerSourceForm) => {
+  return request({
+    url: '/riskctrl/dangerSource',
+    method: 'put',
+    data: data
+  });
+};
+
+/**
+ * 删除危险源
+ */
+export const delDangerSource = (sourceId: Array<string | number> | string | number) => {
+  return request({
+    url: '/riskctrl/dangerSource/' + sourceId,
+    method: 'delete'
+  });
+};
+
+/**
+ * 危险源统计
+ */
+export const getDangerSourceStats = (): AxiosPromise<DangerSourceStats> => {
+  return request({
+    url: '/riskctrl/dangerSource/stats',
+    method: 'get'
+  });
+};
+
+/**
+ * 危险源分布
+ */
+export const getDangerSourceDistribution = (): AxiosPromise<DangerSourceDistribution[]> => {
+  return request({
+    url: '/riskctrl/dangerSource/distribution',
+    method: 'get'
+  });
+};

+ 58 - 0
src/api/riskctrl/dangerSource/types.ts

@@ -0,0 +1,58 @@
+/**
+ * 危险源查询对象类型
+ */
+export interface DangerSourceQuery extends PageQuery {
+  sourceNo?: string;
+  sourceName?: string;
+  sourceType?: string;
+  status?: string;
+}
+
+/**
+ * 危险源返回对象
+ */
+export interface DangerSourceVO extends BaseEntity {
+  sourceId: string | number;
+  sourceNo: string;
+  sourceName: string;
+  sourceType: string;
+  location: string;
+  possibleHarm: string;
+  preventionMethod: string;
+  reportTime: string;
+  status: string;
+  remark: string;
+}
+
+/**
+ * 危险源表单类型
+ */
+export interface DangerSourceForm {
+  sourceId?: string | number;
+  sourceNo?: string;
+  sourceName: string;
+  sourceType: string;
+  location?: string;
+  possibleHarm?: string;
+  preventionMethod?: string;
+  reportTime?: string;
+  status: string;
+  remark?: string;
+}
+
+/**
+ * 危险源统计
+ */
+export interface DangerSourceStats {
+  totalCount: number;
+  processedCount: number;
+  pendingCount: number;
+}
+
+/**
+ * 危险源分布
+ */
+export interface DangerSourceDistribution {
+  type: string;
+  count: number;
+}

+ 27 - 0
src/api/riskctrl/equipmentMaintenance/index.ts

@@ -0,0 +1,27 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import { EquipmentMaintenanceForm, EquipmentMaintenanceQuery, EquipmentMaintenanceVO } from './types';
+
+export const listEquipmentMaintenance = (query: EquipmentMaintenanceQuery): AxiosPromise<EquipmentMaintenanceVO[]> => {
+  return request({ url: '/riskctrl/equipmentMaintenance/list', method: 'get', params: query });
+};
+
+export const getEquipmentMaintenance = (maintenanceId: string | number): AxiosPromise<EquipmentMaintenanceVO> => {
+  return request({ url: '/riskctrl/equipmentMaintenance/' + maintenanceId, method: 'get' });
+};
+
+export const addEquipmentMaintenance = (data: EquipmentMaintenanceForm) => {
+  return request({ url: '/riskctrl/equipmentMaintenance', method: 'post', data });
+};
+
+export const updateEquipmentMaintenance = (data: EquipmentMaintenanceForm) => {
+  return request({ url: '/riskctrl/equipmentMaintenance', method: 'put', data });
+};
+
+export const delEquipmentMaintenance = (maintenanceId: Array<string | number> | string | number) => {
+  return request({ url: '/riskctrl/equipmentMaintenance/' + maintenanceId, method: 'delete' });
+};
+
+export const getMaintenanceByEquipment = (equipmentId: string | number): AxiosPromise<any[]> => {
+  return request({ url: '/riskctrl/equipmentMaintenance/byEquipment/' + equipmentId, method: 'get' });
+};

+ 39 - 0
src/api/riskctrl/equipmentMaintenance/types.ts

@@ -0,0 +1,39 @@
+/**
+ * 设备维护记录查询对象类型
+ */
+export interface EquipmentMaintenanceQuery extends PageQuery {
+  equipmentId?: string | number;
+  maintenanceStatus?: string;
+}
+
+/**
+ * 设备维护记录返回对象
+ */
+export interface EquipmentMaintenanceVO extends BaseEntity {
+  maintenanceId: string | number;
+  equipmentId: string | number;
+  equipmentName: string;
+  maintenanceStartTime: string;
+  maintenanceEndTime: string;
+  maintenancePerson: string;
+  onSitePerson: string;
+  maintenanceStatus: string;
+  maintenancePhotos: string;
+  remark: string;
+}
+
+/**
+ * 设备维护记录表单类型
+ */
+export interface EquipmentMaintenanceForm {
+  maintenanceId?: string | number;
+  equipmentId: string | number;
+  equipmentName?: string;
+  maintenanceStartTime?: string;
+  maintenanceEndTime?: string;
+  maintenancePerson?: string;
+  onSitePerson?: string;
+  maintenanceStatus?: string;
+  maintenancePhotos?: string;
+  remark?: string;
+}

+ 59 - 0
src/api/riskctrl/event/index.ts

@@ -0,0 +1,59 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import { EventForm, EventQuery, EventVO } from './types';
+
+/**
+ * 查询风控事件列表
+ * @param query
+ */
+export const listEvent = (query: EventQuery): AxiosPromise<EventVO[]> => {
+  return request({
+    url: '/riskctrl/event/list',
+    method: 'get',
+    params: query
+  });
+};
+
+/**
+ * 获取风控事件详情
+ * @param eventId
+ */
+export const getEvent = (eventId: string | number): AxiosPromise<EventVO> => {
+  return request({
+    url: '/riskctrl/event/' + eventId,
+    method: 'get'
+  });
+};
+
+/**
+ * 新增风控事件
+ */
+export const addEvent = (data: EventForm) => {
+  return request({
+    url: '/riskctrl/event',
+    method: 'post',
+    data: data
+  });
+};
+
+/**
+ * 修改风控事件
+ */
+export const updateEvent = (data: EventForm) => {
+  return request({
+    url: '/riskctrl/event',
+    method: 'put',
+    data: data
+  });
+};
+
+/**
+ * 删除风控事件
+ * @param eventId 事件ID
+ */
+export const delEvent = (eventId: Array<string | number> | string | number) => {
+  return request({
+    url: '/riskctrl/event/' + eventId,
+    method: 'delete'
+  });
+};

+ 58 - 0
src/api/riskctrl/event/types.ts

@@ -0,0 +1,58 @@
+/**
+ * 风控事件查询对象类型
+ */
+export interface EventQuery extends PageQuery {
+  eventNo?: string;
+  eventType?: string;
+  riskLevel?: string;
+  status?: string;
+  userId?: string | number;
+  userName?: string;
+  ipAddress?: string;
+}
+
+/**
+ * 风控事件返回对象
+ */
+export interface EventVO extends BaseEntity {
+  eventId: string | number;
+  eventNo: string;
+  eventType: string;
+  eventSource: string;
+  userId: string | number;
+  userName: string;
+  deviceId: string;
+  ipAddress: string;
+  location: string;
+  eventData: string;
+  riskLevel: string;
+  riskScore: number;
+  hitRules: string;
+  actionTaken: string;
+  status: string;
+  remark: string;
+  eventTime: string;
+}
+
+/**
+ * 风控事件表单类型
+ */
+export interface EventForm {
+  eventId?: string | number;
+  eventNo?: string;
+  eventType: string;
+  eventSource?: string;
+  userId?: string | number;
+  userName?: string;
+  deviceId?: string;
+  ipAddress?: string;
+  location?: string;
+  eventData?: string;
+  riskLevel?: string;
+  riskScore?: number;
+  hitRules?: string;
+  actionTaken?: string;
+  status: string;
+  remark?: string;
+  eventTime?: string;
+}

+ 109 - 0
src/api/riskctrl/hazard/index.ts

@@ -0,0 +1,109 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import { HazardForm, HazardQuery, HazardVO, HazardStats, HazardTrend, HazardDistribution } from './types';
+
+/**
+ * 查询安全隐患列表
+ */
+export const listHazard = (query: HazardQuery): AxiosPromise<HazardVO[]> => {
+  return request({
+    url: '/riskctrl/hazard/list',
+    method: 'get',
+    params: query
+  });
+};
+
+/**
+ * 获取安全隐患详情
+ */
+export const getHazard = (hazardId: string | number): AxiosPromise<HazardVO> => {
+  return request({
+    url: '/riskctrl/hazard/' + hazardId,
+    method: 'get'
+  });
+};
+
+/**
+ * 新增安全隐患
+ */
+export const addHazard = (data: HazardForm) => {
+  return request({
+    url: '/riskctrl/hazard',
+    method: 'post',
+    data: data
+  });
+};
+
+/**
+ * 修改安全隐患
+ */
+export const updateHazard = (data: HazardForm) => {
+  return request({
+    url: '/riskctrl/hazard',
+    method: 'put',
+    data: data
+  });
+};
+
+/**
+ * 处理安全隐患
+ */
+export const processHazard = (data: HazardForm) => {
+  return request({
+    url: '/riskctrl/hazard/process',
+    method: 'put',
+    data: data
+  });
+};
+
+/**
+ * 删除安全隐患
+ */
+export const delHazard = (hazardId: Array<string | number> | string | number) => {
+  return request({
+    url: '/riskctrl/hazard/' + hazardId,
+    method: 'delete'
+  });
+};
+
+/**
+ * 隐患统计
+ */
+export const getHazardStats = (): AxiosPromise<HazardStats> => {
+  return request({
+    url: '/riskctrl/hazard/stats',
+    method: 'get'
+  });
+};
+
+/**
+ * 隐患趋势
+ */
+export const getHazardTrend = (months: number = 6): AxiosPromise<HazardTrend[]> => {
+  return request({
+    url: '/riskctrl/hazard/trend',
+    method: 'get',
+    params: { months }
+  });
+};
+
+/**
+ * 隐患分布
+ */
+export const getHazardDistribution = (): AxiosPromise<HazardDistribution[]> => {
+  return request({
+    url: '/riskctrl/hazard/distribution',
+    method: 'get'
+  });
+};
+
+/**
+ * 最新隐患
+ */
+export const getHazardLatest = (limit: number = 10): AxiosPromise<HazardVO[]> => {
+  return request({
+    url: '/riskctrl/hazard/latest',
+    method: 'get',
+    params: { limit }
+  });
+};

+ 79 - 0
src/api/riskctrl/hazard/types.ts

@@ -0,0 +1,79 @@
+/**
+ * 安全隐患查询对象类型
+ */
+export interface HazardQuery extends PageQuery {
+  hazardNo?: string;
+  hazardName?: string;
+  hazardType?: string;
+  riskLevel?: string;
+  processStatus?: string;
+  status?: string;
+}
+
+/**
+ * 安全隐患返回对象
+ */
+export interface HazardVO extends BaseEntity {
+  hazardId: string | number;
+  hazardNo: string;
+  hazardName: string;
+  hazardType: string;
+  riskLevel: string;
+  discoverTime: string;
+  hazardLocation: string;
+  hazardDesc: string;
+  photoUrls: string;
+  processStatus: string;
+  processResult: string;
+  processTime: string;
+  processUser: string;
+  status: string;
+  remark: string;
+}
+
+/**
+ * 安全隐患表单类型
+ */
+export interface HazardForm {
+  hazardId?: string | number;
+  hazardNo?: string;
+  hazardName: string;
+  hazardType: string;
+  riskLevel: string;
+  discoverTime?: string;
+  hazardLocation?: string;
+  hazardDesc?: string;
+  photoUrls?: string;
+  processStatus?: string;
+  processResult?: string;
+  processTime?: string;
+  processUser?: string;
+  status: string;
+  remark?: string;
+}
+
+/**
+ * 隐患统计
+ */
+export interface HazardStats {
+  totalCount: number;
+  pendingCount: number;
+  processingCount: number;
+  completedCount: number;
+}
+
+/**
+ * 隐患趋势
+ */
+export interface HazardTrend {
+  month: string;
+  count: number;
+}
+
+/**
+ * 隐患分布
+ */
+export interface HazardDistribution {
+  type: string;
+  count: number;
+}

+ 31 - 0
src/api/riskctrl/noticeAnnouncement/index.ts

@@ -0,0 +1,31 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import { NoticeAnnouncementForm, NoticeAnnouncementQuery, NoticeAnnouncementVO } from './types';
+
+export const listNoticeAnnouncement = (query: NoticeAnnouncementQuery): AxiosPromise<NoticeAnnouncementVO[]> => {
+  return request({ url: '/riskctrl/noticeAnnouncement/list', method: 'get', params: query });
+};
+
+export const getNoticeAnnouncement = (noticeId: string | number): AxiosPromise<NoticeAnnouncementVO> => {
+  return request({ url: '/riskctrl/noticeAnnouncement/' + noticeId, method: 'get' });
+};
+
+export const addNoticeAnnouncement = (data: NoticeAnnouncementForm) => {
+  return request({ url: '/riskctrl/noticeAnnouncement', method: 'post', data });
+};
+
+export const updateNoticeAnnouncement = (data: NoticeAnnouncementForm) => {
+  return request({ url: '/riskctrl/noticeAnnouncement', method: 'put', data });
+};
+
+export const publishNotice = (noticeId: string | number) => {
+  return request({ url: '/riskctrl/noticeAnnouncement/publish/' + noticeId, method: 'put' });
+};
+
+export const delNoticeAnnouncement = (noticeId: Array<string | number> | string | number) => {
+  return request({ url: '/riskctrl/noticeAnnouncement/' + noticeId, method: 'delete' });
+};
+
+export const getPublishedNotices = (limit: number = 10): AxiosPromise<NoticeAnnouncementVO[]> => {
+  return request({ url: '/riskctrl/noticeAnnouncement/published', method: 'get', params: { limit } });
+};

+ 38 - 0
src/api/riskctrl/noticeAnnouncement/types.ts

@@ -0,0 +1,38 @@
+/**
+ * 通知公告查询对象类型
+ */
+export interface NoticeAnnouncementQuery extends PageQuery {
+  noticeTitle?: string;
+  noticeType?: string;
+  status?: string;
+}
+
+/**
+ * 通知公告返回对象
+ */
+export interface NoticeAnnouncementVO extends BaseEntity {
+  noticeId: string | number;
+  noticeTitle: string;
+  noticeType: string;
+  noticeContent: string;
+  publishStatus: string;
+  publishTime: string;
+  publisher: string;
+  status: string;
+  remark: string;
+}
+
+/**
+ * 通知公告表单类型
+ */
+export interface NoticeAnnouncementForm {
+  noticeId?: string | number;
+  noticeTitle: string;
+  noticeType: string;
+  noticeContent?: string;
+  publishStatus?: string;
+  publishTime?: string;
+  publisher?: string;
+  status: string;
+  remark?: string;
+}

+ 23 - 0
src/api/riskctrl/patrolPoint/index.ts

@@ -0,0 +1,23 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import { PatrolPointForm, PatrolPointQuery, PatrolPointVO } from './types';
+
+export const listPatrolPoint = (query: PatrolPointQuery): AxiosPromise<PatrolPointVO[]> => {
+  return request({ url: '/riskctrl/patrolPoint/list', method: 'get', params: query });
+};
+
+export const getPatrolPoint = (pointId: string | number): AxiosPromise<PatrolPointVO> => {
+  return request({ url: '/riskctrl/patrolPoint/' + pointId, method: 'get' });
+};
+
+export const addPatrolPoint = (data: PatrolPointForm) => {
+  return request({ url: '/riskctrl/patrolPoint', method: 'post', data });
+};
+
+export const updatePatrolPoint = (data: PatrolPointForm) => {
+  return request({ url: '/riskctrl/patrolPoint', method: 'put', data });
+};
+
+export const delPatrolPoint = (pointId: Array<string | number> | string | number) => {
+  return request({ url: '/riskctrl/patrolPoint/' + pointId, method: 'delete' });
+};

+ 42 - 0
src/api/riskctrl/patrolPoint/types.ts

@@ -0,0 +1,42 @@
+/**
+ * 巡检点查询对象类型
+ */
+export interface PatrolPointQuery extends PageQuery {
+  pointName?: string;
+  location?: string;
+  responsiblePerson?: string;
+}
+
+/**
+ * 巡检点返回对象
+ */
+export interface PatrolPointVO extends BaseEntity {
+  pointId: string | number;
+  pointName: string;
+  responsiblePerson: string;
+  area: string;
+  address: string;
+  longitude: string;
+  latitude: string;
+  checkTimes: string;
+  nfcTag: string;
+  checkItems: string;
+  remark: string;
+}
+
+/**
+ * 巡检点表单类型
+ */
+export interface PatrolPointForm {
+  pointId?: string | number;
+  pointName: string;
+  responsiblePerson: string;
+  area?: string;
+  address?: string;
+  longitude?: string;
+  latitude?: string;
+  checkTimes?: string;
+  nfcTag?: string;
+  checkItems?: string;
+  remark?: string;
+}

+ 15 - 0
src/api/riskctrl/patrolRecord/index.ts

@@ -0,0 +1,15 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import { PatrolRecordQuery, PatrolRecordVO, PatrolRecordStats } from './types';
+
+export const listPatrolRecord = (query: PatrolRecordQuery): AxiosPromise<PatrolRecordVO[]> => {
+  return request({ url: '/riskctrl/patrolRecord/list', method: 'get', params: query });
+};
+
+export const getPatrolRecord = (recordId: string | number): AxiosPromise<PatrolRecordVO> => {
+  return request({ url: '/riskctrl/patrolRecord/' + recordId, method: 'get' });
+};
+
+export const getPatrolRecordStats = (): AxiosPromise<PatrolRecordStats> => {
+  return request({ url: '/riskctrl/patrolRecord/stats', method: 'get' });
+};

+ 34 - 0
src/api/riskctrl/patrolRecord/types.ts

@@ -0,0 +1,34 @@
+/**
+ * 巡检记录查询对象类型
+ */
+export interface PatrolRecordQuery extends PageQuery {
+  pointId?: string | number;
+  patrolDate?: string;
+  status?: string;
+}
+
+/**
+ * 巡检记录返回对象
+ */
+export interface PatrolRecordVO extends BaseEntity {
+  recordId: string | number;
+  pointId: string | number;
+  pointName: string;
+  patrolTime: string;
+  patrolPerson: string;
+  status: string;
+  result: string;
+  abnormalReason: string;
+  checkDetail: string;
+  remark: string;
+}
+
+/**
+ * 巡检记录统计
+ */
+export interface PatrolRecordStats {
+  todayShouldCheck: number;
+  todayChecked: number;
+  todayAbnormal: number;
+  todayUnchecked: number;
+}

+ 66 - 0
src/api/riskctrl/personInfo/index.ts

@@ -0,0 +1,66 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import { PersonInfoForm, PersonInfoQuery, PersonInfoVO, PersonStats } from './types';
+
+/**
+ * 查询人员信息列表
+ */
+export const listPersonInfo = (query: PersonInfoQuery): AxiosPromise<PersonInfoVO[]> => {
+  return request({
+    url: '/riskctrl/personInfo/list',
+    method: 'get',
+    params: query
+  });
+};
+
+/**
+ * 获取人员信息详情
+ */
+export const getPersonInfo = (personId: string | number): AxiosPromise<PersonInfoVO> => {
+  return request({
+    url: '/riskctrl/personInfo/' + personId,
+    method: 'get'
+  });
+};
+
+/**
+ * 新增人员信息
+ */
+export const addPersonInfo = (data: PersonInfoForm) => {
+  return request({
+    url: '/riskctrl/personInfo',
+    method: 'post',
+    data: data
+  });
+};
+
+/**
+ * 修改人员信息
+ */
+export const updatePersonInfo = (data: PersonInfoForm) => {
+  return request({
+    url: '/riskctrl/personInfo',
+    method: 'put',
+    data: data
+  });
+};
+
+/**
+ * 删除人员信息
+ */
+export const delPersonInfo = (personId: Array<string | number> | string | number) => {
+  return request({
+    url: '/riskctrl/personInfo/' + personId,
+    method: 'delete'
+  });
+};
+
+/**
+ * 人员统计
+ */
+export const getPersonStats = (): AxiosPromise<PersonStats> => {
+  return request({
+    url: '/riskctrl/personInfo/stats',
+    method: 'get'
+  });
+};

+ 65 - 0
src/api/riskctrl/personInfo/types.ts

@@ -0,0 +1,65 @@
+/**
+ * 人员信息查询对象类型
+ */
+export interface PersonInfoQuery extends PageQuery {
+  personName?: string;
+  personType?: string;
+  workStoreId?: string | number;
+  insuranceStatus?: string;
+  status?: string;
+}
+
+/**
+ * 岗位证书
+ */
+export interface Certificate {
+  certType: string;
+  certTypeName?: string;
+  certUrl: string;
+}
+
+/**
+ * 人员信息返回对象
+ */
+export interface PersonInfoVO extends BaseEntity {
+  personId: string | number;
+  personType: string;
+  personName: string;
+  idCard: string;
+  age?: number;
+  workStoreId?: string | number;
+  workStoreName?: string;
+  insuranceStatus: string;
+  insuranceExpireDate?: string;
+  certificates?: string;
+  status: string;
+  remark?: string;
+}
+
+/**
+ * 人员信息表单类型
+ */
+export interface PersonInfoForm {
+  personId?: string | number;
+  personType: string;
+  personName: string;
+  idCard: string;
+  age?: number;
+  workStoreId?: string | number;
+  workStoreName?: string;
+  insuranceStatus: string;
+  insuranceExpireDate?: string;
+  certificates?: string;
+  status: string;
+  remark?: string;
+}
+
+/**
+ * 人员统计
+ */
+export interface PersonStats {
+  total: number;
+  insured: number;
+  uninsured: number;
+  expiringSoon: number;
+}

+ 39 - 0
src/api/riskctrl/publicOpinion/index.ts

@@ -0,0 +1,39 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import { PublicOpinionForm, PublicOpinionQuery, PublicOpinionVO, OpinionStats, OpinionTrend } from './types';
+
+export const listPublicOpinion = (query: PublicOpinionQuery): AxiosPromise<PublicOpinionVO[]> => {
+  return request({ url: '/riskctrl/publicOpinion/list', method: 'get', params: query });
+};
+
+export const getPublicOpinion = (opinionId: string | number): AxiosPromise<PublicOpinionVO> => {
+  return request({ url: '/riskctrl/publicOpinion/' + opinionId, method: 'get' });
+};
+
+export const addPublicOpinion = (data: PublicOpinionForm) => {
+  return request({ url: '/riskctrl/publicOpinion', method: 'post', data });
+};
+
+export const updatePublicOpinion = (data: PublicOpinionForm) => {
+  return request({ url: '/riskctrl/publicOpinion', method: 'put', data });
+};
+
+export const handlePublicOpinion = (data: PublicOpinionForm) => {
+  return request({ url: '/riskctrl/publicOpinion/handle', method: 'put', data });
+};
+
+export const delPublicOpinion = (opinionId: Array<string | number> | string | number) => {
+  return request({ url: '/riskctrl/publicOpinion/' + opinionId, method: 'delete' });
+};
+
+export const getOpinionStats = (): AxiosPromise<OpinionStats> => {
+  return request({ url: '/riskctrl/publicOpinion/stats', method: 'get' });
+};
+
+export const getOpinionTrend = (): AxiosPromise<OpinionTrend[]> => {
+  return request({ url: '/riskctrl/publicOpinion/trend', method: 'get' });
+};
+
+export const getMajorOpinions = (): AxiosPromise<PublicOpinionVO[]> => {
+  return request({ url: '/riskctrl/publicOpinion/major', method: 'get' });
+};

+ 70 - 0
src/api/riskctrl/publicOpinion/types.ts

@@ -0,0 +1,70 @@
+/**
+ * 舆情推送查询对象类型
+ */
+export interface PublicOpinionQuery extends PageQuery {
+  opinionTitle?: string;
+  source?: string;
+  riskLevel?: string;
+  status?: string;
+}
+
+/**
+ * 舆情推送返回对象
+ */
+export interface PublicOpinionVO extends BaseEntity {
+  opinionId: string | number;
+  opinionTitle: string;
+  source: string;
+  publishTime: string;
+  riskLevel: string;
+  responsiblePerson: string;
+  opinionLink: string;
+  opinionContent: string;
+  disposalSuggestion: string;
+  disposalResult: string;
+  disposalTime: string;
+  status: string;
+  remark: string;
+}
+
+/**
+ * 舆情推送表单类型
+ */
+export interface PublicOpinionForm {
+  opinionId?: string | number;
+  opinionTitle: string;
+  source: string;
+  publishTime?: string;
+  riskLevel: string;
+  responsiblePerson?: string;
+  opinionLink?: string;
+  opinionContent?: string;
+  disposalSuggestion?: string;
+  disposalResult?: string;
+  disposalTime?: string;
+  status: string;
+  remark?: string;
+}
+
+/**
+ * 舆情统计
+ */
+export interface OpinionStats {
+  todayTotal: number;
+  todayGrowthRate: string;
+  majorCount: number;
+  majorChange: string;
+  processedCount: number;
+  processRate: string;
+  pendingCount: number;
+}
+
+/**
+ * 舆情趋势
+ */
+export interface OpinionTrend {
+  date: string;
+  total: number;
+  major: number;
+  processed: number;
+}

+ 35 - 0
src/api/riskctrl/riskWarning/index.ts

@@ -0,0 +1,35 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import { RiskWarningForm, RiskWarningQuery, RiskWarningVO, RiskWarningStats, RiskLevelDistribution } from './types';
+
+export const listRiskWarning = (query: RiskWarningQuery): AxiosPromise<RiskWarningVO[]> => {
+  return request({ url: '/riskctrl/riskWarning/list', method: 'get', params: query });
+};
+
+export const getRiskWarning = (warningId: string | number): AxiosPromise<RiskWarningVO> => {
+  return request({ url: '/riskctrl/riskWarning/' + warningId, method: 'get' });
+};
+
+export const addRiskWarning = (data: RiskWarningForm) => {
+  return request({ url: '/riskctrl/riskWarning', method: 'post', data });
+};
+
+export const updateRiskWarning = (data: RiskWarningForm) => {
+  return request({ url: '/riskctrl/riskWarning', method: 'put', data });
+};
+
+export const delRiskWarning = (warningId: Array<string | number> | string | number) => {
+  return request({ url: '/riskctrl/riskWarning/' + warningId, method: 'delete' });
+};
+
+export const getRiskWarningStats = (): AxiosPromise<RiskWarningStats> => {
+  return request({ url: '/riskctrl/riskWarning/stats', method: 'get' });
+};
+
+export const getRiskLevelDistribution = (): AxiosPromise<RiskLevelDistribution[]> => {
+  return request({ url: '/riskctrl/riskWarning/distribution', method: 'get' });
+};
+
+export const getLatestWarnings = (limit: number = 10): AxiosPromise<RiskWarningVO[]> => {
+  return request({ url: '/riskctrl/riskWarning/latest', method: 'get', params: { limit } });
+};

+ 59 - 0
src/api/riskctrl/riskWarning/types.ts

@@ -0,0 +1,59 @@
+/**
+ * 风险警示查询对象类型
+ */
+export interface RiskWarningQuery extends PageQuery {
+  warningTitle?: string;
+  warningType?: string;
+  warningLevel?: string;
+  publishTime?: string;
+}
+
+/**
+ * 风险警示返回对象
+ */
+export interface RiskWarningVO extends BaseEntity {
+  warningId: string | number;
+  warningTitle: string;
+  warningType: string;
+  warningLevel: string;
+  warningContent: string;
+  effectiveTime: string;
+  publishTime: string;
+  publisher: string;
+  status: string;
+  remark: string;
+}
+
+/**
+ * 风险警示表单类型
+ */
+export interface RiskWarningForm {
+  warningId?: string | number;
+  warningTitle: string;
+  warningType: string;
+  warningLevel: string;
+  warningContent?: string;
+  effectiveTime?: string;
+  publishTime?: string;
+  publisher?: string;
+  status: string;
+  remark?: string;
+}
+
+/**
+ * 风险警示统计
+ */
+export interface RiskWarningStats {
+  totalCount: number;
+  majorCount: number;
+  moderateCount: number;
+  minorCount: number;
+}
+
+/**
+ * 风险等级分布
+ */
+export interface RiskLevelDistribution {
+  level: string;
+  count: number;
+}

+ 70 - 0
src/api/riskctrl/rule/index.ts

@@ -0,0 +1,70 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import { RuleForm, RuleQuery, RuleVO } from './types';
+
+/**
+ * 查询风控规则列表
+ * @param query
+ */
+export const listRule = (query: RuleQuery): AxiosPromise<RuleVO[]> => {
+  return request({
+    url: '/riskctrl/rule/list',
+    method: 'get',
+    params: query
+  });
+};
+
+/**
+ * 获取风控规则详情
+ * @param ruleId
+ */
+export const getRule = (ruleId: string | number): AxiosPromise<RuleVO> => {
+  return request({
+    url: '/riskctrl/rule/' + ruleId,
+    method: 'get'
+  });
+};
+
+/**
+ * 新增风控规则
+ */
+export const addRule = (data: RuleForm) => {
+  return request({
+    url: '/riskctrl/rule',
+    method: 'post',
+    data: data
+  });
+};
+
+/**
+ * 修改风控规则
+ */
+export const updateRule = (data: RuleForm) => {
+  return request({
+    url: '/riskctrl/rule',
+    method: 'put',
+    data: data
+  });
+};
+
+/**
+ * 修改风控规则状态
+ */
+export const changeRuleStatus = (ruleId: string | number, status: string) => {
+  return request({
+    url: '/riskctrl/rule/changeStatus',
+    method: 'put',
+    data: { ruleId, status }
+  });
+};
+
+/**
+ * 删除风控规则
+ * @param ruleId 规则ID
+ */
+export const delRule = (ruleId: Array<string | number> | string | number) => {
+  return request({
+    url: '/riskctrl/rule/' + ruleId,
+    method: 'delete'
+  });
+};

+ 46 - 0
src/api/riskctrl/rule/types.ts

@@ -0,0 +1,46 @@
+/**
+ * 风控规则查询对象类型
+ */
+export interface RuleQuery extends PageQuery {
+  ruleName?: string;
+  ruleCode?: string;
+  ruleType?: string;
+  riskLevel?: string;
+  status?: string;
+}
+
+/**
+ * 风控规则返回对象
+ */
+export interface RuleVO extends BaseEntity {
+  ruleId: string | number;
+  ruleName: string;
+  ruleCode: string;
+  ruleType: string;
+  ruleDesc: string;
+  ruleScript: string;
+  riskLevel: string;
+  ruleScore: number;
+  actionType: string;
+  status: string;
+  priority: number;
+  remark: string;
+}
+
+/**
+ * 风控规则表单类型
+ */
+export interface RuleForm {
+  ruleId?: string | number;
+  ruleName: string;
+  ruleCode: string;
+  ruleType: string;
+  ruleDesc?: string;
+  ruleScript?: string;
+  riskLevel: string;
+  ruleScore: number;
+  actionType: string;
+  status: string;
+  priority?: number;
+  remark?: string;
+}

+ 67 - 0
src/api/riskctrl/safetyTraining/index.ts

@@ -0,0 +1,67 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import { SafetyTrainingForm, SafetyTrainingQuery, SafetyTrainingVO } from './types';
+
+/**
+ * 查询安全培训列表
+ */
+export const listSafetyTraining = (query: SafetyTrainingQuery): AxiosPromise<SafetyTrainingVO[]> => {
+  return request({
+    url: '/riskctrl/safetyTraining/list',
+    method: 'get',
+    params: query
+  });
+};
+
+/**
+ * 获取安全培训详情
+ */
+export const getSafetyTraining = (trainingId: string | number): AxiosPromise<SafetyTrainingVO> => {
+  return request({
+    url: '/riskctrl/safetyTraining/' + trainingId,
+    method: 'get'
+  });
+};
+
+/**
+ * 新增安全培训
+ */
+export const addSafetyTraining = (data: SafetyTrainingForm) => {
+  return request({
+    url: '/riskctrl/safetyTraining',
+    method: 'post',
+    data: data
+  });
+};
+
+/**
+ * 修改安全培训
+ */
+export const updateSafetyTraining = (data: SafetyTrainingForm) => {
+  return request({
+    url: '/riskctrl/safetyTraining',
+    method: 'put',
+    data: data
+  });
+};
+
+/**
+ * 删除安全培训
+ */
+export const delSafetyTraining = (trainingId: Array<string | number> | string | number) => {
+  return request({
+    url: '/riskctrl/safetyTraining/' + trainingId,
+    method: 'delete'
+  });
+};
+
+/**
+ * 获取最新安全培训列表
+ */
+export const getLatestTrainings = (limit?: number): AxiosPromise<SafetyTrainingVO[]> => {
+  return request({
+    url: '/riskctrl/safetyTraining/latest',
+    method: 'get',
+    params: { limit: limit || 5 }
+  });
+};

+ 47 - 0
src/api/riskctrl/safetyTraining/types.ts

@@ -0,0 +1,47 @@
+/**
+ * 安全培训查询对象类型
+ */
+export interface SafetyTrainingQuery extends PageQuery {
+  trainingTitle?: string;
+  trainingType?: string;
+  trainingStatus?: string;
+  trainingTime?: string;
+}
+
+/**
+ * 安全培训返回对象
+ */
+export interface SafetyTrainingVO extends BaseEntity {
+  trainingId: string | number;
+  trainingNo: string;
+  trainingTitle: string;
+  trainingType: string;
+  trainingTime: string;
+  trainingLocation: string;
+  trainer: string;
+  participantCount: number;
+  trainingStatus: string;
+  trainingContent: string;
+  trainingMaterial: string;
+  status: string;
+  remark: string;
+}
+
+/**
+ * 安全培训表单类型
+ */
+export interface SafetyTrainingForm {
+  trainingId?: string | number;
+  trainingNo?: string;
+  trainingTitle: string;
+  trainingType: string;
+  trainingTime?: string;
+  trainingLocation?: string;
+  trainer?: string;
+  participantCount?: number;
+  trainingStatus?: string;
+  trainingContent?: string;
+  trainingMaterial?: string;
+  status: string;
+  remark?: string;
+}

+ 59 - 0
src/api/riskctrl/score/index.ts

@@ -0,0 +1,59 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import { ScoreForm, ScoreQuery, ScoreVO } from './types';
+
+/**
+ * 查询风险评分列表
+ * @param query
+ */
+export const listScore = (query: ScoreQuery): AxiosPromise<ScoreVO[]> => {
+  return request({
+    url: '/riskctrl/score/list',
+    method: 'get',
+    params: query
+  });
+};
+
+/**
+ * 获取风险评分详情
+ * @param scoreId
+ */
+export const getScore = (scoreId: string | number): AxiosPromise<ScoreVO> => {
+  return request({
+    url: '/riskctrl/score/' + scoreId,
+    method: 'get'
+  });
+};
+
+/**
+ * 新增风险评分
+ */
+export const addScore = (data: ScoreForm) => {
+  return request({
+    url: '/riskctrl/score',
+    method: 'post',
+    data: data
+  });
+};
+
+/**
+ * 修改风险评分
+ */
+export const updateScore = (data: ScoreForm) => {
+  return request({
+    url: '/riskctrl/score',
+    method: 'put',
+    data: data
+  });
+};
+
+/**
+ * 删除风险评分
+ * @param scoreId 评分ID
+ */
+export const delScore = (scoreId: Array<string | number> | string | number) => {
+  return request({
+    url: '/riskctrl/score/' + scoreId,
+    method: 'delete'
+  });
+};

+ 53 - 0
src/api/riskctrl/score/types.ts

@@ -0,0 +1,53 @@
+/**
+ * 风险评分查询对象类型
+ */
+export interface ScoreQuery extends PageQuery {
+  scoreNo?: string;
+  scoreType?: string;
+  riskLevel?: string;
+  status?: string;
+  userId?: string | number;
+  userName?: string;
+}
+
+/**
+ * 风险评分返回对象
+ */
+export interface ScoreVO extends BaseEntity {
+  scoreId: string | number;
+  scoreNo: string;
+  userId: string | number;
+  userName: string;
+  scoreType: string;
+  totalScore: number;
+  baseScore: number;
+  ruleScore: number;
+  behaviorScore: number;
+  creditScore: number;
+  riskLevel: string;
+  evaluation: string;
+  status: string;
+  remark: string;
+  evaluateTime: string;
+}
+
+/**
+ * 风险评分表单类型
+ */
+export interface ScoreForm {
+  scoreId?: string | number;
+  scoreNo?: string;
+  userId?: string | number;
+  userName?: string;
+  scoreType?: string;
+  totalScore?: number;
+  baseScore?: number;
+  ruleScore?: number;
+  behaviorScore?: number;
+  creditScore?: number;
+  riskLevel?: string;
+  evaluation?: string;
+  status: string;
+  remark?: string;
+  evaluateTime?: string;
+}

+ 31 - 0
src/api/riskctrl/specialEquipment/index.ts

@@ -0,0 +1,31 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import { SpecialEquipmentForm, SpecialEquipmentQuery, SpecialEquipmentVO, EquipmentStats, EquipmentTypeDistribution } from './types';
+
+export const listSpecialEquipment = (query: SpecialEquipmentQuery): AxiosPromise<SpecialEquipmentVO[]> => {
+  return request({ url: '/riskctrl/specialEquipment/list', method: 'get', params: query });
+};
+
+export const getSpecialEquipment = (equipmentId: string | number): AxiosPromise<SpecialEquipmentVO> => {
+  return request({ url: '/riskctrl/specialEquipment/' + equipmentId, method: 'get' });
+};
+
+export const addSpecialEquipment = (data: SpecialEquipmentForm) => {
+  return request({ url: '/riskctrl/specialEquipment', method: 'post', data });
+};
+
+export const updateSpecialEquipment = (data: SpecialEquipmentForm) => {
+  return request({ url: '/riskctrl/specialEquipment', method: 'put', data });
+};
+
+export const delSpecialEquipment = (equipmentId: Array<string | number> | string | number) => {
+  return request({ url: '/riskctrl/specialEquipment/' + equipmentId, method: 'delete' });
+};
+
+export const getEquipmentStats = (): AxiosPromise<EquipmentStats> => {
+  return request({ url: '/riskctrl/specialEquipment/stats', method: 'get' });
+};
+
+export const getEquipmentTypeDistribution = (): AxiosPromise<EquipmentTypeDistribution[]> => {
+  return request({ url: '/riskctrl/specialEquipment/distribution', method: 'get' });
+};

+ 57 - 0
src/api/riskctrl/specialEquipment/types.ts

@@ -0,0 +1,57 @@
+/**
+ * 特种设备查询对象类型
+ */
+export interface SpecialEquipmentQuery extends PageQuery {
+  equipmentName?: string;
+  equipmentType?: string;
+  maintenanceStatus?: string;
+  nextMaintenanceDate?: string;
+}
+
+/**
+ * 特种设备返回对象
+ */
+export interface SpecialEquipmentVO extends BaseEntity {
+  equipmentId: string | number;
+  equipmentNo: string;
+  equipmentName: string;
+  equipmentType: string;
+  installLocation: string;
+  equipmentPhotos: string;
+  maintenanceStatus: string;
+  status: string;
+  remark: string;
+}
+
+/**
+ * 特种设备表单类型
+ */
+export interface SpecialEquipmentForm {
+  equipmentId?: string | number;
+  equipmentNo?: string;
+  equipmentName: string;
+  equipmentType: string;
+  installLocation?: string;
+  equipmentPhotos?: string;
+  maintenanceStatus?: string;
+  status: string;
+  remark?: string;
+}
+
+/**
+ * 设备统计
+ */
+export interface EquipmentStats {
+  totalCount: number;
+  normalCount: number;
+  maintenanceCount: number;
+  overdueCount: number;
+}
+
+/**
+ * 设备类型分布
+ */
+export interface EquipmentTypeDistribution {
+  type: string;
+  count: number;
+}

+ 70 - 0
src/api/riskctrl/whitelist/index.ts

@@ -0,0 +1,70 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import { WhitelistForm, WhitelistQuery, WhitelistVO } from './types';
+
+/**
+ * 查询白名单列表
+ * @param query
+ */
+export const listWhitelist = (query: WhitelistQuery): AxiosPromise<WhitelistVO[]> => {
+  return request({
+    url: '/riskctrl/whitelist/list',
+    method: 'get',
+    params: query
+  });
+};
+
+/**
+ * 获取白名单详情
+ * @param whiteId
+ */
+export const getWhitelist = (whiteId: string | number): AxiosPromise<WhitelistVO> => {
+  return request({
+    url: '/riskctrl/whitelist/' + whiteId,
+    method: 'get'
+  });
+};
+
+/**
+ * 新增白名单
+ */
+export const addWhitelist = (data: WhitelistForm) => {
+  return request({
+    url: '/riskctrl/whitelist',
+    method: 'post',
+    data: data
+  });
+};
+
+/**
+ * 修改白名单
+ */
+export const updateWhitelist = (data: WhitelistForm) => {
+  return request({
+    url: '/riskctrl/whitelist',
+    method: 'put',
+    data: data
+  });
+};
+
+/**
+ * 修改白名单状态
+ */
+export const changeWhitelistStatus = (whiteId: string | number, status: string) => {
+  return request({
+    url: '/riskctrl/whitelist/changeStatus',
+    method: 'put',
+    data: { whiteId, status }
+  });
+};
+
+/**
+ * 删除白名单
+ * @param whiteId 白名单ID
+ */
+export const delWhitelist = (whiteId: Array<string | number> | string | number) => {
+  return request({
+    url: '/riskctrl/whitelist/' + whiteId,
+    method: 'delete'
+  });
+};

+ 39 - 0
src/api/riskctrl/whitelist/types.ts

@@ -0,0 +1,39 @@
+/**
+ * 白名单查询对象类型
+ */
+export interface WhitelistQuery extends PageQuery {
+  whiteType?: string;
+  whiteValue?: string;
+  whiteName?: string;
+  status?: string;
+}
+
+/**
+ * 白名单返回对象
+ */
+export interface WhitelistVO extends BaseEntity {
+  whiteId: string | number;
+  whiteType: string;
+  whiteValue: string;
+  whiteName: string;
+  reason: string;
+  source: string;
+  expireTime: string;
+  status: string;
+  remark: string;
+}
+
+/**
+ * 白名单表单类型
+ */
+export interface WhitelistForm {
+  whiteId?: string | number;
+  whiteType: string;
+  whiteValue: string;
+  whiteName?: string;
+  reason?: string;
+  source?: string;
+  expireTime?: string;
+  status: string;
+  remark?: string;
+}

+ 35 - 0
src/api/riskctrl/workReport/index.ts

@@ -0,0 +1,35 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import { WorkReportForm, WorkReportQuery, WorkReportVO, WorkReportStats, WorkReportTrend } from './types';
+
+export const listWorkReport = (query: WorkReportQuery): AxiosPromise<WorkReportVO[]> => {
+  return request({ url: '/riskctrl/workReport/list', method: 'get', params: query });
+};
+
+export const getWorkReport = (reportId: string | number): AxiosPromise<WorkReportVO> => {
+  return request({ url: '/riskctrl/workReport/' + reportId, method: 'get' });
+};
+
+export const addWorkReport = (data: WorkReportForm) => {
+  return request({ url: '/riskctrl/workReport', method: 'post', data });
+};
+
+export const updateWorkReport = (data: WorkReportForm) => {
+  return request({ url: '/riskctrl/workReport', method: 'put', data });
+};
+
+export const auditWorkReport = (data: WorkReportForm) => {
+  return request({ url: '/riskctrl/workReport/audit', method: 'put', data });
+};
+
+export const delWorkReport = (reportId: Array<string | number> | string | number) => {
+  return request({ url: '/riskctrl/workReport/' + reportId, method: 'delete' });
+};
+
+export const getWorkReportStats = (): AxiosPromise<WorkReportStats> => {
+  return request({ url: '/riskctrl/workReport/stats', method: 'get' });
+};
+
+export const getWorkReportTrend = (months: number = 6): AxiosPromise<WorkReportTrend[]> => {
+  return request({ url: '/riskctrl/workReport/trend', method: 'get', params: { months } });
+};

+ 88 - 0
src/api/riskctrl/workReport/types.ts

@@ -0,0 +1,88 @@
+/**
+ * 作业上报查询对象类型
+ */
+export interface WorkReportQuery extends PageQuery {
+  reportNo?: string;
+  projectName?: string;
+  workType?: string;
+  auditStatus?: string;
+  status?: string;
+}
+
+/**
+ * 作业上报返回对象(视图对象)
+ */
+export interface WorkReportVO extends BaseEntity {
+  reportId: string | number;
+  reportNo: string;
+  workType: string;
+  projectName: string;
+  startTime: string;
+  endTime: string;
+  safetyOfficer: string;
+  contactPhone: string;
+  workImages: string;
+  permitFiles: string;
+  workDesc: string;
+  auditStatus: string;
+  auditComment: string;
+  auditUser: string;
+  auditTime: string;
+  status: string;
+  remark: string;
+  createTime: string;
+  createBy: string;
+  createDept: string;
+  updateBy: string;
+  updateTime: string;
+}
+
+/**
+ * 作业上报表单类型(新增/编辑用)
+ */
+export interface WorkReportForm {
+  reportId?: string | number;
+  reportNo?: string;
+  workType: string;
+  projectName: string;
+  startTime?: string;
+  endTime?: string;
+  safetyOfficer?: string;
+  contactPhone?: string;
+  workImages?: string;
+  permitFiles?: string;
+  workDesc?: string;
+  auditStatus?: string;
+  auditComment?: string;
+  auditUser?: string;
+  auditTime?: string;
+  status?: string;
+  remark?: string;
+}
+
+/**
+ * 作业审核表单
+ */
+export interface WorkReportAuditForm {
+  reportId: string | number;
+  auditStatus: string;
+  auditComment?: string;
+}
+
+/**
+ * 作业统计
+ */
+export interface WorkReportStats {
+  total: number;
+  pending: number;
+  passed: number;
+  rejected: number;
+}
+
+/**
+ * 作业趋势
+ */
+export interface WorkReportTrend {
+  month: string;
+  count: number;
+}

+ 9 - 9
src/assets/styles/sidebar.scss

@@ -121,18 +121,18 @@
 
     & .theme-dark .nest-menu .el-sub-menu > .el-sub-menu__title,
     & .theme-dark .el-sub-menu .el-menu-item {
-      background-color: $base-sub-menu-background !important;
+      background-color: #ffffff !important;
       border-radius: var(--app-radius-md);
       height: 45px;
       margin: 1px 5px 1px 5px;
 
       &.is-active {
-        background-color: var(--el-menu-active-color) !important;
+        background-color: #2563eb !important;
         color: #fff;
       }
       &:not(.is-active):hover {
         // you can use $sub-menuHover
-        background-color: $base-sub-menu-hover !important;
+        background-color: #dbeafe !important;
       }
     }
 
@@ -143,12 +143,12 @@
       margin: 1px 5px 1px 5px;
 
       &.is-active {
-        background-color: var(--el-menu-active-color) !important;
+        background-color: #2563eb !important;
         color: #fff;
       }
       &:not(.is-active):hover {
         // you can use $sub-menuHover
-        background-color: $base-menu-hover !important;
+        background-color: #dbeafe !important;
       }
     }
 
@@ -159,12 +159,12 @@
       margin: 1px 5px 1px 5px;
 
       &.is-active {
-        background-color: var(--el-menu-active-color) !important;
+        background-color: #2563eb !important;
         color: #fff;
       }
       &:not(.is-active):hover {
         // you can use $sub-menuHover
-        background-color: rgba(0, 0, 0, 0.04) !important;
+        background-color: #dbeafe !important;
       }
     }
   }
@@ -185,7 +185,7 @@
       height: 45px;
       // 选中状态的菜单
       &.is-active {
-        background-color: var(--el-menu-active-color) !important;
+        background-color: #2563eb !important;
         color: #fff !important;
       }
 
@@ -205,7 +205,7 @@
 
       // 选中状态的菜单
       &.is-active .el-sub-menu__title.el-tooltip__trigger {
-        background-color: var(--el-menu-active-color) !important;
+        background-color: #2563eb !important;
       }
 
 

+ 17 - 17
src/assets/styles/variables.module.scss

@@ -1,14 +1,14 @@
 // 全局SCSS变量
 :root {
-  --menuBg: #1f2d3d;
-  --menuColor: #bfcbd9;
-  --menuActiveText: #f4f4f5;
-  --menuHover: #263445;
+  --menuBg: #ffffff;
+  --menuColor: #303133;
+  --menuActiveText: #ffffff;
+  --menuHover: #dbeafe;
 
-  --subMenuBg: #1f2d3d;
-  --subMenuActiveText: #f4f4f5;
-  --subMenuHover: #001528;
-  --subMenuTitleHover: #293444;
+  --subMenuBg: #ffffff;
+  --subMenuActiveText: #ffffff;
+  --subMenuHover: #dbeafe;
+  --subMenuTitleHover: #dbeafe;
 
   // 菜单栏缩进
   --el-menu-base-level-padding: 12px;
@@ -48,15 +48,15 @@
 }
 
 html.dark {
-  --menuBg: #1d1e1f;
-  --menuColor: #bfcbd9;
-  --menuActiveText: #f4f4f5;
-  --menuHover: #171819;
-
-  --subMenuBg: #1d1e1f;
-  --subMenuActiveText: #f4f4f5;
-  --subMenuHover: #171819;
-  --subMenuTitleHover: #171819;
+  --menuBg: #ffffff;
+  --menuColor: #303133;
+  --menuActiveText: #ffffff;
+  --menuHover: #dbeafe;
+
+  --subMenuBg: #ffffff;
+  --subMenuActiveText: #ffffff;
+  --subMenuHover: #dbeafe;
+  --subMenuTitleHover: #dbeafe;
 
   --fixedHeaderBg: #171819;
   --tableHeaderBg: var(--el-bg-color);

+ 63 - 61
src/layout/components/Navbar.vue

@@ -1,37 +1,20 @@
 <template>
   <div class="navbar" :class="'nav' + navType">
-    <hamburger id="hamburger-container" :is-active="appStore.sidebar.opened" class="hamburger-container" @toggle-click="toggleSideBar" />
+    <!-- Logo和标题 -->
+    <div v-if="navType == NavTypeEnum.LEFT" class="logo-title">
+      <img :src="logo" class="navbar-logo" />
+      <span class="navbar-title">企业安全风控平台</span>
+    </div>
 
-    <breadcrumb v-if="navType == NavTypeEnum.LEFT" id="breadcrumb-container" class="breadcrumb-container" />
     <top-nav v-if="navType == NavTypeEnum.MIX" id="topmenu-container" class="topmenu-container" />
 
     <template v-if="navType == NavTypeEnum.TOP">
       <logo v-show="showLogo" :collapse="false"></logo>
       <top-bar id="topbar-container" class="topbar-container" />
     </template>
+    
     <div class="right-menu flex align-center">
       <template v-if="appStore.device !== 'mobile'">
-        <el-select
-          v-if="userId === 1 && tenantEnabled"
-          v-model="companyName"
-          class="min-w-244px mr-2"
-          clearable
-          filterable
-          reserve-keyword
-          :placeholder="proxy.$t('navbar.selectTenant')"
-          @change="dynamicTenantEvent"
-          @clear="dynamicClearEvent"
-        >
-          <el-option v-for="item in tenantList" :key="item.tenantId" :label="item.companyName" :value="item.tenantId"> </el-option>
-          <template #prefix><svg-icon icon-class="company" class="el-input__icon input-icon" /></template>
-        </el-select>
-
-        <search-menu ref="searchMenuRef" />
-        <el-tooltip content="搜索" effect="dark" placement="bottom">
-          <div class="right-menu-item hover-effect" @click="openSearchMenu">
-            <svg-icon class-name="search-icon" icon-class="search" />
-          </div>
-        </el-tooltip>
         <!-- 消息 -->
         <el-tooltip :content="proxy.$t('navbar.message')" effect="dark" placement="bottom">
           <div style="display:flex;align-items:center">
@@ -47,30 +30,12 @@
             </el-popover>
           </div>
         </el-tooltip>
-        <el-tooltip content="Github" effect="dark" placement="bottom">
-          <ruo-yi-git id="ruoyi-git" class="right-menu-item hover-effect" />
-        </el-tooltip>
-
-        <el-tooltip :content="proxy.$t('navbar.document')" effect="dark" placement="bottom">
-          <ruo-yi-doc id="ruoyi-doc" class="right-menu-item hover-effect" />
-        </el-tooltip>
-
-        <el-tooltip :content="proxy.$t('navbar.full')" effect="dark" placement="bottom">
-          <screenfull id="screenfull" class="right-menu-item hover-effect" />
-        </el-tooltip>
-
-        <el-tooltip :content="proxy.$t('navbar.language')" effect="dark" placement="bottom">
-          <lang-select id="lang-select" class="right-menu-item hover-effect" />
-        </el-tooltip>
-
-        <el-tooltip :content="proxy.$t('navbar.layoutSize')" effect="dark" placement="bottom">
-          <size-select id="size-select" class="right-menu-item hover-effect" />
-        </el-tooltip>
       </template>
       <div class="avatar-container">
         <el-dropdown class="right-menu-item hover-effect" trigger="click" @command="handleCommand">
           <div class="avatar-wrapper">
             <img :src="userStore.avatar" class="user-avatar" />
+            <span class="user-name">管理员</span>
             <el-icon><caret-bottom /></el-icon>
           </div>
           <template #dropdown>
@@ -93,20 +58,16 @@
 </template>
 
 <script setup lang="ts">
-import SearchMenu from './TopBar/search.vue';
 import { useAppStore } from '@/store/modules/app';
 import { useUserStore } from '@/store/modules/user';
 import { useSettingsStore } from '@/store/modules/settings';
 import { useNoticeStore } from '@/store/modules/notice';
-import { getTenantList } from '@/api/login';
-import { dynamicClear, dynamicTenant } from '@/api/system/tenant';
-import { TenantVO } from '@/api/types';
 import notice from './notice/index.vue';
 import router from '@/router';
 import { ElMessageBoxOptions } from 'element-plus/es/components/message-box/src/message-box.type';
 import { NavTypeEnum } from '@/enums/NavTypeEnum';
-import Logo from "@/layout/components/Sidebar/Logo.vue";
-import TopBar from './TopBar'
+import TopBar from './TopBar';
+import logo from '@/assets/logo/logo.png';
 
 const appStore = useAppStore();
 const userStore = useUserStore();
@@ -133,6 +94,13 @@ const openSearchMenu = () => {
   searchMenuRef.value?.openSearch();
 };
 
+const searchText = ref('');
+
+const handleSearch = () => {
+  // 搜索逻辑
+  console.log('搜索:', searchText.value);
+};
+
 // 动态切换
 const dynamicTenantEvent = async (tenantId: string) => {
   if (companyName.value != null && companyName.value !== '') {
@@ -238,12 +206,12 @@ watch(
   height: 50px;
   overflow: hidden;
   position: relative;
-  background: var(--el-bg-color);
-  border-bottom: 1px solid var(--el-border-color-lighter);
+  background: #2563eb;
+  border-bottom: none;
   box-shadow: none;
   display: flex;
   align-items: center;
-  // padding: 0 8px;
+  padding: 0 16px;
   box-sizing: border-box;
 
   .hamburger-container {
@@ -257,15 +225,39 @@ watch(
     align-items: center;
     flex-shrink: 0;
     margin-right: 8px;
+    color: #fff;
 
     &:hover {
-      background: var(--el-fill-color-lighter);
+      background: rgba(255, 255, 255, 0.15);
+    }
+  }
+
+  .logo-title {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    padding: 0 16px;
+    
+    .navbar-logo {
+      width: 24px;
+      height: 24px;
+    }
+    
+    .navbar-title {
+      color: #ffffff;
+      font-size: 16px;
+      font-weight: 600;
     }
   }
 
   .breadcrumb-container {
-    //float: left;
-    flex-shrink: 0;
+    flex: 1;
+    padding: 0 16px;
+    
+    .breadcrumb-text {
+      color: rgba(255, 255, 255, 0.95);
+      font-size: 14px;
+    }
   }
 
   .topmenu-container {
@@ -293,6 +285,7 @@ watch(
     display: flex;
     align-items: center;
     margin-left: auto;
+    gap: 12px;
 
     &:focus {
       outline: none;
@@ -305,7 +298,7 @@ watch(
       padding: 0 8px;
       height: 32px;
       font-size: 18px;
-      color: var(--el-text-color-regular);
+      color: #fff;
       border-radius: var(--app-radius-md);
 
       &.hover-effect {
@@ -313,28 +306,37 @@ watch(
         transition: background 0.2s ease, color 0.2s ease;
 
         &:hover {
-          background: var(--el-fill-color-light);
-          color: var(--el-color-primary);
+          background: rgba(255, 255, 255, 0.15);
+          color: #fff;
         }
       }
     }
 
     .avatar-container {
-      margin-right: 40px;
+      margin-right: 16px;
 
       .avatar-wrapper {
         margin-top: 0;
         position: relative;
+        display: flex;
+        align-items: center;
+        gap: 8px;
 
         .user-avatar {
           cursor: pointer;
-          width: 40px;
-          height: 40px;
-          border-radius: var(--app-radius-md);
+          width: 36px;
+          height: 36px;
+          border-radius: 50%;
           margin-top: 0;
           display: block;
         }
 
+        .user-name {
+          color: #ffffff;
+          font-size: 14px;
+          margin-right: 4px;
+        }
+
         i {
           cursor: pointer;
           position: absolute;

+ 2 - 19
src/layout/components/Sidebar/Logo.vue

@@ -21,11 +21,8 @@
 </template>
 
 <script setup lang="ts">
-import variables from '@/assets/styles/variables.module.scss';
 import logo from '@/assets/logo/logo.png';
-import { useSettingsStore } from '@/store/modules/settings';
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
-import { NavTypeEnum } from '@/enums/NavTypeEnum';
 
 defineProps({
   collapse: {
@@ -35,29 +32,15 @@ defineProps({
 });
 
 const title = import.meta.env.VITE_APP_LOGO_TITLE;
-const settingsStore = useSettingsStore();
-const sideTheme = computed(() => settingsStore.sideTheme);
 
 // 获取Logo背景色
 const getLogoBackground = computed(() => {
-  if (settingsStore.isDark) {
-    return 'var(--sidebar-bg)'
-  }
-  if (settingsStore.navType == NavTypeEnum.TOP) {
-    return variables.menuLightBackground
-  }
-  return sideTheme.value === 'theme-dark' ? variables.menuBg : variables.menuLightBackground
+  return '#ffffff'
 })
 
 // 获取Logo文字颜色
 const getLogoTextColor = computed(() => {
-  if (settingsStore.isDark) {
-    return 'var(--sidebar-text)'
-  }
-  if (settingsStore.navType == NavTypeEnum.TOP) {
-    return variables.logoLightTitleColor
-  }
-  return sideTheme.value === 'theme-dark' ? variables.logoTitleColor : variables.logoLightTitleColor
+  return '#303133'
 })
 </script>
 

+ 53 - 9
src/layout/components/Sidebar/index.vue

@@ -1,15 +1,15 @@
 <template>
-  <div :class="{ 'has-logo': showLogo }" :style="{ backgroundColor: bgColor }">
+  <div :class="{ 'has-logo': showLogo }" :style="{ backgroundColor: '#ffffff' }">
     <logo v-if="showLogo" :collapse="isCollapse" />
     <el-scrollbar :class="sideTheme" wrap-class="scrollbar-wrapper">
       <transition :enter-active-class="proxy?.animate.menuSearchAnimate.enter" mode="out-in">
         <el-menu
           :default-active="activeMenu"
           :collapse="isCollapse"
-          :background-color="bgColor"
-          :text-color="textColor"
+          :background-color="'#ffffff'"
+          :text-color="'#303133'"
           :unique-opened="true"
-          :active-text-color="theme"
+          :active-text-color="'#2563eb'"
           :collapse-transition="false"
           :popper-offset="12"
           mode="vertical"
@@ -24,7 +24,6 @@
 <script setup lang="ts">
 import Logo from './Logo.vue';
 import SidebarItem from './SidebarItem.vue';
-import variables from '@/assets/styles/variables.module.scss';
 import { useAppStore } from '@/store/modules/app';
 import { useSettingsStore } from '@/store/modules/settings';
 import { usePermissionStore } from '@/store/modules/permission';
@@ -39,7 +38,6 @@ const permissionStore = usePermissionStore();
 const sidebarRouters = computed<RouteRecordRaw[]>(() => permissionStore.getSidebarRoutes());
 const showLogo = computed(() => settingsStore.sidebarLogo);
 const sideTheme = computed(() => settingsStore.sideTheme);
-const theme = computed(() => settingsStore.theme);
 const isCollapse = computed(() => !appStore.sidebar.opened);
 
 const activeMenu = computed(() => {
@@ -50,7 +48,53 @@ const activeMenu = computed(() => {
   }
   return path;
 });
-
-const bgColor = computed(() => (sideTheme.value === 'theme-dark' ? variables.menuBackground : variables.menuLightBackground));
-const textColor = computed(() => (sideTheme.value === 'theme-dark' ? variables.menuColor : variables.menuLightColor));
 </script>
+
+<style lang="scss" scoped>
+:deep(.el-menu) {
+  background-color: #ffffff !important;
+}
+:deep(.el-menu-item) {
+  background-color: #ffffff !important;
+  color: #303133 !important;
+  margin: 4px 8px !important;
+  border-radius: 6px !important;
+}
+:deep(.el-menu-item:hover) {
+  background-color: #dbeafe !important;
+  color: #2563eb !important;
+}
+:deep(.el-sub-menu__title) {
+  background-color: #ffffff !important;
+  color: #303133 !important;
+  margin: 4px 8px !important;
+  border-radius: 6px !important;
+}
+:deep(.el-sub-menu__title:hover) {
+  background-color: #dbeafe !important;
+  color: #2563eb !important;
+}
+:deep(.el-menu-item.is-active) {
+  background-color: #2563eb !important;
+  color: #ffffff !important;
+}
+:deep(.el-sub-menu.is-active .el-sub-menu__title) {
+  background-color: #2563eb !important;
+  color: #ffffff !important;
+}
+:deep(.el-sub-menu .el-menu) {
+  background-color: #ffffff !important;
+}
+:deep(.el-sub-menu .el-menu-item) {
+  background-color: #ffffff !important;
+  color: #303133 !important;
+}
+:deep(.el-sub-menu .el-menu-item:hover) {
+  background-color: #dbeafe !important;
+  color: #2563eb !important;
+}
+:deep(.el-sub-menu .el-menu-item.is-active) {
+  background-color: #2563eb !important;
+  color: #ffffff !important;
+}
+</style>

+ 2 - 2
src/layout/index.vue

@@ -126,8 +126,8 @@ const setLayout = () => {
   z-index: 9;
   width: calc(100% - #{$base-sidebar-width});
   transition: width 0.28s;
-  background: $fixed-header-bg;
-  box-shadow: 0 2px 8px rgba(0, 21, 41, 0.10);
+  background: #2563eb;
+  box-shadow: none;
 }
 
 .hideSidebar .fixed-header {

+ 1 - 1
src/router/index.ts

@@ -71,7 +71,7 @@ export const constantRoutes: RouteRecordRaw[] = [
         path: '/index',
         component: () => import('@/views/index.vue'),
         name: 'Index',
-        meta: { title: '首页', icon: 'dashboard', affix: true }
+        meta: { title: '平台首页', icon: 'dashboard', affix: true }
       }
     ]
   },

+ 715 - 130
src/views/index.vue

@@ -1,165 +1,750 @@
 <template>
   <div class="app-container home">
-    <el-row :gutter="20">
-      <el-col :sm="24" :lg="12" style="padding-left: 20px">
-        <h2>RuoYi-Vue-Plus多租户管理系统</h2>
-        <p>
-          RuoYi-Vue-Plus 是基于 RuoYi-Vue 针对 分布式集群 场景升级(不兼容原框架)
-          <br />
-          * 前端开发框架 Vue3、TS、Element Plus<br />
-          * 后端开发框架 Spring Boot<br />
-          * 容器框架 Undertow 基于 Netty 的高性能容器<br />
-          * 权限认证框架 Sa-Token 支持多终端认证系统<br />
-          * 关系数据库 MySQL 适配 8.X 最低 5.7<br />
-          * 缓存数据库 Redis 适配 6.X 最低 4.X<br />
-          * 数据库框架 Mybatis-Plus 快速 CRUD 增加开发效率<br />
-          * 数据库框架 p6spy 更强劲的 SQL 分析<br />
-          * 多数据源框架 dynamic-datasource 支持主从与多种类数据库异构<br />
-          * 序列化框架 Jackson 统一使用 jackson 高效可靠<br />
-          * Redis客户端 Redisson 性能强劲、API丰富<br />
-          * 分布式限流 Redisson 全局、请求IP、集群ID 多种限流<br />
-          * 分布式锁 Lock4j 注解锁、工具锁 多种多样<br />
-          * 分布式幂等 Lock4j 基于分布式锁实现<br />
-          * 分布式链路追踪 SkyWalking 支持链路追踪、网格分析、度量聚合、可视化<br />
-          * 分布式任务调度 SnailJob 高性能 高可靠 易扩展<br />
-          * 文件存储 Minio 本地存储<br />
-          * 文件存储 七牛、阿里、腾讯 云存储<br />
-          * 监控框架 SpringBoot-Admin 全方位服务监控<br />
-          * 校验框架 Validation 增强接口安全性 严谨性<br />
-          * Excel框架 FastExcel(原Alibaba EasyExcel) 性能优异 扩展性强<br />
-          * 文档框架 SpringDoc、javadoc 无注解零入侵基于java注释<br />
-          * 工具类框架 Hutool、Lombok 减少代码冗余 增加安全性<br />
-          * 代码生成器 适配MP、SpringDoc规范化代码 一键生成前后端代码<br />
-          * 部署方式 Docker 容器编排 一键部署业务集群<br />
-          * 国际化 SpringMessage Spring标准国际化方案<br />
-        </p>
-        <p><b>当前版本:</b> <span>v5.6.1</span></p>
-        <p>
-          <el-tag type="danger">&yen;免费开源</el-tag>
-        </p>
-        <p>
-          <el-button type="primary" icon="Cloudy" plain @click="goTarget('https://gitee.com/dromara/RuoYi-Vue-Plus')">访问码云</el-button>
-          <el-button type="primary" icon="Cloudy" plain @click="goTarget('https://github.com/dromara/RuoYi-Vue-Plus')">访问GitHub</el-button>
-          <el-button type="primary" icon="Cloudy" plain @click="goTarget('https://plus-doc.dromara.org/#/ruoyi-vue-plus/changlog')"
-            >更新日志</el-button
-          >
-        </p>
+    <!-- 欢迎标题区 -->
+    <div class="welcome-header mb-[20px]">
+      <h2 class="text-xl font-bold text-gray-800">欢迎回来,管理员</h2>
+      <p class="text-sm text-gray-500 mt-1">今日是,今天也是安全的一天</p>
+    </div>
+
+    <!-- 今日数据卡片 -->
+    <el-row :gutter="16" class="mb-[20px]">
+      <el-col :xs="24" :sm="12" :lg="6">
+        <div class="stat-card stat-card-blue" @click="goToHazard()">
+          <div class="stat-card-content">
+            <div class="stat-info">
+              <p class="stat-label">安全隐患总数</p>
+              <p class="stat-value">{{ dashboardData.hazardTotal }}</p>
+              <p class="stat-trend">
+                <span class="trend-up">↑ 较前日 +3.2%</span>
+              </p>
+            </div>
+            <div class="stat-icon">
+              <svg viewBox="0 0 24 24" width="32" height="32">
+                <path fill="currentColor" d="M12 2L2 7v6c0 5.5 3.8 10.7 9 12 5.2-1.3 9-6.5 9-12V7l-10-5z"/>
+              </svg>
+            </div>
+          </div>
+        </div>
+      </el-col>
+      <el-col :xs="24" :sm="12" :lg="6">
+        <div class="stat-card stat-card-green" @click="goToHazard('已处理')">
+          <div class="stat-card-content">
+            <div class="stat-info">
+              <p class="stat-label">已处理隐患</p>
+              <p class="stat-value">{{ dashboardData.hazardProcessed }}</p>
+              <p class="stat-trend">
+                <span class="trend-normal">↑ 处理率 75%</span>
+              </p>
+            </div>
+            <div class="stat-icon">
+              <svg viewBox="0 0 24 24" width="32" height="32">
+                <path fill="currentColor" d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/>
+              </svg>
+            </div>
+          </div>
+        </div>
+      </el-col>
+      <el-col :xs="24" :sm="12" :lg="6">
+        <div class="stat-card stat-card-orange" @click="goToHazard('待处理')">
+          <div class="stat-card-content">
+            <div class="stat-info">
+              <p class="stat-label">待处理隐患</p>
+              <p class="stat-value">{{ dashboardData.hazardPending }}</p>
+              <p class="stat-trend">
+                <span class="trend-warn">需尽快处理</span>
+              </p>
+            </div>
+            <div class="stat-icon">
+              <svg viewBox="0 0 24 24" width="32" height="32">
+                <path fill="currentColor" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/>
+              </svg>
+            </div>
+          </div>
+        </div>
       </el-col>
+      <el-col :xs="24" :sm="12" :lg="6">
+        <div class="stat-card stat-card-purple" @click="goToEquipment()">
+          <div class="stat-card-content">
+            <div class="stat-info">
+              <p class="stat-label">特种设备数量</p>
+              <p class="stat-value">{{ dashboardData.equipmentTotal }}</p>
+              <p class="stat-trend">
+                <span class="trend-normal">✓ 全部检验合格</span>
+              </p>
+            </div>
+            <div class="stat-icon">
+              <svg viewBox="0 0 24 24" width="32" height="32">
+                <path fill="currentColor" d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-5 14H7v-2h7v2zm3-4H7v-2h10v2zm0-4H7V7h10v2z"/>
+              </svg>
+            </div>
+          </div>
+        </div>
+      </el-col>
+    </el-row>
+
+    <!-- 图表区域 -->
+    <el-row :gutter="16" class="mb-[20px]">
+      <el-col :xs="24" :lg="16">
+        <el-card shadow="never" class="chart-card">
+          <template #header>
+            <div class="chart-header">
+              <span class="chart-title">月度隐患处理趋势</span>
+            </div>
+          </template>
+          <div ref="trendChartRef" style="width: 100%; height: 320px;"></div>
+        </el-card>
+      </el-col>
+      <el-col :xs="24" :lg="8">
+        <el-card shadow="never" class="chart-card">
+          <template #header>
+            <div class="chart-header">
+              <span class="chart-title">隐患类型分布</span>
+            </div>
+          </template>
+          <div ref="pieChartRef" style="width: 100%; height: 320px;"></div>
+        </el-card>
+      </el-col>
+    </el-row>
 
-      <el-col :sm="24" :lg="12" style="padding-left: 20px">
-        <h2>RuoYi-Cloud-Plus多租户微服务管理系统</h2>
-        <p>
-          RuoYi-Cloud-Plus 微服务通用权限管理系统 重写 RuoYi-Cloud 全方位升级(不兼容原框架)
-          <br />
-          * 前端开发框架 Vue3、TS、Element UI<br />
-          * 后端开发框架 Spring Boot<br />
-          * 微服务开发框架 Spring Cloud、Spring Cloud Alibaba<br />
-          * 容器框架 Undertow 基于 XNIO 的高性能容器<br />
-          * 权限认证框架 Sa-Token、Jwt 支持多终端认证系统<br />
-          * 关系数据库 MySQL 适配 8.X 最低 5.7<br />
-          * 关系数据库 Oracle 适配 11g 12c<br />
-          * 关系数据库 PostgreSQL 适配 13 14<br />
-          * 关系数据库 SQLServer 适配 2017 2019<br />
-          * 缓存数据库 Redis 适配 6.X 最低 5.X<br />
-          * 分布式注册中心 Alibaba Nacos 采用2.X 基于GRPC通信高性能<br />
-          * 分布式配置中心 Alibaba Nacos 采用2.X 基于GRPC通信高性能<br />
-          * 服务网关 Spring Cloud Gateway 响应式高性能网关<br />
-          * 负载均衡 Spring Cloud Loadbalancer 负载均衡处理<br />
-          * RPC远程调用 Apache Dubbo 原生态使用体验、高性能<br />
-          * 分布式限流熔断 Alibaba Sentinel 无侵入、高扩展<br />
-          * 分布式事务 Alibaba Seata 无侵入、高扩展 支持 四种模式<br />
-          * 分布式消息队列 Apache Kafka 高性能高速度<br />
-          * 分布式消息队列 Apache RocketMQ 高可用功能多样<br />
-          * 分布式消息队列 RabbitMQ 支持各种扩展插件功能多样性<br />
-          * 分布式搜索引擎 ElasticSearch 业界知名<br />
-          * 分布式链路追踪 Apache SkyWalking 链路追踪、网格分析、度量聚合、可视化<br />
-          * 分布式日志中心 ELK 业界成熟解决方案<br />
-          * 分布式监控 Prometheus、Grafana 全方位性能监控<br />
-          * 其余与 Vue 版本一致<br />
-        </p>
-        <p><b>当前版本:</b> <span>v2.6.1</span></p>
-        <p>
-          <el-tag type="danger">&yen;免费开源</el-tag>
-        </p>
-        <p>
-          <el-button type="primary" icon="Cloudy" plain @click="goTarget('https://gitee.com/dromara/RuoYi-Cloud-Plus')">访问码云</el-button>
-          <el-button type="primary" icon="Cloudy" plain @click="goTarget('https://github.com/dromara/RuoYi-Cloud-Plus')">访问GitHub</el-button>
-          <el-button type="primary" icon="Cloudy" plain @click="goTarget('https://plus-doc.dromara.org/#/ruoyi-cloud-plus/changlog')"
-            >更新日志</el-button
-          >
-        </p>
+    <!-- 最新安全隐患 -->
+    <el-row class="mb-[20px]">
+      <el-col :span="24">
+        <el-card shadow="never" class="list-card">
+          <template #header>
+            <div class="list-header">
+              <span class="list-title">最新安全隐患</span>
+              <a class="view-all" @click="goToHazard()">查看全部→</a>
+            </div>
+          </template>
+          <el-table :data="latestHazards" stripe style="width: 100%" :show-overflow-tooltip="true">
+            <el-table-column prop="hazardNo" label="隐患编号" min-width="120" />
+            <el-table-column prop="hazardLocation" label="隐患位置" min-width="140" />
+            <el-table-column prop="hazardType" label="隐患类型" min-width="120" />
+            <el-table-column prop="riskLevel" label="风险等级" min-width="100">
+              <template #default="scope">
+                <el-tag :type="getRiskLevelType(scope.row.riskLevel)" size="small">{{ scope.row.riskLevel }}</el-tag>
+              </template>
+            </el-table-column>
+            <el-table-column prop="discoverTime" label="上报时间" min-width="120" />
+            <el-table-column prop="processStatus" label="状态" min-width="100">
+              <template #default="scope">
+                <el-tag :type="getStatusType(scope.row.processStatus)" size="small">{{ scope.row.processStatus }}</el-tag>
+              </template>
+            </el-table-column>
+          </el-table>
+        </el-card>
+      </el-col>
+    </el-row>
+
+    <!-- 底部列表 -->
+    <el-row :gutter="16">
+      <el-col :xs="24" :lg="8">
+        <el-card shadow="never" class="list-card">
+          <template #header>
+            <div class="list-header">
+              <span class="list-title">近期安全培训</span>
+              <a class="view-all" @click="goToTraining()">查看全部→</a>
+            </div>
+          </template>
+          <div class="training-list">
+            <div v-for="(item, index) in trainingList" :key="index" class="training-item" @click="goToTraining()">
+              <div class="training-info">
+                <p class="training-title">{{ item.trainingTitle }}</p>
+                <p class="training-time">{{ item.trainingTime }}</p>
+              </div>
+            </div>
+            <div v-if="!trainingList.length" class="empty-hint">暂无培训安排</div>
+          </div>
+        </el-card>
+      </el-col>
+      <el-col :xs="24" :lg="16">
+        <el-card shadow="never" class="list-card">
+          <template #header>
+            <div class="list-header">
+              <span class="list-title">最新公告</span>
+              <a class="view-all" @click="goToNotice()">查看全部→</a>
+            </div>
+          </template>
+          <div class="notice-list">
+            <div v-for="(item, index) in noticeList" :key="index" class="notice-item" @click="goToNotice()">
+              <div class="notice-content">
+                <p class="notice-title">{{ item.noticeTitle }}</p>
+                <p class="notice-desc">{{ item.noticeContent }}</p>
+              </div>
+              <span class="notice-date">{{ item.createTime }}</span>
+            </div>
+            <div v-if="!noticeList.length" class="empty-hint">暂无公告</div>
+          </div>
+        </el-card>
       </el-col>
     </el-row>
-    <el-divider />
   </div>
 </template>
 
-<script setup name="Index" lang="ts">
-const goTarget = (url: string) => {
-  window.open(url, '__blank');
+<script setup name="Dashboard" lang="ts">
+import { ref, reactive, onMounted } from 'vue';
+import { useRouter } from 'vue-router';
+import * as echarts from 'echarts';
+import { getHazardStats, getHazardTrend, getHazardDistribution, getHazardLatest } from '@/api/riskctrl/hazard';
+import { getEquipmentStats } from '@/api/riskctrl/specialEquipment';
+import { getLatestTrainings } from '@/api/riskctrl/safetyTraining';
+import { getPublishedNotices } from '@/api/riskctrl/noticeAnnouncement';
+import type { HazardVO } from '@/api/riskctrl/hazard/types';
+
+const router = useRouter();
+
+// 图表引用
+const trendChartRef = ref<HTMLDivElement>();
+const pieChartRef = ref<HTMLDivElement>();
+let trendChart: echarts.ECharts | null = null;
+let pieChart: echarts.ECharts | null = null;
+
+// 数据(与截图保持一致)
+const dashboardData = reactive({
+  hazardTotal: 128,
+  hazardProcessed: 96,
+  hazardPending: 32,
+  equipmentTotal: 47
+});
+
+// 月度隐患处理趋势数据(与截图保持一致)
+const trendData = ref<any[]>([
+  { month: '1月', total: 10, closed: 10 },
+  { month: '2月', total: 15, closed: 12 },
+  { month: '3月', total: 18, closed: 14 },
+  { month: '4月', total: 13, closed: 12 },
+  { month: '5月', total: 22, closed: 18 },
+  { month: '6月', total: 18, closed: 15 }
+]);
+
+// 隐患类型分布数据(与截图保持一致)
+const pieData = ref<any[]>([
+  { type: '电气安全', count: 35 },
+  { type: '消防隐患', count: 25 },
+  { type: '设备安全', count: 20 },
+  { type: '环境安全', count: 18 },
+  { type: '人员操作', count: 30 }
+]);
+
+const latestHazards = ref<HazardVO[]>([]);
+
+// 近期安全培训
+const trainingList = ref<Array<{ trainingTitle: string; trainingTime: string; trainingId: string | number }>>([]);
+
+// 最新公告
+const noticeList = ref<Array<{ noticeTitle: string; noticeContent: string; createTime: string; noticeId: string | number }>>([]);
+
+// 风险等级标签样式
+const getRiskLevelType = (level: string) => {
+  const map: Record<string, string> = {
+    '重大': 'danger',
+    '较大': 'warning',
+    '一般': ''
+  };
+  return map[level] || 'info';
+};
+
+// 状态标签样式
+const getStatusType = (status: string) => {
+  const map: Record<string, string> = {
+    '待处理': 'danger',
+    '处理中': 'warning',
+    '已处理': 'success'
+  };
+  return map[status] || 'info';
+};
+
+// 加载统计数据
+const loadStats = async () => {
+  try {
+    const [hazardRes, equipRes] = await Promise.all([
+      getHazardStats(),
+      getEquipmentStats()
+    ]);
+    const hdata = hazardRes.data as any;
+    if (hdata.total) dashboardData.hazardTotal = hdata.total;
+    if (hdata.processed) dashboardData.hazardProcessed = hdata.processed;
+    if (hdata.unprocessed) dashboardData.hazardPending = hdata.unprocessed;
+    if ((equipRes.data as any)?.total) dashboardData.equipmentTotal = (equipRes.data as any).total;
+  } catch (e) {
+    console.error('加载统计失败', e);
+  }
+};
+
+// 加载最新隐患
+const loadLatestHazards = async () => {
+  try {
+    const res = await getHazardLatest(5);
+    latestHazards.value = res.data || [];
+  } catch (e) {
+    console.error('加载最新隐患失败', e);
+  }
+};
+
+// 加载趋势
+const loadTrend = async () => {
+  try {
+    const res = await getHazardTrend(6);
+    trendData.value = res.data || [];
+    initTrendChart();
+  } catch (e) {
+    console.error('加载趋势失败', e);
+  }
+};
+
+// 加载分布
+const loadDistribution = async () => {
+  try {
+    const res = await getHazardDistribution();
+    pieData.value = res.data || [];
+    initPieChart();
+  } catch (e) {
+    console.error('加载分布失败', e);
+  }
+};
+
+// 初始化趋势图
+const initTrendChart = () => {
+  if (!trendChartRef.value) return;
+  if (trendChart) trendChart.dispose();
+  trendChart = echarts.init(trendChartRef.value);
+
+  const months = trendData.value.map((item: any) => item.month);
+  const totals = trendData.value.map((item: any) => item.total || 0);
+  const closeds = trendData.value.map((item: any) => item.closed || 0);
+
+  const option: echarts.EChartsOption = {
+    tooltip: {
+      trigger: 'axis',
+      axisPointer: { type: 'cross' }
+    },
+    legend: {
+      data: ['上报隐患', '已处理'],
+      bottom: 0,
+      icon: 'circle',
+      itemWidth: 8,
+      itemHeight: 8
+    },
+    grid: {
+      left: '3%',
+      right: '4%',
+      bottom: '15%',
+      top: '10%',
+      containLabel: true
+    },
+    xAxis: {
+      type: 'category',
+      boundaryGap: false,
+      data: months,
+      axisLine: { lineStyle: { color: '#e0e0e0' } },
+      axisLabel: { color: '#666', fontSize: 12 }
+    },
+    yAxis: {
+      type: 'value',
+      axisLine: { show: false },
+      splitLine: { lineStyle: { color: '#f0f0f0' } },
+      axisLabel: { color: '#666', fontSize: 12 }
+    },
+    series: [
+      {
+        name: '上报隐患',
+        type: 'line',
+        smooth: true,
+        data: totals,
+        itemStyle: { color: '#3b82f6' },
+        lineStyle: { width: 2 },
+        symbol: 'circle',
+        symbolSize: 6
+      },
+      {
+        name: '已处理',
+        type: 'line',
+        smooth: true,
+        data: closeds,
+        itemStyle: { color: '#22c55e' },
+        lineStyle: { width: 2 },
+        symbol: 'circle',
+        symbolSize: 6
+      }
+    ]
+  };
+  trendChart.setOption(option);
+};
+
+// 初始化饼图
+const initPieChart = () => {
+  if (!pieChartRef.value) return;
+  if (pieChart) pieChart.dispose();
+  pieChart = echarts.init(pieChartRef.value);
+
+  const data = pieData.value.map((item: any) => ({
+    name: item.type,
+    value: item.count
+  }));
+
+  const colors = ['#3b82f6', '#ef4444', '#f59e0b', '#22c55e', '#8b5cf6'];
+
+  const option: echarts.EChartsOption = {
+    tooltip: {
+      trigger: 'item',
+      formatter: '{b}: {c} ({d}%)'
+    },
+    legend: {
+      orient: 'vertical',
+      right: '5%',
+      top: 'center',
+      itemWidth: 10,
+      itemHeight: 10,
+      textStyle: { fontSize: 12, color: '#666' },
+      data: ['电气安全', '消防隐患', '设备安全', '环境安全', '人员操作']
+    },
+    color: colors,
+    series: [
+      {
+        name: '隐患类型',
+        type: 'pie',
+        radius: ['45%', '70%'],
+        center: ['35%', '50%'],
+        avoidLabelOverlap: false,
+        itemStyle: {
+          borderRadius: 6,
+          borderColor: '#fff',
+          borderWidth: 2
+        },
+        label: {
+          show: false
+        },
+        emphasis: {
+          label: {
+            show: true,
+            fontSize: 14,
+            fontWeight: 'bold'
+          }
+        },
+        labelLine: {
+          show: false
+        },
+        data: data.length ? data : [{ name: '暂无数据', value: 0 }]
+      }
+    ]
+  };
+  pieChart.setOption(option);
+};
+
+// 路由跳转
+const goToHazard = (status?: string) => {
+  const query: any = {};
+  if (status === '已处理') query.processStatus = '已处理';
+  if (status === '待处理') query.processStatus = '待处理';
+  router.push({ path: '/riskctrl/hazard', query });
+};
+
+const goToEquipment = () => {
+  router.push('/riskctrl/specialEquipment');
+};
+
+const goToTraining = () => {
+  router.push('/riskctrl/safetyTraining');
 };
+
+const goToNotice = () => {
+  router.push('/riskctrl/noticeAnnouncement');
+};
+
+// 加载培训和公告数据
+const loadTrainingsAndNotices = async () => {
+  try {
+    const [trainingRes, noticeRes] = await Promise.all([
+      getLatestTrainings(3),
+      getPublishedNotices(3)
+    ]);
+    if (trainingRes.data) {
+      trainingList.value = (trainingRes.data as any) || [];
+    }
+    if (noticeRes.data) {
+      noticeList.value = (noticeRes.data as any) || [];
+    }
+  } catch (e) {
+    console.error('加载培训/公告失败', e);
+  }
+};
+
+// 窗口大小变化时重绘图表
+const handleResize = () => {
+  trendChart?.resize();
+  pieChart?.resize();
+};
+
+onMounted(() => {
+  // 先初始化图表(使用静态数据)
+  setTimeout(() => {
+    initTrendChart();
+    initPieChart();
+  }, 100);
+  
+  // 尝试从后端加载数据(失败则保持静态数据)
+  loadStats();
+  loadLatestHazards();
+  loadTrend();
+  loadDistribution();
+  loadTrainingsAndNotices();
+  window.addEventListener('resize', handleResize);
+});
 </script>
 
 <style lang="scss" scoped>
 .home {
-  blockquote {
-    padding: 10px 20px;
-    margin: 0 0 20px;
-    font-size: 17.5px;
-    border-left: 5px solid #eee;
+  padding: 20px;
+  background-color: #f5f7fb;
+  min-height: 100%;
+}
+
+.welcome-header {
+  h2 {
+    font-size: 20px;
+    font-weight: 600;
+    color: #1f2937;
+  }
+  p {
+    font-size: 13px;
+    color: #9ca3af;
+  }
+}
+
+.stat-card {
+  background: #fff;
+  border-radius: 12px;
+  padding: 20px;
+  cursor: pointer;
+  transition: all 0.3s ease;
+  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
+
+  &:hover {
+    transform: translateY(-2px);
+    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08);
   }
-  hr {
-    margin-top: 20px;
-    margin-bottom: 20px;
-    border: 0;
-    border-top: 1px solid #eee;
+
+  .stat-card-content {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
   }
-  .col-item {
-    margin-bottom: 20px;
+
+  .stat-info {
+    flex: 1;
   }
 
-  ul {
-    padding: 0;
-    margin: 0;
+  .stat-label {
+    font-size: 13px;
+    color: #6b7280;
+    margin-bottom: 8px;
   }
 
-  font-family: 'open sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
-  font-size: 13px;
-  color: #676a6c;
-  overflow-x: hidden;
+  .stat-value {
+    font-size: 28px;
+    font-weight: 700;
+    color: #1f2937;
+    line-height: 1.2;
+    margin-bottom: 6px;
+  }
 
-  ul {
-    list-style-type: none;
+  .stat-trend {
+    font-size: 12px;
+
+    .trend-up {
+      color: #22c55e;
+    }
+    .trend-normal {
+      color: #22c55e;
+    }
+    .trend-warn {
+      color: #f59e0b;
+    }
   }
 
-  h4 {
-    margin-top: 0px;
+  .stat-icon {
+    width: 48px;
+    height: 48px;
+    border-radius: 10px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    flex-shrink: 0;
   }
 
-  h2 {
-    margin-top: 10px;
-    font-size: 26px;
-    font-weight: 100;
+  &.stat-card-blue .stat-icon {
+    background-color: #eff6ff;
+    color: #3b82f6;
   }
 
-  p {
-    margin-top: 10px;
+  &.stat-card-green .stat-icon {
+    background-color: #f0fdf4;
+    color: #22c55e;
+  }
+
+  &.stat-card-orange .stat-icon {
+    background-color: #fffbeb;
+    color: #f59e0b;
+  }
+
+  &.stat-card-purple .stat-icon {
+    background-color: #faf5ff;
+    color: #8b5cf6;
+  }
+}
+
+.chart-card {
+  border-radius: 12px;
+  border: none;
+
+  :deep(.el-card__header) {
+    padding: 16px 20px;
+    border-bottom: 1px solid #f3f4f6;
+  }
+
+  :deep(.el-card__body) {
+    padding: 16px 20px;
+  }
+
+  .chart-header {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+  }
+
+  .chart-title {
+    font-size: 15px;
+    font-weight: 600;
+    color: #1f2937;
+  }
+}
+
+.list-card {
+  border-radius: 12px;
+  border: none;
+
+  :deep(.el-card__header) {
+    padding: 16px 20px;
+    border-bottom: 1px solid #f3f4f6;
+  }
+
+  :deep(.el-card__body) {
+    padding: 0;
+  }
+
+  .list-header {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+  }
+
+  .list-title {
+    font-size: 15px;
+    font-weight: 600;
+    color: #1f2937;
+  }
+
+  .view-all {
+    font-size: 13px;
+    color: #3b82f6;
+    cursor: pointer;
+
+    &:hover {
+      color: #2563eb;
+    }
+  }
+}
+
+.training-list {
+  padding: 12px 20px;
+
+  .training-item {
+    padding: 12px 0;
+    border-bottom: 1px solid #f3f4f6;
+    cursor: pointer;
+
+    &:last-child {
+      border-bottom: none;
+    }
+
+    &:hover {
+      .training-title {
+        color: #3b82f6;
+      }
+    }
+
+    .training-title {
+      font-size: 14px;
+      color: #374151;
+      margin-bottom: 4px;
+      transition: color 0.2s;
+    }
 
-    b {
-      font-weight: 700;
+    .training-time {
+      font-size: 12px;
+      color: #9ca3af;
     }
   }
+}
+
+.notice-list {
+  padding: 12px 20px;
 
-  .update-log {
-    ol {
-      display: block;
-      list-style-type: decimal;
-      margin-block-start: 1em;
-      margin-block-end: 1em;
-      margin-inline-start: 0;
-      margin-inline-end: 0;
-      padding-inline-start: 40px;
+  .notice-item {
+    display: flex;
+    align-items: flex-start;
+    justify-content: space-between;
+    padding: 12px 0;
+    border-bottom: 1px solid #f3f4f6;
+    cursor: pointer;
+
+    &:last-child {
+      border-bottom: none;
+    }
+
+    &:hover {
+      .notice-title {
+        color: #3b82f6;
+      }
+    }
+
+    .notice-content {
+      flex: 1;
+      margin-right: 16px;
+    }
+
+    .notice-title {
+      font-size: 14px;
+      font-weight: 500;
+      color: #374151;
+      margin-bottom: 4px;
+      transition: color 0.2s;
+    }
+
+    .notice-desc {
+      font-size: 12px;
+      color: #9ca3af;
+      line-height: 1.5;
+      overflow: hidden;
+      text-overflow: ellipsis;
+      display: -webkit-box;
+      -webkit-line-clamp: 1;
+      -webkit-box-orient: vertical;
+    }
+
+    .notice-date {
+      font-size: 12px;
+      color: #9ca3af;
+      white-space: nowrap;
+      flex-shrink: 0;
     }
   }
 }
+
+.empty-hint {
+  text-align: center;
+  padding: 20px;
+  color: #9ca3af;
+  font-size: 13px;
+}
 </style>

+ 472 - 0
src/views/riskctrl/accidentReport/index.vue

@@ -0,0 +1,472 @@
+<template>
+  <div class="p-2">
+    <!-- 顶部数据卡片 -->
+    <el-row :gutter="16" class="mb-[10px]">
+      <el-col :span="6">
+        <el-card shadow="hover">
+          <div class="flex items-center justify-between">
+            <div>
+              <p class="text-gray-500 text-sm">本月上报事故</p>
+              <p class="text-2xl font-bold text-blue-500">{{ statsData.monthTotal }}</p>
+              <p class="text-xs mt-1">
+                <span :class="parseFloat(statsData.monthRate) >= 0 ? 'text-red-500' : 'text-green-500'">
+                  较上月 {{ parseFloat(statsData.monthRate) >= 0 ? '+' : '' }}{{ statsData.monthRate }}
+                </span>
+              </p>
+            </div>
+            <el-icon :size="40" color="#409EFF"><DataAnalysis /></el-icon>
+          </div>
+        </el-card>
+      </el-col>
+      <el-col :span="6">
+        <el-card shadow="hover">
+          <div class="flex items-center justify-between">
+            <div>
+              <p class="text-gray-500 text-sm">调查中</p>
+              <p class="text-2xl font-bold text-orange-500">{{ statsData.investigating }}</p>
+              <p class="text-xs text-gray-400 mt-1">较上月 {{ statsData.investigatingRate }}</p>
+            </div>
+            <el-icon :size="40" color="#E6A23C"><Search /></el-icon>
+          </div>
+        </el-card>
+      </el-col>
+      <el-col :span="6">
+        <el-card shadow="hover">
+          <div class="flex items-center justify-between">
+            <div>
+              <p class="text-gray-500 text-sm">已结束</p>
+              <p class="text-2xl font-bold text-green-500">{{ statsData.closed }}</p>
+              <p class="text-xs text-gray-400 mt-1">较上月 {{ statsData.closedRate }}</p>
+            </div>
+            <el-icon :size="40" color="#67C23A"><CircleCheckFilled /></el-icon>
+          </div>
+        </el-card>
+      </el-col>
+      <el-col :span="6">
+        <el-card shadow="hover">
+          <div class="flex items-center justify-between">
+            <div>
+              <p class="text-gray-500 text-sm">待考核</p>
+              <p class="text-2xl font-bold text-purple-500">{{ statsData.pendingAssessment }}</p>
+              <p class="text-xs text-gray-400 mt-1">较上月 {{ statsData.pendingRate }}</p>
+            </div>
+            <el-icon :size="40" color="#9B59B6"><DocumentChecked /></el-icon>
+          </div>
+        </el-card>
+      </el-col>
+    </el-row>
+
+    <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
+      <div v-show="showSearch" class="mb-[10px]">
+        <el-card shadow="hover">
+          <el-form ref="queryFormRef" :model="queryParams" :inline="true">
+            <el-form-item label="关键词" prop="keyword">
+              <el-input v-model="queryParams.keyword" placeholder="编号/名称搜索" clearable @keyup.enter="handleQuery" />
+            </el-form-item>
+            <el-form-item label="状态" prop="status">
+              <el-select v-model="queryParams.status" placeholder="状态" clearable>
+                <el-option label="调查中" value="调查中" />
+                <el-option label="已归档" value="已归档" />
+                <el-option label="已结束" value="已结束" />
+                <el-option label="考核中" value="考核中" />
+                <el-option label="待考核" value="待考核" />
+              </el-select>
+            </el-form-item>
+            <el-form-item label="等级" prop="level">
+              <el-select v-model="queryParams.level" placeholder="等级" clearable>
+                <el-option label="特大事故" value="特大事故" />
+                <el-option label="重大事故" value="重大事故" />
+                <el-option label="较大事故" value="较大事故" />
+                <el-option label="一般事故" value="一般事故" />
+              </el-select>
+            </el-form-item>
+            <el-form-item label="类型" prop="type">
+              <el-select v-model="queryParams.type" placeholder="类型" clearable>
+                <el-option label="高处坠落" value="高处坠落" />
+                <el-option label="物体打击" value="物体打击" />
+                <el-option label="机械伤害" value="机械伤害" />
+                <el-option label="触电" value="触电" />
+                <el-option label="火灾" value="火灾" />
+                <el-option label="坍塌" value="坍塌" />
+                <el-option label="其他" value="其他" />
+              </el-select>
+            </el-form-item>
+            <el-form-item>
+              <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
+              <el-button icon="Refresh" @click="resetQuery">重置</el-button>
+            </el-form-item>
+          </el-form>
+        </el-card>
+      </div>
+    </transition>
+
+    <el-card shadow="hover">
+      <template #header>
+        <el-row :gutter="10">
+          <el-col :span="1.5">
+            <el-button v-has-permi="['riskctrl:accidentReport:add']" type="primary" plain icon="Plus" @click="handleAdd()">新增上报</el-button>
+          </el-col>
+          <el-col :span="1.5">
+            <el-button v-has-permi="['riskctrl:accidentReport:edit']" type="success" plain :disabled="single" icon="Edit" @click="handleUpdate()">修改</el-button>
+          </el-col>
+          <el-col :span="1.5">
+            <el-button v-has-permi="['riskctrl:accidentReport:remove']" type="danger" plain :disabled="multiple" icon="Delete" @click="handleDelete()">删除</el-button>
+          </el-col>
+          <right-toolbar v-model:show-search="showSearch" :columns="columns" :search="true" @query-table="getList"></right-toolbar>
+        </el-row>
+      </template>
+
+      <el-table v-loading="loading" border :data="accidentList" @selection-change="handleSelectionChange">
+        <el-table-column type="selection" width="50" align="center" />
+        <el-table-column v-if="columns[0].visible" key="accidentNo" label="事故编号" align="center" prop="accidentNo" width="140" />
+        <el-table-column v-if="columns[1].visible" key="accidentName" label="事故名称" align="center" prop="accidentName" :show-overflow-tooltip="true" />
+        <el-table-column v-if="columns[2].visible" key="accidentTime" label="事故时间" align="center" prop="accidentTime" width="160" />
+        <el-table-column v-if="columns[3].visible" key="accidentLocation" label="地点" align="center" prop="accidentLocation" :show-overflow-tooltip="true" />
+        <el-table-column v-if="columns[4].visible" key="level" label="等级" align="center" prop="level" width="100">
+          <template #default="scope">
+            <el-tag :type="getLevelType(scope.row.level)">{{ scope.row.level }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column v-if="columns[5].visible" key="type" label="类型" align="center" prop="type" width="100" />
+        <el-table-column v-if="columns[6].visible" key="status" label="状态" align="center" prop="status" width="100">
+          <template #default="scope">
+            <el-tag :type="getStatusType(scope.row.status)">{{ scope.row.status }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column v-if="columns[7].visible" label="创建时间" align="center" prop="createTime" width="160" />
+        <el-table-column label="操作" fixed="right" width="180" class-name="small-padding fixed-width">
+          <template #default="scope">
+            <el-tooltip content="修改" placement="top">
+              <el-button v-has-permi="['riskctrl:accidentReport:edit']" link type="primary" icon="Edit" @click="handleUpdate(scope.row)"></el-button>
+            </el-tooltip>
+            <el-tooltip content="详情" placement="top">
+              <el-button link type="primary" icon="View" @click="handleDetail(scope.row)"></el-button>
+            </el-tooltip>
+            <el-tooltip content="删除" placement="top">
+              <el-button v-has-permi="['riskctrl:accidentReport:remove']" link type="primary" icon="Delete" @click="handleDelete(scope.row)"></el-button>
+            </el-tooltip>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <pagination v-show="total > 0" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" :total="total" @pagination="getList" />
+    </el-card>
+
+    <!-- 新增上报弹窗 -->
+    <el-dialog v-model="open" :title="title" width="800px" append-to-body>
+      <el-form ref="accidentFormRef" :model="form" :rules="rules" label-width="120px">
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="事故名称" prop="accidentName">
+              <el-input v-model="form.accidentName" placeholder="如:XX工位机械夹伤事故" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="事故时间" prop="accidentTime">
+              <el-date-picker v-model="form.accidentTime" type="datetime" placeholder="选择时间" value-format="YYYY-MM-DD HH:mm:ss" class="w-full" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="事故等级" prop="level">
+              <el-select v-model="form.level" placeholder="请选择" class="w-full">
+                <el-option label="特大事故" value="特大事故" />
+                <el-option label="重大事故" value="重大事故" />
+                <el-option label="较大事故" value="较大事故" />
+                <el-option label="一般事故" value="一般事故" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="事故类型" prop="type">
+              <el-select v-model="form.type" placeholder="请选择" class="w-full">
+                <el-option label="高处坠落" value="高处坠落" />
+                <el-option label="物体打击" value="物体打击" />
+                <el-option label="机械伤害" value="机械伤害" />
+                <el-option label="触电" value="触电" />
+                <el-option label="火灾" value="火灾" />
+                <el-option label="坍塌" value="坍塌" />
+                <el-option label="其他" value="其他" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-form-item label="事故地点" prop="accidentLocation">
+          <el-input v-model="form.accidentLocation" placeholder="如:XX车间XX工位 / XX园区XX路段" />
+        </el-form-item>
+        <el-form-item label="涉及人员" prop="involvedPerson">
+          <el-input v-model="form.involvedPerson" placeholder="如:张三(操作工)、李四(班组长)" />
+        </el-form-item>
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="死亡人数">
+              <el-input-number v-model="form.deathCount" :min="0" class="w-full" controls-position="right" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="受伤人数">
+              <el-input-number v-model="form.injuryCount" :min="0" class="w-full" controls-position="right" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-form-item label="事故简要经过" prop="accidentBrief">
+          <el-input v-model="form.accidentBrief" type="textarea" :rows="3" placeholder="描述事故发生的过程,包含时间、地点、事件、后果等关键信息" />
+        </el-form-item>
+        <el-form-item label="初步原因分析" prop="preliminaryAnalysis">
+          <el-input v-model="form.preliminaryAnalysis" type="textarea" :rows="3" placeholder="如:操作违规、设备故障、环境因素等" />
+        </el-form-item>
+        <el-form-item label="上报人" prop="reporter">
+          <el-input v-model="form.reporter" placeholder="自动填充当前用户" />
+        </el-form-item>
+        <el-form-item label="现场照片">
+          <el-upload :action="uploadUrl" :headers="uploadHeaders" list-type="picture-card" :limit="5">
+            <el-icon><Plus /></el-icon>
+          </el-upload>
+        </el-form-item>
+        <el-form-item label="状态" prop="status">
+          <el-radio-group v-model="form.status">
+            <el-radio v-for="dict in sys_normal_disable" :key="dict.value" :label="dict.value">{{ dict.label }}</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="备注" prop="remark">
+          <el-input v-model="form.remark" type="textarea" placeholder="请输入备注" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button type="primary" @click="submitForm">确 定</el-button>
+          <el-button @click="cancel">取 消</el-button>
+        </div>
+      </template>
+    </el-dialog>
+
+    <!-- 详情弹窗 -->
+    <el-dialog v-model="detailOpen" title="事故详情" width="800px" append-to-body>
+      <el-descriptions :column="2" border>
+        <el-descriptions-item label="事故编号">{{ detailData.accidentNo }}</el-descriptions-item>
+        <el-descriptions-item label="事故名称">{{ detailData.accidentName }}</el-descriptions-item>
+        <el-descriptions-item label="事故时间">{{ detailData.accidentTime }}</el-descriptions-item>
+        <el-descriptions-item label="事故地点">{{ detailData.accidentLocation }}</el-descriptions-item>
+        <el-descriptions-item label="事故等级">
+          <el-tag :type="getLevelType(detailData.level)">{{ detailData.level }}</el-tag>
+        </el-descriptions-item>
+        <el-descriptions-item label="事故类型">{{ detailData.type }}</el-descriptions-item>
+        <el-descriptions-item label="涉及人员" :span="2">{{ detailData.involvedPerson }}</el-descriptions-item>
+        <el-descriptions-item label="死亡人数">{{ detailData.deathCount }}</el-descriptions-item>
+        <el-descriptions-item label="受伤人数">{{ detailData.injuryCount }}</el-descriptions-item>
+        <el-descriptions-item label="事故经过" :span="2">{{ detailData.accidentBrief }}</el-descriptions-item>
+        <el-descriptions-item label="初步分析" :span="2">{{ detailData.preliminaryAnalysis }}</el-descriptions-item>
+        <el-descriptions-item label="上报人">{{ detailData.reporter }}</el-descriptions-item>
+        <el-descriptions-item label="状态">
+          <el-tag :type="getStatusType(detailData.status)">{{ detailData.status }}</el-tag>
+        </el-descriptions-item>
+      </el-descriptions>
+      <template #footer>
+        <el-button @click="detailOpen = false">关 闭</el-button>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup name="AccidentReport" lang="ts">
+import { getToken } from '@/utils/auth';
+import { listAccidentReport, getAccidentReport, delAccidentReport, addAccidentReport, updateAccidentReport, getAccidentStats } from '@/api/riskctrl/accidentReport';
+import { AccidentReportForm, AccidentReportQuery, AccidentReportVO, AccidentStats } from '@/api/riskctrl/accidentReport/types';
+
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+const { sys_normal_disable } = proxy.useDict('sys_normal_disable');
+
+const accidentList = ref<AccidentReportVO[]>([]);
+const loading = ref(true);
+const showSearch = ref(true);
+const ids = ref<Array<string | number>>([]);
+const single = ref(true);
+const multiple = ref(true);
+const total = ref(0);
+const title = ref('');
+const open = ref(false);
+const detailOpen = ref(false);
+
+const uploadUrl = import.meta.env.VITE_APP_BASE_API + '/resource/oss/upload';
+const uploadHeaders = { Authorization: 'Bearer ' + getToken(), clientid: import.meta.env.VITE_APP_CLIENT_ID };
+
+const queryFormRef = ref<ElFormInstance>();
+const accidentFormRef = ref<ElFormInstance>();
+
+const statsData = reactive<AccidentStats>({
+  monthTotal: 0,
+  monthRate: '0%',
+  investigating: 0,
+  investigatingRate: '0%',
+  closed: 0,
+  closedRate: '0%',
+  pendingAssessment: 0,
+  pendingRate: '0%'
+});
+
+const detailData = reactive<AccidentReportVO>({} as AccidentReportVO);
+
+const queryParams = reactive<AccidentReportQuery>({
+  pageNum: 1,
+  pageSize: 10,
+  keyword: undefined,
+  status: undefined,
+  level: undefined,
+  type: undefined
+});
+
+const form = reactive<AccidentReportForm>({
+  accidentId: undefined,
+  accidentName: '',
+  accidentTime: '',
+  accidentLocation: '',
+  level: '一般事故',
+  type: '',
+  involvedPerson: '',
+  deathCount: 0,
+  injuryCount: 0,
+  accidentBrief: '',
+  preliminaryAnalysis: '',
+  reporter: '',
+  status: '0',
+  remark: ''
+});
+
+const rules = {
+  accidentName: [{ required: true, message: '事故名称不能为空', trigger: 'blur' }],
+  accidentTime: [{ required: true, message: '事故时间不能为空', trigger: 'change' }],
+  accidentLocation: [{ required: true, message: '事故地点不能为空', trigger: 'blur' }],
+  level: [{ required: true, message: '事故等级不能为空', trigger: 'change' }],
+  type: [{ required: true, message: '事故类型不能为空', trigger: 'change' }]
+};
+
+const columns = ref([
+  { key: 0, label: '事故编号', visible: true },
+  { key: 1, label: '事故名称', visible: true },
+  { key: 2, label: '事故时间', visible: true },
+  { key: 3, label: '地点', visible: true },
+  { key: 4, label: '等级', visible: true },
+  { key: 5, label: '类型', visible: true },
+  { key: 6, label: '状态', visible: true },
+  { key: 7, label: '创建时间', visible: true }
+]);
+
+type TagType = 'success' | 'warning' | 'danger' | 'info' | 'primary';
+
+const getLevelType = (level: string): TagType => {
+  const map: Record<string, TagType> = {
+    '特大事故': 'danger',
+    '重大事故': 'warning',
+    '较大事故': 'primary',
+    '一般事故': 'info'
+  };
+  return map[level] || 'info';
+};
+
+const getStatusType = (status: string): TagType => {
+  const map: Record<string, TagType> = {
+    '调查中': 'warning',
+    '已归档': 'success',
+    '已结束': 'info',
+    '考核中': 'danger',
+    '待考核': 'primary'
+  };
+  return map[status] || 'info';
+};
+
+const loadStats = async () => {
+  try {
+    const res = await getAccidentStats();
+    Object.assign(statsData, res.data);
+  } catch { /* ignore */ }
+};
+
+const getList = async () => {
+  loading.value = true;
+  const res = await listAccidentReport(queryParams);
+  accidentList.value = res.rows;
+  total.value = res.total;
+  loading.value = false;
+};
+
+const cancel = () => {
+  open.value = false;
+  reset();
+};
+
+const reset = () => {
+  form.accidentId = undefined;
+  form.accidentName = '';
+  form.accidentTime = '';
+  form.accidentLocation = '';
+  form.level = '一般事故';
+  form.type = '';
+  form.involvedPerson = '';
+  form.deathCount = 0;
+  form.injuryCount = 0;
+  form.accidentBrief = '';
+  form.preliminaryAnalysis = '';
+  form.reporter = '';
+  form.status = '0';
+  form.remark = '';
+  nextTick(() => { accidentFormRef.value?.resetFields(); });
+};
+
+const handleQuery = () => { queryParams.pageNum = 1; getList(); };
+const resetQuery = () => { queryFormRef.value?.resetFields(); handleQuery(); };
+
+const handleSelectionChange = (selection: AccidentReportVO[]) => {
+  ids.value = selection.map((item) => item.accidentId);
+  single.value = selection.length !== 1;
+  multiple.value = !selection.length;
+};
+
+const handleAdd = () => { reset(); open.value = true; title.value = '新增事故上报'; };
+
+const handleUpdate = async (row?: AccidentReportVO) => {
+  reset();
+  const accidentId = row?.accidentId || ids.value[0];
+  const res = await getAccidentReport(accidentId);
+  Object.assign(form, res.data);
+  open.value = true;
+  title.value = '修改事故上报';
+};
+
+const submitForm = () => {
+  accidentFormRef.value?.validate(async (valid: boolean) => {
+    if (valid) {
+      if (form.accidentId !== undefined) {
+        await updateAccidentReport(form);
+        proxy?.$modal.msgSuccess('修改成功');
+      } else {
+        await addAccidentReport(form);
+        proxy?.$modal.msgSuccess('上报成功');
+      }
+      open.value = false;
+      await getList();
+      await loadStats();
+    }
+  });
+};
+
+const handleDetail = async (row: AccidentReportVO) => {
+  const res = await getAccidentReport(row.accidentId);
+  Object.assign(detailData, res.data);
+  detailOpen.value = true;
+};
+
+const handleDelete = async (row?: AccidentReportVO) => {
+  const accidentIds = row?.accidentId || ids.value;
+  await proxy?.$modal.confirm('是否确认删除该事故上报记录?');
+  await delAccidentReport(accidentIds);
+  await getList();
+  await loadStats();
+  proxy?.$modal.msgSuccess('删除成功');
+};
+
+onMounted(() => {
+  loadStats();
+  getList();
+});
+</script>

+ 404 - 0
src/views/riskctrl/auditPersonChange/index.vue

@@ -0,0 +1,404 @@
+<template>
+  <div class="p-2">
+    <!-- 顶部统计卡片 -->
+    <el-row :gutter="16" class="mb-[10px]">
+      <el-col :span="6">
+        <el-card shadow="hover">
+          <div class="flex items-center justify-between">
+            <div>
+              <p class="text-gray-500 text-sm">人员变动总数</p>
+              <p class="text-2xl font-bold text-blue-500">{{ statsData.totalChanges }}</p>
+              <p class="text-xs mt-1">
+                <span :class="statsData.totalTrend >= 0 ? 'text-green-500' : 'text-red-500'">
+                  {{ statsData.totalTrend >= 0 ? '↑' : '↓' }} 较上月 {{ Math.abs(statsData.totalTrend) }}
+                </span>
+              </p>
+            </div>
+            <el-icon :size="40" color="#409EFF"><UserFilled /></el-icon>
+          </div>
+        </el-card>
+      </el-col>
+      <el-col :span="6">
+        <el-card shadow="hover">
+          <div class="flex items-center justify-between">
+            <div>
+              <p class="text-gray-500 text-sm">待审计离职</p>
+              <p class="text-2xl font-bold text-orange-500">{{ statsData.pendingAudit }}</p>
+              <p class="text-xs mt-1">
+                <span :class="statsData.pendingTrend >= 0 ? 'text-green-500' : 'text-red-500'">
+                  {{ statsData.pendingTrend >= 0 ? '↑' : '↓' }} 较上月 {{ Math.abs(statsData.pendingTrend) }}
+                </span>
+              </p>
+            </div>
+            <el-icon :size="40" color="#E6A23C"><Clock /></el-icon>
+          </div>
+        </el-card>
+      </el-col>
+      <el-col :span="6">
+        <el-card shadow="hover">
+          <div class="flex items-center justify-between">
+            <div>
+              <p class="text-gray-500 text-sm">已完成调任</p>
+              <p class="text-2xl font-bold text-green-500">{{ statsData.completedTransfer }}</p>
+              <p class="text-xs mt-1">
+                <span :class="statsData.transferTrend >= 0 ? 'text-green-500' : 'text-red-500'">
+                  {{ statsData.transferTrend >= 0 ? '↑' : '↓' }} 较上月 {{ Math.abs(statsData.transferTrend) }}
+                </span>
+              </p>
+            </div>
+            <el-icon :size="40" color="#67C23A"><CircleCheckFilled /></el-icon>
+          </div>
+        </el-card>
+      </el-col>
+      <el-col :span="6">
+        <el-card shadow="hover">
+          <div class="flex items-center justify-between">
+            <div>
+              <p class="text-gray-500 text-sm">审计文件总数</p>
+              <p class="text-2xl font-bold text-purple-500">{{ statsData.totalFiles }}</p>
+              <p class="text-xs mt-1">
+                <span :class="statsData.fileTrend >= 0 ? 'text-green-500' : 'text-red-500'">
+                  {{ statsData.fileTrend >= 0 ? '↑' : '↓' }} 较上月 {{ Math.abs(statsData.fileTrend) }}
+                </span>
+              </p>
+            </div>
+            <el-icon :size="40" color="#8B5CF6"><FolderOpened /></el-icon>
+          </div>
+        </el-card>
+      </el-col>
+    </el-row>
+
+    <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
+      <div v-show="showSearch" class="mb-[10px]">
+        <el-card shadow="hover">
+          <el-form ref="queryFormRef" :model="queryParams" :inline="true">
+            <el-form-item label="变动类型" prop="changeType">
+              <el-select v-model="queryParams.changeType" placeholder="类型" clearable>
+                <el-option label="调任" value="调任" />
+                <el-option label="离职" value="离职" />
+              </el-select>
+            </el-form-item>
+            <el-form-item label="部门" prop="department">
+              <el-input v-model="queryParams.department" placeholder="部门" clearable @keyup.enter="handleQuery" />
+            </el-form-item>
+            <el-form-item label="关键词" prop="employeeName">
+              <el-input v-model="queryParams.employeeName" placeholder="搜索姓名" clearable @keyup.enter="handleQuery" />
+            </el-form-item>
+            <el-form-item>
+              <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
+              <el-button icon="Refresh" @click="resetQuery">重置</el-button>
+            </el-form-item>
+          </el-form>
+        </el-card>
+      </div>
+    </transition>
+
+    <el-card shadow="hover">
+      <template #header>
+        <el-row :gutter="10">
+          <el-col :span="1.5">
+            <el-button v-has-permi="['riskctrl:auditPersonChange:add']" type="primary" plain icon="Plus" @click="handleAdd()">新增人员变动</el-button>
+          </el-col>
+          <el-col :span="1.5">
+            <el-button v-has-permi="['riskctrl:auditPersonChange:upload']" type="warning" plain icon="Upload" @click="uploadFileOpen = true">上传审计文件</el-button>
+          </el-col>
+          <el-col :span="1.5">
+            <el-button v-has-permi="['riskctrl:auditPersonChange:remove']" type="danger" plain :disabled="multiple" icon="Delete" @click="handleDelete()">删除</el-button>
+          </el-col>
+          <right-toolbar v-model:show-search="showSearch" :columns="columns" :search="true" @query-table="getList"></right-toolbar>
+        </el-row>
+      </template>
+
+      <el-table v-loading="loading" border :data="changeList" @selection-change="handleSelectionChange">
+        <el-table-column type="selection" width="50" align="center" />
+        <el-table-column v-if="columns[0].visible" key="employeeName" label="员工姓名" align="center" prop="employeeName" width="100" />
+        <el-table-column v-if="columns[1].visible" key="department" label="部门" align="center" prop="department" width="120" />
+        <el-table-column v-if="columns[2].visible" key="oldPosition" label="原岗位" align="center" prop="oldPosition" :show-overflow-tooltip="true" />
+        <el-table-column v-if="columns[3].visible" key="newPosition" label="变动后岗位" align="center" prop="newPosition" :show-overflow-tooltip="true" />
+        <el-table-column v-if="columns[4].visible" key="changeType" label="变动类型" align="center" prop="changeType" width="100">
+          <template #default="scope">
+            <el-tag :type="scope.row.changeType === '离职' ? 'danger' : 'primary'">{{ scope.row.changeType }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column v-if="columns[5].visible" key="changeDate" label="变动日期" align="center" prop="changeDate" width="120" />
+        <el-table-column v-if="columns[6].visible" key="auditStatus" label="审计状态" align="center" prop="auditStatus" width="100">
+          <template #default="scope">
+            <el-tag :type="scope.row.auditStatus === '已完成' ? 'success' : 'warning'">{{ scope.row.auditStatus }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column v-if="columns[7].visible" label="创建时间" align="center" prop="createTime" width="160" />
+        <el-table-column label="操作" fixed="right" width="180" class-name="small-padding fixed-width">
+          <template #default="scope">
+            <el-tooltip content="修改" placement="top">
+              <el-button v-has-permi="['riskctrl:auditPersonChange:edit']" link type="primary" icon="Edit" @click="handleUpdate(scope.row)"></el-button>
+            </el-tooltip>
+            <el-tooltip content="删除" placement="top">
+              <el-button v-has-permi="['riskctrl:auditPersonChange:remove']" link type="primary" icon="Delete" @click="handleDelete(scope.row)"></el-button>
+            </el-tooltip>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <pagination v-show="total > 0" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" :total="total" @pagination="getList" />
+    </el-card>
+
+    <!-- 新增/修改弹窗 -->
+    <el-dialog v-model="open" :title="title" width="700px" append-to-body>
+      <el-form ref="changeFormRef" :model="form" :rules="rules" label-width="110px">
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="员工姓名" prop="employeeName">
+              <el-input v-model="form.employeeName" placeholder="请输入员工姓名" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="所属部门" prop="department">
+              <el-input v-model="form.department" placeholder="自动带出" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="变动类型" prop="changeType">
+              <el-select v-model="form.changeType" placeholder="请选择" class="w-full" @change="onChangeType">
+                <el-option label="调任" value="调任" />
+                <el-option label="离职" value="离职" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="变动日期" prop="changeDate">
+              <el-date-picker v-model="form.changeDate" type="date" placeholder="选择日期" value-format="YYYY-MM-DD" class="w-full" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="原岗位" prop="oldPosition">
+              <el-input v-model="form.oldPosition" placeholder="请输入原岗位" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="变动后岗位" prop="newPosition">
+              <el-input v-model="form.newPosition" placeholder="离职填写-" :disabled="form.changeType === '离职'" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-form-item v-if="form.changeType === '调任'" label="调入部门" prop="newDepartment">
+          <el-input v-model="form.newDepartment" placeholder="请输入调入部门" />
+        </el-form-item>
+        <el-form-item label="备注" prop="remark">
+          <el-input v-model="form.remark" type="textarea" placeholder="请输入备注" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button type="primary" @click="submitForm">确 定</el-button>
+          <el-button @click="cancel">取 消</el-button>
+        </div>
+      </template>
+    </el-dialog>
+
+    <!-- 上传审计文件弹窗 -->
+    <el-dialog v-model="uploadFileOpen" title="上传审计文件" width="550px" append-to-body>
+      <el-upload
+        v-model:file-list="auditFileList"
+        action="#"
+        :auto-upload="false"
+        multiple
+        drag
+        :limit="10"
+      >
+        <el-icon class="el-icon--upload"><UploadFilled /></el-icon>
+        <div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
+        <template #tip>
+          <div class="el-upload__tip">支持 PDF、Word、Excel 等格式,单文件不超过 10MB</div>
+        </template>
+      </el-upload>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button type="primary" @click="submitAuditFiles">确 定</el-button>
+          <el-button @click="uploadFileOpen = false">取 消</el-button>
+        </div>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup name="AuditPersonChange" lang="ts">
+import { listAuditPersonChange, getAuditPersonChange, delAuditPersonChange, addAuditPersonChange, updateAuditPersonChange } from '@/api/riskctrl/auditPersonChange';
+import { AuditPersonChangeForm, AuditPersonChangeQuery, AuditPersonChangeVO } from '@/api/riskctrl/auditPersonChange/types';
+
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+
+const changeList = ref<AuditPersonChangeVO[]>([]);
+const loading = ref(true);
+const showSearch = ref(true);
+const ids = ref<Array<string | number>>([]);
+const single = ref(true);
+const multiple = ref(true);
+const total = ref(0);
+const title = ref('');
+const open = ref(false);
+const uploadFileOpen = ref(false);
+const auditFileList = ref<any[]>([]);
+
+// 统计数据
+const statsData = reactive({
+  totalChanges: 0,
+  pendingAudit: 0,
+  completedTransfer: 0,
+  totalFiles: 0,
+  totalTrend: 0,
+  pendingTrend: 0,
+  transferTrend: 0,
+  fileTrend: 0
+});
+
+const queryFormRef = ref<ElFormInstance>();
+const changeFormRef = ref<ElFormInstance>();
+
+const queryParams = reactive<AuditPersonChangeQuery & { department?: string }>({
+  pageNum: 1,
+  pageSize: 10,
+  employeeName: undefined,
+  department: undefined,
+  changeType: undefined,
+  auditStatus: undefined
+});
+
+const form = reactive<AuditPersonChangeForm & { newDepartment?: string }>({
+  changeId: undefined,
+  employeeName: '',
+  department: '',
+  oldPosition: '',
+  newPosition: '',
+  changeType: '',
+  changeDate: '',
+  remark: ''
+});
+
+const rules = {
+  employeeName: [{ required: true, message: '员工姓名不能为空', trigger: 'blur' }],
+  changeType: [{ required: true, message: '变动类型不能为空', trigger: 'change' }]
+};
+
+const columns = ref([
+  { key: 0, label: '员工姓名', visible: true },
+  { key: 1, label: '部门', visible: true },
+  { key: 2, label: '原岗位', visible: true },
+  { key: 3, label: '变动后岗位', visible: true },
+  { key: 4, label: '变动类型', visible: true },
+  { key: 5, label: '变动日期', visible: true },
+  { key: 6, label: '审计状态', visible: true },
+  { key: 7, label: '创建时间', visible: true }
+]);
+
+const onChangeType = (val: string) => {
+  if (val === '离职') {
+    form.newPosition = '-';
+  } else {
+    form.newPosition = '';
+  }
+};
+
+const getList = async () => {
+  loading.value = true;
+  const res = await listAuditPersonChange(queryParams);
+  changeList.value = res.rows;
+  total.value = res.total;
+  loading.value = false;
+};
+
+const cancel = () => {
+  open.value = false;
+  reset();
+};
+
+const reset = () => {
+  form.changeId = undefined;
+  form.employeeName = '';
+  form.department = '';
+  form.oldPosition = '';
+  form.newPosition = '';
+  form.changeType = '';
+  form.changeDate = '';
+  form.newDepartment = '';
+  form.remark = '';
+  nextTick(() => { changeFormRef.value?.resetFields(); });
+};
+
+const handleQuery = () => { queryParams.pageNum = 1; getList(); };
+const resetQuery = () => { queryFormRef.value?.resetFields(); handleQuery(); };
+
+const handleSelectionChange = (selection: AuditPersonChangeVO[]) => {
+  ids.value = selection.map((item) => item.changeId);
+  single.value = selection.length !== 1;
+  multiple.value = !selection.length;
+};
+
+const handleAdd = () => { reset(); open.value = true; title.value = '新增人员变动'; };
+
+const handleUpdate = async (row?: AuditPersonChangeVO) => {
+  reset();
+  const changeId = row?.changeId || ids.value[0];
+  const res = await getAuditPersonChange(changeId);
+  Object.assign(form, res.data);
+  open.value = true;
+  title.value = '修改人员变动';
+};
+
+const submitForm = () => {
+  changeFormRef.value?.validate(async (valid: boolean) => {
+    if (valid) {
+      if (form.changeId !== undefined) {
+        await updateAuditPersonChange(form as AuditPersonChangeForm);
+        proxy?.$modal.msgSuccess('修改成功');
+      } else {
+        await addAuditPersonChange(form as AuditPersonChangeForm);
+        proxy?.$modal.msgSuccess('新增成功');
+      }
+      open.value = false;
+      await getList();
+    }
+  });
+};
+
+const handleDelete = async (row?: AuditPersonChangeVO) => {
+  const changeIds = row?.changeId || ids.value;
+  await proxy?.$modal.confirm('是否确认删除该人员变动记录?');
+  await delAuditPersonChange(changeIds);
+  await getList();
+  proxy?.$modal.msgSuccess('删除成功');
+};
+
+// 加载统计数据
+const loadStats = async () => {
+  try {
+    const res = await listAuditPersonChange({ pageNum: 1, pageSize: 9999 } as any);
+    const allData = (res as any).rows || [];
+    statsData.totalChanges = allData.length;
+    statsData.pendingAudit = allData.filter((d: any) => d.auditStatus === '待审计').length;
+    statsData.completedTransfer = allData.filter((d: any) => d.changeType === '调任' && d.auditStatus === '已完成').length;
+  } catch (e) {
+    console.error('加载统计数据失败', e);
+  }
+};
+
+// 提交审计文件
+const submitAuditFiles = () => {
+  if (auditFileList.value.length === 0) {
+    proxy?.$modal.msgWarning('请选择要上传的文件');
+    return;
+  }
+  statsData.totalFiles += auditFileList.value.length;
+  proxy?.$modal.msgSuccess(`成功上传 ${auditFileList.value.length} 个审计文件`);
+  uploadFileOpen.value = false;
+  auditFileList.value = [];
+};
+
+onMounted(() => {
+  getList();
+  loadStats();
+});
+</script>

+ 277 - 0
src/views/riskctrl/blacklist/index.vue

@@ -0,0 +1,277 @@
+<template>
+  <div class="p-2">
+    <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
+      <div v-show="showSearch" class="mb-[10px]">
+        <el-card shadow="hover">
+          <el-form ref="queryFormRef" :model="queryParams" :inline="true">
+            <el-form-item label="黑名单类型" prop="blackType">
+              <el-select v-model="queryParams.blackType" placeholder="黑名单类型" clearable>
+                <el-option v-for="dict in risk_list_type" :key="dict.value" :label="dict.label" :value="dict.value" />
+              </el-select>
+            </el-form-item>
+            <el-form-item label="黑名单值" prop="blackValue">
+              <el-input v-model="queryParams.blackValue" placeholder="请输入黑名单值" clearable @keyup.enter="handleQuery" />
+            </el-form-item>
+            <el-form-item label="状态" prop="status">
+              <el-select v-model="queryParams.status" placeholder="状态" clearable>
+                <el-option v-for="dict in sys_normal_disable" :key="dict.value" :label="dict.label" :value="dict.value" />
+              </el-select>
+            </el-form-item>
+            <el-form-item>
+              <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
+              <el-button icon="Refresh" @click="resetQuery">重置</el-button>
+            </el-form-item>
+          </el-form>
+        </el-card>
+      </div>
+    </transition>
+
+    <el-card shadow="hover">
+      <template #header>
+        <el-row :gutter="10">
+          <el-col :span="1.5">
+            <el-button v-has-permi="['riskctrl:blacklist:add']" type="primary" plain icon="Plus" @click="handleAdd()">新增</el-button>
+          </el-col>
+          <el-col :span="1.5">
+            <el-button v-has-permi="['riskctrl:blacklist:edit']" type="success" plain :disabled="single" icon="Edit" @click="handleUpdate()">修改</el-button>
+          </el-col>
+          <el-col :span="1.5">
+            <el-button v-has-permi="['riskctrl:blacklist:remove']" type="danger" plain :disabled="multiple" icon="Delete" @click="handleDelete()">删除</el-button>
+          </el-col>
+          <right-toolbar v-model:show-search="showSearch" :columns="columns" :search="true" @query-table="getList"></right-toolbar>
+        </el-row>
+      </template>
+
+      <el-table v-loading="loading" border :data="blacklistList" @selection-change="handleSelectionChange">
+        <el-table-column type="selection" width="50" align="center" />
+        <el-table-column v-if="columns[0].visible" key="blackId" label="编号" align="center" prop="blackId" />
+        <el-table-column v-if="columns[1].visible" key="blackType" label="类型" align="center" prop="blackType">
+          <template #default="scope">
+            <dict-tag :options="risk_list_type" :value="scope.row.blackType" />
+          </template>
+        </el-table-column>
+        <el-table-column v-if="columns[2].visible" key="blackValue" label="黑名单值" align="center" prop="blackValue" :show-overflow-tooltip="true" />
+        <el-table-column v-if="columns[3].visible" key="blackName" label="名称" align="center" prop="blackName" :show-overflow-tooltip="true" />
+        <el-table-column v-if="columns[4].visible" key="reason" label="原因" align="center" prop="reason" :show-overflow-tooltip="true" />
+        <el-table-column v-if="columns[5].visible" key="source" label="来源" align="center" prop="source" />
+        <el-table-column v-if="columns[6].visible" key="expireTime" label="过期时间" align="center" prop="expireTime" width="160" />
+        <el-table-column v-if="columns[7].visible" key="status" label="状态" align="center" prop="status">
+          <template #default="scope">
+            <el-switch v-model="scope.row.status" active-value="0" inactive-value="1" @change="handleStatusChange(scope.row)"></el-switch>
+          </template>
+        </el-table-column>
+        <el-table-column v-if="columns[8].visible" label="创建时间" align="center" prop="createTime" width="160" />
+        <el-table-column label="操作" fixed="right" width="180" class-name="small-padding fixed-width">
+          <template #default="scope">
+            <el-tooltip content="修改" placement="top">
+              <el-button v-hasPermi="['riskctrl:blacklist:edit']" link type="primary" icon="Edit" @click="handleUpdate(scope.row)"></el-button>
+            </el-tooltip>
+            <el-tooltip content="删除" placement="top">
+              <el-button v-hasPermi="['riskctrl:blacklist:remove']" link type="primary" icon="Delete" @click="handleDelete(scope.row)"></el-button>
+            </el-tooltip>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <pagination v-show="total > 0" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" :total="total" @pagination="getList" />
+    </el-card>
+
+    <!-- 添加或修改黑名单对话框 -->
+    <el-dialog v-model="open" :title="title" width="600px" append-to-body>
+      <el-form ref="blacklistFormRef" :model="form" :rules="rules" label-width="100px">
+        <el-form-item label="黑名单类型" prop="blackType">
+          <el-select v-model="form.blackType" placeholder="请选择黑名单类型">
+            <el-option v-for="dict in risk_list_type" :key="dict.value" :label="dict.label" :value="dict.value" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="黑名单值" prop="blackValue">
+          <el-input v-model="form.blackValue" placeholder="请输入黑名单值" />
+        </el-form-item>
+        <el-form-item label="名称" prop="blackName">
+          <el-input v-model="form.blackName" placeholder="请输入名称" />
+        </el-form-item>
+        <el-form-item label="加入原因" prop="reason">
+          <el-input v-model="form.reason" type="textarea" placeholder="请输入加入原因" />
+        </el-form-item>
+        <el-form-item label="来源" prop="source">
+          <el-input v-model="form.source" placeholder="请输入来源" />
+        </el-form-item>
+        <el-form-item label="过期时间" prop="expireTime">
+          <el-date-picker v-model="form.expireTime" type="datetime" placeholder="选择过期时间" value-format="YYYY-MM-DD HH:mm:ss" />
+        </el-form-item>
+        <el-form-item label="状态" prop="status">
+          <el-radio-group v-model="form.status">
+            <el-radio v-for="dict in sys_normal_disable" :key="dict.value" :label="dict.value">{{ dict.label }}</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="备注" prop="remark">
+          <el-input v-model="form.remark" type="textarea" placeholder="请输入备注" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button type="primary" @click="submitForm">确 定</el-button>
+          <el-button @click="cancel">取 消</el-button>
+        </div>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup name="Blacklist" lang="ts">
+import { listBlacklist, getBlacklist, delBlacklist, addBlacklist, updateBlacklist, changeBlacklistStatus } from '@/api/riskctrl/blacklist';
+import { BlacklistForm, BlacklistQuery, BlacklistVO } from '@/api/riskctrl/blacklist/types';
+
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+const { risk_list_type, sys_normal_disable } = proxy.useDict('risk_list_type', 'sys_normal_disable');
+
+const blacklistList = ref<BlacklistVO[]>([]);
+const loading = ref(true);
+const showSearch = ref(true);
+const ids = ref<Array<string | number>>([]);
+const single = ref(true);
+const multiple = ref(true);
+const total = ref(0);
+const title = ref('');
+const open = ref(false);
+
+const queryFormRef = ref<ElFormInstance>();
+const blacklistFormRef = ref<ElFormInstance>();
+
+const queryParams = reactive<BlacklistQuery>({
+  pageNum: 1,
+  pageSize: 10,
+  blackType: undefined,
+  blackValue: undefined,
+  blackName: undefined,
+  status: undefined
+});
+
+const form = reactive<BlacklistForm>({
+  blackId: undefined,
+  blackType: '',
+  blackValue: '',
+  blackName: '',
+  reason: '',
+  source: '',
+  expireTime: '',
+  status: '0',
+  remark: ''
+});
+
+const rules = {
+  blackType: [{ required: true, message: '黑名单类型不能为空', trigger: 'change' }],
+  blackValue: [{ required: true, message: '黑名单值不能为空', trigger: 'blur' }]
+};
+
+const columns = ref([
+  { key: 0, label: `编号`, visible: true },
+  { key: 1, label: `类型`, visible: true },
+  { key: 2, label: `黑名单值`, visible: true },
+  { key: 3, label: `名称`, visible: true },
+  { key: 4, label: `原因`, visible: true },
+  { key: 5, label: `来源`, visible: true },
+  { key: 6, label: `过期时间`, visible: true },
+  { key: 7, label: `状态`, visible: true },
+  { key: 8, label: `创建时间`, visible: true }
+]);
+
+/** 查询黑名单列表 */
+const getList = async () => {
+  loading.value = true;
+  const res = await listBlacklist(queryParams);
+  blacklistList.value = res.rows;
+  total.value = res.total;
+  loading.value = false;
+};
+
+const cancel = () => {
+  open.value = false;
+  reset();
+};
+
+const reset = () => {
+  form.blackId = undefined;
+  form.blackType = '';
+  form.blackValue = '';
+  form.blackName = '';
+  form.reason = '';
+  form.source = '';
+  form.expireTime = '';
+  form.status = '0';
+  form.remark = '';
+  nextTick(() => {
+    blacklistFormRef.value?.resetFields();
+  });
+};
+
+const handleQuery = () => {
+  queryParams.pageNum = 1;
+  getList();
+};
+
+const resetQuery = () => {
+  queryFormRef.value?.resetFields();
+  handleQuery();
+};
+
+const handleSelectionChange = (selection: BlacklistVO[]) => {
+  ids.value = selection.map((item) => item.blackId);
+  single.value = selection.length !== 1;
+  multiple.value = !selection.length;
+};
+
+const handleAdd = () => {
+  reset();
+  open.value = true;
+  title.value = '添加黑名单';
+};
+
+const handleUpdate = async (row?: BlacklistVO) => {
+  reset();
+  const blackId = row?.blackId || ids.value[0];
+  const res = await getBlacklist(blackId);
+  Object.assign(form, res.data);
+  open.value = true;
+  title.value = '修改黑名单';
+};
+
+const submitForm = () => {
+  blacklistFormRef.value?.validate(async (valid: boolean) => {
+    if (valid) {
+      if (form.blackId !== undefined) {
+        await updateBlacklist(form);
+        proxy?.$modal.msgSuccess('修改成功');
+      } else {
+        await addBlacklist(form);
+        proxy?.$modal.msgSuccess('新增成功');
+      }
+      open.value = false;
+      await getList();
+    }
+  });
+};
+
+const handleDelete = async (row?: BlacklistVO) => {
+  const blackIds = row?.blackId || ids.value;
+  await proxy?.$modal.confirm('是否确认删除编号为"' + blackIds + '"的数据项?');
+  await delBlacklist(blackIds);
+  await getList();
+  proxy?.$modal.msgSuccess('删除成功');
+};
+
+const handleStatusChange = async (row: BlacklistVO) => {
+  const text = row.status === '0' ? '启用' : '停用';
+  try {
+    await proxy?.$modal.confirm('确认要"' + text + '""' + row.blackValue + '"黑名单吗?');
+    await changeBlacklistStatus(row.blackId, row.status);
+    proxy?.$modal.msgSuccess(text + '成功');
+  } catch {
+    row.status = row.status === '0' ? '1' : '0';
+  }
+};
+
+onMounted(() => {
+  getList();
+});
+</script>

+ 312 - 0
src/views/riskctrl/case/index.vue

@@ -0,0 +1,312 @@
+<template>
+  <div class="p-2">
+    <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
+      <div v-show="showSearch" class="mb-[10px]">
+        <el-card shadow="hover">
+          <el-form ref="queryFormRef" :model="queryParams" :inline="true">
+            <el-form-item label="案例编号" prop="caseNo">
+              <el-input v-model="queryParams.caseNo" placeholder="请输入案例编号" clearable @keyup.enter="handleQuery" />
+            </el-form-item>
+            <el-form-item label="案例名称" prop="caseName">
+              <el-input v-model="queryParams.caseName" placeholder="请输入案例名称" clearable @keyup.enter="handleQuery" />
+            </el-form-item>
+            <el-form-item label="案例类型" prop="caseType">
+              <el-select v-model="queryParams.caseType" placeholder="案例类型" clearable>
+                <el-option v-for="dict in risk_case_type" :key="dict.value" :label="dict.label" :value="dict.value" />
+              </el-select>
+            </el-form-item>
+            <el-form-item label="风险等级" prop="riskLevel">
+              <el-select v-model="queryParams.riskLevel" placeholder="风险等级" clearable>
+                <el-option v-for="dict in risk_level" :key="dict.value" :label="dict.label" :value="dict.value" />
+              </el-select>
+            </el-form-item>
+            <el-form-item label="状态" prop="status">
+              <el-select v-model="queryParams.status" placeholder="状态" clearable>
+                <el-option v-for="dict in risk_case_status" :key="dict.value" :label="dict.label" :value="dict.value" />
+              </el-select>
+            </el-form-item>
+            <el-form-item>
+              <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
+              <el-button icon="Refresh" @click="resetQuery">重置</el-button>
+            </el-form-item>
+          </el-form>
+        </el-card>
+      </div>
+    </transition>
+
+    <el-card shadow="hover">
+      <template #header>
+        <el-row :gutter="10">
+          <el-col :span="1.5">
+            <el-button v-has-permi="['riskctrl:case:add']" type="primary" plain icon="Plus" @click="handleAdd()">新增</el-button>
+          </el-col>
+          <el-col :span="1.5">
+            <el-button v-has-permi="['riskctrl:case:edit']" type="success" plain :disabled="single" icon="Edit" @click="handleUpdate()">修改</el-button>
+          </el-col>
+          <el-col :span="1.5">
+            <el-button v-has-permi="['riskctrl:case:remove']" type="danger" plain :disabled="multiple" icon="Delete" @click="handleDelete()">删除</el-button>
+          </el-col>
+          <right-toolbar v-model:show-search="showSearch" :columns="columns" :search="true" @query-table="getList"></right-toolbar>
+        </el-row>
+      </template>
+
+      <el-table v-loading="loading" border :data="caseList" @selection-change="handleSelectionChange">
+        <el-table-column type="selection" width="50" align="center" />
+        <el-table-column v-if="columns[0].visible" key="caseId" label="案例编号" align="center" prop="caseId" />
+        <el-table-column v-if="columns[1].visible" key="caseNo" label="案例编号" align="center" prop="caseNo" :show-overflow-tooltip="true" />
+        <el-table-column v-if="columns[2].visible" key="caseName" label="案例名称" align="center" prop="caseName" :show-overflow-tooltip="true" />
+        <el-table-column v-if="columns[3].visible" key="caseType" label="案例类型" align="center" prop="caseType">
+          <template #default="scope">
+            <dict-tag :options="risk_case_type" :value="scope.row.caseType" />
+          </template>
+        </el-table-column>
+        <el-table-column v-if="columns[4].visible" key="userName" label="用户名称" align="center" prop="userName" />
+        <el-table-column v-if="columns[5].visible" key="riskLevel" label="风险等级" align="center" prop="riskLevel">
+          <template #default="scope">
+            <dict-tag :options="risk_level" :value="scope.row.riskLevel" />
+          </template>
+        </el-table-column>
+        <el-table-column v-if="columns[6].visible" key="riskScore" label="风险分值" align="center" prop="riskScore" />
+        <el-table-column v-if="columns[7].visible" key="caseResult" label="案例结果" align="center" prop="caseResult" />
+        <el-table-column v-if="columns[8].visible" key="status" label="状态" align="center" prop="status">
+          <template #default="scope">
+            <dict-tag :options="risk_case_status" :value="scope.row.status" />
+          </template>
+        </el-table-column>
+        <el-table-column v-if="columns[9].visible" label="创建时间" align="center" prop="createTime" width="160" />
+        <el-table-column label="操作" fixed="right" width="180" class-name="small-padding fixed-width">
+          <template #default="scope">
+            <el-tooltip content="修改" placement="top">
+              <el-button v-hasPermi="['riskctrl:case:edit']" link type="primary" icon="Edit" @click="handleUpdate(scope.row)"></el-button>
+            </el-tooltip>
+            <el-tooltip content="删除" placement="top">
+              <el-button v-hasPermi="['riskctrl:case:remove']" link type="primary" icon="Delete" @click="handleDelete(scope.row)"></el-button>
+            </el-tooltip>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <pagination v-show="total > 0" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" :total="total" @pagination="getList" />
+    </el-card>
+
+    <!-- 添加或修改风控案例对话框 -->
+    <el-dialog v-model="open" :title="title" width="600px" append-to-body>
+      <el-form ref="caseFormRef" :model="form" :rules="rules" label-width="100px">
+        <el-form-item label="案例编号" prop="caseNo">
+          <el-input v-model="form.caseNo" placeholder="请输入案例编号" />
+        </el-form-item>
+        <el-form-item label="案例名称" prop="caseName">
+          <el-input v-model="form.caseName" placeholder="请输入案例名称" />
+        </el-form-item>
+        <el-form-item label="案例类型" prop="caseType">
+          <el-select v-model="form.caseType" placeholder="请选择案例类型">
+            <el-option v-for="dict in risk_case_type" :key="dict.value" :label="dict.label" :value="dict.value" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="案例描述" prop="caseDesc">
+          <el-input v-model="form.caseDesc" type="textarea" placeholder="请输入案例描述" />
+        </el-form-item>
+        <el-form-item label="关联事件ID" prop="eventId">
+          <el-input v-model="form.eventId" placeholder="请输入关联事件ID" />
+        </el-form-item>
+        <el-form-item label="关联事件编号" prop="eventNo">
+          <el-input v-model="form.eventNo" placeholder="请输入关联事件编号" />
+        </el-form-item>
+        <el-form-item label="用户ID" prop="userId">
+          <el-input v-model="form.userId" placeholder="请输入用户ID" />
+        </el-form-item>
+        <el-form-item label="用户名称" prop="userName">
+          <el-input v-model="form.userName" placeholder="请输入用户名称" />
+        </el-form-item>
+        <el-form-item label="风险等级" prop="riskLevel">
+          <el-select v-model="form.riskLevel" placeholder="请选择风险等级">
+            <el-option v-for="dict in risk_level" :key="dict.value" :label="dict.label" :value="dict.value" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="风险分值" prop="riskScore">
+          <el-input-number v-model="form.riskScore" :min="0" :max="100" :precision="2" />
+        </el-form-item>
+        <el-form-item label="命中规则" prop="hitRules">
+          <el-input v-model="form.hitRules" placeholder="请输入命中规则" />
+        </el-form-item>
+        <el-form-item label="案例结果" prop="caseResult">
+          <el-input v-model="form.caseResult" placeholder="请输入案例结果" />
+        </el-form-item>
+        <el-form-item label="状态" prop="status">
+          <el-radio-group v-model="form.status">
+            <el-radio v-for="dict in risk_case_status" :key="dict.value" :label="dict.value">{{ dict.label }}</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="备注" prop="remark">
+          <el-input v-model="form.remark" type="textarea" placeholder="请输入备注" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button type="primary" @click="submitForm">确 定</el-button>
+          <el-button @click="cancel">取 消</el-button>
+        </div>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup name="RiskCase" lang="ts">
+import { listCase, getCase, delCase, addCase, updateCase } from '@/api/riskctrl/case';
+import { CaseForm, CaseQuery, CaseVO } from '@/api/riskctrl/case/types';
+
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+const { risk_case_type, risk_level, risk_case_status } = proxy.useDict('risk_case_type', 'risk_level', 'risk_case_status');
+
+const caseList = ref<CaseVO[]>([]);
+const loading = ref(true);
+const showSearch = ref(true);
+const ids = ref<Array<string | number>>([]);
+const single = ref(true);
+const multiple = ref(true);
+const total = ref(0);
+const title = ref('');
+const open = ref(false);
+
+const queryFormRef = ref<ElFormInstance>();
+const caseFormRef = ref<ElFormInstance>();
+
+const queryParams = reactive<CaseQuery>({
+  pageNum: 1,
+  pageSize: 10,
+  caseNo: undefined,
+  caseName: undefined,
+  caseType: undefined,
+  riskLevel: undefined,
+  status: undefined
+});
+
+const form = reactive<CaseForm>({
+  caseId: undefined,
+  caseNo: '',
+  caseName: '',
+  caseType: '',
+  caseDesc: '',
+  eventId: undefined,
+  eventNo: '',
+  userId: undefined,
+  userName: '',
+  riskLevel: '',
+  riskScore: 0,
+  hitRules: '',
+  caseResult: '',
+  status: '0',
+  remark: ''
+});
+
+const rules = {
+  caseName: [{ required: true, message: '案例名称不能为空', trigger: 'blur' }],
+  caseType: [{ required: true, message: '案例类型不能为空', trigger: 'change' }]
+};
+
+const columns = ref([
+  { key: 0, label: `案例编号`, visible: true },
+  { key: 1, label: `案例编号`, visible: true },
+  { key: 2, label: `案例名称`, visible: true },
+  { key: 3, label: `案例类型`, visible: true },
+  { key: 4, label: `用户名称`, visible: true },
+  { key: 5, label: `风险等级`, visible: true },
+  { key: 6, label: `风险分值`, visible: true },
+  { key: 7, label: `案例结果`, visible: true },
+  { key: 8, label: `状态`, visible: true },
+  { key: 9, label: `创建时间`, visible: true }
+]);
+
+const getList = async () => {
+  loading.value = true;
+  const res = await listCase(queryParams);
+  caseList.value = res.rows;
+  total.value = res.total;
+  loading.value = false;
+};
+
+const cancel = () => {
+  open.value = false;
+  reset();
+};
+
+const reset = () => {
+  form.caseId = undefined;
+  form.caseNo = '';
+  form.caseName = '';
+  form.caseType = '';
+  form.caseDesc = '';
+  form.eventId = undefined;
+  form.eventNo = '';
+  form.userId = undefined;
+  form.userName = '';
+  form.riskLevel = '';
+  form.riskScore = 0;
+  form.hitRules = '';
+  form.caseResult = '';
+  form.status = '0';
+  form.remark = '';
+  nextTick(() => {
+    caseFormRef.value?.resetFields();
+  });
+};
+
+const handleQuery = () => {
+  queryParams.pageNum = 1;
+  getList();
+};
+
+const resetQuery = () => {
+  queryFormRef.value?.resetFields();
+  handleQuery();
+};
+
+const handleSelectionChange = (selection: CaseVO[]) => {
+  ids.value = selection.map((item) => item.caseId);
+  single.value = selection.length !== 1;
+  multiple.value = !selection.length;
+};
+
+const handleAdd = () => {
+  reset();
+  open.value = true;
+  title.value = '添加风控案例';
+};
+
+const handleUpdate = async (row?: CaseVO) => {
+  reset();
+  const caseId = row?.caseId || ids.value[0];
+  const res = await getCase(caseId);
+  Object.assign(form, res.data);
+  open.value = true;
+  title.value = '修改风控案例';
+};
+
+const submitForm = () => {
+  caseFormRef.value?.validate(async (valid: boolean) => {
+    if (valid) {
+      if (form.caseId !== undefined) {
+        await updateCase(form);
+        proxy?.$modal.msgSuccess('修改成功');
+      } else {
+        await addCase(form);
+        proxy?.$modal.msgSuccess('新增成功');
+      }
+      open.value = false;
+      await getList();
+    }
+  });
+};
+
+const handleDelete = async (row?: CaseVO) => {
+  const caseIds = row?.caseId || ids.value;
+  await proxy?.$modal.confirm('是否确认删除编号为"' + caseIds + '"的数据项?');
+  await delCase(caseIds);
+  await getList();
+  proxy?.$modal.msgSuccess('删除成功');
+};
+
+onMounted(() => {
+  getList();
+});
+</script>

+ 412 - 0
src/views/riskctrl/dangerSource/index.vue

@@ -0,0 +1,412 @@
+<template>
+  <div class="p-2">
+    <!-- 顶部统计卡片 -->
+    <el-row :gutter="16" class="mb-[10px]">
+      <el-col :span="6">
+        <el-card shadow="hover">
+          <div class="flex items-center justify-between">
+            <div>
+              <p class="text-gray-500 text-sm">危险源总数</p>
+              <p class="text-2xl font-bold text-blue-500">{{ statsData.total }}</p>
+            </div>
+            <el-icon :size="40" color="#409EFF"><WarningFilled /></el-icon>
+          </div>
+        </el-card>
+      </el-col>
+      <el-col :span="6">
+        <el-card shadow="hover">
+          <div class="flex items-center justify-between">
+            <div>
+              <p class="text-gray-500 text-sm">本月新增</p>
+              <p class="text-2xl font-bold text-green-500">{{ statsData.monthNew }}</p>
+            </div>
+            <el-icon :size="40" color="#67C23A"><CirclePlusFilled /></el-icon>
+          </div>
+        </el-card>
+      </el-col>
+      <el-col :span="6">
+        <el-card shadow="hover">
+          <div class="flex items-center justify-between">
+            <div>
+              <p class="text-gray-500 text-sm">高风险数</p>
+              <p class="text-2xl font-bold text-red-500">{{ statsData.highRisk }}</p>
+            </div>
+            <el-icon :size="40" color="#F56C6C"><TriangleFilled /></el-icon>
+          </div>
+        </el-card>
+      </el-col>
+      <el-col :span="6">
+        <el-card shadow="hover">
+          <div class="flex items-center justify-between">
+            <div>
+              <p class="text-gray-500 text-sm">已管控</p>
+              <p class="text-2xl font-bold text-purple-500">{{ statsData.controlled }}</p>
+            </div>
+            <el-icon :size="40" color="#8B5CF6"><CircleCheckFilled /></el-icon>
+          </div>
+        </el-card>
+      </el-col>
+    </el-row>
+
+    <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
+      <div v-show="showSearch" class="mb-[10px]">
+        <el-card shadow="hover">
+          <el-form ref="queryFormRef" :model="queryParams" :inline="true">
+            <el-form-item label="危险源名称" prop="sourceName">
+              <el-input v-model="queryParams.sourceName" placeholder="搜索危险源名称" clearable @keyup.enter="handleQuery" />
+            </el-form-item>
+            <el-form-item label="类型" prop="sourceType">
+              <el-select v-model="queryParams.sourceType" placeholder="全部类型" clearable>
+                <el-option label="电气危险源" value="电气危险源" />
+                <el-option label="消防危险源" value="消防危险源" />
+                <el-option label="高空危险源" value="高空危险源" />
+                <el-option label="设备危险源" value="设备危险源" />
+                <el-option label="食品危险源" value="食品危险源" />
+              </el-select>
+            </el-form-item>
+            <el-form-item>
+              <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
+              <el-button icon="Refresh" @click="resetQuery">重置</el-button>
+            </el-form-item>
+          </el-form>
+        </el-card>
+      </div>
+    </transition>
+
+    <el-card shadow="hover">
+      <template #header>
+        <el-row :gutter="10">
+          <el-col :span="1.5">
+            <el-button v-has-permi="['riskctrl:dangerSource:add']" type="primary" plain icon="Plus" @click="handleAdd()">新增</el-button>
+          </el-col>
+          <el-col :span="1.5">
+            <el-button v-has-permi="['riskctrl:dangerSource:edit']" type="success" plain :disabled="single" icon="Edit" @click="handleUpdate()">修改</el-button>
+          </el-col>
+          <el-col :span="1.5">
+            <el-button v-has-permi="['riskctrl:dangerSource:remove']" type="danger" plain :disabled="multiple" icon="Delete" @click="handleDelete()">删除</el-button>
+          </el-col>
+          <right-toolbar v-model:show-search="showSearch" :columns="columns" :search="true" @query-table="getList"></right-toolbar>
+        </el-row>
+      </template>
+
+      <el-table v-loading="loading" border :data="sourceList" @selection-change="handleSelectionChange">
+        <el-table-column type="selection" width="50" align="center" />
+        <el-table-column v-if="columns[0].visible" key="sourceNo" label="编号" align="center" prop="sourceNo" />
+        <el-table-column v-if="columns[1].visible" key="sourceName" label="危险源名称" align="center" prop="sourceName" :show-overflow-tooltip="true" />
+        <el-table-column v-if="columns[2].visible" key="sourceType" label="类型" align="center" prop="sourceType" />
+        <el-table-column v-if="columns[3].visible" key="location" label="位置" align="center" prop="location" :show-overflow-tooltip="true" />
+        <el-table-column v-if="columns[4].visible" key="possibleHarm" label="可能危害" align="center" prop="possibleHarm" :show-overflow-tooltip="true" />
+        <el-table-column v-if="columns[5].visible" key="preventionMethod" label="防控方式" align="center" prop="preventionMethod" :show-overflow-tooltip="true" />
+        <el-table-column v-if="columns[6].visible" key="reportTime" label="上报时间" align="center" prop="reportTime" width="160" />
+        <el-table-column v-if="columns[7].visible" key="status" label="状态" align="center" prop="status">
+          <template #default="scope">
+            <el-tag :type="scope.row.status === '0' ? 'success' : 'danger'">
+              {{ scope.row.status === '0' ? '启用' : '停用' }}
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column v-if="columns[8].visible" label="创建时间" align="center" prop="createTime" width="160" />
+        <el-table-column label="操作" fixed="right" width="180" class-name="small-padding fixed-width">
+          <template #default="scope">
+            <el-tooltip content="修改" placement="top">
+              <el-button v-has-permi="['riskctrl:dangerSource:edit']" link type="primary" icon="Edit" @click="handleUpdate(scope.row)"></el-button>
+            </el-tooltip>
+            <el-tooltip content="删除" placement="top">
+              <el-button v-has-permi="['riskctrl:dangerSource:remove']" link type="primary" icon="Delete" @click="handleDelete(scope.row)"></el-button>
+            </el-tooltip>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <pagination v-show="total > 0" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" :total="total" @pagination="getList" />
+    </el-card>
+
+    <!-- 新增/修改弹窗 -->
+    <el-dialog v-model="open" :title="title" width="750px" append-to-body>
+      <el-form ref="sourceFormRef" :model="form" :rules="rules" label-width="110px">
+        <el-form-item label="危险源编号" prop="sourceNo">
+          <el-input v-model="form.sourceNo" placeholder="自动生成" disabled />
+        </el-form-item>
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="危险源名称" prop="sourceName">
+              <el-input v-model="form.sourceName" placeholder="请输入危险源名称" maxlength="50" show-word-limit />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="类型" prop="sourceType">
+              <el-select v-model="form.sourceType" placeholder="请选择危险源类型" class="w-full">
+                <el-option label="电气危险源" value="电气危险源" />
+                <el-option label="消防危险源" value="消防危险源" />
+                <el-option label="高空危险源" value="高空危险源" />
+                <el-option label="设备危险源" value="设备危险源" />
+                <el-option label="食品危险源" value="食品危险源" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-form-item label="位置" prop="location">
+          <el-input v-model="form.location" placeholder="请输入危险源位置(如:XX车间XX工位)" maxlength="100" show-word-limit />
+        </el-form-item>
+        <el-form-item label="可能危害" prop="possibleHarm">
+          <el-input v-model="form.possibleHarm" type="textarea" :rows="3" placeholder="请描述可能造成的危害" maxlength="500" show-word-limit />
+        </el-form-item>
+        <el-form-item label="防控方式" prop="preventionMethod">
+          <el-input v-model="form.preventionMethod" type="textarea" :rows="3" placeholder="请描述防控方式" maxlength="500" show-word-limit />
+        </el-form-item>
+        <el-form-item label="上报时间" prop="reportTime">
+          <el-date-picker v-model="form.reportTime" type="date" placeholder="选择日期" value-format="YYYY-MM-DD" class="w-full" :disabled-date="disabledDate" />
+        </el-form-item>
+        <el-form-item label="危险源照片">
+          <el-upload
+            v-model:file-list="uploadFileList"
+            action="#"
+            list-type="picture-card"
+            :auto-upload="false"
+            :limit="5"
+            :on-exceed="handleExceed"
+            accept=".jpg,.jpeg,.png"
+          >
+            <el-icon><Plus /></el-icon>
+            <template #file="{ file }">
+              <img class="el-upload-list__item-thumbnail" :src="file.url" alt="" />
+              <span class="el-upload-actions">
+                <span class="el-upload-actions__preview" @click="handlePictureCardPreview(file)">
+                  <el-icon><ZoomIn /></el-icon>
+                </span>
+                <span class="el-upload-actions__delete" @click="handleRemoveFile(file)">
+                  <el-icon><Delete /></el-icon>
+                </span>
+              </span>
+            </template>
+          </el-upload>
+        </el-form-item>
+        <el-form-item label="备注" prop="remark">
+          <el-input v-model="form.remark" type="textarea" placeholder="请输入备注" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button type="primary" @click="submitForm">确 定</el-button>
+          <el-button @click="cancel">取 消</el-button>
+        </div>
+      </template>
+    </el-dialog>
+
+    <!-- 图片预览 -->
+    <el-dialog v-model="dialogVisible" width="500px" append-to-body>
+      <img :src="previewUrl" style="width: 100%" />
+    </el-dialog>
+  </div>
+</template>
+
+<script setup name="DangerSource" lang="ts">
+import { ref, reactive, onMounted, nextTick, getCurrentInstance } from 'vue';
+import type { ComponentInternalInstance } from 'vue';
+import type { ElFormInstance, UploadFile, UploadFiles, UploadRawFile } from 'element-plus';
+import { genFileId } from 'element-plus';
+import { listDangerSource, getDangerSource, delDangerSource, addDangerSource, updateDangerSource } from '@/api/riskctrl/dangerSource';
+import { DangerSourceForm, DangerSourceQuery, DangerSourceVO } from '@/api/riskctrl/dangerSource/types';
+
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+
+const sourceList = ref<DangerSourceVO[]>([]);
+const loading = ref(true);
+const showSearch = ref(true);
+const ids = ref<Array<string | number>>([]);
+const single = ref(true);
+const multiple = ref(true);
+const total = ref(0);
+const title = ref('');
+const open = ref(false);
+
+// 图片相关
+const uploadFileList = ref<UploadFile[]>([]);
+const dialogVisible = ref(false);
+const previewUrl = ref('');
+
+// 统计数据
+const statsData = reactive({
+  total: 0,
+  monthNew: 0,
+  highRisk: 0,
+  controlled: 0
+});
+
+const queryFormRef = ref<ElFormInstance>();
+const sourceFormRef = ref<ElFormInstance>();
+
+const queryParams = reactive<DangerSourceQuery>({
+  pageNum: 1,
+  pageSize: 10,
+  sourceNo: undefined,
+  sourceName: undefined,
+  sourceType: undefined,
+  status: undefined
+});
+
+const form = reactive<DangerSourceForm>({
+  sourceId: undefined,
+  sourceNo: '自动生成',
+  sourceName: '',
+  sourceType: '',
+  location: '',
+  possibleHarm: '',
+  preventionMethod: '',
+  reportTime: '',
+  status: '0',
+  remark: ''
+});
+
+const rules = {
+  sourceName: [{ required: true, message: '请填写危险源名称', trigger: 'blur' }],
+  sourceType: [{ required: true, message: '请选择危险源类型', trigger: 'change' }],
+  location: [{ required: true, message: '请填写危险源位置', trigger: 'blur' }],
+  possibleHarm: [{ required: true, message: '请描述可能危害', trigger: 'blur' }],
+  preventionMethod: [{ required: true, message: '请描述防控方式', trigger: 'blur' }],
+  reportTime: [{ required: true, message: '请选择上报日期', trigger: 'change' }]
+};
+
+const columns = ref([
+  { key: 0, label: '编号', visible: true },
+  { key: 1, label: '危险源名称', visible: true },
+  { key: 2, label: '类型', visible: true },
+  { key: 3, label: '位置', visible: true },
+  { key: 4, label: '可能危害', visible: true },
+  { key: 5, label: '防控方式', visible: true },
+  { key: 6, label: '上报时间', visible: true },
+  { key: 7, label: '状态', visible: true },
+  { key: 8, label: '创建时间', visible: true }
+]);
+
+// 禁用未来日期
+const disabledDate = (time: Date) => {
+  return time.getTime() > Date.now();
+};
+
+// 图片预览
+const handlePictureCardPreview = (file: UploadFile) => {
+  previewUrl.value = file.url as string;
+  dialogVisible.value = true;
+};
+
+// 删除文件
+const handleRemoveFile = (file: UploadFile) => {
+  const index = uploadFileList.value.findIndex(f => f.uid === file.uid);
+  if (index > -1) {
+    uploadFileList.value.splice(index, 1);
+  }
+};
+
+// 超出文件数限制
+const handleExceed = (files: File[], uploadFiles: UploadFiles) => {
+  proxy?.$modal.msgWarning('最多只能上传5张照片');
+};
+
+// 加载统计数据
+const loadStats = async () => {
+  try {
+    const res = await listDangerSource({ pageNum: 1, pageSize: 9999 } as any);
+    const allData = (res as any).rows || [];
+    statsData.total = allData.length;
+    // 本月新增
+    const now = new Date();
+    const monthStart = new Date(now.getFullYear(), now.getMonth(), 1).toISOString().split('T')[0];
+    statsData.monthNew = allData.filter((d: any) => d.createTime && d.createTime >= monthStart).length;
+    statsData.highRisk = allData.filter((d: any) => d.riskLevel === '高风险').length;
+    statsData.controlled = allData.filter((d: any) => d.status === '0').length;
+  } catch (e) {
+    console.error('加载统计数据失败', e);
+  }
+};
+
+const getList = async () => {
+  loading.value = true;
+  const res = await listDangerSource(queryParams);
+  sourceList.value = res.rows;
+  total.value = res.total;
+  loading.value = false;
+};
+
+const cancel = () => {
+  open.value = false;
+  reset();
+};
+
+const reset = () => {
+  form.sourceId = undefined;
+  form.sourceNo = '自动生成';
+  form.sourceName = '';
+  form.sourceType = '';
+  form.location = '';
+  form.possibleHarm = '';
+  form.preventionMethod = '';
+  form.reportTime = '';
+  form.status = '0';
+  form.remark = '';
+  uploadFileList.value = [];
+  nextTick(() => {
+    sourceFormRef.value?.resetFields();
+  });
+};
+
+const handleQuery = () => {
+  queryParams.pageNum = 1;
+  getList();
+};
+
+const resetQuery = () => {
+  queryFormRef.value?.resetFields();
+  handleQuery();
+};
+
+const handleSelectionChange = (selection: DangerSourceVO[]) => {
+  ids.value = selection.map((item) => item.sourceId);
+  single.value = selection.length !== 1;
+  multiple.value = !selection.length;
+};
+
+const handleAdd = () => {
+  reset();
+  open.value = true;
+  title.value = '新增危险源';
+};
+
+const handleUpdate = async (row?: DangerSourceVO) => {
+  reset();
+  const sourceId = row?.sourceId || ids.value[0];
+  const res = await getDangerSource(sourceId);
+  Object.assign(form, res.data);
+  open.value = true;
+  title.value = '修改危险源';
+};
+
+const submitForm = () => {
+  sourceFormRef.value?.validate(async (valid: boolean) => {
+    if (valid) {
+      if (form.sourceId !== undefined) {
+        await updateDangerSource(form);
+        proxy?.$modal.msgSuccess('修改成功');
+      } else {
+        await addDangerSource(form);
+        proxy?.$modal.msgSuccess('新增成功');
+      }
+      open.value = false;
+      await getList();
+    }
+  });
+};
+
+const handleDelete = async (row?: DangerSourceVO) => {
+  const sourceIds = row?.sourceId || ids.value;
+  await proxy?.$modal.confirm('是否确认删除编号为"' + sourceIds + '"的数据项?');
+  await delDangerSource(sourceIds);
+  await getList();
+  proxy?.$modal.msgSuccess('删除成功');
+};
+
+onMounted(() => {
+  getList();
+  loadStats();
+});
+</script>

+ 321 - 0
src/views/riskctrl/event/index.vue

@@ -0,0 +1,321 @@
+<template>
+  <div class="p-2">
+    <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
+      <div v-show="showSearch" class="mb-[10px]">
+        <el-card shadow="hover">
+          <el-form ref="queryFormRef" :model="queryParams" :inline="true">
+            <el-form-item label="事件编号" prop="eventNo">
+              <el-input v-model="queryParams.eventNo" placeholder="请输入事件编号" clearable @keyup.enter="handleQuery" />
+            </el-form-item>
+            <el-form-item label="事件类型" prop="eventType">
+              <el-select v-model="queryParams.eventType" placeholder="事件类型" clearable>
+                <el-option v-for="dict in risk_event_type" :key="dict.value" :label="dict.label" :value="dict.value" />
+              </el-select>
+            </el-form-item>
+            <el-form-item label="风险等级" prop="riskLevel">
+              <el-select v-model="queryParams.riskLevel" placeholder="风险等级" clearable>
+                <el-option v-for="dict in risk_level" :key="dict.value" :label="dict.label" :value="dict.value" />
+              </el-select>
+            </el-form-item>
+            <el-form-item label="状态" prop="status">
+              <el-select v-model="queryParams.status" placeholder="状态" clearable>
+                <el-option v-for="dict in risk_event_status" :key="dict.value" :label="dict.label" :value="dict.value" />
+              </el-select>
+            </el-form-item>
+            <el-form-item label="用户名称" prop="userName">
+              <el-input v-model="queryParams.userName" placeholder="请输入用户名称" clearable @keyup.enter="handleQuery" />
+            </el-form-item>
+            <el-form-item label="IP地址" prop="ipAddress">
+              <el-input v-model="queryParams.ipAddress" placeholder="请输入IP地址" clearable @keyup.enter="handleQuery" />
+            </el-form-item>
+            <el-form-item>
+              <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
+              <el-button icon="Refresh" @click="resetQuery">重置</el-button>
+            </el-form-item>
+          </el-form>
+        </el-card>
+      </div>
+    </transition>
+
+    <el-card shadow="hover">
+      <template #header>
+        <el-row :gutter="10">
+          <el-col :span="1.5">
+            <el-button v-has-permi="['riskctrl:event:add']" type="primary" plain icon="Plus" @click="handleAdd()">新增</el-button>
+          </el-col>
+          <el-col :span="1.5">
+            <el-button v-has-permi="['riskctrl:event:edit']" type="success" plain :disabled="single" icon="Edit" @click="handleUpdate()">修改</el-button>
+          </el-col>
+          <el-col :span="1.5">
+            <el-button v-has-permi="['riskctrl:event:remove']" type="danger" plain :disabled="multiple" icon="Delete" @click="handleDelete()">删除</el-button>
+          </el-col>
+          <right-toolbar v-model:show-search="showSearch" :columns="columns" :search="true" @query-table="getList"></right-toolbar>
+        </el-row>
+      </template>
+
+      <el-table v-loading="loading" border :data="eventList" @selection-change="handleSelectionChange">
+        <el-table-column type="selection" width="50" align="center" />
+        <el-table-column v-if="columns[0].visible" key="eventId" label="事件编号" align="center" prop="eventId" />
+        <el-table-column v-if="columns[1].visible" key="eventNo" label="事件编号" align="center" prop="eventNo" :show-overflow-tooltip="true" />
+        <el-table-column v-if="columns[2].visible" key="eventType" label="事件类型" align="center" prop="eventType">
+          <template #default="scope">
+            <dict-tag :options="risk_event_type" :value="scope.row.eventType" />
+          </template>
+        </el-table-column>
+        <el-table-column v-if="columns[3].visible" key="userName" label="用户名称" align="center" prop="userName" />
+        <el-table-column v-if="columns[4].visible" key="ipAddress" label="IP地址" align="center" prop="ipAddress" />
+        <el-table-column v-if="columns[5].visible" key="riskLevel" label="风险等级" align="center" prop="riskLevel">
+          <template #default="scope">
+            <dict-tag :options="risk_level" :value="scope.row.riskLevel" />
+          </template>
+        </el-table-column>
+        <el-table-column v-if="columns[6].visible" key="riskScore" label="风险分值" align="center" prop="riskScore" />
+        <el-table-column v-if="columns[7].visible" key="actionTaken" label="已执行动作" align="center" prop="actionTaken" />
+        <el-table-column v-if="columns[8].visible" key="status" label="状态" align="center" prop="status">
+          <template #default="scope">
+            <dict-tag :options="risk_event_status" :value="scope.row.status" />
+          </template>
+        </el-table-column>
+        <el-table-column v-if="columns[9].visible" label="事件时间" align="center" prop="eventTime" width="160" />
+        <el-table-column label="操作" fixed="right" width="180" class-name="small-padding fixed-width">
+          <template #default="scope">
+            <el-tooltip content="修改" placement="top">
+              <el-button v-hasPermi="['riskctrl:event:edit']" link type="primary" icon="Edit" @click="handleUpdate(scope.row)"></el-button>
+            </el-tooltip>
+            <el-tooltip content="删除" placement="top">
+              <el-button v-hasPermi="['riskctrl:event:remove']" link type="primary" icon="Delete" @click="handleDelete(scope.row)"></el-button>
+            </el-tooltip>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <pagination v-show="total > 0" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" :total="total" @pagination="getList" />
+    </el-card>
+
+    <!-- 添加或修改风控事件对话框 -->
+    <el-dialog v-model="open" :title="title" width="600px" append-to-body>
+      <el-form ref="eventFormRef" :model="form" :rules="rules" label-width="100px">
+        <el-form-item label="事件编号" prop="eventNo">
+          <el-input v-model="form.eventNo" placeholder="请输入事件编号" />
+        </el-form-item>
+        <el-form-item label="事件类型" prop="eventType">
+          <el-select v-model="form.eventType" placeholder="请选择事件类型">
+            <el-option v-for="dict in risk_event_type" :key="dict.value" :label="dict.label" :value="dict.value" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="事件来源" prop="eventSource">
+          <el-input v-model="form.eventSource" placeholder="请输入事件来源" />
+        </el-form-item>
+        <el-form-item label="用户ID" prop="userId">
+          <el-input v-model="form.userId" placeholder="请输入用户ID" />
+        </el-form-item>
+        <el-form-item label="用户名称" prop="userName">
+          <el-input v-model="form.userName" placeholder="请输入用户名称" />
+        </el-form-item>
+        <el-form-item label="设备ID" prop="deviceId">
+          <el-input v-model="form.deviceId" placeholder="请输入设备ID" />
+        </el-form-item>
+        <el-form-item label="IP地址" prop="ipAddress">
+          <el-input v-model="form.ipAddress" placeholder="请输入IP地址" />
+        </el-form-item>
+        <el-form-item label="地理位置" prop="location">
+          <el-input v-model="form.location" placeholder="请输入地理位置" />
+        </el-form-item>
+        <el-form-item label="风险等级" prop="riskLevel">
+          <el-select v-model="form.riskLevel" placeholder="请选择风险等级">
+            <el-option v-for="dict in risk_level" :key="dict.value" :label="dict.label" :value="dict.value" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="风险分值" prop="riskScore">
+          <el-input-number v-model="form.riskScore" :min="0" :max="100" :precision="2" />
+        </el-form-item>
+        <el-form-item label="命中规则" prop="hitRules">
+          <el-input v-model="form.hitRules" placeholder="请输入命中规则" />
+        </el-form-item>
+        <el-form-item label="已执行动作" prop="actionTaken">
+          <el-input v-model="form.actionTaken" placeholder="请输入已执行动作" />
+        </el-form-item>
+        <el-form-item label="状态" prop="status">
+          <el-radio-group v-model="form.status">
+            <el-radio v-for="dict in risk_event_status" :key="dict.value" :label="dict.value">{{ dict.label }}</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="事件数据" prop="eventData">
+          <el-input v-model="form.eventData" type="textarea" placeholder="请输入事件数据" />
+        </el-form-item>
+        <el-form-item label="备注" prop="remark">
+          <el-input v-model="form.remark" type="textarea" placeholder="请输入备注" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button type="primary" @click="submitForm">确 定</el-button>
+          <el-button @click="cancel">取 消</el-button>
+        </div>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup name="RiskEvent" lang="ts">
+import { listEvent, getEvent, delEvent, addEvent, updateEvent } from '@/api/riskctrl/event';
+import { EventForm, EventQuery, EventVO } from '@/api/riskctrl/event/types';
+
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+const { risk_event_type, risk_level, risk_event_status } = proxy.useDict('risk_event_type', 'risk_level', 'risk_event_status');
+
+const eventList = ref<EventVO[]>([]);
+const loading = ref(true);
+const showSearch = ref(true);
+const ids = ref<Array<string | number>>([]);
+const single = ref(true);
+const multiple = ref(true);
+const total = ref(0);
+const title = ref('');
+const open = ref(false);
+
+const queryFormRef = ref<ElFormInstance>();
+const eventFormRef = ref<ElFormInstance>();
+
+const queryParams = reactive<EventQuery>({
+  pageNum: 1,
+  pageSize: 10,
+  eventNo: undefined,
+  eventType: undefined,
+  riskLevel: undefined,
+  status: undefined,
+  userName: undefined,
+  ipAddress: undefined
+});
+
+const form = reactive<EventForm>({
+  eventId: undefined,
+  eventNo: '',
+  eventType: '',
+  eventSource: '',
+  userId: undefined,
+  userName: '',
+  deviceId: '',
+  ipAddress: '',
+  location: '',
+  eventData: '',
+  riskLevel: '',
+  riskScore: 0,
+  hitRules: '',
+  actionTaken: '',
+  status: '0',
+  remark: ''
+});
+
+const rules = {
+  eventType: [{ required: true, message: '事件类型不能为空', trigger: 'change' }],
+  status: [{ required: true, message: '状态不能为空', trigger: 'change' }]
+};
+
+const columns = ref([
+  { key: 0, label: `事件编号`, visible: true },
+  { key: 1, label: `事件编号`, visible: true },
+  { key: 2, label: `事件类型`, visible: true },
+  { key: 3, label: `用户名称`, visible: true },
+  { key: 4, label: `IP地址`, visible: true },
+  { key: 5, label: `风险等级`, visible: true },
+  { key: 6, label: `风险分值`, visible: true },
+  { key: 7, label: `已执行动作`, visible: true },
+  { key: 8, label: `状态`, visible: true },
+  { key: 9, label: `事件时间`, visible: true }
+]);
+
+const getList = async () => {
+  loading.value = true;
+  const res = await listEvent(queryParams);
+  eventList.value = res.rows;
+  total.value = res.total;
+  loading.value = false;
+};
+
+const cancel = () => {
+  open.value = false;
+  reset();
+};
+
+const reset = () => {
+  form.eventId = undefined;
+  form.eventNo = '';
+  form.eventType = '';
+  form.eventSource = '';
+  form.userId = undefined;
+  form.userName = '';
+  form.deviceId = '';
+  form.ipAddress = '';
+  form.location = '';
+  form.eventData = '';
+  form.riskLevel = '';
+  form.riskScore = 0;
+  form.hitRules = '';
+  form.actionTaken = '';
+  form.status = '0';
+  form.remark = '';
+  nextTick(() => {
+    eventFormRef.value?.resetFields();
+  });
+};
+
+const handleQuery = () => {
+  queryParams.pageNum = 1;
+  getList();
+};
+
+const resetQuery = () => {
+  queryFormRef.value?.resetFields();
+  handleQuery();
+};
+
+const handleSelectionChange = (selection: EventVO[]) => {
+  ids.value = selection.map((item) => item.eventId);
+  single.value = selection.length !== 1;
+  multiple.value = !selection.length;
+};
+
+const handleAdd = () => {
+  reset();
+  open.value = true;
+  title.value = '添加风控事件';
+};
+
+const handleUpdate = async (row?: EventVO) => {
+  reset();
+  const eventId = row?.eventId || ids.value[0];
+  const res = await getEvent(eventId);
+  Object.assign(form, res.data);
+  open.value = true;
+  title.value = '修改风控事件';
+};
+
+const submitForm = () => {
+  eventFormRef.value?.validate(async (valid: boolean) => {
+    if (valid) {
+      if (form.eventId !== undefined) {
+        await updateEvent(form);
+        proxy?.$modal.msgSuccess('修改成功');
+      } else {
+        await addEvent(form);
+        proxy?.$modal.msgSuccess('新增成功');
+      }
+      open.value = false;
+      await getList();
+    }
+  });
+};
+
+const handleDelete = async (row?: EventVO) => {
+  const eventIds = row?.eventId || ids.value;
+  await proxy?.$modal.confirm('是否确认删除编号为"' + eventIds + '"的数据项?');
+  await delEvent(eventIds);
+  await getList();
+  proxy?.$modal.msgSuccess('删除成功');
+};
+
+onMounted(() => {
+  getList();
+});
+</script>

+ 452 - 0
src/views/riskctrl/hazard/index.vue

@@ -0,0 +1,452 @@
+<template>
+  <div class="p-2">
+    <!-- 顶部统计卡片 -->
+    <el-row :gutter="16" class="mb-[10px]">
+      <el-col :span="6">
+        <el-card shadow="hover">
+          <div class="flex items-center justify-between">
+            <div>
+              <p class="text-gray-500 text-sm">隐患总数</p>
+              <p class="text-2xl font-bold text-blue-500">{{ statsData.totalCount }}</p>
+            </div>
+            <el-icon :size="40" color="#409EFF"><WarningFilled /></el-icon>
+          </div>
+        </el-card>
+      </el-col>
+      <el-col :span="6">
+        <el-card shadow="hover">
+          <div class="flex items-center justify-between">
+            <div>
+              <p class="text-gray-500 text-sm">待处理</p>
+              <p class="text-2xl font-bold text-orange-500">{{ statsData.pendingCount }}</p>
+            </div>
+            <el-icon :size="40" color="#E6A23C"><Clock /></el-icon>
+          </div>
+        </el-card>
+      </el-col>
+      <el-col :span="6">
+        <el-card shadow="hover">
+          <div class="flex items-center justify-between">
+            <div>
+              <p class="text-gray-500 text-sm">处理中</p>
+              <p class="text-2xl font-bold text-yellow-500">{{ statsData.processingCount }}</p>
+            </div>
+            <el-icon :size="40" color="#F2C811"><Loading /></el-icon>
+          </div>
+        </el-card>
+      </el-col>
+      <el-col :span="6">
+        <el-card shadow="hover">
+          <div class="flex items-center justify-between">
+            <div>
+              <p class="text-gray-500 text-sm">已完成</p>
+              <p class="text-2xl font-bold text-green-500">{{ statsData.completedCount }}</p>
+            </div>
+            <el-icon :size="40" color="#67C23A"><CircleCheckFilled /></el-icon>
+          </div>
+        </el-card>
+      </el-col>
+    </el-row>
+
+    <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
+      <div v-show="showSearch" class="mb-[10px]">
+        <el-card shadow="hover">
+          <el-form ref="queryFormRef" :model="queryParams" :inline="true">
+            <el-form-item label="编号" prop="hazardNo">
+              <el-input v-model="queryParams.hazardNo" placeholder="请输入编号" clearable @keyup.enter="handleQuery" />
+            </el-form-item>
+            <el-form-item label="隐患名称" prop="hazardName">
+              <el-input v-model="queryParams.hazardName" placeholder="请输入名称" clearable @keyup.enter="handleQuery" />
+            </el-form-item>
+            <el-form-item label="隐患类型" prop="hazardType">
+              <el-select v-model="queryParams.hazardType" placeholder="类型" clearable>
+                <el-option label="门店安全隐患" value="门店安全隐患" />
+                <el-option label="物流安全隐患" value="物流安全隐患" />
+                <el-option label="市场安全隐患" value="市场安全隐患" />
+              </el-select>
+            </el-form-item>
+            <el-form-item label="风险等级" prop="riskLevel">
+              <el-select v-model="queryParams.riskLevel" placeholder="等级" clearable>
+                <el-option label="高风险" value="高风险" />
+                <el-option label="中风险" value="中风险" />
+                <el-option label="低风险" value="低风险" />
+              </el-select>
+            </el-form-item>
+            <el-form-item label="状态" prop="processStatus">
+              <el-select v-model="queryParams.processStatus" placeholder="状态" clearable>
+                <el-option label="待处理" value="待处理" />
+                <el-option label="处理中" value="处理中" />
+                <el-option label="已处理" value="已处理" />
+              </el-select>
+            </el-form-item>
+            <el-form-item>
+              <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
+              <el-button icon="Refresh" @click="resetQuery">重置</el-button>
+            </el-form-item>
+          </el-form>
+        </el-card>
+      </div>
+    </transition>
+
+    <el-card shadow="hover">
+      <template #header>
+        <el-row :gutter="10">
+          <el-col :span="1.5">
+            <el-button v-has-permi="['riskctrl:hazard:add']" type="primary" plain icon="Plus" @click="handleAdd()">新增</el-button>
+          </el-col>
+          <el-col :span="1.5">
+            <el-button v-has-permi="['riskctrl:hazard:edit']" type="success" plain :disabled="single" icon="Edit" @click="handleUpdate()">修改</el-button>
+          </el-col>
+          <el-col :span="1.5">
+            <el-button v-has-permi="['riskctrl:hazard:remove']" type="danger" plain :disabled="multiple" icon="Delete" @click="handleDelete()">删除</el-button>
+          </el-col>
+          <el-col :span="1.5">
+            <el-button v-has-permi="['riskctrl:hazard:process']" type="warning" plain :disabled="single" icon="Operation" @click="handleProcess()">处理</el-button>
+          </el-col>
+          <right-toolbar v-model:show-search="showSearch" :columns="columns" :search="true" @query-table="getList"></right-toolbar>
+        </el-row>
+      </template>
+
+      <el-table v-loading="loading" border :data="hazardList" @selection-change="handleSelectionChange">
+        <el-table-column type="selection" width="50" align="center" />
+        <el-table-column v-if="columns[0].visible" key="hazardNo" label="编号" align="center" prop="hazardNo" width="140" />
+        <el-table-column v-if="columns[1].visible" key="hazardName" label="隐患名称" align="center" prop="hazardName" :show-overflow-tooltip="true" />
+        <el-table-column v-if="columns[2].visible" key="hazardType" label="类型" align="center" prop="hazardType" width="100" />
+        <el-table-column v-if="columns[3].visible" key="riskLevel" label="风险等级" align="center" prop="riskLevel" width="100">
+          <template #default="scope">
+            <el-tag :type="getLevelType(scope.row.riskLevel)">{{ scope.row.riskLevel }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column v-if="columns[4].visible" key="hazardLocation" label="位置" align="center" prop="hazardLocation" :show-overflow-tooltip="true" />
+        <el-table-column v-if="columns[5].visible" key="discoverTime" label="发现时间" align="center" prop="discoverTime" width="160" />
+        <el-table-column v-if="columns[6].visible" key="processStatus" label="处理状态" align="center" prop="processStatus" width="100">
+          <template #default="scope">
+            <el-tag :type="getStatusType(scope.row.processStatus)">{{ scope.row.processStatus }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column v-if="columns[7].visible" key="processUser" label="处理人" align="center" prop="processUser" width="100" />
+        <el-table-column v-if="columns[8].visible" label="创建时间" align="center" prop="createTime" width="160" />
+        <el-table-column label="操作" fixed="right" width="220" class-name="small-padding fixed-width">
+          <template #default="scope">
+            <el-tooltip content="修改" placement="top">
+              <el-button v-has-permi="['riskctrl:hazard:edit']" link type="primary" icon="Edit" @click="handleUpdate(scope.row)"></el-button>
+            </el-tooltip>
+            <el-tooltip content="处理" placement="top">
+              <el-button v-has-permi="['riskctrl:hazard:process']" link type="warning" icon="Operation" @click="handleProcess(scope.row)"></el-button>
+            </el-tooltip>
+            <el-tooltip content="删除" placement="top">
+              <el-button v-has-permi="['riskctrl:hazard:remove']" link type="primary" icon="Delete" @click="handleDelete(scope.row)"></el-button>
+            </el-tooltip>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <pagination v-show="total > 0" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" :total="total" @pagination="getList" />
+    </el-card>
+
+    <!-- 新增/修改弹窗 -->
+    <el-dialog v-model="open" :title="title" width="750px" append-to-body>
+      <el-form ref="hazardFormRef" :model="form" :rules="rules" label-width="100px">
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="隐患名称" prop="hazardName">
+              <el-input v-model="form.hazardName" placeholder="请输入隐患名称" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="隐患类型" prop="hazardType">
+              <el-select v-model="form.hazardType" placeholder="请选择类型" class="w-full">
+                <el-option label="门店安全隐患" value="门店安全隐患" />
+                <el-option label="物流安全隐患" value="物流安全隐患" />
+                <el-option label="市场安全隐患" value="市场安全隐患" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="风险等级" prop="riskLevel">
+              <el-select v-model="form.riskLevel" placeholder="请选择风险等级" class="w-full">
+                <el-option label="高风险" value="高风险" />
+                <el-option label="中风险" value="中风险" />
+                <el-option label="低风险" value="低风险" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="发现时间" prop="discoverTime">
+              <el-date-picker v-model="form.discoverTime" type="datetime" placeholder="选择时间" value-format="YYYY-MM-DD HH:mm:ss" class="w-full" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-form-item label="位置" prop="hazardLocation">
+          <el-input v-model="form.hazardLocation" placeholder="请输入隐患位置" />
+        </el-form-item>
+        <el-form-item label="隐患描述" prop="hazardDesc">
+          <el-input v-model="form.hazardDesc" type="textarea" :rows="3" placeholder="请输入隐患描述" />
+        </el-form-item>
+        <el-form-item label="状态" prop="status">
+          <el-radio-group v-model="form.status">
+            <el-radio v-for="dict in sys_normal_disable" :key="dict.value" :label="dict.value">{{ dict.label }}</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="备注" prop="remark">
+          <el-input v-model="form.remark" type="textarea" placeholder="请输入备注" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button type="primary" @click="submitForm">确 定</el-button>
+          <el-button @click="cancel">取 消</el-button>
+        </div>
+      </template>
+    </el-dialog>
+
+    <!-- 处理弹窗 -->
+    <el-dialog v-model="processOpen" title="处理隐患" width="600px" append-to-body>
+      <el-form ref="processFormRef" :model="processForm" :rules="processRules" label-width="100px">
+        <el-form-item label="处理状态" prop="processStatus">
+          <el-select v-model="processForm.processStatus" placeholder="请选择" class="w-full">
+            <el-option label="处理中" value="处理中" />
+            <el-option label="已处理" value="已处理" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="处理结果" prop="processResult">
+          <el-input v-model="processForm.processResult" type="textarea" :rows="4" placeholder="请输入处理结果" />
+        </el-form-item>
+        <el-form-item label="处理人" prop="processUser">
+          <el-input v-model="processForm.processUser" placeholder="请输入处理人" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button type="primary" @click="submitProcess">确 定</el-button>
+          <el-button @click="processOpen = false">取 消</el-button>
+        </div>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup name="SafetyHazard" lang="ts">
+import { listHazard, getHazard, delHazard, addHazard, updateHazard, processHazard, getHazardStats } from '@/api/riskctrl/hazard';
+import { HazardForm, HazardQuery, HazardVO, HazardStats } from '@/api/riskctrl/hazard/types';
+
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+const { sys_normal_disable } = proxy.useDict('sys_normal_disable');
+
+const hazardList = ref<HazardVO[]>([]);
+const loading = ref(true);
+const showSearch = ref(true);
+const ids = ref<Array<string | number>>([]);
+const single = ref(true);
+const multiple = ref(true);
+const total = ref(0);
+const title = ref('');
+const open = ref(false);
+const processOpen = ref(false);
+
+const queryFormRef = ref<ElFormInstance>();
+const hazardFormRef = ref<ElFormInstance>();
+const processFormRef = ref<ElFormInstance>();
+
+const statsData = reactive<HazardStats>({
+  totalCount: 0,
+  pendingCount: 0,
+  processingCount: 0,
+  completedCount: 0
+});
+
+const queryParams = reactive<HazardQuery>({
+  pageNum: 1,
+  pageSize: 10,
+  hazardNo: undefined,
+  hazardName: undefined,
+  hazardType: undefined,
+  riskLevel: undefined,
+  processStatus: undefined
+});
+
+const form = reactive<HazardForm>({
+  hazardId: undefined,
+  hazardName: '',
+  hazardType: '',
+  riskLevel: '中风险',
+  discoverTime: '',
+  hazardLocation: '',
+  hazardDesc: '',
+  status: '0',
+  remark: ''
+});
+
+const processForm = reactive({
+  hazardId: undefined as string | number | undefined,
+  processStatus: '',
+  processResult: '',
+  processUser: '',
+  processTime: ''
+});
+
+const rules = {
+  hazardName: [{ required: true, message: '隐患名称不能为空', trigger: 'blur' }],
+  hazardType: [{ required: true, message: '类型不能为空', trigger: 'change' }],
+  riskLevel: [{ required: true, message: '风险等级不能为空', trigger: 'change' }]
+};
+
+const processRules = {
+  processStatus: [{ required: true, message: '处理状态不能为空', trigger: 'change' }],
+  processResult: [{ required: true, message: '处理结果不能为空', trigger: 'blur' }]
+};
+
+const columns = ref([
+  { key: 0, label: '编号', visible: true },
+  { key: 1, label: '隐患名称', visible: true },
+  { key: 2, label: '类型', visible: true },
+  { key: 3, label: '风险等级', visible: true },
+  { key: 4, label: '位置', visible: true },
+  { key: 5, label: '发现时间', visible: true },
+  { key: 6, label: '处理状态', visible: true },
+  { key: 7, label: '处理人', visible: true },
+  { key: 8, label: '创建时间', visible: true }
+]);
+
+type TagType = 'success' | 'warning' | 'danger' | 'info' | 'primary';
+
+const getLevelType = (level: string): TagType => {
+  const map: Record<string, TagType> = {
+    '高风险': 'danger',
+    '中风险': 'warning',
+    '低风险': 'info'
+  };
+  return map[level] || 'info';
+};
+
+const getStatusType = (status: string): TagType => {
+  const map: Record<string, TagType> = {
+    '待处理': 'danger',
+    '处理中': 'warning',
+    '已处理': 'success'
+  };
+  return map[status] || 'info';
+};
+
+const loadStats = async () => {
+  try {
+    const res = await getHazardStats();
+    Object.assign(statsData, res.data);
+  } catch { /* ignore */ }
+};
+
+const getList = async () => {
+  loading.value = true;
+  const res = await listHazard(queryParams);
+  hazardList.value = res.rows;
+  total.value = res.total;
+  loading.value = false;
+};
+
+const cancel = () => {
+  open.value = false;
+  reset();
+};
+
+const reset = () => {
+  form.hazardId = undefined;
+  form.hazardName = '';
+  form.hazardType = '';
+  form.riskLevel = '中风险';
+  form.discoverTime = '';
+  form.hazardLocation = '';
+  form.hazardDesc = '';
+  form.status = '0';
+  form.remark = '';
+  nextTick(() => {
+    hazardFormRef.value?.resetFields();
+  });
+};
+
+const handleQuery = () => {
+  queryParams.pageNum = 1;
+  getList();
+};
+
+const resetQuery = () => {
+  queryFormRef.value?.resetFields();
+  handleQuery();
+};
+
+const handleSelectionChange = (selection: HazardVO[]) => {
+  ids.value = selection.map((item) => item.hazardId);
+  single.value = selection.length !== 1;
+  multiple.value = !selection.length;
+};
+
+const handleAdd = () => {
+  reset();
+  open.value = true;
+  title.value = '新增安全隐患';
+};
+
+const handleUpdate = async (row?: HazardVO) => {
+  reset();
+  const hazardId = row?.hazardId || ids.value[0];
+  const res = await getHazard(hazardId);
+  Object.assign(form, res.data);
+  open.value = true;
+  title.value = '修改安全隐患';
+};
+
+const submitForm = () => {
+  hazardFormRef.value?.validate(async (valid: boolean) => {
+    if (valid) {
+      if (form.hazardId !== undefined) {
+        await updateHazard(form);
+        proxy?.$modal.msgSuccess('修改成功');
+      } else {
+        await addHazard(form);
+        proxy?.$modal.msgSuccess('新增成功');
+      }
+      open.value = false;
+      await getList();
+      await loadStats();
+    }
+  });
+};
+
+const handleProcess = async (row?: HazardVO) => {
+  const hazardId = row?.hazardId || ids.value[0];
+  const res = await getHazard(hazardId);
+  processForm.hazardId = res.data.hazardId;
+  processForm.processStatus = res.data.processStatus || '处理中';
+  processForm.processResult = res.data.processResult || '';
+  processForm.processUser = res.data.processUser || '';
+  processOpen.value = true;
+};
+
+const submitProcess = () => {
+  processFormRef.value?.validate(async (valid: boolean) => {
+    if (valid) {
+      processForm.processTime = new Date().toISOString();
+      await processHazard(processForm as any);
+      proxy?.$modal.msgSuccess('处理成功');
+      processOpen.value = false;
+      await getList();
+      await loadStats();
+    }
+  });
+};
+
+const handleDelete = async (row?: HazardVO) => {
+  const hazardIds = row?.hazardId || ids.value;
+  await proxy?.$modal.confirm('是否确认删除编号为"' + hazardIds + '"的数据项?');
+  await delHazard(hazardIds);
+  await getList();
+  await loadStats();
+  proxy?.$modal.msgSuccess('删除成功');
+};
+
+onMounted(() => {
+  loadStats();
+  getList();
+});
+</script>

+ 267 - 0
src/views/riskctrl/noticeAnnouncement/index.vue

@@ -0,0 +1,267 @@
+<template>
+  <div class="p-2">
+    <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
+      <div v-show="showSearch" class="mb-[10px]">
+        <el-card shadow="hover">
+          <el-form ref="queryFormRef" :model="queryParams" :inline="true">
+            <el-form-item label="公告标题" prop="noticeTitle">
+              <el-input v-model="queryParams.noticeTitle" placeholder="请输入标题" clearable @keyup.enter="handleQuery" />
+            </el-form-item>
+            <el-form-item label="公告类型" prop="noticeType">
+              <el-select v-model="queryParams.noticeType" placeholder="类型" clearable>
+                <el-option label="通知" value="通知" />
+                <el-option label="公告" value="公告" />
+                <el-option label="提醒" value="提醒" />
+              </el-select>
+            </el-form-item>
+            <el-form-item label="发布状态" prop="status">
+              <el-select v-model="queryParams.status" placeholder="状态" clearable>
+                <el-option label="已发布" value="已发布" />
+                <el-option label="草稿" value="草稿" />
+              </el-select>
+            </el-form-item>
+            <el-form-item>
+              <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
+              <el-button icon="Refresh" @click="resetQuery">重置</el-button>
+            </el-form-item>
+          </el-form>
+        </el-card>
+      </div>
+    </transition>
+
+    <el-card shadow="hover">
+      <template #header>
+        <el-row :gutter="10">
+          <el-col :span="1.5">
+            <el-button v-has-permi="['riskctrl:noticeAnnouncement:add']" type="primary" plain icon="Plus" @click="handleAdd()">新增</el-button>
+          </el-col>
+          <el-col :span="1.5">
+            <el-button v-has-permi="['riskctrl:noticeAnnouncement:edit']" type="success" plain :disabled="single" icon="Edit" @click="handleUpdate()">修改</el-button>
+          </el-col>
+          <el-col :span="1.5">
+            <el-button v-has-permi="['riskctrl:noticeAnnouncement:remove']" type="danger" plain :disabled="multiple" icon="Delete" @click="handleDelete()">删除</el-button>
+          </el-col>
+          <right-toolbar v-model:show-search="showSearch" :columns="columns" :search="true" @query-table="getList"></right-toolbar>
+        </el-row>
+      </template>
+
+      <el-table v-loading="loading" border :data="noticeList" @selection-change="handleSelectionChange">
+        <el-table-column type="selection" width="50" align="center" />
+        <el-table-column v-if="columns[0].visible" key="noticeTitle" label="公告标题" align="center" prop="noticeTitle" :show-overflow-tooltip="true" />
+        <el-table-column v-if="columns[1].visible" key="noticeType" label="类型" align="center" prop="noticeType" width="100">
+          <template #default="scope">
+            <el-tag :type="scope.row.noticeType === '提醒' ? 'warning' : 'primary'">{{ scope.row.noticeType }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column v-if="columns[2].visible" key="publishStatus" label="发布状态" align="center" prop="publishStatus" width="100">
+          <template #default="scope">
+            <el-tag :type="scope.row.publishStatus === '已发布' ? 'success' : 'info'">{{ scope.row.publishStatus }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column v-if="columns[3].visible" key="publishTime" label="发布时间" align="center" prop="publishTime" width="160" />
+        <el-table-column v-if="columns[4].visible" key="publisher" label="发布人" align="center" prop="publisher" width="100" />
+        <el-table-column v-if="columns[5].visible" label="创建时间" align="center" prop="createTime" width="160" />
+        <el-table-column label="操作" fixed="right" width="220" class-name="small-padding fixed-width">
+          <template #default="scope">
+            <el-tooltip content="修改" placement="top">
+              <el-button v-has-permi="['riskctrl:noticeAnnouncement:edit']" link type="primary" icon="Edit" @click="handleUpdate(scope.row)"></el-button>
+            </el-tooltip>
+            <el-tooltip content="发布" placement="top" v-if="scope.row.publishStatus !== '已发布'">
+              <el-button v-has-permi="['riskctrl:noticeAnnouncement:publish']" link type="success" icon="Promotion" @click="handlePublish(scope.row)"></el-button>
+            </el-tooltip>
+            <el-tooltip content="删除" placement="top">
+              <el-button v-has-permi="['riskctrl:noticeAnnouncement:remove']" link type="primary" icon="Delete" @click="handleDelete(scope.row)"></el-button>
+            </el-tooltip>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <pagination v-show="total > 0" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" :total="total" @pagination="getList" />
+    </el-card>
+
+    <!-- 新增/修改弹窗 -->
+    <el-dialog v-model="open" :title="title" width="750px" append-to-body>
+      <el-form ref="noticeFormRef" :model="form" :rules="rules" label-width="100px">
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="公告标题" prop="noticeTitle">
+              <el-input v-model="form.noticeTitle" placeholder="请输入公告标题" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="公告类型" prop="noticeType">
+              <el-select v-model="form.noticeType" placeholder="请选择" class="w-full">
+                <el-option label="通知" value="通知" />
+                <el-option label="公告" value="公告" />
+                <el-option label="提醒" value="提醒" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-form-item label="公告内容" prop="noticeContent">
+          <el-input v-model="form.noticeContent" type="textarea" :rows="6" placeholder="请输入公告内容" />
+        </el-form-item>
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="发布时间" prop="publishTime">
+              <el-date-picker v-model="form.publishTime" type="datetime" placeholder="选择发布时间" value-format="YYYY-MM-DD HH:mm:ss" class="w-full" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="发布人" prop="publisher">
+              <el-input v-model="form.publisher" placeholder="请输入发布人" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-form-item label="状态" prop="status">
+          <el-radio-group v-model="form.status">
+            <el-radio v-for="dict in sys_normal_disable" :key="dict.value" :label="dict.value">{{ dict.label }}</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="备注" prop="remark">
+          <el-input v-model="form.remark" type="textarea" placeholder="请输入备注" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button type="primary" @click="submitForm">确 定</el-button>
+          <el-button @click="cancel">取 消</el-button>
+        </div>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup name="NoticeAnnouncement" lang="ts">
+import { listNoticeAnnouncement, getNoticeAnnouncement, delNoticeAnnouncement, addNoticeAnnouncement, updateNoticeAnnouncement, publishNotice } from '@/api/riskctrl/noticeAnnouncement';
+import { NoticeAnnouncementForm, NoticeAnnouncementQuery, NoticeAnnouncementVO } from '@/api/riskctrl/noticeAnnouncement/types';
+
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+const { sys_normal_disable } = proxy.useDict('sys_normal_disable');
+
+const noticeList = ref<NoticeAnnouncementVO[]>([]);
+const loading = ref(true);
+const showSearch = ref(true);
+const ids = ref<Array<string | number>>([]);
+const single = ref(true);
+const multiple = ref(true);
+const total = ref(0);
+const title = ref('');
+const open = ref(false);
+
+const queryFormRef = ref<ElFormInstance>();
+const noticeFormRef = ref<ElFormInstance>();
+
+const queryParams = reactive<NoticeAnnouncementQuery>({
+  pageNum: 1,
+  pageSize: 10,
+  noticeTitle: undefined,
+  noticeType: undefined,
+  status: undefined
+});
+
+const form = reactive<NoticeAnnouncementForm>({
+  noticeId: undefined,
+  noticeTitle: '',
+  noticeType: '',
+  noticeContent: '',
+  publishStatus: '草稿',
+  publishTime: '',
+  publisher: '',
+  status: '0',
+  remark: ''
+});
+
+const rules = {
+  noticeTitle: [{ required: true, message: '公告标题不能为空', trigger: 'blur' }],
+  noticeType: [{ required: true, message: '公告类型不能为空', trigger: 'change' }]
+};
+
+const columns = ref([
+  { key: 0, label: '公告标题', visible: true },
+  { key: 1, label: '类型', visible: true },
+  { key: 2, label: '发布状态', visible: true },
+  { key: 3, label: '发布时间', visible: true },
+  { key: 4, label: '发布人', visible: true },
+  { key: 5, label: '创建时间', visible: true }
+]);
+
+const getList = async () => {
+  loading.value = true;
+  const res = await listNoticeAnnouncement(queryParams);
+  noticeList.value = res.rows;
+  total.value = res.total;
+  loading.value = false;
+};
+
+const cancel = () => {
+  open.value = false;
+  reset();
+};
+
+const reset = () => {
+  form.noticeId = undefined;
+  form.noticeTitle = '';
+  form.noticeType = '';
+  form.noticeContent = '';
+  form.publishStatus = '草稿';
+  form.publishTime = '';
+  form.publisher = '';
+  form.status = '0';
+  form.remark = '';
+  nextTick(() => { noticeFormRef.value?.resetFields(); });
+};
+
+const handleQuery = () => { queryParams.pageNum = 1; getList(); };
+const resetQuery = () => { queryFormRef.value?.resetFields(); handleQuery(); };
+
+const handleSelectionChange = (selection: NoticeAnnouncementVO[]) => {
+  ids.value = selection.map((item) => item.noticeId);
+  single.value = selection.length !== 1;
+  multiple.value = !selection.length;
+};
+
+const handleAdd = () => { reset(); open.value = true; title.value = '新增通知公告'; };
+
+const handleUpdate = async (row?: NoticeAnnouncementVO) => {
+  reset();
+  const noticeId = row?.noticeId || ids.value[0];
+  const res = await getNoticeAnnouncement(noticeId);
+  Object.assign(form, res.data);
+  open.value = true;
+  title.value = '修改通知公告';
+};
+
+const submitForm = () => {
+  noticeFormRef.value?.validate(async (valid: boolean) => {
+    if (valid) {
+      if (form.noticeId !== undefined) {
+        await updateNoticeAnnouncement(form);
+        proxy?.$modal.msgSuccess('修改成功');
+      } else {
+        await addNoticeAnnouncement(form);
+        proxy?.$modal.msgSuccess('新增成功');
+      }
+      open.value = false;
+      await getList();
+    }
+  });
+};
+
+const handlePublish = async (row: NoticeAnnouncementVO) => {
+  await proxy?.$modal.confirm('确认发布该公告?');
+  await publishNotice({ noticeId: row.noticeId, publishStatus: '已发布', publishTime: new Date().toISOString() } as NoticeAnnouncementForm);
+  proxy?.$modal.msgSuccess('发布成功');
+  await getList();
+};
+
+const handleDelete = async (row?: NoticeAnnouncementVO) => {
+  const noticeIds = row?.noticeId || ids.value;
+  await proxy?.$modal.confirm('是否确认删除该公告?');
+  await delNoticeAnnouncement(noticeIds);
+  await getList();
+  proxy?.$modal.msgSuccess('删除成功');
+};
+
+onMounted(() => { getList(); });
+</script>

+ 401 - 0
src/views/riskctrl/patrolPoint/index.vue

@@ -0,0 +1,401 @@
+<template>
+  <div class="p-2">
+    <!-- 巡检打卡数据卡片 -->
+    <el-row :gutter="16" class="mb-[10px]">
+      <el-col :span="6">
+        <el-card shadow="hover">
+          <div class="flex items-center justify-between">
+            <div>
+              <p class="text-gray-500 text-sm">今日应打卡</p>
+              <p class="text-2xl font-bold text-blue-500">{{ statsData.todayShouldCheck }}</p>
+            </div>
+            <el-icon :size="40" color="#409EFF"><Calendar /></el-icon>
+          </div>
+        </el-card>
+      </el-col>
+      <el-col :span="6">
+        <el-card shadow="hover">
+          <div class="flex items-center justify-between">
+            <div>
+              <p class="text-gray-500 text-sm">今日已打卡</p>
+              <p class="text-2xl font-bold text-green-500">{{ statsData.todayChecked }}</p>
+            </div>
+            <el-icon :size="40" color="#67C23A"><CircleCheckFilled /></el-icon>
+          </div>
+        </el-card>
+      </el-col>
+      <el-col :span="6">
+        <el-card shadow="hover">
+          <div class="flex items-center justify-between">
+            <div>
+              <p class="text-gray-500 text-sm">异常打卡</p>
+              <p class="text-2xl font-bold text-red-500">{{ statsData.todayAbnormal }}</p>
+            </div>
+            <el-icon :size="40" color="#F56C6C"><WarningFilled /></el-icon>
+          </div>
+        </el-card>
+      </el-col>
+      <el-col :span="6">
+        <el-card shadow="hover">
+          <div class="flex items-center justify-between">
+            <div>
+              <p class="text-gray-500 text-sm">未打卡</p>
+              <p class="text-2xl font-bold text-gray-500">{{ statsData.todayUnchecked }}</p>
+            </div>
+            <el-icon :size="40" color="#909399"><Clock /></el-icon>
+          </div>
+        </el-card>
+      </el-col>
+    </el-row>
+
+    <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
+      <div v-show="showSearch" class="mb-[10px]">
+        <el-card shadow="hover">
+          <el-form ref="queryFormRef" :model="queryParams" :inline="true">
+            <el-form-item label="巡检点名称" prop="pointName">
+              <el-input v-model="queryParams.pointName" placeholder="请输入名称" clearable @keyup.enter="handleQuery" />
+            </el-form-item>
+            <el-form-item label="位置区域" prop="location">
+              <el-input v-model="queryParams.location" placeholder="请输入位置" clearable @keyup.enter="handleQuery" />
+            </el-form-item>
+            <el-form-item label="责任人" prop="responsiblePerson">
+              <el-input v-model="queryParams.responsiblePerson" placeholder="请输入责任人" clearable @keyup.enter="handleQuery" />
+            </el-form-item>
+            <el-form-item>
+              <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
+              <el-button icon="Refresh" @click="resetQuery">重置</el-button>
+            </el-form-item>
+          </el-form>
+        </el-card>
+      </div>
+    </transition>
+
+    <el-card shadow="hover">
+      <template #header>
+        <el-row :gutter="10">
+          <el-col :span="1.5">
+            <el-button v-has-permi="['riskctrl:patrolPoint:add']" type="primary" plain icon="Plus" @click="handleAdd()">新增巡检点</el-button>
+          </el-col>
+          <el-col :span="1.5">
+            <el-button v-has-permi="['riskctrl:patrolPoint:edit']" type="success" plain :disabled="single" icon="Edit" @click="handleUpdate()">编辑</el-button>
+          </el-col>
+          <el-col :span="1.5">
+            <el-button v-has-permi="['riskctrl:patrolPoint:remove']" type="danger" plain :disabled="multiple" icon="Delete" @click="handleDelete()">删除</el-button>
+          </el-col>
+          <el-col :span="1.5">
+            <el-button type="warning" plain :disabled="single" icon="List" @click="handleRecords()">打卡记录</el-button>
+          </el-col>
+          <right-toolbar v-model:show-search="showSearch" :columns="columns" :search="true" @query-table="getList"></right-toolbar>
+        </el-row>
+      </template>
+
+      <el-table v-loading="loading" border :data="pointList" @selection-change="handleSelectionChange">
+        <el-table-column type="selection" width="50" align="center" />
+        <el-table-column v-if="columns[0].visible" key="pointName" label="巡检点名称" align="center" prop="pointName" :show-overflow-tooltip="true" />
+        <el-table-column v-if="columns[1].visible" key="area" label="位置区域" align="center" prop="area" :show-overflow-tooltip="true" />
+        <el-table-column v-if="columns[2].visible" key="address" label="详细地址" align="center" prop="address" :show-overflow-tooltip="true" />
+        <el-table-column v-if="columns[3].visible" key="nfcTag" label="NFC绑定" align="center" prop="nfcTag" width="120">
+          <template #default="scope">
+            <el-tag :type="scope.row.nfcTag ? 'success' : 'info'">{{ scope.row.nfcTag || '未绑定' }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column v-if="columns[4].visible" key="responsiblePerson" label="责任人" align="center" prop="responsiblePerson" width="100" />
+        <el-table-column v-if="columns[5].visible" label="创建时间" align="center" prop="createTime" width="160" />
+        <el-table-column label="操作" fixed="right" width="200" class-name="small-padding fixed-width">
+          <template #default="scope">
+            <el-tooltip content="编辑" placement="top">
+              <el-button v-has-permi="['riskctrl:patrolPoint:edit']" link type="primary" icon="Edit" @click="handleUpdate(scope.row)"></el-button>
+            </el-tooltip>
+            <el-tooltip content="打卡记录" placement="top">
+              <el-button link type="warning" icon="List" @click="handleRecords(scope.row)"></el-button>
+            </el-tooltip>
+            <el-tooltip content="删除" placement="top">
+              <el-button v-has-permi="['riskctrl:patrolPoint:remove']" link type="primary" icon="Delete" @click="handleDelete(scope.row)"></el-button>
+            </el-tooltip>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <pagination v-show="total > 0" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" :total="total" @pagination="getList" />
+    </el-card>
+
+    <!-- 新增/编辑巡检点弹窗 -->
+    <el-dialog v-model="open" :title="title" width="800px" append-to-body>
+      <el-form ref="pointFormRef" :model="form" :rules="rules" label-width="120px">
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="巡检名称" prop="pointName">
+              <el-input v-model="form.pointName" placeholder="请输入巡检点名称" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="责任人" prop="responsiblePerson">
+              <el-select v-model="form.responsiblePerson" placeholder="请选择责任人" class="w-full" filterable>
+                <el-option label="张三" value="张三" />
+                <el-option label="李四" value="李四" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="位置区域" prop="area">
+              <el-input v-model="form.area" placeholder="请输入区域名称" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="详细地址" prop="address">
+              <el-input v-model="form.address" placeholder="请输入详细地址" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="纬度">
+              <el-input v-model="form.latitude" placeholder="请输入纬度" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="经度">
+              <el-input v-model="form.longitude" placeholder="请输入经度" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-form-item label="每日打卡时间">
+          <div v-for="(item, index) in checkTimeList" :key="index" class="flex items-center mb-2">
+            <el-time-picker v-model="item.value" value-format="HH:mm" placeholder="选择时间" class="mr-2" />
+            <el-button type="danger" :icon="'Delete'" circle size="small" @click="removeCheckTime(index)" v-if="checkTimeList.length > 1" />
+          </div>
+          <el-button type="primary" link icon="Plus" @click="addCheckTime">添加时段</el-button>
+        </el-form-item>
+        <el-form-item label="NFC绑定" prop="nfcTag">
+          <el-select v-model="form.nfcTag" placeholder="选择NFC标签" class="w-full" clearable>
+            <el-option label="NFC-001" value="NFC-001" />
+            <el-option label="NFC-002" value="NFC-002" />
+            <el-option label="NFC-003" value="NFC-003" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="打卡项目列表">
+          <div v-for="(item, index) in checkItemList" :key="index" class="flex items-center mb-2">
+            <el-input v-model="item.value" placeholder="如:设备温度、线路电压" class="mr-2" />
+            <el-button type="danger" :icon="'Delete'" circle size="small" @click="removeCheckItem(index)" v-if="checkItemList.length > 1" />
+          </div>
+          <el-button type="primary" link icon="Plus" @click="addCheckItem">添加项目</el-button>
+        </el-form-item>
+        <el-form-item label="备注说明" prop="remark">
+          <el-input v-model="form.remark" type="textarea" :rows="2" placeholder="请输入备注" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button type="primary" @click="submitForm">确 定</el-button>
+          <el-button @click="cancel">取 消</el-button>
+        </div>
+      </template>
+    </el-dialog>
+
+    <!-- 打卡记录弹窗 -->
+    <el-dialog v-model="recordOpen" title="打卡记录" width="900px" append-to-body>
+      <el-table :data="recordList" border>
+        <el-table-column label="打卡时间" align="center" prop="patrolTime" width="160" />
+        <el-table-column label="打卡人" align="center" prop="patrolPerson" width="100" />
+        <el-table-column label="打卡状态" align="center" prop="status" width="100">
+          <template #default="scope">
+            <el-tag :type="scope.row.status === '已打卡' ? 'success' : 'danger'">{{ scope.row.status }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="打卡结果" align="center" prop="result" width="100">
+          <template #default="scope">
+            <el-tag :type="scope.row.result === '正常' ? 'success' : 'danger'">{{ scope.row.result }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="异常原因" align="center" prop="abnormalReason" :show-overflow-tooltip="true" />
+      </el-table>
+      <template #footer>
+        <el-button @click="recordOpen = false">关 闭</el-button>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup name="PatrolPoint" lang="ts">
+import { listPatrolPoint, getPatrolPoint, delPatrolPoint, addPatrolPoint, updatePatrolPoint } from '@/api/riskctrl/patrolPoint';
+import { PatrolPointForm, PatrolPointQuery, PatrolPointVO } from '@/api/riskctrl/patrolPoint/types';
+import { listPatrolRecord, getPatrolRecordStats } from '@/api/riskctrl/patrolRecord';
+import { PatrolRecordVO, PatrolRecordStats } from '@/api/riskctrl/patrolRecord/types';
+
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+
+const pointList = ref<PatrolPointVO[]>([]);
+const recordList = ref<PatrolRecordVO[]>([]);
+const loading = ref(true);
+const showSearch = ref(true);
+const ids = ref<Array<string | number>>([]);
+const single = ref(true);
+const multiple = ref(true);
+const total = ref(0);
+const title = ref('');
+const open = ref(false);
+const recordOpen = ref(false);
+const currentPointId = ref<string | number>();
+
+const checkTimeList = ref<{ value: string }[]>([{ value: '' }]);
+const checkItemList = ref<{ value: string }[]>([{ value: '' }]);
+
+const queryFormRef = ref<ElFormInstance>();
+const pointFormRef = ref<ElFormInstance>();
+
+const statsData = reactive<PatrolRecordStats>({
+  todayShouldCheck: 0,
+  todayChecked: 0,
+  todayAbnormal: 0,
+  todayUnchecked: 0
+});
+
+const queryParams = reactive<PatrolPointQuery>({
+  pageNum: 1,
+  pageSize: 10,
+  pointName: undefined,
+  location: undefined,
+  responsiblePerson: undefined
+});
+
+const form = reactive<PatrolPointForm>({
+  pointId: undefined,
+  pointName: '',
+  responsiblePerson: '',
+  area: '',
+  address: '',
+  longitude: '',
+  latitude: '',
+  checkTimes: '',
+  nfcTag: '',
+  checkItems: '',
+  remark: ''
+});
+
+const rules = {
+  pointName: [{ required: true, message: '巡检点名称不能为空', trigger: 'blur' }],
+  responsiblePerson: [{ required: true, message: '责任人不能为空', trigger: 'change' }]
+};
+
+const columns = ref([
+  { key: 0, label: '巡检点名称', visible: true },
+  { key: 1, label: '位置区域', visible: true },
+  { key: 2, label: '详细地址', visible: true },
+  { key: 3, label: 'NFC绑定', visible: true },
+  { key: 4, label: '责任人', visible: true },
+  { key: 5, label: '创建时间', visible: true }
+]);
+
+const addCheckTime = () => { checkTimeList.value.push({ value: '' }); };
+const removeCheckTime = (index: number) => { checkTimeList.value.splice(index, 1); };
+const addCheckItem = () => { checkItemList.value.push({ value: '' }); };
+const removeCheckItem = (index: number) => { checkItemList.value.splice(index, 1); };
+
+const loadStats = async () => {
+  try {
+    const res = await getPatrolRecordStats();
+    Object.assign(statsData, res.data);
+  } catch { /* ignore */ }
+};
+
+const getList = async () => {
+  loading.value = true;
+  const res = await listPatrolPoint(queryParams);
+  pointList.value = res.rows;
+  total.value = res.total;
+  loading.value = false;
+};
+
+const cancel = () => {
+  open.value = false;
+  reset();
+};
+
+const reset = () => {
+  form.pointId = undefined;
+  form.pointName = '';
+  form.responsiblePerson = '';
+  form.area = '';
+  form.address = '';
+  form.longitude = '';
+  form.latitude = '';
+  form.checkTimes = '';
+  form.nfcTag = '';
+  form.checkItems = '';
+  form.remark = '';
+  checkTimeList.value = [{ value: '' }];
+  checkItemList.value = [{ value: '' }];
+  nextTick(() => { pointFormRef.value?.resetFields(); });
+};
+
+const handleQuery = () => { queryParams.pageNum = 1; getList(); };
+const resetQuery = () => { queryFormRef.value?.resetFields(); handleQuery(); };
+
+const handleSelectionChange = (selection: PatrolPointVO[]) => {
+  ids.value = selection.map((item) => item.pointId);
+  single.value = selection.length !== 1;
+  multiple.value = !selection.length;
+};
+
+const handleAdd = () => { reset(); open.value = true; title.value = '新增巡检点'; };
+
+const handleUpdate = async (row?: PatrolPointVO) => {
+  reset();
+  const pointId = row?.pointId || ids.value[0];
+  const res = await getPatrolPoint(pointId);
+  Object.assign(form, res.data);
+  if (res.data.checkTimes) {
+    checkTimeList.value = res.data.checkTimes.split(',').map((t: string) => ({ value: t }));
+  }
+  if (res.data.checkItems) {
+    checkItemList.value = res.data.checkItems.split(',').map((t: string) => ({ value: t }));
+  }
+  open.value = true;
+  title.value = '编辑巡检点';
+};
+
+const submitForm = () => {
+  pointFormRef.value?.validate(async (valid: boolean) => {
+    if (valid) {
+      form.checkTimes = checkTimeList.value.map(t => t.value).filter(Boolean).join(',');
+      form.checkItems = checkItemList.value.map(t => t.value).filter(Boolean).join(',');
+      if (form.pointId !== undefined) {
+        await updatePatrolPoint(form);
+        proxy?.$modal.msgSuccess('修改成功');
+      } else {
+        await addPatrolPoint(form);
+        proxy?.$modal.msgSuccess('新增成功');
+      }
+      open.value = false;
+      await getList();
+    }
+  });
+};
+
+const handleRecords = async (row?: PatrolPointVO) => {
+  const pointId = row?.pointId || ids.value[0];
+  currentPointId.value = pointId;
+  try {
+    const res = await listPatrolRecord({ pageNum: 1, pageSize: 100, pointId });
+    recordList.value = res.rows || [];
+  } catch {
+    recordList.value = [];
+  }
+  recordOpen.value = true;
+};
+
+const handleDelete = async (row?: PatrolPointVO) => {
+  const pointIds = row?.pointId || ids.value;
+  await proxy?.$modal.confirm('是否确认删除该巡检点?');
+  await delPatrolPoint(pointIds);
+  await getList();
+  proxy?.$modal.msgSuccess('删除成功');
+};
+
+onMounted(() => {
+  loadStats();
+  getList();
+});
+</script>

+ 272 - 0
src/views/riskctrl/patrolRecord/index.vue

@@ -0,0 +1,272 @@
+<template>
+  <div class="p-2">
+    <!-- 数据卡片 -->
+    <el-row :gutter="16" class="mb-[10px]">
+      <el-col :span="6">
+        <el-card shadow="hover">
+          <div class="flex items-center justify-between">
+            <div>
+              <p class="text-gray-500 text-sm">总打卡记录</p>
+              <p class="text-2xl font-bold text-blue-500">{{ statsData.totalRecords }}</p>
+            </div>
+            <el-icon :size="40" color="#409EFF"><List /></el-icon>
+          </div>
+        </el-card>
+      </el-col>
+      <el-col :span="6">
+        <el-card shadow="hover">
+          <div class="flex items-center justify-between">
+            <div>
+              <p class="text-gray-500 text-sm">正常打卡</p>
+              <p class="text-2xl font-bold text-green-500">{{ statsData.normalCount }}</p>
+            </div>
+            <el-icon :size="40" color="#67C23A"><CircleCheckFilled /></el-icon>
+          </div>
+        </el-card>
+      </el-col>
+      <el-col :span="6">
+        <el-card shadow="hover">
+          <div class="flex items-center justify-between">
+            <div>
+              <p class="text-gray-500 text-sm">异常打卡</p>
+              <p class="text-2xl font-bold text-red-500">{{ statsData.abnormalCount }}</p>
+            </div>
+            <el-icon :size="40" color="#F56C6C"><WarningFilled /></el-icon>
+          </div>
+        </el-card>
+      </el-col>
+      <el-col :span="6">
+        <el-card shadow="hover">
+          <div class="flex items-center justify-between">
+            <div>
+              <p class="text-gray-500 text-sm">今日打卡率</p>
+              <p class="text-2xl font-bold text-purple-500">{{ statsData.todayRate }}%</p>
+            </div>
+            <el-icon :size="40" color="#8B5CF6"><TrendCharts /></el-icon>
+          </div>
+        </el-card>
+      </el-col>
+    </el-row>
+
+    <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
+      <div v-show="showSearch" class="mb-[10px]">
+        <el-card shadow="hover">
+          <el-form ref="queryFormRef" :model="queryParams" :inline="true">
+            <el-form-item label="巡检点" prop="pointName">
+              <el-input v-model="queryParams.pointName" placeholder="巡检点名称" clearable @keyup.enter="handleQuery" />
+            </el-form-item>
+            <el-form-item label="打卡人" prop="checkUser">
+              <el-input v-model="queryParams.checkUser" placeholder="打卡人姓名" clearable @keyup.enter="handleQuery" />
+            </el-form-item>
+            <el-form-item label="打卡状态" prop="checkStatus">
+              <el-select v-model="queryParams.checkStatus" placeholder="状态" clearable>
+                <el-option label="已打卡" value="已打卡" />
+                <el-option label="未打卡" value="未打卡" />
+              </el-select>
+            </el-form-item>
+            <el-form-item label="打卡结果" prop="checkResult">
+              <el-select v-model="queryParams.checkResult" placeholder="结果" clearable>
+                <el-option label="正常" value="正常" />
+                <el-option label="异常" value="异常" />
+              </el-select>
+            </el-form-item>
+            <el-form-item label="打卡时间" prop="checkTime">
+              <el-date-picker v-model="queryParams.checkTime" type="daterange" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期" value-format="YYYY-MM-DD" />
+            </el-form-item>
+            <el-form-item>
+              <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
+              <el-button icon="Refresh" @click="resetQuery">重置</el-button>
+            </el-form-item>
+          </el-form>
+        </el-card>
+      </div>
+    </transition>
+
+    <el-card shadow="hover">
+      <template #header>
+        <el-row :gutter="10">
+          <el-col :span="1.5">
+            <el-button type="warning" plain icon="Download" @click="handleExport">导出</el-button>
+          </el-col>
+          <right-toolbar v-model:show-search="showSearch" :columns="columns" :search="true" @query-table="getList"></right-toolbar>
+        </el-row>
+      </template>
+
+      <el-table v-loading="loading" border :data="recordList" @selection-change="handleSelectionChange">
+        <el-table-column type="selection" width="50" align="center" />
+        <el-table-column v-if="columns[0].visible" key="pointName" label="巡检点名称" align="center" prop="pointName" :show-overflow-tooltip="true" />
+        <el-table-column v-if="columns[1].visible" key="checkTime" label="巡检时间" align="center" prop="checkTime" width="170" />
+        <el-table-column v-if="columns[2].visible" key="checkUser" label="打卡人" align="center" prop="checkUser" width="100" />
+        <el-table-column v-if="columns[3].visible" key="checkStatus" label="打卡状态" align="center" prop="checkStatus" width="100">
+          <template #default="scope">
+            <el-tag :type="scope.row.checkStatus === '已打卡' ? 'success' : 'danger'">{{ scope.row.checkStatus }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column v-if="columns[4].visible" key="checkResult" label="打卡结果" align="center" prop="checkResult" width="100">
+          <template #default="scope">
+            <el-tag :type="scope.row.checkResult === '正常' ? 'success' : 'danger'">{{ scope.row.checkResult || '-' }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column v-if="columns[5].visible" key="abnormalReason" label="异常原因" align="center" prop="abnormalReason" :show-overflow-tooltip="true" />
+        <el-table-column v-if="columns[6].visible" key="location" label="打卡位置" align="center" prop="location" :show-overflow-tooltip="true" />
+        <el-table-column label="操作" fixed="right" width="120" class-name="small-padding fixed-width">
+          <template #default="scope">
+            <el-tooltip content="详情" placement="top">
+              <el-button link type="primary" icon="View" @click="handleDetail(scope.row)"></el-button>
+            </el-tooltip>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <pagination v-show="total > 0" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" :total="total" @pagination="getList" />
+    </el-card>
+
+    <!-- 详情弹窗 -->
+    <el-dialog v-model="detailOpen" title="巡检记录详情" width="650px" append-to-body>
+      <el-descriptions :column="2" border>
+        <el-descriptions-item label="巡检点名称">{{ detailForm.pointName }}</el-descriptions-item>
+        <el-descriptions-item label="打卡人">{{ detailForm.checkUser }}</el-descriptions-item>
+        <el-descriptions-item label="巡检时间">{{ detailForm.checkTime }}</el-descriptions-item>
+        <el-descriptions-item label="打卡状态">
+          <el-tag :type="detailForm.checkStatus === '已打卡' ? 'success' : 'danger'">{{ detailForm.checkStatus }}</el-tag>
+        </el-descriptions-item>
+        <el-descriptions-item label="打卡结果">
+          <el-tag :type="detailForm.checkResult === '正常' ? 'success' : 'danger'">{{ detailForm.checkResult || '-' }}</el-tag>
+        </el-descriptions-item>
+        <el-descriptions-item label="打卡位置">{{ detailForm.location }}</el-descriptions-item>
+        <el-descriptions-item label="异常原因" :span="2">{{ detailForm.abnormalReason || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="巡检项目" :span="2">
+          <div v-if="detailForm.checkItems && detailForm.checkItems.length">
+            <div v-for="(item, idx) in detailForm.checkItems" :key="idx" class="mb-2 p-2 bg-gray-50 rounded">
+              <p><strong>项目大类:</strong>{{ item.category }}</p>
+              <p><strong>巡检内容:</strong>{{ item.content }}</p>
+              <p><strong>巡检结果:</strong>{{ item.result || '-' }}</p>
+              <div v-if="item.photos && item.photos.length" class="mt-2 flex gap-2">
+                <el-image v-for="(photo, pidx) in item.photos" :key="pidx" :src="photo" style="width:80px;height:80px" fit="cover" :preview-src-list="item.photos" />
+              </div>
+            </div>
+          </div>
+          <span v-else>-</span>
+        </el-descriptions-item>
+      </el-descriptions>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button @click="detailOpen = false">关 闭</el-button>
+        </div>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup name="PatrolRecord" lang="ts">
+import { ref, reactive, onMounted, getCurrentInstance } from 'vue';
+import type { ComponentInternalInstance } from 'vue';
+import type { ElFormInstance } from 'element-plus';
+import { listPatrolRecord } from '@/api/riskctrl/patrolRecord';
+import { PatrolRecordVO } from '@/api/riskctrl/patrolRecord/types';
+
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+
+const recordList = ref<PatrolRecordVO[]>([]);
+const loading = ref(true);
+const showSearch = ref(true);
+const ids = ref<Array<string | number>>([]);
+const total = ref(0);
+const detailOpen = ref(false);
+
+const queryFormRef = ref<ElFormInstance>();
+
+const queryParams = reactive<any>({
+  pageNum: 1,
+  pageSize: 10,
+  pointName: undefined,
+  checkUser: undefined,
+  checkStatus: undefined,
+  checkResult: undefined,
+  checkTime: undefined
+});
+
+const detailForm = reactive<any>({
+  pointName: '',
+  checkUser: '',
+  checkTime: '',
+  checkStatus: '',
+  checkResult: '',
+  location: '',
+  abnormalReason: '',
+  checkItems: []
+});
+
+const statsData = reactive({
+  totalRecords: 0,
+  normalCount: 0,
+  abnormalCount: 0,
+  todayRate: 0
+});
+
+const columns = ref([
+  { key: 0, label: '巡检点名称', visible: true },
+  { key: 1, label: '巡检时间', visible: true },
+  { key: 2, label: '打卡人', visible: true },
+  { key: 3, label: '打卡状态', visible: true },
+  { key: 4, label: '打卡结果', visible: true },
+  { key: 5, label: '异常原因', visible: true },
+  { key: 6, label: '打卡位置', visible: true }
+]);
+
+const getList = async () => {
+  loading.value = true;
+  try {
+    const res = await listPatrolRecord(queryParams as any);
+    recordList.value = (res as any).rows || [];
+    total.value = (res as any).total || 0;
+  } catch (e) {
+    console.error('加载巡检记录失败', e);
+  } finally {
+    loading.value = false;
+  }
+};
+
+const loadStats = async () => {
+  try {
+    const res = await listPatrolRecord({ pageNum: 1, pageSize: 9999 } as any);
+    const allData = (res as any).rows || [];
+    statsData.totalRecords = allData.length;
+    statsData.normalCount = allData.filter((d: any) => d.checkResult === '正常').length;
+    statsData.abnormalCount = allData.filter((d: any) => d.checkResult === '异常').length;
+    const today = new Date().toISOString().split('T')[0];
+    const todayRecords = allData.filter((d: any) => d.checkTime && d.checkTime.startsWith(today));
+    statsData.todayRate = statsData.totalRecords > 0 ? Math.round((todayRecords.length / statsData.totalRecords) * 100) : 0;
+  } catch (e) {
+    console.error('加载统计数据失败', e);
+  }
+};
+
+const handleQuery = () => {
+  queryParams.pageNum = 1;
+  getList();
+};
+
+const resetQuery = () => {
+  queryFormRef.value?.resetFields();
+  handleQuery();
+};
+
+const handleSelectionChange = (selection: PatrolRecordVO[]) => {
+  ids.value = selection.map((item) => item.recordId);
+};
+
+const handleDetail = (row: PatrolRecordVO) => {
+  Object.assign(detailForm, row);
+  detailOpen.value = true;
+};
+
+const handleExport = () => {
+  proxy?.$modal.msgSuccess('导出成功');
+};
+
+onMounted(() => {
+  getList();
+  loadStats();
+});
+</script>

+ 592 - 0
src/views/riskctrl/personInfo/index.vue

@@ -0,0 +1,592 @@
+<template>
+  <div class="p-2">
+    <!-- 顶部统计卡片 -->
+    <el-row :gutter="16" class="mb-[10px]">
+      <el-col :span="6">
+        <el-card shadow="hover">
+          <div class="flex items-center justify-between">
+            <div>
+              <p class="text-gray-500 text-sm">人员总数</p>
+              <p class="text-2xl font-bold text-blue-500">{{ statsData.total }}</p>
+            </div>
+            <el-icon :size="40" color="#409EFF"><UserFilled /></el-icon>
+          </div>
+        </el-card>
+      </el-col>
+      <el-col :span="6">
+        <el-card shadow="hover">
+          <div class="flex items-center justify-between">
+            <div>
+              <p class="text-gray-500 text-sm">已投保</p>
+              <p class="text-2xl font-bold text-green-500">{{ statsData.insured }}</p>
+            </div>
+            <el-icon :size="40" color="#67C23A"><CircleCheckFilled /></el-icon>
+          </div>
+        </el-card>
+      </el-col>
+      <el-col :span="6">
+        <el-card shadow="hover">
+          <div class="flex items-center justify-between">
+            <div>
+              <p class="text-gray-500 text-sm">未投保</p>
+              <p class="text-2xl font-bold text-red-500">{{ statsData.uninsured }}</p>
+            </div>
+            <el-icon :size="40" color="#F56C6C"><CircleCloseFilled /></el-icon>
+          </div>
+        </el-card>
+      </el-col>
+      <el-col :span="6">
+        <el-card shadow="hover">
+          <div class="flex items-center justify-between">
+            <div>
+              <p class="text-gray-500 text-sm">保险即将到期</p>
+              <p class="text-2xl font-bold text-orange-500">{{ statsData.expiringSoon }}</p>
+            </div>
+            <el-icon :size="40" color="#E6A23C"><WarningFilled /></el-icon>
+          </div>
+        </el-card>
+      </el-col>
+    </el-row>
+
+    <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
+      <div v-show="showSearch" class="mb-[10px]">
+        <el-card shadow="hover">
+          <el-form ref="queryFormRef" :model="queryParams" :inline="true">
+            <el-form-item label="姓名" prop="personName">
+              <el-input v-model="queryParams.personName" placeholder="请输入姓名" clearable @keyup.enter="handleQuery" />
+            </el-form-item>
+            <el-form-item label="人员类型" prop="personType">
+              <el-select v-model="queryParams.personType" placeholder="请选择" clearable>
+                <el-option
+                  v-for="dict in person_type_options"
+                  :key="dict.value"
+                  :label="dict.label"
+                  :value="dict.value"
+                />
+              </el-select>
+            </el-form-item>
+            <el-form-item label="工作门店" prop="workStoreId">
+              <el-tree-select
+                v-model="queryParams.workStoreId"
+                :data="deptOptions"
+                :props="{ value: 'id', label: 'label', children: 'children' }"
+                value-key="id"
+                placeholder="请选择门店"
+                check-strictly
+                clearable
+              />
+            </el-form-item>
+            <el-form-item label="保险状态" prop="insuranceStatus">
+              <el-select v-model="queryParams.insuranceStatus" placeholder="请选择" clearable>
+                <el-option label="已投保" value="1" />
+                <el-option label="未投保" value="0" />
+              </el-select>
+            </el-form-item>
+            <el-form-item>
+              <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
+              <el-button icon="Refresh" @click="resetQuery">重置</el-button>
+            </el-form-item>
+          </el-form>
+        </el-card>
+      </div>
+    </transition>
+
+    <el-card shadow="hover">
+      <template #header>
+        <el-row :gutter="10">
+          <el-col :span="1.5">
+            <el-button v-has-permi="['riskctrl:personInfo:add']" type="primary" plain icon="Plus" @click="handleAdd()">新增</el-button>
+          </el-col>
+          <el-col :span="1.5">
+            <el-button v-has-permi="['riskctrl:personInfo:edit']" type="success" plain :disabled="single" icon="Edit" @click="handleUpdate()">修改</el-button>
+          </el-col>
+          <el-col :span="1.5">
+            <el-button v-has-permi="['riskctrl:personInfo:remove']" type="danger" plain :disabled="multiple" icon="Delete" @click="handleDelete()">删除</el-button>
+          </el-col>
+          <right-toolbar v-model:show-search="showSearch" :columns="columns" :search="true" @query-table="getList"></right-toolbar>
+        </el-row>
+      </template>
+
+      <el-table v-loading="loading" border :data="personList" @selection-change="handleSelectionChange">
+        <el-table-column type="selection" width="50" align="center" />
+        <el-table-column v-if="columns[0].visible" label="人员类型" align="center" prop="personType" width="100">
+          <template #default="scope">
+            <el-tag :type="getPersonTypeType(scope.row.personType)">{{ scope.row.personType }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column v-if="columns[1].visible" label="姓名" align="center" prop="personName" />
+        <el-table-column v-if="columns[2].visible" label="年龄" align="center" prop="age" width="80" />
+        <el-table-column v-if="columns[3].visible" label="身份证号" align="center" prop="idCard" width="180">
+          <template #default="scope">
+            <span>{{ maskIdCard(scope.row.idCard) }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column v-if="columns[4].visible" label="工作门店" align="center" prop="workStoreName" />
+        <el-table-column v-if="columns[5].visible" label="保险状态" align="center" prop="insuranceStatus" width="100">
+          <template #default="scope">
+            <el-tag :type="getInsuranceType(scope.row)">{{ getInsuranceLabel(scope.row) }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column v-if="columns[6].visible" label="保险有效期" align="center" prop="insuranceExpireDate" width="120">
+          <template #default="scope">
+            <span>{{ scope.row.insuranceExpireDate ? scope.row.insuranceExpireDate.substring(0, 10) : '-' }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column v-if="columns[7].visible" label="岗位证书" align="center" width="120">
+          <template #default="scope">
+            <el-button link type="primary" @click="viewCertificates(scope.row)">
+              {{ getCertCount(scope.row.certificates) }}个证书
+            </el-button>
+          </template>
+        </el-table-column>
+        <el-table-column v-if="columns[8].visible" label="创建时间" align="center" prop="createTime" width="160" />
+        <el-table-column label="操作" fixed="right" width="200" class-name="small-padding fixed-width">
+          <template #default="scope">
+            <el-tooltip content="修改" placement="top">
+              <el-button v-has-permi="['riskctrl:personInfo:edit']" link type="primary" icon="Edit" @click="handleUpdate(scope.row)"></el-button>
+            </el-tooltip>
+            <el-tooltip content="查看证书" placement="top">
+              <el-button link type="primary" icon="Document" @click="viewCertificates(scope.row)"></el-button>
+            </el-tooltip>
+            <el-tooltip content="删除" placement="top">
+              <el-button v-has-permi="['riskctrl:personInfo:remove']" link type="primary" icon="Delete" @click="handleDelete(scope.row)"></el-button>
+            </el-tooltip>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <pagination v-show="total > 0" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" :total="total" @pagination="getList" />
+    </el-card>
+
+    <!-- 新增/修改弹窗 -->
+    <el-dialog v-model="open" :title="title" width="700px" append-to-body>
+      <el-form ref="personFormRef" :model="form" :rules="rules" label-width="100px">
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="人员类型" prop="personType">
+              <el-select v-model="form.personType" placeholder="请选择" class="w-full">
+                <el-option
+                  v-for="dict in person_type_options"
+                  :key="dict.value"
+                  :label="dict.label"
+                  :value="dict.value"
+                />
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="姓名" prop="personName">
+              <el-input v-model="form.personName" placeholder="请输入姓名" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="身份证号" prop="idCard">
+              <el-input v-model="form.idCard" placeholder="请输入身份证号" maxlength="18" @blur="calcAge" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="年龄">
+              <el-input v-model="form.age" placeholder="自动计算" disabled />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="工作门店" prop="workStoreId">
+              <el-tree-select
+                v-model="form.workStoreId"
+                :data="deptOptions"
+                :props="{ value: 'id', label: 'label', children: 'children' }"
+                value-key="id"
+                placeholder="请选择门店"
+                check-strictly
+                clearable
+                class="w-full"
+              />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="保险状态" prop="insuranceStatus">
+              <el-select v-model="form.insuranceStatus" placeholder="请选择" class="w-full">
+                <el-option label="已投保" value="1" />
+                <el-option label="未投保" value="0" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-form-item v-if="form.insuranceStatus === '1'" label="保险有效期" prop="insuranceExpireDate">
+          <el-date-picker v-model="form.insuranceExpireDate" type="date" placeholder="选择日期" value-format="YYYY-MM-DD" class="w-full" />
+        </el-form-item>
+        <el-form-item label="岗位证书">
+          <div class="w-full">
+            <div v-for="(cert, index) in certificateList" :key="index" class="flex items-center gap-2 mb-2">
+              <el-select v-model="cert.certType" placeholder="证书类型" style="width: 180px">
+                <el-option
+                  v-for="dict in certificate_type_options"
+                  :key="dict.value"
+                  :label="dict.label"
+                  :value="dict.value"
+                />
+              </el-select>
+              <el-upload
+                :action="uploadAction"
+                :headers="uploadHeaders"
+                :show-file-list="false"
+                :on-success="(res: any) => handleCertUploadSuccess(res, index)"
+                accept=".jpg,.png,.pdf"
+              >
+                <el-button type="primary" link>
+                  {{ cert.certUrl ? '重新上传' : '上传证书' }}
+                </el-button>
+              </el-upload>
+              <el-tag v-if="cert.certUrl" type="success" size="small">已上传</el-tag>
+              <el-button type="danger" link icon="Delete" @click="removeCertificate(index)"></el-button>
+            </div>
+            <el-button type="primary" link icon="Plus" @click="addCertificate">添加证书</el-button>
+          </div>
+        </el-form-item>
+        <el-form-item label="状态" prop="status">
+          <el-radio-group v-model="form.status">
+            <el-radio label="0">正常</el-radio>
+            <el-radio label="1">停用</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="备注" prop="remark">
+          <el-input v-model="form.remark" type="textarea" :rows="2" placeholder="请输入备注" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button type="primary" @click="submitForm">确 定</el-button>
+          <el-button @click="cancel">取 消</el-button>
+        </div>
+      </template>
+    </el-dialog>
+
+    <!-- 证书查看弹窗 -->
+    <el-dialog v-model="certOpen" title="岗位证书" width="600px" append-to-body>
+      <div v-if="currentCerts.length > 0">
+        <div v-for="(cert, index) in currentCerts" :key="index" class="cert-item mb-4 p-4 border rounded">
+          <p class="font-bold mb-2">证书类型:{{ cert.certTypeName || cert.certType }}</p>
+          <el-image v-if="isImage(cert.certUrl)" :src="cert.certUrl" style="max-width: 100%; max-height: 300px;" fit="contain" :preview-src-list="[cert.certUrl]" />
+          <div v-else class="text-center py-4">
+            <el-icon :size="48" color="#409EFF"><Document /></el-icon>
+            <p class="mt-2">
+              <el-link :href="cert.certUrl" target="_blank" type="primary">查看文件</el-link>
+            </p>
+          </div>
+        </div>
+      </div>
+      <el-empty v-else description="暂无证书" />
+    </el-dialog>
+  </div>
+</template>
+
+<script setup name="PersonInfo" lang="ts">
+import { listPersonInfo, getPersonInfo, delPersonInfo, addPersonInfo, updatePersonInfo, getPersonStats } from '@/api/riskctrl/personInfo';
+import { PersonInfoForm, PersonInfoQuery, PersonInfoVO, Certificate } from '@/api/riskctrl/personInfo/types';
+import { listDept } from '@/api/system/dept';
+import { getToken } from '@/utils/auth';
+
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+
+const personList = ref<PersonInfoVO[]>([]);
+const loading = ref(true);
+const showSearch = ref(true);
+const ids = ref<Array<string | number>>([]);
+const single = ref(true);
+const multiple = ref(true);
+const total = ref(0);
+const title = ref('');
+const open = ref(false);
+const certOpen = ref(false);
+
+const queryFormRef = ref<ElFormInstance>();
+const personFormRef = ref<ElFormInstance>();
+
+const statsData = reactive({
+  total: 0,
+  insured: 0,
+  uninsured: 0,
+  expiringSoon: 0
+});
+
+const queryParams = reactive<PersonInfoQuery>({
+  pageNum: 1,
+  pageSize: 10,
+  personName: undefined,
+  personType: undefined,
+  workStoreId: undefined,
+  insuranceStatus: undefined
+});
+
+const form = reactive<PersonInfoForm>({
+  personId: undefined,
+  personType: '',
+  personName: '',
+  idCard: '',
+  age: undefined,
+  workStoreId: undefined,
+  workStoreName: '',
+  insuranceStatus: '0',
+  insuranceExpireDate: undefined,
+  certificates: '[]',
+  status: '0',
+  remark: ''
+});
+
+const certificateList = ref<Certificate[]>([]);
+const currentCerts = ref<Certificate[]>([]);
+
+const deptOptions = ref<any[]>([]);
+const person_type_options = ref<any[]>([]);
+const certificate_type_options = ref<any[]>([]);
+
+const uploadAction = ref(import.meta.env.VITE_APP_BASE_API + '/resource/oss/upload');
+const uploadHeaders = ref({ Authorization: 'Bearer ' + getToken(), clientid: import.meta.env.VITE_APP_CLIENT_ID });
+
+const rules = {
+  personType: [{ required: true, message: '人员类型不能为空', trigger: 'change' }],
+  personName: [{ required: true, message: '姓名不能为空', trigger: 'blur' }],
+  idCard: [
+    { required: true, message: '身份证号不能为空', trigger: 'blur' },
+    { pattern: /^[1-9]\d{5}(18|19|20)\d{2}((0[1-9])|(1[0-2]))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$/, message: '身份证号格式不正确', trigger: 'blur' }
+  ],
+  insuranceStatus: [{ required: true, message: '保险状态不能为空', trigger: 'change' }]
+};
+
+const columns = ref([
+  { key: 0, label: '人员类型', visible: true },
+  { key: 1, label: '姓名', visible: true },
+  { key: 2, label: '年龄', visible: true },
+  { key: 3, label: '身份证号', visible: true },
+  { key: 4, label: '工作门店', visible: true },
+  { key: 5, label: '保险状态', visible: true },
+  { key: 6, label: '保险有效期', visible: true },
+  { key: 7, label: '岗位证书', visible: true },
+  { key: 8, label: '创建时间', visible: true }
+]);
+
+const loadStats = async () => {
+  try {
+    const res = await getPersonStats();
+    Object.assign(statsData, res.data);
+  } catch { /* ignore */ }
+};
+
+const getList = async () => {
+  loading.value = true;
+  const res = await listPersonInfo(queryParams);
+  personList.value = res.rows;
+  total.value = res.total;
+  loading.value = false;
+};
+
+const getDeptTree = async () => {
+  const res = await listDept();
+  deptOptions.value = res.data;
+};
+
+const getDicts = async () => {
+  const ptRes = await proxy?.getDicts('person_type');
+  person_type_options.value = ptRes.data || [];
+  const ctRes = await proxy?.getDicts('certificate_type');
+  certificate_type_options.value = ctRes.data || [];
+};
+
+const cancel = () => {
+  open.value = false;
+  reset();
+};
+
+const reset = () => {
+  form.personId = undefined;
+  form.personType = '';
+  form.personName = '';
+  form.idCard = '';
+  form.age = undefined;
+  form.workStoreId = undefined;
+  form.workStoreName = '';
+  form.insuranceStatus = '0';
+  form.insuranceExpireDate = undefined;
+  form.certificates = '[]';
+  form.status = '0';
+  form.remark = '';
+  certificateList.value = [];
+  nextTick(() => {
+    personFormRef.value?.resetFields();
+  });
+};
+
+const handleQuery = () => {
+  queryParams.pageNum = 1;
+  getList();
+};
+
+const resetQuery = () => {
+  queryFormRef.value?.resetFields();
+  handleQuery();
+};
+
+const handleSelectionChange = (selection: PersonInfoVO[]) => {
+  ids.value = selection.map((item) => item.personId);
+  single.value = selection.length !== 1;
+  multiple.value = !selection.length;
+};
+
+const handleAdd = () => {
+  reset();
+  open.value = true;
+  title.value = '新增人员';
+};
+
+const handleUpdate = async (row?: PersonInfoVO) => {
+  reset();
+  const personId = row?.personId || ids.value[0];
+  const res = await getPersonInfo(personId);
+  Object.assign(form, res.data);
+  try {
+    certificateList.value = JSON.parse(form.certificates || '[]');
+  } catch {
+    certificateList.value = [];
+  }
+  open.value = true;
+  title.value = '修改人员';
+};
+
+const submitForm = () => {
+  personFormRef.value?.validate(async (valid: boolean) => {
+    if (valid) {
+      form.certificates = JSON.stringify(certificateList.value);
+      if (form.workStoreId && deptOptions.value.length > 0) {
+        const dept = findDeptById(deptOptions.value, String(form.workStoreId));
+        if (dept) form.workStoreName = dept.label;
+      }
+      if (form.personId !== undefined) {
+        await updatePersonInfo(form);
+        proxy?.$modal.msgSuccess('修改成功');
+      } else {
+        await addPersonInfo(form);
+        proxy?.$modal.msgSuccess('新增成功');
+      }
+      open.value = false;
+      await getList();
+      await loadStats();
+    }
+  });
+};
+
+const handleDelete = async (row?: PersonInfoVO) => {
+  const personIds = row?.personId || ids.value;
+  await proxy?.$modal.confirm('是否确认删除姓名为"' + (row?.personName || '') + '"的数据项?');
+  await delPersonInfo(personIds);
+  await getList();
+  await loadStats();
+  proxy?.$modal.msgSuccess('删除成功');
+};
+
+const calcAge = () => {
+  if (!form.idCard || form.idCard.length !== 18) return;
+  const year = parseInt(form.idCard.substring(6, 10));
+  const currentYear = new Date().getFullYear();
+  form.age = currentYear - year;
+};
+
+const addCertificate = () => {
+  certificateList.value.push({ certType: '', certUrl: '' });
+};
+
+const removeCertificate = (index: number) => {
+  certificateList.value.splice(index, 1);
+};
+
+const handleCertUploadSuccess = (res: any, index: number) => {
+  if (res.code === 200) {
+    certificateList.value[index].certUrl = res.url || res.fileName;
+    proxy?.$modal.msgSuccess('上传成功');
+  } else {
+    proxy?.$modal.msgError(res.msg || '上传失败');
+  }
+};
+
+const viewCertificates = (row: PersonInfoVO) => {
+  try {
+    currentCerts.value = JSON.parse(row.certificates || '[]');
+    currentCerts.value.forEach((cert: any) => {
+      const dict = certificate_type_options.value.find((d: any) => d.value === cert.certType);
+      if (dict) cert.certTypeName = dict.label;
+    });
+  } catch {
+    currentCerts.value = [];
+  }
+  certOpen.value = true;
+};
+
+const isImage = (url: string) => {
+  return url && /\.(jpg|jpeg|png)$/i.test(url);
+};
+
+const getCertCount = (certs: string) => {
+  try {
+    return JSON.parse(certs || '[]').length;
+  } catch {
+    return 0;
+  }
+};
+
+const maskIdCard = (idCard: string) => {
+  if (!idCard || idCard.length < 8) return idCard;
+  return idCard.substring(0, 4) + '**********' + idCard.substring(idCard.length - 4);
+};
+
+type TagType = 'success' | 'warning' | 'danger' | 'info' | 'primary';
+
+const getPersonTypeType = (type: string): TagType => {
+  const map: Record<string, TagType> = {
+    '防损人员': 'danger',
+    '施工人员': 'warning',
+    '特岗人员': 'primary'
+  };
+  return map[type] || 'info';
+};
+
+const getInsuranceType = (row: PersonInfoVO): TagType => {
+  if (row.insuranceStatus === '0') return 'danger';
+  if (isExpiringSoon(row.insuranceExpireDate)) return 'warning';
+  return 'success';
+};
+
+const getInsuranceLabel = (row: PersonInfoVO): string => {
+  if (row.insuranceStatus === '0') return '未投保';
+  if (isExpiringSoon(row.insuranceExpireDate)) return '即将到期';
+  return '已投保';
+};
+
+const isExpiringSoon = (dateStr?: string) => {
+  if (!dateStr) return false;
+  const expire = new Date(dateStr);
+  const now = new Date();
+  const diff = expire.getTime() - now.getTime();
+  return diff > 0 && diff <= 30 * 24 * 60 * 60 * 1000;
+};
+
+const findDeptById = (depts: any[], id: string): any => {
+  for (const dept of depts) {
+    if (String(dept.id) === id) return dept;
+    if (dept.children) {
+      const found = findDeptById(dept.children, id);
+      if (found) return found;
+    }
+  }
+  return null;
+};
+
+onMounted(() => {
+  getList();
+  loadStats();
+  getDeptTree();
+  getDicts();
+});
+</script>

+ 437 - 0
src/views/riskctrl/publicOpinion/index.vue

@@ -0,0 +1,437 @@
+<template>
+  <div class="p-2">
+    <!-- 顶部数据卡片 -->
+    <el-row :gutter="16" class="mb-[10px]">
+      <el-col :span="6">
+        <el-card shadow="hover">
+          <div class="flex items-center justify-between">
+            <div>
+              <p class="text-gray-500 text-sm">今日舆情总数</p>
+              <p class="text-2xl font-bold text-blue-500">{{ statsData.todayTotal }}</p>
+              <p class="text-xs mt-1">
+                <span :class="parseFloat(statsData.todayGrowthRate) >= 0 ? 'text-red-500' : 'text-green-500'">
+                  较昨日 {{ parseFloat(statsData.todayGrowthRate) >= 0 ? '+' : '' }}{{ statsData.todayGrowthRate }}
+                </span>
+              </p>
+            </div>
+            <el-icon :size="40" color="#409EFF"><DataAnalysis /></el-icon>
+          </div>
+        </el-card>
+      </el-col>
+      <el-col :span="6">
+        <el-card shadow="hover">
+          <div class="flex items-center justify-between">
+            <div>
+              <p class="text-gray-500 text-sm">重大舆情数</p>
+              <p class="text-2xl font-bold text-red-500">{{ statsData.majorCount }}</p>
+              <p class="text-xs text-gray-400 mt-1">较昨日 {{ statsData.majorChange }}</p>
+            </div>
+            <el-icon :size="40" color="#F56C6C"><WarningFilled /></el-icon>
+          </div>
+        </el-card>
+      </el-col>
+      <el-col :span="6">
+        <el-card shadow="hover">
+          <div class="flex items-center justify-between">
+            <div>
+              <p class="text-gray-500 text-sm">已处理</p>
+              <p class="text-2xl font-bold text-green-500">{{ statsData.processedCount }}</p>
+              <p class="text-xs text-gray-400 mt-1">处理率 {{ statsData.processRate }}</p>
+            </div>
+            <el-icon :size="40" color="#67C23A"><CircleCheckFilled /></el-icon>
+          </div>
+        </el-card>
+      </el-col>
+      <el-col :span="6">
+        <el-card shadow="hover">
+          <div class="flex items-center justify-between">
+            <div>
+              <p class="text-gray-500 text-sm">待处理</p>
+              <p class="text-2xl font-bold text-orange-500">{{ statsData.pendingCount }}</p>
+              <p class="text-xs text-red-400 mt-1">需要紧急处理</p>
+            </div>
+            <el-icon :size="40" color="#E6A23C"><Clock /></el-icon>
+          </div>
+        </el-card>
+      </el-col>
+    </el-row>
+
+    <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
+      <div v-show="showSearch" class="mb-[10px]">
+        <el-card shadow="hover">
+          <el-form ref="queryFormRef" :model="queryParams" :inline="true">
+            <el-form-item label="舆情标题" prop="opinionTitle">
+              <el-input v-model="queryParams.opinionTitle" placeholder="请输入标题" clearable @keyup.enter="handleQuery" />
+            </el-form-item>
+            <el-form-item label="来源" prop="source">
+              <el-select v-model="queryParams.source" placeholder="来源" clearable>
+                <el-option label="微博" value="微博" />
+                <el-option label="微信" value="微信" />
+                <el-option label="新闻网站" value="新闻网站" />
+                <el-option label="论坛" value="论坛" />
+                <el-option label="短视频" value="短视频" />
+                <el-option label="其他" value="其他" />
+              </el-select>
+            </el-form-item>
+            <el-form-item label="风险等级" prop="riskLevel">
+              <el-select v-model="queryParams.riskLevel" placeholder="等级" clearable>
+                <el-option label="重大" value="重大" />
+                <el-option label="较大" value="较大" />
+                <el-option label="一般" value="一般" />
+                <el-option label="低" value="低" />
+              </el-select>
+            </el-form-item>
+            <el-form-item label="状态" prop="status">
+              <el-select v-model="queryParams.status" placeholder="状态" clearable>
+                <el-option label="待处理" value="待处理" />
+                <el-option label="处理中" value="处理中" />
+                <el-option label="已处理" value="已处理" />
+              </el-select>
+            </el-form-item>
+            <el-form-item>
+              <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
+              <el-button icon="Refresh" @click="resetQuery">重置</el-button>
+            </el-form-item>
+          </el-form>
+        </el-card>
+      </div>
+    </transition>
+
+    <el-card shadow="hover">
+      <template #header>
+        <el-row :gutter="10">
+          <el-col :span="1.5">
+            <el-button v-has-permi="['riskctrl:publicOpinion:add']" type="primary" plain icon="Plus" @click="handleAdd()">新增舆情</el-button>
+          </el-col>
+          <el-col :span="1.5">
+            <el-button v-has-permi="['riskctrl:publicOpinion:edit']" type="success" plain :disabled="single" icon="Edit" @click="handleUpdate()">修改</el-button>
+          </el-col>
+          <el-col :span="1.5">
+            <el-button v-has-permi="['riskctrl:publicOpinion:remove']" type="danger" plain :disabled="multiple" icon="Delete" @click="handleDelete()">删除</el-button>
+          </el-col>
+          <right-toolbar v-model:show-search="showSearch" :columns="columns" :search="true" @query-table="getList"></right-toolbar>
+        </el-row>
+      </template>
+
+      <el-table v-loading="loading" border :data="opinionList" @selection-change="handleSelectionChange">
+        <el-table-column type="selection" width="50" align="center" />
+        <el-table-column v-if="columns[0].visible" key="opinionTitle" label="舆情标题" align="center" prop="opinionTitle" :show-overflow-tooltip="true" />
+        <el-table-column v-if="columns[1].visible" key="source" label="来源" align="center" prop="source" width="100" />
+        <el-table-column v-if="columns[2].visible" key="publishTime" label="发布时间" align="center" prop="publishTime" width="160" />
+        <el-table-column v-if="columns[3].visible" key="riskLevel" label="风险等级" align="center" prop="riskLevel" width="100">
+          <template #default="scope">
+            <el-tag :type="getLevelType(scope.row.riskLevel)">{{ scope.row.riskLevel }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column v-if="columns[4].visible" key="responsiblePerson" label="负责人" align="center" prop="responsiblePerson" width="100" />
+        <el-table-column v-if="columns[5].visible" key="status" label="状态" align="center" prop="status" width="100">
+          <template #default="scope">
+            <el-tag :type="getStatusType(scope.row.status)">{{ scope.row.status }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column v-if="columns[6].visible" label="创建时间" align="center" prop="createTime" width="160" />
+        <el-table-column label="操作" fixed="right" width="220" class-name="small-padding fixed-width">
+          <template #default="scope">
+            <el-tooltip content="修改" placement="top">
+              <el-button v-has-permi="['riskctrl:publicOpinion:edit']" link type="primary" icon="Edit" @click="handleUpdate(scope.row)"></el-button>
+            </el-tooltip>
+            <el-tooltip content="处理" placement="top">
+              <el-button v-has-permi="['riskctrl:publicOpinion:handle']" link type="warning" icon="Operation" @click="handleDisposal(scope.row)"></el-button>
+            </el-tooltip>
+            <el-tooltip content="删除" placement="top">
+              <el-button v-has-permi="['riskctrl:publicOpinion:remove']" link type="primary" icon="Delete" @click="handleDelete(scope.row)"></el-button>
+            </el-tooltip>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <pagination v-show="total > 0" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" :total="total" @pagination="getList" />
+    </el-card>
+
+    <!-- 新增/修改弹窗 -->
+    <el-dialog v-model="open" :title="title" width="750px" append-to-body>
+      <el-form ref="opinionFormRef" :model="form" :rules="rules" label-width="100px">
+        <el-form-item label="舆情标题" prop="opinionTitle">
+          <el-input v-model="form.opinionTitle" placeholder="请输入舆情标题" />
+        </el-form-item>
+        <el-row :gutter="20">
+          <el-col :span="8">
+            <el-form-item label="来源" prop="source">
+              <el-select v-model="form.source" placeholder="请选择" class="w-full">
+                <el-option label="微博" value="微博" />
+                <el-option label="微信" value="微信" />
+                <el-option label="新闻网站" value="新闻网站" />
+                <el-option label="短视频" value="短视频" />
+                <el-option label="论坛" value="论坛" />
+                <el-option label="其他" value="其他" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <el-col :span="8">
+            <el-form-item label="发布时间" prop="publishTime">
+              <el-date-picker v-model="form.publishTime" type="datetime" placeholder="选择时间" value-format="YYYY-MM-DD HH:mm:ss" class="w-full" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="8">
+            <el-form-item label="风险等级" prop="riskLevel">
+              <el-select v-model="form.riskLevel" placeholder="请选择" class="w-full">
+                <el-option label="重大" value="重大" />
+                <el-option label="较大" value="较大" />
+                <el-option label="一般" value="一般" />
+                <el-option label="低" value="低" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-form-item label="负责人" prop="responsiblePerson">
+          <el-input v-model="form.responsiblePerson" placeholder="请输入负责人" />
+        </el-form-item>
+        <el-form-item label="舆情链接" prop="opinionLink">
+          <el-input v-model="form.opinionLink" placeholder="请输入舆情外部链接" />
+        </el-form-item>
+        <el-form-item label="舆情内容" prop="opinionContent">
+          <el-input v-model="form.opinionContent" type="textarea" :rows="4" placeholder="请输入舆情内容" />
+        </el-form-item>
+        <el-form-item label="处置建议" prop="disposalSuggestion">
+          <el-input v-model="form.disposalSuggestion" type="textarea" :rows="3" placeholder="请输入处置建议" />
+        </el-form-item>
+        <el-form-item label="状态" prop="status">
+          <el-radio-group v-model="form.status">
+            <el-radio v-for="dict in sys_normal_disable" :key="dict.value" :label="dict.value">{{ dict.label }}</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="备注" prop="remark">
+          <el-input v-model="form.remark" type="textarea" placeholder="请输入备注" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button type="primary" @click="submitForm">确 定</el-button>
+          <el-button @click="cancel">取 消</el-button>
+        </div>
+      </template>
+    </el-dialog>
+
+    <!-- 处理舆情弹窗 -->
+    <el-dialog v-model="disposalOpen" title="处理舆情" width="600px" append-to-body>
+      <el-form ref="disposalFormRef" :model="disposalForm" :rules="disposalRules" label-width="100px">
+        <el-form-item label="处理状态" prop="status">
+          <el-select v-model="disposalForm.status" placeholder="请选择" class="w-full">
+            <el-option label="处理中" value="处理中" />
+            <el-option label="已处理" value="已处理" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="处理结果" prop="disposalResult">
+          <el-input v-model="disposalForm.disposalResult" type="textarea" :rows="4" placeholder="请输入处理结果" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button type="primary" @click="submitDisposal">确 定</el-button>
+          <el-button @click="disposalOpen = false">取 消</el-button>
+        </div>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup name="PublicOpinion" lang="ts">
+import { listPublicOpinion, getPublicOpinion, delPublicOpinion, addPublicOpinion, updatePublicOpinion, handlePublicOpinion, getOpinionStats } from '@/api/riskctrl/publicOpinion';
+import { PublicOpinionForm, PublicOpinionQuery, PublicOpinionVO, OpinionStats } from '@/api/riskctrl/publicOpinion/types';
+
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+const { sys_normal_disable } = proxy.useDict('sys_normal_disable');
+
+const opinionList = ref<PublicOpinionVO[]>([]);
+const loading = ref(true);
+const showSearch = ref(true);
+const ids = ref<Array<string | number>>([]);
+const single = ref(true);
+const multiple = ref(true);
+const total = ref(0);
+const title = ref('');
+const open = ref(false);
+const disposalOpen = ref(false);
+
+const queryFormRef = ref<ElFormInstance>();
+const opinionFormRef = ref<ElFormInstance>();
+const disposalFormRef = ref<ElFormInstance>();
+
+const statsData = reactive<OpinionStats>({
+  todayTotal: 0,
+  todayGrowthRate: '0%',
+  majorCount: 0,
+  majorChange: '0',
+  processedCount: 0,
+  processRate: '0%',
+  pendingCount: 0
+});
+
+const queryParams = reactive<PublicOpinionQuery>({
+  pageNum: 1,
+  pageSize: 10,
+  opinionTitle: undefined,
+  source: undefined,
+  riskLevel: undefined,
+  status: undefined
+});
+
+const form = reactive<PublicOpinionForm>({
+  opinionId: undefined,
+  opinionTitle: '',
+  source: '',
+  publishTime: '',
+  riskLevel: '一般',
+  responsiblePerson: '',
+  opinionLink: '',
+  opinionContent: '',
+  disposalSuggestion: '',
+  status: '0',
+  remark: ''
+});
+
+const disposalForm = reactive({
+  opinionId: undefined as string | number | undefined,
+  status: '',
+  disposalResult: '',
+  disposalTime: ''
+});
+
+const rules = {
+  opinionTitle: [{ required: true, message: '舆情标题不能为空', trigger: 'blur' }],
+  source: [{ required: true, message: '来源不能为空', trigger: 'change' }],
+  riskLevel: [{ required: true, message: '风险等级不能为空', trigger: 'change' }]
+};
+
+const disposalRules = {
+  status: [{ required: true, message: '处理状态不能为空', trigger: 'change' }]
+};
+
+const columns = ref([
+  { key: 0, label: '舆情标题', visible: true },
+  { key: 1, label: '来源', visible: true },
+  { key: 2, label: '发布时间', visible: true },
+  { key: 3, label: '风险等级', visible: true },
+  { key: 4, label: '负责人', visible: true },
+  { key: 5, label: '状态', visible: true },
+  { key: 6, label: '创建时间', visible: true }
+]);
+
+type TagType = 'success' | 'warning' | 'danger' | 'info' | 'primary';
+
+const getLevelType = (level: string): TagType => {
+  const map: Record<string, TagType> = { '重大': 'danger', '较大': 'warning', '一般': 'primary', '低': 'info' };
+  return map[level] || 'info';
+};
+
+const getStatusType = (status: string): TagType => {
+  const map: Record<string, TagType> = { '待处理': 'danger', '处理中': 'warning', '已处理': 'success' };
+  return map[status] || 'info';
+};
+
+const loadStats = async () => {
+  try {
+    const res = await getOpinionStats();
+    Object.assign(statsData, res.data);
+  } catch { /* ignore */ }
+};
+
+const getList = async () => {
+  loading.value = true;
+  const res = await listPublicOpinion(queryParams);
+  opinionList.value = res.rows;
+  total.value = res.total;
+  loading.value = false;
+};
+
+const cancel = () => {
+  open.value = false;
+  reset();
+};
+
+const reset = () => {
+  form.opinionId = undefined;
+  form.opinionTitle = '';
+  form.source = '';
+  form.publishTime = '';
+  form.riskLevel = '一般';
+  form.responsiblePerson = '';
+  form.opinionLink = '';
+  form.opinionContent = '';
+  form.disposalSuggestion = '';
+  form.status = '0';
+  form.remark = '';
+  nextTick(() => { opinionFormRef.value?.resetFields(); });
+};
+
+const handleQuery = () => { queryParams.pageNum = 1; getList(); };
+const resetQuery = () => { queryFormRef.value?.resetFields(); handleQuery(); };
+
+const handleSelectionChange = (selection: PublicOpinionVO[]) => {
+  ids.value = selection.map((item) => item.opinionId);
+  single.value = selection.length !== 1;
+  multiple.value = !selection.length;
+};
+
+const handleAdd = () => { reset(); open.value = true; title.value = '新增舆情'; };
+
+const handleUpdate = async (row?: PublicOpinionVO) => {
+  reset();
+  const opinionId = row?.opinionId || ids.value[0];
+  const res = await getPublicOpinion(opinionId);
+  Object.assign(form, res.data);
+  open.value = true;
+  title.value = '修改舆情';
+};
+
+const submitForm = () => {
+  opinionFormRef.value?.validate(async (valid: boolean) => {
+    if (valid) {
+      if (form.opinionId !== undefined) {
+        await updatePublicOpinion(form);
+        proxy?.$modal.msgSuccess('修改成功');
+      } else {
+        await addPublicOpinion(form);
+        proxy?.$modal.msgSuccess('新增成功');
+      }
+      open.value = false;
+      await getList();
+      await loadStats();
+    }
+  });
+};
+
+const handleDisposal = async (row: PublicOpinionVO) => {
+  disposalForm.opinionId = row.opinionId;
+  disposalForm.status = row.status === '待处理' ? '处理中' : row.status;
+  disposalForm.disposalResult = row.disposalResult || '';
+  disposalOpen.value = true;
+};
+
+const submitDisposal = () => {
+  disposalFormRef.value?.validate(async (valid: boolean) => {
+    if (valid) {
+      disposalForm.disposalTime = new Date().toISOString();
+      await handlePublicOpinion(disposalForm as any);
+      proxy?.$modal.msgSuccess('处理成功');
+      disposalOpen.value = false;
+      await getList();
+      await loadStats();
+    }
+  });
+};
+
+const handleDelete = async (row?: PublicOpinionVO) => {
+  const opinionIds = row?.opinionId || ids.value;
+  await proxy?.$modal.confirm('是否确认删除该舆情信息?');
+  await delPublicOpinion(opinionIds);
+  await getList();
+  await loadStats();
+  proxy?.$modal.msgSuccess('删除成功');
+};
+
+onMounted(() => {
+  loadStats();
+  getList();
+});
+</script>

+ 341 - 0
src/views/riskctrl/riskWarning/index.vue

@@ -0,0 +1,341 @@
+<template>
+  <div class="p-2">
+    <!-- 统计卡片 -->
+    <el-row :gutter="16" class="mb-[10px]">
+      <el-col :span="6">
+        <el-card shadow="hover">
+          <div class="flex items-center justify-between">
+            <div>
+              <p class="text-gray-500 text-sm">警示总数</p>
+              <p class="text-2xl font-bold text-blue-500">{{ statsData.total }}</p>
+            </div>
+            <el-icon :size="40" color="#409EFF"><WarningFilled /></el-icon>
+          </div>
+        </el-card>
+      </el-col>
+      <el-col :span="6">
+        <el-card shadow="hover">
+          <div class="flex items-center justify-between">
+            <div>
+              <p class="text-gray-500 text-sm">重大风险</p>
+              <p class="text-2xl font-bold text-red-500">{{ statsData.highLevel }}</p>
+            </div>
+            <el-icon :size="40" color="#F56C6C"><TriangleFilled /></el-icon>
+          </div>
+        </el-card>
+      </el-col>
+      <el-col :span="6">
+        <el-card shadow="hover">
+          <div class="flex items-center justify-between">
+            <div>
+              <p class="text-gray-500 text-sm">较大风险</p>
+              <p class="text-2xl font-bold text-orange-500">{{ statsData.midLevel }}</p>
+            </div>
+            <el-icon :size="40" color="#E6A23C"><WarningFilled /></el-icon>
+          </div>
+        </el-card>
+      </el-col>
+      <el-col :span="6">
+        <el-card shadow="hover">
+          <div class="flex items-center justify-between">
+            <div>
+              <p class="text-gray-500 text-sm">一般风险</p>
+              <p class="text-2xl font-bold text-blue-500">{{ statsData.lowLevel }}</p>
+            </div>
+            <el-icon :size="40" color="#409EFF"><InfoFilled /></el-icon>
+          </div>
+        </el-card>
+      </el-col>
+    </el-row>
+
+    <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
+      <div v-show="showSearch" class="mb-[10px]">
+        <el-card shadow="hover">
+          <el-form ref="queryFormRef" :model="queryParams" :inline="true">
+            <el-form-item label="警示类型" prop="warningType">
+              <el-select v-model="queryParams.warningType" placeholder="类型" clearable>
+                <el-option label="安全警示" value="安全警示" />
+                <el-option label="食安警示" value="食安警示" />
+                <el-option label="社会警示" value="社会警示" />
+              </el-select>
+            </el-form-item>
+            <el-form-item label="警示等级" prop="warningLevel">
+              <el-select v-model="queryParams.warningLevel" placeholder="等级" clearable>
+                <el-option label="重大风险" value="重大风险" />
+                <el-option label="较大风险" value="较大风险" />
+                <el-option label="一般风险" value="一般风险" />
+              </el-select>
+            </el-form-item>
+            <el-form-item label="发布时间" prop="publishTime">
+              <el-date-picker v-model="queryParams.publishTime" type="daterange" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期" value-format="YYYY-MM-DD" />
+            </el-form-item>
+            <el-form-item label="关键词" prop="warningTitle">
+              <el-input v-model="queryParams.warningTitle" placeholder="警示标题模糊搜索" clearable @keyup.enter="handleQuery" />
+            </el-form-item>
+            <el-form-item>
+              <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
+              <el-button icon="Refresh" @click="resetQuery">重置</el-button>
+            </el-form-item>
+          </el-form>
+        </el-card>
+      </div>
+    </transition>
+
+    <el-card shadow="hover">
+      <template #header>
+        <el-row :gutter="10">
+          <el-col :span="1.5">
+            <el-button v-has-permi="['riskctrl:riskWarning:add']" type="primary" plain icon="Plus" @click="handleAdd()">发布警示</el-button>
+          </el-col>
+          <el-col :span="1.5">
+            <el-button v-has-permi="['riskctrl:riskWarning:remove']" type="danger" plain :disabled="multiple" icon="Delete" @click="handleDelete()">删除</el-button>
+          </el-col>
+          <right-toolbar v-model:show-search="showSearch" :columns="columns" :search="true" @query-table="getList"></right-toolbar>
+        </el-row>
+      </template>
+
+      <el-table v-loading="loading" border :data="warningList" @selection-change="handleSelectionChange">
+        <el-table-column type="selection" width="50" align="center" />
+        <el-table-column v-if="columns[0].visible" key="warningTitle" label="警示标题" align="center" prop="warningTitle" :show-overflow-tooltip="true" />
+        <el-table-column v-if="columns[1].visible" key="warningType" label="警示类型" align="center" prop="warningType" width="100" />
+        <el-table-column v-if="columns[2].visible" key="warningLevel" label="警示等级" align="center" prop="warningLevel" width="100">
+          <template #default="scope">
+            <el-tag :type="getLevelType(scope.row.warningLevel)">{{ scope.row.warningLevel }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column v-if="columns[3].visible" key="publishTime" label="发布时间" align="center" prop="publishTime" width="160" />
+        <el-table-column v-if="columns[4].visible" key="publisher" label="发布人" align="center" prop="publisher" width="100" />
+        <el-table-column v-if="columns[5].visible" label="创建时间" align="center" prop="createTime" width="160" />
+        <el-table-column label="操作" fixed="right" width="150" class-name="small-padding fixed-width">
+          <template #default="scope">
+            <el-tooltip content="详情" placement="top">
+              <el-button v-has-permi="['riskctrl:riskWarning:query']" link type="primary" icon="View" @click="handleDetail(scope.row)"></el-button>
+            </el-tooltip>
+            <el-tooltip content="删除" placement="top">
+              <el-button v-has-permi="['riskctrl:riskWarning:remove']" link type="primary" icon="Delete" @click="handleDelete(scope.row)"></el-button>
+            </el-tooltip>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <pagination v-show="total > 0" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" :total="total" @pagination="getList" />
+    </el-card>
+
+    <!-- 发布警示弹窗 -->
+    <el-dialog v-model="open" title="发布警示" width="700px" append-to-body>
+      <el-form ref="warningFormRef" :model="form" :rules="rules" label-width="100px">
+        <el-form-item label="警示标题" prop="warningTitle">
+          <el-input v-model="form.warningTitle" placeholder="请输入警示标题(2-50字符)" maxlength="50" show-word-limit />
+        </el-form-item>
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="警示类型" prop="warningType">
+              <el-select v-model="form.warningType" placeholder="请选择" class="w-full">
+                <el-option label="安全警示" value="安全警示" />
+                <el-option label="食安警示" value="食安警示" />
+                <el-option label="社会警示" value="社会警示" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="警示等级" prop="warningLevel">
+              <el-select v-model="form.warningLevel" placeholder="请选择" class="w-full">
+                <el-option label="重大风险" value="重大风险" />
+                <el-option label="较大风险" value="较大风险" />
+                <el-option label="一般风险" value="一般风险" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-form-item label="警示内容" prop="warningContent">
+          <el-input v-model="form.warningContent" type="textarea" :rows="6" placeholder="输入详细的风险说明、影响范围、防范措施等内容" />
+        </el-form-item>
+        <el-form-item label="生效时间" prop="effectiveTime">
+          <el-date-picker v-model="form.effectiveTime" type="datetime" placeholder="选择生效时间" value-format="YYYY-MM-DD HH:mm:ss" class="w-full" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button type="primary" @click="submitForm">发 布</el-button>
+          <el-button @click="cancel">取 消</el-button>
+        </div>
+      </template>
+    </el-dialog>
+
+    <!-- 详情弹窗 -->
+    <el-dialog v-model="detailOpen" title="警示详情" width="700px" append-to-body>
+      <el-descriptions :column="2" border>
+        <el-descriptions-item label="警示标题">{{ detailData.warningTitle }}</el-descriptions-item>
+        <el-descriptions-item label="警示类型">{{ detailData.warningType }}</el-descriptions-item>
+        <el-descriptions-item label="警示等级">
+          <el-tag :type="getLevelType(detailData.warningLevel)">{{ detailData.warningLevel }}</el-tag>
+        </el-descriptions-item>
+        <el-descriptions-item label="发布人">{{ detailData.publisher }}</el-descriptions-item>
+        <el-descriptions-item label="发布时间">{{ detailData.publishTime }}</el-descriptions-item>
+        <el-descriptions-item label="生效时间">{{ detailData.effectiveTime }}</el-descriptions-item>
+        <el-descriptions-item label="警示内容" :span="2">{{ detailData.warningContent }}</el-descriptions-item>
+      </el-descriptions>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button @click="detailOpen = false">关 闭</el-button>
+        </div>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup name="RiskWarning" lang="ts">
+import { listRiskWarning, getRiskWarning, delRiskWarning, addRiskWarning } from '@/api/riskctrl/riskWarning';
+import { RiskWarningForm, RiskWarningQuery, RiskWarningVO } from '@/api/riskctrl/riskWarning/types';
+
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+
+const warningList = ref<RiskWarningVO[]>([]);
+const loading = ref(true);
+const showSearch = ref(true);
+const ids = ref<Array<string | number>>([]);
+const single = ref(true);
+const multiple = ref(true);
+const total = ref(0);
+const open = ref(false);
+const detailOpen = ref(false);
+
+// 统计数据
+const statsData = reactive({
+  total: 0,
+  highLevel: 0,
+  midLevel: 0,
+  lowLevel: 0
+});
+
+const queryFormRef = ref<ElFormInstance>();
+const warningFormRef = ref<ElFormInstance>();
+
+const detailData = reactive<RiskWarningVO>({} as RiskWarningVO);
+
+const queryParams = reactive<RiskWarningQuery>({
+  pageNum: 1,
+  pageSize: 10,
+  warningTitle: undefined,
+  warningType: undefined,
+  warningLevel: undefined,
+  publishTime: undefined
+});
+
+const form = reactive<RiskWarningForm>({
+  warningId: undefined,
+  warningTitle: '',
+  warningType: '',
+  warningLevel: '',
+  warningContent: '',
+  effectiveTime: '',
+  status: '0',
+  remark: ''
+});
+
+const rules = {
+  warningTitle: [
+    { required: true, message: '警示标题不能为空', trigger: 'blur' },
+    { min: 2, max: 50, message: '长度在2-50个字符之间', trigger: 'blur' }
+  ],
+  warningType: [{ required: true, message: '警示类型不能为空', trigger: 'change' }],
+  warningLevel: [{ required: true, message: '警示等级不能为空', trigger: 'change' }]
+};
+
+const columns = ref([
+  { key: 0, label: '警示标题', visible: true },
+  { key: 1, label: '警示类型', visible: true },
+  { key: 2, label: '警示等级', visible: true },
+  { key: 3, label: '发布时间', visible: true },
+  { key: 4, label: '发布人', visible: true },
+  { key: 5, label: '创建时间', visible: true }
+]);
+
+type TagType = 'success' | 'warning' | 'danger' | 'info' | 'primary';
+
+const getLevelType = (level: string): TagType => {
+  const map: Record<string, TagType> = {
+    '重大风险': 'danger',
+    '较大风险': 'warning',
+    '一般风险': 'primary'
+  };
+  return map[level] || 'info';
+};
+
+const getList = async () => {
+  loading.value = true;
+  const res = await listRiskWarning(queryParams);
+  warningList.value = res.rows;
+  total.value = res.total;
+  loading.value = false;
+};
+
+const cancel = () => {
+  open.value = false;
+  form.warningId = undefined;
+  form.warningTitle = '';
+  form.warningType = '';
+  form.warningLevel = '';
+  form.warningContent = '';
+  form.effectiveTime = '';
+  nextTick(() => { warningFormRef.value?.resetFields(); });
+};
+
+const handleQuery = () => { queryParams.pageNum = 1; getList(); };
+const resetQuery = () => { queryFormRef.value?.resetFields(); handleQuery(); };
+
+const handleSelectionChange = (selection: RiskWarningVO[]) => {
+  ids.value = selection.map((item) => item.warningId);
+  multiple.value = !selection.length;
+};
+
+const handleAdd = () => {
+  cancel();
+  open.value = true;
+};
+
+const submitForm = () => {
+  warningFormRef.value?.validate(async (valid: boolean) => {
+    if (valid) {
+      form.publishTime = new Date().toISOString();
+      await addRiskWarning(form);
+      proxy?.$modal.msgSuccess('发布成功');
+      open.value = false;
+      await getList();
+    }
+  });
+};
+
+const handleDetail = async (row: RiskWarningVO) => {
+  const res = await getRiskWarning(row.warningId);
+  Object.assign(detailData, res.data);
+  detailOpen.value = true;
+};
+
+const handleDelete = async (row?: RiskWarningVO) => {
+  const warningIds = row?.warningId || ids.value;
+  await proxy?.$modal.confirm('是否确认删除该警示信息?');
+  await delRiskWarning(warningIds);
+  await getList();
+  proxy?.$modal.msgSuccess('删除成功');
+};
+
+// 加载统计数据
+const loadStats = async () => {
+  try {
+    const res = await listRiskWarning({ pageNum: 1, pageSize: 9999 } as any);
+    const allData = (res as any).rows || [];
+    statsData.total = allData.length;
+    statsData.highLevel = allData.filter((d: any) => d.warningLevel === '重大风险').length;
+    statsData.midLevel = allData.filter((d: any) => d.warningLevel === '较大风险').length;
+    statsData.lowLevel = allData.filter((d: any) => d.warningLevel === '一般风险').length;
+  } catch (e) {
+    console.error('加载统计数据失败', e);
+  }
+};
+
+onMounted(() => {
+  getList();
+  loadStats();
+});
+</script>

+ 329 - 0
src/views/riskctrl/rule/index.vue

@@ -0,0 +1,329 @@
+<template>
+  <div class="p-2">
+    <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
+      <div v-show="showSearch" class="mb-[10px]">
+        <el-card shadow="hover">
+          <el-form ref="queryFormRef" :model="queryParams" :inline="true">
+            <el-form-item label="规则名称" prop="ruleName">
+              <el-input v-model="queryParams.ruleName" placeholder="请输入规则名称" clearable @keyup.enter="handleQuery" />
+            </el-form-item>
+            <el-form-item label="规则编码" prop="ruleCode">
+              <el-input v-model="queryParams.ruleCode" placeholder="请输入规则编码" clearable @keyup.enter="handleQuery" />
+            </el-form-item>
+            <el-form-item label="规则类型" prop="ruleType">
+              <el-select v-model="queryParams.ruleType" placeholder="规则类型" clearable>
+                <el-option v-for="dict in risk_rule_type" :key="dict.value" :label="dict.label" :value="dict.value" />
+              </el-select>
+            </el-form-item>
+            <el-form-item label="风险等级" prop="riskLevel">
+              <el-select v-model="queryParams.riskLevel" placeholder="风险等级" clearable>
+                <el-option v-for="dict in risk_level" :key="dict.value" :label="dict.label" :value="dict.value" />
+              </el-select>
+            </el-form-item>
+            <el-form-item label="状态" prop="status">
+              <el-select v-model="queryParams.status" placeholder="规则状态" clearable>
+                <el-option v-for="dict in sys_normal_disable" :key="dict.value" :label="dict.label" :value="dict.value" />
+              </el-select>
+            </el-form-item>
+            <el-form-item>
+              <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
+              <el-button icon="Refresh" @click="resetQuery">重置</el-button>
+            </el-form-item>
+          </el-form>
+        </el-card>
+      </div>
+    </transition>
+
+    <el-card shadow="hover">
+      <template #header>
+        <el-row :gutter="10">
+          <el-col :span="1.5">
+            <el-button v-has-permi="['riskctrl:rule:add']" type="primary" plain icon="Plus" @click="handleAdd()">新增</el-button>
+          </el-col>
+          <el-col :span="1.5">
+            <el-button v-has-permi="['riskctrl:rule:edit']" type="success" plain :disabled="single" icon="Edit" @click="handleUpdate()">修改</el-button>
+          </el-col>
+          <el-col :span="1.5">
+            <el-button v-has-permi="['riskctrl:rule:remove']" type="danger" plain :disabled="multiple" icon="Delete" @click="handleDelete()">删除</el-button>
+          </el-col>
+          <right-toolbar v-model:show-search="showSearch" :columns="columns" :search="true" @query-table="getList"></right-toolbar>
+        </el-row>
+      </template>
+
+      <el-table v-loading="loading" border :data="ruleList" @selection-change="handleSelectionChange">
+        <el-table-column type="selection" width="50" align="center" />
+        <el-table-column v-if="columns[0].visible" key="ruleId" label="规则编号" align="center" prop="ruleId" />
+        <el-table-column v-if="columns[1].visible" key="ruleName" label="规则名称" align="center" prop="ruleName" :show-overflow-tooltip="true" />
+        <el-table-column v-if="columns[2].visible" key="ruleCode" label="规则编码" align="center" prop="ruleCode" :show-overflow-tooltip="true" />
+        <el-table-column v-if="columns[3].visible" key="ruleType" label="规则类型" align="center" prop="ruleType">
+          <template #default="scope">
+            <dict-tag :options="risk_rule_type" :value="scope.row.ruleType" />
+          </template>
+        </el-table-column>
+        <el-table-column v-if="columns[4].visible" key="riskLevel" label="风险等级" align="center" prop="riskLevel">
+          <template #default="scope">
+            <dict-tag :options="risk_level" :value="scope.row.riskLevel" />
+          </template>
+        </el-table-column>
+        <el-table-column v-if="columns[5].visible" key="ruleScore" label="规则分值" align="center" prop="ruleScore" />
+        <el-table-column v-if="columns[6].visible" key="actionType" label="触发动作" align="center" prop="actionType">
+          <template #default="scope">
+            <dict-tag :options="risk_action_type" :value="scope.row.actionType" />
+          </template>
+        </el-table-column>
+        <el-table-column v-if="columns[7].visible" key="priority" label="优先级" align="center" prop="priority" />
+        <el-table-column v-if="columns[8].visible" key="status" label="状态" align="center" prop="status">
+          <template #default="scope">
+            <el-switch v-model="scope.row.status" active-value="0" inactive-value="1" @change="handleStatusChange(scope.row)"></el-switch>
+          </template>
+        </el-table-column>
+        <el-table-column v-if="columns[9].visible" label="创建时间" align="center" prop="createTime" width="160" />
+        <el-table-column label="操作" fixed="right" width="180" class-name="small-padding fixed-width">
+          <template #default="scope">
+            <el-tooltip content="修改" placement="top">
+              <el-button v-hasPermi="['riskctrl:rule:edit']" link type="primary" icon="Edit" @click="handleUpdate(scope.row)"></el-button>
+            </el-tooltip>
+            <el-tooltip content="删除" placement="top">
+              <el-button v-hasPermi="['riskctrl:rule:remove']" link type="primary" icon="Delete" @click="handleDelete(scope.row)"></el-button>
+            </el-tooltip>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <pagination v-show="total > 0" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" :total="total" @pagination="getList" />
+    </el-card>
+
+    <!-- 添加或修改风控规则对话框 -->
+    <el-dialog v-model="open" :title="title" width="600px" append-to-body>
+      <el-form ref="ruleFormRef" :model="form" :rules="rules" label-width="100px">
+        <el-form-item label="规则名称" prop="ruleName">
+          <el-input v-model="form.ruleName" placeholder="请输入规则名称" />
+        </el-form-item>
+        <el-form-item label="规则编码" prop="ruleCode">
+          <el-input v-model="form.ruleCode" placeholder="请输入规则编码" />
+        </el-form-item>
+        <el-form-item label="规则类型" prop="ruleType">
+          <el-select v-model="form.ruleType" placeholder="请选择规则类型">
+            <el-option v-for="dict in risk_rule_type" :key="dict.value" :label="dict.label" :value="dict.value" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="风险等级" prop="riskLevel">
+          <el-select v-model="form.riskLevel" placeholder="请选择风险等级">
+            <el-option v-for="dict in risk_level" :key="dict.value" :label="dict.label" :value="dict.value" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="规则分值" prop="ruleScore">
+          <el-input-number v-model="form.ruleScore" :min="0" :max="100" :precision="2" />
+        </el-form-item>
+        <el-form-item label="触发动作" prop="actionType">
+          <el-select v-model="form.actionType" placeholder="请选择触发动作">
+            <el-option v-for="dict in risk_action_type" :key="dict.value" :label="dict.label" :value="dict.value" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="优先级" prop="priority">
+          <el-input-number v-model="form.priority" :min="0" :max="999" />
+        </el-form-item>
+        <el-form-item label="规则描述" prop="ruleDesc">
+          <el-input v-model="form.ruleDesc" type="textarea" placeholder="请输入规则描述" />
+        </el-form-item>
+        <el-form-item label="规则脚本" prop="ruleScript">
+          <el-input v-model="form.ruleScript" type="textarea" placeholder="请输入规则脚本/条件" />
+        </el-form-item>
+        <el-form-item label="状态" prop="status">
+          <el-radio-group v-model="form.status">
+            <el-radio v-for="dict in sys_normal_disable" :key="dict.value" :label="dict.value">{{ dict.label }}</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="备注" prop="remark">
+          <el-input v-model="form.remark" type="textarea" placeholder="请输入备注" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button type="primary" @click="submitForm">确 定</el-button>
+          <el-button @click="cancel">取 消</el-button>
+        </div>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup name="RiskRule" lang="ts">
+import { listRule, getRule, delRule, addRule, updateRule, changeRuleStatus } from '@/api/riskctrl/rule';
+import { RuleForm, RuleQuery, RuleVO } from '@/api/riskctrl/rule/types';
+
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+const { risk_rule_type, risk_level, risk_action_type, sys_normal_disable } = proxy.useDict('risk_rule_type', 'risk_level', 'risk_action_type', 'sys_normal_disable');
+
+const ruleList = ref<RuleVO[]>([]);
+const loading = ref(true);
+const showSearch = ref(true);
+const ids = ref<Array<string | number>>([]);
+const single = ref(true);
+const multiple = ref(true);
+const total = ref(0);
+const title = ref('');
+const open = ref(false);
+
+const queryFormRef = ref<ElFormInstance>();
+const ruleFormRef = ref<ElFormInstance>();
+
+const queryParams = reactive<RuleQuery>({
+  pageNum: 1,
+  pageSize: 10,
+  ruleName: undefined,
+  ruleCode: undefined,
+  ruleType: undefined,
+  riskLevel: undefined,
+  status: undefined
+});
+
+const form = reactive<RuleForm>({
+  ruleId: undefined,
+  ruleName: '',
+  ruleCode: '',
+  ruleType: '',
+  ruleDesc: '',
+  ruleScript: '',
+  riskLevel: '',
+  ruleScore: 0,
+  actionType: '',
+  status: '0',
+  priority: 0,
+  remark: ''
+});
+
+const rules = {
+  ruleName: [{ required: true, message: '规则名称不能为空', trigger: 'blur' }],
+  ruleCode: [{ required: true, message: '规则编码不能为空', trigger: 'blur' }],
+  ruleType: [{ required: true, message: '规则类型不能为空', trigger: 'change' }],
+  riskLevel: [{ required: true, message: '风险等级不能为空', trigger: 'change' }],
+  ruleScore: [{ required: true, message: '规则分值不能为空', trigger: 'blur' }],
+  actionType: [{ required: true, message: '触发动作不能为空', trigger: 'change' }]
+};
+
+const columns = ref([
+  { key: 0, label: `规则编号`, visible: true },
+  { key: 1, label: `规则名称`, visible: true },
+  { key: 2, label: `规则编码`, visible: true },
+  { key: 3, label: `规则类型`, visible: true },
+  { key: 4, label: `风险等级`, visible: true },
+  { key: 5, label: `规则分值`, visible: true },
+  { key: 6, label: `触发动作`, visible: true },
+  { key: 7, label: `优先级`, visible: true },
+  { key: 8, label: `状态`, visible: true },
+  { key: 9, label: `创建时间`, visible: true }
+]);
+
+/** 查询风控规则列表 */
+const getList = async () => {
+  loading.value = true;
+  const res = await listRule(queryParams);
+  ruleList.value = res.rows;
+  total.value = res.total;
+  loading.value = false;
+};
+
+/** 取消按钮 */
+const cancel = () => {
+  open.value = false;
+  reset();
+};
+
+/** 表单重置 */
+const reset = () => {
+  form.ruleId = undefined;
+  form.ruleName = '';
+  form.ruleCode = '';
+  form.ruleType = '';
+  form.ruleDesc = '';
+  form.ruleScript = '';
+  form.riskLevel = '';
+  form.ruleScore = 0;
+  form.actionType = '';
+  form.status = '0';
+  form.priority = 0;
+  form.remark = '';
+  nextTick(() => {
+    ruleFormRef.value?.resetFields();
+  });
+};
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNum = 1;
+  getList();
+};
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value?.resetFields();
+  handleQuery();
+};
+
+/** 多选框选中数据 */
+const handleSelectionChange = (selection: RuleVO[]) => {
+  ids.value = selection.map((item) => item.ruleId);
+  single.value = selection.length !== 1;
+  multiple.value = !selection.length;
+};
+
+/** 新增按钮操作 */
+const handleAdd = () => {
+  reset();
+  open.value = true;
+  title.value = '添加风控规则';
+};
+
+/** 修改按钮操作 */
+const handleUpdate = async (row?: RuleVO) => {
+  reset();
+  const ruleId = row?.ruleId || ids.value[0];
+  const res = await getRule(ruleId);
+  Object.assign(form, res.data);
+  open.value = true;
+  title.value = '修改风控规则';
+};
+
+/** 提交按钮 */
+const submitForm = () => {
+  ruleFormRef.value?.validate(async (valid: boolean) => {
+    if (valid) {
+      if (form.ruleId !== undefined) {
+        await updateRule(form);
+        proxy?.$modal.msgSuccess('修改成功');
+      } else {
+        await addRule(form);
+        proxy?.$modal.msgSuccess('新增成功');
+      }
+      open.value = false;
+      await getList();
+    }
+  });
+};
+
+/** 删除按钮操作 */
+const handleDelete = async (row?: RuleVO) => {
+  const ruleIds = row?.ruleId || ids.value;
+  await proxy?.$modal.confirm('是否确认删除编号为"' + ruleIds + '"的数据项?');
+  await delRule(ruleIds);
+  await getList();
+  proxy?.$modal.msgSuccess('删除成功');
+};
+
+/** 状态修改 */
+const handleStatusChange = async (row: RuleVO) => {
+  const text = row.status === '0' ? '启用' : '停用';
+  try {
+    await proxy?.$modal.confirm('确认要"' + text + '""' + row.ruleName + '"规则吗?');
+    await changeRuleStatus(row.ruleId, row.status);
+    proxy?.$modal.msgSuccess(text + '成功');
+  } catch {
+    row.status = row.status === '0' ? '1' : '0';
+  }
+};
+
+onMounted(() => {
+  getList();
+});
+</script>

+ 435 - 0
src/views/riskctrl/safetyTraining/index.vue

@@ -0,0 +1,435 @@
+<template>
+  <div class="p-2">
+    <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
+      <div v-show="showSearch" class="mb-[10px]">
+        <el-card shadow="hover">
+          <el-form ref="queryFormRef" :model="queryParams" :inline="true">
+            <el-form-item label="培训主题" prop="trainingTitle">
+              <el-input v-model="queryParams.trainingTitle" placeholder="请输入培训主题" clearable @keyup.enter="handleQuery" />
+            </el-form-item>
+            <el-form-item label="培训类型" prop="trainingType">
+              <el-select v-model="queryParams.trainingType" placeholder="请选择类型" clearable>
+                <el-option label="安全知识" value="安全知识" />
+                <el-option label="操作规范" value="操作规范" />
+                <el-option label="应急预案" value="应急预案" />
+                <el-option label="设备维护" value="设备维护" />
+              </el-select>
+            </el-form-item>
+            <el-form-item label="培训状态" prop="trainingStatus">
+              <el-select v-model="queryParams.trainingStatus" placeholder="请选择状态" clearable>
+                <el-option label="未开始" value="未开始" />
+                <el-option label="进行中" value="进行中" />
+                <el-option label="已结束" value="已结束" />
+              </el-select>
+            </el-form-item>
+            <el-form-item label="培训时间" prop="trainingTime">
+              <el-date-picker v-model="queryParams.trainingTime" type="date" placeholder="选择日期" value-format="YYYY-MM-DD" />
+            </el-form-item>
+            <el-form-item>
+              <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
+              <el-button icon="Refresh" @click="resetQuery">重置</el-button>
+            </el-form-item>
+          </el-form>
+        </el-card>
+      </div>
+    </transition>
+
+    <el-card shadow="hover">
+      <template #header>
+        <el-row :gutter="10">
+          <el-col :span="1.5">
+            <el-button v-has-permi="['riskctrl:safetyTraining:add']" type="primary" plain icon="Plus" @click="handleAdd()">新增</el-button>
+          </el-col>
+          <el-col :span="1.5">
+            <el-button v-has-permi="['riskctrl:safetyTraining:edit']" type="success" plain :disabled="single" icon="Edit" @click="handleUpdate()">修改</el-button>
+          </el-col>
+          <el-col :span="1.5">
+            <el-button v-has-permi="['riskctrl:safetyTraining:remove']" type="danger" plain :disabled="multiple" icon="Delete" @click="handleDelete()">删除</el-button>
+          </el-col>
+          <el-col :span="1.5">
+            <el-button v-has-permi="['riskctrl:safetyTraining:export']" type="warning" plain icon="Download" @click="handleExport()">导出</el-button>
+          </el-col>
+          <right-toolbar v-model:show-search="showSearch" :columns="columns" :search="true" @query-table="getList"></right-toolbar>
+        </el-row>
+      </template>
+
+      <el-table v-loading="loading" border :data="trainingList" @selection-change="handleSelectionChange">
+        <el-table-column type="selection" width="50" align="center" />
+        <el-table-column v-if="columns[0].visible" key="trainingNo" label="培训编号" align="center" prop="trainingNo" width="140" />
+        <el-table-column v-if="columns[1].visible" key="trainingTitle" label="培训主题" align="center" prop="trainingTitle" :show-overflow-tooltip="true" />
+        <el-table-column v-if="columns[2].visible" key="trainingType" label="培训类型" align="center" prop="trainingType" width="120" />
+        <el-table-column v-if="columns[3].visible" key="trainingTime" label="培训时间" align="center" prop="trainingTime" width="160" />
+        <el-table-column v-if="columns[4].visible" key="trainingLocation" label="培训地点" align="center" prop="trainingLocation" :show-overflow-tooltip="true" />
+        <el-table-column v-if="columns[5].visible" key="trainer" label="培训师" align="center" prop="trainer" width="100" />
+        <el-table-column v-if="columns[6].visible" key="participantCount" label="参与人数" align="center" prop="participantCount" width="100" />
+        <el-table-column v-if="columns[7].visible" key="trainingStatus" label="培训状态" align="center" prop="trainingStatus" width="100">
+          <template #default="scope">
+            <el-tag :type="getStatusType(scope.row.trainingStatus)">{{ scope.row.trainingStatus }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column v-if="columns[8].visible" label="创建时间" align="center" prop="createTime" width="160" />
+        <el-table-column label="操作" fixed="right" width="220" class-name="small-padding fixed-width">
+          <template #default="scope">
+            <el-tooltip content="修改" placement="top">
+              <el-button v-has-permi="['riskctrl:safetyTraining:edit']" link type="primary" icon="Edit" @click="handleUpdate(scope.row)"></el-button>
+            </el-tooltip>
+            <el-tooltip content="详情" placement="top">
+              <el-button v-has-permi="['riskctrl:safetyTraining:detail']" link type="primary" icon="Info" @click="handleDetail(scope.row)"></el-button>
+            </el-tooltip>
+            <el-tooltip content="删除" placement="top">
+              <el-button v-has-permi="['riskctrl:safetyTraining:remove']" link type="primary" icon="Delete" @click="handleDelete(scope.row)"></el-button>
+            </el-tooltip>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <pagination v-show="total > 0" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" :total="total" @pagination="getList" />
+    </el-card>
+
+    <!-- 新增/修改弹窗 -->
+    <el-dialog v-model="open" :title="title" width="750px" append-to-body>
+      <el-form ref="trainingFormRef" :model="form" :rules="rules" label-width="100px">
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="培训主题" prop="trainingTitle">
+              <el-input v-model="form.trainingTitle" placeholder="请输入培训主题" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="培训类型" prop="trainingType">
+              <el-select v-model="form.trainingType" placeholder="请选择培训类型" class="w-full">
+                <el-option label="安全知识" value="安全知识" />
+                <el-option label="操作规范" value="操作规范" />
+                <el-option label="应急预案" value="应急预案" />
+                <el-option label="设备维护" value="设备维护" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="培训时间" prop="trainingTime">
+              <el-date-picker v-model="form.trainingTime" type="datetime" placeholder="选择培训时间" value-format="YYYY-MM-DD HH:mm:ss" class="w-full" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="培训地点" prop="trainingLocation">
+              <el-input v-model="form.trainingLocation" placeholder="请输入培训地点" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="培训师" prop="trainer">
+              <el-input v-model="form.trainer" placeholder="请输入培训师" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="参与人数" prop="participantCount">
+              <el-input v-model.number="form.participantCount" type="number" placeholder="请输入参与人数" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-form-item label="培训内容" prop="trainingContent">
+          <el-input v-model="form.trainingContent" type="textarea" :rows="4" placeholder="请输入培训内容" />
+        </el-form-item>
+        <el-form-item label="培训资料" prop="trainingMaterial">
+          <el-input v-model="form.trainingMaterial" placeholder="请输入培训资料路径或链接" />
+        </el-form-item>
+        <el-form-item label="状态" prop="status">
+          <el-radio-group v-model="form.status">
+            <el-radio v-for="dict in sys_normal_disable" :key="dict.value" :label="dict.value">{{ dict.label }}</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="备注" prop="remark">
+          <el-input v-model="form.remark" type="textarea" placeholder="请输入备注" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button type="primary" @click="submitForm">确 定</el-button>
+          <el-button @click="cancel">取 消</el-button>
+        </div>
+      </template>
+    </el-dialog>
+
+    <!-- 详情弹窗 -->
+    <el-dialog v-model="detailOpen" title="培训详情" width="600px" append-to-body>
+      <el-descriptions :column="2" :data="detailForm" border>
+        <el-descriptions-item label="培训编号" prop="trainingNo" />
+        <el-descriptions-item label="培训主题" prop="trainingTitle" />
+        <el-descriptions-item label="培训类型" prop="trainingType" />
+        <el-descriptions-item label="培训时间" prop="trainingTime" />
+        <el-descriptions-item label="培训地点" prop="trainingLocation" />
+        <el-descriptions-item label="培训师" prop="trainer" />
+        <el-descriptions-item label="参与人数" prop="participantCount" />
+        <el-descriptions-item label="培训状态" prop="trainingStatus">
+          <template #content>
+            <el-tag :type="getStatusType(detailForm.trainingStatus)">{{ detailForm.trainingStatus }}</el-tag>
+          </template>
+        </el-descriptions-item>
+        <el-descriptions-item label="培训内容" :span="2" prop="trainingContent" />
+        <el-descriptions-item label="培训资料" :span="2" prop="trainingMaterial" />
+      </el-descriptions>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button @click="detailOpen = false">关 闭</el-button>
+        </div>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup name="SafetyTraining" lang="ts">
+import { ref, reactive, onMounted, nextTick } from 'vue';
+import type { ComponentInternalInstance } from 'vue';
+import type { ElFormInstance } from 'element-plus';
+import { getCurrentInstance } from 'vue';
+import { listSafetyTraining, getSafetyTraining, addSafetyTraining, updateSafetyTraining, delSafetyTraining } from '@/api/riskctrl/safetyTraining';
+
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+const { sys_normal_disable } = proxy.useDict('sys_normal_disable');
+
+interface TrainingVO {
+  trainingId: string | number;
+  trainingNo: string;
+  trainingTitle: string;
+  trainingType: string;
+  trainingTime: string;
+  trainingLocation: string;
+  trainer: string;
+  participantCount: number;
+  trainingStatus: string;
+  trainingContent: string;
+  trainingMaterial: string;
+  status: string;
+  remark: string;
+  createTime: string;
+}
+
+interface TrainingQuery {
+  pageNum: number;
+  pageSize: number;
+  trainingTitle: string | undefined;
+  trainingType: string | undefined;
+  trainingStatus: string | undefined;
+  trainingTime: string | undefined;
+}
+
+interface TrainingForm {
+  trainingId: string | number | undefined;
+  trainingNo: string;
+  trainingTitle: string;
+  trainingType: string;
+  trainingTime: string;
+  trainingLocation: string;
+  trainer: string;
+  participantCount: number;
+  trainingStatus: string;
+  trainingContent: string;
+  trainingMaterial: string;
+  status: string;
+  remark: string;
+}
+
+const trainingList = ref<TrainingVO[]>([]);
+const loading = ref(true);
+const showSearch = ref(true);
+const ids = ref<Array<string | number>>([]);
+const single = ref(true);
+const multiple = ref(true);
+const total = ref(0);
+const title = ref('');
+const open = ref(false);
+const detailOpen = ref(false);
+
+const queryFormRef = ref<ElFormInstance>();
+const trainingFormRef = ref<ElFormInstance>();
+
+const queryParams = reactive<TrainingQuery>({
+  pageNum: 1,
+  pageSize: 10,
+  trainingTitle: undefined,
+  trainingType: undefined,
+  trainingStatus: undefined,
+  trainingTime: undefined
+});
+
+const form = reactive<TrainingForm>({
+  trainingId: undefined,
+  trainingNo: '',
+  trainingTitle: '',
+  trainingType: '',
+  trainingTime: '',
+  trainingLocation: '',
+  trainer: '',
+  participantCount: 0,
+  trainingStatus: '未开始',
+  trainingContent: '',
+  trainingMaterial: '',
+  status: '0',
+  remark: ''
+});
+
+const detailForm = reactive<TrainingVO>({
+  trainingId: '',
+  trainingNo: '',
+  trainingTitle: '',
+  trainingType: '',
+  trainingTime: '',
+  trainingLocation: '',
+  trainer: '',
+  participantCount: 0,
+  trainingStatus: '',
+  trainingContent: '',
+  trainingMaterial: '',
+  status: '',
+  remark: '',
+  createTime: ''
+});
+
+const rules = {
+  trainingTitle: [{ required: true, message: '培训主题不能为空', trigger: 'blur' }],
+  trainingType: [{ required: true, message: '培训类型不能为空', trigger: 'change' }],
+  trainingTime: [{ required: true, message: '培训时间不能为空', trigger: 'change' }],
+  trainer: [{ required: true, message: '培训师不能为空', trigger: 'blur' }]
+};
+
+const columns = ref([
+  { key: 0, label: '培训编号', visible: true },
+  { key: 1, label: '培训主题', visible: true },
+  { key: 2, label: '培训类型', visible: true },
+  { key: 3, label: '培训时间', visible: true },
+  { key: 4, label: '培训地点', visible: true },
+  { key: 5, label: '培训师', visible: true },
+  { key: 6, label: '参与人数', visible: true },
+  { key: 7, label: '培训状态', visible: true },
+  { key: 8, label: '创建时间', visible: true }
+]);
+
+type TagType = 'success' | 'warning' | 'danger' | 'info' | 'primary';
+
+const getStatusType = (status: string): TagType => {
+  const map: Record<string, TagType> = {
+    '未开始': 'warning',
+    '进行中': 'primary',
+    '已结束': 'success'
+  };
+  return map[status] || 'info';
+};
+
+const getList = async () => {
+  loading.value = true;
+  try {
+    const res = await listSafetyTraining(queryParams as any);
+    trainingList.value = (res as any).rows || [];
+    total.value = (res as any).total || 0;
+  } catch (e) {
+    console.error('加载安全培训列表失败', e);
+  } finally {
+    loading.value = false;
+  }
+};
+
+const cancel = () => {
+  open.value = false;
+  reset();
+};
+
+const reset = () => {
+  form.trainingId = undefined;
+  form.trainingNo = '';
+  form.trainingTitle = '';
+  form.trainingType = '';
+  form.trainingTime = '';
+  form.trainingLocation = '';
+  form.trainer = '';
+  form.participantCount = 0;
+  form.trainingStatus = '未开始';
+  form.trainingContent = '';
+  form.trainingMaterial = '';
+  form.status = '0';
+  form.remark = '';
+  nextTick(() => {
+    trainingFormRef.value?.resetFields();
+  });
+};
+
+const handleQuery = () => {
+  queryParams.pageNum = 1;
+  getList();
+};
+
+const resetQuery = () => {
+  queryFormRef.value?.resetFields();
+  handleQuery();
+};
+
+const handleSelectionChange = (selection: TrainingVO[]) => {
+  ids.value = selection.map((item) => item.trainingId);
+  single.value = selection.length !== 1;
+  multiple.value = !selection.length;
+};
+
+const handleAdd = () => {
+  reset();
+  open.value = true;
+  title.value = '新增安全培训';
+};
+
+const handleUpdate = async (row?: TrainingVO) => {
+  reset();
+  const trainingId = row?.trainingId || ids.value[0];
+  const data = trainingList.value.find(item => item.trainingId === trainingId);
+  if (data) {
+    Object.assign(form, data);
+  }
+  open.value = true;
+  title.value = '修改安全培训';
+};
+
+const handleDetail = (row: TrainingVO) => {
+  Object.assign(detailForm, row);
+  detailOpen.value = true;
+};
+
+const submitForm = () => {
+  trainingFormRef.value?.validate(async (valid: boolean) => {
+    if (valid) {
+      try {
+        if (form.trainingId !== undefined && form.trainingId !== '') {
+          await updateSafetyTraining(form as any);
+          proxy?.$modal.msgSuccess('修改成功');
+        } else {
+          await addSafetyTraining(form as any);
+          proxy?.$modal.msgSuccess('新增成功');
+        }
+        open.value = false;
+        await getList();
+      } catch (e) {
+        console.error('提交失败', e);
+      }
+    }
+  });
+};
+
+const handleDelete = async (row?: TrainingVO) => {
+  const trainingIds = row?.trainingId || ids.value;
+  await proxy?.$modal.confirm('是否确认删除选中的数据项?');
+  try {
+    await delSafetyTraining(trainingIds as any);
+    proxy?.$modal.msgSuccess('删除成功');
+    await getList();
+  } catch (e) {
+    console.error('删除失败', e);
+  }
+};
+
+const handleExport = () => {
+  proxy?.$modal.msgSuccess('导出成功');
+};
+
+onMounted(() => {
+  getList();
+});
+</script>

+ 298 - 0
src/views/riskctrl/score/index.vue

@@ -0,0 +1,298 @@
+<template>
+  <div class="p-2">
+    <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
+      <div v-show="showSearch" class="mb-[10px]">
+        <el-card shadow="hover">
+          <el-form ref="queryFormRef" :model="queryParams" :inline="true">
+            <el-form-item label="评分编号" prop="scoreNo">
+              <el-input v-model="queryParams.scoreNo" placeholder="请输入评分编号" clearable @keyup.enter="handleQuery" />
+            </el-form-item>
+            <el-form-item label="评分类型" prop="scoreType">
+              <el-select v-model="queryParams.scoreType" placeholder="评分类型" clearable>
+                <el-option v-for="dict in risk_score_type" :key="dict.value" :label="dict.label" :value="dict.value" />
+              </el-select>
+            </el-form-item>
+            <el-form-item label="风险等级" prop="riskLevel">
+              <el-select v-model="queryParams.riskLevel" placeholder="风险等级" clearable>
+                <el-option v-for="dict in risk_level" :key="dict.value" :label="dict.label" :value="dict.value" />
+              </el-select>
+            </el-form-item>
+            <el-form-item label="用户名称" prop="userName">
+              <el-input v-model="queryParams.userName" placeholder="请输入用户名称" clearable @keyup.enter="handleQuery" />
+            </el-form-item>
+            <el-form-item>
+              <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
+              <el-button icon="Refresh" @click="resetQuery">重置</el-button>
+            </el-form-item>
+          </el-form>
+        </el-card>
+      </div>
+    </transition>
+
+    <el-card shadow="hover">
+      <template #header>
+        <el-row :gutter="10">
+          <el-col :span="1.5">
+            <el-button v-has-permi="['riskctrl:score:add']" type="primary" plain icon="Plus" @click="handleAdd()">新增</el-button>
+          </el-col>
+          <el-col :span="1.5">
+            <el-button v-has-permi="['riskctrl:score:edit']" type="success" plain :disabled="single" icon="Edit" @click="handleUpdate()">修改</el-button>
+          </el-col>
+          <el-col :span="1.5">
+            <el-button v-has-permi="['riskctrl:score:remove']" type="danger" plain :disabled="multiple" icon="Delete" @click="handleDelete()">删除</el-button>
+          </el-col>
+          <right-toolbar v-model:show-search="showSearch" :columns="columns" :search="true" @query-table="getList"></right-toolbar>
+        </el-row>
+      </template>
+
+      <el-table v-loading="loading" border :data="scoreList" @selection-change="handleSelectionChange">
+        <el-table-column type="selection" width="50" align="center" />
+        <el-table-column v-if="columns[0].visible" key="scoreId" label="评分编号" align="center" prop="scoreId" />
+        <el-table-column v-if="columns[1].visible" key="scoreNo" label="评分编号" align="center" prop="scoreNo" :show-overflow-tooltip="true" />
+        <el-table-column v-if="columns[2].visible" key="userName" label="用户名称" align="center" prop="userName" />
+        <el-table-column v-if="columns[3].visible" key="scoreType" label="评分类型" align="center" prop="scoreType">
+          <template #default="scope">
+            <dict-tag :options="risk_score_type" :value="scope.row.scoreType" />
+          </template>
+        </el-table-column>
+        <el-table-column v-if="columns[4].visible" key="totalScore" label="总分" align="center" prop="totalScore" />
+        <el-table-column v-if="columns[5].visible" key="baseScore" label="基础分" align="center" prop="baseScore" />
+        <el-table-column v-if="columns[6].visible" key="ruleScore" label="规则分" align="center" prop="ruleScore" />
+        <el-table-column v-if="columns[7].visible" key="behaviorScore" label="行为分" align="center" prop="behaviorScore" />
+        <el-table-column v-if="columns[8].visible" key="creditScore" label="信用分" align="center" prop="creditScore" />
+        <el-table-column v-if="columns[9].visible" key="riskLevel" label="风险等级" align="center" prop="riskLevel">
+          <template #default="scope">
+            <dict-tag :options="risk_level" :value="scope.row.riskLevel" />
+          </template>
+        </el-table-column>
+        <el-table-column v-if="columns[10].visible" label="评估时间" align="center" prop="evaluateTime" width="160" />
+        <el-table-column label="操作" fixed="right" width="180" class-name="small-padding fixed-width">
+          <template #default="scope">
+            <el-tooltip content="修改" placement="top">
+              <el-button v-hasPermi="['riskctrl:score:edit']" link type="primary" icon="Edit" @click="handleUpdate(scope.row)"></el-button>
+            </el-tooltip>
+            <el-tooltip content="删除" placement="top">
+              <el-button v-hasPermi="['riskctrl:score:remove']" link type="primary" icon="Delete" @click="handleDelete(scope.row)"></el-button>
+            </el-tooltip>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <pagination v-show="total > 0" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" :total="total" @pagination="getList" />
+    </el-card>
+
+    <!-- 添加或修改风险评分对话框 -->
+    <el-dialog v-model="open" :title="title" width="600px" append-to-body>
+      <el-form ref="scoreFormRef" :model="form" :rules="rules" label-width="100px">
+        <el-form-item label="评分编号" prop="scoreNo">
+          <el-input v-model="form.scoreNo" placeholder="请输入评分编号" />
+        </el-form-item>
+        <el-form-item label="用户ID" prop="userId">
+          <el-input v-model="form.userId" placeholder="请输入用户ID" />
+        </el-form-item>
+        <el-form-item label="用户名称" prop="userName">
+          <el-input v-model="form.userName" placeholder="请输入用户名称" />
+        </el-form-item>
+        <el-form-item label="评分类型" prop="scoreType">
+          <el-select v-model="form.scoreType" placeholder="请选择评分类型">
+            <el-option v-for="dict in risk_score_type" :key="dict.value" :label="dict.label" :value="dict.value" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="总分" prop="totalScore">
+          <el-input-number v-model="form.totalScore" :min="0" :max="100" :precision="2" />
+        </el-form-item>
+        <el-form-item label="基础分" prop="baseScore">
+          <el-input-number v-model="form.baseScore" :min="0" :max="100" :precision="2" />
+        </el-form-item>
+        <el-form-item label="规则分" prop="ruleScore">
+          <el-input-number v-model="form.ruleScore" :min="0" :max="100" :precision="2" />
+        </el-form-item>
+        <el-form-item label="行为分" prop="behaviorScore">
+          <el-input-number v-model="form.behaviorScore" :min="0" :max="100" :precision="2" />
+        </el-form-item>
+        <el-form-item label="信用分" prop="creditScore">
+          <el-input-number v-model="form.creditScore" :min="0" :max="100" :precision="2" />
+        </el-form-item>
+        <el-form-item label="风险等级" prop="riskLevel">
+          <el-select v-model="form.riskLevel" placeholder="请选择风险等级">
+            <el-option v-for="dict in risk_level" :key="dict.value" :label="dict.label" :value="dict.value" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="评估说明" prop="evaluation">
+          <el-input v-model="form.evaluation" type="textarea" placeholder="请输入评估说明" />
+        </el-form-item>
+        <el-form-item label="评估时间" prop="evaluateTime">
+          <el-date-picker v-model="form.evaluateTime" type="datetime" placeholder="选择评估时间" value-format="YYYY-MM-DD HH:mm:ss" />
+        </el-form-item>
+        <el-form-item label="备注" prop="remark">
+          <el-input v-model="form.remark" type="textarea" placeholder="请输入备注" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button type="primary" @click="submitForm">确 定</el-button>
+          <el-button @click="cancel">取 消</el-button>
+        </div>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup name="RiskScore" lang="ts">
+import { listScore, getScore, delScore, addScore, updateScore } from '@/api/riskctrl/score';
+import { ScoreForm, ScoreQuery, ScoreVO } from '@/api/riskctrl/score/types';
+
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+const { risk_score_type, risk_level } = proxy.useDict('risk_score_type', 'risk_level');
+
+const scoreList = ref<ScoreVO[]>([]);
+const loading = ref(true);
+const showSearch = ref(true);
+const ids = ref<Array<string | number>>([]);
+const single = ref(true);
+const multiple = ref(true);
+const total = ref(0);
+const title = ref('');
+const open = ref(false);
+
+const queryFormRef = ref<ElFormInstance>();
+const scoreFormRef = ref<ElFormInstance>();
+
+const queryParams = reactive<ScoreQuery>({
+  pageNum: 1,
+  pageSize: 10,
+  scoreNo: undefined,
+  scoreType: undefined,
+  riskLevel: undefined,
+  userName: undefined
+});
+
+const form = reactive<ScoreForm>({
+  scoreId: undefined,
+  scoreNo: '',
+  userId: undefined,
+  userName: '',
+  scoreType: '',
+  totalScore: 0,
+  baseScore: 0,
+  ruleScore: 0,
+  behaviorScore: 0,
+  creditScore: 0,
+  riskLevel: '',
+  evaluation: '',
+  status: '0',
+  remark: '',
+  evaluateTime: ''
+});
+
+const rules = {
+  scoreType: [{ required: true, message: '评分类型不能为空', trigger: 'change' }]
+};
+
+const columns = ref([
+  { key: 0, label: `评分编号`, visible: true },
+  { key: 1, label: `评分编号`, visible: true },
+  { key: 2, label: `用户名称`, visible: true },
+  { key: 3, label: `评分类型`, visible: true },
+  { key: 4, label: `总分`, visible: true },
+  { key: 5, label: `基础分`, visible: true },
+  { key: 6, label: `规则分`, visible: true },
+  { key: 7, label: `行为分`, visible: true },
+  { key: 8, label: `信用分`, visible: true },
+  { key: 9, label: `风险等级`, visible: true },
+  { key: 10, label: `评估时间`, visible: true }
+]);
+
+const getList = async () => {
+  loading.value = true;
+  const res = await listScore(queryParams);
+  scoreList.value = res.rows;
+  total.value = res.total;
+  loading.value = false;
+};
+
+const cancel = () => {
+  open.value = false;
+  reset();
+};
+
+const reset = () => {
+  form.scoreId = undefined;
+  form.scoreNo = '';
+  form.userId = undefined;
+  form.userName = '';
+  form.scoreType = '';
+  form.totalScore = 0;
+  form.baseScore = 0;
+  form.ruleScore = 0;
+  form.behaviorScore = 0;
+  form.creditScore = 0;
+  form.riskLevel = '';
+  form.evaluation = '';
+  form.status = '0';
+  form.remark = '';
+  form.evaluateTime = '';
+  nextTick(() => {
+    scoreFormRef.value?.resetFields();
+  });
+};
+
+const handleQuery = () => {
+  queryParams.pageNum = 1;
+  getList();
+};
+
+const resetQuery = () => {
+  queryFormRef.value?.resetFields();
+  handleQuery();
+};
+
+const handleSelectionChange = (selection: ScoreVO[]) => {
+  ids.value = selection.map((item) => item.scoreId);
+  single.value = selection.length !== 1;
+  multiple.value = !selection.length;
+};
+
+const handleAdd = () => {
+  reset();
+  open.value = true;
+  title.value = '添加风险评分';
+};
+
+const handleUpdate = async (row?: ScoreVO) => {
+  reset();
+  const scoreId = row?.scoreId || ids.value[0];
+  const res = await getScore(scoreId);
+  Object.assign(form, res.data);
+  open.value = true;
+  title.value = '修改风险评分';
+};
+
+const submitForm = () => {
+  scoreFormRef.value?.validate(async (valid: boolean) => {
+    if (valid) {
+      if (form.scoreId !== undefined) {
+        await updateScore(form);
+        proxy?.$modal.msgSuccess('修改成功');
+      } else {
+        await addScore(form);
+        proxy?.$modal.msgSuccess('新增成功');
+      }
+      open.value = false;
+      await getList();
+    }
+  });
+};
+
+const handleDelete = async (row?: ScoreVO) => {
+  const scoreIds = row?.scoreId || ids.value;
+  await proxy?.$modal.confirm('是否确认删除编号为"' + scoreIds + '"的数据项?');
+  await delScore(scoreIds);
+  await getList();
+  proxy?.$modal.msgSuccess('删除成功');
+};
+
+onMounted(() => {
+  getList();
+});
+</script>

+ 537 - 0
src/views/riskctrl/specialEquipment/index.vue

@@ -0,0 +1,537 @@
+<template>
+  <div class="p-2">
+    <!-- 统计卡片 -->
+    <el-row :gutter="16" class="mb-[10px]">
+      <el-col :span="6">
+        <el-card shadow="hover">
+          <div class="flex items-center justify-between">
+            <div>
+              <p class="text-gray-500 text-sm">设备总数</p>
+              <p class="text-2xl font-bold text-blue-500">{{ statsData.totalCount }}</p>
+            </div>
+            <el-icon :size="40" color="#409EFF"><OfficeBuilding /></el-icon>
+          </div>
+        </el-card>
+      </el-col>
+      <el-col :span="6">
+        <el-card shadow="hover">
+          <div class="flex items-center justify-between">
+            <div>
+              <p class="text-gray-500 text-sm">正常</p>
+              <p class="text-2xl font-bold text-green-500">{{ statsData.normalCount }}</p>
+            </div>
+            <el-icon :size="40" color="#67C23A"><CircleCheckFilled /></el-icon>
+          </div>
+        </el-card>
+      </el-col>
+      <el-col :span="6">
+        <el-card shadow="hover">
+          <div class="flex items-center justify-between">
+            <div>
+              <p class="text-gray-500 text-sm">待维护</p>
+              <p class="text-2xl font-bold text-orange-500">{{ statsData.maintenanceCount }}</p>
+            </div>
+            <el-icon :size="40" color="#E6A23C"><Tools /></el-icon>
+          </div>
+        </el-card>
+      </el-col>
+      <el-col :span="6">
+        <el-card shadow="hover">
+          <div class="flex items-center justify-between">
+            <div>
+              <p class="text-gray-500 text-sm">已过期</p>
+              <p class="text-2xl font-bold text-red-500">{{ statsData.overdueCount }}</p>
+            </div>
+            <el-icon :size="40" color="#F56C6C"><WarningFilled /></el-icon>
+          </div>
+        </el-card>
+      </el-col>
+    </el-row>
+
+    <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
+      <div v-show="showSearch" class="mb-[10px]">
+        <el-card shadow="hover">
+          <el-form ref="queryFormRef" :model="queryParams" :inline="true">
+            <el-form-item label="设备名称" prop="equipmentName">
+              <el-input v-model="queryParams.equipmentName" placeholder="请输入名称" clearable @keyup.enter="handleQuery" />
+            </el-form-item>
+            <el-form-item label="设备类型" prop="equipmentType">
+              <el-select v-model="queryParams.equipmentType" placeholder="类型" clearable>
+                <el-option label="电梯" value="电梯" />
+                <el-option label="锅炉" value="锅炉" />
+                <el-option label="压力容器" value="压力容器" />
+                <el-option label="起重机械" value="起重机械" />
+                <el-option label="叉车" value="叉车" />
+              </el-select>
+            </el-form-item>
+            <el-form-item label="维护状态" prop="maintenanceStatus">
+              <el-select v-model="queryParams.maintenanceStatus" placeholder="状态" clearable>
+                <el-option label="正常" value="正常" />
+                <el-option label="待维护" value="待维护" />
+                <el-option label="维护中" value="维护中" />
+                <el-option label="已过期" value="已过期" />
+              </el-select>
+            </el-form-item>
+            <el-form-item>
+              <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
+              <el-button icon="Refresh" @click="resetQuery">重置</el-button>
+            </el-form-item>
+          </el-form>
+        </el-card>
+      </div>
+    </transition>
+
+    <el-card shadow="hover">
+      <template #header>
+        <el-row :gutter="10">
+          <el-col :span="1.5">
+            <el-button v-has-permi="['riskctrl:specialEquipment:add']" type="primary" plain icon="Plus" @click="handleAdd()">新增设备</el-button>
+          </el-col>
+          <el-col :span="1.5">
+            <el-button v-has-permi="['riskctrl:specialEquipment:edit']" type="success" plain :disabled="single" icon="Edit" @click="handleUpdate()">编辑</el-button>
+          </el-col>
+          <el-col :span="1.5">
+            <el-button v-has-permi="['riskctrl:specialEquipment:remove']" type="danger" plain :disabled="multiple" icon="Delete" @click="handleDelete()">删除</el-button>
+          </el-col>
+          <right-toolbar v-model:show-search="showSearch" :columns="columns" :search="true" @query-table="getList"></right-toolbar>
+        </el-row>
+      </template>
+
+      <el-table v-loading="loading" border :data="equipmentList" @selection-change="handleSelectionChange">
+        <el-table-column type="selection" width="50" align="center" />
+        <el-table-column v-if="columns[0].visible" key="equipmentNo" label="设备编号" align="center" prop="equipmentNo" width="140" />
+        <el-table-column v-if="columns[1].visible" key="equipmentName" label="设备名称" align="center" prop="equipmentName" :show-overflow-tooltip="true" />
+        <el-table-column v-if="columns[2].visible" key="equipmentType" label="设备类型" align="center" prop="equipmentType" width="100" />
+        <el-table-column v-if="columns[3].visible" key="installLocation" label="安装位置" align="center" prop="installLocation" :show-overflow-tooltip="true" />
+        <el-table-column v-if="columns[4].visible" key="maintenanceStatus" label="维护状态" align="center" prop="maintenanceStatus" width="100">
+          <template #default="scope">
+            <el-tag :type="getStatusType(scope.row.maintenanceStatus)">{{ scope.row.maintenanceStatus || '正常' }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column v-if="columns[5].visible" label="创建时间" align="center" prop="createTime" width="160" />
+        <el-table-column label="操作" fixed="right" width="220" class-name="small-padding fixed-width">
+          <template #default="scope">
+            <el-tooltip content="编辑" placement="top">
+              <el-button v-has-permi="['riskctrl:specialEquipment:edit']" link type="primary" icon="Edit" @click="handleUpdate(scope.row)"></el-button>
+            </el-tooltip>
+            <el-tooltip content="照片" placement="top">
+              <el-button link type="primary" icon="Picture" @click="handlePhotos(scope.row)"></el-button>
+            </el-tooltip>
+            <el-tooltip content="设备维护" placement="top">
+              <el-button link type="warning" icon="Tools" @click="handleMaintenance(scope.row)"></el-button>
+            </el-tooltip>
+            <el-tooltip content="维护记录" placement="top">
+              <el-button link type="info" icon="List" @click="handleMaintenanceRecord(scope.row)"></el-button>
+            </el-tooltip>
+            <el-tooltip content="删除" placement="top">
+              <el-button v-has-permi="['riskctrl:specialEquipment:remove']" link type="primary" icon="Delete" @click="handleDelete(scope.row)"></el-button>
+            </el-tooltip>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <pagination v-show="total > 0" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" :total="total" @pagination="getList" />
+    </el-card>
+
+    <!-- 新增/编辑弹窗 -->
+    <el-dialog v-model="open" :title="title" width="700px" append-to-body>
+      <el-form ref="equipmentFormRef" :model="form" :rules="rules" label-width="100px">
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="设备名称" prop="equipmentName">
+              <el-input v-model="form.equipmentName" placeholder="请输入设备名称" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="设备类型" prop="equipmentType">
+              <el-select v-model="form.equipmentType" placeholder="请选择" class="w-full">
+                <el-option label="电梯" value="电梯" />
+                <el-option label="锅炉" value="锅炉" />
+                <el-option label="压力容器" value="压力容器" />
+                <el-option label="起重机械" value="起重机械" />
+                <el-option label="叉车" value="叉车" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-form-item label="安装位置" prop="installLocation">
+          <el-input v-model="form.installLocation" placeholder="请输入安装位置" />
+        </el-form-item>
+        <el-form-item label="设备照片">
+          <el-upload :action="uploadUrl" list-type="picture-card" :limit="5" :file-list="photoList" :headers="uploadHeaders" :on-success="handleUploadSuccess" :on-remove="handleUploadRemove">
+            <el-icon><Plus /></el-icon>
+          </el-upload>
+        </el-form-item>
+        <el-form-item label="状态" prop="status">
+          <el-radio-group v-model="form.status">
+            <el-radio v-for="dict in sys_normal_disable" :key="dict.value" :label="dict.value">{{ dict.label }}</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="备注" prop="remark">
+          <el-input v-model="form.remark" type="textarea" placeholder="请输入备注" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button type="primary" @click="submitForm">确 定</el-button>
+          <el-button @click="cancel">取 消</el-button>
+        </div>
+      </template>
+    </el-dialog>
+
+    <!-- 照片预览弹窗 -->
+    <el-dialog v-model="photoVisible" title="设备照片" width="600px" append-to-body>
+      <div class="flex justify-center flex-wrap" v-if="currentPhotos.length">
+        <el-image v-for="(url, idx) in currentPhotos" :key="idx" :src="url" class="m-2" style="width:200px;height:200px" fit="cover" :preview-src-list="currentPhotos" />
+      </div>
+      <div v-else class="text-center text-gray-400 py-8">暂无照片</div>
+    </el-dialog>
+
+    <!-- 维护记录弹窗 -->
+    <el-dialog v-model="recordOpen" :title="currentEquipmentName + ' - 维护记录'" width="900px" append-to-body>
+      <el-table :data="maintenanceList" border v-loading="recordLoading">
+        <el-table-column label="维护开始" align="center" prop="startTime" width="160" />
+        <el-table-column label="维护结束" align="center" prop="endTime" width="160" />
+        <el-table-column label="维护人员" align="center" prop="maintainUser" width="100" />
+        <el-table-column label="现场人员" align="center" prop="siteUser" width="100" />
+        <el-table-column label="维护状态" align="center" prop="maintainStatus" width="100">
+          <template #default="scope">
+            <el-tag :type="getMaintainStatusType(scope.row.maintainStatus)">{{ scope.row.maintainStatus }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="备注" align="center" prop="remark" :show-overflow-tooltip="true" />
+      </el-table>
+      <template #footer>
+        <el-button @click="recordOpen = false">关 闭</el-button>
+      </template>
+    </el-dialog>
+
+    <!-- 新增维护记录弹窗 -->
+    <el-dialog v-model="maintainOpen" title="新增设备维护记录" width="700px" append-to-body>
+      <el-form ref="maintainFormRef" :model="maintainForm" :rules="maintainRules" label-width="100px">
+        <el-form-item label="设备">
+          <el-input v-model="currentEquipmentName" disabled />
+        </el-form-item>
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="维护开始" prop="startTime">
+              <el-date-picker v-model="maintainForm.startTime" type="datetime" placeholder="选择时间" value-format="YYYY-MM-DD HH:mm:ss" class="w-full" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="维护结束" prop="endTime">
+              <el-date-picker v-model="maintainForm.endTime" type="datetime" placeholder="选择时间" value-format="YYYY-MM-DD HH:mm:ss" class="w-full" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="维护人员" prop="maintainUser">
+              <el-input v-model="maintainForm.maintainUser" placeholder="请输入维护人员" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="现场人员" prop="siteUser">
+              <el-input v-model="maintainForm.siteUser" placeholder="请输入现场人员" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-form-item label="维护状态" prop="maintainStatus">
+          <el-select v-model="maintainForm.maintainStatus" placeholder="请选择" class="w-full">
+            <el-option label="正常" value="正常" />
+            <el-option label="待维护" value="待维护" />
+            <el-option label="维护中" value="维护中" />
+            <el-option label="已过期" value="已过期" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="维护照片">
+          <el-upload :action="uploadUrl" list-type="picture-card" :limit="5" :file-list="maintainPhotoList" :headers="uploadHeaders" :on-success="handleMaintainUploadSuccess" :on-remove="handleMaintainUploadRemove">
+            <el-icon><Plus /></el-icon>
+          </el-upload>
+        </el-form-item>
+        <el-form-item label="备注" prop="remark">
+          <el-input v-model="maintainForm.remark" type="textarea" placeholder="请输入备注" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button type="primary" @click="submitMaintenance">确 定</el-button>
+          <el-button @click="maintainOpen = false">取 消</el-button>
+        </div>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup name="SpecialEquipment" lang="ts">
+import { listSpecialEquipment, getSpecialEquipment, delSpecialEquipment, addSpecialEquipment, updateSpecialEquipment, getEquipmentStats } from '@/api/riskctrl/specialEquipment';
+import { getMaintenanceByEquipment, addEquipmentMaintenance } from '@/api/riskctrl/equipmentMaintenance';
+import { SpecialEquipmentForm, SpecialEquipmentQuery, SpecialEquipmentVO, EquipmentStats } from '@/api/riskctrl/specialEquipment/types';
+import { EquipmentMaintenanceForm } from '@/api/riskctrl/equipmentMaintenance/types';
+import { getToken } from '@/utils/auth';
+
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+const { sys_normal_disable } = proxy.useDict('sys_normal_disable');
+
+const equipmentList = ref<SpecialEquipmentVO[]>([]);
+const maintenanceList = ref<any[]>([]);
+const loading = ref(true);
+const showSearch = ref(true);
+const ids = ref<Array<string | number>>([]);
+const single = ref(true);
+const multiple = ref(true);
+const total = ref(0);
+const title = ref('');
+const open = ref(false);
+const photoVisible = ref(false);
+const recordOpen = ref(false);
+const recordLoading = ref(false);
+const maintainOpen = ref(false);
+const currentPhotos = ref<string[]>([]);
+const photoList = ref<any[]>([]);
+const maintainPhotoList = ref<any[]>([]);
+const currentEquipmentId = ref<string | number>();
+const currentEquipmentName = ref('');
+
+const uploadUrl = import.meta.env.VITE_APP_BASE_API + '/resource/oss/upload';
+const uploadHeaders = ref({ Authorization: 'Bearer ' + getToken(), clientid: import.meta.env.VITE_APP_CLIENT_ID });
+
+const queryFormRef = ref<ElFormInstance>();
+const equipmentFormRef = ref<ElFormInstance>();
+const maintainFormRef = ref<ElFormInstance>();
+
+const statsData = reactive<EquipmentStats>({
+  totalCount: 0,
+  normalCount: 0,
+  maintenanceCount: 0,
+  overdueCount: 0
+});
+
+const queryParams = reactive<SpecialEquipmentQuery>({
+  pageNum: 1,
+  pageSize: 10,
+  equipmentName: undefined,
+  equipmentType: undefined,
+  maintenanceStatus: undefined
+});
+
+const form = reactive<SpecialEquipmentForm>({
+  equipmentId: undefined,
+  equipmentName: '',
+  equipmentType: '',
+  installLocation: '',
+  equipmentPhotos: '',
+  status: '0',
+  remark: ''
+});
+
+const maintainForm = reactive<EquipmentMaintenanceForm>({
+  equipmentId: undefined,
+  equipmentName: '',
+  startTime: '',
+  endTime: '',
+  maintainUser: '',
+  siteUser: '',
+  maintainStatus: '正常',
+  maintenancePhotos: '',
+  remark: ''
+});
+
+const rules = {
+  equipmentName: [{ required: true, message: '设备名称不能为空', trigger: 'blur' }],
+  equipmentType: [{ required: true, message: '设备类型不能为空', trigger: 'change' }],
+  installLocation: [{ required: true, message: '安装位置不能为空', trigger: 'blur' }]
+};
+
+const maintainRules = {
+  startTime: [{ required: true, message: '维护开始时间不能为空', trigger: 'change' }],
+  maintainUser: [{ required: true, message: '维护人员不能为空', trigger: 'blur' }],
+  maintainStatus: [{ required: true, message: '维护状态不能为空', trigger: 'change' }]
+};
+
+const columns = ref([
+  { key: 0, label: '设备编号', visible: true },
+  { key: 1, label: '设备名称', visible: true },
+  { key: 2, label: '设备类型', visible: true },
+  { key: 3, label: '安装位置', visible: true },
+  { key: 4, label: '维护状态', visible: true },
+  { key: 5, label: '创建时间', visible: true }
+]);
+
+type TagType = 'success' | 'warning' | 'danger' | 'info' | 'primary';
+
+const getStatusType = (status: string): TagType => {
+  const map: Record<string, TagType> = {
+    '正常': 'success',
+    '待维护': 'warning',
+    '维护中': 'primary',
+    '已过期': 'danger'
+  };
+  return map[status || '正常'] || 'info';
+};
+
+const getMaintainStatusType = (status: string): TagType => {
+  return getStatusType(status);
+};
+
+const loadStats = async () => {
+  try {
+    const res = await getEquipmentStats();
+    Object.assign(statsData, res.data);
+  } catch { /* ignore */ }
+};
+
+const getList = async () => {
+  loading.value = true;
+  const res = await listSpecialEquipment(queryParams);
+  equipmentList.value = res.rows;
+  total.value = res.total;
+  loading.value = false;
+};
+
+const cancel = () => {
+  open.value = false;
+  reset();
+};
+
+const reset = () => {
+  form.equipmentId = undefined;
+  form.equipmentName = '';
+  form.equipmentType = '';
+  form.installLocation = '';
+  form.equipmentPhotos = '';
+  form.status = '0';
+  form.remark = '';
+  photoList.value = [];
+  nextTick(() => { equipmentFormRef.value?.resetFields(); });
+};
+
+const resetMaintain = () => {
+  maintainForm.equipmentId = undefined;
+  maintainForm.equipmentName = '';
+  maintainForm.startTime = '';
+  maintainForm.endTime = '';
+  maintainForm.maintainUser = '';
+  maintainForm.siteUser = '';
+  maintainForm.maintainStatus = '正常';
+  maintainForm.maintenancePhotos = '';
+  maintainForm.remark = '';
+  maintainPhotoList.value = [];
+  nextTick(() => { maintainFormRef.value?.resetFields(); });
+};
+
+const handleQuery = () => { queryParams.pageNum = 1; getList(); };
+const resetQuery = () => { queryFormRef.value?.resetFields(); handleQuery(); };
+
+const handleSelectionChange = (selection: SpecialEquipmentVO[]) => {
+  ids.value = selection.map((item) => item.equipmentId);
+  single.value = selection.length !== 1;
+  multiple.value = !selection.length;
+};
+
+const handleAdd = () => { reset(); open.value = true; title.value = '新增特种设备'; };
+
+const handleUpdate = async (row?: SpecialEquipmentVO) => {
+  reset();
+  const equipmentId = row?.equipmentId || ids.value[0];
+  const res = await getSpecialEquipment(equipmentId);
+  Object.assign(form, res.data);
+  if (res.data.equipmentPhotos) {
+    photoList.value = res.data.equipmentPhotos.split(',').filter(Boolean).map((url: string, idx: number) => ({
+      name: 'photo-' + idx,
+      url
+    }));
+  }
+  open.value = true;
+  title.value = '编辑特种设备';
+};
+
+const submitForm = () => {
+  equipmentFormRef.value?.validate(async (valid: boolean) => {
+    if (valid) {
+      form.equipmentPhotos = photoList.value.map((f: any) => f.response?.url || f.url).filter(Boolean).join(',');
+      if (form.equipmentId !== undefined) {
+        await updateSpecialEquipment(form);
+        proxy?.$modal.msgSuccess('修改成功');
+      } else {
+        await addSpecialEquipment(form);
+        proxy?.$modal.msgSuccess('新增成功');
+      }
+      open.value = false;
+      await getList();
+      await loadStats();
+    }
+  });
+};
+
+const handlePhotos = (row: SpecialEquipmentVO) => {
+  currentPhotos.value = row.equipmentPhotos ? row.equipmentPhotos.split(',').filter(Boolean) : [];
+  photoVisible.value = true;
+};
+
+const handleUploadSuccess = (response: any, file: any) => {
+  file.url = response.url;
+};
+
+const handleUploadRemove = (file: any) => {
+  photoList.value = photoList.value.filter((f: any) => f.uid !== file.uid);
+};
+
+const handleMaintainUploadSuccess = (response: any, file: any) => {
+  file.url = response.url;
+};
+
+const handleMaintainUploadRemove = (file: any) => {
+  maintainPhotoList.value = maintainPhotoList.value.filter((f: any) => f.uid !== file.uid);
+};
+
+const handleMaintenance = (row: SpecialEquipmentVO) => {
+  resetMaintain();
+  currentEquipmentId.value = row.equipmentId;
+  currentEquipmentName.value = row.equipmentName;
+  maintainForm.equipmentId = row.equipmentId;
+  maintainForm.equipmentName = row.equipmentName;
+  maintainOpen.value = true;
+};
+
+const submitMaintenance = () => {
+  maintainFormRef.value?.validate(async (valid: boolean) => {
+    if (valid) {
+      maintainForm.maintenancePhotos = maintainPhotoList.value.map((f: any) => f.response?.url || f.url).filter(Boolean).join(',');
+      await addEquipmentMaintenance(maintainForm);
+      proxy?.$modal.msgSuccess('维护记录新增成功');
+      maintainOpen.value = false;
+      await getList();
+      await loadStats();
+    }
+  });
+};
+
+const handleMaintenanceRecord = async (row: SpecialEquipmentVO) => {
+  currentEquipmentId.value = row.equipmentId;
+  currentEquipmentName.value = row.equipmentName;
+  recordLoading.value = true;
+  recordOpen.value = true;
+  try {
+    const res = await getMaintenanceByEquipment(row.equipmentId);
+    maintenanceList.value = res.data || [];
+  } catch {
+    maintenanceList.value = [];
+  }
+  recordLoading.value = false;
+};
+
+const handleDelete = async (row?: SpecialEquipmentVO) => {
+  const equipmentIds = row?.equipmentId || ids.value;
+  await proxy?.$modal.confirm('是否确认删除该设备信息?');
+  await delSpecialEquipment(equipmentIds);
+  await getList();
+  await loadStats();
+  proxy?.$modal.msgSuccess('删除成功');
+};
+
+onMounted(() => {
+  loadStats();
+  getList();
+});
+</script>

+ 313 - 0
src/views/riskctrl/statistics/index.vue

@@ -0,0 +1,313 @@
+<template>
+  <div class="p-2">
+    <!-- 顶部统计卡片 -->
+    <el-row :gutter="16" class="mb-[10px]">
+      <el-col :span="6">
+        <el-card shadow="hover" class="stat-card">
+          <div class="flex items-center justify-between">
+            <div>
+              <p class="text-gray-500 text-sm">隐患总数</p>
+              <p class="text-2xl font-bold text-blue-500">{{ statsData.hazardTotal }}</p>
+            </div>
+            <div class="stat-icon stat-icon-blue">
+              <el-icon :size="40" color="#409EFF"><WarningFilled /></el-icon>
+            </div>
+          </div>
+        </el-card>
+      </el-col>
+      <el-col :span="6">
+        <el-card shadow="hover" class="stat-card">
+          <div class="flex items-center justify-between">
+            <div>
+              <p class="text-gray-500 text-sm">设备数量</p>
+              <p class="text-2xl font-bold text-purple-500">{{ statsData.equipmentTotal }}</p>
+            </div>
+            <div class="stat-icon stat-icon-purple">
+              <el-icon :size="40" color="#9B59B6"><Monitor /></el-icon>
+            </div>
+          </div>
+        </el-card>
+      </el-col>
+      <el-col :span="6">
+        <el-card shadow="hover" class="stat-card">
+          <div class="flex items-center justify-between">
+            <div>
+              <p class="text-gray-500 text-sm">培训次数</p>
+              <p class="text-2xl font-bold text-green-500">{{ statsData.trainingCount }}</p>
+            </div>
+            <div class="stat-icon stat-icon-green">
+              <el-icon :size="40" color="#27AE60"><Reading /></el-icon>
+            </div>
+          </div>
+        </el-card>
+      </el-col>
+      <el-col :span="6">
+        <el-card shadow="hover" class="stat-card">
+          <div class="flex items-center justify-between">
+            <div>
+              <p class="text-gray-500 text-sm">事故次数</p>
+              <p class="text-2xl font-bold text-red-500">{{ statsData.accidentCount }}</p>
+            </div>
+            <div class="stat-icon stat-icon-red">
+              <el-icon :size="40" color="#E74C3C"><WarnTriangleFilled /></el-icon>
+            </div>
+          </div>
+        </el-card>
+      </el-col>
+    </el-row>
+
+    <!-- 图表区域 -->
+    <el-row :gutter="16" class="mb-[10px]">
+      <el-col :span="16">
+        <el-card shadow="hover">
+          <template #header>
+            <span class="font-medium">隐患趋势分析</span>
+          </template>
+          <div ref="trendChartRef" style="width: 100%; height: 320px;"></div>
+        </el-card>
+      </el-col>
+      <el-col :span="8">
+        <el-card shadow="hover">
+          <template #header>
+            <span class="font-medium">隐患类型分布</span>
+          </template>
+          <div ref="pieChartRef" style="width: 100%; height: 320px;"></div>
+        </el-card>
+      </el-col>
+    </el-row>
+
+    <!-- 统计表格 -->
+    <el-row>
+      <el-col :span="24">
+        <el-card shadow="hover">
+          <template #header>
+            <span class="font-medium">月度统计报表</span>
+          </template>
+          <el-table border :data="monthlyData" style="width: 100%">
+            <el-table-column prop="month" label="月份" align="center" />
+            <el-table-column prop="hazardCount" label="隐患数量" align="center" />
+            <el-table-column prop="processedCount" label="已处理" align="center" />
+            <el-table-column prop="pendingCount" label="待处理" align="center" />
+            <el-table-column prop="trainingCount" label="培训次数" align="center" />
+            <el-table-column prop="accidentCount" label="事故次数" align="center" />
+            <el-table-column prop="rate" label="处理率" align="center">
+              <template #default="scope">
+                <el-progress :percentage="scope.row.rate" :show-text="false" :stroke-width="10" />
+              </template>
+            </el-table-column>
+          </el-table>
+        </el-card>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script setup name="Statistics" lang="ts">
+import { ref, reactive, onMounted, onUnmounted } from 'vue';
+import * as echarts from 'echarts';
+import { WarningFilled, Monitor, Reading, WarnTriangleFilled } from '@element-plus/icons-vue';
+
+const trendChartRef = ref<HTMLDivElement>();
+const pieChartRef = ref<HTMLDivElement>();
+let trendChart: echarts.ECharts | null = null;
+let pieChart: echarts.ECharts | null = null;
+
+const statsData = reactive({
+  hazardTotal: 128,
+  equipmentTotal: 47,
+  trainingCount: 15,
+  accidentCount: 3
+});
+
+const monthlyData = ref([
+  { month: '1月', hazardCount: 20, processedCount: 18, pendingCount: 2, trainingCount: 3, accidentCount: 1, rate: 90 },
+  { month: '2月', hazardCount: 25, processedCount: 22, pendingCount: 3, trainingCount: 2, accidentCount: 0, rate: 88 },
+  { month: '3月', hazardCount: 18, processedCount: 16, pendingCount: 2, trainingCount: 4, accidentCount: 1, rate: 89 },
+  { month: '4月', hazardCount: 30, processedCount: 24, pendingCount: 6, trainingCount: 3, accidentCount: 1, rate: 80 },
+  { month: '5月', hazardCount: 25, processedCount: 20, pendingCount: 5, trainingCount: 2, accidentCount: 0, rate: 80 },
+  { month: '6月', hazardCount: 10, processedCount: 6, pendingCount: 4, trainingCount: 1, accidentCount: 0, rate: 60 }
+]);
+
+const trendData = ref([
+  { month: '1月', total: 20, processed: 18 },
+  { month: '2月', total: 25, processed: 22 },
+  { month: '3月', total: 18, processed: 16 },
+  { month: '4月', total: 30, processed: 24 },
+  { month: '5月', total: 25, processed: 20 },
+  { month: '6月', total: 10, processed: 6 }
+]);
+
+const pieData = ref([
+  { name: '设备安全', value: 45 },
+  { name: '消防隐患', value: 30 },
+  { name: '环境安全', value: 25 },
+  { name: '人员操作', value: 18 },
+  { name: '其他', value: 10 }
+]);
+
+const initTrendChart = () => {
+  if (!trendChartRef.value) return;
+  if (trendChart) trendChart.dispose();
+  trendChart = echarts.init(trendChartRef.value);
+
+  const months = trendData.value.map(item => item.month);
+  const totals = trendData.value.map(item => item.total);
+  const processed = trendData.value.map(item => item.processed);
+
+  const option: echarts.EChartsOption = {
+    tooltip: {
+      trigger: 'axis',
+      axisPointer: { type: 'cross' }
+    },
+    legend: {
+      data: ['隐患总数', '已处理'],
+      bottom: 0,
+      icon: 'circle',
+      itemWidth: 8,
+      itemHeight: 8
+    },
+    grid: {
+      left: '3%',
+      right: '4%',
+      bottom: '15%',
+      top: '10%',
+      containLabel: true
+    },
+    xAxis: {
+      type: 'category',
+      boundaryGap: false,
+      data: months,
+      axisLine: { lineStyle: { color: '#e0e0e0' } },
+      axisLabel: { color: '#666', fontSize: 12 }
+    },
+    yAxis: {
+      type: 'value',
+      axisLine: { show: false },
+      splitLine: { lineStyle: { color: '#f0f0f0' } },
+      axisLabel: { color: '#666', fontSize: 12 }
+    },
+    series: [
+      {
+        name: '隐患总数',
+        type: 'line',
+        smooth: true,
+        data: totals,
+        itemStyle: { color: '#3b82f6' },
+        lineStyle: { width: 2 },
+        symbol: 'circle',
+        symbolSize: 6
+      },
+      {
+        name: '已处理',
+        type: 'line',
+        smooth: true,
+        data: processed,
+        itemStyle: { color: '#22c55e' },
+        lineStyle: { width: 2 },
+        symbol: 'circle',
+        symbolSize: 6
+      }
+    ]
+  };
+  trendChart.setOption(option);
+};
+
+const initPieChart = () => {
+  if (!pieChartRef.value) return;
+  if (pieChart) pieChart.dispose();
+  pieChart = echarts.init(pieChartRef.value);
+
+  const colors = ['#3b82f6', '#ef4444', '#f59e0b', '#22c55e', '#8b5cf6'];
+
+  const option: echarts.EChartsOption = {
+    tooltip: {
+      trigger: 'item',
+      formatter: '{b}: {c} ({d}%)'
+    },
+    legend: {
+      orient: 'vertical',
+      right: '5%',
+      top: 'center',
+      itemWidth: 10,
+      itemHeight: 10,
+      textStyle: { fontSize: 12, color: '#666' }
+    },
+    color: colors,
+    series: [
+      {
+        name: '隐患类型',
+        type: 'pie',
+        radius: ['45%', '70%'],
+        center: ['35%', '50%'],
+        avoidLabelOverlap: false,
+        itemStyle: {
+          borderRadius: 6,
+          borderColor: '#fff',
+          borderWidth: 2
+        },
+        label: { show: false },
+        emphasis: {
+          label: { show: true, fontSize: 14, fontWeight: 'bold' }
+        },
+        labelLine: { show: false },
+        data: pieData.value
+      }
+    ]
+  };
+  pieChart.setOption(option);
+};
+
+const handleResize = () => {
+  trendChart?.resize();
+  pieChart?.resize();
+};
+
+onMounted(() => {
+  initTrendChart();
+  initPieChart();
+  window.addEventListener('resize', handleResize);
+});
+
+onUnmounted(() => {
+  window.removeEventListener('resize', handleResize);
+  trendChart?.dispose();
+  pieChart?.dispose();
+});
+</script>
+
+<style lang="scss" scoped>
+.stat-card {
+  border-radius: 12px;
+  transition: all 0.3s ease;
+
+  &:hover {
+    transform: translateY(-2px);
+    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08);
+  }
+}
+
+.stat-icon {
+  width: 48px;
+  height: 48px;
+  border-radius: 10px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+
+  &.stat-icon-blue {
+    background-color: #eff6ff;
+  }
+
+  &.stat-icon-purple {
+    background-color: #faf5ff;
+  }
+
+  &.stat-icon-green {
+    background-color: #f0fdf4;
+  }
+
+  &.stat-icon-red {
+    background-color: #fef2f2;
+  }
+}
+</style>

+ 276 - 0
src/views/riskctrl/whitelist/index.vue

@@ -0,0 +1,276 @@
+<template>
+  <div class="p-2">
+    <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
+      <div v-show="showSearch" class="mb-[10px]">
+        <el-card shadow="hover">
+          <el-form ref="queryFormRef" :model="queryParams" :inline="true">
+            <el-form-item label="白名单类型" prop="whiteType">
+              <el-select v-model="queryParams.whiteType" placeholder="白名单类型" clearable>
+                <el-option v-for="dict in risk_list_type" :key="dict.value" :label="dict.label" :value="dict.value" />
+              </el-select>
+            </el-form-item>
+            <el-form-item label="白名单值" prop="whiteValue">
+              <el-input v-model="queryParams.whiteValue" placeholder="请输入白名单值" clearable @keyup.enter="handleQuery" />
+            </el-form-item>
+            <el-form-item label="状态" prop="status">
+              <el-select v-model="queryParams.status" placeholder="状态" clearable>
+                <el-option v-for="dict in sys_normal_disable" :key="dict.value" :label="dict.label" :value="dict.value" />
+              </el-select>
+            </el-form-item>
+            <el-form-item>
+              <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
+              <el-button icon="Refresh" @click="resetQuery">重置</el-button>
+            </el-form-item>
+          </el-form>
+        </el-card>
+      </div>
+    </transition>
+
+    <el-card shadow="hover">
+      <template #header>
+        <el-row :gutter="10">
+          <el-col :span="1.5">
+            <el-button v-has-permi="['riskctrl:whitelist:add']" type="primary" plain icon="Plus" @click="handleAdd()">新增</el-button>
+          </el-col>
+          <el-col :span="1.5">
+            <el-button v-has-permi="['riskctrl:whitelist:edit']" type="success" plain :disabled="single" icon="Edit" @click="handleUpdate()">修改</el-button>
+          </el-col>
+          <el-col :span="1.5">
+            <el-button v-has-permi="['riskctrl:whitelist:remove']" type="danger" plain :disabled="multiple" icon="Delete" @click="handleDelete()">删除</el-button>
+          </el-col>
+          <right-toolbar v-model:show-search="showSearch" :columns="columns" :search="true" @query-table="getList"></right-toolbar>
+        </el-row>
+      </template>
+
+      <el-table v-loading="loading" border :data="whitelistList" @selection-change="handleSelectionChange">
+        <el-table-column type="selection" width="50" align="center" />
+        <el-table-column v-if="columns[0].visible" key="whiteId" label="编号" align="center" prop="whiteId" />
+        <el-table-column v-if="columns[1].visible" key="whiteType" label="类型" align="center" prop="whiteType">
+          <template #default="scope">
+            <dict-tag :options="risk_list_type" :value="scope.row.whiteType" />
+          </template>
+        </el-table-column>
+        <el-table-column v-if="columns[2].visible" key="whiteValue" label="白名单值" align="center" prop="whiteValue" :show-overflow-tooltip="true" />
+        <el-table-column v-if="columns[3].visible" key="whiteName" label="名称" align="center" prop="whiteName" :show-overflow-tooltip="true" />
+        <el-table-column v-if="columns[4].visible" key="reason" label="原因" align="center" prop="reason" :show-overflow-tooltip="true" />
+        <el-table-column v-if="columns[5].visible" key="source" label="来源" align="center" prop="source" />
+        <el-table-column v-if="columns[6].visible" key="expireTime" label="过期时间" align="center" prop="expireTime" width="160" />
+        <el-table-column v-if="columns[7].visible" key="status" label="状态" align="center" prop="status">
+          <template #default="scope">
+            <el-switch v-model="scope.row.status" active-value="0" inactive-value="1" @change="handleStatusChange(scope.row)"></el-switch>
+          </template>
+        </el-table-column>
+        <el-table-column v-if="columns[8].visible" label="创建时间" align="center" prop="createTime" width="160" />
+        <el-table-column label="操作" fixed="right" width="180" class-name="small-padding fixed-width">
+          <template #default="scope">
+            <el-tooltip content="修改" placement="top">
+              <el-button v-hasPermi="['riskctrl:whitelist:edit']" link type="primary" icon="Edit" @click="handleUpdate(scope.row)"></el-button>
+            </el-tooltip>
+            <el-tooltip content="删除" placement="top">
+              <el-button v-hasPermi="['riskctrl:whitelist:remove']" link type="primary" icon="Delete" @click="handleDelete(scope.row)"></el-button>
+            </el-tooltip>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <pagination v-show="total > 0" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" :total="total" @pagination="getList" />
+    </el-card>
+
+    <!-- 添加或修改白名单对话框 -->
+    <el-dialog v-model="open" :title="title" width="600px" append-to-body>
+      <el-form ref="whitelistFormRef" :model="form" :rules="rules" label-width="100px">
+        <el-form-item label="白名单类型" prop="whiteType">
+          <el-select v-model="form.whiteType" placeholder="请选择白名单类型">
+            <el-option v-for="dict in risk_list_type" :key="dict.value" :label="dict.label" :value="dict.value" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="白名单值" prop="whiteValue">
+          <el-input v-model="form.whiteValue" placeholder="请输入白名单值" />
+        </el-form-item>
+        <el-form-item label="名称" prop="whiteName">
+          <el-input v-model="form.whiteName" placeholder="请输入名称" />
+        </el-form-item>
+        <el-form-item label="加入原因" prop="reason">
+          <el-input v-model="form.reason" type="textarea" placeholder="请输入加入原因" />
+        </el-form-item>
+        <el-form-item label="来源" prop="source">
+          <el-input v-model="form.source" placeholder="请输入来源" />
+        </el-form-item>
+        <el-form-item label="过期时间" prop="expireTime">
+          <el-date-picker v-model="form.expireTime" type="datetime" placeholder="选择过期时间" value-format="YYYY-MM-DD HH:mm:ss" />
+        </el-form-item>
+        <el-form-item label="状态" prop="status">
+          <el-radio-group v-model="form.status">
+            <el-radio v-for="dict in sys_normal_disable" :key="dict.value" :label="dict.value">{{ dict.label }}</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="备注" prop="remark">
+          <el-input v-model="form.remark" type="textarea" placeholder="请输入备注" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button type="primary" @click="submitForm">确 定</el-button>
+          <el-button @click="cancel">取 消</el-button>
+        </div>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup name="Whitelist" lang="ts">
+import { listWhitelist, getWhitelist, delWhitelist, addWhitelist, updateWhitelist, changeWhitelistStatus } from '@/api/riskctrl/whitelist';
+import { WhitelistForm, WhitelistQuery, WhitelistVO } from '@/api/riskctrl/whitelist/types';
+
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+const { risk_list_type, sys_normal_disable } = proxy.useDict('risk_list_type', 'sys_normal_disable');
+
+const whitelistList = ref<WhitelistVO[]>([]);
+const loading = ref(true);
+const showSearch = ref(true);
+const ids = ref<Array<string | number>>([]);
+const single = ref(true);
+const multiple = ref(true);
+const total = ref(0);
+const title = ref('');
+const open = ref(false);
+
+const queryFormRef = ref<ElFormInstance>();
+const whitelistFormRef = ref<ElFormInstance>();
+
+const queryParams = reactive<WhitelistQuery>({
+  pageNum: 1,
+  pageSize: 10,
+  whiteType: undefined,
+  whiteValue: undefined,
+  whiteName: undefined,
+  status: undefined
+});
+
+const form = reactive<WhitelistForm>({
+  whiteId: undefined,
+  whiteType: '',
+  whiteValue: '',
+  whiteName: '',
+  reason: '',
+  source: '',
+  expireTime: '',
+  status: '0',
+  remark: ''
+});
+
+const rules = {
+  whiteType: [{ required: true, message: '白名单类型不能为空', trigger: 'change' }],
+  whiteValue: [{ required: true, message: '白名单值不能为空', trigger: 'blur' }]
+};
+
+const columns = ref([
+  { key: 0, label: `编号`, visible: true },
+  { key: 1, label: `类型`, visible: true },
+  { key: 2, label: `白名单值`, visible: true },
+  { key: 3, label: `名称`, visible: true },
+  { key: 4, label: `原因`, visible: true },
+  { key: 5, label: `来源`, visible: true },
+  { key: 6, label: `过期时间`, visible: true },
+  { key: 7, label: `状态`, visible: true },
+  { key: 8, label: `创建时间`, visible: true }
+]);
+
+const getList = async () => {
+  loading.value = true;
+  const res = await listWhitelist(queryParams);
+  whitelistList.value = res.rows;
+  total.value = res.total;
+  loading.value = false;
+};
+
+const cancel = () => {
+  open.value = false;
+  reset();
+};
+
+const reset = () => {
+  form.whiteId = undefined;
+  form.whiteType = '';
+  form.whiteValue = '';
+  form.whiteName = '';
+  form.reason = '';
+  form.source = '';
+  form.expireTime = '';
+  form.status = '0';
+  form.remark = '';
+  nextTick(() => {
+    whitelistFormRef.value?.resetFields();
+  });
+};
+
+const handleQuery = () => {
+  queryParams.pageNum = 1;
+  getList();
+};
+
+const resetQuery = () => {
+  queryFormRef.value?.resetFields();
+  handleQuery();
+};
+
+const handleSelectionChange = (selection: WhitelistVO[]) => {
+  ids.value = selection.map((item) => item.whiteId);
+  single.value = selection.length !== 1;
+  multiple.value = !selection.length;
+};
+
+const handleAdd = () => {
+  reset();
+  open.value = true;
+  title.value = '添加白名单';
+};
+
+const handleUpdate = async (row?: WhitelistVO) => {
+  reset();
+  const whiteId = row?.whiteId || ids.value[0];
+  const res = await getWhitelist(whiteId);
+  Object.assign(form, res.data);
+  open.value = true;
+  title.value = '修改白名单';
+};
+
+const submitForm = () => {
+  whitelistFormRef.value?.validate(async (valid: boolean) => {
+    if (valid) {
+      if (form.whiteId !== undefined) {
+        await updateWhitelist(form);
+        proxy?.$modal.msgSuccess('修改成功');
+      } else {
+        await addWhitelist(form);
+        proxy?.$modal.msgSuccess('新增成功');
+      }
+      open.value = false;
+      await getList();
+    }
+  });
+};
+
+const handleDelete = async (row?: WhitelistVO) => {
+  const whiteIds = row?.whiteId || ids.value;
+  await proxy?.$modal.confirm('是否确认删除编号为"' + whiteIds + '"的数据项?');
+  await delWhitelist(whiteIds);
+  await getList();
+  proxy?.$modal.msgSuccess('删除成功');
+};
+
+const handleStatusChange = async (row: WhitelistVO) => {
+  const text = row.status === '0' ? '启用' : '停用';
+  try {
+    await proxy?.$modal.confirm('确认要"' + text + '""' + row.whiteValue + '"白名单吗?');
+    await changeWhitelistStatus(row.whiteId, row.status);
+    proxy?.$modal.msgSuccess(text + '成功');
+  } catch {
+    row.status = row.status === '0' ? '1' : '0';
+  }
+};
+
+onMounted(() => {
+  getList();
+});
+</script>

+ 755 - 0
src/views/riskctrl/workReport/index.vue

@@ -0,0 +1,755 @@
+<template>
+  <div class="p-2">
+    <!-- 搜索区域 -->
+    <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
+      <div v-show="showSearch" class="mb-[10px]">
+        <el-card shadow="hover">
+          <el-form ref="queryFormRef" :model="queryParams" :inline="true">
+            <el-form-item label="上报编号" prop="reportNo">
+              <el-input v-model="queryParams.reportNo" placeholder="请输入上报编号" clearable @keyup.enter="handleQuery" />
+            </el-form-item>
+            <el-form-item label="作业类型" prop="workType">
+              <el-select v-model="queryParams.workType" placeholder="请选择作业类型" clearable>
+                <el-option label="临时施工上报" value="临时施工上报" />
+                <el-option label="改造工程上报" value="改造工程上报" />
+              </el-select>
+            </el-form-item>
+            <el-form-item label="项目名称" prop="projectName">
+              <el-input v-model="queryParams.projectName" placeholder="请输入项目名称" clearable @keyup.enter="handleQuery" />
+            </el-form-item>
+            <el-form-item label="审核状态" prop="auditStatus">
+              <el-select v-model="queryParams.auditStatus" placeholder="请选择审核状态" clearable>
+                <el-option label="待审核" value="1" />
+                <el-option label="已通过" value="0" />
+                <el-option label="已驳回" value="2" />
+              </el-select>
+            </el-form-item>
+            <el-form-item>
+              <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
+              <el-button icon="Refresh" @click="resetQuery">重置</el-button>
+            </el-form-item>
+          </el-form>
+        </el-card>
+      </div>
+    </transition>
+
+    <!-- 列表区域 -->
+    <el-card shadow="hover">
+      <template #header>
+        <el-row :gutter="10">
+          <el-col :span="1.5">
+            <el-button v-has-permi="['riskctrl:workReport:add']" type="primary" plain icon="Plus" @click="handleAdd()">新增作业上报</el-button>
+          </el-col>
+          <el-col :span="1.5">
+            <el-button v-has-permi="['riskctrl:workReport:edit']" type="success" plain :disabled="single" icon="Edit" @click="handleUpdate()">修改</el-button>
+          </el-col>
+          <el-col :span="1.5">
+            <el-button type="info" plain :disabled="single" icon="View" @click="handleDetail()">详情</el-button>
+          </el-col>
+          <el-col :span="1.5">
+            <el-button v-has-permi="['riskctrl:workReport:remove']" type="danger" plain :disabled="multiple" icon="Delete" @click="handleDelete()">删除</el-button>
+          </el-col>
+          <el-col :span="1.5">
+            <el-button v-has-permi="['riskctrl:workReport:audit']" type="warning" plain :disabled="single" icon="Select" @click="handleAudit()">审核</el-button>
+          </el-col>
+          <right-toolbar v-model:show-search="showSearch" :columns="columns" :search="true" @query-table="getList"></right-toolbar>
+        </el-row>
+      </template>
+
+      <el-table v-loading="loading" border :data="reportList" @selection-change="handleSelectionChange">
+        <el-table-column type="selection" width="50" align="center" />
+        <el-table-column v-if="columns[0].visible" key="reportNo" label="上报编号" align="center" prop="reportNo" width="160" />
+        <el-table-column v-if="columns[1].visible" key="workType" label="作业类型" align="center" prop="workType" width="120" />
+        <el-table-column v-if="columns[2].visible" key="projectName" label="项目名称" align="center" prop="projectName" :show-overflow-tooltip="true" />
+        <el-table-column v-if="columns[3].visible" key="startTime" label="施工开始时间" align="center" prop="startTime" width="160" />
+        <el-table-column v-if="columns[4].visible" key="endTime" label="施工结束时间" align="center" prop="endTime" width="160" />
+        <el-table-column v-if="columns[5].visible" key="safetyOfficer" label="现场安全员" align="center" prop="safetyOfficer" width="120" />
+        <el-table-column v-if="columns[6].visible" key="contactPhone" label="联系电话" align="center" prop="contactPhone" width="130" />
+        <el-table-column v-if="columns[7].visible" key="auditStatus" label="审核状态" align="center" prop="auditStatus" width="100">
+          <template #default="scope">
+            <el-tag v-if="scope.row.auditStatus === '0'" type="success">已通过</el-tag>
+            <el-tag v-else-if="scope.row.auditStatus === '1'" type="warning">待审核</el-tag>
+            <el-tag v-else-if="scope.row.auditStatus === '2'" type="danger">已驳回</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column v-if="columns[8].visible" label="创建时间" align="center" prop="createTime" width="160" />
+        <el-table-column label="操作" fixed="right" width="260" class-name="small-padding fixed-width">
+          <template #default="scope">
+            <el-tooltip content="详情" placement="top">
+              <el-button link type="info" icon="View" @click="handleDetail(scope.row)"></el-button>
+            </el-tooltip>
+            <el-tooltip content="修改" placement="top">
+              <el-button v-has-permi="['riskctrl:workReport:edit']" link type="primary" icon="Edit" @click="handleUpdate(scope.row)"></el-button>
+            </el-tooltip>
+            <el-tooltip content="审核" placement="top">
+              <el-button v-has-permi="['riskctrl:workReport:audit']" link type="warning" icon="Select" @click="handleAudit(scope.row)"></el-button>
+            </el-tooltip>
+            <el-tooltip content="删除" placement="top">
+              <el-button v-has-permi="['riskctrl:workReport:remove']" link type="primary" icon="Delete" @click="handleDelete(scope.row)"></el-button>
+            </el-tooltip>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <pagination v-show="total > 0" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" :total="total" @pagination="getList" />
+    </el-card>
+
+    <!-- 新增/修改弹窗 -->
+    <el-dialog v-model="open" :title="title" width="800px" append-to-body :close-on-click-modal="false" destroy-on-close>
+      <el-form ref="reportFormRef" :model="form" :rules="rules" label-width="120px">
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="作业类型" prop="workType">
+              <el-select v-model="form.workType" placeholder="请选择作业类型" class="w-full">
+                <el-option label="临时施工上报" value="临时施工上报" />
+                <el-option label="改造工程上报" value="改造工程上报" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="项目名称" prop="projectName">
+              <el-input v-model="form.projectName" placeholder="请输入项目名称" maxlength="200" show-word-limit />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="施工开始时间" prop="startTime">
+              <el-date-picker
+                v-model="form.startTime"
+                type="datetime"
+                value-format="YYYY-MM-DD HH:mm:ss"
+                placeholder="请选择施工开始时间"
+                class="w-full"
+                :disabled-date="startDisabledDate"
+              />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="施工结束时间" prop="endTime">
+              <el-date-picker
+                v-model="form.endTime"
+                type="datetime"
+                value-format="YYYY-MM-DD HH:mm:ss"
+                placeholder="请选择施工结束时间"
+                class="w-full"
+                :disabled-date="endDisabledDate"
+              />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="现场安全员" prop="safetyOfficer">
+              <el-input v-model="form.safetyOfficer" placeholder="请输入现场安全员姓名" maxlength="50" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="联系电话" prop="contactPhone">
+              <el-input v-model="form.contactPhone" placeholder="请输入联系电话" maxlength="20" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-form-item label="现场施工图片" prop="workImages">
+          <el-upload
+            v-model:file-list="workImageList"
+            :action="uploadUrl"
+            :headers="uploadHeaders"
+            list-type="picture-card"
+            :accept="'image/jpeg,image/png'"
+            :limit="5"
+            :on-success="handleWorkImageSuccess"
+            :on-remove="handleWorkImageRemove"
+            :on-exceed="handleImageExceed"
+            :before-upload="beforeWorkImageUpload"
+          >
+            <el-icon><Plus /></el-icon>
+            <template #tip>
+              <div class="el-upload__tip">点击上传现场施工图片,仅支持 JPG/PNG 格式,单张不超过 5MB,最多 5 张</div>
+            </template>
+          </el-upload>
+        </el-form-item>
+        <el-form-item label="施工证件" prop="permitFiles">
+          <el-upload
+            ref="permitUploadRef"
+            v-model:file-list="permitFileList"
+            :action="uploadUrl"
+            :headers="uploadHeaders"
+            :accept="'.pdf,.jpg,.jpeg,.png'"
+            :limit="1"
+            :on-success="handlePermitFileSuccess"
+            :on-remove="handlePermitFileRemove"
+            :before-upload="beforePermitFileUpload"
+          >
+            <el-button type="primary">点击上传施工证件</el-button>
+            <template #tip>
+              <div class="el-upload__tip">支持 PDF、JPG、PNG 格式,单个文件不超过 10MB</div>
+            </template>
+            <template #file="{ file }">
+              <div class="permit-file-item">
+                <el-icon style="margin-right: 8px;"><Document /></el-icon>
+                <el-link type="primary" :href="file.url || file.response?.data?.url" target="_blank" :underline="false">
+                  {{ file.name }}
+                </el-link>
+                <span class="permit-file-remove" @click="handlePermitFileRemove(file)">
+                  <el-icon><Close /></el-icon>
+                </span>
+              </div>
+            </template>
+          </el-upload>
+        </el-form-item>
+        <el-form-item label="施工说明" prop="workDesc">
+          <el-input
+            v-model="form.workDesc"
+            type="textarea"
+            :rows="4"
+            placeholder="请输入施工说明,补充作业细节、风险点等"
+            maxlength="500"
+            show-word-limit
+          />
+        </el-form-item>
+        <el-form-item label="备注" prop="remark">
+          <el-input v-model="form.remark" type="textarea" :rows="2" placeholder="请输入备注" maxlength="500" show-word-limit />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button type="primary" :loading="buttonLoading" @click="submitForm">确 定</el-button>
+          <el-button @click="cancel">取 消</el-button>
+        </div>
+      </template>
+    </el-dialog>
+
+    <!-- 审核弹窗 -->
+    <el-dialog v-model="auditOpen" title="作业审核" width="600px" append-to-body :close-on-click-modal="false" destroy-on-close>
+      <el-form ref="auditFormRef" :model="auditForm" :rules="auditRules" label-width="100px">
+        <el-form-item label="审核结果" prop="auditStatus">
+          <el-radio-group v-model="auditForm.auditStatus">
+            <el-radio value="0">审核通过</el-radio>
+            <el-radio value="2">审核不通过</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="审核意见" prop="auditComment">
+          <el-input v-model="auditForm.auditComment" type="textarea" :rows="4" placeholder="请输入审核意见" maxlength="500" show-word-limit />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button type="primary" @click="submitAudit">确 定</el-button>
+          <el-button @click="auditOpen = false">取 消</el-button>
+        </div>
+      </template>
+    </el-dialog>
+
+    <!-- 详情弹窗 -->
+    <el-dialog v-model="detailOpen" title="作业上报详情" width="800px" append-to-body :close-on-click-modal="false" destroy-on-close>
+      <el-descriptions :column="2" border size="default">
+        <el-descriptions-item label="上报编号">{{ detailData.reportNo }}</el-descriptions-item>
+        <el-descriptions-item label="作业类型">{{ detailData.workType }}</el-descriptions-item>
+        <el-descriptions-item label="项目名称">{{ detailData.projectName }}</el-descriptions-item>
+        <el-descriptions-item label="审核状态">
+          <el-tag v-if="detailData.auditStatus === '0'" type="success">已通过</el-tag>
+          <el-tag v-else-if="detailData.auditStatus === '1'" type="warning">待审核</el-tag>
+          <el-tag v-else-if="detailData.auditStatus === '2'" type="danger">已驳回</el-tag>
+        </el-descriptions-item>
+        <el-descriptions-item label="施工开始时间">{{ detailData.startTime }}</el-descriptions-item>
+        <el-descriptions-item label="施工结束时间">{{ detailData.endTime }}</el-descriptions-item>
+        <el-descriptions-item label="现场安全员">{{ detailData.safetyOfficer }}</el-descriptions-item>
+        <el-descriptions-item label="联系电话">{{ detailData.contactPhone }}</el-descriptions-item>
+        <el-descriptions-item v-if="detailData.auditStatus !== '1'" label="审核意见" :span="2">{{ detailData.auditComment || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="施工说明" :span="2">{{ detailData.workDesc || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="备注" :span="2">{{ detailData.remark || '-' }}</el-descriptions-item>
+      </el-descriptions>
+
+      <el-divider content-position="left">现场施工图片</el-divider>
+      <div v-if="detailImageUrls.length > 0" style="display: flex; flex-wrap: wrap; gap: 10px;">
+        <el-image
+          v-for="(url, idx) in detailImageUrls"
+          :key="idx"
+          :src="url"
+          :preview-src-list="detailImageUrls"
+          :initial-index="idx"
+          fit="cover"
+          style="width: 120px; height: 120px; border-radius: 6px; cursor: pointer;"
+        />
+      </div>
+      <el-empty v-else description="暂无施工图片" :image-size="60" />
+
+      <el-divider content-position="left">施工证件</el-divider>
+      <div v-if="detailPermitUrls.length > 0">
+        <div v-for="(url, idx) in detailPermitUrls" :key="idx" style="margin-bottom: 8px;">
+          <el-link type="primary" :href="url" target="_blank">
+            <el-icon><Document /></el-icon>
+            {{ getFileNameFromUrl(url) }}
+          </el-link>
+          <el-tag v-if="isPdfUrl(url)" size="small" type="info" style="margin-left: 8px;">PDF</el-tag>
+          <el-tag v-else size="small" type="success" style="margin-left: 8px;">图片</el-tag>
+        </div>
+      </div>
+      <el-empty v-else description="暂无施工证件" :image-size="60" />
+
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button @click="detailOpen = false">关 闭</el-button>
+        </div>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup name="WorkReport" lang="ts">
+import { ref, reactive, onMounted, nextTick } from 'vue';
+import type { ComponentInternalInstance } from 'vue';
+import type { ElFormInstance } from 'element-plus';
+import { Plus, Document, Close } from '@element-plus/icons-vue';
+import { getCurrentInstance } from 'vue';
+import {
+  listWorkReport, getWorkReport, delWorkReport,
+  addWorkReport, updateWorkReport, auditWorkReport
+} from '@/api/riskctrl/workReport';
+import type { WorkReportVO } from '@/api/riskctrl/workReport/types';
+import { getToken } from '@/utils/auth';
+
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+
+const reportList = ref<WorkReportVO[]>([]);
+const loading = ref(true);
+const buttonLoading = ref(false);
+const showSearch = ref(true);
+const ids = ref<Array<string | number>>([]);
+const single = ref(true);
+const multiple = ref(true);
+const total = ref(0);
+const title = ref('');
+const open = ref(false);
+const auditOpen = ref(false);
+
+const queryFormRef = ref<ElFormInstance>();
+const reportFormRef = ref<ElFormInstance>();
+const auditFormRef = ref<ElFormInstance>();
+
+const uploadUrl = ref(import.meta.env.VITE_APP_BASE_API + '/resource/oss/upload');
+const uploadHeaders = ref({ Authorization: 'Bearer ' + getToken(), clientid: import.meta.env.VITE_APP_CLIENT_ID });
+
+const workImageList = ref<any[]>([]);
+const permitFileList = ref<any[]>([]);
+
+interface ReportQuery {
+  pageNum: number;
+  pageSize: number;
+  reportNo?: string;
+  projectName?: string;
+  workType?: string;
+  auditStatus?: string;
+}
+
+interface ReportForm {
+  reportId?: string | number;
+  reportNo?: string;
+  workType: string;
+  projectName: string;
+  startTime: string;
+  endTime: string;
+  safetyOfficer: string;
+  contactPhone: string;
+  workImages: string;
+  permitFiles: string;
+  workDesc: string;
+  remark: string;
+}
+
+interface AuditFormType {
+  reportId?: string | number;
+  auditStatus: string;
+  auditComment: string;
+}
+
+const queryParams = reactive<ReportQuery>({
+  pageNum: 1,
+  pageSize: 10,
+  reportNo: undefined,
+  projectName: undefined,
+  workType: undefined,
+  auditStatus: undefined
+});
+
+const form = reactive<ReportForm>({
+  reportId: undefined,
+  reportNo: '',
+  workType: '',
+  projectName: '',
+  startTime: '',
+  endTime: '',
+  safetyOfficer: '',
+  contactPhone: '',
+  workImages: '',
+  permitFiles: '',
+  workDesc: '',
+  remark: ''
+});
+
+const auditForm = reactive<AuditFormType>({
+  reportId: undefined,
+  auditStatus: '',
+  auditComment: ''
+});
+
+// 详情弹窗
+const detailOpen = ref(false);
+const detailData = ref<WorkReportVO>({});
+const detailImageUrls = ref<string[]>([]);
+const detailPermitUrls = ref<string[]>([]);
+
+/** 手机号/固话验证 */
+const validatePhone = (_rule: any, value: string, callback: any) => {
+  if (!value) {
+    callback();
+    return;
+  }
+  const mobile = /^1[3-9]\d{9}$/;
+  const phone = /^0\d{2,3}-?\d{7,8}$/;
+  if (mobile.test(value) || phone.test(value)) {
+    callback();
+  } else {
+    callback(new Error('请输入正确的手机号或固定电话格式'));
+  }
+};
+
+const rules = {
+  workType: [{ required: true, message: '请选择作业类型', trigger: 'change' }],
+  projectName: [{ required: true, message: '请输入项目名称', trigger: 'blur' }],
+  startTime: [{ required: true, message: '请选择施工开始时间', trigger: 'change' }],
+  endTime: [
+    { required: true, message: '请选择施工结束时间', trigger: 'change' },
+    {
+      validator: (_rule: any, value: string, callback: any) => {
+        if (value && form.startTime && value <= form.startTime) {
+          callback(new Error('施工结束时间必须晚于施工开始时间'));
+        } else {
+          callback();
+        }
+      },
+      trigger: 'change'
+    }
+  ],
+  safetyOfficer: [{ required: true, message: '请输入现场安全员姓名', trigger: 'blur' }],
+  contactPhone: [{ validator: validatePhone, trigger: 'blur' }]
+};
+
+const auditRules = {
+  auditStatus: [{ required: true, message: '请选择审核结果', trigger: 'change' }]
+};
+
+const columns = ref([
+  { key: 0, label: '上报编号', visible: true },
+  { key: 1, label: '作业类型', visible: true },
+  { key: 2, label: '项目名称', visible: true },
+  { key: 3, label: '施工开始时间', visible: true },
+  { key: 4, label: '施工结束时间', visible: true },
+  { key: 5, label: '现场安全员', visible: true },
+  { key: 6, label: '联系电话', visible: true },
+  { key: 7, label: '审核状态', visible: true },
+  { key: 8, label: '创建时间', visible: true }
+]);
+
+/** 施工开始时间禁用未来日期(可选:不晚于当前时间) */
+const startDisabledDate = (date: Date) => {
+  // return date.getTime() > Date.now(); // 开启则禁止选择未来时间
+  return false;
+};
+
+/** 施工结束时间不能早于开始时间 */
+const endDisabledDate = (date: Date) => {
+  if (form.startTime) {
+    return date.getTime() < new Date(form.startTime).getTime();
+  }
+  return false;
+};
+
+/** 获取列表 */
+const getList = async () => {
+  loading.value = true;
+  try {
+    const res: any = await listWorkReport(queryParams as any);
+    reportList.value = res.rows || [];
+    total.value = res.total || 0;
+  } catch (e) {
+    console.error('加载作业上报列表失败', e);
+  } finally {
+    loading.value = false;
+  }
+};
+
+const cancel = () => {
+  open.value = false;
+  reset();
+};
+
+const reset = () => {
+  form.reportId = undefined;
+  form.reportNo = '';
+  form.workType = '';
+  form.projectName = '';
+  form.startTime = '';
+  form.endTime = '';
+  form.safetyOfficer = '';
+  form.contactPhone = '';
+  form.workImages = '';
+  form.permitFiles = '';
+  form.workDesc = '';
+  form.remark = '';
+  workImageList.value = [];
+  permitFileList.value = [];
+  nextTick(() => {
+    reportFormRef.value?.resetFields();
+  });
+};
+
+const handleQuery = () => {
+  queryParams.pageNum = 1;
+  getList();
+};
+
+const resetQuery = () => {
+  queryFormRef.value?.resetFields();
+  handleQuery();
+};
+
+const handleSelectionChange = (selection: WorkReportVO[]) => {
+  ids.value = selection.map((item) => item.reportId);
+  single.value = selection.length !== 1;
+  multiple.value = !selection.length;
+};
+
+/** 新增 */
+const handleAdd = () => {
+  reset();
+  open.value = true;
+  title.value = '新增作业上报表单';
+};
+
+/** 修改 */
+const handleUpdate = async (row?: WorkReportVO) => {
+  reset();
+  const reportId = row?.reportId || ids.value[0];
+  const res: any = await getWorkReport(reportId);
+  const data = res.data;
+  Object.assign(form, data);
+  // 回显图片列表
+  if (data.workImages) {
+    const urls = data.workImages.split(',').filter(Boolean);
+    workImageList.value = urls.map((url: string, idx: number) => ({
+      name: 'image_' + idx,
+      url: url,
+      response: { data: { url: url } }
+    }));
+  }
+  // 回显证件列表
+  if (data.permitFiles) {
+    const urls = data.permitFiles.split(',').filter(Boolean);
+    permitFileList.value = urls.map((url: string, idx: number) => ({
+      name: 'permit_' + idx,
+      url: url,
+      response: { data: { url: url } }
+    }));
+  }
+  open.value = true;
+  title.value = '修改作业上报';
+};
+
+/** 详情查看 */
+const handleDetail = async (row?: WorkReportVO) => {
+  const reportId = row?.reportId || ids.value[0];
+  try {
+    const res: any = await getWorkReport(reportId);
+    detailData.value = res.data;
+    // 解析图片和证件URL
+    detailImageUrls.value = res.data.workImages
+      ? res.data.workImages.split(',').filter(Boolean)
+      : [];
+    detailPermitUrls.value = res.data.permitFiles
+      ? res.data.permitFiles.split(',').filter(Boolean)
+      : [];
+    detailOpen.value = true;
+  } catch (e) {
+    console.error('获取详情失败', e);
+  }
+};
+
+/** 从URL提取文件名 */
+const getFileNameFromUrl = (url: string) => {
+  if (!url) return '未知文件';
+  const parts = url.split('/');
+  return parts[parts.length - 1] || '未知文件';
+};
+
+/** 判断是否为PDF文件 */
+const isPdfUrl = (url: string) => {
+  return url?.toLowerCase().endsWith('.pdf');
+};
+
+/** 提交表单 */
+const submitForm = () => {
+  reportFormRef.value?.validate(async (valid: boolean) => {
+    if (valid) {
+      buttonLoading.value = true;
+      try {
+        // 拼接图片路径
+        form.workImages = workImageList.value
+          .filter((f: any) => f.response?.data?.url || f.url)
+          .map((f: any) => f.response?.data?.url || f.url)
+          .join(',');
+        // 拼接证件路径
+        form.permitFiles = permitFileList.value
+          .filter((f: any) => f.response?.data?.url || f.url)
+          .map((f: any) => f.response?.data?.url || f.url)
+          .join(',');
+
+        if (form.reportId) {
+          await updateWorkReport(form as any);
+          proxy?.$modal.msgSuccess('修改成功');
+        } else {
+          await addWorkReport(form as any);
+          proxy?.$modal.msgSuccess('新增成功');
+        }
+        open.value = false;
+        await getList();
+      } catch (e) {
+        console.error('提交失败', e);
+      } finally {
+        buttonLoading.value = false;
+      }
+    }
+  });
+};
+
+/** 图片上传成功 */
+const handleWorkImageSuccess = (response: any, _file: any) => {
+  if (response.code !== 200) {
+    proxy?.$modal.msgError(response.msg || '上传失败');
+  }
+};
+
+/** 图片移除 */
+const handleWorkImageRemove = (_file: any) => {
+  // 从列表中移除
+};
+
+/** 超出图片数量限制 */
+const handleImageExceed = () => {
+  proxy?.$modal.msgError('最多只能上传 5 张图片');
+};
+
+/** 图片上传前校验 */
+const beforeWorkImageUpload = (file: any) => {
+  const isJpgOrPng = file.type === 'image/jpeg' || file.type === 'image/png';
+  if (!isJpgOrPng) {
+    proxy?.$modal.msgError('仅支持 JPG、PNG 格式的图片');
+    return false;
+  }
+  const isLt5M = file.size / 1024 / 1024 < 5;
+  if (!isLt5M) {
+    proxy?.$modal.msgError('单张图片大小不能超过 5MB');
+    return false;
+  }
+  return true;
+};
+
+/** 证件上传成功 */
+const handlePermitFileSuccess = (response: any, _file: any) => {
+  if (response.code !== 200) {
+    proxy?.$modal.msgError(response.msg || '上传失败');
+  }
+};
+
+/** 证件移除 */
+const handlePermitFileRemove = (_file: any) => {};
+
+/** 证件上传前校验 */
+const beforePermitFileUpload = (file: any) => {
+  const allowedTypes = ['application/pdf', 'image/jpeg', 'image/png'];
+  if (!allowedTypes.includes(file.type)) {
+    proxy?.$modal.msgError('仅支持 PDF、JPG、PNG 格式的文件');
+    return false;
+  }
+  const isLt10M = file.size / 1024 / 1024 < 10;
+  if (!isLt10M) {
+    proxy?.$modal.msgError('文件大小不能超过 10MB');
+    return false;
+  }
+  return true;
+};
+
+/** 审核 */
+const handleAudit = async (row?: WorkReportVO) => {
+  const reportId = row?.reportId || ids.value[0];
+  const res: any = await getWorkReport(reportId);
+  auditForm.reportId = res.data.reportId;
+  auditForm.auditStatus = '';
+  auditForm.auditComment = '';
+  auditOpen.value = true;
+};
+
+/** 提交审核 */
+const submitAudit = () => {
+  auditFormRef.value?.validate(async (valid: boolean) => {
+    if (valid) {
+      try {
+        await auditWorkReport({
+          reportId: auditForm.reportId,
+          auditStatus: auditForm.auditStatus,
+          auditComment: auditForm.auditComment
+        } as any);
+        proxy?.$modal.msgSuccess('审核完成');
+        auditOpen.value = false;
+        await getList();
+      } catch (e) {
+        console.error('审核失败', e);
+      }
+    }
+  });
+};
+
+/** 删除 */
+const handleDelete = async (row?: WorkReportVO) => {
+  const reportIds = row?.reportId || ids.value;
+  await proxy?.$modal.confirm('是否确认删除选中的作业上报数据?');
+  try {
+    await delWorkReport(reportIds);
+    proxy?.$modal.msgSuccess('删除成功');
+    await getList();
+  } catch (e) {
+    console.error('删除失败', e);
+  }
+};
+
+onMounted(() => {
+  getList();
+});
+</script>
+
+<style scoped>
+.permit-file-item {
+  display: flex;
+  align-items: center;
+  padding: 6px 10px;
+  border: 1px solid var(--el-border-color);
+  border-radius: 6px;
+  margin-top: 6px;
+  background: var(--el-fill-color-light);
+}
+.permit-file-item .el-link {
+  flex: 1;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+.permit-file-remove {
+  cursor: pointer;
+  color: var(--el-text-color-secondary);
+  margin-left: 8px;
+}
+.permit-file-remove:hover {
+  color: var(--el-color-danger);
+}
+</style>

+ 1 - 1
vite.config.ts

@@ -24,7 +24,7 @@ export default defineConfig(({ mode, command }) => {
       open: true,
       proxy: {
         [env.VITE_APP_BASE_API]: {
-          target: 'http://localhost:8080',
+          target: 'http://localhost:9224',
           changeOrigin: true,
           ws: true,
           rewrite: (path) => path.replace(new RegExp('^' + env.VITE_APP_BASE_API), '')