Browse Source

流程更新

master
whyzxhnd 2 weeks ago
parent
commit
9613d2cbd6
  1. 2
      web/.env
  2. 3
      web/.env.dev
  3. 4
      web/.env.prod
  4. 2
      web/index.html
  5. 13962
      web/package-lock.json
  6. 5
      web/package.json
  7. 2154
      web/pnpm-lock.yaml
  8. BIN
      web/public/favicon.ico
  9. BIN
      web/public/logo.gif
  10. BIN
      web/public/logo.png
  11. 52
      web/src/api/electron/lock/index.ts
  12. 53
      web/src/api/electron/lockworkcord/index.ts
  13. 47
      web/src/api/guide/isolationpoint/index.ts
  14. 54
      web/src/api/guide/lockguide/index.ts
  15. 47
      web/src/api/isolation/plan/index.ts
  16. 52
      web/src/api/isolation/planitem/index.ts
  17. 49
      web/src/api/isolation/planitemdetail/index.ts
  18. 51
      web/src/api/isolation/planlifelock/index.ts
  19. 51
      web/src/api/isolation/point/index.ts
  20. 66
      web/src/api/lock/index.ts
  21. 2
      web/src/api/login/types.ts
  22. BIN
      web/src/assets/imgs/logo.png
  23. BIN
      web/src/assets/imgs/user.png
  24. 2
      web/src/components/Cropper/src/CropperAvatar.vue
  25. 5
      web/src/components/DictTag/src/DictTag.vue
  26. 2
      web/src/components/DiyEditor/index.vue
  27. 208
      web/src/components/Lock/DetailPointModal.vue
  28. 390
      web/src/components/LogicFlow/LockPointNode.vue
  29. 129
      web/src/components/LogicFlow/LockPointNodeAdapter.ts
  30. 2
      web/src/layout/components/Footer/src/Footer.vue
  31. 17
      web/src/layout/components/Menu/src/Menu.vue
  32. 2
      web/src/layout/components/Message/src/Message.vue
  33. 2
      web/src/layout/components/UserInfo/src/UserInfo.vue
  34. 2
      web/src/layout/components/UserInfo/src/components/LockDialog.vue
  35. 2
      web/src/layout/components/UserInfo/src/components/LockPage.vue
  36. 3
      web/src/locales/en.ts
  37. 3
      web/src/locales/zh-CN.ts
  38. 7
      web/src/main.ts
  39. 3
      web/src/store/modules/app.ts
  40. 126
      web/src/store/modules/elLock.ts
  41. 4
      web/src/store/modules/user.ts
  42. 565
      web/src/utils/bluetooth.ts
  43. 15
      web/src/utils/dict.ts
  44. 334
      web/src/utils/lock.old.js
  45. 300
      web/src/utils/lock.ts
  46. 1855
      web/src/utils/ww.ts
  47. 7
      web/src/views/Home/Index.vue
  48. 14
      web/src/views/Login/Login.vue
  49. 10
      web/src/views/Login/components/LoginForm.vue
  50. 23
      web/src/views/Login/components/RegisterForm.vue
  51. 2
      web/src/views/Profile/components/UserAvatar.vue
  52. 1457
      web/src/views/lock/grouplock/components/BluetoothPanel.vue
  53. 225
      web/src/views/lock/grouplock/components/MobileGroupLock.vue
  54. 377
      web/src/views/lock/grouplock/components/PCGroupLock.vue
  55. 670
      web/src/views/lock/grouplock/components/PlanCard.vue
  56. 283
      web/src/views/lock/grouplock/grouplock.vue
  57. 115
      web/src/views/lock/guide/isolationpoint/IsolationPointForm.vue
  58. 257
      web/src/views/lock/guide/isolationpoint/index.vue
  59. 232
      web/src/views/lock/guide/isolationpoint/lockGuide.vue
  60. 157
      web/src/views/lock/guide/lockguide/LockGuideForm.vue
  61. 259
      web/src/views/lock/guide/lockguide/LockGuideFormNew.vue
  62. 244
      web/src/views/lock/guide/lockguide/index.vue
  63. 903
      web/src/views/lock/isolation/plan/PlanForm.vue
  64. 92
      web/src/views/lock/isolation/plan/PlanFormOld.vue
  65. 228
      web/src/views/lock/isolation/plan/index.vue
  66. 188
      web/src/views/lock/isolation/planitem/PlanItemForm.vue
  67. 343
      web/src/views/lock/isolation/planitem/index.vue
  68. 144
      web/src/views/lock/isolation/planitemdetail/PlanItemDetailForm.vue
  69. 294
      web/src/views/lock/isolation/planitemdetail/index.vue
  70. 162
      web/src/views/lock/isolation/planlifelock/PlanLifeLockForm.vue
  71. 305
      web/src/views/lock/isolation/planlifelock/index.vue
  72. 125
      web/src/views/lock/isolation/point/PointForm.vue
  73. 254
      web/src/views/lock/isolation/point/index.vue
  74. 1263
      web/src/views/lock/lifelock/lifelock.vue
  75. 124
      web/src/views/lock/lock/LockForm.vue
  76. 208
      web/src/views/lock/lock/index.vue
  77. 85
      web/src/views/lock/monitorTable.vue
  78. 171
      web/src/views/lock/record/LockWorkRecordForm.vue
  79. 296
      web/src/views/lock/record/index.vue
  80. 20
      web/vite.config.ts

2
web/.env

@ -1,5 +1,5 @@
# 标题
VITE_APP_TITLE=管理系统
VITE_APP_TITLE=挂牌上锁平台
# 项目本地运行端口号
VITE_PORT=80

3
web/.env.dev

@ -3,7 +3,8 @@ NODE_ENV=production
VITE_DEV=true
# 请求路径
VITE_BASE_URL='http://192.168.0.129:48080'
VITE_BASE_URL='https://lock.zdhlcn.com:9807'
# VITE_BASE_URL='http://192.168.0.129:48080'
# 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持S3服务
VITE_UPLOAD_TYPE=server

4
web/.env.prod

@ -3,7 +3,7 @@ NODE_ENV=production
VITE_DEV=false
# 请求路径
VITE_BASE_URL='http://localhost:48080'
VITE_BASE_URL='https://lock.zdhlcn.com:9807'
# 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持S3服务
VITE_UPLOAD_TYPE=server
@ -15,7 +15,7 @@ VITE_API_URL=/admin-api
VITE_DROP_DEBUGGER=true
# 是否删除console.log
VITE_DROP_CONSOLE=true
VITE_DROP_CONSOLE=false
# 是否sourcemap
VITE_SOURCEMAP=false

2
web/index.html

@ -128,7 +128,7 @@
<div class="app-loading">
<div class="app-loading-wrap">
<div class="app-loading-title">
<img src="/logo.gif" class="app-loading-logo" alt="Logo" />
<img src="/logo.png" class="app-loading-logo" alt="Logo" />
<div class="app-loading-title">%VITE_APP_TITLE%</div>
</div>
<div class="app-loading-item">

13962
web/package-lock.json

File diff suppressed because it is too large

5
web/package.json

@ -21,7 +21,8 @@
"lint:eslint": "eslint --fix --ext .js,.ts,.vue ./src",
"lint:format": "prettier --write --loglevel warn \"src/**/*.{js,ts,json,tsx,css,less,scss,vue,html,md}\"",
"lint:style": "stylelint --fix \"./src/**/*.{vue,less,postcss,css,scss}\" --cache --cache-location node_modules/.cache/stylelint/",
"lint:lint-staged": "lint-staged -c "
"lint:lint-staged": "lint-staged -c ",
"gen:ww": "npx wwutil ticket ww6e1eee0a8ae45397 ITbfuoZkmUifGoDL5ZB8SyuMzVM8VXZNkfZJzYn5sGo"
},
"dependencies": {
"@element-plus/icons-vue": "^2.1.0",
@ -34,6 +35,7 @@
"@vueuse/core": "^10.9.0",
"@wangeditor/editor": "^5.1.23",
"@wangeditor/editor-for-vue": "^5.1.10",
"@wecom/jssdk": "^2.3.1",
"@zxcvbn-ts/core": "^3.0.4",
"animate.css": "^4.1.1",
"axios": "1.9.0",
@ -68,6 +70,7 @@
"steady-xml": "^0.1.0",
"url": "^0.11.3",
"v3-jsoneditor": "^0.0.6",
"vconsole": "^3.15.1",
"video.js": "^7.21.5",
"vue": "3.5.12",
"vue-dompurify-html": "^4.1.4",

2154
web/pnpm-lock.yaml

File diff suppressed because it is too large

BIN
web/public/favicon.ico

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 17 KiB

BIN
web/public/logo.gif

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

BIN
web/public/logo.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

52
web/src/api/electron/lock/index.ts

@ -0,0 +1,52 @@
import request from '@/config/axios'
import type { Dayjs } from 'dayjs';
/** 电子锁信息 */
export interface Lock {
id?: number; // 主键ID
lockNumber?: number; // 编号
lockName?: string; // 名称
lockStatus?: number; // 状态
lockType?: number; // 锁具类型
lockEnableStatus?: number; // 启用状态: 0=未启用, 1=已启用
lockLastChargeTime?: string | Dayjs; // 上次充电时间
lockBluetoothId?: string; // 蓝牙ID
}
// 电子锁 API
export const LockApi = {
// 查询电子锁分页
getLockPage: async (params: any) => {
return await request.get({ url: `/electron/lock/page`, params })
},
// 查询电子锁详情
getLock: async (id: number) => {
return await request.get({ url: `/electron/lock/get?id=` + id })
},
// 新增电子锁
createLock: async (data: Lock) => {
return await request.post({ url: `/electron/lock/create`, data })
},
// 修改电子锁
updateLock: async (data: Lock) => {
return await request.put({ url: `/electron/lock/update`, data })
},
// 删除电子锁
deleteLock: async (id: number) => {
return await request.delete({ url: `/electron/lock/delete?id=` + id })
},
/** 批量删除电子锁 */
deleteLockList: async (ids: number[]) => {
return await request.delete({ url: `/electron/lock/delete-list?ids=${ids.join(',')}` })
},
// 导出电子锁 Excel
exportLock: async (params) => {
return await request.download({ url: `/electron/lock/export-excel`, params })
}
}

53
web/src/api/electron/lockworkcord/index.ts

@ -0,0 +1,53 @@
import request from '@/config/axios'
import type { Dayjs } from 'dayjs';
/** 电子锁操作记录信息 */
export interface LockWorkRecord {
id?: number; // 主键ID
operatorId?: number; // 操作人ID
lockId?: number; // 电子锁ID
isolationPlanItemDetailId?: number; // 关联的子项详情ID (某些操作可能不关联)
recordType?: number; // 记录类型
signaturePath?: string; // 操作签名 (图片路径)
beforePhotoPath?: string; // 操作前照片 (图片路径)
afterPhotoPath?: string; // 操作后照片 (图片路径)
gpsCoordinates?: string; // 操作GPS坐标
}
// 电子锁操作记录 API
export const LockWorkRecordApi = {
// 查询电子锁操作记录分页
getLockWorkRecordPage: async (params: any) => {
return await request.get({ url: `/electron/lock-word-record/page`, params })
},
// 查询电子锁操作记录详情
getLockWorkRecord: async (id: number) => {
return await request.get({ url: `/electron/lock-word-record/get?id=` + id })
},
// 新增电子锁操作记录
createLockWorkRecord: async (data: LockWorkRecord) => {
return await request.post({ url: `/electron/lock-word-record/create`, data })
},
// 修改电子锁操作记录
updateLockWorkRecord: async (data: LockWorkRecord) => {
return await request.put({ url: `/electron/lock-word-record/update`, data })
},
// 删除电子锁操作记录
deleteLockWorkRecord: async (id: number) => {
return await request.delete({ url: `/electron/lock-word-record/delete?id=` + id })
},
/** 批量删除电子锁操作记录 */
deleteLockWorkRecordList: async (ids: number[]) => {
return await request.delete({ url: `/electron/lock-word-record/delete-list?ids=${ids.join(',')}` })
},
// 导出电子锁操作记录 Excel
exportLockWorkRecord: async (params) => {
return await request.download({ url: `/electron/lock-word-record/export-excel`, params })
}
}

47
web/src/api/guide/isolationpoint/index.ts

@ -0,0 +1,47 @@
import request from '@/config/axios'
import type { Dayjs } from 'dayjs';
/** 指导书与隔离点关联信息 */
export interface IsolationPoint {
id?: number; // id
guideId?: number; // 隔离指导书ID
isolationPointId?: number; // 隔离点ID
}
// 指导书与隔离点关联 API
export const IsolationPointApi = {
// 查询指导书与隔离点关联分页
getIsolationPointPage: async (params: any) => {
return await request.get({ url: `/guide/isolation-point/page`, params })
},
// 查询指导书与隔离点关联详情
getIsolationPoint: async (id: number) => {
return await request.get({ url: `/guide/isolation-point/get?id=` + id })
},
// 新增指导书与隔离点关联
createIsolationPoint: async (data: IsolationPoint) => {
return await request.post({ url: `/guide/isolation-point/create`, data })
},
// 修改指导书与隔离点关联
updateIsolationPoint: async (data: IsolationPoint) => {
return await request.put({ url: `/guide/isolation-point/update`, data })
},
// 删除指导书与隔离点关联
deleteIsolationPoint: async (id: number) => {
return await request.delete({ url: `/guide/isolation-point/delete?id=` + id })
},
/** 批量删除指导书与隔离点关联 */
deleteIsolationPointList: async (ids: number[]) => {
return await request.delete({ url: `/guide/isolation-point/delete-list?ids=${ids.join(',')}` })
},
// 导出指导书与隔离点关联 Excel
exportIsolationPoint: async (params) => {
return await request.download({ url: `/guide/isolation-point/export-excel`, params })
}
}

54
web/src/api/guide/lockguide/index.ts

@ -0,0 +1,54 @@
import request from '@/config/axios'
import type { Dayjs } from 'dayjs';
/** 隔离指导书信息 */
export interface LockGuide {
id?: number; // 主键ID
name?: string; // 指导书名称
code?: string; // 指导书编码
operatorId?: number; // 操作人ID
operatorHelperId?: number; // 操作协助人ID
verifierId?: number; // 验证人ID
verifierHelperId?: number; // 验证协助人ID
guideContent?: string; // 工作内容和范围
guideLockNums?: number; // 所需设备锁数量
isolationPointIds?: number[]; // 关联隔离点ID
}
// 隔离指导书 API
export const LockGuideApi = {
// 查询隔离指导书分页
getLockGuidePage: async (params: any) => {
return await request.get({ url: `/guide/lock-guide/page`, params })
},
// 查询隔离指导书详情
getLockGuide: async (id: number) => {
return await request.get({ url: `/guide/lock-guide/get?id=` + id })
},
// 新增隔离指导书
createLockGuide: async (data: LockGuide) => {
return await request.post({ url: `/guide/lock-guide/create`, data })
},
// 修改隔离指导书
updateLockGuide: async (data: LockGuide) => {
return await request.put({ url: `/guide/lock-guide/update`, data })
},
// 删除隔离指导书
deleteLockGuide: async (id: number) => {
return await request.delete({ url: `/guide/lock-guide/delete?id=` + id })
},
/** 批量删除隔离指导书 */
deleteLockGuideList: async (ids: number[]) => {
return await request.delete({ url: `/guide/lock-guide/delete-list?ids=${ids.join(',')}` })
},
// 导出隔离指导书 Excel
exportLockGuide: async (params) => {
return await request.download({ url: `/guide/lock-guide/export-excel`, params })
}
}

47
web/src/api/isolation/plan/index.ts

@ -0,0 +1,47 @@
import request from '@/config/axios'
import type { Dayjs } from 'dayjs';
/** 检修任务信息 */
export interface Plan {
id: number; // 主键ID
ipName?: string; // 任务名称
status?: number; // 状态
}
// 检修任务 API
export const PlanApi = {
// 查询检修任务分页
getPlanPage: async (params: any) => {
return await request.get({ url: `/isolation/plan/page`, params })
},
// 查询检修任务详情
getPlan: async (id: number) => {
return await request.get({ url: `/isolation/plan/get?id=` + id })
},
// 新增检修任务
createPlan: async (data: Plan) => {
return await request.post({ url: `/isolation/plan/create`, data })
},
// 修改检修任务
updatePlan: async (data: Plan) => {
return await request.put({ url: `/isolation/plan/update`, data })
},
// 删除检修任务
deletePlan: async (id: number) => {
return await request.delete({ url: `/isolation/plan/delete?id=` + id })
},
/** 批量删除检修任务 */
deletePlanList: async (ids: number[]) => {
return await request.delete({ url: `/isolation/plan/delete-list?ids=${ids.join(',')}` })
},
// 导出检修任务 Excel
exportPlan: async (params) => {
return await request.download({ url: `/isolation/plan/export-excel`, params })
}
}

52
web/src/api/isolation/planitem/index.ts

@ -0,0 +1,52 @@
import request from '@/config/axios'
import type { Dayjs } from 'dayjs';
/** 检修任务子项信息 */
export interface PlanItem {
id?: number; // 主键ID
isolationPlanId?: number; // 检修任务ID
guideId?: number; // 隔离指导书ID
operatorId?: number; // 集中挂牌人ID
operatorHelperId?: number; // 集中挂牌协助人ID
verifierId?: number; // 验证人ID
verifierHelperId?: number; // 验证协助人ID
status?: number; // 子项状态: 0=未完成, 1=已完成
}
// 检修任务子项 API
export const PlanItemApi = {
// 查询检修任务子项分页
getPlanItemPage: async (params: any) => {
return await request.get({ url: `/isolation/plan-item/page`, params })
},
// 查询检修任务子项详情
getPlanItem: async (id: number) => {
return await request.get({ url: `/isolation/plan-item/get?id=` + id })
},
// 新增检修任务子项
createPlanItem: async (data: PlanItem) => {
return await request.post({ url: `/isolation/plan-item/create`, data })
},
// 修改检修任务子项
updatePlanItem: async (data: PlanItem) => {
return await request.put({ url: `/isolation/plan-item/update`, data })
},
// 删除检修任务子项
deletePlanItem: async (id: number) => {
return await request.delete({ url: `/isolation/plan-item/delete?id=` + id })
},
/** 批量删除检修任务子项 */
deletePlanItemList: async (ids: number[]) => {
return await request.delete({ url: `/isolation/plan-item/delete-list?ids=${ids.join(',')}` })
},
// 导出检修任务子项 Excel
exportPlanItem: async (params) => {
return await request.download({ url: `/isolation/plan-item/export-excel`, params })
}
}

49
web/src/api/isolation/planitemdetail/index.ts

@ -0,0 +1,49 @@
import request from '@/config/axios'
import type { Dayjs } from 'dayjs';
/** 检修任务子项详情信息 */
export interface PlanItemDetail {
id?: number; // 主键ID
isolationPlanItemId?: number; // 检修任务子项ID
isolationPointId?: number; // 隔离点ID
lockId?: number; // 电子锁ID
lockStatus?: number; // 锁状态: 0=未上锁, 1=已上锁, 2=已解锁
}
// 检修任务子项详情 API
export const PlanItemDetailApi = {
// 查询检修任务子项详情分页
getPlanItemDetailPage: async (params: any) => {
return await request.get({ url: `/isolation/plan-item-detail/page`, params })
},
// 查询检修任务子项详情详情
getPlanItemDetail: async (id: number) => {
return await request.get({ url: `/isolation/plan-item-detail/get?id=` + id })
},
// 新增检修任务子项详情
createPlanItemDetail: async (data: PlanItemDetail) => {
return await request.post({ url: `/isolation/plan-item-detail/create`, data })
},
// 修改检修任务子项详情
updatePlanItemDetail: async (data: PlanItemDetail) => {
return await request.put({ url: `/isolation/plan-item-detail/update`, data })
},
// 删除检修任务子项详情
deletePlanItemDetail: async (id: number) => {
return await request.delete({ url: `/isolation/plan-item-detail/delete?id=` + id })
},
/** 批量删除检修任务子项详情 */
deletePlanItemDetailList: async (ids: number[]) => {
return await request.delete({ url: `/isolation/plan-item-detail/delete-list?ids=${ids.join(',')}` })
},
// 导出检修任务子项详情 Excel
exportPlanItemDetail: async (params) => {
return await request.download({ url: `/isolation/plan-item-detail/export-excel`, params })
}
}

51
web/src/api/isolation/planlifelock/index.ts

@ -0,0 +1,51 @@
import request from '@/config/axios'
import type { Dayjs } from 'dayjs';
/** 个人生命锁信息 */
export interface PlanLifeLock {
id: number; // 主键ID
isolationPlanItemDetailId?: number; // 子项详情ID
userId?: number; // 上锁人ID
lockType?: number; // 生命锁类型
lockStatus?: number; // 锁定状态: 0=未上锁, 1=已上锁
lockTime?: number; // 上锁时间
unlockTime?: number; // 解锁时间
}
// 个人生命锁 API
export const PlanLifeLockApi = {
// 查询个人生命锁分页
getPlanLifeLockPage: async (params: any) => {
return await request.get({ url: `/isolation/plan-life-lock/page`, params })
},
// 查询个人生命锁详情
getPlanLifeLock: async (id: number) => {
return await request.get({ url: `/isolation/plan-life-lock/get?id=` + id })
},
// 新增个人生命锁
createPlanLifeLock: async (data: PlanLifeLock) => {
return await request.post({ url: `/isolation/plan-life-lock/create`, data })
},
// 修改个人生命锁
updatePlanLifeLock: async (data: PlanLifeLock) => {
return await request.put({ url: `/isolation/plan-life-lock/update`, data })
},
// 删除个人生命锁
deletePlanLifeLock: async (id: number) => {
return await request.delete({ url: `/isolation/plan-life-lock/delete?id=` + id })
},
/** 批量删除个人生命锁 */
deletePlanLifeLockList: async (ids: number[]) => {
return await request.delete({ url: `/isolation/plan-life-lock/delete-list?ids=${ids.join(',')}` })
},
// 导出个人生命锁 Excel
exportPlanLifeLock: async (params) => {
return await request.download({ url: `/isolation/plan-life-lock/export-excel`, params })
}
}

51
web/src/api/isolation/point/index.ts

@ -0,0 +1,51 @@
import request from '@/config/axios'
import type { Dayjs } from 'dayjs';
/** 隔离点信息 */
export interface Point {
id?: number; // 主键ID
ipType?: string; // 隔离点类型
ipName?: string; // 隔离点名称
ipLocation?: string; // 隔离点位置
ipNumber?: number; // 隔离点编号
status?: number; // 隔离点状态
guideLockNums?: number; // 电子锁数量
}
// 隔离点 API
export const PointApi = {
// 查询隔离点分页
getPointPage: async (params: any) => {
return await request.get({ url: `/isolation/point/page`, params })
},
// 查询隔离点详情
getPoint: async (id: number) => {
return await request.get({ url: `/isolation/point/get?id=` + id })
},
// 新增隔离点
createPoint: async (data: Point) => {
return await request.post({ url: `/isolation/point/create`, data })
},
// 修改隔离点
updatePoint: async (data: Point) => {
return await request.put({ url: `/isolation/point/update`, data })
},
// 删除隔离点
deletePoint: async (id: number) => {
return await request.delete({ url: `/isolation/point/delete?id=` + id })
},
/** 批量删除隔离点 */
deletePointList: async (ids: number[]) => {
return await request.delete({ url: `/isolation/point/delete-list?ids=${ids.join(',')}` })
},
// 导出隔离点 Excel
exportPoint: async (params) => {
return await request.download({ url: `/isolation/point/export-excel`, params })
}
}

66
web/src/api/lock/index.ts

@ -0,0 +1,66 @@
import request from '@/config/axios'
export const getAllLock = (params: PageParam = { pageSize: 9999, pageNo: 1 }) => {
return request.get({ url: `/electron/lock/page`, params })
}
export const getAllIsolationPoint = (params: PageParam = { pageSize: 9999, pageNo: 1 }) => {
return request.get({ url: `/isolation/point/page`, params })
}
export const getAllIsolationPlan = (params: PageParam = { pageSize: 9999, pageNo: 1 }) => {
return request.get({ url: `/isolation/plan/page`, params })
}
export const getAllGuidance = (params: PageParam = { pageSize: 9999, pageNo: 1 }) => {
return request.get({ url: `/guide/lock-guide/page`, params })
}
export const getAllGuidanceIsolationPoint = (params: PageParam = { pageSize: 9999, pageNo: 1 }) => {
return request.get({ url: `/guide/isolation-point/page`, params })
}
export const getAllPlanItem = (params: PageParam = { pageSize: 9999, pageNo: 1 }) => {
return request.get({ url: `/isolation/plan-item/page`, params })
}
export const getAllPlanItemDetail = (params: PageParam = { pageSize: 9999, pageNo: 1 }) => {
return request.get({ url: `/isolation/plan-item-detail/page`, params })
}
export const getAllPlanLifeLock = (params: PageParam = { pageSize: 9999, pageNo: 1 }) => {
return request.get({ url: `/isolation/plan-life-lock/page`, params })
}
const baseUrl = import.meta.env.VITE_BASE_URL + import.meta.env.VITE_API_URL
export const getAgentConfigSignature = (url: string) => {
return fetch(`${baseUrl}/js/weixin/getAgentConfigSignature?url=${url}`).then(async res => {
return await res.json()
})
}
export const getConfigSignature = (url: string) => {
return fetch(`${baseUrl}/js/weixin/getConfigSignature?url=${url}`).then(async res => {
return await res.json()
})
}
export const getAllFormattedIsolationPlan = () => {
return request.get({ url: `/isolation/plan/planListAll` })
}
// 获取所有基础数据
export const getAllBaseData = () => request.get({ url: `isolation/point/getListAll` })
// 查询用户管理列表
export const getAllUser = () => request.get({ url: 'system/user/list-all-simple' })
// 根据隔离点获取相关记录
export const getIsolationPointDetail = (id: number) => request.get({ url: `isolation/point/getPointListAll`, params: { id } })
export const bindLock = (data: { planItemDetailId: number; lockId: number }) => request.put({ url: `isolation/point/bindlock`, data })
export const lockAction = (data: { planItemDetailId: number, operateRecordId: number }) => request.put({ url: `isolation/point/createLock`, data })
export const verifyLockAction = (data: { planItemDetailId: number, verifyRecordId: number }) => request.put({ url: `isolation/point/verifyLock`, data })
export const verifyUnlockAction = (data: { planItemDetailId: number, lifelockId: number }) => request.put({ url: `isolation/point/verifyUnLock`, data })
export const unLockAction = (data: { planItemDetailId: number, planId: number, lifelockId: number }) => request.put({ url: `isolation/point/unLock`, data })

2
web/src/api/login/types.ts

@ -31,5 +31,7 @@ export type RegisterVO = {
tenantName: string
username: string
password: string
mobile?: string
captchaVerification: string
type: number
}

BIN
web/src/assets/imgs/logo.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

BIN
web/src/assets/imgs/user.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

2
web/src/components/Cropper/src/CropperAvatar.vue

@ -18,7 +18,7 @@ import { useDesign } from '@/hooks/web/useDesign'
import { propTypes } from '@/utils/propTypes'
import { useI18n } from 'vue-i18n'
import CopperModal from './CopperModal.vue'
import avatar from '@/assets/imgs/avatar.gif'
import avatar from '@/assets/imgs/user.png'
defineOptions({ name: 'CropperAvatar' })

5
web/src/components/DictTag/src/DictTag.vue

@ -25,6 +25,10 @@ export default defineComponent({
gutter: {
type: String as PropType<string>,
default: '5px'
},
size: {
type: String as PropType<string>,
default: 'default'
}
},
setup(props) {
@ -75,6 +79,7 @@ export default defineComponent({
type={dict?.colorType || null}
color={dict?.cssClass && isHexColor(dict?.cssClass) ? dict?.cssClass : ''}
disableTransitions={true}
size={props.size}
>
{dict?.label}
</ElTag>

2
web/src/components/DiyEditor/index.vue

@ -171,7 +171,7 @@
/>
<div class="flex flex-col">
<el-text>手机扫码预览</el-text>
<Qrcode :text="previewUrl" logo="/logo.gif" />
<Qrcode :text="previewUrl" logo="/logo.png" />
</div>
</div>
</Dialog>

208
web/src/components/Lock/DetailPointModal.vue

@ -0,0 +1,208 @@
<template>
<el-dialog
v-model="visible"
title="任务隔离点详情"
width="80%"
:before-close="handleClose"
destroy-on-close
>
<div class="detail-container">
<div class="detail-header mb-5">
<div class="title">隔离点: {{ isolationPoint?.ipName }}</div>
<div class="title" v-show="isolationPlanId">检修任务: {{ isolationPlan?.ipName }}</div>
</div>
<el-table :data="allDetail" style="width: 100%" border>
<el-table-column label="关联任务" align="center" prop="planId">
<template #default="scope">
{{ elLockStore.isolationPlans.find((item) => item.id === scope.row.planId)?.ipName }}
</template>
</el-table-column>
<el-table-column label="任务状态" align="center" prop="lockStatus">
<template #default="scope">
<DictTag
:type="DICT_TYPE.LOCK_PLAN_ITEM_DETAIL_STATUS"
:value="scope.row.lockStatus"
size="small"
/>
</template>
</el-table-column>
<el-table-column label="电子锁" align="center" prop="lock.lockName">
<template #default="scope">
<template v-if="scope.row.lock">
{{ scope.row.lock?.lockName }}
<DictTag
:type="DICT_TYPE.LOCK_STATUS"
:value="scope.row.lock.lockStatus"
size="small"
/>
</template>
</template>
</el-table-column>
<el-table-column label="集中挂牌人" align="center" prop="operatorId">
<template #default="scope">
{{ elLockStore.users.find((user) => user.id === scope.row.item?.operatorId)?.nickname }}
</template>
</el-table-column>
<el-table-column label="集中挂牌协助人" align="center" prop="operatorHelperId">
<template #default="scope">
{{
elLockStore.users.find((user) => user.id === scope.row.item?.operatorHelperId)
?.nickname
}}
</template>
</el-table-column>
<el-table-column label="验证人" align="center" prop="verifierId">
<template #default="scope">
{{ elLockStore.users.find((user) => user.id === scope.row.item?.verifierId)?.nickname }}
</template>
</el-table-column>
<el-table-column label="验证协助人" align="center" prop="verifierHelperId">
<template #default="scope">
{{
elLockStore.users.find((user) => user.id === scope.row.item?.verifierHelperId)
?.nickname
}}
</template>
</el-table-column>
</el-table>
<div class="chart-container" ref="detailPanel"></div>
</div>
</el-dialog>
</template>
<script lang="ts" setup>
import { ref, watch, nextTick, defineExpose, computed } from 'vue'
import LogicFlow from '@logicflow/core'
import { useElLockStore } from '@/store/modules/elLock'
import '@logicflow/core/lib/style/index.css'
import { PlanItemDetail } from '@/api/isolation/planitemdetail'
import { DICT_TYPE } from '@/utils/dict'
import { Lock } from '@/api/electron/lock'
import { PlanItem } from '@/api/isolation/planitem'
import LockPointNodeAdapter from '@/components/LogicFlow/LockPointNodeAdapter'
defineOptions({ name: 'DetaiPointModal' })
const elLockStore = useElLockStore()
const { isolationPointId, isolationPlanId } = defineProps<{
isolationPointId: number | undefined
isolationPlanId: number | undefined
}>()
const detailPanel = ref<HTMLElement | null>(null)
const visible = ref(false)
let lf: LogicFlow | null = null
let allDetail = ref<PlanItemDetail & { lock?: Lock; item?: PlanItem }[]>([])
const initChart = async () => {
if (!detailPanel.value) return
//
if (lf) {
lf.destroy()
lf = null
}
await nextTick()
lf = new LogicFlow({
container: detailPanel.value,
width: detailPanel.value.clientWidth,
height: detailPanel.value.clientHeight
})
// Vue
lf.register(LockPointNodeAdapter)
//
renderNodeData()
//
lf.fitView()
}
const renderNodeData = () => {
if (!lf || !allDetail.value.length) return
const nodes = [
{
properties: {
planId: isolationPlanId,
pointId: isolationPointId,
expand: true
},
type: 'lock-point-node',
x: 0,
y: 0
}
]
//
lf.render({ nodes, edges: [] })
requestAnimationFrame(() => {
lf?.fitView()
setTimeout(() => lf?.fitView(), 50)
setTimeout(() => lf?.fitView(), 200)
})
}
const isolationPoint = computed(() => {
return elLockStore.isolationPoints.find((item) => item.id === isolationPointId)
})
const isolationPlan = computed(() => {
return elLockStore.isolationPlans.find((item) => item.id === isolationPlanId)
})
const show = () => {
visible.value = true
allDetail.value = elLockStore.planItemDetails
.filter(
(item) =>
(isolationPlanId ? item.planId === isolationPlanId : true) &&
item.isolationPointId === isolationPointId
)
.map((item) => {
return {
...item,
lock: elLockStore.locks.find((lock) => lock.id === item.lockId),
item: elLockStore.planItems.find((i) => i.id === item.isolationPlanItemId)
}
})
nextTick(() => {
initChart()
})
}
const handleClose = () => {
visible.value = false
if (lf) {
lf.destroy()
lf = null
}
}
watch([() => isolationPlanId, () => isolationPointId], ([newPlanId, newPointId]) => {
if (newPlanId && newPointId) {
show()
}
})
defineExpose({
show,
handleClose
})
</script>
<style scoped>
.detail-container {
display: flex;
flex-direction: column;
height: 100%;
.detail-header {
display: flex;
gap: 3rem;
.title {
font-size: 16px;
font-weight: bold;
color: #333;
}
}
}
.chart-container {
min-height: 25rem;
}
</style>

390
web/src/components/LogicFlow/LockPointNode.vue

@ -0,0 +1,390 @@
<template>
<div
class="node-container"
:class="{ horizontal: useHorizontal, locked: IsolationPoint?.status == 1 }"
>
<div class="header">
<div class="title">隔离点</div>
<div class="point-name">
{{ IsolationPoint?.ipName || '—' }}
</div>
<el-switch v-model="useHorizontal" size="small" class="ml-auto" />
</div>
<div class="section" v-if="affectedPlans.length">
<div class="section-title">关联任务</div>
<div class="task-list">
<div class="task-card" v-for="plan in affectedPlans" :key="plan!.id">
<div class="task-row">
<span class="label">任务名称</span>
<span class="value">{{ plan!.ipName }}</span>
</div>
<div class="task-row">
<span class="label">任务状态</span>
<DictTag size="small" :type="DICT_TYPE.LOCK_PLAN_ITEM_STATUS" :value="plan!.status!" />
</div>
</div>
</div>
</div>
<div class="section">
<div class="section-title">人员</div>
<div class="person-rows">
<div class="person-row">
<span class="label">集中挂牌人</span>
<div class="user-tags">
<span v-for="user in operatorUsers" :key="user.id" class="tag tag--info">{{
user.nickname
}}</span>
<span v-if="!operatorUsers.length" class="placeholder"></span>
</div>
</div>
<div class="person-row" v-if="operatorHelperUsers.length">
<span class="label">集中挂牌协助人</span>
<div class="user-tags">
<span v-for="user in operatorHelperUsers" :key="user.id" class="tag tag--info">{{
user.nickname
}}</span>
</div>
</div>
<div class="person-row">
<span class="label">验证人</span>
<div class="user-tags">
<span v-for="user in verifierUsers" :key="user.id" class="tag tag--success">{{
user.nickname
}}</span>
<span v-if="!verifierUsers.length" class="placeholder"></span>
</div>
</div>
<div class="person-row" v-if="verifierHelperUsers.length">
<span class="label">验证协助人</span>
<div class="user-tags">
<span v-for="user in verifierHelperUsers" :key="user.id" class="tag tag--success">{{
user.nickname
}}</span>
</div>
</div>
</div>
</div>
<div class="section" v-if="locks.length">
<div class="section-title">电子锁</div>
<div class="lock-list">
<div class="lock-item" v-for="lock in locks" :key="lock!.id">
{{ lock!.lockName }}
<DictTag :type="DICT_TYPE.LOCK_STATUS" :value="lock!.lockStatus!" size="small" />
</div>
</div>
</div>
<div class="section">
<div class="section-title">个人生命锁</div>
<div class="lifelock-list">
<div class="lifelock-item" v-for="detail in details" :key="detail.id">
<div class="lifelock-users" v-if="detail.affectedUsers && detail.affectedUsers.length">
<div class="lifelock-user" v-for="lifelock in detail.affectedUsers" :key="lifelock.id">
<span class="user-name">{{
getUserById(lifelock.userId)?.nickname || '未知用户'
}}</span>
<DictTag
size="small"
:type="DICT_TYPE.LOCK_LIFE_LOCK_STATUS"
:value="lifelock.lockStatus ?? 0"
/>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import { useElLockStore } from '@/store/modules/elLock'
import { isEmpty } from '@/utils/is'
import { DICT_TYPE, getDictLabel } from '@/utils/dict'
import { UserVO } from '@/api/system/user'
const elLockStore = useElLockStore()
interface Props {
pointId?: number
planId?: number
expand?: boolean
}
const props = withDefaults(defineProps<Props>(), {
pointId: undefined,
planId: undefined,
expand: false
})
const affectedPlans = computed(() => {
if (props.planId) {
const plan = elLockStore.isolationPlans.find((item) => item.id === props.planId)
return plan ? [plan] : []
}
const planIds = new Set(
elLockStore.planItemDetails
.filter((item) => item.isolationPointId === props.pointId)
.map((item) => item.planId)
)
return [...planIds]
.map((id) => elLockStore.isolationPlans.find((p) => p.id === id))
.filter((i) => !isEmpty(i))
.sort((a, b) => a!.id - b!.id)
})
const details = computed(() => {
const activePlanIds = new Set(affectedPlans.value.map((p) => p!.id))
return elLockStore.planItemDetails
.filter(
(detail) => detail.isolationPointId === props.pointId && activePlanIds.has(detail.planId!)
)
.map((detail) => {
const lock = elLockStore.planLifeLocks.find((l) => l.isolationPlanItemDetailId === detail.id)
const affectedUsers = elLockStore.planLifeLocks.filter(
(l) => l.isolationPlanItemDetailId === detail.id && l.lockType == 5
)
const item = elLockStore.planItems.find((i) => i.id === detail.isolationPlanItemId)
return { ...detail, lock, affectedUsers, item }
})
})
const IsolationPoint = computed(() => {
return elLockStore.isolationPoints.find((item) => item.id === (props.pointId ?? -1))
})
const locks = computed(() => {
return details.value
.map((d) => d.lockId)
.filter((i) => !isEmpty(i))
.map((i) => elLockStore.locks.find((l) => l.id === i))
})
// find
const userMap = computed(() => {
const map = new Map<number, UserVO>()
elLockStore.users.forEach((u) => map.set(u.id as number, u))
return map
})
const getUserById = (id?: number) => (id != null ? userMap.value.get(id) : undefined)
const dedupeUsers = (users: (UserVO | undefined)[]) => {
const seen = new Set<number>()
const result: UserVO[] = []
users.forEach((u) => {
if (u && !seen.has(u.id as number)) {
seen.add(u.id as number)
result.push(u)
}
})
return result
}
const operatorUsers = computed(() =>
dedupeUsers(details.value.map((d) => getUserById(d.item?.operatorId)))
)
const operatorHelperUsers = computed(() =>
dedupeUsers(details.value.map((d) => getUserById(d.item?.operatorHelperId)))
)
const verifierUsers = computed(() =>
dedupeUsers(details.value.map((d) => getUserById(d.item?.verifierId)))
)
const verifierHelperUsers = computed(() =>
dedupeUsers(details.value.map((d) => getUserById(d.item?.verifierHelperId)))
)
const useHorizontal = ref(props.expand)
</script>
<style scoped>
.node-container {
display: flex;
flex-direction: column;
gap: 8px;
padding: 8px;
background: #ffffffcc;
border: 1px solid #00f369;
border-radius: 6px;
backdrop-filter: blur(2px);
}
.header {
display: flex;
align-items: center;
gap: 6px;
}
.title {
font-size: 12px;
color: #909399;
}
.point-name {
font-weight: 600;
color: #303133;
}
.section {
display: flex;
flex-direction: column;
gap: 6px;
}
.section-title {
font-size: 12px;
color: #606266;
font-weight: 600;
}
.task-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 6px;
}
.task-card {
border: 1px solid #ebeef5;
border-radius: 4px;
padding: 6px;
background: #fff;
}
.task-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 6px;
font-size: 11px;
padding: 1px 0;
}
.label {
text-wrap: nowrap;
color: #909399;
}
.value {
color: #303133;
font-weight: 500;
}
.person-rows {
display: flex;
flex-direction: column;
gap: 4px;
}
.person-row {
display: flex;
align-items: center;
gap: 8px;
}
.user-tags {
display: flex;
flex-wrap: wrap;
gap: 4px;
}
.tag {
display: inline-flex;
align-items: center;
padding: 0 6px;
font-size: 11px;
border-radius: 20px;
border: 1px solid transparent;
}
.tag--info {
color: #409eff;
background: #ecf5ff;
border-color: #b3d8ff;
}
.tag--success {
color: #67c23a;
background: #f0f9eb;
border-color: #c2e7b0;
}
.tag--warning {
color: #e6a23c;
background: #fdf6ec;
border-color: #f3d19e;
}
.tag--danger {
color: #f56c6c;
background: #fef0f0;
border-color: #fbc4c4;
}
.lifelock-list {
display: flex;
flex-direction: column;
gap: 4px;
}
.lifelock-users {
display: flex;
flex-direction: column;
gap: 4px;
}
.lifelock-user {
display: flex;
align-items: center;
justify-content: space-between;
}
.user-name {
color: #303133;
}
.placeholder {
color: #c0c4cc;
}
/* 横向布局样式 */
.node-container.horizontal {
flex-direction: row;
flex-wrap: wrap;
align-items: flex-start;
gap: 12px;
max-width: 1000px;
min-width: 800px;
}
.node-container.horizontal .header {
width: 100%;
flex-shrink: 0;
border-bottom: 1px solid #e6e8eb;
padding-bottom: 8px;
margin-bottom: 4px;
}
.node-container.horizontal .section {
flex: 1;
min-width: 180px;
max-width: 250px;
border: 1px solid #ebeef5;
border-radius: 4px;
padding: 8px;
background: #fafafa;
}
.node-container.horizontal .task-list {
grid-template-columns: 1fr;
}
.node-container.horizontal .person-rows {
gap: 6px;
}
.node-container.horizontal .person-row {
flex-direction: column;
align-items: flex-start;
gap: 4px;
}
.node-container.horizontal .user-tags {
width: 100%;
}
.node-container.horizontal .lifelock-item {
margin-bottom: 4px;
}
.lock-list {
display: flex;
flex-direction: column;
gap: 4px;
}
.lock-item {
display: flex;
align-items: center;
justify-content: space-between;
}
.locked {
border-color: #f56c6c;
}
</style>

129
web/src/components/LogicFlow/LockPointNodeAdapter.ts

@ -0,0 +1,129 @@
import { HtmlNode, HtmlNodeModel } from '@logicflow/core'
import { createApp, h, App as VueApp } from 'vue'
import LockPointVueNode from './LockPointNode.vue'
class LockPointNodeModel extends HtmlNodeModel {
setAttributes() {
// 设置节点的默认属性
this.text.editable = false // 禁用默认文本编辑
this.text.value = '' // 清空默认文本
this.draggable = false
// 初始占位,后续通过 ResizeObserver 自适应
this.width = 400
this.height = 200
}
/**
*
*/
getNodeStyle() {
const style = super.getNodeStyle()
return {
...style,
stroke: 'transparent',
fill: 'transparent'
}
}
}
class LockPointNode extends HtmlNode {
private vueApp: VueApp | null = null
private resizeObserver: ResizeObserver | null = null
/**
* HTML内容
*/
setHtml(rootEl: SVGForeignObjectElement) {
const node = this.props.model
const pointId = node.properties.pointId
// 创建Vue应用实例
this.vueApp = createApp({
components: {
LockPointVueNode
},
render: () => h(LockPointVueNode, {
...node.properties
})
})
// 创建容器元素
const container = document.createElement('div')
container.id = `lock-point-node-${pointId}`
container.style.position = 'relative'
container.style.pointerEvents = 'auto'
container.style.display = 'inline-block'
// 挂载Vue应用
this.vueApp.mount(container)
rootEl.appendChild(container)
// 使用 ResizeObserver 监听容器尺寸变化,并同步到模型与 foreignObject
const syncSize = () => {
const width = Math.ceil(container.offsetWidth)
const height = Math.ceil(container.offsetHeight)
if (!width || !height) return
if (this.props.model.width !== width || this.props.model.height !== height) {
this.props.model.width = width
this.props.model.height = height
rootEl.setAttribute('width', String(width))
rootEl.setAttribute('height', String(height))
}
}
// 初次挂载后同步一次
requestAnimationFrame(syncSize)
this.resizeObserver = new ResizeObserver(() => {
syncSize()
})
this.resizeObserver.observe(container)
}
/**
* Vue应用
*/
destroy() {
if (this.resizeObserver) {
this.resizeObserver.disconnect()
this.resizeObserver = null
}
if (this.vueApp) {
this.vueApp.unmount()
this.vueApp = null
}
}
}
// 导出节点配置
export default {
type: 'lock-point-node',
view: LockPointNode,
model: LockPointNodeModel
}
// 导出节点类型
export const LockPointNodeType = 'lock-point-node'
// 导出节点属性接口
export interface LockPointNodeProperties {
pointId?: number
planId?: number
}
// 导出创建节点的辅助函数
export function createLockPointNode(
id: string,
x: number,
y: number,
properties: LockPointNodeProperties = {}
) {
return {
id,
type: LockPointNodeType,
x,
y,
properties: {
...properties
}
}
}

2
web/src/layout/components/Footer/src/Footer.vue

@ -22,6 +22,6 @@ const currentYear = computed(() => new Date().getFullYear())
:class="prefixCls"
class="h-[var(--app-footer-height)] bg-[var(--app-content-bg-color)] text-center leading-[var(--app-footer-height)] text-[var(--el-text-color-placeholder)] dark:bg-[var(--el-bg-color)] overflow-hidden"
>
<span class="text-14px">Copyright ©{{ currentYear }} {{ title }}</span>
<span class="text-14px">Copyright ©{{ currentYear }} {{ title }} 京公网安备 11010702002311</span>
</div>
</template>

17
web/src/layout/components/Menu/src/Menu.vue

@ -4,6 +4,7 @@ import { ElMenu, ElScrollbar } from 'element-plus'
import { useAppStore } from '@/store/modules/app'
import { usePermissionStore } from '@/store/modules/permission'
import { useRenderMenuItem } from './components/useRenderMenuItem'
import { useUserStore } from '@/store/modules/user'
import { isUrl } from '@/utils/is'
import { useDesign } from '@/hooks/web/useDesign'
import { LayoutType } from '@/types/layout'
@ -29,7 +30,7 @@ export default defineComponent({
const { push, currentRoute } = useRouter()
const permissionStore = usePermissionStore()
const userStore = useUserStore()
const menuMode = computed((): 'vertical' | 'horizontal' => {
//
const vertical: LayoutType[] = ['classic', 'topLeft', 'cutMenu']
@ -41,9 +42,17 @@ export default defineComponent({
}
})
const routers = computed(() =>
unref(layout) === 'cutMenu' ? permissionStore.getMenuTabRouters : permissionStore.getRouters
)
const routers = computed(() => {
let menu =
unref(layout) === 'cutMenu' ? permissionStore.getMenuTabRouters : permissionStore.getRouters
if (userStore.getRoles.join(',').includes('affected')) {
return menu.filter((item) => {
return !item.path.startsWith('/lock')
})
} else {
return menu
}
})
const collapse = computed(() => appStore.getCollapse)

2
web/src/layout/components/Message/src/Message.vue

@ -62,7 +62,7 @@ onMounted(() => {
<el-scrollbar class="message-list">
<template v-for="item in list" :key="item.id">
<div class="message-item">
<img alt="" class="message-icon" src="@/assets/imgs/avatar.gif" />
<img alt="" class="message-icon" src="@/assets/imgs/user.png" />
<div class="message-content">
<span class="message-title">
{{ item.templateNickname }}{{ item.templateContent }}

2
web/src/layout/components/UserInfo/src/UserInfo.vue

@ -1,7 +1,7 @@
<script lang="ts" setup>
import { ElMessageBox } from 'element-plus'
import avatarImg from '@/assets/imgs/avatar.gif'
import avatarImg from '@/assets/imgs/user.png'
import { useDesign } from '@/hooks/web/useDesign'
import { useTagsViewStore } from '@/store/modules/tagsView'
import { useUserStore } from '@/store/modules/user'

2
web/src/layout/components/UserInfo/src/components/LockDialog.vue

@ -2,7 +2,7 @@
import { useValidator } from '@/hooks/web/useValidator'
import { useDesign } from '@/hooks/web/useDesign'
import { useLockStore } from '@/store/modules/lock'
import avatarImg from '@/assets/imgs/avatar.gif'
import avatarImg from '@/assets/imgs/user.png'
import { useUserStore } from '@/store/modules/user'
const { getPrefixCls } = useDesign()

2
web/src/layout/components/UserInfo/src/components/LockPage.vue

@ -6,7 +6,7 @@ import { useNow } from '@/hooks/web/useNow'
import { useDesign } from '@/hooks/web/useDesign'
import { useTagsViewStore } from '@/store/modules/tagsView'
import { useUserStore } from '@/store/modules/user'
import avatarImg from '@/assets/imgs/avatar.gif'
import avatarImg from '@/assets/imgs/user.png'
const tagsViewStore = useTagsViewStore()

3
web/src/locales/en.ts

@ -143,7 +143,8 @@ export default {
SmsSendMsg: 'code has been sent',
resetPassword: "Reset Password",
resetPasswordSuccess: "Reset Password Success",
invalidTenantName:"Invalid Tenant Name"
invalidTenantName:"Invalid Tenant Name",
mobile:"Mobile",
},
captcha: {
verification: 'Please complete security verification',

3
web/src/locales/zh-CN.ts

@ -143,7 +143,8 @@ export default {
SmsSendMsg: '验证码已发送',
resetPassword: '重置密码',
resetPasswordSuccess: '重置密码成功',
invalidTenantName: '无效的租户名称'
invalidTenantName: '无效的租户名称',
mobile: '手机号码'
},
captcha: {
verification: '请完成安全验证',

7
web/src/main.ts

@ -38,9 +38,12 @@ import App from './App.vue'
import './permission'
import Logger from '@/utils/Logger'
import VueDOMPurifyHTML from 'vue-dompurify-html' // 解决v-html 的安全隐患
import VConsole from 'vconsole'
if (import.meta.env.VITE_DEV === 'true') {
// eslint-disable-next-line no-unused-vars
const vConsole = new VConsole();
}
// 创建实例
const setupAll = async () => {
const app = createApp(App)

3
web/src/store/modules/app.ts

@ -38,6 +38,7 @@ interface AppState {
footer: boolean
theme: ThemeTypes
fixedMenu: boolean
isWorkWechat: boolean
}
export const useAppStore = defineStore('app', {
@ -67,7 +68,7 @@ export const useAppStore = defineStore('app', {
footer: true, // 显示页脚
greyMode: false, // 是否开始灰色模式,用于特殊悼念日
fixedMenu: wsCache.get('fixedMenu') || false, // 是否固定菜单
isWorkWechat: false, // 是否是工作微信
layout: wsCache.get(CACHE_KEY.LAYOUT) || 'classic', // layout布局
isDark: wsCache.get(CACHE_KEY.IS_DARK) || false, // 是否是暗黑模式
currentSize: wsCache.get('default') || 'default', // 组件尺寸

126
web/src/store/modules/elLock.ts

@ -0,0 +1,126 @@
import { defineStore } from 'pinia'
import { store } from '@/store'
import { UserVO } from '@/api/system/user'
import {
getAllUser,
getAllBaseData
} from '@/api/lock'
import { Lock } from '@/api/electron/lock'
import { Point } from '@/api/isolation/point'
import { Plan } from '@/api/isolation/plan'
import { LockGuide } from '@/api/guide/lockguide'
import { IsolationPoint } from '@/api/guide/isolationpoint'
import { PlanItem } from '@/api/isolation/planitem'
import { PlanItemDetail } from '@/api/isolation/planitemdetail'
import { PlanLifeLock } from '@/api/isolation/planlifelock'
import { cloneDeep } from 'lodash-es'
interface LockState {
users: UserVO[]
locks: Lock[]
isolationPoints: Point[]
isolationPlans: Plan[]
lockGuides: LockGuide[]
isolationPointGuides: IsolationPoint[]
planItems: PlanItem[]
planItemDetails: (PlanItemDetail & { planId?: number })[]
planLifeLocks: (PlanLifeLock & { planId?: number; itemId?: number })[]
plan2DetailTree: any[]
plan2ItemTree: any[]
}
export const useElLockStore = defineStore('elLock', {
state: (): LockState => {
return {
users: [],
locks: [],
isolationPoints: [],
isolationPlans: [],
lockGuides: [],
isolationPointGuides: [],
planItems: [],
planItemDetails: [],
planLifeLocks: [],
plan2DetailTree: [],//plan item itemdetail lock lifelock
plan2ItemTree: [], //plan item
}
},
getters: {},
actions: {
async init() {
await this.getUsers()
await this.getBaseData()
this.getPlan2DetailTree()
},
async getUsers() {
const res = await getAllUser()
this.users = res
},
async getBaseData() {
try {
const res = await getAllBaseData()
this.isolationPoints = res.pointList
this.locks = res.lockList
this.planItems = res.planItemList
this.planItemDetails = res.planItemDetailList.map((detail: PlanItemDetail) => {
const item: PlanItem | undefined = this.planItems.find(item => item.id === detail.isolationPlanItemId)
return {
...detail,
planId: item?.isolationPlanId,
}
})
this.planLifeLocks = res.planLifeLockList.map((lock: PlanLifeLock) => {
const detail: PlanItemDetail | undefined = this.planItemDetails.find(detail => detail.id === lock.isolationPlanItemDetailId)
const item: PlanItem | undefined = this.planItems.find(item => item.id === detail?.isolationPlanItemId)
return {
...lock,
planId: item?.isolationPlanId,
itemId: item?.id,
}
})
this.isolationPlans = res.planList
this.lockGuides = res.lockGuideList
this.isolationPointGuides = res.isolationPointList
} catch (error) {
this.isolationPoints = []
this.locks = []
this.planItems = []
this.planItemDetails = []
this.planLifeLocks = []
this.isolationPlans = []
this.lockGuides = []
this.isolationPointGuides = []
}
},
getPlan2DetailTree() {
const planlist = cloneDeep(this.isolationPlans).filter(plan => plan.status == 0)
const itemlist = cloneDeep(this.planItems)
const detaillist = cloneDeep(this.planItemDetails)
const lifelocklist = cloneDeep(this.planLifeLocks)
planlist.forEach((plan: any) => {
const items = itemlist.filter(item => item.isolationPlanId === plan.id)
plan.planItem = items
items.forEach((item: any) => {
const details = detaillist.filter(detail => detail.isolationPlanItemId === item.id).map(deteail => {
const lock = this.locks.find(lock => lock.id === deteail.lockId)
const isolationPoint = this.isolationPoints.find(point => point.id === deteail.isolationPointId)
return {
...deteail,
lock,
isolationPoint
}
})
item.planItemDetail = details
details.forEach((detail: any) => {
const lifelock = lifelocklist.filter(lifelock => lifelock.isolationPlanItemDetailId === detail.id)
detail.itemLifeLock = lifelock
})
})
})
this.plan2DetailTree = planlist
}
},
persist: true
})
export const useElLockStoreWithOut = () => {
return useElLockStore(store)
}

4
web/src/store/modules/user.ts

@ -3,7 +3,7 @@ import { defineStore } from 'pinia'
import { getAccessToken, removeToken } from '@/utils/auth'
import { CACHE_KEY, useCache, deleteUserCache } from '@/hooks/web/useCache'
import { getInfo, loginOut } from '@/api/login'
import { useElLockStore } from './elLock'
const { wsCache } = useCache()
interface UserVO {
@ -68,6 +68,8 @@ export const useUserStore = defineStore('admin-user', {
this.isSetUser = true
wsCache.set(CACHE_KEY.USER, userInfo)
wsCache.set(CACHE_KEY.ROLE_ROUTERS, userInfo.menus)
const elLockStore = useElLockStore()
elLockStore.init()
},
async setUserAvatarAction(avatar: string) {
const userInfo = wsCache.get(CACHE_KEY.USER)

565
web/src/utils/bluetooth.ts

@ -0,0 +1,565 @@
/**
*
*
*/
// 声明微信小程序API类型
declare const wx: {
onBLECharacteristicValueChange: (callback: (res: { value: ArrayBuffer }) => void) => void;
writeBLECharacteristicValue: (options: {
deviceId: string;
serviceId: string;
characteristicId: string;
value: ArrayBuffer;
success?: (res: any) => void;
fail?: (error: any) => void;
}) => void;
};
// 全局变量
let deviceId = '';
const serviceId = '0000FFF0-0000-1000-8000-00805F9B34FB';
const characteristicId = '0000FFF2-0000-1000-8000-00805F9B34FB';
let callback: any = '';
let taskTime = 10;
/**
*
* @param command
* @param data
* @returns
*/
function createInstruct(command: number, data: number[]): number[] {
const header = [0xaa, 0xbb];
let instruction: number[] = [];
let payload: number[] = [];
instruction = instruction.concat(header);
payload.push(command);
payload = payload.concat(data);
payload.splice(payload.length + 2);
payload = doCrc(payload);
instruction = instruction.concat(payload);
return instruction;
}
/**
* CRC校验
* @param data
* @returns CRC后的数据
*/
function doCrc(data: number[]): number[] {
const crcResult = crc16ab(data, true, false);
return data.concat(crcResult);
}
/**
* Uint8数组
* @param str
* @returns Uint8数组
*/
function strToUint8(str: string): number[] {
const result: number[] = [];
for (let i = 0; i < str.length; i++) {
result[i] = str.charCodeAt(i);
}
return result;
}
/**
* CRC16校验算法
* @param data
* @param reverseResult
* @param isHexString
* @returns CRC校验结果
*/
function crc16ab(data: any[], reverseResult: boolean, isHexString: boolean): number[] {
let crc = 0xffff;
const polynomial = 0x1021;
for (let i = 0; i < data.length; i++) {
const byte = isHexString ? parseInt(data[i], 16) : data[i];
for (let bit = 0; bit < 8; bit++) {
const bitValue = (byte >> (7 - bit) & 1) === 1;
const crcBit = (crc >> 15 & 1) === 1;
crc <<= 1;
if (crcBit !== bitValue) {
crc ^= polynomial;
}
}
}
const high = (crc & 0xff00) >> 8;
const low = crc & 0xff;
return reverseResult ? [low, high] : [high, low];
}
/**
*
* @param array
* @param chunkSize
* @returns
*/
function arrayChunks(array: any[], chunkSize: number): any[] {
const chunks: any[] = [];
for (let i = 0; i < array.length; i += chunkSize) {
chunks.push(array.slice(i, i + chunkSize));
}
return chunks;
}
/**
* CRC十六进制值
* @param data
* @returns CRC值
*/
function getCrcHex(data: any[]): string {
const crcArray = crc16ab(data, true, true);
const hexString = ab2hex(crcArray).join('');
return hexString;
}
/**
*
* @param arrayBuffer
* @returns
*/
function ab2hex(arrayBuffer: ArrayBuffer | number[]): string[] {
const hexArray = Array.prototype.map.call(
new Uint8Array(arrayBuffer),
function(byte: number): string {
return ('00' + byte.toString(16)).slice(-2);
}
) as string[];
return hexArray;
}
/**
*
* @param hexString
* @returns
*/
function hex2ab(hexString: string[]): number[] {
const result: number[] = [];
for (let i = 0; i < hexString.length; i++) {
result.push(parseInt(hexString[i], 16));
}
return result;
}
/**
*
* @param instructData
*/
function dealInstruct(instructData: any): void {
const hexData = ab2hex(instructData);
while (hexData.indexOf('aa') >= 0) {
const startIndex = hexData.indexOf('aa');
const length = parseInt(hexData[startIndex + 2], 16);
const instructHex = hexData.slice(startIndex, length + 3);
callback(resolutionInstruct(instructHex));
}
}
/**
*
* @param data
* @returns
*/
function checkValidity(data: any[]): boolean {
if (data.length < 2) return false;
const chunks = arrayChunks(data, data.length - 2);
const crcData = chunks.pop();
if (!crcData) return false;
const receivedCrc = crcData[0] + crcData[1];
const payload = chunks.pop();
if (!payload) return false;
const calculatedCrc = getCrcHex(payload);
return receivedCrc === calculatedCrc;
}
/**
*
* @param deviceIdParam ID
* @param callbackParam
*/
function init(deviceIdParam: string, callbackParam: any): void {
deviceId = deviceIdParam;
callback = callbackParam;
wx.onBLECharacteristicValueChange(function(res: any) {
dealInstruct(res.value);
});
}
/**
*
* @param instructArray
* @returns
*/
function resolutionInstruct(instructArray: string[]): any {
if (instructArray.shift() != 'aa') return setCallData('-1', '-1', '数据错误');
if (instructArray.shift() != 'bb') return setCallData('-1', '-1', '数据错误');
if (checkValidity(instructArray)) {
const command = parseInt(instructArray[1], 16);
if (command == 0x8) {
// 锁状态指令
if (instructArray[2] == '01') return setCallData('08', '01', '手柄打开');
else if (instructArray[2] == '00') return setCallData('08', '00', '手柄关闭');
} else if (command == 0x7) {
// 开锁指令
if (instructArray[2] == '01') return setCallData('07', '01', '开锁成功');
else if (instructArray[2] == '02') return setCallData('07', '00', '钥匙开锁');
} else if (command == 0x12) {
// 设置锁ID指令
if (instructArray[2] == '01') return setCallData('12', '01', '修改成功');
else if (instructArray[2] == '02') return setCallData('12', '00', '修改失败');
} else if (command == 0x11) {
// 获取锁ID指令
if (instructArray[2] == '01') {
const idData = instructArray.slice(3, 7);
const idArray = hexToAb(idData);
const lockId = byteToInt(idArray);
return setCallData('11', lockId, '获取成功');
}
} else if (command == 0x10) {
// 发送开锁密钥指令
if (instructArray[2] == '01') return setCallData('10', '01', '开锁成功');
} else if (command == 0x5) {
// 设置蓝牙名称指令
return instructArray[2] == '01' ? setCallData('05', '01', '修改成功') : setCallData('05', '00', '修改失败');
} else if (command == 0xa) {
// 获取当前时间指令
const timeData = instructArray.slice(2, 7);
setTaskTime(timeData, taskTime, () => {});
} else if (command == 0x22) {
// 设置任务时间指令
return instructArray[2] == '01' ? setCallData('22', '01', '修改成功') : setCallData('22', '00', '修改失败');
} else if (command == 0x23) {
// 设置任务权限指令
return instructArray[2] == '01' ? setCallData('23', '01', '修改成功') : setCallData('23', '00', '修改失败');
} else if (command == 0x2a) {
// 清除任务权限指令
return instructArray[2] == '01' ? setCallData('2A', '01', '清除成功') : setCallData('2A', '00', '清除失败');
}
}
}
/**
*
* @param code
* @param data
* @param msg
* @returns
*/
function setCallData(code: string, data: any, msg: string): any {
const callbackData = {
'code': code,
'data': data,
'msg': msg
};
return callbackData;
}
/**
*
* @param instruction
* @param callback
*/
function submitInstructions(instruction: number[], callback: any): void {
const buffer = new Uint8Array(instruction).buffer;
if (buffer.byteLength > 0) {
wx.writeBLECharacteristicValue({
'deviceId': deviceId,
'serviceId': serviceId,
'characteristicId': characteristicId,
'value': buffer,
'success'(res: any) {
callback(res);
},
'fail'(error: any) {
callback(error);
}
});
} else {
callback('参数错误');
}
}
/**
*
* @param callback
* @returns
*/
function checkCallback(callback: any): boolean {
return typeof callback == 'function';
}
/**
*
* @param callback
* @returns
*/
function openLock(callback: any): any {
const isValidCallback = checkCallback(callback);
if (isValidCallback) {
const data = [0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff];
const instruction = createInstruct(0x7, data);
return submitInstructions(instruction, callback);
} else {
return '参数错误';
}
}
/**
*
* @param callback
* @returns
*/
function sendOpenKey(callback: any): any {
const isValidCallback = checkCallback(callback);
if (isValidCallback) {
const data = [0x32, 0x0, 0xff, 0xff, 0xff, 0xff];
const instruction = createInstruct(0x10, data);
return submitInstructions(instruction, callback);
} else {
return '参数错误';
}
}
/**
*
* @param callback
* @returns
*/
function lockStatus(callback: any): any {
const isValidCallback = checkCallback(callback);
if (isValidCallback) {
const data: number[] = [];
const instruction = createInstruct(0x8, data);
return submitInstructions(instruction, callback);
} else {
return '参数错误';
}
}
/**
*
* @param callback
* @returns
*/
function sendHeart(callback: any): any {
const data: number[] = [];
const instruction = createInstruct(0xff, data);
return submitInstructions(instruction, callback);
}
/**
*
* @param name
* @param callback
* @returns
*/
function setBluetoothName(name: string, callback: any): any {
const isValidCallback = checkCallback(callback);
if (isValidCallback) {
const nameData = strToUint8(name);
const instruction = createInstruct(0x5, nameData);
return submitInstructions(instruction, callback);
} else {
return '参数错误';
}
}
/**
* ID
* @param callback
* @returns
*/
function getLockId(callback: any): any {
const isValidCallback = checkCallback(callback);
if (isValidCallback) {
const data = [0xff, 0xff, 0xff, 0xff];
const instruction = createInstruct(0x11, data);
return submitInstructions(instruction, callback);
} else {
return '参数错误';
}
}
/**
* ID
* @param lockId ID
* @param callback
* @returns
*/
function setLockId(lockId: number, callback: any): any {
const isValidCallback = checkCallback(callback);
if (isValidCallback) {
const padding = [0xff, 0xff, 0xff, 0xff];
const idBytes = intToByte4(lockId);
const data = idBytes.concat(padding);
const instruction = createInstruct(0x12, data);
return submitInstructions(instruction, callback);
} else {
return '参数错误';
}
}
/**
*
* @param time
* @param callback
*/
function startTaskTime(time: number, callback: any): void {
taskTime = time;
getCurrentTime(callback);
}
/**
*
* @param callback
* @returns
*/
function getCurrentTime(callback: any): any {
const isValidCallback = checkCallback(callback);
if (isValidCallback) {
const data: number[] = [];
const instruction = createInstruct(0xa, data);
return submitInstructions(instruction, callback);
} else {
return '参数错误';
}
}
/**
*
* @param timeHex
* @param timeValue
* @param callback
* @returns
*/
function setTaskTime(timeHex: string[], timeValue: number, callback: any): any {
const isValidCallback = checkCallback(callback);
if (isValidCallback) {
const timeData = hex2ab(timeHex);
const timeBytes = intToByte4R(timeValue);
const data = timeData.concat(timeBytes).concat([0xff]);
const instruction = createInstruct(0x22, data);
return submitInstructions(instruction, callback);
} else {
return '参数错误';
}
}
/**
*
* @param taskId ID
* @param permission
* @param callback
* @returns
*/
function setTaskPrem(taskId: number, permission: number, callback: any): any {
const isValidCallback = checkCallback(callback);
if (isValidCallback) {
const padding = [0xff, 0xff, 0xff, 0xff];
const idBytes = intToByte4(taskId);
const data = idBytes.concat(padding);
data.push(permission);
const instruction = createInstruct(0x23, data);
return submitInstructions(instruction, callback);
} else {
return '参数错误';
}
}
/**
*
* @param callback
* @returns
*/
function cleanTaskPrem(callback: any): any {
const isValidCallback = checkCallback(callback);
if (isValidCallback) {
const data: number[] = [];
const instruction = createInstruct(0x2a, data);
return submitInstructions(instruction, callback);
} else {
return '参数错误';
}
}
/**
* 4
* @param value
* @returns
*/
function intToByte4(value: number): number[] {
const bytes: number[] = [];
bytes[3] = value & 0xff;
bytes[2] = (value >> 8) & 0xff;
bytes[1] = (value >> 16) & 0xff;
bytes[0] = (value >> 24) & 0xff;
return bytes;
}
/**
* 4
* @param value
* @returns
*/
function intToByte4R(value: number): number[] {
const bytes: number[] = [];
bytes[0] = value & 0xff;
bytes[1] = (value >> 8) & 0xff;
bytes[2] = (value >> 16) & 0xff;
bytes[3] = (value >> 24) & 0xff;
return bytes;
}
/**
*
* @param bytes
* @returns
*/
function byteToInt(bytes: number[]): number {
const byte0 = bytes[3] & 0xff;
const byte1 = bytes[2] & 0xff;
const byte2 = bytes[1] & 0xff;
const byte3 = bytes[0] & 0xff;
return (byte3 << 24) | (byte2 << 16) | (byte1 << 8) | byte0;
}
/**
*
* @param hexArray
* @returns
*/
function hexToAb(hexArray: string[]): number[] {
const result: number[] = [];
for (let i = 0; i < hexArray.length; i++) {
result.push(parseInt(hexArray[i], 16));
}
return result;
}
// 模块导出
module.exports = {
'init': init,
'openLock': openLock,
'lockStatus': lockStatus,
'sendHeart': sendHeart,
'setBluetoothName': setBluetoothName,
'sendOpenKey': sendOpenKey,
'getLockId': getLockId,
'setLockId': setLockId,
'startTaskTime': startTaskTime,
'setTaskPrem': setTaskPrem,
'cleanTaskPrem': cleanTaskPrem
};

15
web/src/utils/dict.ts

@ -167,5 +167,18 @@ export enum DICT_TYPE {
IOT_PLUGIN_STATUS = 'iot_plugin_status', // IOT 插件状态
IOT_PLUGIN_TYPE = 'iot_plugin_type', // IOT 插件类型
IOT_DATA_BRIDGE_DIRECTION_ENUM = 'iot_data_bridge_direction_enum', // 桥梁方向
IOT_DATA_BRIDGE_TYPE_ENUM = 'iot_data_bridge_type_enum' // 桥梁类型
IOT_DATA_BRIDGE_TYPE_ENUM = 'iot_data_bridge_type_enum', // 桥梁类型
// ========== LOCK 模块 ==========
LOCK_STATUS = 'lock_status', // 锁具状态
LOCK_TYPE = 'lock_type', // 锁具类型
LOCK_ENABLE_STATUS = 'lock_enable_status', // 锁具启用状态
RECORD_TYPE = 'record_type', // 锁具记录类型
LOCK_ISOLATION_TYPE = 'lock_isolation_type', // 隔离类型
LOCK_PLAN_ITEM_STATUS = 'lock_plan_item_status', // 检修任务子项状态
LOCK_PLAN_ITEM_DETAIL_STATUS = 'lock_plan_item_detail_status', // 检修任务子项详情状态
LOCK_LIFE_LOCK_TYPE = 'lock_life_lock_type', // 生命锁类型
LOCK_LIFE_LOCK_STATUS = 'lock_life_lock_status', // 生命锁状态
LOCK_ISOLATION_POINT_STATUS = 'isolation_point_status', // 隔离点状态
LOCK_PLAN_STATUS = 'lock_plan_status' // 检修任务状态
}

334
web/src/utils/lock.old.js

@ -0,0 +1,334 @@
var deviceId = '';
var serviceId = '0000FFF0-0000-1000-8000-00805F9B34FB';
var characteristicId = '0000FFF2-0000-1000-8000-00805F9B34FB';
var callback = '';
var taskTime = 10;
function createInstruct(command, data) {
let header = [0xaa, 0xbb];
let instruction = [];
let payload = [];
instruction = instruction.concat(header);
payload.push(command);
payload = payload.concat(data);
payload.push(payload.length + 2);
payload = doCrc(payload);
instruction = instruction.concat(payload);
return instruction;
}
function doCrc(data) {
let crc = crc16ab(data, true, false);
data = data.concat(crc);
return data;
}
function strToUint8(str) {
let result = [];
for (let i = 0; i < str.length; i++) {
result[i] = str.charCodeAt(i);
}
return result;
}
function crc16ab(data, swapBytes, isHex) {
let crc = 0xffff;
let polynomial = 0x1021;
for (let i = 0; i < data.length; i++) {
let byte = isHex ? parseInt(data[i], 16) : data[i];
for (let j = 0; j < 8; j++) {
let bit = (byte >> (7 - j) & 0x1) === 0x1;
let crcBit = (crc >> 15 & 0x1) === 0x1;
crc <<= 1;
if (crcBit ^ bit) crc ^= polynomial;
}
}
let highByte = (crc & 0xff00) >> 8;
let lowByte = crc & 0xff;
return swapBytes ? [lowByte, highByte] : [highByte, lowByte];
}
function arrayChunks(array, size) {
let chunks = [];
for (let i = 0; i < array.length; i += size) {
chunks.push(array.slice(i, i + size));
}
return chunks;
}
function getCrcHex(data) {
let crc = crc16ab(data, true, true);
let hex = ab2hex(crc).join('');
return hex;
}
function ab2hex(buffer) {
let hexArray = Array.prototype.map.call(new Uint8Array(buffer), function(byte) {
return ('00' + byte.toString(16)).slice(-2);
});
return hexArray;
}
function hex2ab(hexArray) {
let result = Array.prototype.map.call(new Uint8Array(hexArray), function(byte) {
return parseInt(byte, 16);
});
return result;
}
function dealInstruct(data) {
let hexData = ab2hex(data);
while (hexData.indexOf('aa') >= 0) {
let headerIndex = hexData.indexOf('aa');
let length = parseInt(hexData[headerIndex + 2], 16);
let instruction = hexData.slice(headerIndex, length + 3);
callback(resolutionInstruct(instruction));
}
}
function checkValidity(data) {
if (data.length < 2) return false;
let chunks = arrayChunks(data, data.length - 2);
let crcBytes = chunks.pop();
let crcHex = crcBytes[0] + crcBytes[1];
let dataToCheck = chunks.pop();
let calculatedCrc = getCrcHex(dataToCheck);
return crcHex === calculatedCrc;
}
function init(device, cb) {
deviceId = device;
callback = cb;
wx.onBLECharacteristicValueChange(function(res) {
dealInstruct(res.value);
});
}
function resolutionInstruct(instruction) {
if (instruction.shift() !== 'aa') return setCallData('-1', '-1', 'Data error');
if (instruction.pop() !== 'bb') return setCallData('-1', '-1', 'Data error');
if (checkValidity(instruction)) {
let command = parseInt(instruction[1], 16);
if (command === 0x8) {
if (instruction[2] === '01') return setCallData('08', '01', 'Lock opened successfully');
else if (instruction[2] === '00') return setCallData('08', '00', 'Lock failed to open');
} else if (command === 0x7) {
if (instruction[2] === '01') return setCallData('07', '01', 'Handle opened');
else if (instruction[2] === '00') return setCallData('07', '00', 'Handle closed');
} else if (command === 0x12) {
if (instruction[2] === '01') return setCallData('12', '01', 'Modification successful');
else if (instruction[2] === '00') return setCallData('12', '00', 'Modification failed');
} else if (command === 0x11) {
if (instruction[2] === '01') {
let lockIdBytes = instruction.slice(3, 7);
lockIdBytes = hexToAb(lockIdBytes);
let lockId = byteToInt(lockIdBytes);
return setCallData('11', lockId, 'Retrieved successfully');
}
} else if (command === 0x10) {
if (instruction[2] === '01') return setCallData('10', '01', 'Key unlock successful');
} else if (command === 0x5) {
return instruction[2] === '01' ? setCallData('05', '01', 'Modification successful') : setCallData('05', '00', 'Modification failed');
} else if (command === 0xa) {
let timeData = instruction.slice(2, 7);
setTaskTime(timeData, taskTime, () => {});
} else if (command === 0x22) {
return instruction[2] === '01' ? setCallData('22', '01', 'Modification successful') : setCallData('22', '00', 'Modification failed');
} else if (command === 0x23) {
return instruction[2] === '01' ? setCallData('23', '01', 'Modification successful') : setCallData('23', '00', 'Modification failed');
} else if (command === 0x2a) {
return instruction[2] === '01' ? setCallData('2A', '01', 'Clear successful') : setCallData('2A', '00', 'Clear failed');
}
}
}
function setCallData(code, data, msg) {
return { code: code, data: data, msg: msg };
}
function submitInstructions(instruction, callback) {
let buffer = new Uint8Array(instruction).buffer;
if (buffer !== '') {
wx.writeBLECharacteristicValue({
deviceId: deviceId,
serviceId: serviceId,
characteristicId: characteristicId,
value: buffer,
success(res) { callback(res); },
fail(err) { callback(err); }
});
} else {
callback('Parameter error');
}
}
function checkCallback(cb) {
return typeof cb === 'function';
}
function openLock(callback) {
if (checkCallback(callback)) {
let data = [0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff];
let instruction = createInstruct(0x7, data);
return submitInstructions(instruction, callback);
} else {
return 'Parameter error';
}
}
function sendOpenKey(callback) {
if (checkCallback(callback)) {
let data = [0x32, 0x0, 0xff, 0xff, 0xff, 0xff];
let instruction = createInstruct(0x10, data);
return submitInstructions(instruction, callback);
} else {
return 'Parameter error';
}
}
function lockStatus(callback) {
if (checkCallback(callback)) {
let data = [];
let instruction = createInstruct(0x8, data);
return submitInstructions(instruction, callback);
} else {
return 'Parameter error';
}
}
function sendHeart(callback) {
let data = [];
let instruction = createInstruct(0xff, data);
return submitInstructions(instruction, callback);
}
function setBluetoothName(name, callback) {
if (checkCallback(callback)) {
let data = strToUint8(name);
let instruction = createInstruct(0x5, data);
return submitInstructions(instruction, callback);
} else {
return 'Parameter error';
}
}
function getLockId(callback) {
if (checkCallback(callback)) {
let data = [0xff, 0xff, 0xff, 0xff];
let instruction = createInstruct(0x11, data);
return submitInstructions(instruction, callback);
} else {
return 'Parameter error';
}
}
function setLockId(lockId, callback) {
if (checkCallback(callback)) {
let data = [0xff, 0xff, 0xff, 0xff];
let lockIdBytes = intToByte4(lockId);
lockIdBytes = lockIdBytes.concat(data);
let instruction = createInstruct(0x12, lockIdBytes);
return submitInstructions(instruction, callback);
} else {
return 'Parameter error';
}
}
function startTaskTime(time, callback) {
taskTime = time;
getCurrentTime(callback);
}
function getCurrentTime(callback) {
if (checkCallback(callback)) {
let data = [];
let instruction = createInstruct(0xa, data);
return submitInstructions(instruction, callback);
} else {
return 'Parameter error';
}
}
function setTaskTime(timeData, time, callback) {
if (checkCallback(callback)) {
let timeBytes = hex2ab(timeData);
let timeValue = intToByte4R(time);
timeBytes = timeBytes.concat(timeValue);
timeBytes = timeBytes.concat([0xff]);
let instruction = createInstruct(0x22, timeBytes);
return submitInstructions(instruction, callback);
} else {
return 'Parameter error';
}
}
function setTaskPrem(param1, param2, callback) {
if (checkCallback(callback)) {
let data = [0xff, 0xff, 0xff, 0xff];
let param1Bytes = intToByte4(param1);
param1Bytes = param1Bytes.concat(data);
param1Bytes.push(param2);
let instruction = createInstruct(0x23, param1Bytes);
return submitInstructions(instruction, callback);
} else {
return 'Parameter error';
}
}
function cleanTaskPrem(callback) {
if (checkCallback(callback)) {
let data = [];
let instruction = createInstruct(0x2a, data);
return submitInstructions(instruction, callback);
} else {
return 'Parameter error';
}
}
function intToByte4(number) {
let bytes = [];
bytes[3] = number & 0xff;
bytes[2] = (number >> 8) & 0xff;
bytes[1] = (number >> 16) & 0xff;
bytes[0] = (number >> 24) & 0xff;
return bytes;
}
function intToByte4R(number) {
let bytes = [];
bytes[0] = number & 0xff;
bytes[1] = (number >> 8) & 0xff;
bytes[2] = (number >> 16) & 0xff;
bytes[3] = (number >> 24) & 0xff;
return bytes;
}
function byteToInt(bytes) {
let b0 = bytes[3] & 0xff;
let b1 = bytes[2] & 0xff;
let b2 = bytes[1] & 0xff;
let b3 = bytes[0] & 0xff;
return (b3 << 24) | (b2 << 16) | (b1 << 8) | b0;
}
function hexToAb(hexArray) {
let result = [];
for (let i = 0; i < hexArray.length; i++) {
result.push(parseInt(hexArray[i], 16));
}
return result;
}
module.exports = {
init,
openLock,
lockStatus,
sendHeart,
setBluetoothName,
sendOpenKey,
getLockId,
setLockId,
startTaskTime,
setTaskPrem,
cleanTaskPrem
};

300
web/src/utils/lock.ts

@ -0,0 +1,300 @@
import { useAppStore } from '@/store/modules/app'
import { useUserStore } from '@/store/modules/user'
import { useElLockStore } from '@/store/modules/elLock'
const elLockStore = useElLockStore()
const appStore = useAppStore()
import ww from '@/utils/ww'
import { LockApi } from '@/api/electron/lock'
import { LockWorkRecordApi } from '@/api/electron/lockworkcord'
import { bindLock as bindLockApi } from '@/api/lock'
import { ElMessage, ElMessageBox } from 'element-plus'
import download from '@/utils/download'
import { updateFile } from '@/api/infra/file'
type Location = {
latitude: number
longitude: number
accuracy: number
}
export const getCurrentLocation = async (): Promise<Location> => {
if (appStore.isWorkWechat) {
const res = await ww.getLocation()
return {
latitude: res.latitude,
longitude: res.longitude,
accuracy: res.accuracy
}
} else if (window.navigator.geolocation) {
return new Promise((resolve, reject) => {
window.navigator.geolocation.getCurrentPosition((position) => {
resolve({
latitude: position.coords.latitude,
longitude: position.coords.longitude,
accuracy: position.coords.accuracy
})
}, (error) => {
ElMessage.error('获取位置失败')
resolve({
latitude: 0,
longitude: 0,
accuracy: 0
})
})
})
} else {
return Promise.resolve({
latitude: 0,
longitude: 0,
accuracy: 0
})
}
}
// 统一执行绑定逻辑:校验锁状态 -> 获取位置 -> 记录工单 -> 批量更新状态
async function performBind(detail: any, lockId: number, operatorId: number): Promise<void> {
// 校验锁具状态
const lock = await LockApi.getLock(lockId)
if (!lock) {
ElMessage.error('锁具不存在')
return
}
if (lock.lockStatus != 2) {
ElMessage.error('锁具状态异常 无法绑定')
return
}
// 获取当前位置
let location: Location
try {
location = await getCurrentLocation()
} catch (_) {
ElMessage.error('获取位置失败,无法绑定')
return
}
// // 批量更新业务状态
// await Promise.all([
// PointApi.updatePoint({
// id: detail.isolationPointId,
// status: 1 // 已锁定
// }),
// LockApi.updateLock({
// id: lockId,
// lockStatus: 7 // 已绑定
// }),
// PlanItemDetailApi.updatePlanItemDetail({
// id: detail.id,
// lockId: lockId,
// lockStatus: 1 // 未上锁
// })
// ])
await bindLockApi({ planItemDetailId: detail.id, lockId })
// 创建工单记录
await LockWorkRecordApi.createLockWorkRecord({
operatorId: Number(operatorId),
lockId: lockId,
isolationPlanItemDetailId: detail.id,
recordType: 2, // 未绑定
gpsCoordinates: `${location.latitude},${location.longitude}`
})
ElMessage.success('绑定成功')
}
export const bindLock = async (detail: any) => {
try {
if (appStore.isWorkWechat) {
const currentUserId = useUserStore().getUser.id
// 扫描二维码获取锁具ID
const scanRes = await (ww as any).scanQRCode({
needResult: true,
scanType: ['qrCode']
})
const lockId = Number((scanRes && scanRes.resultStr) || NaN)
if (!lockId || Number.isNaN(lockId)) {
ElMessage.error('二维码内容无效')
return
}
await performBind(detail, lockId, currentUserId)
} else {
const lockOptions = elLockStore.locks.filter(i => i.lockEnableStatus == 1 && i.lockStatus == 2).map((lock) => ({
label: lock.lockName,
number: lock.lockNumber,
value: lock.id
}))
// 检查是否有可用的锁具
if (lockOptions.length === 0) {
ElMessage.error('暂无可用的锁具')
return
}
// 如果只有一个锁具,直接使用
if (lockOptions.length === 1) {
const lockId = lockOptions[0]?.value
if (lockId) {
await performBind(detail, lockId, useUserStore().getUser.id)
}
return
}
// 有多个锁具时,让用户选择
const lockOptionsHtml = lockOptions.map((option, index) =>
`<option value="${option.value}">${option.number}. ${option.label}</option>`
).join('')
let selectedLockId: string | null = null
try {
await ElMessageBox.confirm(
`<div>
<p style="margin-bottom: 10px;"></p>
<select id="lockSelector" style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px;">
<option value="">-- --</option>
${lockOptionsHtml}
</select>
</div>`,
'选择锁具',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
dangerouslyUseHTMLString: true,
beforeClose: (action, _instance, done) => {
if (action === 'confirm') {
const selector = document.getElementById('lockSelector') as HTMLSelectElement
const selectedValue = selector?.value
if (!selectedValue) {
ElMessage.warning('请选择一个锁具')
return false
}
selectedLockId = selectedValue
}
done()
}
}
)
// 用户点击确定后执行绑定
if (selectedLockId) {
await performBind(detail, Number(selectedLockId), useUserStore().getUser.id)
}
} catch (error) {
// 用户取消选择,不做任何操作
console.log('用户取消选择锁具')
}
}
} catch (error) {
ElMessage.error('绑定失败,请重试')
throw error
}
}
// 将 localId 转换为 File(先尝试 getLocalImgData,失败则用 Canvas 回退)
async function convertLocalIdToFile(localId: string): Promise<File> {
// 优先:getLocalImgData(iOS 常见)
if ((ww as any).getLocalImgData) {
try {
const localDataRes = await (ww as any).getLocalImgData({ localId })
const base64 = String((localDataRes && localDataRes.localData) || '')
const dataUrl = base64.startsWith('data:') ? base64 : `data:image/jpeg;base64,${base64}`
return download.base64ToFile(dataUrl, 'photo')
} catch (_) { /* fallback to canvas */ }
}
// 回退:使用 Canvas 生成 base64 再转 File(Android 常见)
return await new Promise<File>((resolve, reject) => {
const img = new Image()
img.crossOrigin = 'anonymous'
img.onload = () => {
try {
const canvas = document.createElement('canvas')
canvas.width = img.width
canvas.height = img.height
const ctx = canvas.getContext('2d')
if (!ctx) throw new Error('canvas context 获取失败')
ctx.drawImage(img, 0, 0, img.width, img.height)
const dataUrl = canvas.toDataURL('image/jpeg')
resolve(download.base64ToFile(dataUrl, 'photo'))
} catch (err) {
reject(err)
}
}
img.onerror = (err) => reject(err)
img.src = localId
})
}
// 创建文件选择器(非企业微信环境使用)
async function selectFile(): Promise<File> {
return new Promise((resolve, reject) => {
const input = document.createElement('input')
input.type = 'file'
input.accept = 'image/*'
input.style.display = 'none'
input.onchange = (event) => {
const target = event.target as HTMLInputElement
const file = target.files?.[0]
document.body.removeChild(input)
if (file) {
resolve(file)
} else {
reject(new Error('未选择文件'))
}
}
input.oncancel = () => {
document.body.removeChild(input)
reject(new Error('用户取消选择'))
}
document.body.appendChild(input)
input.click()
})
}
export const getPhoto = async () => {
try {
if (appStore.isWorkWechat) {
// 放宽企业微信JSSDK类型限制,兼容不同终端返回与参数
const chooseRes = await (ww as any).chooseImage({
count: 1,
sizeType: ['original'],
sourceType: ['camera'],
defaultCameraMode: 'normal',
isSaveToAlbum: false
} as any) as {
localIds?: string[]
localId?: string
tempFiles?: Array<{ file?: File }>
}
// 企业微信返回可能是 tempFiles 或 localIds/localId
const directFile: File | undefined = (chooseRes as any)?.tempFiles?.[0]?.file
const localId: string | undefined = chooseRes?.localIds?.[0] || (chooseRes as any)?.localId
const file: File | null = directFile
? directFile
: (localId ? await convertLocalIdToFile(localId) : null)
if (!file) throw new Error('无法获取拍照文件')
const uploadRes = await updateFile({ file })
return uploadRes && uploadRes.data
} else {
// 非企业微信环境:选择文件并上传
const file = await selectFile()
const uploadRes = await updateFile({ file })
return uploadRes && uploadRes.data
}
} catch (err) {
ElMessage.error('文件上传失败')
throw err
}
}

1855
web/src/utils/ww.ts

File diff suppressed because it is too large

7
web/src/views/Home/Index.vue

@ -6,7 +6,7 @@
<el-col :xl="12" :lg="12" :md="12" :sm="24" :xs="24">
<div class="flex items-center">
<el-avatar :src="avatar" :size="70" class="mr-16px">
<img src="@/assets/imgs/avatar.gif" alt="" />
<img src="@/assets/imgs/user.png" alt="" />
</el-avatar>
<div>
<div class="text-20px">
@ -18,10 +18,15 @@
</el-row>
</el-skeleton>
</el-card>
<el-card shadow="never" v-if="!userStore.getRoles.join(',').includes('affected')">
<div class="text-20px">隔离点监控</div>
<monitorTable />
</el-card>
</div>
</template>
<script lang="ts" setup>
import { useUserStore } from '@/store/modules/user'
import monitorTable from '../lock/monitorTable.vue'
defineOptions({ name: 'Index' })
const { t } = useI18n()
const userStore = useUserStore()

14
web/src/views/Login/Login.vue

@ -1,5 +1,6 @@
<template>
<div :class="prefixCls" class="relative h-[100%] lt-md:px-10px lt-sm:px-10px lt-xl:px-10px lt-xl:px-10px">
<div class="footer">京公网安备 11010702002311 </div>
<div class="relative mx-auto h-full flex">
<div
:class="`${prefixCls}__left flex-1 bg-gray-500 bg-opacity-20 relative p-30px lt-xl:hidden overflow-x-hidden overflow-y-auto`">
@ -93,6 +94,19 @@ $prefix-cls: #{$namespace}-login;
content: '';
}
}
.footer {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
text-align: center;
color: #fff;
font-size: 14px;
padding: 10px 0;
background-color: #000;
opacity: 0.5;
}
}
</style>

10
web/src/views/Login/components/LoginForm.vue

@ -91,10 +91,10 @@
mode="pop"
@success="handleLogin"
/>
<!-- <el-col :span="24" class="px-10px">
<el-col :span="24" class="px-10px">
<el-form-item>
<el-row :gutter="5" justify="space-between" style="width: 100%">
<el-col :span="8">
<!-- <el-col :span="8">
<XButton
:title="t('login.btnMobile')"
class="w-full"
@ -107,7 +107,7 @@
class="w-full"
@click="setLoginState(LoginStateEnum.QR_CODE)"
/>
</el-col>
</el-col> -->
<el-col :span="8">
<XButton
:title="t('login.btnRegister')"
@ -117,9 +117,9 @@
</el-col>
</el-row>
</el-form-item>
</el-col> -->
</el-col>
</el-row>
</el-form>
</el-form>
</template>
<script lang="ts" setup>
import { ElLoading } from 'element-plus'

23
web/src/views/Login/components/RegisterForm.vue

@ -15,7 +15,7 @@
<LoginFormTitle class="w-full" />
</el-form-item>
</el-col>
<el-col :span="24" class="px-10px">
<!-- <el-col :span="24" class="px-10px">
<el-form-item v-if="registerData.tenantEnable === 'true'" prop="tenantName">
<el-input
v-model="registerData.registerForm.tenantName"
@ -26,7 +26,7 @@
size="large"
/>
</el-form-item>
</el-col>
</el-col> -->
<el-col :span="24" class="px-10px">
<el-form-item prop="username">
<el-input
@ -38,6 +38,16 @@
</el-form-item>
</el-col>
<el-col :span="24" class="px-10px">
<el-form-item prop="mobile">
<el-input
v-model="registerData.registerForm.mobile"
:placeholder="t('login.mobileNumberPlaceholder')"
size="large"
:prefix-icon="iconCellphone"
/>
</el-form-item>
</el-col>
<el-col :span="24" class="px-10px">
<el-form-item prop="username">
<el-input
v-model="registerData.registerForm.nickname"
@ -112,6 +122,7 @@ const { t } = useI18n()
const iconHouse = useIcon({ icon: 'ep:house' })
const iconAvatar = useIcon({ icon: 'ep:avatar' })
const iconLock = useIcon({ icon: 'ep:lock' })
const iconCellphone = useIcon({ icon: 'ep:phone' })
const formLogin = ref()
const { handleBackLogin, getLoginState } = useLoginState()
const { currentRoute, push } = useRouter()
@ -152,6 +163,10 @@ const registerRules = {
confirmPassword: [
{ required: true, trigger: 'blur', message: '请再次输入您的密码' },
{ required: true, validator: equalToPassword, trigger: 'blur' }
],
mobile: [
{ required: true, trigger: 'blur', message: '请输入您的手机号码' },
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号码', trigger: 'blur' }
]
}
@ -166,7 +181,9 @@ const registerData = reactive({
username: '',
password: '',
confirmPassword: '',
captchaVerification: ''
captchaVerification: '',
mobile: '',
type: 1
}
})

2
web/src/views/Profile/components/UserAvatar.vue

@ -32,7 +32,7 @@ const handelUpload = async ({ data }) => {
const avatar = (
(await httpRequest({
file: data,
filename: 'avatar.png'
filename: 'user.png'
} as UploadRequestOptions)) as unknown as { data: string }
).data
await updateUserProfile({ avatar })

1457
web/src/views/lock/grouplock/components/BluetoothPanel.vue

File diff suppressed because it is too large

225
web/src/views/lock/grouplock/components/MobileGroupLock.vue

@ -0,0 +1,225 @@
<template>
<div class="mobile-container">
<div>位置权限{{ locationPermission ? '是' : '否' }}</div>
<div>蓝牙权限{{ bluetoothPermission ? '是' : '否' }}</div>
<div>相机权限{{ cameraPermission ? '是' : '否' }}</div>
<div class="mobile-header">
<el-input v-model="searchText" placeholder="搜索隔离点或任务名称" prefix-icon="Search" clearable class="search-input" />
<el-button type="primary" @click="$emit('refresh')" class="refresh-btn">刷新</el-button>
</div>
<div class="plan-list">
<PlanCard v-for="isolationPlan in filteredPlans" :key="isolationPlan.id" :isolation-plan="isolationPlan"
:expanded-plan="expandedPlans.includes(isolationPlan.id)" :expanded-points="expandedPoints"
:isolation-points="isolationPoints" :locks="locks" :users="users" :current-user-id="currentUserId"
:is-bluetooth-supported="isBluetoothSupported" :plan-id="isolationPlan[0]?.isolationPlanId"
@toggle-plan="togglePlan" @toggle-point="togglePoint" @bind-lock="$emit('bindLock', $event)"
@lock-operation="$emit('lockOperation', $event)" @verify-lock="$emit('verifyLock', $event)"
@unlock-verify="$emit('unlockVerify', $event)" @unlock-operation="$emit('unlockOperation', $event)" />
</div>
</div>
</template>
<script setup>
import { ref, computed, toRefs } from 'vue'
import { Refresh, Search } from '@element-plus/icons-vue'
import PlanCard from './PlanCard.vue'
import { useUserStore } from '@/store/modules/user'
// Props
const props = defineProps({
isolationPlanList: {
type: Object,
required: true
},
isolationPoints: {
type: Array,
default: () => []
},
locks: {
type: Array,
default: () => []
},
users: {
type: Array,
default: () => []
},
currentUserId: {
type: Number,
default: 0
},
isBluetoothSupported: {
type: Boolean,
default: false
}
})
// Emits
const emit = defineEmits([
'refresh',
'bindLock',
'lockOperation',
'unlockOperation',
'verifyLock',
'unlockVerify',
'unlockOperation'
])
// Reactive props
const { isolationPlanList, isolationPoints, locks, users, currentUserId } = toRefs(props)
//
const searchText = ref('')
const expandedPlans = ref([])
const expandedPoints = ref([])
//
const filteredPlans = computed(() => {
let plans = Object.values(isolationPlanList.value)
plans = plans.filter((plan) => {
return plan.some((item) =>
[item.operatorId, item.verifierId, item.operatorHelperId, item.verifierHelperId].some(
(val) => val === currentUserId.value
)
)
})
if (!searchText.value) return plans
return plans.filter((plan) =>
plan.some((item) =>
[item.isolationPointId, item.isolationPlanName].some((val) =>
val?.toString().toLowerCase().includes(searchText.value.toLowerCase())
)
)
)
})
const togglePlan = (planId) => {
const index = expandedPlans.value.indexOf(planId)
if (index > -1) {
expandedPlans.value.splice(index, 1)
} else {
expandedPlans.value.push(planId)
}
}
const togglePoint = ({ pointId, planId }) => {
const compositeKey = `${planId}_${pointId}`
const index = expandedPoints.value.indexOf(compositeKey)
if (index > -1) {
expandedPoints.value.splice(index, 1)
} else {
expandedPoints.value.push(compositeKey)
}
}
//
defineExpose({
searchText,
expandedPlans,
expandedPoints,
filteredPlans,
})
</script>
<style scoped>
.mobile-container {
padding: 16px;
background-color: #f5f5f5;
min-height: 100vh;
}
.mb-4 {
margin-bottom: 16px;
}
.mobile-header {
display: flex;
gap: 12px;
margin-bottom: 16px;
align-items: center;
}
.search-input {
flex: 1;
}
.refresh-btn {
flex-shrink: 0;
}
.status-summary {
display: flex;
gap: 12px;
margin-bottom: 16px;
padding: 16px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.summary-item {
flex: 1;
text-align: center;
}
.summary-value {
font-size: 24px;
font-weight: bold;
color: #409eff;
margin-bottom: 4px;
}
.summary-value.locked {
color: #67c23a;
}
.summary-value.unlocked {
color: #f56c6c;
}
.summary-label {
font-size: 14px;
color: #666;
}
.plan-list {
display: flex;
flex-direction: column;
gap: 12px;
}
@media (max-width: 480px) {
.mobile-container {
padding: 12px;
}
.mobile-header {
flex-direction: column;
gap: 8px;
}
.refresh-btn {
width: 100%;
}
.status-summary {
padding: 12px;
}
.summary-value {
font-size: 20px;
}
}
.mobile-container .el-tag {
border-radius: 12px;
}
.mobile-container .el-button {
border-radius: 6px;
font-size: 14px;
}
.mobile-container .el-input {
border-radius: 6px;
}
</style>

377
web/src/views/lock/grouplock/components/PCGroupLock.vue

@ -0,0 +1,377 @@
<template>
<div class="pc-group-lock">
<!-- 顶部工具栏 -->
<div class="toolbar">
<el-input v-model="searchText" placeholder="搜索隔离点或计划名称" prefix-icon="Search" clearable class="toolbar__search" />
<el-switch v-model="onlyMine" active-text="只看与我相关" inactive-text="全部" class="toolbar__switch" />
<el-button type="primary" @click="$emit('refresh')">刷新</el-button>
</div>
<template v-for="(isolationPlan, idx) in filteredPlans" :key="idx">
<div class="table-card">
<div class="table-card__header">
<div class="title">
<span class="name">{{ isolationPlan[0]?.isolationPlanName }}</span>
<el-tag type="info" size="small">{{ isolationPlan[0]?.isolationPlanId }}</el-tag>
</div>
<div class="meta">
<span>隔离点: {{ countPoint(isolationPlan) }}</span>
<span class="divider"></span>
<span>任务: {{ isolationPlan.length }}</span>
</div>
</div>
<el-table :data="isolationPlan" :stripe="true" :show-overflow-tooltip="true" :header-row-style="headerRowStyle"
:row-key="(row) => row.id" border :span-method="(r) => arraySpanMethod(r, isolationPlan)">
<el-table-column label="编号" align="center" prop="isolationPlanItemId" width="80" />
<el-table-column label="隔离点" align="center" prop="isolationPointId" min-width="180">
<template #default="scope">
{{ getPointName(scope.row.isolationPointId) }}
</template>
</el-table-column>
<el-table-column label="电子锁" align="center" prop="lockId" min-width="160">
<template #default="scope">
{{ getLockName(scope.row.lockId) }}
</template>
</el-table-column>
<el-table-column label="子项状态" align="center" prop="lockStatus" width="140">
<template #default="scope">
<DictTag :type="DICT_TYPE.LOCK_PLAN_ITEM_DETAIL_STATUS" :value="scope.row.lockStatus" />
</template>
</el-table-column>
<el-table-column label="集中挂牌人" align="center" prop="operatorId" min-width="160">
<template #default="scope">
{{users.find((user) => user.id === scope.row.operatorId)?.nickname}}
<small class="ml-4">{{ getLifeLockStatus(scope.row.lifeLock, 1) }}</small>
</template>
</el-table-column>
<el-table-column label="协助人" align="center" prop="operatorHelperId" min-width="160">
<template #default="scope">
<div v-if="scope.row.operatorHelperId">
{{users.find((user) => user.id === scope.row.operatorHelperId)?.nickname}}
<small class="ml-4">{{ getLifeLockStatus(scope.row.lifeLock, 2) }}</small>
</div>
<span v-else class="text-muted">-</span>
</template>
</el-table-column>
<el-table-column label="验证人" align="center" prop="verifierId" min-width="160">
<template #default="scope">
{{users.find((user) => user.id === scope.row.verifierId)?.nickname}}
<small class="ml-4">{{ getLifeLockStatus(scope.row.lifeLock, 3) }}</small>
</template>
</el-table-column>
<el-table-column label="验证协助" align="center" prop="verifierHelperId" min-width="160">
<template #default="scope">
<div v-if="scope.row.verifierHelperId">
{{users.find((user) => user.id === scope.row.verifierHelperId)?.nickname}}
<small class="ml-4">{{ getLifeLockStatus(scope.row.lifeLock, 4) }}</small>
</div>
<span v-else class="text-muted">-</span>
</template>
</el-table-column>
<el-table-column label="受影响人" align="center" min-width="200">
<template #default="scope">
<div v-if="getAffectedUsers(scope.row).length">
<el-space wrap>
<el-tag v-for="aff in getAffectedUsers(scope.row)" :key="aff.id" size="small" effect="plain">
{{users.find((u) => u.id === aff.userId)?.nickname}}
<span class="ml-4">
<DictTag :type="DICT_TYPE.LOCK_LIFE_LOCK_STATUS" :value="aff.lockStatus" />
</span>
</el-tag>
</el-space>
</div>
<span v-else class="text-muted">-</span>
</template>
</el-table-column>
<el-table-column label="生命锁邀请码" align="center" min-width="100">
<template #default="scope">
<div v-if="scope.row.lockStatus == 3 && scope.row.lockId" class="qrcode-container">
<Qrcode :text="getInviteCode(scope.row)" width="100" />
</div>
</template>
</el-table-column>
<el-table-column label="操作" align="center" fixed="right" width="360">
<template #default="scope">
<el-space>
<el-button v-show="scope.row.lockStatus == 0 && !scope.row.lockId && isOperator(scope.row)"
type="primary" size="small" @click="$emit('bindLock', scope.row)">
绑定
</el-button>
<el-button v-show="scope.row.lockStatus == 1 && scope.row.lockId && isOperator(scope.row)"
type="primary" size="small" @click="$emit('lockOperation', scope.row)">
上锁
</el-button>
<el-button v-show="scope.row.lockStatus == 2 && scope.row.lockId && isVerifier(scope.row)"
type="primary" size="small" @click="$emit('verifyLock', scope.row)">
验证
</el-button>
<el-button v-show="scope.row.lockStatus == 3 && scope.row.lockId && isVerifier(scope.row)"
type="warning" size="small" :disabled="!checkUnlockVerify(scope.row)"
@click="$emit('unlockVerify', scope.row)">
解锁验证
</el-button>
<el-button v-show="scope.row.lockStatus == 4 && scope.row.lockId && isOperator(scope.row)"
type="success" size="small" @click="$emit('unlockOperation', scope.row)">
解锁
</el-button>
</el-space>
</template>
</el-table-column>
</el-table>
</div>
</template>
</div>
</template>
<script setup>
import { ref, computed, toRefs } from 'vue'
import { DICT_TYPE, getDictLabel } from '@/utils/dict'
import { Qrcode } from '@/components/Qrcode'
const BASE_URL = import.meta.env.VITE_BASE_URL
// Emits
defineEmits([
'refresh',
'bindLock',
'lockOperation',
'verifyLock',
'unlockVerify',
'unlockOperation'
])
// Props
const props = defineProps({
isolationPlanList: {
type: Object,
required: true
},
isolationPoints: {
type: Array,
default: () => []
},
locks: {
type: Array,
default: () => []
},
users: {
type: Array,
default: () => []
},
currentUserId: {
type: Number,
default: 0
}
})
// Reactive props
const { isolationPlanList, isolationPoints, locks, users, currentUserId } = toRefs(props)
//
const searchText = ref('')
const onlyMine = ref(true)
//
const filteredPlans = computed(() => {
let plans = Object.values(isolationPlanList.value)
if (onlyMine.value) {
plans = plans.filter((plan) =>
plan.some((item) =>
[item.operatorId, item.verifierId, item.operatorHelperId, item.verifierHelperId].some(
(id) => id === currentUserId.value
)
)
)
}
if (!searchText.value) return plans
const keyword = searchText.value.toLowerCase()
return plans.filter((plan) =>
plan.some((item) => {
const pointName = getPointName(item.isolationPointId)
const planName = item.isolationPlanName || ''
return pointName.toLowerCase().includes(keyword) || planName.toLowerCase().includes(keyword)
})
)
})
//
const countPoint = (plan) => {
const set = new Set(plan.map((i) => i.isolationPointId))
return set.size
}
//
const headerRowStyle = ({ rowIndex }) => {
if (rowIndex === 0) {
return { backgroundColor: '#f5f7fa' }
}
return {}
}
//
const arraySpanMethod = ({ rowIndex, column }, isolationPlan) => {
const getSpanRow = (plan) => {
const list = []
plan.forEach((item, index) => {
if (list.length === 0) {
list.push({ rowindex: index, spanRowNum: 1, isolationPlanItemId: item.isolationPlanItemId })
} else {
const last = list[list.length - 1]
if (last.isolationPlanItemId === item.isolationPlanItemId) {
last.spanRowNum++
} else {
list.push({
rowindex: index,
spanRowNum: 1,
isolationPlanItemId: item.isolationPlanItemId
})
}
}
})
return list
}
const sameIsolationPlanItemIdColumns = ['isolationPlanItemId']
if (sameIsolationPlanItemIdColumns.includes(column.property)) {
const spanRowList = getSpanRow(isolationPlan)
const row = spanRowList.find((item) => item.rowindex === rowIndex)
if (row) return { rowspan: row.spanRowNum, colspan: 1 }
return { rowspan: 0, colspan: 0 }
}
return [1, 1]
}
// //
const isOperator = (item) =>
item.operatorId === currentUserId.value || item.operatorHelperId === currentUserId.value
const isVerifier = (item) =>
item.verifierId === currentUserId.value || item.verifierHelperId === currentUserId.value
const getPointName = (pointId) =>
isolationPoints.value.find((p) => p.id === pointId)?.ipName || `隔离点 ${pointId}`
const getLockName = (lockId) =>
lockId ? locks.value.find((l) => l.id === lockId)?.lockName || `${lockId}` : '待绑定'
const getAffectedUsers = (item) => item.lifeLock?.filter((l) => l.lockType == 5) || []
const getLifeLockStatus = (lifeLock, type) => {
if (Array.isArray(lifeLock)) {
const lock = lifeLock.find((i) => i.lockType == type)
if (lock) return getDictLabel(DICT_TYPE.LOCK_LIFE_LOCK_STATUS, lock.lockStatus)
} else if (lifeLock) {
return getDictLabel(DICT_TYPE.LOCK_LIFE_LOCK_STATUS, lifeLock.lockStatus)
}
return '未挂锁'
}
const checkUnlockVerify = (item) => getAffectedUsers(item).every((lock) => lock.lockStatus == 0)
const getInviteCode = (item) => {
return BASE_URL + '/isolationplan/isolationplan/lifelock?itemid=' + item.id
}
</script>
<style scoped>
.pc-group-lock {
padding: 12px;
}
/* 工具栏 */
.toolbar {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 12px;
}
.toolbar__search {
flex: 1;
}
.toolbar__switch {
margin-left: auto;
}
/* 卡片化表格容器 */
.table-card {
background: #fff;
border: 1px solid #ebeef5;
border-radius: 8px;
margin-bottom: 16px;
overflow: hidden;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
}
.table-card__header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
background: #f8f9fa;
border-bottom: 1px solid #ebeef5;
}
.table-card__header .title {
display: flex;
align-items: center;
gap: 10px;
}
.table-card__header .name {
font-weight: 600;
color: #303133;
}
.table-card__header .meta {
color: #909399;
display: flex;
align-items: center;
gap: 10px;
font-size: 12px;
}
.table-card__header .divider {
display: inline-block;
width: 1px;
height: 12px;
background: #e4e7ed;
}
/* PC端表格样式可以在这里自定义 */
:deep(.el-table) {
font-size: 14px;
}
:deep(.el-table th) {
background-color: #f5f7fa;
color: #606266;
font-weight: 600;
}
:deep(.el-table td) {
padding: 12px 0;
}
:deep(.el-table--border .el-table__cell) {
border-right: 1px solid #ebeef5;
}
:deep(.el-table--striped .el-table__body tr.el-table__row--striped td) {
background-color: #fafafa;
}
.ml-4 {
margin-left: 4px;
}
.text-muted {
color: #909399;
}
.qrcode-container {
display: flex;
justify-content: center;
align-items: center;
}
</style>

670
web/src/views/lock/grouplock/components/PlanCard.vue

@ -0,0 +1,670 @@
<template>
<div class="plan-card">
<!-- 计划头部 -->
<div class="plan-header" @click="togglePlan()">
<div class="plan-title">
<span class="plan-name">{{ isolationPlan[0]?.isolationPlanName }} </span>
<span class="plan-role"> ({{ currentUserRole }}) </span>
</div>
<el-icon class="expand-icon" :class="{ expanded: isExpanded }">
<ArrowDown />
</el-icon>
</div>
<el-collapse-transition>
<div v-show="isExpanded" class="plan-content">
<!-- 人员信息 -->
<div class="plan-personnel-info">
<div class="section-title">
<el-icon>
<User />
</el-icon>
</div>
<div class="personnel-list">
<div v-for="role in personnelRoles" :key="role.key" class="personnel-item"
v-show="getUserName(isolationPlan[0], role.key) !== '未指定'">
<div class="personnel-info">
<span class="personnel-role">{{ role.label }}:</span>
<span class="personnel-name">{{ getUserName(isolationPlan[0], role.key) }}</span>
</div>
</div>
</div>
</div>
<!-- 隔离点详情 -->
<div class="isolation-points-section">
<div class="section-title">
<el-icon>
<Location />
</el-icon>
</div>
<template v-for="(pointGroup, pointId) in groupByIsolationPoint(isolationPlan)" :key="pointId">
<div class="point-card">
<div class="point-header" @click="togglePoint(pointId)">
<div class="point-info">
<div class="point-name">{{ getPointName(pointId) }}</div>
<DictTag :type="DICT_TYPE.LOCK_ISOLATION_POINT_STATUS"
:value="elLockStore.isolationPoints.find((p) => p.id == pointId)?.status" />
</div>
<el-icon class="expand-icon" :class="{ expanded: expandedPoints.includes(`${planId}_${pointId}`) }">
<ArrowDown />
</el-icon>
</div>
<el-collapse-transition>
<div v-show="expandedPoints.includes(`${planId}_${pointId}`)" class="point-content">
<div class="locks-section">
<div v-for="item in pointGroup" :key="item.id" class="lock-item">
<div class="lock-header">
<div class="lock-info">
<div class="lock-name">
<el-icon>
<Lock />
</el-icon>{{ getLockName(item.lockId) }}
</div>
</div>
<DictTag :type="DICT_TYPE.LOCK_PLAN_ITEM_DETAIL_STATUS" :value="item.lockStatus" />
</div>
<!-- 受影响人员 -->
<div v-if="getAffectedUsers(item).length" class="affected-section">
<div class="affected-title">
<el-icon>
<UserFilled />
</el-icon>
</div>
<div class="affected-list">
<div v-for="affected in getAffectedUsers(item)" :key="affected.id" class="affected-item">
<span class="affected-name">{{
users.find((u) => u.id === affected.userId)?.nickname
}}</span>
<DictTag :type="DICT_TYPE.LOCK_LIFE_LOCK_STATUS" :value="affected.lockStatus" />
</div>
</div>
</div>
<!-- 锁操作 -->
<div class="lock-actions">
<el-button v-show="item.lockStatus == 0 && !item.lockId && isOperator(item)" type="primary"
size="small" @click="$emit('bindLock', item)">
<el-icon>
<Lock />
</el-icon>
</el-button>
<el-button v-show="item.lockStatus == 1 && item.lockId && isOperator(item)" type="primary"
size="small" @click="$emit('lockOperation', item)">
<el-icon>
<Lock />
</el-icon>
</el-button>
<el-button v-show="item.lockStatus == 2 && item.lockId && isVerifier(item)" type="primary"
size="small" @click="$emit('verifyLock', item)">
<el-icon>
<Lock />
</el-icon>
</el-button>
<div v-show="item.lockStatus == 3 && item.lockId">
<div class="qrcode-container">
<Qrcode :text="getInviteCode(item)" />
</div>
</div>
<el-button v-show="item.lockStatus == 3 && item.lockId && isVerifier(item)" type="primary"
size="small" @click="$emit('unlockVerify', item)" :disabled="!checkUnlockVerify(item)">
<el-icon>
<Lock />
</el-icon>
</el-button>
<el-button v-show="item.lockStatus == 4 && item.lockId && isOperator(item)" type="success"
size="small" @click="$emit('unlockOperation', item)">
<el-icon>
<Unlock />
</el-icon>
</el-button>
</div>
</div>
</div>
</div>
</el-collapse-transition>
</div>
</template>
</div>
</div>
</el-collapse-transition>
</div>
</template>
<script setup>
import { ref, computed, toRefs, watch } from 'vue'
import { ArrowDown, User, Location, UserFilled, Lock, Unlock } from '@element-plus/icons-vue'
import { DICT_TYPE, getDictLabel } from '@/utils/dict'
import { useUserStore } from '@/store/modules/user'
import { useElLockStore } from '@/store/modules/elLock'
import { Qrcode } from '@/components/Qrcode'
// 0 1 2 3 4 5
// Props
const props = defineProps({
isolationPlan: {
type: Array,
required: true
},
expandedPlan: {
type: Boolean,
default: false
},
expandedPoints: {
type: Array,
default: () => []
},
isolationPoints: {
type: Array,
default: () => []
},
locks: {
type: Array,
default: () => []
},
users: {
type: Array,
default: () => []
},
currentUserId: {
type: Number,
default: 0
},
isBluetoothSupported: {
type: Boolean,
default: false
},
planId: {
type: Number,
default: 0
}
})
const BASE_URL = import.meta.env.VITE_BASE_URL
// Emits
const emit = defineEmits([
'togglePlan',
'togglePoint',
'bindLock', //
'lockOperation', //
'verifyLock', //
'unlockVerify', //
'unlockOperation' //
])
// Reactive props
const {
isolationPlan,
expandedPlan,
expandedPoints,
isolationPoints,
locks,
users,
planId,
currentUserId
} = toRefs(props)
const elLockStore = useElLockStore()
//
const isExpanded = ref(expandedPlan.value)
//
const personnelRoles = [
{ key: 'operatorId', label: '集中挂牌人', type: '1' },
{ key: 'operatorHelperId', label: '挂牌协助人', type: '2' },
{ key: 'verifierId', label: '验证人', type: '3' },
{ key: 'verifierHelperId', label: '验证协助人', type: '4' }
]
//
const currentUserRole = computed(() => {
let role = ''
personnelRoles.forEach((item) => {
if (isolationPlan.value.find((p) => p[item.key] === currentUserId.value)) {
role += item.label
}
})
return role
})
//
const togglePlan = () => {
isExpanded.value = !isExpanded.value
emit('togglePlan', planId.value)
}
const togglePoint = (pointId) => {
emit('togglePoint', { pointId, planId: planId.value })
}
//
const groupByIsolationPoint = (isolationPlan) => {
let list = isolationPlan.reduce(
(groups, item) => {
const pointId = item.isolationPointId
if (!groups[pointId]) groups[pointId] = []
groups[pointId].push(item)
return groups
},
{}
)
return list
}
const isOperator = (item) => {
return item.operatorId === currentUserId.value || item.operatorHelperId === currentUserId.value
}
const isVerifier = (item) => {
return item.verifierId === currentUserId.value || item.verifierHelperId === currentUserId.value
}
const getUserName = (item, key) =>
users.value.find((user) => user.id === item?.[key])?.nickname || '未指定'
const getPointName = (pointId) =>
elLockStore.isolationPoints.find((p) => p.id == pointId)?.ipName
const getLockName = (lockId) =>
lockId ? locks.value.find((l) => l.id === lockId)?.lockName || `${lockId}` : '待绑定'
const getAffectedUsers = (item) => item.lifeLock?.filter((l) => l.lockType == '5') || []
const getLockStatus = (lifeLock, type) => {
if (Array.isArray(lifeLock)) {
let lock = lifeLock.find((item) => item.lockType == type)
if (lock) {
return getDictLabel(DICT_TYPE.LOCK_LIFE_LOCK_STATUS, lock.lockStatus)
}
} else {
return getDictLabel(DICT_TYPE.LOCK_LIFE_LOCK_STATUS, lifeLock.lockStatus)
}
return '未挂锁'
}
//
const calculateStatus = (items, checkFn) => {
const results = items.map(checkFn)
const lockedCount = results.filter(Boolean).length
const totalCount = results.length
return {
type: lockedCount === 0 ? 'danger' : lockedCount === totalCount ? 'success' : 'warning',
text:
lockedCount === 0
? '未锁定'
: lockedCount === totalCount
? '全部锁定'
: `${lockedCount}/${totalCount} 锁定`
}
}
const getInviteCode = (item) => {
return BASE_URL + '/isolationplan/isolationplan/lifelock?itemid=' + item.id
}
const getLockStatusType = (status) =>
({
1: 'success',
2: 'warning'
})[status] || 'danger'
const getLockStatusTagType = (lifeLock, type) => {
//
const status = getLockStatus(lifeLock, type)
return status === '未挂锁' ? 'danger' : status.includes('已锁') ? 'success' : 'warning'
}
const checkUnlockVerify = (item) => {
let affectedUsers = getAffectedUsers(item)
return affectedUsers.every((lock) => lock.lockStatus == 0)
}
// prop
watch(
() => props.expandedPlan,
(newVal) => {
isExpanded.value = newVal
}
)
</script>
<style scoped>
.plan-card {
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
overflow: hidden;
transition: box-shadow 0.2s ease;
}
.plan-card:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.plan-header {
padding: 16px;
background: #f8f9fa;
border-bottom: 1px solid #ebeef5;
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
}
.plan-title {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
flex: 1;
}
.plan-name {
font-weight: 600;
font-size: 16px;
color: #303133;
}
.plan-role {
font-size: 12px;
color: #909399;
}
.expand-icon {
transition: transform 0.3s ease;
color: #909399;
}
.expand-icon.expanded {
transform: rotate(180deg);
}
.plan-content {
padding: 12px;
}
.plan-personnel-info {
background: white;
border-radius: 6px;
padding: 8px 12px;
margin-bottom: 12px;
border: 1px solid #e4e7ed;
}
.isolation-points-section {
background: white;
border-radius: 8px;
padding: 16px;
margin-bottom: 16px;
border: 1px solid #e4e7ed;
}
.personnel-list {
display: flex;
flex-direction: column;
gap: 6px;
}
.personnel-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 6px 8px;
background: #f8f9fa;
border-radius: 4px;
border: 1px solid #ebeef5;
}
.personnel-info {
display: flex;
align-items: center;
gap: 6px;
flex: 1;
}
.personnel-role {
font-size: 12px;
color: #909399;
font-weight: 500;
white-space: nowrap;
}
.personnel-name {
font-size: 13px;
color: #303133;
font-weight: 600;
}
.section-title {
font-weight: 600;
color: #303133;
margin-bottom: 12px;
padding-bottom: 6px;
border-bottom: 1px solid #ebeef5;
display: flex;
align-items: center;
gap: 6px;
font-size: 14px;
}
.plan-personnel-info .section-title {
margin-bottom: 8px;
padding-bottom: 4px;
font-size: 13px;
}
.section-title .el-icon {
color: #409eff;
}
.point-card {
background: #fafbfc;
border-radius: 6px;
margin-bottom: 12px;
border: 1px solid #e4e7ed;
overflow: hidden;
}
.point-card:last-child {
margin-bottom: 0;
}
.point-header {
padding: 12px 16px;
background: #f5f7fa;
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
}
.point-info {
display: flex;
align-items: center;
gap: 12px;
flex: 1;
}
.point-name {
font-weight: 600;
font-size: 15px;
color: #303133;
}
.point-content {
padding: 12px;
}
.locks-section {
display: flex;
flex-direction: column;
gap: 12px;
}
.lock-item {
background: white;
border-radius: 6px;
padding: 12px;
border: 1px solid #ebeef5;
}
.lock-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.lock-info {
flex: 1;
}
.lock-name {
font-weight: 600;
color: #303133;
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 4px;
}
.lock-name .el-icon {
color: #909399;
font-size: 14px;
}
.lock-id {
font-size: 12px;
color: #909399;
}
.affected-section {
margin: 12px 0;
padding: 12px;
background: #f8f9fa;
border-radius: 6px;
border: 1px solid #ebeef5;
}
.affected-title {
font-size: 14px;
color: #606266;
margin-bottom: 8px;
font-weight: 500;
display: flex;
align-items: center;
gap: 6px;
}
.affected-title .el-icon {
color: #f56c6c;
font-size: 14px;
}
.affected-list {
display: flex;
flex-direction: column;
gap: 6px;
}
.affected-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 6px 8px;
background: white;
border-radius: 4px;
border: 1px solid #ebeef5;
}
.affected-name {
color: #303133;
font-weight: 500;
flex: 1;
}
.lock-actions {
display: flex;
gap: 8px;
justify-content: center;
padding-top: 12px;
border-top: 1px solid #ebeef5;
}
.lock-actions .el-button {
flex: 1;
max-width: 100px;
}
@media (max-width: 480px) {
.plan-header {
padding: 12px;
}
.plan-name {
font-size: 14px;
}
.plan-personnel-info {
padding: 6px 8px;
}
.isolation-points-section {
padding: 12px;
}
.personnel-list {
gap: 4px;
}
.personnel-item {
padding: 4px 6px;
}
.personnel-role {
font-size: 11px;
}
.personnel-name {
font-size: 12px;
}
.plan-personnel-info .section-title {
font-size: 12px;
margin-bottom: 6px;
}
.point-header {
padding: 10px 12px;
}
.point-content {
padding: 8px;
}
.lock-item {
padding: 10px;
}
.lock-actions {
flex-direction: column;
}
.lock-actions .el-button {
max-width: none;
}
}
.el-button+.el-button {
margin-left: 0;
}
.qrcode-container {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
border: 1px solid #ebeef5;
border-radius: 6px;
}
</style>

283
web/src/views/lock/grouplock/grouplock.vue

@ -0,0 +1,283 @@
<template>
<div>
<!-- 移动端显示 -->
<template v-if="appStore.mobile">
<!-- 蓝牙操作面板 -->
<!-- <BluetoothPanel @lock-response="handleLockResponse" /> -->
<!-- 移动端主内容 -->
<MobileGroupLock v-loading="loading" :isolation-plan-list="isolationPlanList"
:isolation-points="elLockStore.isolationPoints" :locks="elLockStore.locks" :users="elLockStore.users"
:current-user-id="currentUserId" :is-bluetooth-supported="bluetooth.isSupport" @refresh="refreshData"
@bind-lock="handleBindLock" @lock-operation="handleLockOperation" @unlock-operation="handleUnlockOperation"
@verify-lock="handleVerifyLock" @unlock-verify="handleUnlockVerify" />
</template>
<!-- PC端显示 -->
<template v-else>
<PCGroupLock :isolation-plan-list="isolationPlanList" :isolation-points="elLockStore.isolationPoints"
:locks="elLockStore.locks" :users="elLockStore.users" :current-user-id="currentUserId" @refresh="refreshData"
@bind-lock="handleBindLock" @lock-operation="handleLockOperation" @verify-lock="handleVerifyLock"
@unlock-verify="handleUnlockVerify" @unlock-operation="handleUnlockOperation" />
</template>
</div>
</template>
<script setup>
import { ref, onMounted, onActivated } from 'vue'
import ww from '@/utils/ww'
import { useAppStore } from '@/store/modules/app'
import { useElLockStore } from '@/store/modules/elLock'
import { ElMessage, ElMessageBox } from 'element-plus'
import { useUserStore } from '@/store/modules/user'
import { PlanLifeLockApi } from '@/api/isolation/planlifelock'
import { LockApi } from '@/api/electron/lock'
import { LockWorkRecordApi } from '@/api/electron/lockworkcord'
import { PointApi } from '@/api/isolation/point'
import { PlanItemDetailApi } from '@/api/isolation/planitemdetail'
import { PlanItemApi } from '@/api/isolation/planitem'
import { PlanApi } from '@/api/isolation/plan'
import { updateFile } from '@/api/infra/file'
import dayjs from 'dayjs'
import download from '@/utils/download'
import { getCurrentLocation, bindLock, getPhoto } from '@/utils/lock'
import { lockAction, verifyLockAction, verifyUnlockAction, unLockAction } from '@/api/lock'
//
import BluetoothPanel from './components/BluetoothPanel.vue'
import MobileGroupLock from './components/MobileGroupLock.vue'
import PCGroupLock from './components/PCGroupLock.vue'
const locationPermission = ref(false)
const bluetoothPermission = ref(false)
const cameraPermission = ref(false)
onMounted(async () => {
let location = await getCurrentLocation()
locationPermission.value = location.latitude != 0 && location.longitude != 0
if (appStore.mobile && appStore.isWorkWechat) {
bluetoothPermission.value = await ww.bluetooth.checkSupport()
}
})
defineOptions({ name: 'Grouplock' })
// Store
const appStore = useAppStore()
const elLockStore = useElLockStore()
const userStore = useUserStore()
//
const isolationPlanList = ref({})
const bluetooth = ww.bluetooth
// id
const currentUserId = computed(() => {
return useUserStore().getUser.id
})
let loading = ref(false)
//
onMounted(() => {
refreshData()
})
onActivated(() => {
refreshData()
})
//
const getItemDetailList = (isolationPlan) => {
let list = []
isolationPlan.planItem.forEach((item) => {
item.planItemDetail.forEach((detail) => {
list.push({
isolationPlanId: isolationPlan.id,
isolationPlanName: isolationPlan.ipName,
...item,
...detail,
lifeLock: detail.itemLifeLock
})
})
})
return list
}
const refreshData = async () => {
loading.value = true
try {
await elLockStore.init()
isolationPlanList.value = elLockStore.plan2DetailTree.reduce((acc, item) => {
acc[item.id] = getItemDetailList(item)
return acc
}, {})
} catch (error) {
ElMessage.error('数据刷新失败')
} finally {
loading.value = false
}
}
const handleLockOperation = async (item) => {
//
let lock = await LockApi.getLock(item.lockId)
if (lock.lockStatus != 7) {
ElMessage.error('锁具状态异常 无法锁定')
refreshData()
return
}
//
let location = await getCurrentLocation()
let photo = await getPhoto()
LockWorkRecordApi.createLockWorkRecord({
operatorId: currentUserId.value,
lockId: item.lockId,
isolationPlanItemDetailId: item.id,
recordType: 3, //
afterPhotoPath: photo,
gpsCoordinates: `${location.latitude},${location.longitude}`
}).then((res) => {
//
let promiseList = [
lockAction({ planItemDetailId: item.id, operateRecordId: res })
]
Promise.all(promiseList).then((res) => {
ElMessage.success('上锁成功')
refreshData()
}).catch((err) => {
ElMessage.error('上锁失败')
refreshData()
})
})
}
const handleUnlockOperation = async (detail) => {
//
let lock = await LockApi.getLock(detail.lockId)
if (lock.lockStatus != 3) {
ElMessage.error('锁具状态异常 无法解锁')
refreshData()
return
}
let location = await getCurrentLocation()
let photo = await getPhoto()
const lifelock = elLockStore.planLifeLocks.find(
(item) =>
item.isolationPlanItemDetailId == detail.id &&
item.userId == currentUserId.value &&
item.lockStatus == 1 &&
item.lockType == 1
)
if (!lifelock) {
ElMessage.error('未找到生命锁')
refreshData()
return
}
LockWorkRecordApi.createLockWorkRecord({
operatorId: currentUserId.value,
lockId: detail.lockId,
isolationPlanItemDetailId: detail.id,
recordType: 14, //
afterPhotoPath: photo,
gpsCoordinates: `${location.latitude},${location.longitude}`
}).then((res) => {
//
let promiseList = [
unLockAction({ planItemDetailId: detail.id, planId: detail.planId, lifelockId: lifelock.id })
]
Promise.all(promiseList).then((res) => {
ElMessage.success('解锁成功')
refreshData()
}).catch((err) => {
ElMessage.error('解锁失败')
refreshData()
})
})
}
const handleBindLock = (item) => {
bindLock(item).then(() => {
ElMessage.success('绑定成功')
refreshData()
}).catch((err) => {
ElMessage.error('绑定失败')
refreshData()
})
}
const handleVerifyLock = async (item) => {
//
try {
let location = await getCurrentLocation()
let photo = await getPhoto()
} catch (err) {
ElMessage.error('获取照片失败,无法验证')
refreshData()
return
}
LockWorkRecordApi.createLockWorkRecord({
operatorId: currentUserId.value,
lockId: item.lockId,
isolationPlanItemDetailId: item.id,
recordType: 17, //
afterPhotoPath: photo,
gpsCoordinates: `${location.latitude},${location.longitude}`
}).then((res) => {
//
let promiseList = [
verifyLockAction({ planItemDetailId: item.id, verifyRecordId: res })
]
Promise.all(promiseList).then((res) => {
ElMessage.success('验证成功')
refreshData()
}).catch((err) => {
ElMessage.error('验证失败')
refreshData()
})
})
}
const handleUnlockVerify = async (item) => {
//
ElMessageBox.confirm('确定要验证解锁吗?', '解锁确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async (res) => {
if (res === 'confirm') {
let location = await getCurrentLocation()
const lifelock = elLockStore.planLifeLocks.find(
(lifelock) =>
lifelock.isolationPlanItemDetailId == item.id &&
lifelock.userId == currentUserId.value &&
lifelock.lockStatus == 1 &&
lifelock.lockType == 3
)
if (!lifelock) {
ElMessage.error('未找到生命锁')
refreshData()
return
}
LockWorkRecordApi.createLockWorkRecord({
operatorId: currentUserId.value,
lockId: item.lockId,
isolationPlanItemDetailId: item.id,
recordType: 18, //
gpsCoordinates: `${location.latitude},${location.longitude}`
}).then((res) => {
const promiseList = [
verifyUnlockAction({ planItemDetailId: item.id, lifelockId: lifelock.id })
]
Promise.all(promiseList).then((res) => {
ElMessage.success('验证成功')
refreshData()
}).catch((err) => {
ElMessage.error('验证失败')
refreshData()
})
})
}
})
}
//
const handleLockResponse = (data) => {
//
}
</script>
<style scoped>
/* 主组件样式 - 只保留容器级别的样式 */
</style>

115
web/src/views/lock/guide/isolationpoint/IsolationPointForm.vue

@ -0,0 +1,115 @@
<template>
<Dialog :title="dialogTitle" v-model="dialogVisible">
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="140px"
v-loading="formLoading"
>
<el-form-item label="隔离指导书" prop="guideId">
<el-select v-model="formData.guideId" placeholder="请选择隔离指导书" filterable>
<el-option
v-for="item in elLockStore.lockGuides"
:key="item.id"
:label="item.guideContent"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="隔离点" prop="isolationPointId">
<el-select v-model="formData.isolationPointId" placeholder="请选择隔离点" filterable>
<el-option
v-for="item in elLockStore.isolationPoints"
:key="item.id"
:label="item.ipName"
:value="item.id"
/>
</el-select>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="submitForm" type="primary" :disabled="formLoading"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { IsolationPointApi, IsolationPoint } from '@/api/guide/isolationpoint'
import { useElLockStore } from '@/store/modules/elLock'
const elLockStore = useElLockStore()
/** 指导书与隔离点关联 表单 */
defineOptions({ name: 'IsolationPointForm' })
const { t } = useI18n() //
const message = useMessage() //
const dialogVisible = ref(false) //
const dialogTitle = ref('') //
const formLoading = ref(false) // 12
const formType = ref('') // create - update -
const formData = ref({
id: undefined,
guideId: undefined,
isolationPointId: undefined
})
const formRules = reactive({
guideId: [{ required: true, message: '隔离指导书ID不能为空', trigger: 'blur' }],
isolationPointId: [{ required: true, message: '隔离点ID不能为空', trigger: 'blur' }]
})
const formRef = ref() // Ref
/** 打开弹窗 */
const open = async (type: string, id?: number) => {
dialogVisible.value = true
dialogTitle.value = t('action.' + type)
formType.value = type
resetForm()
//
if (id) {
formLoading.value = true
try {
formData.value = await IsolationPointApi.getIsolationPoint(id)
} finally {
formLoading.value = false
}
}
}
defineExpose({ open }) // open
/** 提交表单 */
const emit = defineEmits(['success']) // success
const submitForm = async () => {
//
await formRef.value.validate()
//
formLoading.value = true
try {
const data = formData.value as unknown as IsolationPoint
if (formType.value === 'create') {
await IsolationPointApi.createIsolationPoint(data)
message.success(t('common.createSuccess'))
} else {
await IsolationPointApi.updateIsolationPoint(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
//
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: undefined,
guideId: undefined,
isolationPointId: undefined
}
formRef.value?.resetFields()
}
</script>

257
web/src/views/lock/guide/isolationpoint/index.vue

@ -0,0 +1,257 @@
<template>
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="100px"
>
<el-form-item label="隔离指导书" prop="guideId">
<el-select
v-model="queryParams.guideId"
placeholder="请输入隔离指导书"
clearable
filterable
class="!w-240px"
>
<el-option
v-for="item in elLockStore.lockGuides"
:key="item.id"
:label="item.guideContent"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="隔离点" prop="isolationPointId">
<el-select
v-model="queryParams.isolationPointId"
placeholder="请输入隔离点"
clearable
filterable
class="!w-240px"
>
<el-option
v-for="item in elLockStore.isolationPoints"
:key="item.id"
:label="item.ipName"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="创建时间" prop="createTime">
<el-date-picker
v-model="queryParams.createTime"
value-format="YYYY-MM-DD HH:mm:ss"
type="daterange"
start-placeholder="开始日期"
end-placeholder="结束日期"
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
class="!w-220px"
/>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
<el-button
type="primary"
plain
@click="openForm('create')"
v-hasPermi="['guide:isolation-point:create']"
>
<Icon icon="ep:plus" class="mr-5px" /> 新增
</el-button>
<el-button
type="success"
plain
@click="handleExport"
:loading="exportLoading"
v-hasPermi="['guide:isolation-point:export']"
>
<Icon icon="ep:download" class="mr-5px" /> 导出
</el-button>
<el-button
type="danger"
plain
:disabled="isEmpty(checkedIds)"
@click="handleDeleteBatch"
v-hasPermi="['guide:isolation-point:delete']"
>
<Icon icon="ep:delete" class="mr-5px" /> 批量删除
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table
row-key="id"
v-loading="loading"
:data="list"
:stripe="true"
:show-overflow-tooltip="true"
@selection-change="handleRowCheckboxChange"
>
<el-table-column type="selection" width="55" />
<el-table-column label="隔离指导书" align="center" prop="guideId">
<template #default="scope">
{{ elLockStore.lockGuides.find((item) => item.id === scope.row.guideId)?.guideContent }}
</template>
</el-table-column>
<el-table-column label="隔离点" align="center" prop="isolationPointId">
<template #default="scope">
{{
elLockStore.isolationPoints.find((item) => item.id === scope.row.isolationPointId)
?.ipName
}}
</template>
</el-table-column>
<el-table-column
label="创建时间"
align="center"
prop="createTime"
:formatter="dateFormatter"
width="180px"
/>
<el-table-column label="操作" align="center" min-width="120px">
<template #default="scope">
<el-button
link
type="primary"
@click="openForm('update', scope.row.id)"
v-hasPermi="['guide:isolation-point:update']"
>
编辑
</el-button>
<el-button
link
type="danger"
@click="handleDelete(scope.row.id)"
v-hasPermi="['guide:isolation-point:delete']"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap>
<!-- 表单弹窗添加/修改 -->
<IsolationPointForm ref="formRef" @success="getList" />
</template>
<script setup lang="ts">
import { isEmpty } from '@/utils/is'
import { dateFormatter } from '@/utils/formatTime'
import download from '@/utils/download'
import { IsolationPointApi, IsolationPoint } from '@/api/guide/isolationpoint'
import IsolationPointForm from './IsolationPointForm.vue'
import { useElLockStore } from '@/store/modules/elLock'
/** 指导书与隔离点关联 列表 */
defineOptions({ name: 'IsolationPoint' })
const message = useMessage() //
const { t } = useI18n() //
const elLockStore = useElLockStore()
const loading = ref(true) //
const list = ref<IsolationPoint[]>([]) //
const total = ref(0) //
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
guideId: undefined,
isolationPointId: undefined,
createTime: []
})
const queryFormRef = ref() //
const exportLoading = ref(false) //
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
await elLockStore.init()
const data = await IsolationPointApi.getIsolationPointPage(queryParams)
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value.resetFields()
handleQuery()
}
/** 添加/修改操作 */
const formRef = ref()
const openForm = (type: string, id?: number) => {
formRef.value.open(type, id)
}
/** 删除按钮操作 */
const handleDelete = async (id: number) => {
try {
//
await message.delConfirm()
//
await IsolationPointApi.deleteIsolationPoint(id)
message.success(t('common.delSuccess'))
//
await getList()
} catch {}
}
/** 批量删除指导书与隔离点关联 */
const handleDeleteBatch = async () => {
try {
//
await message.delConfirm()
await IsolationPointApi.deleteIsolationPointList(checkedIds.value)
message.success(t('common.delSuccess'))
await getList()
} catch {}
}
const checkedIds = ref<number[]>([])
const handleRowCheckboxChange = (records: IsolationPoint[]) => {
checkedIds.value = records.map((item) => item.id)
}
/** 导出按钮操作 */
const handleExport = async () => {
try {
//
await message.exportConfirm()
//
exportLoading.value = true
const data = await IsolationPointApi.exportIsolationPoint(queryParams)
download.excel(data, '指导书与隔离点关联.xls')
} catch {
} finally {
exportLoading.value = false
}
}
/** 初始化 **/
onMounted(() => {
getList()
})
</script>

232
web/src/views/lock/guide/isolationpoint/lockGuide.vue

@ -0,0 +1,232 @@
<template>
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="68px"
>
<el-form-item label="隔离指导书" prop="guideId">
<el-input
v-model="queryParams.guideId"
placeholder="请输入隔离指导书ID"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="隔离点ID" prop="isolationPointId">
<el-input
v-model="queryParams.isolationPointId"
placeholder="请输入隔离点ID"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="创建时间" prop="createTime">
<el-date-picker
v-model="queryParams.createTime"
value-format="YYYY-MM-DD HH:mm:ss"
type="daterange"
start-placeholder="开始日期"
end-placeholder="结束日期"
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
class="!w-220px"
/>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
<el-button
type="primary"
plain
@click="openForm('create')"
v-hasPermi="['guide:isolation-point:create']"
>
<Icon icon="ep:plus" class="mr-5px" /> 新增
</el-button>
<el-button
type="success"
plain
@click="handleExport"
:loading="exportLoading"
v-hasPermi="['guide:isolation-point:export']"
>
<Icon icon="ep:download" class="mr-5px" /> 导出
</el-button>
<el-button
type="danger"
plain
:disabled="isEmpty(checkedIds)"
@click="handleDeleteBatch"
v-hasPermi="['guide:isolation-point:delete']"
>
<Icon icon="ep:delete" class="mr-5px" /> 批量删除
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table
row-key="id"
v-loading="loading"
:data="list"
:stripe="true"
:show-overflow-tooltip="true"
@selection-change="handleRowCheckboxChange"
>
<el-table-column type="selection" width="55" />
<el-table-column label="id" align="center" prop="id" />
<el-table-column label="隔离指导书ID" align="center" prop="guideId" />
<el-table-column label="隔离点ID" align="center" prop="isolationPointId" />
<el-table-column
label="创建时间"
align="center"
prop="createTime"
:formatter="dateFormatter"
width="180px"
/>
<el-table-column label="操作" align="center" min-width="120px">
<template #default="scope">
<el-button
link
type="primary"
@click="openForm('update', scope.row.id)"
v-hasPermi="['guide:isolation-point:update']"
>
编辑
</el-button>
<el-button
link
type="danger"
@click="handleDelete(scope.row.id)"
v-hasPermi="['guide:isolation-point:delete']"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap>
<!-- 表单弹窗添加/修改 -->
<IsolationPointForm ref="formRef" @success="getList" />
</template>
<script setup lang="ts">
import { isEmpty } from '@/utils/is'
import { dateFormatter } from '@/utils/formatTime'
import download from '@/utils/download'
import { IsolationPointApi, IsolationPoint } from '@/api/guide/isolationpoint'
import IsolationPointForm from './IsolationPointForm.vue'
/** 指导书与隔离点关联 列表 */
defineOptions({ name: 'IsolationPoint' })
const message = useMessage() //
const { t } = useI18n() //
const loading = ref(true) //
const list = ref<IsolationPoint[]>([]) //
const total = ref(0) //
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
guideId: undefined,
isolationPointId: undefined,
createTime: []
})
const queryFormRef = ref() //
const exportLoading = ref(false) //
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
const data = await IsolationPointApi.getIsolationPointPage(queryParams)
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value.resetFields()
handleQuery()
}
/** 添加/修改操作 */
const formRef = ref()
const openForm = (type: string, id?: number) => {
formRef.value.open(type, id)
}
/** 删除按钮操作 */
const handleDelete = async (id: number) => {
try {
//
await message.delConfirm()
//
await IsolationPointApi.deleteIsolationPoint(id)
message.success(t('common.delSuccess'))
//
await getList()
} catch {}
}
/** 批量删除指导书与隔离点关联 */
const handleDeleteBatch = async () => {
try {
//
await message.delConfirm()
await IsolationPointApi.deleteIsolationPointList(checkedIds.value)
message.success(t('common.delSuccess'))
await getList()
} catch {}
}
const checkedIds = ref<number[]>([])
const handleRowCheckboxChange = (records: IsolationPoint[]) => {
checkedIds.value = records.map((item) => item.id)
}
/** 导出按钮操作 */
const handleExport = async () => {
try {
//
await message.exportConfirm()
//
exportLoading.value = true
const data = await IsolationPointApi.exportIsolationPoint(queryParams)
download.excel(data, '指导书与隔离点关联.xls')
} catch {
} finally {
exportLoading.value = false
}
}
/** 初始化 **/
onMounted(() => {
getList()
})
</script>

157
web/src/views/lock/guide/lockguide/LockGuideForm.vue

@ -0,0 +1,157 @@
<template>
<Dialog :title="dialogTitle" v-model="dialogVisible">
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="150px"
v-loading="formLoading"
>
<el-form-item label="指导书名称" prop="name">
<el-input v-model="formData.name" placeholder="请输入指导书名称" />
</el-form-item>
<el-form-item label="指导书编码" prop="code">
<el-input v-model="formData.code" placeholder="请输入指导书编码" />
</el-form-item>
<el-form-item label="工作内容和范围" prop="guideContent">
<el-input v-model="formData.guideContent" type="textarea" :rows="10" />
</el-form-item>
<el-form-item label="集中挂牌人" prop="operatorId">
<el-select v-model="formData.operatorId" clearable filterable>
<el-option
v-for="item in options.userOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="集中挂牌协助人" prop="operatorHelperId">
<el-select v-model="formData.operatorHelperId" clearable filterable>
<el-option
v-for="item in options.userOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="验证人" prop="verifierId">
<el-select v-model="formData.verifierId" clearable filterable>
<el-option
v-for="item in options.userOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="验证协助人" prop="verifierHelperId">
<el-select v-model="formData.verifierHelperId" clearable filterable>
<el-option
v-for="item in options.userOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="submitForm" type="primary" :disabled="formLoading"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { LockGuideApi, LockGuide } from '@/api/guide/lockguide'
/** 隔离指导书 表单 */
defineOptions({ name: 'LockGuideForm' })
const { t } = useI18n() //
const message = useMessage() //
const { options } = defineProps({
options: {
type: Object,
default: () => ({})
}
})
const dialogVisible = ref(false) //
const dialogTitle = ref('') //
const formLoading = ref(false) // 12
const formType = ref('') // create - update -
const formData = ref({
id: undefined,
name: undefined,
code: undefined,
guideContent: undefined,
operatorId: undefined,
operatorHelperId: undefined,
verifierId: undefined,
verifierHelperId: undefined
})
const formRules = reactive({
guideContent: [{ required: true, message: '工作内容不能为空', trigger: 'blur' }],
name: [{ required: true, message: '指导书名称不能为空', trigger: 'blur' }],
code: [{ required: true, message: '指导书编码不能为空', trigger: 'blur' }]
})
const formRef = ref() // Ref
/** 打开弹窗 */
const open = async (type: string, id?: number) => {
dialogVisible.value = true
dialogTitle.value = t('action.' + type)
formType.value = type
resetForm()
//
if (id) {
formLoading.value = true
try {
formData.value = await LockGuideApi.getLockGuide(id)
} finally {
formLoading.value = false
}
}
}
defineExpose({ open }) // open
/** 提交表单 */
const emit = defineEmits(['success']) // success
const submitForm = async () => {
//
await formRef.value.validate()
//
formLoading.value = true
try {
const data = formData.value as unknown as LockGuide
if (formType.value === 'create') {
await LockGuideApi.createLockGuide(data)
message.success(t('common.createSuccess'))
} else {
await LockGuideApi.updateLockGuide(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
//
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: undefined,
name: undefined,
code: undefined,
guideContent: undefined,
operatorId: undefined,
operatorHelperId: undefined,
verifierId: undefined,
verifierHelperId: undefined
}
formRef.value?.resetFields()
}
</script>

259
web/src/views/lock/guide/lockguide/LockGuideFormNew.vue

@ -0,0 +1,259 @@
<template>
<Dialog :title="dialogTitle" v-model="dialogVisible">
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="150px" v-loading="formLoading">
<el-form-item label="指导书名称" prop="name">
<el-input v-model="formData.name" placeholder="请输入指导书名称" :disabled="formLoading" maxlength="100"
show-word-limit />
</el-form-item>
<el-form-item label="指导书编码" prop="code">
<el-input v-model="formData.code" placeholder="请输入指导书编码" :disabled="formLoading" maxlength="50"
show-word-limit />
</el-form-item>
<el-form-item label="工作内容和范围" prop="guideContent">
<el-input v-model="formData.guideContent" type="textarea" :rows="10" placeholder="请输入工作内容和范围"
:disabled="formLoading" maxlength="2000" show-word-limit />
</el-form-item>
<el-form-item label="集中挂牌人" prop="operatorId">
<el-select v-model="formData.operatorId" clearable filterable placeholder="请选择集中挂牌人" :disabled="formLoading">
<el-option v-for="item in options.userOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item label="集中挂牌协助人" prop="operatorHelperId">
<el-select v-model="formData.operatorHelperId" clearable filterable placeholder="请选择集中挂牌协助人"
:disabled="formLoading">
<el-option v-for="item in options.userOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item label="验证人" prop="verifierId">
<el-select v-model="formData.verifierId" clearable filterable placeholder="请选择验证人" :disabled="formLoading">
<el-option v-for="item in options.userOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item label="验证协助人" prop="verifierHelperId">
<el-select v-model="formData.verifierHelperId" clearable filterable placeholder="请选择验证协助人"
:disabled="formLoading">
<el-option v-for="item in options.userOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item label="关联隔离点">
<el-select v-model="formData.isolationPointIds" clearable filterable multiple placeholder="请选择关联的隔离点"
:disabled="formLoading">
<el-option v-for="item in elLockStore.isolationPoints" :key="item.id"
:label="item.ipName + '-' + item.ipLocation +'('+item.ipNumber+')'" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item label="需要所锁数量">
{{ lockNums }}
</el-form-item>
</el-form>
<template #footer>
<el-button @click="submitForm" type="primary" :loading="formLoading" :disabled="formLoading">
{{ formLoading ? (isCreating ? '创建中...' : '更新中...') : '确 定' }}
</el-button>
<el-button @click="dialogVisible = false" :disabled="formLoading"> </el-button>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { LockGuideApi, LockGuide } from '@/api/guide/lockguide'
import { useElLockStore } from '@/store/modules/elLock'
import { IsolationPointApi } from '@/api/guide/isolationpoint'
const elLockStore = useElLockStore()
/** 隔离指导书 表单 */
defineOptions({ name: 'LockGuideForm' })
const { t } = useI18n() //
const message = useMessage() //
const { options } = defineProps({
options: {
type: Object,
default: () => ({})
}
})
const dialogVisible = ref(false) //
const dialogTitle = ref('') //
const formLoading = ref(false) // 12
const formType = ref('') // create - update -
const formData = ref({
id: undefined,
name: undefined,
code: undefined,
guideContent: undefined,
operatorId: undefined,
operatorHelperId: undefined,
verifierId: undefined,
verifierHelperId: undefined,
isolationPointIds: [] as number[]
})
const formRules = reactive({
guideContent: [{ required: true, message: '工作内容不能为空', trigger: 'blur' }],
name: [{ required: true, message: '指导书名称不能为空', trigger: 'blur' }],
code: [{ required: true, message: '指导书编码不能为空', trigger: 'blur' }]
})
const formRef = ref() // Ref
const oldIsolationPointIds = ref<number[]>([])
//
const hasIsolationPointChanges = computed(() => {
const currentIds = formData.value.isolationPointIds || []
const oldIds = oldIsolationPointIds.value
return (
currentIds.length !== oldIds.length ||
!currentIds.every((id) => oldIds.includes(id)) ||
!oldIds.every((id) => currentIds.includes(id))
)
})
const lockNums = computed(() => {
return formData.value.isolationPointIds?.reduce((acc, curr) => {
const point = elLockStore.isolationPoints.find((item) => item.id === curr)
return acc + (point?.guideLockNums || 0)
}, 0)
})
//
const isCreating = computed(() => formType.value === 'create')
//
const isUpdating = computed(() => formType.value === 'update')
/** 获取指导书关联的隔离点ID列表 */
const getGuideIsolationPointIds = (guideId: number): number[] => {
return elLockStore.isolationPointGuides
.filter((item) => item.guideId === guideId)
.map((item) => item.isolationPointId) as number[]
}
/** 打开弹窗 */
const open = async (type: string, id?: number) => {
try {
dialogVisible.value = true
dialogTitle.value = t('action.' + type)
formType.value = type
resetForm()
//
if (id && type === 'update') {
formLoading.value = true
//
const [guideData] = await Promise.all([LockGuideApi.getLockGuide(id)])
formData.value = guideData
const pointIds = getGuideIsolationPointIds(id)
formData.value.isolationPointIds = pointIds
oldIsolationPointIds.value = [...pointIds] //
}
} catch (error) {
console.error('打开表单失败:', error)
message.error('获取数据失败,请重试')
dialogVisible.value = false
} finally {
formLoading.value = false
}
}
defineExpose({ open }) // open
/** 处理隔离点关联 */
const handleIsolationPoints = async (
guideId: number,
addPoints: number[],
deletePoints: number[]
) => {
const operations: Promise<any>[] = []
//
if (deletePoints.length > 0) {
const deleteOperations = deletePoints.map((id: number) => {
const point = elLockStore.isolationPointGuides.find(
(item) => item.isolationPointId === id && item.guideId === guideId
)
return point ? IsolationPointApi.deleteIsolationPoint(point.id!) : Promise.resolve()
})
operations.push(...deleteOperations)
}
//
if (addPoints.length > 0) {
const addOperations = addPoints.map((id: number) =>
IsolationPointApi.createIsolationPoint({
guideId: guideId,
isolationPointId: id
})
)
operations.push(...addOperations)
}
return Promise.all(operations)
}
/** 提交表单 */
const emit = defineEmits(['success']) // success
const submitForm = async () => {
try {
//
await formRef.value.validate()
formLoading.value = true
//
const currentPointIds = formData.value.isolationPointIds || []
const addPoints = currentPointIds.filter((id) => !oldIsolationPointIds.value.includes(id))
const deletePoints = oldIsolationPointIds.value.filter((id) => !currentPointIds.includes(id))
const data = formData.value as unknown as LockGuide
let guideId: number
if (isCreating.value) {
//
guideId = await LockGuideApi.createLockGuide(data)
//
if (currentPointIds.length > 0) {
await handleIsolationPoints(guideId, currentPointIds, [])
}
message.success(t('common.createSuccess'))
} else if (isUpdating.value) {
//
guideId = data.id!
//
await LockGuideApi.updateLockGuide(data)
//
if (hasIsolationPointChanges.value) {
await handleIsolationPoints(guideId, addPoints, deletePoints)
}
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
//
emit('success')
} catch (error) {
console.error('提交表单失败:', error)
message.error(formType.value === 'create' ? '创建失败,请重试' : '更新失败,请重试')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: undefined,
name: undefined,
code: undefined,
guideContent: undefined,
operatorId: undefined,
operatorHelperId: undefined,
verifierId: undefined,
verifierHelperId: undefined,
isolationPointIds: []
}
formRef.value?.resetFields()
oldIsolationPointIds.value = []
}
</script>

244
web/src/views/lock/guide/lockguide/index.vue

@ -0,0 +1,244 @@
<template>
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="120px"
>
<el-form-item label="工作内容" prop="guideContent">
<el-input
v-model="queryParams.guideContent"
placeholder="请输入工作内容"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="创建时间" prop="createTime">
<el-date-picker
v-model="queryParams.createTime"
value-format="YYYY-MM-DD HH:mm:ss"
type="daterange"
start-placeholder="开始日期"
end-placeholder="结束日期"
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
class="!w-220px"
/>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
<el-button
type="primary"
plain
@click="openForm('create')"
v-hasPermi="['guide:lock-guide:create']"
>
<Icon icon="ep:plus" class="mr-5px" /> 新增
</el-button>
<el-button
type="success"
plain
@click="handleExport"
:loading="exportLoading"
v-hasPermi="['guide:lock-guide:export']"
>
<Icon icon="ep:download" class="mr-5px" /> 导出
</el-button>
<el-button
type="danger"
plain
:disabled="isEmpty(checkedIds)"
@click="handleDeleteBatch"
v-hasPermi="['guide:lock-guide:delete']"
>
<Icon icon="ep:delete" class="mr-5px" /> 批量删除
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table
row-key="id"
v-loading="loading"
:data="list"
:stripe="true"
:show-overflow-tooltip="true"
@selection-change="handleRowCheckboxChange"
>
<el-table-column type="selection" width="55" />
<el-table-column label="指导书名称" align="center" prop="name" />
<el-table-column label="指导书编码" align="center" prop="code" />
<el-table-column label="工作内容和范围" align="center" min-width="300px" prop="guideContent" />
<!-- <el-table-column label="集中挂牌人" align="center" prop="operatorId">
<template #default="scope">
{{ options.userOptions.find((user) => user.value === scope.row.operatorId)?.label }}
</template>
</el-table-column>
<el-table-column label="集中挂牌协助人" align="center" prop="operatorHelperId">
<template #default="scope">
{{ options.userOptions.find((user) => user.value === scope.row.operatorHelperId)?.label }}
</template>
</el-table-column>
<el-table-column label="验证人" align="center" prop="verifierId">
<template #default="scope">
{{ options.userOptions.find((user) => user.value === scope.row.verifierId)?.label }}
</template>
</el-table-column>
<el-table-column label="验证协助人" align="center" prop="verifierHelperId">
<template #default="scope">
{{ options.userOptions.find((user) => user.value === scope.row.verifierHelperId)?.label }}
</template>
</el-table-column> -->
<el-table-column label="操作" align="center" min-width="120px">
<template #default="scope">
<el-button
link
type="primary"
@click="openForm('update', scope.row.id)"
v-hasPermi="['guide:lock-guide:update']"
>
编辑
</el-button>
<el-button
link
type="danger"
@click="handleDelete(scope.row.id)"
v-hasPermi="['guide:lock-guide:delete']"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap>
<!-- 表单弹窗添加/修改 -->
<LockGuideForm ref="formRef" @success="getList" :options="options" />
</template>
<script setup lang="ts">
import { isEmpty } from '@/utils/is'
import { dateFormatter } from '@/utils/formatTime'
import download from '@/utils/download'
import { LockGuideApi, LockGuide } from '@/api/guide/lockguide'
import LockGuideForm from './LockGuideFormNew.vue'
import { useElLockStore } from '@/store/modules/elLock'
const elLockStore = useElLockStore()
/** 隔离指导书 列表 */
defineOptions({ name: 'LockGuide' })
const message = useMessage() //
const { t } = useI18n() //
const options = ref({
userOptions: elLockStore.users.map((item) => ({
label: item.nickname,
value: item.id
}))
})
const loading = ref(true) //
const list = ref<LockGuide[]>([]) //
const total = ref(0) //
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
guideContent: undefined,
guideLockNums: undefined,
createTime: []
})
const queryFormRef = ref() //
const exportLoading = ref(false) //
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
await elLockStore.init()
const data = await LockGuideApi.getLockGuidePage(queryParams)
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value.resetFields()
handleQuery()
}
/** 添加/修改操作 */
const formRef = ref()
const openForm = (type: string, id?: number) => {
formRef.value.open(type, id)
}
/** 删除按钮操作 */
const handleDelete = async (id: number) => {
try {
//
await message.delConfirm()
//
await LockGuideApi.deleteLockGuide(id)
message.success(t('common.delSuccess'))
//
await getList()
} catch {}
}
/** 批量删除隔离指导书 */
const handleDeleteBatch = async () => {
try {
//
await message.delConfirm()
await LockGuideApi.deleteLockGuideList(checkedIds.value)
message.success(t('common.delSuccess'))
await getList()
} catch {}
}
const checkedIds = ref<number[]>([])
const handleRowCheckboxChange = (records: LockGuide[]) => {
checkedIds.value = records.map((item) => item.id)
}
/** 导出按钮操作 */
const handleExport = async () => {
try {
//
await message.exportConfirm()
//
exportLoading.value = true
const data = await LockGuideApi.exportLockGuide(queryParams)
download.excel(data, '隔离指导书.xls')
} catch {
} finally {
exportLoading.value = false
}
}
/** 初始化 **/
onMounted(() => {
getList()
})
</script>

903
web/src/views/lock/isolation/plan/PlanForm.vue

@ -0,0 +1,903 @@
<template>
<Dialog :title="dialogTitle" v-model="dialogVisible" :width="appStore.mobile ? '95%' : '80%'">
<!-- 桌面端表单 -->
<el-form v-if="!appStore.mobile" ref="formRef" :model="formData" :rules="formRules" label-width="100px"
v-loading="formLoading">
<el-form-item label="任务名称" prop="ipName" v-if="formType === 'create'">
<el-input v-model="formData.ipName" placeholder="请输入任务名称">
<template #prefix v-if="formType === 'create'">
<span class="text-12px">{{ currentDay }}</span>
</template>
</el-input>
</el-form-item>
<div v-else class="flex items-center justify-center">
<span class="text-16px font-bold">{{ formData.ipName }}</span>
</div>
<div class="plan-item-container">
<div class="flex gap-2 justify-items-end">
<el-select v-if="formType === 'create'" placeholder="请选择检修任务指导书" clearable @change="addPlanItem" filterable
v-model="selectedGuideId">
<el-option v-for="item in availableGuides" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</div>
<el-form-item prop="planItems" class="mt-5 w-full" label-width="0">
<el-table :data="formData.planItems" class="w-full" border>
<el-table-column type="expand" v-if="formType !== 'create'">
<template #default="props">
<div>
<el-table :data="props.row.planItemDetails">
<el-table-column label="电子锁" align="center" prop="lock.lockName">
<template #default="scope">
<template v-if="scope.row.lock">
{{ scope.row.lock?.lockName }}
<el-icon v-if="scope.row.lock?.lockStatus == 1">
<Lock />
</el-icon>
<el-icon v-else>
<Unlock />
</el-icon>
</template>
<template v-else> 未绑定电子锁 </template>
</template>
</el-table-column>
<el-table-column label="隔离点" align="center" prop="isolationPoint.ipName">
<template #default="scope">
{{ scope.row.isolationPoint.ipName }}
<DictTag :type="DICT_TYPE.LOCK_ISOLATION_POINT_STATUS"
:value="scope.row.isolationPoint.status" />
</template>
</el-table-column>
<el-table-column label="任务状态" align="center" prop="lockStatus">
<template #default="scope">
<DictTag :type="DICT_TYPE.LOCK_PLAN_ITEM_DETAIL_STATUS" :value="scope.row.lockStatus" />
</template>
</el-table-column>
<el-table-column label="受影响人" align="center" prop="lifeLock">
<template #default="scope">
<div v-if="scope.row.lifeLock">
<div v-for="item in scope.row.lifeLock" :key="item.id">
{{
elLockStore.users.find((user) => user.id === item.userId)?.nickname
}}
<DictTag :type="DICT_TYPE.LOCK_LIFE_LOCK_STATUS" :value="item.lockStatus" />
</div>
</div>
</template>
</el-table-column>
</el-table>
</div>
</template>
</el-table-column>
<el-table-column label="检修任务指导书" align="center" prop="guideId" width="220px">
<template #default="scope">
{{ getGuideName(scope.row.guideId) }}
</template>
</el-table-column>
<el-table-column label="任务状态" align="center" prop="status" v-if="formType != 'create'">
<template #default="scope">
<DictTag :type="DICT_TYPE.LOCK_PLAN_ITEM_STATUS" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column label="集中挂牌人" align="center" prop="operatorId" width="150px">
<template #default="scope">
<el-select v-if="formType === 'create'" v-model="scope.row.operatorId" placeholder="请选择集中挂牌人"
:disabled="formType !== 'create'" clearable filterable>
<el-option v-for="item in elLockStore.users" :key="item.id" :label="item.nickname" :value="item.id" />
</el-select>
<span v-else>{{
elLockStore.users.find((user) => user.id === scope.row.operatorId)?.nickname
}}</span>
</template>
</el-table-column>
<el-table-column label="集中挂牌协助人" align="center" prop="operatorHelperId" width="150px">
<template #default="scope">
<el-select v-if="formType === 'create'" v-model="scope.row.operatorHelperId" placeholder="请选择集中挂牌协助人"
:disabled="formType !== 'create'" clearable filterable>
<el-option v-for="item in elLockStore.users" :key="item.id" :label="item.nickname" :value="item.id" />
</el-select>
<span v-else>{{
elLockStore.users.find((user) => user.id === scope.row.operatorHelperId)?.nickname
}}</span>
</template>
</el-table-column>
<el-table-column label="验证人" align="center" prop="verifierId" width="150px">
<template #default="scope">
<el-select v-if="formType === 'create'" v-model="scope.row.verifierId" placeholder="请选择验证人"
:disabled="formType !== 'create'" clearable filterable>
<el-option v-for="item in elLockStore.users" :key="item.id" :label="item.nickname" :value="item.id" />
</el-select>
<span v-else>{{
elLockStore.users.find((user) => user.id === scope.row.verifierId)?.nickname
}}</span>
</template>
</el-table-column>
<el-table-column label="验证协助人" align="center" prop="verifierHelperId" width="150px">
<template #default="scope">
<el-select v-if="formType === 'create'" v-model="scope.row.verifierHelperId" placeholder="请选择验证协助人"
:disabled="formType !== 'create'" clearable filterable>
<el-option v-for="item in elLockStore.users" :key="item.id" :label="item.nickname" :value="item.id" />
</el-select>
<span v-else>{{
elLockStore.users.find((user) => user.id === scope.row.verifierHelperId)?.nickname
}}</span>
</template>
</el-table-column>
<el-table-column label="隔离点" align="center">
<template #default="scope">
<div v-for="point in getGuidePoints(scope.row.guideId)" :key="point?.id">
{{ point?.ipName + '-' + point?.ipLocation + '(' + point?.ipNumber + ')' }}
</div>
</template>
</el-table-column>
<el-table-column label="锁数量" align="center" width="50px">
<template #default="scope">
{{ getGuideLockNums(scope.row.guideId) }}
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="100" v-if="formType === 'create'">
<template #default="scope">
<el-button type="danger" @click="deletePlanItem(scope.$index)">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-form-item>
</div>
</el-form>
<!-- 移动端表单 -->
<el-form v-else ref="formRef" :model="formData" :rules="formRules" label-width="0" v-loading="formLoading"
class="mobile-form">
<!-- 任务名称 -->
<div class="mobile-form-section">
<div class="mobile-form-title">任务名称</div>
<el-form-item prop="ipName" v-if="formType === 'create'">
<el-input v-model="formData.ipName" placeholder="请输入任务名称" class="mobile-input">
<template #prefix>
<span class="text-12px">{{ currentDay }}</span>
</template>
</el-input>
</el-form-item>
<div v-else class="mobile-display-value">
<span class="text-16px font-bold">{{ formData.ipName }}</span>
</div>
</div>
<!-- 添加指导书选择 -->
<div class="mobile-form-section" v-if="formType === 'create'">
<div class="mobile-form-title">添加检修任务指导书</div>
<el-select v-model="selectedGuideId" placeholder="请选择检修任务指导书" clearable @change="addPlanItem"
filterable class="mobile-select w-full">
<el-option v-for="item in availableGuides" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</div>
<!-- 计划项列表 -->
<div class="mobile-form-section">
<div class="mobile-form-title">检修任务子项</div>
<el-form-item prop="planItems" class="w-full">
<div v-if="formData.planItems.length === 0" class="mobile-empty-state">
<el-empty description="暂无检修任务子项" />
</div>
<!-- 计划项卡片列表 -->
<div v-else class="mobile-plan-items">
<div v-for="(item, index) in formData.planItems" :key="index" class="mobile-plan-item-card">
<div class="mobile-card-header">
<div class="mobile-card-title">
<el-icon class="mr-2"><Document /></el-icon>
{{ getGuideName(item.guideId) }}
</div>
<el-button v-if="formType === 'create'" type="danger" size="small" @click="deletePlanItem(index)"
class="mobile-delete-btn">
删除
</el-button>
</div>
<div class="mobile-card-content">
<!-- 集中挂牌人 -->
<div class="mobile-form-row">
<div class="mobile-label">集中挂牌人</div>
<div class="mobile-value">
<el-select v-if="formType === 'create'" v-model="item.operatorId" placeholder="请选择集中挂牌人"
clearable filterable class="mobile-select w-full">
<el-option v-for="user in elLockStore.users" :key="user.id" :label="user.nickname" :value="user.id" />
</el-select>
<span v-else class="mobile-display-text">
{{ elLockStore.users.find((user) => user.id === item.operatorId)?.nickname || '未选择' }}
</span>
</div>
</div>
<!-- 集中挂牌协助人 -->
<div class="mobile-form-row">
<div class="mobile-label">集中挂牌协助人</div>
<div class="mobile-value">
<el-select v-if="formType === 'create'" v-model="item.operatorHelperId" placeholder="请选择集中挂牌协助人"
clearable filterable class="mobile-select w-full">
<el-option v-for="user in elLockStore.users" :key="user.id" :label="user.nickname" :value="user.id" />
</el-select>
<span v-else class="mobile-display-text">
{{ elLockStore.users.find((user) => user.id === item.operatorHelperId)?.nickname || '未选择' }}
</span>
</div>
</div>
<!-- 验证人 -->
<div class="mobile-form-row">
<div class="mobile-label">验证人</div>
<div class="mobile-value">
<el-select v-if="formType === 'create'" v-model="item.verifierId" placeholder="请选择验证人"
clearable filterable class="mobile-select w-full">
<el-option v-for="user in elLockStore.users" :key="user.id" :label="user.nickname" :value="user.id" />
</el-select>
<span v-else class="mobile-display-text">
{{ elLockStore.users.find((user) => user.id === item.verifierId)?.nickname || '未选择' }}
</span>
</div>
</div>
<!-- 验证协助人 -->
<div class="mobile-form-row">
<div class="mobile-label">验证协助人</div>
<div class="mobile-value">
<el-select v-if="formType === 'create'" v-model="item.verifierHelperId" placeholder="请选择验证协助人"
clearable filterable class="mobile-select w-full">
<el-option v-for="user in elLockStore.users" :key="user.id" :label="user.nickname" :value="user.id" />
</el-select>
<span v-else class="mobile-display-text">
{{ elLockStore.users.find((user) => user.id === item.verifierHelperId)?.nickname || '未选择' }}
</span>
</div>
</div>
<!-- 任务状态 -->
<div class="mobile-form-row" v-if="formType != 'create'">
<div class="mobile-label">任务状态</div>
<div class="mobile-value">
<DictTag :type="DICT_TYPE.LOCK_PLAN_ITEM_STATUS" :value="item.status || 0" />
</div>
</div>
<!-- 隔离点信息 -->
<div class="mobile-form-row">
<div class="mobile-label">隔离点</div>
<div class="mobile-value">
<div v-for="point in getGuidePoints(item.guideId)" :key="point?.id" class="mobile-point-item">
{{ point?.ipName + '-' + point?.ipLocation + '(' + point?.ipNumber + ')' }}
</div>
</div>
</div>
<!-- 需要锁数量 -->
<div class="mobile-form-row">
<div class="mobile-label">需要锁数量</div>
<div class="mobile-value">
<span class="mobile-lock-count">{{ getGuideLockNums(item.guideId) }}</span>
</div>
</div>
<!-- 查看模式下的详情展开 -->
<div v-if="formType !== 'create' && item.planItemDetails?.length" class="mobile-details-section">
<div class="mobile-details-title">详细任务</div>
<div v-for="detail in item.planItemDetails" :key="detail.id" class="mobile-detail-item">
<div class="mobile-detail-row">
<span class="mobile-detail-label">电子锁:</span>
<span class="mobile-detail-value">
<template v-if="detail.lock">
{{ detail.lock?.lockName }}
<el-icon v-if="detail.lock?.lockStatus == 1" class="ml-1">
<Lock />
</el-icon>
<el-icon v-else class="ml-1">
<Unlock />
</el-icon>
</template>
<template v-else>未绑定电子锁</template>
</span>
</div>
<div class="mobile-detail-row">
<span class="mobile-detail-label">隔离点:</span>
<span class="mobile-detail-value">
{{ detail.isolationPoint.ipName }}
<DictTag :type="DICT_TYPE.LOCK_ISOLATION_POINT_STATUS" :value="detail.isolationPoint.status || 0" />
</span>
</div>
<div class="mobile-detail-row">
<span class="mobile-detail-label">任务状态:</span>
<span class="mobile-detail-value">
<DictTag :type="DICT_TYPE.LOCK_PLAN_ITEM_DETAIL_STATUS" :value="detail.lockStatus || 0" />
</span>
</div>
<div v-if="detail.lifeLock?.length" class="mobile-detail-row">
<span class="mobile-detail-label">受影响人:</span>
<div class="mobile-detail-value">
<div v-for="lifeLock in detail.lifeLock" :key="lifeLock.id" class="mobile-life-lock-item">
{{ elLockStore.users.find((user) => user.id === lifeLock.userId)?.nickname }}
<DictTag :type="DICT_TYPE.LOCK_LIFE_LOCK_STATUS" :value="lifeLock.lockStatus || 0" />
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</el-form-item>
</div>
</el-form>
<div v-if="formType != 'create' && !appStore.mobile" ref="flowRef" class="flex justify-center h-xl"> </div>
<template #footer>
<template v-if="formType === 'create'">
<el-button @click="submitForm" type="primary" :disabled="formLoading" class="mobile-submit-btn"> </el-button>
<el-button @click="dialogVisible = false" class="mobile-cancel-btn"> </el-button>
</template>
<template v-else>
<el-button @click="dialogVisible = false" type="primary" class="mobile-close-btn"> </el-button>
</template>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { PlanApi, Plan } from '@/api/isolation/plan'
import { useElLockStore } from '@/store/modules/elLock'
import { PlanItemApi } from '@/api/isolation/planitem'
import { PlanItemDetailApi } from '@/api/isolation/planitemdetail'
import { DICT_TYPE } from '@/utils/dict'
import { Document, Lock, Unlock } from '@element-plus/icons-vue'
import dayjs from 'dayjs'
import LogicFlow from '@logicflow/core'
import '@logicflow/core/lib/style/index.css'
import LockPointNodeAdapter from '@/components/LogicFlow/LockPointNodeAdapter'
import { useAppStore } from '@/store/modules/app'
//
interface PlanItem {
guideId: number
operatorId?: number
operatorHelperId?: number
verifierId?: number
verifierHelperId?: number
status?: number
planItemDetails?: any[]
}
interface FormData {
id?: number
ipName?: string
status?: number
planItems: PlanItem[]
}
const currentDay = dayjs().format('YYYYMMDD')
type FormType = 'create' | 'update'
const elLockStore = useElLockStore()
/** 检修任务 表单 */
defineOptions({ name: 'PlanForm' })
const { t } = useI18n() //
const message = useMessage() //
const appStore = useAppStore()
//
const dialogVisible = ref(false)
const dialogTitle = ref('')
const formLoading = ref(false)
const formType = ref<FormType>('create')
const formData = ref<FormData>({
id: undefined,
ipName: undefined,
status: 0,
planItems: []
})
const formRef = ref()
const selectedGuideId = ref<number | undefined>(undefined)
const flowRef = ref()
let lf: LogicFlow | null = null
let datas = ref<any[]>([])
// -
const availableGuides = computed(() =>
elLockStore.lockGuides.filter(
(item) => !formData.value.planItems?.some((planItem) => planItem.guideId === item.id)
)
)
const getGuideName = (guideId: number) =>
elLockStore.lockGuides.find((item) => item.id === guideId)?.name
const getGuidePoints = (guideId: number) =>
elLockStore.isolationPointGuides
.filter((item) => item.guideId === guideId)
.map((i) => elLockStore.isolationPoints.find((p) => p.id === i.isolationPointId))
const getGuideLockNums = (guideId: number) =>
getGuidePoints(guideId).reduce((acc, curr) => acc + (curr?.guideLockNums || 0), 0)
//
const validatePlanItems = (_rule: any, value: PlanItem[], callback: any) => {
if (value.length === 0) {
return callback(new Error('检修任务子项不能为空'))
}
for (const item of value) {
if (!item.operatorId) {
return callback(new Error('集中挂牌人不能为空'))
}
if (!item.verifierId) {
return callback(new Error('验证人不能为空'))
}
}
callback()
}
const initChart = async () => {
if (appStore.mobile) return
if (!flowRef.value) return
//
if (lf) {
lf.destroy()
lf = null
}
await nextTick()
lf = new LogicFlow({
container: flowRef.value,
width: flowRef.value.clientWidth,
height: flowRef.value.clientHeight
})
// Vue
lf.register(LockPointNodeAdapter)
//
renderNodeData()
//
lf.fitView()
}
const renderNodeData = () => {
getPlanData(formData.value.id!)
if (!lf || !datas.value.length) return
const nodes = datas.value.map((item) => ({
...item,
type: 'lock-point-node'
}))
lf.render({ nodes, edges: [] })
requestAnimationFrame(() => {
lf?.fitView()
setTimeout(() => lf?.fitView(), 50)
setTimeout(() => lf?.fitView(), 200)
})
}
const formRules = reactive({
ipName: [{ required: true, message: '计划名称不能为空', trigger: 'blur' }],
planItems: [
{ required: true, message: '检修任务子项不能为空', trigger: 'blur' },
{ validator: validatePlanItems, trigger: 'blur' }
]
})
//
/**
* 创建计划项详情
*/
async function createPlanItemDetails(planItemId: number, guideId: number): Promise<void> {
const isolationPoints = elLockStore.isolationPointGuides.filter(
(item) => item.guideId === guideId
)
const detailPromises = isolationPoints.map(async (point) => {
const lockNums =
elLockStore.isolationPoints.find((i) => i.id === point.isolationPointId)?.guideLockNums || 0
const lockPromises = Array.from({ length: lockNums }, () =>
PlanItemDetailApi.createPlanItemDetail({
isolationPlanItemId: planItemId,
isolationPointId: point.isolationPointId,
lockStatus: 0
})
)
return Promise.all(lockPromises)
})
await Promise.all(detailPromises)
}
/**
* 创建计划项
*/
async function createPlanItems(planId: number): Promise<void> {
const itemPromises = formData.value.planItems.map(async (item) => {
const planItemId = await PlanItemApi.createPlanItem({
isolationPlanId: planId,
guideId: item.guideId,
operatorId: item.operatorId,
operatorHelperId: item.operatorHelperId,
verifierId: item.verifierId,
verifierHelperId: item.verifierHelperId,
status: 0
})
await createPlanItemDetails(planItemId, item.guideId)
})
await Promise.all(itemPromises)
}
/** 打开弹窗 */
const open = async (type: FormType, id?: number) => {
dialogVisible.value = true
if (id) {
dialogTitle.value = '查看检修任务'
} else {
dialogTitle.value = '添加检修任务'
}
formType.value = type
resetForm()
//
if (id && type === 'update') {
formLoading.value = true
try {
formData.value = await PlanApi.getPlan(id)
nextTick(() => {
initChart()
})
let planItems = elLockStore.planItems.filter((item) => item.isolationPlanId === id)
formData.value.planItems = planItems.map((item) => {
return {
guideId: item.guideId!,
operatorId: item.operatorId,
operatorHelperId: item.operatorHelperId,
verifierId: item.verifierId,
verifierHelperId: item.verifierHelperId,
status: item.status,
planItemDetails: elLockStore.planItemDetails
.filter((detail) => detail.isolationPlanItemId === item.id)
.map((detail) => {
return {
...detail,
lock: elLockStore.locks.find((lock) => lock.id === detail.lockId),
isolationPoint: elLockStore.isolationPoints.find(
(point) => point.id === detail.isolationPointId
),
lifeLock: elLockStore.planLifeLocks.filter(
(lock) => lock.isolationPlanItemDetailId === detail.id && lock.lockType == 5
)
}
})
}
})
} catch (error) {
message.error('获取计划数据失败')
console.error('获取计划数据失败:', error)
} finally {
formLoading.value = false
}
}
}
const getPlanData = (id: number) => {
let details = elLockStore.planItemDetails.filter((item) => item.planId == id)
let points = Array.from(new Set(details.map((i) => i.isolationPointId)))
let data = points.map((i, index) => ({
properties: {
planId: id,
pointId: i,
expand: points.length > 2 ? false : true
},
type: 'lock-point-node',
x: points.length > 2 ? 300 * index : 800 * index,
y: 0
}))
datas.value = data
return data
}
defineExpose({ open })
/** 提交表单 */
const emit = defineEmits(['success'])
const submitForm = async () => {
try {
//
await formRef.value.validate()
formLoading.value = true
const data = formData.value as unknown as Plan
let planId: number
if (formType.value === 'create') {
planId = await PlanApi.createPlan({ ...data, ipName: currentDay + '-' + data.ipName })
message.success(t('common.createSuccess'))
//
if (formData.value.planItems.length > 0) {
await createPlanItems(planId)
}
} else {
await PlanApi.updatePlan(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
emit('success')
} catch (error) {
message.error('保存失败,请重试')
console.error('提交表单失败:', error)
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: undefined,
ipName: undefined,
status: 0,
planItems: []
}
selectedGuideId.value = undefined
formRef.value?.resetFields()
}
/**
* 添加计划项
*/
const addPlanItem = () => {
if (!selectedGuideId.value) {
message.error('请选择检修任务指导书')
return
}
const guide = elLockStore.lockGuides.find((item) => item.id === selectedGuideId.value)
if (!guide) {
message.error('指导书信息不存在')
return
}
const newPlanItem: PlanItem = {
guideId: selectedGuideId.value,
operatorId: guide.operatorId,
operatorHelperId: guide.operatorHelperId,
verifierId: guide.verifierId,
verifierHelperId: guide.verifierHelperId,
status: 0,
planItemDetails: []
}
if (!formData.value.planItems) {
formData.value.planItems = []
}
formData.value.planItems.push(newPlanItem)
console.log(formData.value.planItems)
selectedGuideId.value = undefined
}
/**
* 删除计划项
*/
const deletePlanItem = (index: number) => {
if (index >= 0 && index < formData.value.planItems.length) {
formData.value.planItems.splice(index, 1)
}
}
</script>
<style lang="scss" scoped>
.mobile-form {
.mobile-form-section {
margin-bottom: 20px;
padding: 16px;
background: #f8f9fa;
border-radius: 8px;
.mobile-form-title {
font-size: 16px;
font-weight: 600;
color: #303133;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 1px solid #e4e7ed;
}
}
.mobile-input,
.mobile-select {
width: 100%;
:deep(.el-input__wrapper) {
border-radius: 6px;
}
}
.mobile-display-value {
padding: 12px;
background: #fff;
border-radius: 6px;
border: 1px solid #e4e7ed;
}
.mobile-empty-state {
padding: 40px 20px;
text-align: center;
}
.mobile-plan-items {
display: flex;
flex-direction: column;
gap: 16px;
}
.mobile-plan-item-card {
background: #fff;
border-radius: 8px;
border: 1px solid #e4e7ed;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
.mobile-card-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px;
background: #f5f7fa;
border-bottom: 1px solid #e4e7ed;
.mobile-card-title {
display: flex;
align-items: center;
font-size: 16px;
font-weight: 600;
color: #303133;
}
.mobile-delete-btn {
padding: 6px 12px;
font-size: 12px;
}
}
.mobile-card-content {
padding: 16px;
}
}
.mobile-form-row {
display: flex;
flex-direction: column;
margin-bottom: 16px;
gap: 8px;
.mobile-label {
font-size: 14px;
font-weight: 500;
color: #606266;
min-width: 80px;
}
.mobile-value {
flex: 1;
}
.mobile-display-text {
padding: 8px 12px;
background: #f5f7fa;
border-radius: 4px;
color: #303133;
display: inline-block;
min-height: 32px;
line-height: 16px;
}
.mobile-point-item {
padding: 4px 8px;
background: #e1f3d8;
border-radius: 4px;
margin-bottom: 4px;
font-size: 12px;
color: #67c23a;
}
.mobile-lock-count {
display: inline-block;
padding: 4px 12px;
background: #409eff;
color: #fff;
border-radius: 12px;
font-weight: 600;
font-size: 14px;
}
}
.mobile-details-section {
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid #e4e7ed;
.mobile-details-title {
font-size: 14px;
font-weight: 600;
color: #303133;
margin-bottom: 12px;
}
.mobile-detail-item {
background: #f8f9fa;
border-radius: 6px;
padding: 12px;
margin-bottom: 12px;
.mobile-detail-row {
display: flex;
margin-bottom: 8px;
align-items: flex-start;
&:last-child {
margin-bottom: 0;
}
.mobile-detail-label {
font-size: 12px;
color: #909399;
min-width: 60px;
margin-right: 8px;
}
.mobile-detail-value {
flex: 1;
font-size: 12px;
color: #303133;
word-break: break-all;
}
}
.mobile-life-lock-item {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 4px;
padding: 4px 8px;
background: #fff;
border-radius: 4px;
&:last-child {
margin-bottom: 0;
}
}
}
}
}
//
.mobile-submit-btn,
.mobile-cancel-btn,
.mobile-close-btn {
min-width: 80px;
height: 40px;
font-size: 14px;
}
//
@media (max-width: 768px) {
.mobile-form {
.mobile-form-section {
padding: 12px;
margin-bottom: 16px;
}
.mobile-plan-item-card {
.mobile-card-header {
padding: 12px;
}
.mobile-card-content {
padding: 12px;
}
}
.mobile-form-row {
margin-bottom: 12px;
}
}
}
</style>

92
web/src/views/lock/isolation/plan/PlanFormOld.vue

@ -0,0 +1,92 @@
<template>
<Dialog :title="dialogTitle" v-model="dialogVisible">
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="100px"
v-loading="formLoading"
>
<el-form-item label="任务名称" prop="ipName">
<el-input v-model="formData.ipName" placeholder="请输入任务名称" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="submitForm" type="primary" :disabled="formLoading"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { PlanApi, Plan } from '@/api/isolation/plan'
/** 检修任务 表单 */
defineOptions({ name: 'PlanForm' })
const { t } = useI18n() //
const message = useMessage() //
const dialogVisible = ref(false) //
const dialogTitle = ref('') //
const formLoading = ref(false) // 12
const formType = ref('') // create - update -
const formData = ref({
id: undefined,
ipName: undefined
})
const formRules = reactive({
ipName: [{ required: true, message: '计划名称不能为空', trigger: 'blur' }]
})
const formRef = ref() // Ref
/** 打开弹窗 */
const open = async (type: string, id?: number) => {
dialogVisible.value = true
dialogTitle.value = t('action.' + type)
formType.value = type
resetForm()
//
if (id) {
formLoading.value = true
try {
formData.value = await PlanApi.getPlan(id)
} finally {
formLoading.value = false
}
}
}
defineExpose({ open }) // open
/** 提交表单 */
const emit = defineEmits(['success']) // success
const submitForm = async () => {
//
await formRef.value.validate()
//
formLoading.value = true
try {
const data = formData.value as unknown as Plan
if (formType.value === 'create') {
await PlanApi.createPlan(data)
message.success(t('common.createSuccess'))
} else {
await PlanApi.updatePlan(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
//
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: undefined,
ipName: undefined
}
formRef.value?.resetFields()
}
</script>

228
web/src/views/lock/isolation/plan/index.vue

@ -0,0 +1,228 @@
<template>
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="68px"
>
<el-form-item label="任务名称" prop="ipName">
<el-input
v-model="queryParams.ipName"
placeholder="请输入任务名称"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="创建时间" prop="createTime">
<el-date-picker
v-model="queryParams.createTime"
value-format="YYYY-MM-DD HH:mm:ss"
type="daterange"
start-placeholder="开始日期"
end-placeholder="结束日期"
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
class="!w-220px"
/>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
<el-button
type="primary"
plain
@click="openForm('create')"
v-hasPermi="['isolation:plan:create']"
>
<Icon icon="ep:plus" class="mr-5px" /> 添加检修任务
</el-button>
<!--
<el-button
type="success"
plain
@click="handleExport"
:loading="exportLoading"
v-hasPermi="['isolation:plan:export']"
>
<Icon icon="ep:download" class="mr-5px" /> 导出
</el-button>
<el-button
type="danger"
plain
:disabled="isEmpty(checkedIds)"
@click="handleDeleteBatch"
v-hasPermi="['isolation:plan:delete']"
>
<Icon icon="ep:delete" class="mr-5px" /> 批量删除
</el-button> -->
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table
row-key="id"
v-loading="loading"
:data="list"
:stripe="true"
:show-overflow-tooltip="true"
@selection-change="handleRowCheckboxChange"
>
<el-table-column type="selection" width="55" />
<el-table-column label="任务名称" align="center" prop="ipName" />
<el-table-column label="任务状态" align="center" prop="status">
<template #default="scope">
<DictTag :type="DICT_TYPE.LOCK_PLAN_STATUS" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column
label="创建时间"
align="center"
prop="createTime"
:formatter="dateFormatter"
width="180px"
/>
<el-table-column label="操作" align="center" min-width="120px">
<template #default="scope">
<el-button
link
type="primary"
@click="openForm('update', scope.row.id)"
v-hasPermi="['isolation:plan:update']"
>
查看任务
</el-button>
<!-- <el-button
link
type="danger"
@click="handleDelete(scope.row.id)"
v-hasPermi="['isolation:plan:delete']"
>
删除
</el-button> -->
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap>
<!-- 表单弹窗添加/修改 -->
<PlanForm ref="formRef" @success="getList" />
</template>
<script setup lang="ts">
import { dateFormatter } from '@/utils/formatTime'
import download from '@/utils/download'
import { PlanApi, Plan } from '@/api/isolation/plan'
import PlanForm from './PlanForm.vue'
import { useElLockStore } from '@/store/modules/elLock'
import { DICT_TYPE } from '@/utils/dict'
/** 检修任务 列表 */
defineOptions({ name: 'Plan' })
const message = useMessage() //
const { t } = useI18n() //
const elLockStore = useElLockStore()
const loading = ref(true) //
const list = ref<Plan[]>([]) //
const total = ref(0) //
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
ipName: undefined,
createTime: []
})
const queryFormRef = ref() //
const exportLoading = ref(false) //
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
await elLockStore.init()
const data = await PlanApi.getPlanPage(queryParams)
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value.resetFields()
handleQuery()
}
/** 添加/修改操作 */
const formRef = ref()
const openForm = (type: string, id?: number) => {
formRef.value.open(type, id)
}
/** 删除按钮操作 */
const handleDelete = async (id: number) => {
try {
//
await message.delConfirm()
//
await PlanApi.deletePlan(id)
message.success(t('common.delSuccess'))
//
await getList()
} catch {}
}
/** 批量删除检修任务 */
const handleDeleteBatch = async () => {
try {
//
await message.delConfirm()
await PlanApi.deletePlanList(checkedIds.value)
message.success(t('common.delSuccess'))
await getList()
} catch {}
}
const checkedIds = ref<number[]>([])
const handleRowCheckboxChange = (records: Plan[]) => {
checkedIds.value = records.map((item) => item.id)
}
/** 导出按钮操作 */
const handleExport = async () => {
try {
//
await message.exportConfirm()
//
exportLoading.value = true
const data = await PlanApi.exportPlan(queryParams)
download.excel(data, '检修任务.xls')
} catch {
} finally {
exportLoading.value = false
}
}
/** 初始化 **/
onMounted(() => {
getList()
})
</script>

188
web/src/views/lock/isolation/planitem/PlanItemForm.vue

@ -0,0 +1,188 @@
<template>
<Dialog :title="dialogTitle" v-model="dialogVisible">
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="120px"
v-loading="formLoading"
>
<el-form-item label="检修任务" prop="isolationPlanId">
<el-select v-model="formData.isolationPlanId" placeholder="请选择检修任务" filterable>
<el-option
v-for="item in elLockStore.isolationPlans"
:key="item.id"
:label="item.ipName"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="隔离指导书" prop="guideId">
<el-select v-model="formData.guideId" placeholder="请选择隔离指导书" filterable>
<el-option
v-for="item in elLockStore.lockGuides"
:key="item.id"
:label="item.guideContent"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="集中挂牌人" prop="operatorId">
<el-select v-model="formData.operatorId" placeholder="请选择集中挂牌人" filterable>
<el-option
v-for="item in elLockStore.users"
:key="item.id"
:label="item.nickname"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="集中挂牌协助人" prop="operatorHelperId">
<el-select
v-model="formData.operatorHelperId"
placeholder="请选择集中挂牌协助人"
filterable
clearable
>
<el-option
v-for="item in elLockStore.users"
:key="item.id"
:label="item.nickname"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="验证人" prop="verifierId">
<el-select v-model="formData.verifierId" placeholder="请选择验证人" filterable>
<el-option
v-for="item in elLockStore.users"
:key="item.id"
:label="item.nickname"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="验证协助人" prop="verifierHelperId">
<el-select
v-model="formData.verifierHelperId"
placeholder="请选择验证协助人"
filterable
clearable
>
<el-option
v-for="item in elLockStore.users"
:key="item.id"
:label="item.nickname"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="formData.status" placeholder="请选择状态">
<el-option
v-for="item in getIntDictOptions(DICT_TYPE.LOCK_PLAN_ITEM_STATUS)"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="submitForm" type="primary" :disabled="formLoading"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { PlanItemApi, PlanItem } from '@/api/isolation/planitem'
import { useElLockStore } from '@/store/modules/elLock'
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
const elLockStore = useElLockStore()
/** 检修任务子项 表单 */
defineOptions({ name: 'PlanItemForm' })
const { t } = useI18n() //
const message = useMessage() //
const dialogVisible = ref(false) //
const dialogTitle = ref('') //
const formLoading = ref(false) // 12
const formType = ref('') // create - update -
const formData = ref({
id: undefined,
isolationPlanId: undefined,
guideId: undefined,
operatorId: undefined,
operatorHelperId: undefined,
verifierId: undefined,
verifierHelperId: undefined,
status: 0
})
const formRules = reactive({
isolationPlanId: [{ required: true, message: '检修任务不能为空', trigger: 'blur' }],
guideId: [{ required: true, message: '隔离指导书不能为空', trigger: 'blur' }],
operatorId: [{ required: true, message: '集中挂牌人不能为空', trigger: 'blur' }],
verifierId: [{ required: true, message: '验证人不能为空', trigger: 'blur' }]
})
const formRef = ref() // Ref
/** 打开弹窗 */
const open = async (type: string, id?: number) => {
dialogVisible.value = true
dialogTitle.value = t('action.' + type)
formType.value = type
resetForm()
//
if (id) {
formLoading.value = true
try {
formData.value = await PlanItemApi.getPlanItem(id)
} finally {
formLoading.value = false
}
}
}
defineExpose({ open }) // open
/** 提交表单 */
const emit = defineEmits(['success']) // success
const submitForm = async () => {
//
await formRef.value.validate()
//
formLoading.value = true
try {
const data = formData.value as unknown as PlanItem
if (formType.value === 'create') {
await PlanItemApi.createPlanItem(data)
message.success(t('common.createSuccess'))
} else {
await PlanItemApi.updatePlanItem(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
//
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: undefined,
isolationPlanId: undefined,
guideId: undefined,
operatorId: undefined,
operatorHelperId: undefined,
verifierId: undefined,
verifierHelperId: undefined,
status: 0
}
formRef.value?.resetFields()
}
</script>

343
web/src/views/lock/isolation/planitem/index.vue

@ -0,0 +1,343 @@
<template>
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="120px"
>
<el-form-item label="检修任务" prop="isolationPlanId">
<el-select
v-model="queryParams.isolationPlanId"
placeholder="请选择检修任务"
clearable
filterable
class="!w-240px"
>
<el-option
v-for="item in elLockStore.isolationPlans"
:key="item.id"
:label="item.ipName"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="隔离指导书" prop="guideId">
<el-select
v-model="queryParams.guideId"
placeholder="请选择隔离指导书"
clearable
filterable
class="!w-240px"
>
<el-option
v-for="item in elLockStore.lockGuides"
:key="item.id"
:label="item.guideContent"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="集中挂牌人" prop="operatorId">
<el-select
v-model="queryParams.operatorId"
placeholder="请选择集中挂牌人"
clearable
filterable
class="!w-240px"
>
<el-option
v-for="item in elLockStore.users"
:key="item.id"
:label="item.nickname"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="集中挂牌协助人" prop="operatorHelperId">
<el-select
v-model="queryParams.operatorHelperId"
placeholder="请选择集中挂牌协助人"
clearable
filterable
class="!w-240px"
>
<el-option
v-for="item in elLockStore.users"
:key="item.id"
:label="item.nickname"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="验证人" prop="verifierId">
<el-select
v-model="queryParams.verifierId"
placeholder="请选择验证人"
clearable
filterable
class="!w-240px"
>
<el-option
v-for="item in elLockStore.users"
:key="item.id"
:label="item.nickname"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="验证协助人" prop="verifierHelperId">
<el-select
v-model="queryParams.verifierHelperId"
placeholder="请选择验证协助人"
clearable
filterable
class="!w-240px"
>
<el-option
v-for="item in elLockStore.users"
:key="item.id"
:label="item.nickname"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="子项状态" prop="status">
<el-select
v-model="queryParams.status"
placeholder="请选择子项状态"
clearable
filterable
class="!w-240px"
>
<el-option
v-for="item in getDictOptions(DICT_TYPE.LOCK_PLAN_ITEM_STATUS)"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="创建时间" prop="createTime">
<el-date-picker
v-model="queryParams.createTime"
value-format="YYYY-MM-DD HH:mm:ss"
type="daterange"
start-placeholder="开始日期"
end-placeholder="结束日期"
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
class="!w-220px"
/>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
<el-button
type="primary"
plain
@click="openForm('create')"
v-hasPermi="['isolation:plan-item:create']"
>
<Icon icon="ep:plus" class="mr-5px" /> 新增
</el-button>
<el-button
type="success"
plain
@click="handleExport"
:loading="exportLoading"
v-hasPermi="['isolation:plan-item:export']"
>
<Icon icon="ep:download" class="mr-5px" /> 导出
</el-button>
<el-button
type="danger"
plain
:disabled="isEmpty(checkedIds)"
@click="handleDeleteBatch"
v-hasPermi="['isolation:plan-item:delete']"
>
<Icon icon="ep:delete" class="mr-5px" /> 批量删除
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table
row-key="id"
v-loading="loading"
:data="list"
:stripe="true"
:show-overflow-tooltip="true"
@selection-change="handleRowCheckboxChange"
>
<el-table-column type="selection" width="55" />
<el-table-column label="检修任务" align="center" prop="isolationPlanId" />
<el-table-column label="隔离指导书" align="center" prop="guideId">
<template #default="scope">
{{ elLockStore.lockGuides.find((item) => item.id === scope.row.guideId)?.guideContent }}
</template>
</el-table-column>
<el-table-column label="子项状态" align="center" prop="status">
<template #default="scope">
{{ getDictLabel(DICT_TYPE.LOCK_PLAN_ITEM_STATUS, scope.row.status) }}
</template>
</el-table-column>
<el-table-column
label="创建时间"
align="center"
prop="createTime"
:formatter="dateFormatter"
width="180px"
/>
<el-table-column label="操作" align="center" min-width="120px">
<template #default="scope">
<el-button
link
type="primary"
@click="openForm('update', scope.row.id)"
v-hasPermi="['isolation:plan-item:update']"
>
编辑
</el-button>
<el-button
link
type="danger"
@click="handleDelete(scope.row.id)"
v-hasPermi="['isolation:plan-item:delete']"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap>
<!-- 表单弹窗添加/修改 -->
<PlanItemForm ref="formRef" @success="getList" />
</template>
<script setup lang="ts">
import { isEmpty } from '@/utils/is'
import { dateFormatter } from '@/utils/formatTime'
import download from '@/utils/download'
import { PlanItemApi, PlanItem } from '@/api/isolation/planitem'
import PlanItemForm from './PlanItemForm.vue'
import { useElLockStore } from '@/store/modules/elLock'
import { DICT_TYPE, getDictLabel, getDictOptions } from '@/utils/dict'
const elLockStore = useElLockStore()
/** 检修任务子项 列表 */
defineOptions({ name: 'PlanItem' })
const message = useMessage() //
const { t } = useI18n() //
const loading = ref(true) //
const list = ref<PlanItem[]>([]) //
const total = ref(0) //
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
isolationPlanId: undefined,
guideId: undefined,
operatorId: undefined,
operatorHelperId: undefined,
verifierId: undefined,
verifierHelperId: undefined,
status: undefined,
createTime: []
})
const queryFormRef = ref() //
const exportLoading = ref(false) //
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
await elLockStore.init()
const data = await PlanItemApi.getPlanItemPage(queryParams)
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value.resetFields()
handleQuery()
}
/** 添加/修改操作 */
const formRef = ref()
const openForm = (type: string, id?: number) => {
formRef.value.open(type, id)
}
/** 删除按钮操作 */
const handleDelete = async (id: number) => {
try {
//
await message.delConfirm()
//
await PlanItemApi.deletePlanItem(id)
message.success(t('common.delSuccess'))
//
await getList()
} catch {}
}
/** 批量删除检修任务子项 */
const handleDeleteBatch = async () => {
try {
//
await message.delConfirm()
await PlanItemApi.deletePlanItemList(checkedIds.value)
message.success(t('common.delSuccess'))
await getList()
} catch {}
}
const checkedIds = ref<number[]>([])
const handleRowCheckboxChange = (records: PlanItem[]) => {
checkedIds.value = records.map((item) => item.id)
}
/** 导出按钮操作 */
const handleExport = async () => {
try {
//
await message.exportConfirm()
//
exportLoading.value = true
const data = await PlanItemApi.exportPlanItem(queryParams)
download.excel(data, '检修任务子项.xls')
} catch {
} finally {
exportLoading.value = false
}
}
/** 初始化 **/
onMounted(() => {
getList()
})
</script>

144
web/src/views/lock/isolation/planitemdetail/PlanItemDetailForm.vue

@ -0,0 +1,144 @@
<template>
<Dialog :title="dialogTitle" v-model="dialogVisible">
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="140px"
v-loading="formLoading"
>
<el-form-item label="检修任务子项" prop="isolationPlanItemId">
<el-select
v-model="formData.isolationPlanItemId"
placeholder="请选择检修任务子项"
filterable
>
<el-option
v-for="item in elLockStore.planItems"
:key="item.id"
:label="item.id"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="隔离点" prop="isolationPointId">
<el-select v-model="formData.isolationPointId" placeholder="请选择隔离点" filterable>
<el-option
v-for="item in elLockStore.isolationPoints"
:key="item.id"
:label="item.ipName"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="电子锁" prop="lockId">
<el-select v-model="formData.lockId" placeholder="请选择电子锁" filterable>
<el-option
v-for="item in elLockStore.locks"
:key="item.id"
:label="item.lockName"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="状态" prop="lockStatus">
<el-radio-group v-model="formData.lockStatus">
<el-radio
v-for="item in getDictOptions(DICT_TYPE.LOCK_PLAN_ITEM_DETAIL_STATUS)"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-radio-group>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="submitForm" type="primary" :disabled="formLoading"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { PlanItemDetailApi, PlanItemDetail } from '@/api/isolation/planitemdetail'
import { DICT_TYPE, getDictOptions } from '@/utils/dict'
import { useElLockStore } from '@/store/modules/elLock'
const elLockStore = useElLockStore()
/** 检修任务子项详情 表单 */
defineOptions({ name: 'PlanItemDetailForm' })
const { t } = useI18n() //
const message = useMessage() //
const dialogVisible = ref(false) //
const dialogTitle = ref('') //
const formLoading = ref(false) // 12
const formType = ref('') // create - update -
const formData = ref({
id: undefined,
isolationPlanItemId: undefined,
isolationPointId: undefined,
lockId: undefined,
lockStatus: 0
})
const formRules = reactive({
isolationPlanItemId: [{ required: true, message: '检修任务子项不能为空', trigger: 'blur' }],
isolationPointId: [{ required: true, message: '隔离点不能为空', trigger: 'blur' }]
})
const formRef = ref() // Ref
/** 打开弹窗 */
const open = async (type: string, id?: number) => {
dialogVisible.value = true
dialogTitle.value = t('action.' + type)
formType.value = type
resetForm()
//
if (id) {
formLoading.value = true
try {
formData.value = await PlanItemDetailApi.getPlanItemDetail(id)
} finally {
formLoading.value = false
}
}
}
defineExpose({ open }) // open
/** 提交表单 */
const emit = defineEmits(['success']) // success
const submitForm = async () => {
//
await formRef.value.validate()
//
formLoading.value = true
try {
const data = formData.value as unknown as PlanItemDetail
if (formType.value === 'create') {
await PlanItemDetailApi.createPlanItemDetail(data)
message.success(t('common.createSuccess'))
} else {
await PlanItemDetailApi.updatePlanItemDetail(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
//
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: undefined,
isolationPlanItemId: undefined,
isolationPointId: undefined,
lockId: undefined,
lockStatus: 0
}
formRef.value?.resetFields()
}
</script>

294
web/src/views/lock/isolation/planitemdetail/index.vue

@ -0,0 +1,294 @@
<template>
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="68px"
>
<el-form-item label="隔离点" prop="isolationPointId">
<el-select
v-model="queryParams.isolationPointId"
placeholder="请选择隔离点"
clearable
filterable
class="!w-240px"
>
<el-option
v-for="item in elLockStore.isolationPoints"
:key="item.id"
:label="item.ipName"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="电子锁" prop="lockId">
<el-select
v-model="queryParams.lockId"
placeholder="请选择电子锁"
clearable
filterable
class="!w-240px"
>
<el-option
v-for="item in elLockStore.locks"
:key="item.id"
:label="item.lockName"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="锁状态" prop="lockStatus">
<el-select
v-model="queryParams.lockStatus"
placeholder="请选择锁状态"
clearable
class="!w-240px"
>
<el-option
v-for="item in getDictOptions(DICT_TYPE.LOCK_PLAN_ITEM_DETAIL_STATUS)"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="创建时间" prop="createTime">
<el-date-picker
v-model="queryParams.createTime"
value-format="YYYY-MM-DD HH:mm:ss"
type="daterange"
start-placeholder="开始日期"
end-placeholder="结束日期"
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
class="!w-220px"
/>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
<el-button
type="primary"
plain
@click="openForm('create')"
v-hasPermi="['isolation:plan-item-detail:create']"
>
<Icon icon="ep:plus" class="mr-5px" /> 新增
</el-button>
<el-button
type="success"
plain
@click="handleExport"
:loading="exportLoading"
v-hasPermi="['isolation:plan-item-detail:export']"
>
<Icon icon="ep:download" class="mr-5px" /> 导出
</el-button>
<el-button
type="danger"
plain
:disabled="isEmpty(checkedIds)"
@click="handleDeleteBatch"
v-hasPermi="['isolation:plan-item-detail:delete']"
>
<Icon icon="ep:delete" class="mr-5px" /> 批量删除
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table
row-key="id"
v-loading="loading"
:data="list"
:stripe="true"
:show-overflow-tooltip="true"
@selection-change="handleRowCheckboxChange"
>
<el-table-column type="selection" width="55" />
<el-table-column label="任务名称" align="center">
<template #default="scope">
{{ getPlanName(scope.row.isolationPlanItemId) }}
</template>
</el-table-column>
<el-table-column label="隔离点" align="center" prop="isolationPointId">
<template #default="scope">
{{
elLockStore.isolationPoints.find((item) => item.id === scope.row.isolationPointId)
?.ipName
}}
</template>
</el-table-column>
<el-table-column label="电子锁" align="center" prop="lockId">
<template #default="scope">
{{ elLockStore.locks.find((item) => item.id === scope.row.lockId)?.lockName }}
</template>
</el-table-column>
<el-table-column label="任务状态" align="center" prop="lockStatus">
<template #default="scope">
{{ getDictLabel(DICT_TYPE.LOCK_PLAN_ITEM_DETAIL_STATUS, scope.row.lockStatus) }}
</template>
</el-table-column>
<el-table-column
label="创建时间"
align="center"
prop="createTime"
:formatter="dateFormatter"
width="180px"
/>
<el-table-column label="操作" align="center" min-width="120px">
<template #default="scope">
<el-button
link
type="primary"
@click="openForm('update', scope.row.id)"
v-hasPermi="['isolation:plan-item-detail:update']"
>
编辑
</el-button>
<el-button
link
type="danger"
@click="handleDelete(scope.row.id)"
v-hasPermi="['isolation:plan-item-detail:delete']"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap>
<!-- 表单弹窗添加/修改 -->
<PlanItemDetailForm ref="formRef" @success="getList" />
</template>
<script setup lang="ts">
import { isEmpty } from '@/utils/is'
import { dateFormatter } from '@/utils/formatTime'
import download from '@/utils/download'
import { PlanItemDetailApi, PlanItemDetail } from '@/api/isolation/planitemdetail'
import PlanItemDetailForm from './PlanItemDetailForm.vue'
import { useElLockStore } from '@/store/modules/elLock'
import { DICT_TYPE, getDictLabel, getDictOptions } from '@/utils/dict'
const elLockStore = useElLockStore()
/** 检修任务子项详情 列表 */
defineOptions({ name: 'PlanItemDetail' })
const message = useMessage() //
const { t } = useI18n() //
const loading = ref(true) //
const list = ref<PlanItemDetail[]>([]) //
const total = ref(0) //
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
isolationPlanItemId: undefined,
isolationPointId: undefined,
lockId: undefined,
lockStatus: undefined,
createTime: []
})
const queryFormRef = ref() //
const exportLoading = ref(false) //
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
await elLockStore.init()
const data = await PlanItemDetailApi.getPlanItemDetailPage(queryParams)
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
const getPlanName = (id: number) => {
let planItem = elLockStore.planItems.find((item) => item.id === id)
let plan = elLockStore.isolationPlans.find((item) => item.id === planItem?.isolationPlanId)
return plan?.ipName
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value.resetFields()
handleQuery()
}
/** 添加/修改操作 */
const formRef = ref()
const openForm = (type: string, id?: number) => {
formRef.value.open(type, id)
}
/** 删除按钮操作 */
const handleDelete = async (id: number) => {
try {
//
await message.delConfirm()
//
await PlanItemDetailApi.deletePlanItemDetail(id)
message.success(t('common.delSuccess'))
//
await getList()
} catch {}
}
/** 批量删除检修任务子项详情 */
const handleDeleteBatch = async () => {
try {
//
await message.delConfirm()
await PlanItemDetailApi.deletePlanItemDetailList(checkedIds.value)
message.success(t('common.delSuccess'))
await getList()
} catch {}
}
const checkedIds = ref<number[]>([])
const handleRowCheckboxChange = (records: PlanItemDetail[]) => {
checkedIds.value = records.map((item) => item.id)
}
/** 导出按钮操作 */
const handleExport = async () => {
try {
//
await message.exportConfirm()
//
exportLoading.value = true
const data = await PlanItemDetailApi.exportPlanItemDetail(queryParams)
download.excel(data, '检修任务子项详情.xls')
} catch {
} finally {
exportLoading.value = false
}
}
/** 初始化 **/
onMounted(() => {
getList()
})
</script>

162
web/src/views/lock/isolation/planlifelock/PlanLifeLockForm.vue

@ -0,0 +1,162 @@
<template>
<Dialog :title="dialogTitle" v-model="dialogVisible">
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="100px"
v-loading="formLoading"
>
<el-form-item label="子项详情" prop="isolationPlanItemDetailId">
<el-select v-model="formData.isolationPlanItemDetailId" placeholder="请选择子项详情">
<el-option
v-for="item in elLockStore.planItemDetails"
:key="item.id"
:label="item.id"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="上锁人" prop="userId">
<el-select v-model="formData.userId" placeholder="请选择上锁人">
<el-option
v-for="item in elLockStore.users"
:key="item.id"
:label="item.nickname"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="生命锁类型" prop="lockType">
<el-select v-model="formData.lockType" placeholder="请选择生命锁类型">
<el-option
v-for="item in getDictOptions(DICT_TYPE.LOCK_LIFE_LOCK_TYPE)"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="锁定状态" prop="lockStatus">
<el-radio-group v-model="formData.lockStatus">
<el-radio
v-for="item in getDictOptions(DICT_TYPE.LOCK_LIFE_LOCK_STATUS)"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-radio-group>
</el-form-item>
<el-form-item label="上锁时间" prop="lockTime">
<el-date-picker
v-model="formData.lockTime"
type="date"
value-format="x"
placeholder="选择上锁时间"
/>
</el-form-item>
<el-form-item label="解锁时间" prop="unlockTime">
<el-date-picker
v-model="formData.unlockTime"
type="date"
value-format="x"
placeholder="选择解锁时间"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="submitForm" type="primary" :disabled="formLoading"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { PlanLifeLockApi, PlanLifeLock } from '@/api/isolation/planlifelock'
import { useElLockStore } from '@/store/modules/elLock'
import { DICT_TYPE, getDictOptions } from '@/utils/dict'
const elLockStore = useElLockStore()
/** 个人生命锁 表单 */
defineOptions({ name: 'PlanLifeLockForm' })
const { t } = useI18n() //
const message = useMessage() //
const dialogVisible = ref(false) //
const dialogTitle = ref('') //
const formLoading = ref(false) // 12
const formType = ref('') // create - update -
const formData = ref({
id: undefined,
isolationPlanItemDetailId: undefined,
userId: undefined,
lockType: undefined,
lockStatus: undefined,
lockTime: undefined,
unlockTime: undefined
})
const formRules = reactive({
isolationPlanItemDetailId: [{ required: true, message: '子项详情ID不能为空', trigger: 'blur' }],
userId: [{ required: true, message: '上锁人ID不能为空', trigger: 'blur' }],
lockType: [{ required: true, message: '生命锁类型不能为空', trigger: 'change' }],
lockStatus: [{ required: true, message: '锁定状态: 0=未上锁, 1=已上锁不能为空', trigger: 'blur' }]
})
const formRef = ref() // Ref
/** 打开弹窗 */
const open = async (type: string, id?: number) => {
dialogVisible.value = true
dialogTitle.value = t('action.' + type)
formType.value = type
resetForm()
//
if (id) {
formLoading.value = true
try {
formData.value = await PlanLifeLockApi.getPlanLifeLock(id)
} finally {
formLoading.value = false
}
}
}
defineExpose({ open }) // open
/** 提交表单 */
const emit = defineEmits(['success']) // success
const submitForm = async () => {
//
await formRef.value.validate()
//
formLoading.value = true
try {
const data = formData.value as unknown as PlanLifeLock
if (formType.value === 'create') {
await PlanLifeLockApi.createPlanLifeLock(data)
message.success(t('common.createSuccess'))
} else {
await PlanLifeLockApi.updatePlanLifeLock(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
//
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: undefined,
isolationPlanItemDetailId: undefined,
userId: undefined,
lockType: undefined,
lockStatus: undefined,
lockTime: undefined,
unlockTime: undefined
}
formRef.value?.resetFields()
}
</script>

305
web/src/views/lock/isolation/planlifelock/index.vue

@ -0,0 +1,305 @@
<template>
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="90px"
>
<el-form-item label="上锁人" prop="userId">
<el-select
v-model="queryParams.userId"
placeholder="请选择上锁人"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
>
<el-option
v-for="item in elLockStore.users"
:key="item.id"
:label="item.nickname"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="生命锁类型" prop="lockType">
<el-select
v-model="queryParams.lockType"
placeholder="请选择生命锁类型"
clearable
class="!w-240px"
>
<el-option label="请选择字典生成" value="" />
</el-select>
</el-form-item>
<el-form-item label="锁定状态" prop="lockStatus">
<el-select
v-model="queryParams.lockStatus"
placeholder="请选择锁定状态"
clearable
class="!w-240px"
>
<el-option
v-for="item in getDictOptions(DICT_TYPE.LOCK_LIFE_LOCK_STATUS)"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="上锁时间" prop="lockTime">
<el-date-picker
v-model="queryParams.lockTime"
value-format="YYYY-MM-DD HH:mm:ss"
type="daterange"
start-placeholder="开始日期"
end-placeholder="结束日期"
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
class="!w-220px"
/>
</el-form-item>
<el-form-item label="解锁时间" prop="unlockTime">
<el-date-picker
v-model="queryParams.unlockTime"
value-format="YYYY-MM-DD HH:mm:ss"
type="daterange"
start-placeholder="开始日期"
end-placeholder="结束日期"
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
class="!w-220px"
/>
</el-form-item>
<el-form-item label="创建时间" prop="createTime">
<el-date-picker
v-model="queryParams.createTime"
value-format="YYYY-MM-DD HH:mm:ss"
type="daterange"
start-placeholder="开始日期"
end-placeholder="结束日期"
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
class="!w-220px"
/>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
<!-- <el-button
type="primary"
plain
@click="openForm('create')"
v-hasPermi="['isolation:plan-life-lock:create']"
>
<Icon icon="ep:plus" class="mr-5px" /> 新增
</el-button>
<el-button
type="success"
plain
@click="handleExport"
:loading="exportLoading"
v-hasPermi="['isolation:plan-life-lock:export']"
>
<Icon icon="ep:download" class="mr-5px" /> 导出
</el-button>
<el-button
type="danger"
plain
:disabled="isEmpty(checkedIds)"
@click="handleDeleteBatch"
v-hasPermi="['isolation:plan-life-lock:delete']"
>
<Icon icon="ep:delete" class="mr-5px" /> 批量删除
</el-button> -->
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table
row-key="id"
v-loading="loading"
:data="list"
:stripe="true"
:show-overflow-tooltip="true"
@selection-change="handleRowCheckboxChange"
>
<el-table-column type="selection" width="55" />
<el-table-column label="上锁人" align="center" prop="userId">
<template #default="scope">
{{ elLockStore.users.find((item) => item.id === scope.row.userId)?.nickname }}
</template>
</el-table-column>
<el-table-column label="生命锁类型" align="center" prop="lockType">
<template #default="scope">
<DictTag :type="DICT_TYPE.LOCK_LIFE_LOCK_TYPE" :value="scope.row.lockType" />
</template>
</el-table-column>
<el-table-column label="锁定状态" align="center" prop="lockStatus">
<template #default="scope">
<DictTag :type="DICT_TYPE.LOCK_LIFE_LOCK_STATUS" :value="scope.row.lockStatus" />
</template>
</el-table-column>
<el-table-column
label="上锁时间"
align="center"
prop="lockTime"
:formatter="dateFormatter"
width="180px"
/>
<el-table-column
label="解锁时间"
align="center"
prop="unlockTime"
:formatter="dateFormatter"
width="180px"
/>
<!-- <el-table-column label="操作" align="center" min-width="120px">
<template #default="scope">
<el-button
link
type="primary"
@click="openForm('update', scope.row.id)"
v-hasPermi="['isolation:plan-life-lock:update']"
>
编辑
</el-button>
<el-button
link
type="danger"
@click="handleDelete(scope.row.id)"
v-hasPermi="['isolation:plan-life-lock:delete']"
>
删除
</el-button>
</template>
</el-table-column> -->
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap>
<!-- 表单弹窗添加/修改 -->
<PlanLifeLockForm ref="formRef" @success="getList" />
</template>
<script setup lang="ts">
import { isEmpty } from '@/utils/is'
import { dateFormatter } from '@/utils/formatTime'
import download from '@/utils/download'
import { PlanLifeLockApi, PlanLifeLock } from '@/api/isolation/planlifelock'
import PlanLifeLockForm from './PlanLifeLockForm.vue'
import { useElLockStore } from '@/store/modules/elLock'
import { DICT_TYPE, getDictLabel, getDictOptions } from '@/utils/dict'
const elLockStore = useElLockStore()
/** 个人生命锁 列表 */
defineOptions({ name: 'PlanLifeLock' })
const message = useMessage() //
const { t } = useI18n() //
const loading = ref(true) //
const list = ref<PlanLifeLock[]>([]) //
const total = ref(0) //
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
isolationPlanItemDetailId: undefined,
userId: undefined,
lockType: undefined,
lockStatus: undefined,
lockTime: [],
unlockTime: [],
createTime: []
})
const queryFormRef = ref() //
const exportLoading = ref(false) //
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
await elLockStore.init()
const data = await PlanLifeLockApi.getPlanLifeLockPage(queryParams)
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value.resetFields()
handleQuery()
}
/** 添加/修改操作 */
const formRef = ref()
const openForm = (type: string, id?: number) => {
formRef.value.open(type, id)
}
/** 删除按钮操作 */
const handleDelete = async (id: number) => {
try {
//
await message.delConfirm()
//
await PlanLifeLockApi.deletePlanLifeLock(id)
message.success(t('common.delSuccess'))
//
await getList()
} catch {}
}
/** 批量删除个人生命锁 */
const handleDeleteBatch = async () => {
try {
//
await message.delConfirm()
await PlanLifeLockApi.deletePlanLifeLockList(checkedIds.value)
message.success(t('common.delSuccess'))
await getList()
} catch {}
}
const checkedIds = ref<number[]>([])
const handleRowCheckboxChange = (records: PlanLifeLock[]) => {
checkedIds.value = records.map((item) => item.id)
}
/** 导出按钮操作 */
const handleExport = async () => {
try {
//
await message.exportConfirm()
//
exportLoading.value = true
const data = await PlanLifeLockApi.exportPlanLifeLock(queryParams)
download.excel(data, '个人生命锁.xls')
} catch {
} finally {
exportLoading.value = false
}
}
/** 初始化 **/
onMounted(() => {
getList()
})
</script>

125
web/src/views/lock/isolation/point/PointForm.vue

@ -0,0 +1,125 @@
<template>
<Dialog :title="dialogTitle" v-model="dialogVisible">
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="100px" v-loading="formLoading">
<el-form-item label="隔离点类型" prop="ipType">
<el-select v-model="formData.ipType" placeholder="请选择隔离点类型">
<el-option
v-for="item in getIntDictOptions(DICT_TYPE.LOCK_ISOLATION_TYPE)" :key="item.value"
:label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item label="隔离点名称" prop="ipName">
<el-input v-model="formData.ipName" placeholder="请输入隔离点名称" />
</el-form-item>
<el-form-item label="隔离点位置" prop="ipLocation">
<el-input v-model="formData.ipLocation" placeholder="请输入隔离点位置" />
</el-form-item>
<el-form-item label="隔离点编号" prop="ipNumber">
<el-input v-model="formData.ipNumber" placeholder="请输入隔离点编号" />
</el-form-item>
<el-form-item label="隔离点状态" prop="status">
<el-select v-model="formData.status" placeholder="请选择隔离点状态" disabled>
<el-option
v-for="item in getIntDictOptions(DICT_TYPE.LOCK_ISOLATION_POINT_STATUS)" :key="item.value"
:label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item label="电子锁数量" prop="guideLockNums">
<el-input-number
v-model="formData.guideLockNums" :precision="0" :step="1" :min="1" :max="1"
placeholder="请输入电子锁数量" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="submitForm" type="primary" :disabled="formLoading"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { PointApi, Point } from '@/api/isolation/point'
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
/** 隔离点 表单 */
defineOptions({ name: 'PointForm' })
const { t } = useI18n() //
const message = useMessage() //
const dialogVisible = ref(false) //
const dialogTitle = ref('') //
const formLoading = ref(false) // 12
const formType = ref('') // create - update -
const formData = ref({
id: undefined,
ipType: undefined,
ipName: undefined,
ipLocation: undefined,
ipNumber: undefined,
status: 0,
guideLockNums: 1
})
const formRules = reactive({
ipType: [{ required: true, message: '隔离点类型不能为空', trigger: 'change' }],
ipName: [{ required: true, message: '隔离点名称不能为空', trigger: 'blur' }],
ipNumber: [{ required: true, message: '隔离点编号不能为空', trigger: 'blur' }],
guideLockNums: [{ required: true, message: '电子锁数量不能为空', trigger: 'blur' }]
})
const formRef = ref() // Ref
/** 打开弹窗 */
const open = async (type: string, id?: number) => {
dialogVisible.value = true
dialogTitle.value = t('action.' + type)
formType.value = type
resetForm()
//
if (id) {
formLoading.value = true
try {
formData.value = await PointApi.getPoint(id)
} finally {
formLoading.value = false
}
}
}
defineExpose({ open }) // open
/** 提交表单 */
const emit = defineEmits(['success']) // success
const submitForm = async () => {
//
await formRef.value.validate()
//
formLoading.value = true
try {
const data = formData.value as unknown as Point
if (formType.value === 'create') {
await PointApi.createPoint(data)
message.success(t('common.createSuccess'))
} else {
await PointApi.updatePoint(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
//
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: undefined,
ipType: undefined,
ipName: undefined,
ipLocation: undefined,
ipNumber: undefined,
status: 0,
guideLockNums: 1
}
formRef.value?.resetFields()
}
</script>

254
web/src/views/lock/isolation/point/index.vue

@ -0,0 +1,254 @@
<template>
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="140px"
>
<el-form-item label="隔离点类型" prop="ipType">
<el-select
v-model="queryParams.ipType"
placeholder="请选择隔离点类型"
clearable
class="!w-240px"
>
<el-option
v-for="item in getIntDictOptions(DICT_TYPE.LOCK_ISOLATION_TYPE)"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="隔离点名称" prop="ipName">
<el-input
v-model="queryParams.ipName"
placeholder="请输入隔离点名称"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="隔离点位置" prop="ipLocation">
<el-input
v-model="queryParams.ipLocation"
placeholder="请输入隔离点位置"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="隔离点编号" prop="ipNumber">
<el-input
v-model="queryParams.ipNumber"
placeholder="请输入隔离点编号"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery"> <Icon icon="ep:search" class="mr-5px" /> 搜索 </el-button>
<el-button @click="resetQuery"> <Icon icon="ep:refresh" class="mr-5px" /> 重置 </el-button>
<el-button
type="primary"
plain
@click="openForm('create')"
v-hasPermi="['isolation:point:create']"
>
<Icon icon="ep:plus" class="mr-5px" /> 新增
</el-button>
<el-button
type="success"
plain
@click="handleExport"
:loading="exportLoading"
v-hasPermi="['isolation:point:export']"
>
<Icon icon="ep:download" class="mr-5px" /> 导出
</el-button>
<el-button
type="danger"
plain
:disabled="isEmpty(checkedIds)"
@click="handleDeleteBatch"
v-hasPermi="['isolation:point:delete']"
>
<Icon icon="ep:delete" class="mr-5px" /> 批量删除
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table
row-key="id"
v-loading="loading"
:data="list"
:stripe="true"
:show-overflow-tooltip="true"
@selection-change="handleRowCheckboxChange"
>
<el-table-column type="selection" width="55" />
<el-table-column label="隔离点类型" align="center" prop="ipType" />
<el-table-column label="隔离点名称" align="center" prop="ipName" />
<el-table-column label="隔离点位置" align="center" prop="ipLocation" />
<el-table-column label="隔离点编号" align="center" prop="ipNumber" />
<el-table-column label="隔离点状态" align="center" prop="status">
<template #default="scope">
<DictTag :type="DICT_TYPE.LOCK_ISOLATION_POINT_STATUS" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column label="锁数量" align="center" prop="guideLockNums" width="80px" />
<el-table-column label="操作" align="center" min-width="120px">
<template #default="scope">
<el-button link type="primary" @click="openForm('detail', scope.row.id)">
查看
</el-button>
<el-button
link
type="primary"
@click="openForm('update', scope.row.id)"
v-hasPermi="['isolation:point:update']"
>
编辑
</el-button>
<el-button
link
type="danger"
@click="handleDelete(scope.row.id)"
v-hasPermi="['isolation:point:delete']"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap>
<!-- 表单弹窗添加/修改 -->
<PointForm ref="formRef" @success="getList" />
</template>
<script setup lang="ts">
import { isEmpty } from '@/utils/is'
import download from '@/utils/download'
import { PointApi, Point } from '@/api/isolation/point'
import PointForm from './PointForm.vue'
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { getDictLabel } from '@/utils/dict'
import { dateFormatter } from '@/utils/formatTime'
import { useElLockStore } from '@/store/modules/elLock'
/** 隔离点 列表 */
defineOptions({ name: 'Point' })
const message = useMessage() //
const { t } = useI18n() //
const elLockStore = useElLockStore()
const loading = ref(true) //
const list = ref<Point[]>([]) //
const total = ref(0) //
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
ipType: undefined,
ipName: undefined,
ipLocation: undefined,
ipNumber: undefined,
createTime: []
})
const queryFormRef = ref() //
const exportLoading = ref(false) //
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
await elLockStore.init()
const data = await PointApi.getPointPage(queryParams)
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value.resetFields()
handleQuery()
}
/** 添加/修改操作 */
const formRef = ref()
const openForm = (type: string, id?: number) => {
formRef.value.open(type, id)
}
/** 删除按钮操作 */
const handleDelete = async (id: number) => {
try {
//
await message.delConfirm()
//
await PointApi.deletePoint(id)
message.success(t('common.delSuccess'))
//
await getList()
} catch {}
}
/** 批量删除隔离点 */
const handleDeleteBatch = async () => {
try {
//
await message.delConfirm()
await PointApi.deletePointList(checkedIds.value)
message.success(t('common.delSuccess'))
await getList()
} catch {}
}
const checkedIds = ref<number[]>([])
const handleRowCheckboxChange = (records: Point[]) => {
checkedIds.value = records.map((item) => item.id)
}
/** 导出按钮操作 */
const handleExport = async () => {
try {
//
await message.exportConfirm()
//
exportLoading.value = true
const data = await PointApi.exportPoint(queryParams)
download.excel(data, '隔离点.xls')
} catch {
} finally {
exportLoading.value = false
}
}
/** 初始化 **/
onMounted(() => {
getList()
})
</script>

1263
web/src/views/lock/lifelock/lifelock.vue

File diff suppressed because it is too large

124
web/src/views/lock/lock/LockForm.vue

@ -0,0 +1,124 @@
<template>
<Dialog :title="dialogTitle" v-model="dialogVisible">
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="100px" v-loading="formLoading">
<el-form-item label="编号" prop="lockNumber">
<el-input v-model="formData.lockNumber" placeholder="请输入编号" />
</el-form-item>
<el-form-item label="名称" prop="lockName">
<el-input v-model="formData.lockName" placeholder="请输入名称" />
</el-form-item>
<el-form-item label="锁具类型" prop="lockType">
<el-select v-model="formData.lockType" placeholder="请选择锁具类型">
<el-option v-for="item in getIntDictOptions(DICT_TYPE.LOCK_TYPE)" :key="item.value" :label="item.label"
:value="item.value" />
</el-select>
</el-form-item>
<el-form-item label="状态" prop="lockStatus">
<el-select v-model="formData.lockStatus" placeholder="请选择锁具状态" disabled>
<el-option v-for="item in getIntDictOptions(DICT_TYPE.LOCK_STATUS)" :key="item.value" :label="item.label"
:value="item.value" />
</el-select>
</el-form-item>
<el-form-item label="启用状态" prop="lockEnableStatus">
<el-radio-group v-model="formData.lockEnableStatus" :disabled="formData.lockStatus != 2">
<el-radio v-for="item in getIntDictOptions(DICT_TYPE.LOCK_ENABLE_STATUS)" :key="item.value"
:value="item.value">
{{ item.label }}
</el-radio>
</el-radio-group>
</el-form-item>
<!-- <el-form-item label="蓝牙标识" prop="lockBluetoothId">
<el-input v-model="formData.lockBluetoothId" placeholder="请输入蓝牙ID" />
</el-form-item> -->
</el-form>
<template #footer>
<el-button @click="submitForm" type="primary" :disabled="formLoading"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { LockApi, Lock } from '@/api/electron/lock'
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
/** 电子锁 表单 */
defineOptions({ name: 'LockForm' })
const { t } = useI18n() //
const message = useMessage() //
const dialogVisible = ref(false) //
const dialogTitle = ref('') //
const formLoading = ref(false) // 12
const formType = ref('') // create - update -
const formData = ref({
id: undefined,
lockNumber: undefined,
lockName: undefined,
lockStatus: 2,
lockType: undefined,
lockEnableStatus: 1,
lockBluetoothId: undefined
})
const formRules = reactive({
lockNumber: [{ required: true, message: '编号不能为空', trigger: 'blur' }],
lockName: [{ required: true, message: '名称不能为空', trigger: 'blur' }],
lockEnableStatus: [{ required: true, message: '启用状态不能为空', trigger: 'blur' }]
})
const formRef = ref() // Ref
/** 打开弹窗 */
const open = async (type: string, id?: number) => {
dialogVisible.value = true
dialogTitle.value = t('action.' + type)
formType.value = type
resetForm()
//
if (id) {
formLoading.value = true
try {
formData.value = await LockApi.getLock(id)
} finally {
formLoading.value = false
}
}
}
defineExpose({ open }) // open
/** 提交表单 */
const emit = defineEmits(['success']) // success
const submitForm = async () => {
//
await formRef.value.validate()
//
formLoading.value = true
try {
const data = formData.value as unknown as Lock
if (formType.value === 'create') {
await LockApi.createLock(data)
message.success(t('common.createSuccess'))
} else {
await LockApi.updateLock(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
//
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: undefined,
lockNumber: undefined,
lockName: undefined,
lockStatus: 2,
lockType: undefined,
lockEnableStatus: 1,
lockBluetoothId: undefined
}
formRef.value?.resetFields()
}
</script>

208
web/src/views/lock/lock/index.vue

@ -0,0 +1,208 @@
<template>
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form class="-mb-15px" :model="queryParams" ref="queryFormRef" :inline="true" label-width="68px">
<el-form-item label="编号" prop="lockNumber">
<el-input v-model="queryParams.lockNumber" placeholder="请输入编号" clearable @keyup.enter="handleQuery"
class="!w-240px" />
</el-form-item>
<el-form-item label="名称" prop="lockName">
<el-input v-model="queryParams.lockName" placeholder="请输入名称" clearable @keyup.enter="handleQuery"
class="!w-240px" />
</el-form-item>
<el-form-item label="状态" prop="lockStatus">
<el-select v-model="queryParams.lockStatus" placeholder="请选择状态" clearable class="!w-240px">
<el-option v-for="item in getIntDictOptions(DICT_TYPE.LOCK_STATUS)" :key="item.value" :label="item.label"
:value="item.value" />
</el-select>
</el-form-item>
<el-form-item label="锁具类型" prop="lockType">
<el-select v-model="queryParams.lockType" placeholder="请选择锁具类型" clearable class="!w-240px">
<el-option v-for="item in getIntDictOptions(DICT_TYPE.LOCK_TYPE)" :key="item.value" :label="item.label"
:value="item.value" />
</el-select>
</el-form-item>
<el-form-item label="启用状态" prop="lockEnableStatus">
<el-select v-model="queryParams.lockEnableStatus" placeholder="请选择启用状态" clearable class="!w-240px">
<el-option v-for="item in getIntDictOptions(DICT_TYPE.LOCK_ENABLE_STATUS)" :key="item.value"
:label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery">
<Icon icon="ep:search" class="mr-5px" /> 搜索
</el-button>
<el-button @click="resetQuery">
<Icon icon="ep:refresh" class="mr-5px" /> 重置
</el-button>
<el-button type="primary" plain @click="openForm('create')" v-hasPermi="['electron:lock:create']">
<Icon icon="ep:plus" class="mr-5px" /> 新增
</el-button>
<el-button type="success" plain @click="handleExport" :loading="exportLoading"
v-hasPermi="['electron:lock:export']">
<Icon icon="ep:download" class="mr-5px" /> 导出
</el-button>
<el-button type="danger" plain :disabled="isEmpty(checkedIds)" @click="handleDeleteBatch"
v-hasPermi="['electron:lock:delete']">
<Icon icon="ep:delete" class="mr-5px" /> 批量删除
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table row-key="id" v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true"
@selection-change="handleRowCheckboxChange">
<el-table-column type="selection" width="55" />
<el-table-column label="编号" align="center" prop="lockNumber" />
<el-table-column label="名称" align="center" prop="lockName" />
<el-table-column label="状态" align="center" prop="lockStatus">
<template #default="scope">
<dict-tag :type="DICT_TYPE.LOCK_STATUS" :value="scope.row.lockStatus" />
</template>
</el-table-column>
<el-table-column label="锁具类型" align="center" prop="lockType">
<template #default="scope">
<dict-tag :type="DICT_TYPE.LOCK_TYPE" :value="scope.row.lockType" />
</template>
</el-table-column>
<el-table-column label="启用状态" align="center" prop="lockEnableStatus">
<template #default="scope">
<dict-tag :type="DICT_TYPE.LOCK_ENABLE_STATUS" :value="scope.row.lockEnableStatus" />
</template>
</el-table-column>
<!-- <el-table-column label="上次充电时间" align="center" prop="lockLastChargeTime" :formatter="dateFormatter"
width="180px" /> -->
<!-- <el-table-column label="创建时间" align="center" prop="createTime" :formatter="dateFormatter" width="180px" /> -->
<el-table-column label="操作" align="center" min-width="120px">
<template #default="scope">
<el-button link type="primary" @click="openForm('update', scope.row.id)"
v-hasPermi="['electron:lock:update']">
编辑
</el-button>
<el-button link type="danger" @click="handleDelete(scope.row.id)" v-hasPermi="['electron:lock:delete']">
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination :total="total" v-model:page="queryParams.pageNo" v-model:limit="queryParams.pageSize"
@pagination="getList" />
</ContentWrap>
<!-- 表单弹窗添加/修改 -->
<LockForm ref="formRef" @success="getList" />
</template>
<script setup lang="ts">
import { isEmpty } from '@/utils/is'
import { dateFormatter } from '@/utils/formatTime'
import download from '@/utils/download'
import { LockApi, Lock } from '@/api/electron/lock'
import LockForm from './LockForm.vue'
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { useElLockStore } from '@/store/modules/elLock'
/** 电子锁 列表 */
defineOptions({ name: 'Lock' })
const message = useMessage() //
const { t } = useI18n() //
const elLockStore = useElLockStore()
const loading = ref(true) //
const list = ref<Lock[]>([]) //
const total = ref(0) //
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
lockNumber: undefined,
lockName: undefined,
lockStatus: undefined,
lockType: undefined,
lockEnableStatus: undefined
})
const queryFormRef = ref() //
const exportLoading = ref(false) //
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
await elLockStore.init()
const data = await LockApi.getLockPage(queryParams)
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value.resetFields()
handleQuery()
}
/** 添加/修改操作 */
const formRef = ref()
const openForm = (type: string, id?: number) => {
formRef.value.open(type, id)
}
/** 删除按钮操作 */
const handleDelete = async (id: number) => {
try {
//
await message.delConfirm()
//
await LockApi.deleteLock(id)
message.success(t('common.delSuccess'))
//
await getList()
} catch { }
}
/** 批量删除电子锁 */
const handleDeleteBatch = async () => {
try {
//
await message.delConfirm()
await LockApi.deleteLockList(checkedIds.value)
message.success(t('common.delSuccess'))
await getList()
} catch { }
}
const checkedIds = ref<number[]>([])
const handleRowCheckboxChange = (records: Lock[]) => {
checkedIds.value = records.map((item) => item.id)
}
/** 导出按钮操作 */
const handleExport = async () => {
try {
//
await message.exportConfirm()
//
exportLoading.value = true
const data = await LockApi.exportLock(queryParams)
download.excel(data, '电子锁.xls')
} catch {
} finally {
exportLoading.value = false
}
}
/** 初始化 **/
onMounted(() => {
getList()
})
</script>

85
web/src/views/lock/monitorTable.vue

@ -0,0 +1,85 @@
<template>
<el-table :data="activedPoint" class="w-full" border>
<el-table-column label="隔离点名称" align="center" prop="ipName">
<template #default="scope">
<el-link type="primary" @click="handleDetail(scope)" :underline="false">
{{ scope.row.ipName }}
</el-link>
</template>
</el-table-column>
<el-table-column
v-for="point in activePlan"
align="center"
:key="point.id"
:label="point.ipName"
:prop="'' + point.id"
>
<template #default="scope">
<el-link
type="primary"
@click="handleDetail(scope)"
:underline="false"
v-if="getDetailStatus(scope).total > 0"
>
<span class="text-green-500 pr-1"> {{ getDetailStatus(scope).completed }} </span>
<span class="text-gray-500">/ {{ getDetailStatus(scope).total }} </span>
</el-link>
</template>
</el-table-column>
</el-table>
<DetailPointModal
:isolation-point-id="selectedCell.isolationPointId"
:isolation-plan-id="selectedCell.isolationPlanId"
ref="detailPointModalRef"
/>
</template>
<script lang="ts" setup>
import { nextTick } from 'vue'
import { useElLockStore } from '@/store/modules/elLock'
import DetailPointModal from '@/components/Lock/DetailPointModal.vue'
defineOptions({ name: 'LockChart' })
const elLockStore = useElLockStore()
let activedPoint = elLockStore.isolationPoints.filter((item) => item.status == 1)
let activePlan = elLockStore.isolationPlans.filter((item) => item.status == 0)
const getDetailStatus = ({ row, column }) =>
elLockStore.planItemDetails
.filter((detail) => {
if (detail.isolationPointId != row.id) {
return false
}
let item = elLockStore.planItems.find((item) => item.id == detail.isolationPlanItemId)
if (item && item.isolationPlanId == column.property) {
return true
}
})
.reduce(
(acc, cur) => {
acc.total++
if (cur.lockStatus == 5) {
acc.completed++
}
return acc
},
{
total: 0,
completed: 0
}
)
const detailPointModalRef = ref()
const selectedCell = ref<{ isolationPointId?: number; isolationPlanId?: number }>({
isolationPointId: undefined,
isolationPlanId: undefined
})
const handleDetail = ({ row, column }) => {
selectedCell.value.isolationPointId = row.id
if (column.property != 'ipName') {
selectedCell.value.isolationPlanId = +column.property
} else {
selectedCell.value.isolationPlanId = undefined
}
// detailPanelshow
nextTick(() => {
detailPointModalRef.value?.show()
})
}
</script>

171
web/src/views/lock/record/LockWorkRecordForm.vue

@ -0,0 +1,171 @@
<template>
<Dialog :title="dialogTitle" v-model="dialogVisible">
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="100px"
v-loading="formLoading"
>
<el-form-item label="操作人" prop="operatorId">
<el-select v-model="formData.operatorId" placeholder="请选择操作人">
<el-option
v-for="item in elLockStore.users"
:key="item.id"
:label="item.nickname"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="电子锁" prop="lockId">
<el-select v-model="formData.lockId" placeholder="请选择电子锁">
<el-option
v-for="item in elLockStore.locks"
:key="item.id"
:label="item.lockName"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="子项详情" prop="isolationPlanItemDetailId">
<el-input v-model="formData.isolationPlanItemDetailId" placeholder="请选择关联的子项详情" />
</el-form-item>
<el-form-item label="记录类型" prop="recordType">
<el-select v-model="formData.recordType" placeholder="请选择记录类型">
<el-option
v-for="item in getIntDictOptions(DICT_TYPE.RECORD_TYPE)"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="操作签名" prop="signaturePath">
<UploadFile
v-model="formData.signaturePath"
:file-type="['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp']"
:limit="1"
:file-size="100"
class="min-w-80px"
/>
</el-form-item>
<el-form-item label="操作前照片" prop="beforePhotoPath">
<UploadFile
v-model="formData.beforePhotoPath"
:file-type="['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp']"
:limit="1"
:file-size="100"
class="min-w-80px"
/>
</el-form-item>
<el-form-item label="操作后照片" prop="afterPhotoPath">
<UploadFile
v-model="formData.afterPhotoPath"
:file-type="['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp']"
:limit="1"
:file-size="100"
class="min-w-80px"
/>
</el-form-item>
<el-form-item label="操作GPS坐标" prop="gpsCoordinates">
<el-input v-model="formData.gpsCoordinates" placeholder="请输入操作GPS坐标" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="submitForm" type="primary" :disabled="formLoading"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { LockWorkRecordApi, LockWorkRecord } from '@/api/electron/lockworkcord'
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { useElLockStore } from '@/store/modules/elLock'
import UploadFile from '@/components/UploadFile/src/UploadFile.vue'
/** 电子锁操作记录 表单 */
defineOptions({ name: 'LockWorkRecordForm' })
const { t } = useI18n() //
const message = useMessage() //
const elLockStore = useElLockStore() // store
const dialogVisible = ref(false) //
const dialogTitle = ref('') //
const formLoading = ref(false) // 12
const formType = ref('') // create - update -
const formData = ref({
id: undefined,
operatorId: undefined,
lockId: undefined,
isolationPlanItemDetailId: undefined,
recordType: undefined,
signaturePath: '',
beforePhotoPath: '',
afterPhotoPath: '',
gpsCoordinates: undefined
})
const formRules = reactive({
operatorId: [{ required: true, message: '操作人不能为空', trigger: 'blur' }],
lockId: [{ required: true, message: '电子锁不能为空', trigger: 'blur' }],
recordType: [{ required: true, message: '记录类型不能为空', trigger: 'change' }]
})
const formRef = ref() // Ref
/** 打开弹窗 */
const open = async (type: string, id?: number) => {
dialogVisible.value = true
dialogTitle.value = t('action.' + type)
formType.value = type
resetForm()
//
if (id) {
formLoading.value = true
try {
formData.value = await LockWorkRecordApi.getLockWorkRecord(id)
} finally {
formLoading.value = false
}
}
}
defineExpose({ open }) // open
/** 提交表单 */
const emit = defineEmits(['success']) // success
const submitForm = async () => {
//
await formRef.value.validate()
//
formLoading.value = true
try {
const data = formData.value as unknown as LockWorkRecord
if (formType.value === 'create') {
await LockWorkRecordApi.createLockWorkRecord(data)
message.success(t('common.createSuccess'))
} else {
await LockWorkRecordApi.updateLockWorkRecord(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
//
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: undefined,
operatorId: undefined,
lockId: undefined,
isolationPlanItemDetailId: undefined,
recordType: undefined,
signaturePath: '',
beforePhotoPath: '',
afterPhotoPath: '',
gpsCoordinates: undefined
}
formRef.value?.resetFields()
}
</script>

296
web/src/views/lock/record/index.vue

@ -0,0 +1,296 @@
<template>
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="68px"
>
<el-form-item label="操作人" prop="operatorId">
<el-select
style="width: 240px"
v-model="queryParams.operatorId"
placeholder="请选择操作人"
clearable
filterable
>
<el-option
v-for="item in elLockStore.users"
:key="item.id"
:label="item.nickname"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="电子锁" prop="lockId">
<el-select
style="width: 240px"
v-model="queryParams.lockId"
placeholder="请选择电子锁"
clearable
filterable
>
<el-option
v-for="item in elLockStore.locks"
:key="item.id"
:label="item.lockName"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="记录类型" prop="recordType">
<el-select
v-model="queryParams.recordType"
placeholder="请选择记录类型"
clearable
class="!w-240px"
>
<el-option
v-for="item in getIntDictOptions(DICT_TYPE.RECORD_TYPE)"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="记录时间" prop="createTime">
<el-date-picker
v-model="queryParams.createTime"
value-format="YYYY-MM-DD HH:mm:ss"
type="daterange"
start-placeholder="开始日期"
end-placeholder="结束日期"
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
class="!w-220px"
/>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
<el-button
type="primary"
plain
@click="openForm('create')"
v-hasPermi="['electron:lock-word-record:create']"
>
<Icon icon="ep:plus" class="mr-5px" /> 新增
</el-button>
<el-button
type="success"
plain
@click="handleExport"
:loading="exportLoading"
v-hasPermi="['electron:lock-word-record:export']"
>
<Icon icon="ep:download" class="mr-5px" /> 导出
</el-button>
<el-button
type="danger"
plain
:disabled="isEmpty(checkedIds)"
@click="handleDeleteBatch"
v-hasPermi="['electron:lock-word-record:delete']"
>
<Icon icon="ep:delete" class="mr-5px" /> 批量删除
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table
row-key="id"
v-loading="loading"
:data="list"
:stripe="true"
:show-overflow-tooltip="true"
@selection-change="handleRowCheckboxChange"
>
<el-table-column type="selection" width="55" />
<el-table-column label="操作人" align="center" prop="operatorId">
<template #default="scope">
<el-tag>{{
elLockStore.users.find((item) => item.id === scope.row.operatorId)?.nickname
}}</el-tag>
</template>
</el-table-column>
<el-table-column label="电子锁" align="center" prop="lockId">
<template #default="scope">
<el-tag>{{
elLockStore.locks.find((item) => item.id === scope.row.lockId)?.lockName
}}</el-tag>
</template>
</el-table-column>
<el-table-column label="记录类型" align="center" prop="recordType">
<template #default="scope">
<dict-tag :type="DICT_TYPE.RECORD_TYPE" :value="scope.row.recordType" />
</template>
</el-table-column>
<el-table-column label="操作签名" align="center" prop="signaturePath">
<template #default="scope">
<el-image v-if="scope.row.signaturePath" :src="scope.row.signaturePath" />
</template>
</el-table-column>
<el-table-column label="操作前照片" align="center" prop="beforePhotoPath">
<template #default="scope">
<el-image v-if="scope.row.beforePhotoPath" :src="scope.row.beforePhotoPath" />
</template>
</el-table-column>
<el-table-column label="操作后照片" align="center" prop="afterPhotoPath">
<template #default="scope">
<el-image v-if="scope.row.afterPhotoPath" :src="scope.row.afterPhotoPath" />
</template>
</el-table-column>
<el-table-column label="GPS坐标" align="center" prop="gpsCoordinates" />
<el-table-column
label="记录时间"
align="center"
prop="createTime"
:formatter="dateFormatter"
width="180px"
/>
<el-table-column label="操作" align="center" min-width="120px">
<template #default="scope">
<el-button
link
type="primary"
@click="openForm('update', scope.row.id)"
v-hasPermi="['electron:lock-word-record:update']"
>
编辑
</el-button>
<el-button
link
type="danger"
@click="handleDelete(scope.row.id)"
v-hasPermi="['electron:lock-word-record:delete']"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap>
<!-- 表单弹窗添加/修改 -->
<LockWorkRecordForm ref="formRef" @success="getList" />
</template>
<script setup lang="ts">
import { isEmpty } from '@/utils/is'
import { dateFormatter } from '@/utils/formatTime'
import download from '@/utils/download'
import { LockWorkRecordApi, LockWorkRecord } from '@/api/electron/lockworkcord'
import LockWorkRecordForm from './LockWorkRecordForm.vue'
import { useElLockStore } from '@/store/modules/elLock'
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
/** 电子锁操作记录 列表 */
defineOptions({ name: 'LockWorkRecord' })
const message = useMessage() //
const { t } = useI18n() //
const elLockStore = useElLockStore()
const loading = ref(true) //
const list = ref<LockWorkRecord[]>([]) //
const total = ref(0) //
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
operatorId: undefined,
lockId: undefined,
recordType: undefined,
createTime: []
})
const queryFormRef = ref() //
const exportLoading = ref(false) //
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
const data = await LockWorkRecordApi.getLockWorkRecordPage(queryParams)
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value.resetFields()
handleQuery()
}
/** 添加/修改操作 */
const formRef = ref()
const openForm = (type: string, id?: number) => {
formRef.value.open(type, id)
}
/** 删除按钮操作 */
const handleDelete = async (id: number) => {
try {
//
await message.delConfirm()
//
await LockWorkRecordApi.deleteLockWorkRecord(id)
message.success(t('common.delSuccess'))
//
await getList()
} catch {}
}
/** 批量删除电子锁操作记录 */
const handleDeleteBatch = async () => {
try {
//
await message.delConfirm()
await LockWorkRecordApi.deleteLockWorkRecordList(checkedIds.value)
message.success(t('common.delSuccess'))
await getList()
} catch {}
}
const checkedIds = ref<number[]>([])
const handleRowCheckboxChange = (records: LockWorkRecord[]) => {
checkedIds.value = records.map((item) => item.id)
}
/** 导出按钮操作 */
const handleExport = async () => {
try {
//
await message.exportConfirm()
//
exportLoading.value = true
const data = await LockWorkRecordApi.exportLockWorkRecord(queryParams)
download.excel(data, '电子锁操作记录.xls')
} catch {
} finally {
exportLoading.value = false
}
}
/** 初始化 **/
onMounted(() => {
getList()
})
</script>

20
web/vite.config.ts

@ -1,8 +1,8 @@
import {resolve} from 'path'
import type {ConfigEnv, UserConfig} from 'vite'
import {loadEnv} from 'vite'
import {createVitePlugins} from './build/vite'
import {exclude, include} from "./build/vite/optimize"
import { resolve } from 'path'
import type { ConfigEnv, UserConfig } from 'vite'
import { loadEnv } from 'vite'
import { createVitePlugins } from './build/vite'
import { exclude, include } from "./build/vite/optimize"
// 当前执行node命令时文件夹的地址(工作目录)
const root = process.cwd()
@ -12,7 +12,7 @@ function pathResolve(dir: string) {
}
// https://vitejs.dev/config/
export default ({command, mode}: ConfigEnv): UserConfig => {
export default ({ command, mode }: ConfigEnv): UserConfig => {
let env = {} as any
const isBuild = command === 'build'
if (!isBuild) {
@ -76,13 +76,13 @@ export default ({command, mode}: ConfigEnv): UserConfig => {
rollupOptions: {
output: {
manualChunks: {
echarts: ['echarts'], // 将 echarts 单独打包,参考 https://gitee.com/yudaocode/yudao-ui-admin-vue3/issues/IAB1SX 讨论
'form-create': ['@form-create/element-ui'], // 参考 https://github.com/yudaocode/yudao-ui-admin-vue3/issues/148 讨论
'form-designer': ['@form-create/designer'],
echarts: ['echarts'], // 将 echarts 单独打包,参考 https://gitee.com/yudaocode/yudao-ui-admin-vue3/issues/IAB1SX 讨论
'form-create': ['@form-create/element-ui'], // 参考 https://github.com/yudaocode/yudao-ui-admin-vue3/issues/148 讨论
'form-designer': ['@form-create/designer'],
}
},
},
},
optimizeDeps: {include, exclude}
optimizeDeps: { include, exclude }
}
}

Loading…
Cancel
Save