Jelajahi Sumber

2025/6/17提交库糖果迁移的部分菜单页面接口未测试

pm 4 bulan lalu
induk
melakukan
ebafc34bcb
100 mengubah file dengan 9957 tambahan dan 6 penghapusan
  1. 2 1
      .env
  2. 2 2
      .env.dev
  3. 2 2
      .env.local
  4. 60 0
      src/api/basic/configuration/index.ts
  5. 46 0
      src/api/basic/mapconfig/index.ts
  6. 52 0
      src/api/basic/mappoint/index.ts
  7. 59 0
      src/api/dv/lotoStation/index.ts
  8. 66 0
      src/api/dv/spm/index.ts
  9. 53 0
      src/api/dv/technology/index.ts
  10. 52 0
      src/api/email/notify/index.ts
  11. 52 0
      src/api/email/templates/index.ts
  12. 69 0
      src/api/hw/hardware/information/index.ts
  13. 52 0
      src/api/hw/hardware/keys/index.ts
  14. 52 0
      src/api/hw/hardware/lockCabinet/index.ts
  15. 54 0
      src/api/hw/hardware/lockCabinet/slots.ts
  16. 52 0
      src/api/hw/hardware/lockset/index.ts
  17. 52 0
      src/api/hw/hardware/padLock/index.ts
  18. 52 0
      src/api/hw/hardware/rfid/index.ts
  19. 59 0
      src/api/hw/hardware/workCard/index.ts
  20. 51 0
      src/api/hw/type/hardwaretype/index.ts
  21. 54 0
      src/api/hw/type/locksettype/index.ts
  22. 54 0
      src/api/hw/type/padLockType/index.ts
  23. 54 0
      src/api/system/unit/index.ts
  24. TEMPAT SAMPAH
      src/assets/images/MapImage.png
  25. TEMPAT SAMPAH
      src/assets/images/accept.png
  26. TEMPAT SAMPAH
      src/assets/images/colocked.png
  27. 39 0
      src/assets/images/dark.svg
  28. TEMPAT SAMPAH
      src/assets/images/error.png
  29. 39 0
      src/assets/images/light.svg
  30. TEMPAT SAMPAH
      src/assets/images/localSetIcon.jpg
  31. TEMPAT SAMPAH
      src/assets/images/localSetSelect.jpg
  32. TEMPAT SAMPAH
      src/assets/images/lockImg.png
  33. TEMPAT SAMPAH
      src/assets/images/locked.png
  34. TEMPAT SAMPAH
      src/assets/images/login-background-back.jpg
  35. TEMPAT SAMPAH
      src/assets/images/login-background.jpg
  36. TEMPAT SAMPAH
      src/assets/images/marsBg.png
  37. TEMPAT SAMPAH
      src/assets/images/marsPoint.png
  38. TEMPAT SAMPAH
      src/assets/images/notcolocked.png
  39. TEMPAT SAMPAH
      src/assets/images/prepare.png
  40. TEMPAT SAMPAH
      src/assets/images/profile.jpg
  41. TEMPAT SAMPAH
      src/assets/images/reject.png
  42. TEMPAT SAMPAH
      src/assets/images/sopPoint.png
  43. TEMPAT SAMPAH
      src/assets/images/sopbgimg.png
  44. TEMPAT SAMPAH
      src/assets/images/success.png
  45. TEMPAT SAMPAH
      src/assets/images/table.png
  46. TEMPAT SAMPAH
      src/assets/images/table_map1.jpg
  47. TEMPAT SAMPAH
      src/assets/images/table_map2.jpg
  48. TEMPAT SAMPAH
      src/assets/images/table_map3.jpg
  49. TEMPAT SAMPAH
      src/assets/images/techonlogy.png
  50. TEMPAT SAMPAH
      src/assets/images/ticketType0.png
  51. TEMPAT SAMPAH
      src/assets/images/ticketType1.png
  52. TEMPAT SAMPAH
      src/assets/images/ticketType2.png
  53. TEMPAT SAMPAH
      src/assets/images/ticketType3.png
  54. TEMPAT SAMPAH
      src/assets/images/ticketType4.png
  55. TEMPAT SAMPAH
      src/assets/images/warn.png
  56. TEMPAT SAMPAH
      src/assets/images/workshop.png
  57. TEMPAT SAMPAH
      src/assets/images/柜中.png
  58. TEMPAT SAMPAH
      src/assets/images/柜外.png
  59. TEMPAT SAMPAH
      src/assets/images/警告.png
  60. TEMPAT SAMPAH
      src/assets/images/警告2.png
  61. 1 0
      src/config/axios/index.ts
  62. 36 0
      src/router/modules/remaining.ts
  63. 54 1
      src/utils/dict.ts
  64. 172 0
      src/views/Basicdata/configuration/ConfigForm.vue
  65. 250 0
      src/views/Basicdata/configuration/index.vue
  66. 138 0
      src/views/Basicdata/mapconfig/MapConfigForm.vue
  67. 217 0
      src/views/Basicdata/mapconfig/index.vue
  68. 171 0
      src/views/Basicdata/mappoint/MapPointForm.vue
  69. 207 0
      src/views/Basicdata/mappoint/index.vue
  70. 40 0
      src/views/dv/lotoStation/LookDetail.vue
  71. 188 0
      src/views/dv/lotoStation/LotoStationForm.vue
  72. 234 0
      src/views/dv/lotoStation/MapData.vue
  73. 259 0
      src/views/dv/lotoStation/PointList.vue
  74. 176 0
      src/views/dv/lotoStation/SwitchStatus.vue
  75. 211 0
      src/views/dv/lotoStation/index.vue
  76. 343 0
      src/views/dv/spm/SegregationPointForm.vue
  77. 294 0
      src/views/dv/spm/index.vue
  78. 167 0
      src/views/dv/technology/TechnologyForm.vue
  79. 347 0
      src/views/dv/technology/index.vue
  80. 641 0
      src/views/dv/technology/technologyDetail/CraftDetail.vue
  81. 312 0
      src/views/dv/technology/technologyDetail/DeviceDetail.vue
  82. 335 0
      src/views/dv/technology/technologyDetail/MapData.vue
  83. 237 0
      src/views/email/emailNotify/EmailNotifyForm.vue
  84. 188 0
      src/views/email/emailNotify/index.vue
  85. 131 0
      src/views/email/emailTemplates/EmailTemplateForm.vue
  86. 188 0
      src/views/email/emailTemplates/index.vue
  87. 321 0
      src/views/hw/hardware/information/HardwareForm.vue
  88. 277 0
      src/views/hw/hardware/information/index.vue
  89. 226 0
      src/views/hw/hardware/keys/KeyForm.vue
  90. 197 0
      src/views/hw/hardware/keys/index.vue
  91. 225 0
      src/views/hw/hardware/lockset/LocksetForm.vue
  92. 207 0
      src/views/hw/hardware/lockset/index.vue
  93. 237 0
      src/views/hw/hardware/padLocks/PadLockForm.vue
  94. 198 0
      src/views/hw/hardware/padLocks/index.vue
  95. 265 0
      src/views/hw/lockCabinet/LockCabinetForm.vue
  96. 225 0
      src/views/hw/lockCabinet/LookList.vue
  97. 256 0
      src/views/hw/lockCabinet/MapData.vue
  98. 273 0
      src/views/hw/lockCabinet/SlotForm.vue
  99. 304 0
      src/views/hw/lockCabinet/index.vue
  100. 30 0
      src/views/hw/lockCabinet/lookDetail.vue

+ 2 - 1
.env

@@ -3,7 +3,8 @@ VITE_APP_TITLE=芋道管理系统
 
 # 项目本地运行端口号
 VITE_PORT=80
-
+# 请求路径
+VITE_BASE_URL='http://192.168.0.10:48080'
 # open 运行 npm run dev 时自动打开浏览器
 VITE_OPEN=true
 

+ 2 - 2
.env.dev

@@ -4,7 +4,7 @@ NODE_ENV=production
 VITE_DEV=true
 
 # 请求路径
-VITE_BASE_URL='http://api-dashboard.yudao.iocoder.cn'
+VITE_BASE_URL='http://192.168.0.10:48080'
 
 # 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持S3服务
 VITE_UPLOAD_TYPE=server
@@ -34,4 +34,4 @@ VITE_MALL_H5_DOMAIN='http://mall.yudao.iocoder.cn'
 VITE_APP_CAPTCHA_ENABLE=true
 
 # GoView域名
-VITE_GOVIEW_URL='http://127.0.0.1:3000'
+VITE_GOVIEW_URL='http://127.0.0.1:3000'

+ 2 - 2
.env.local

@@ -4,7 +4,7 @@ NODE_ENV=development
 VITE_DEV=true
 
 # 请求路径
-VITE_BASE_URL='http://localhost:48080'
+VITE_BASE_URL='http://192.168.0.10:48080'
 
 # 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持 S3 服务
 VITE_UPLOAD_TYPE=server
@@ -31,4 +31,4 @@ VITE_MALL_H5_DOMAIN='http://localhost:3000'
 VITE_APP_CAPTCHA_ENABLE=false
 
 # GoView域名
-VITE_GOVIEW_URL='http://127.0.0.1:3000'
+VITE_GOVIEW_URL='http://127.0.0.1:3000'

+ 60 - 0
src/api/basic/configuration/index.ts

@@ -0,0 +1,60 @@
+import request from '@/config/axios'
+
+export interface ConfigVO {
+  sysAttrId?: number
+  sysAttrName: string
+  sysAttrKey: string
+  sysAttrType?: string
+  sysAttrValue: string
+  remark?: string
+  createTime?: Date
+}
+
+export interface PageParam {
+  current: number
+  size: number
+  sysAttrName?: string
+  sysAttrValue?: string
+  sysAttrKey?: string
+}
+
+// 查询基础数据-全局配置列表
+export const getIsSystemAttributePage = async (params: PageParam) => {
+  return await request.get({ url: '/iscs/attribute/getIsSystemAttributePage', params })
+}
+
+// 查询基础数据-全局配置详细
+export const selectIsSystemAttributeById = async (id: number) => {
+  return await request.get({ url: '/iscs/attribute/selectIsSystemAttributeById', params: { id } })
+}
+
+// 新增基础数据-全局配置
+export const insertIsSystemAttribute = async (data: ConfigVO) => {
+  return await request.post({ url: '/iscs/attribute/insertIsSystemAttribute', data })
+}
+
+// 修改基础数据-全局配置
+export const updateIsSystemAttribute = async (data: ConfigVO) => {
+  return await request.post({ url: '/iscs/attribute/updateIsSystemAttribute', data })
+}
+
+// 删除基础数据-全局配置
+export const deleteIsSystemAttributeByIds = async (id: number) => {
+  return await request.post({
+    url: '/iscs/attribute/deleteIsSystemAttributeByIds',
+    params: { ids: id }
+  })
+}
+
+// 根据键名查询配置
+export const getIsSystemAttributeByKey = async (sysAttrKey: string) => {
+  return await request.get({
+    url: '/iscs/attribute/getIsSystemAttributeByKey',
+    params: { sysAttrKey }
+  })
+}
+
+// 刷新缓存
+export const refreshAttrCache = async () => {
+  return await request.delete({ url: '/iscs/attribute/refreshAttrCache' })
+}

+ 46 - 0
src/api/basic/mapconfig/index.ts

@@ -0,0 +1,46 @@
+import request from '@/config/axios'
+
+export interface MapVO {
+  id?: number
+  name: string
+  imageUrl: string
+  width?: number
+  height?: number
+  x?: number
+  y?: number
+  createTime?: Date
+}
+
+export interface PageParam {
+  current: number
+  size: number
+  name?: string
+}
+
+// 查询地图参数列表
+export const getIsMapPage = async (params: PageParam) => {
+  return await request.get({ url: '/iscs/map/getIsMapPage', params })
+}
+
+// 查询地图参数详细
+export const selectIsMapById = async (id: number) => {
+  return await request.get({ url: '/iscs/map/selectIsMapById', params: { id } })
+}
+
+// 新增地图参数配置
+export const insertIsMap = async (data: MapVO) => {
+  return await request.post({ url: '/iscs/map/insertIsMap', data })
+}
+
+// 修改地图参数配置
+export const updateIsMap = async (data: MapVO) => {
+  return await request.post({ url: '/iscs/map/updateIsMap', data })
+}
+
+// 删除地图参数配置
+export const deleteIsMapByIds = async (id: number) => {
+  return await request.post({
+    url: '/iscs/map/deleteIsMapByIds',
+    params: { ids: id }
+  })
+}

+ 52 - 0
src/api/basic/mappoint/index.ts

@@ -0,0 +1,52 @@
+import request from '@/config/axios'
+
+export interface MapPointVO {
+  id?: number
+  mapId: number
+  mapType: string
+  entityId: number
+  entityName?: string
+  mapName?: string
+  x: number
+  y: number
+  createTime?: Date
+}
+
+export interface PageParam {
+  current: number
+  size: number
+  mapId?: number
+}
+
+// 查询地图点位数据列表
+export const getIsMapPointPage = async (params: PageParam) => {
+  return await request.get({ url: '/iscs/map-point/getIsMapPointPage', params })
+}
+
+// 查询地图点位数据详细
+export const selectIsMapPointById = async (id: number) => {
+  return await request.get({ url: '/iscs/map-point/selectIsMapPointById', params: { id } })
+}
+
+// 新增地图点位数据配置
+export const insertIsMapPoint = async (data: MapPointVO) => {
+  return await request.post({ url: '/iscs/map-point/insertIsMapPoint', data })
+}
+
+// 修改地图点位数据配置
+export const updateIsMapPoint = async (data: MapPointVO) => {
+  return await request.post({ url: '/iscs/map-point/updateIsMapPoint', data })
+}
+
+// 删除地图点位数据配置
+export const deleteIsMapPointByIds = async (id: number) => {
+  return await request.post({
+    url: '/iscs/map-point/deleteIsMapPointByIds',
+    params: { ids: id }
+  })
+}
+
+// 地图点位数据变更(绑定 位移 解绑)
+export const updateMapPointList = async (data: MapPointVO[]) => {
+  return await request.post({ url: '/iscs/map-point/updateMapPointList', data })
+}

+ 59 - 0
src/api/dv/lotoStation/index.ts

@@ -0,0 +1,59 @@
+// src/api/mes/lotoStation/lotoStation.ts
+import request from '@/config/axios'
+
+export interface LotoStationVO {
+  lotoId?: number
+  lotoName: string
+  lotoCode: string
+  lotoType?: string
+  lotoStatus?: string
+  lotoMap?: string
+  remark?: string
+  createTime?: Date
+}
+
+export interface PageParam {
+  current: number
+  size: number
+  lotoName?: string
+  lotoCode?: string
+  lotoType?: string
+}
+
+// 查询锁定站列表
+export const listLoto = async (params: PageParam) => {
+  return await request.get({ url: '/iscs/station/getIsLotoStationPage', params })
+}
+
+// 查询锁定站详情
+export const selectIsLotoStationById = async (id: number) => {
+  return await request.get({ url: '/iscs/station/selectIsLotoStationById', params: { lotoId: id } })
+}
+
+// 查询锁定站地图数据
+export const selectLotoMapById = async (id: number) => {
+  return await request.get({ url: '/iscs/station/selectLotoMapById', params: { lotoId: id } })
+}
+
+// 新增锁定站
+export const addLoto = async (data: LotoStationVO) => {
+  return await request.post({ url: '/iscs/station/insertIsLotoStation', data })
+}
+
+// 修改锁定站
+export const updateLoto = async (data: LotoStationVO) => {
+  return await request.post({ url: '/iscs/station/updateIsLotoStation', data })
+}
+
+// 删除锁定站
+export const delLoto = async (id: number) => {
+  return await request.post({
+    url: '/iscs/station/deleteIsLotoStationByLotoIds',
+    params: { lotoIds: id }
+  })
+}
+
+// 更新隔离点绑定关系
+export const updatePointsBindingLoto = async (data: any) => {
+  return await request.post({ url: '/iscs/station/updatePointsBindingLoto', data })
+}

+ 66 - 0
src/api/dv/spm/index.ts

@@ -0,0 +1,66 @@
+import request from '@/config/axios'
+
+export interface SegregationPointVO {
+  pointId?: number
+  pointCode: string
+  pointName: string
+  pointIcon?: string
+  pointPicture?: string
+  pointNfc: string
+  workstationId: number
+  lotoId: number
+  powerType?: string
+  pointSerialNumber?: string
+  remark?: string
+  createTime?: Date
+}
+
+export interface PageParam {
+  current: number
+  size: number
+  pointCode?: string
+  pointName?: string
+  workstationId?: number
+  lotoId?: number
+}
+
+// 查询隔离点列表
+export const getIsIsolationPointPage = async (params: PageParam) => {
+  return await request.get({ url: '/iscs/point/getIsIsolationPointPage', params })
+}
+
+// 查询隔离点详情
+export const selectIsIsolationPointById = async (id: number) => {
+  return await request.get({ url: '/iscs/point/selectIsIsolationPointById', params: { pointId: id } })
+}
+
+// 新增隔离点
+export const addinsertIsIsolationPoint = async (data: SegregationPointVO) => {
+  return await request.post({ url: '/iscs/point/insertIsIsolationPoint', data })
+}
+
+// 修改隔离点
+export const updateIsIsolationPoint = async (data: SegregationPointVO) => {
+  return await request.post({ url: '/iscs/point/updateIsIsolationPoint', data })
+}
+
+// 删除隔离点
+export const deleteIsIsolationPointByPointIds = async (ids: number) => {
+  return await request.post({
+    url: '/iscs/point/deleteIsIsolationPointByPointIds',
+    params: { pointIds: ids }
+  })
+}
+
+// 查询车间列表
+export const getWorkshopList = async () => {
+  return await request.get({ url: '/mes/md/workshop/listAll' })
+}
+
+// 查询工作区域列表
+export const getWorkareaList = async (workshopId: number) => {
+  return await request.get({
+    url: '/iscs/workarea/getIsWorkareaList',
+    params: { workshopId }
+  })
+}

+ 53 - 0
src/api/dv/technology/index.ts

@@ -0,0 +1,53 @@
+// src/api/system/machinery.ts
+import request from '@/config/axios'
+
+export interface MachineryVO {
+  machineryId?: number
+  machineryName: string
+  machineryCode: string
+  machineryType: string
+  parentId?: number
+  remark?: string
+  createTime?: Date
+}
+
+export interface PageParam {
+  current: number
+  size: number
+  machineryName?: string
+  machineryCode?: string
+  machineryType?: string
+}
+
+// 查询设备工艺列表
+export const listTechnology = async (params: PageParam) => {
+  return await request.get({ url: '/iscs/machinery/getIsMachineryPage', params })
+}
+
+// 查询设备工艺详情
+export const getTechnologyInfo = async (id: number) => {
+  return await request.get({ url: '/iscs/machinery/selectIsMachineryById', params: { machineryId: id } })
+}
+
+// 新增设备工艺
+export const addTechnology = async (data: MachineryVO) => {
+  return await request.post({ url: '/iscs/machinery/insertIsMachinery', data })
+}
+
+// 修改设备工艺
+export const updateTechnology = async (data: MachineryVO) => {
+  return await request.post({ url: '/iscs/machinery/updateIsMachinery', data })
+}
+
+// 删除设备工艺
+export const delTechnology = async (id: number) => {
+  return await request.post({
+    url: '/iscs/machinery/deleteIsMachineryByTechnologyIds',
+    params: { machineryIds: id }
+  })
+}
+
+// 保存设备工艺与隔离点的关系
+export const saveMachineryPoints = async (data: any) => {
+  return await request.post({ url: '/iscs/machinery/saveMachineryPoints', data })
+}

+ 52 - 0
src/api/email/notify/index.ts

@@ -0,0 +1,52 @@
+import request from '@/config/axios'
+
+export interface MailNotifyConfigVO {
+  configId?: number
+  configName: string
+  configCode: string
+  notifyCycle?: string
+  notifyTime?: string
+  notifyType?: string
+  status?: string
+  remark?: string
+  createTime?: Date
+}
+
+export interface PageParam {
+  pageNo: number
+  pageSize: number
+  configName?: string
+  configCode?: string
+  status?: string
+}
+
+// 查看系统邮件提醒周期配置-分页
+export const listIsMailNotifyConfigPage = async (params: PageParam) => {
+  return await request.get({ url: '/iscs/notify/getIsMailNotifyConfigPage', params })
+}
+
+// 新增系统邮件提醒周期配置
+export const addIsMailNotifyConfig = async (data: MailNotifyConfigVO) => {
+  return await request.post({ url: '/iscs/notify/insertIsMailNotifyConfig', data })
+}
+
+// 删除系统邮件提醒周期配置
+export const deleteIsMailNotifyConfig = async (configId: number) => {
+  return await request.post({
+    url: '/iscs/notify/deleteIsMailNotifyConfigByConfigIds',
+    params: { configIds: configId }
+  })
+}
+
+// 修改系统邮件提醒周期配置
+export const updateIsMailNotifyConfig = async (data: MailNotifyConfigVO) => {
+  return await request.post({ url: '/iscs/notify/updateIsMailNotifyConfig', data })
+}
+
+// 获取系统邮件提醒周期配置详细信息
+export const getIsMailNotifyConfigById = async (configId: number) => {
+  return await request.get({
+    url: '/iscs/notify/selectIsMailNotifyConfigById',
+    params: { configId }
+  })
+}

+ 52 - 0
src/api/email/templates/index.ts

@@ -0,0 +1,52 @@
+import request from '@/config/axios'
+
+export interface EmailTemplateVO {
+  templateId?: number
+  templateName: string
+  templateCode: string
+  templateContent?: string
+  templateType?: string
+  status?: string
+  remark?: string
+  createTime?: Date
+}
+
+export interface PageParam {
+  pageNo: number
+  pageSize: number
+  templateName?: string
+  templateCode?: string
+  templateType?: string
+  status?: string
+}
+
+// 查询邮件模板列表
+export const listEmailTemplates = async (params: PageParam) => {
+  return await request.get({ url: '/iscs/template/getIsMailTemplatePage', params })
+}
+
+// 查询邮件模板详细
+export const getEmailTemplatesInfo = async (templateId: number) => {
+  return await request.get({
+    url: '/iscs/template/selectIsMailTemplateById',
+    params: { templateId }
+  })
+}
+
+// 新增邮件模板
+export const addEmailTemplates = async (data: EmailTemplateVO) => {
+  return await request.post({ url: '/iscs/template/insertIsMailTemplate', data })
+}
+
+// 修改邮件模板
+export const updateEmailTemplates = async (data: EmailTemplateVO) => {
+  return await request.post({ url: '/iscs/template/updateIsMailTemplate', data })
+}
+
+// 删除邮件模板
+export const delEmailTemplates = async (templateIds: number) => {
+  return await request.post({
+    url: '/iscs/template/deleteIsMailTemplateByTemplateCodes',
+    params: { templateIds }
+  })
+}

+ 69 - 0
src/api/hw/hardware/information/index.ts

@@ -0,0 +1,69 @@
+// src/api/mes/hw/hardwareinfo.ts
+import request from '@/config/axios'
+
+export interface HardwareVO {
+  id?: number
+  hardwareCode: string
+  hardwareName: string
+  hardwareTypeId?: number
+  workshopId?: number
+  enableFlag?: string
+  remark?: string
+  createBy?: string
+  createTime?: Date
+  updateBy?: string
+  updateTime?: Date
+}
+
+export interface WorkshopVO {
+  id: number
+  workshopCode: string
+  workshopName: string
+  enableFlag?: string
+}
+
+export interface PageParam {
+  pageNo: number
+  pageSize: number
+  hardwareCode?: string
+  hardwareName?: string
+  hardwareTypeId?: number
+  workshopId?: number
+  enableFlag?: string
+}
+
+// 查询硬件-列表
+export const listHardware = async (params: PageParam) => {
+  return await request.get({ url: '/iscs/hardware/getIsHardwarePage', params })
+}
+
+// 获取所有车间
+export const listAllWorkshop = async () => {
+  return await request.get<WorkshopVO[]>({ url: '/mes/md/workshop/listAll' })
+}
+
+// 获取硬件详细信息
+export const getHardwareInfo = async (id: number) => {
+  return await request.get<HardwareVO>({
+    url: '/iscs/hardware/selectIsHardwareById',
+    params: { id }
+  })
+}
+
+// 新增硬件信息
+export const addHardware = async (data: HardwareVO) => {
+  return await request.post({ url: '/iscs/hardware/insertIsHardware', data })
+}
+
+// 修改硬件
+export const updateHardware = async (data: HardwareVO) => {
+  return await request.post({ url: '/iscs/hardware/updateIsHardware', data })
+}
+
+// 删除硬件
+export const delHardware = async (id: number) => {
+  return await request.post({
+    url: '/iscs/hardware/deleteIsHardwareByIds',
+    params: { ids: id }
+  })
+}

+ 52 - 0
src/api/hw/hardware/keys/index.ts

@@ -0,0 +1,52 @@
+import request from '@/config/axios'
+
+export interface KeyVO {
+  keyId?: number
+  keyCode: string
+  keyName: string
+  enableFlag?: string
+  remark?: string
+  createBy?: string
+  createTime?: Date
+  updateBy?: string
+  updateTime?: Date
+}
+
+export interface PageParam {
+  pageNo: number
+  pageSize: number
+  keyCode?: string
+  keyName?: string
+  enableFlag?: string
+}
+
+// 查询钥匙-列表
+export const listKeyAPI = async (params: PageParam) => {
+  return await request.get({ url: '/iscs/key/getIsKeyPage', params })
+}
+
+// 获取钥匙详细信息
+export const getKeyInfoAPI = async (keyId: number) => {
+  return await request.get({
+    url: '/iscs/key/selectIsKeyById',
+    params: { keyId }
+  })
+}
+
+// 新增钥匙
+export const addKeyAPI = async (data: KeyVO) => {
+  return await request.post({ url: '/iscs/key/insertIsKey', data })
+}
+
+// 修改钥匙
+export const updateKeyAPI = async (data: KeyVO) => {
+  return await request.post({ url: '/iscs/key/updateIsKey', data })
+}
+
+// 删除钥匙
+export const delKeyAPI = async (keyId: number) => {
+  return await request.post({
+    url: '/iscs/key/deleteIsKeyByKeyIds',
+    params: { keyIds: keyId }
+  })
+}

+ 52 - 0
src/api/hw/hardware/lockCabinet/index.ts

@@ -0,0 +1,52 @@
+import request from '@/config/axios'
+
+export interface LockCabinetVO {
+  cabinetId?: number
+  cabinetCode: string
+  cabinetName: string
+  enableFlag?: string
+  remark?: string
+  createBy?: string
+  createTime?: Date
+  updateBy?: string
+  updateTime?: Date
+}
+
+export interface PageParam {
+  pageNo: number
+  pageSize: number
+  cabinetCode?: string
+  cabinetName?: string
+  enableFlag?: string
+}
+
+// 查询锁控机柜-列表
+export const getIsLockCabinetPage = async (params: PageParam) => {
+  return await request.get({ url: '/iscs/cabinet/getIsLockCabinetPage', params })
+}
+
+// 获取锁控机柜详细信息
+export const selectIsLockCabinetById = async (cabinetId: number) => {
+  return await request.get({
+    url: '/iscs/cabinet/selectIsLockCabinetById',
+    params: { cabinetId }
+  })
+}
+
+// 新增锁控机柜
+export const insertIsLockCabinet = async (data: LockCabinetVO) => {
+  return await request.post({ url: '/iscs/cabinet/insertIsLockCabinet', data })
+}
+
+// 修改锁控机柜信息
+export const updateIsLockCabinet = async (data: LockCabinetVO) => {
+  return await request.post({ url: '/iscs/cabinet/updateIsLockCabinet', data })
+}
+
+// 删除锁控机柜信息
+export const deleteIsLockCabinetByCabinetIds = async (cabinetId: number) => {
+  return await request.post({
+    url: '/iscs/cabinet/deleteIsLockCabinetByCabinetIds',
+    params: { cabinetIds: cabinetId }
+  })
+}

+ 54 - 0
src/api/hw/hardware/lockCabinet/slots.ts

@@ -0,0 +1,54 @@
+import request from '@/config/axios'
+
+export interface LockCabinetSlotVO {
+  slotId?: number
+  cabinetId?: number
+  slotCode: string
+  slotName: string
+  enableFlag?: string
+  remark?: string
+  createBy?: string
+  createTime?: Date
+  updateBy?: string
+  updateTime?: Date
+}
+
+export interface PageParam {
+  pageNo: number
+  pageSize: number
+  slotCode?: string
+  slotName?: string
+  cabinetId?: number
+  enableFlag?: string
+}
+
+// 查询锁控机柜-仓位-列表
+export const getIsLockCabinetSlotsPage = async (params: PageParam) => {
+  return await request.get({ url: '/iscs/slots/getIsLockCabinetSlotsPage', params })
+}
+
+// 获取锁控机柜-仓位详细信息
+export const selectIsLockCabinetSlotsById = async (slotId: number) => {
+  return await request.get({
+    url: '/iscs/slots/selectIsLockCabinetSlotsById',
+    params: { slotId }
+  })
+}
+
+// 新增锁控机柜-仓位
+export const insertIsLockCabinetSlots = async (data: LockCabinetSlotVO) => {
+  return await request.post({ url: '/iscs/slots/insertIsLockCabinetSlots', data })
+}
+
+// 修改锁控机柜-仓位信息
+export const updateIsLockCabinetSlots = async (data: LockCabinetSlotVO) => {
+  return await request.post({ url: '/iscs/slots/updateIsLockCabinetSlots', data })
+}
+
+// 删除锁控机柜-仓位信息
+export const deleteIsLockCabinetSlotsBySlotIds = async (slotId: number) => {
+  return await request.post({
+    url: '/iscs/slots/deleteIsLockCabinetSlotsBySlotIds',
+    params: { slotIds: slotId }
+  })
+}

+ 52 - 0
src/api/hw/hardware/lockset/index.ts

@@ -0,0 +1,52 @@
+import request from '@/config/axios'
+
+export interface LockVO {
+  locksetId?: number
+  locksetCode: string
+  locksetName: string
+  enableFlag?: string
+  remark?: string
+  createBy?: string
+  createTime?: Date
+  updateBy?: string
+  updateTime?: Date
+}
+
+export interface PageParam {
+  pageNo: number
+  pageSize: number
+  locksetCode?: string
+  locksetName?: string
+  enableFlag?: string
+}
+
+// 查询锁具机构-列表
+export const listLocksetAPI = async (params: PageParam) => {
+  return await request.get({ url: '/iscs/lockset/getIsLocksetPage', params })
+}
+
+// 获取锁具机构详细信息
+export const getLocksetInfoAPI = async (locksetId: number) => {
+  return await request.get({
+    url: '/iscs/lockset/selectIsLocksetById',
+    params: { locksetId }
+  })
+}
+
+// 新增锁具机构
+export const addLocksetAPI = async (data: LockVO) => {
+  return await request.post({ url: '/iscs/lockset/insertIsLockset', data })
+}
+
+// 修改锁具机构信息
+export const updateLocksetAPI = async (data: LockVO) => {
+  return await request.post({ url: '/iscs/lockset/updateIsLockset', data })
+}
+
+// 删除锁具机构信息
+export const delLocksetAPI = async (locksetId: number) => {
+  return await request.post({
+    url: '/iscs/lockset/deleteIsLocksetByLocksetIds',
+    params: { locksetIds: locksetId }
+  })
+}

+ 52 - 0
src/api/hw/hardware/padLock/index.ts

@@ -0,0 +1,52 @@
+import request from '@/config/axios'
+
+export interface PadLockVO {
+  lockId?: number
+  lockCode: string
+  lockName: string
+  enableFlag?: string
+  remark?: string
+  createBy?: string
+  createTime?: Date
+  updateBy?: string
+  updateTime?: Date
+}
+
+export interface PageParam {
+  pageNo: number
+  pageSize: number
+  lockCode?: string
+  lockName?: string
+  enableFlag?: string
+}
+
+// 查询挂锁-列表
+export const listPadLockAPI = async (params: PageParam) => {
+  return await request.get({ url: '/iscs/lock/getIsLockPage', params })
+}
+
+// 获取挂锁详细信息
+export const getPadLockInfoAPI = async (lockId: number) => {
+  return await request.get({
+    url: '/iscs/lock/selectIsLockById',
+    params: { lockId }
+  })
+}
+
+// 新增挂锁
+export const addPadLockAPI = async (data: PadLockVO) => {
+  return await request.post({ url: '/iscs/lock/insertIsLock', data })
+}
+
+// 修改挂锁信息
+export const updatePadLockAPI = async (data: PadLockVO) => {
+  return await request.post({ url: '/iscs/lock/updateIsLock', data })
+}
+
+// 删除挂锁信息
+export const delPadLockAPI = async (lockId: number) => {
+  return await request.post({
+    url: '/iscs/lock/deleteIsLockByLockIds',
+    params: { lockIds: lockId }
+  })
+}

+ 52 - 0
src/api/hw/hardware/rfid/index.ts

@@ -0,0 +1,52 @@
+import request from '@/config/axios'
+
+export interface RfidTokenVO {
+  rfidId?: number
+  rfidCode: string
+  rfidName: string
+  enableFlag?: string
+  remark?: string
+  createBy?: string
+  createTime?: Date
+  updateBy?: string
+  updateTime?: Date
+}
+
+export interface PageParam {
+  pageNo: number
+  pageSize: number
+  rfidCode?: string
+  rfidName?: string
+  enableFlag?: string
+}
+
+// 查询锁控机柜-列表
+export const getIsRfidTokenPage = async (params: PageParam) => {
+  return await request.get({ url: '/iscs/token/getIsRfidTokenPage', params })
+}
+
+// 获取锁控机柜详细信息
+export const selectIsRfidTokenById = async (rfidId: number) => {
+  return await request.get({
+    url: '/iscs/token/selectIsRfidTokenById',
+    params: { rfidId }
+  })
+}
+
+// 新增锁控机柜
+export const insertIsRfidToken = async (data: RfidTokenVO) => {
+  return await request.post({ url: '/iscs/token/insertIsRfidToken', data })
+}
+
+// 修改锁控机柜信息
+export const updateIsRfidToken = async (data: RfidTokenVO) => {
+  return await request.post({ url: '/iscs/token/updateIsRfidToken', data })
+}
+
+// 删除锁控机柜信息
+export const deleteIsRfidTokenByRfidIds = async (rfidId: number) => {
+  return await request.post({
+    url: '/iscs/token/deleteIsRfidTokenByRfidIds',
+    params: { rfidIds: rfidId }
+  })
+}

+ 59 - 0
src/api/hw/hardware/workCard/index.ts

@@ -0,0 +1,59 @@
+import request from '@/config/axios'
+
+export interface WorkCardVO {
+  cardId?: number
+  cardCode: string
+  cardName: string
+  userId?: number
+  enableFlag?: string
+  remark?: string
+  createBy?: string
+  createTime?: Date
+  updateBy?: string
+  updateTime?: Date
+}
+
+export interface PageParam {
+  pageNo: number
+  pageSize: number
+  cardCode?: string
+  cardName?: string
+  userId?: number
+  enableFlag?: string
+}
+
+// 查询工作卡分页
+export const getWordCardList = async (params: PageParam) => {
+  return await request.get({ url: '/iscs/card/getIsJobCardPage', params })
+}
+
+// 工卡详细信息
+export const workCardInfo = async (cardId: number) => {
+  return await request.get({
+    url: '/iscs/card/selectIsJobCardById',
+    params: { cardId }
+  })
+}
+
+// 新增工卡
+export const addWorkCard = async (data: WorkCardVO) => {
+  return await request.post({ url: '/iscs/card/insertIsJobCard', data })
+}
+
+// 修改工卡信息
+export const updateWorkCard = async (data: WorkCardVO) => {
+  return await request.post({ url: '/iscs/card/updateIsJobCard', data })
+}
+
+// 删除工卡信息
+export const delWorkCard = async (cardId: number) => {
+  return await request.post({
+    url: '/iscs/card/deleteIsJobCardByCardIds',
+    params: { cardIds: cardId }
+  })
+}
+
+// 人员列表
+export const getUserList = async (params: PageParam) => {
+  return await request.get({ url: '/system/user/list', params })
+}

+ 51 - 0
src/api/hw/type/hardwaretype/index.ts

@@ -0,0 +1,51 @@
+// src/api/mes/hw/hadrwareType.ts
+import request from '@/config/axios'
+
+export interface HardwareTypeVO {
+  id?: number
+  parentTypeId?: number
+  hardwareTypeCode: string
+  hardwareTypeName: string
+  enableFlag?: string
+  remark?: string
+  createBy?: string
+  createTime?: Date
+  updateBy?: string
+  updateTime?: Date
+}
+
+export interface PageParam {
+  pageNo: number
+  pageSize: number
+  hardwareTypeCode?: string
+  hardwareTypeName?: string
+  enableFlag?: string
+}
+
+// 查询硬件类型列表
+export const listHardwareType = async (params: PageParam) => {
+  return await request.get({ url: '/iscs/type/getIsHardwareTypePage', params })
+}
+
+// 获取硬件类型详细信息
+export const getHardwareTypeInfo = async (id: number) => {
+  return await request.get({ url: '/iscs/type/selectIsHardwareTypeById', params: { id } })
+}
+
+// 新增硬件类型
+export const addHardwareType = async (data: HardwareTypeVO) => {
+  return await request.post({ url: '/iscs/type/insertIsHardwareType', data })
+}
+
+// 修改硬件类型
+export const updateHardwareType = async (data: HardwareTypeVO) => {
+  return await request.post({ url: '/iscs/type/updateIsHardwareType', data })
+}
+
+// 删除硬件类型
+export const delHardwareType = async (id: number) => {
+  return await request.post({
+    url: '/iscs/type/deleteIsHardwareTypeByIds',
+    params: { ids: id }
+  })
+}

+ 54 - 0
src/api/hw/type/locksettype/index.ts

@@ -0,0 +1,54 @@
+// src/api/mes/locktype/locktype.ts
+import request from '@/config/axios'
+
+export interface LockTypeVO {
+  locksetTypeId?: number
+  parentTypeId?: number
+  locksetTypeCode: string
+  locksetTypeName: string
+  enableFlag?: string
+  remark?: string
+  createBy?: string
+  createTime?: Date
+  updateBy?: string
+  updateTime?: Date
+}
+
+export interface PageParam {
+  pageNo: number
+  pageSize: number
+  locksetTypeCode?: string
+  locksetTypeName?: string
+  enableFlag?: string
+}
+
+// 查询锁具机构-列表
+export const listLocksetType = async (params: PageParam) => {
+  return await request.get({ url: '/iscs/type/getIsLocksetTypePage', params })
+}
+
+// 获取锁具机构详细信息
+export const getLocksetTypeInfo = async (locksetTypeId: number) => {
+  return await request.get({
+    url: '/iscs/type/selectIsLocksetTypeById',
+    params: { locksetTypeId }
+  })
+}
+
+// 新增锁具机构
+export const addLocksetType = async (data: LockTypeVO) => {
+  return await request.post({ url: '/iscs/type/insertIsLocksetType', data })
+}
+
+// 修改锁具机构信息
+export const updateLocksetType = async (data: LockTypeVO) => {
+  return await request.post({ url: '/iscs/type/updateIsLocksetType', data })
+}
+
+// 删除锁具机构信息
+export const delLocksetType = async (locksetTypeId: number) => {
+  return await request.post({
+    url: '/iscs/type/deleteIsLocksetTypeByLocksetTypeIds',
+    params: { locksetTypeIds: locksetTypeId }
+  })
+}

+ 54 - 0
src/api/hw/type/padLockType/index.ts

@@ -0,0 +1,54 @@
+// src/api/mes/padLockType/padLockType.ts
+import request from '@/config/axios'
+
+export interface PadLockTypeVO {
+  lockTypeId?: number
+  parentTypeId?: number
+  lockTypeCode: string
+  lockTypeName: string
+  enableFlag?: string
+  remark?: string
+  createBy?: string
+  createTime?: Date
+  updateBy?: string
+  updateTime?: Date
+}
+
+export interface PageParam {
+  pageNo: number
+  pageSize: number
+  lockTypeCode?: string
+  lockTypeName?: string
+  enableFlag?: string
+}
+
+// 查询挂锁类型-列表
+export const listpadLockTypeApi = async (params: PageParam) => {
+  return await request.get({ url: '/iscs/lockType/getIsLockTypePage', params })
+}
+
+// 获取挂锁类型详细信息
+export const getpadLockTypeInfoAPI = async (lockTypeId: number) => {
+  return await request.get({
+    url: '/iscs/lockType/selectIsLockTypeById',
+    params: { lockTypeId }
+  })
+}
+
+// 新增挂锁类型
+export const addpadLockTypeApi = async (data: PadLockTypeVO) => {
+  return await request.post({ url: '/iscs/lockType/insertIsLockType', data })
+}
+
+// 修改挂锁类型信息
+export const updatepadLockTypeApi = async (data: PadLockTypeVO) => {
+  return await request.post({ url: '/iscs/lockType/updateIsLockType', data })
+}
+
+// 删除挂锁类型信息
+export const delpadLockTypeApi = async (lockTypeId: number) => {
+  return await request.post({
+    url: '/iscs/lockType/deleteIsLockTypeByLockTypeIds',
+    params: { lockTypeIds: lockTypeId }
+  })
+}

+ 54 - 0
src/api/system/unit/index.ts

@@ -0,0 +1,54 @@
+import request from '@/config/axios'
+
+export interface UnitVO {
+  unitId?: number
+  parentId?: number
+  unitName: string
+  unitType: string
+  orderNum: number
+  status: number
+  remark?: string
+  createTime?: Date
+}
+
+export interface PageParam {
+  current: number
+  size: number
+  unitName?: string
+  unitId?: string
+  status?: number
+}
+
+// 查询单位列表
+export const listUnit = async (params: PageParam) => {
+  return await request.get({ url: '/iscs/unit/getIsUnitPage', params })
+}
+
+
+// 查询单位详情
+export const getUnitInfo = async (id: number) => {
+  return await request.get({ url: '/iscs/unit/selectIsUnitById', params: { unitId: id } })
+}
+
+// 新增单位
+export const addUnit = async (data: UnitVO) => {
+  return await request.post({ url: '/iscs/unit/insertIsUnit', data })
+}
+
+// 修改单位
+export const updateUnit = async (data: UnitVO) => {
+  return await request.post({ url: '/iscs/unit/updateIsUnit', data })
+}
+
+// 删除单位
+export const delUnit = async (id: number) => {
+  return await request.post({
+    url: '/iscs/unit/deleteIsUnitByUnitIds',
+    params: { unitIds: id }
+  })
+}
+
+// 导出单位
+export const exportUnit = async (params: PageParam) => {
+  return await request.download({ url: '/iscs/unit/export', params })
+}

TEMPAT SAMPAH
src/assets/images/MapImage.png


TEMPAT SAMPAH
src/assets/images/accept.png


TEMPAT SAMPAH
src/assets/images/colocked.png


+ 39 - 0
src/assets/images/dark.svg

@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="52px" height="45px" viewBox="0 0 52 45" version="1.1" 
+    xmlns="http://www.w3.org/2000/svg" 
+    xmlns:xlink="http://www.w3.org/1999/xlink">
+    <defs>
+        <filter x="-9.4%" y="-6.2%" width="118.8%" height="122.5%" filterUnits="objectBoundingBox" id="filter-1">
+            <feOffset dx="0" dy="1" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
+            <feGaussianBlur stdDeviation="1" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
+            <feColorMatrix values="0 0 0 0 0   0 0 0 0 0   0 0 0 0 0  0 0 0 0.15 0" type="matrix" in="shadowBlurOuter1" result="shadowMatrixOuter1"></feColorMatrix>
+            <feMerge>
+                <feMergeNode in="shadowMatrixOuter1"></feMergeNode>
+                <feMergeNode in="SourceGraphic"></feMergeNode>
+            </feMerge>
+        </filter>
+        <rect id="path-2" x="0" y="0" width="48" height="40" rx="4"></rect>
+        <filter x="-4.2%" y="-2.5%" width="108.3%" height="110.0%" filterUnits="objectBoundingBox" id="filter-4">
+            <feOffset dx="0" dy="1" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
+            <feGaussianBlur stdDeviation="0.5" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
+            <feColorMatrix values="0 0 0 0 0   0 0 0 0 0   0 0 0 0 0  0 0 0 0.1 0" type="matrix" in="shadowBlurOuter1"></feColorMatrix>
+        </filter>
+    </defs>
+    <g id="配置面板" width="48" height="40" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="setting-copy-2" width="48" height="40" transform="translate(-1190.000000, -136.000000)">
+            <g id="Group-8" width="48" height="40" transform="translate(1167.000000, 0.000000)">
+                <g id="Group-5-Copy-5" filter="url(#filter-1)" transform="translate(25.000000, 137.000000)">
+                    <mask id="mask-3" fill="white">
+                        <use xlink:href="#path-2"></use>
+                    </mask>
+                    <g id="Rectangle-18">
+                        <use fill="black" fill-opacity="1" filter="url(#filter-4)" xlink:href="#path-2"></use>
+                        <use fill="#F0F2F5" fill-rule="evenodd" xlink:href="#path-2"></use>
+                    </g>
+                    <rect id="Rectangle-11" fill="#FFFFFF" mask="url(#mask-3)" x="0" y="0" width="48" height="10"></rect>
+                    <rect id="Rectangle-18" fill="#303648" mask="url(#mask-3)" x="0" y="0" width="16" height="40"></rect>
+                </g>
+            </g>
+        </g>
+    </g>
+</svg>

TEMPAT SAMPAH
src/assets/images/error.png


+ 39 - 0
src/assets/images/light.svg

@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="52px" height="45px" viewBox="0 0 52 45" version="1.1" 
+    xmlns="http://www.w3.org/2000/svg" 
+    xmlns:xlink="http://www.w3.org/1999/xlink">
+    <defs>
+        <filter x="-9.4%" y="-6.2%" width="118.8%" height="122.5%" filterUnits="objectBoundingBox" id="filter-1">
+            <feOffset dx="0" dy="1" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
+            <feGaussianBlur stdDeviation="1" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
+            <feColorMatrix values="0 0 0 0 0   0 0 0 0 0   0 0 0 0 0  0 0 0 0.15 0" type="matrix" in="shadowBlurOuter1" result="shadowMatrixOuter1"></feColorMatrix>
+            <feMerge>
+                <feMergeNode in="shadowMatrixOuter1"></feMergeNode>
+                <feMergeNode in="SourceGraphic"></feMergeNode>
+            </feMerge>
+        </filter>
+        <rect id="path-2" x="0" y="0" width="48" height="40" rx="4"></rect>
+        <filter x="-4.2%" y="-2.5%" width="108.3%" height="110.0%" filterUnits="objectBoundingBox" id="filter-4">
+            <feOffset dx="0" dy="1" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
+            <feGaussianBlur stdDeviation="0.5" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
+            <feColorMatrix values="0 0 0 0 0   0 0 0 0 0   0 0 0 0 0  0 0 0 0.1 0" type="matrix" in="shadowBlurOuter1"></feColorMatrix>
+        </filter>
+    </defs>
+    <g id="配置面板" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="setting-copy-2" transform="translate(-1254.000000, -136.000000)">
+            <g id="Group-8" transform="translate(1167.000000, 0.000000)">
+                <g id="Group-5" filter="url(#filter-1)" transform="translate(89.000000, 137.000000)">
+                    <mask id="mask-3" fill="white">
+                        <use xlink:href="#path-2"></use>
+                    </mask>
+                    <g id="Rectangle-18">
+                        <use fill="black" fill-opacity="1" filter="url(#filter-4)" xlink:href="#path-2"></use>
+                        <use fill="#F0F2F5" fill-rule="evenodd" xlink:href="#path-2"></use>
+                    </g>
+                    <rect id="Rectangle-18" fill="#FFFFFF" mask="url(#mask-3)" x="0" y="0" width="16" height="40"></rect>
+                    <rect id="Rectangle-11" fill="#FFFFFF" mask="url(#mask-3)" x="0" y="0" width="48" height="10"></rect>
+                </g>
+            </g>
+        </g>
+    </g>
+</svg>

TEMPAT SAMPAH
src/assets/images/localSetIcon.jpg


TEMPAT SAMPAH
src/assets/images/localSetSelect.jpg


TEMPAT SAMPAH
src/assets/images/lockImg.png


TEMPAT SAMPAH
src/assets/images/locked.png


TEMPAT SAMPAH
src/assets/images/login-background-back.jpg


TEMPAT SAMPAH
src/assets/images/login-background.jpg


TEMPAT SAMPAH
src/assets/images/marsBg.png


TEMPAT SAMPAH
src/assets/images/marsPoint.png


TEMPAT SAMPAH
src/assets/images/notcolocked.png


TEMPAT SAMPAH
src/assets/images/prepare.png


TEMPAT SAMPAH
src/assets/images/profile.jpg


TEMPAT SAMPAH
src/assets/images/reject.png


TEMPAT SAMPAH
src/assets/images/sopPoint.png


TEMPAT SAMPAH
src/assets/images/sopbgimg.png


TEMPAT SAMPAH
src/assets/images/success.png


TEMPAT SAMPAH
src/assets/images/table.png


TEMPAT SAMPAH
src/assets/images/table_map1.jpg


TEMPAT SAMPAH
src/assets/images/table_map2.jpg


TEMPAT SAMPAH
src/assets/images/table_map3.jpg


TEMPAT SAMPAH
src/assets/images/techonlogy.png


TEMPAT SAMPAH
src/assets/images/ticketType0.png


TEMPAT SAMPAH
src/assets/images/ticketType1.png


TEMPAT SAMPAH
src/assets/images/ticketType2.png


TEMPAT SAMPAH
src/assets/images/ticketType3.png


TEMPAT SAMPAH
src/assets/images/ticketType4.png


TEMPAT SAMPAH
src/assets/images/warn.png


TEMPAT SAMPAH
src/assets/images/workshop.png


TEMPAT SAMPAH
src/assets/images/柜中.png


TEMPAT SAMPAH
src/assets/images/柜外.png


TEMPAT SAMPAH
src/assets/images/警告.png


TEMPAT SAMPAH
src/assets/images/警告2.png


+ 1 - 0
src/config/axios/index.ts

@@ -10,6 +10,7 @@ const request = (option: any) => {
     ...otherOption,
     headers: {
       'Content-Type': headersType || default_headers,
+      'module': 'Windows',
       ...headers
     }
   })

+ 36 - 0
src/router/modules/remaining.ts

@@ -104,6 +104,42 @@ const remainingRouter: AppRouteRecordRaw[] = [
       }
     ]
   },
+  {
+    path: '/dv',
+    component: Layout,
+    name: 'Dv',
+    meta: {
+      hidden: true
+    },
+    children: [
+      {
+        path: 'technology/technologyDetail/CraftDetail',
+        component: () => import('@/views/dv/technology/technologyDetail/CraftDetail.vue'),
+        name: 'TechnologyCraftDetail',
+        meta: {
+          title: '工艺详情',
+          noCache: true,
+          hidden: true,
+          canTo: true,
+          icon: 'ep:view',
+          activeMenu: '/mes/dv/technology/technologyList'
+        }
+      },
+      {
+        path: 'technology/technologyDetail/DeviceDetail',
+        component: () => import('@/views/dv/technology/technologyDetail/DeviceDetail.vue'),
+        name: 'TechnologyDeviceDetail',
+        meta: {
+          title: '设备详情',
+          noCache: true,
+          hidden: true,
+          canTo: true,
+          icon: 'ep:view',
+          activeMenu: '/mes/dv/technology/technologyList'
+        }
+      }
+    ]
+  },
   {
     path: '/dict',
     component: Layout,

+ 54 - 1
src/utils/dict.ts

@@ -244,5 +244,58 @@ 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', // 桥梁类型
+  //============== ISCS - 新增模块 =====================
+  MES_MACHINERY_STATUS = 'mes_machinery_status',
+  POWER_TYPE = 'power_type',
+  POINT_TYPE = 'point_type',
+  SOP_STATUS = 'sop_status',
+  SOP_TYPE = 'sop_type',
+  TICKET_STATUS = 'ticket_status',
+  TICKET_TYPE = 'ticket_type',
+  HARDWARE_STATUS = 'hardware_status',
+  IS_USER_TYPE = 'is_user_type',
+  LOCK_TYPE = 'lock_type',
+  JOB_STATUS = 'job_status',
+  JOB_USER_ROLE = 'job_user_role',
+  MATERIAL_TYPE_STATUS = 'material_type_status',
+  REMINDERS_STATUS = 'reminders_status',
+  CARD_TYPE = 'card_type',
+  ROLE_SCOPE = 'role_scope',
+  TICKET_USER_TYPE = 'ticket_user_type',
+  MODULE = 'module',
+  RECORD_OF_COLLECTION = 'record_of_collection',
+  CHECKING_STATUS = 'checking_status',
+  EXCEPTIONS_STATUS = 'exceptions_status',
+  CHECKS_STATUS = 'checks_status',
+  CABINET_STATUS = 'cabinet_status',
+  FILE_TYPE = 'file_type',
+  MATERIAL_STATUS = 'material_status',
+  MATERIAL_INFO_STATUS = 'material_info_status',
+  MATERIAL_EXCEPTION_STATUS = 'material_exception_status',
+  EXCEPTION_REASON = 'exception_reason',
+  TIMER_PARAMS = 'timer_params',
+  EXCEPTION_TYPE = 'exception_type',
+  INVENTORY_TYPE = 'Inventory_type',
+  MEASURE = 'measure',
+  MAP_TYPE = 'map_type',
+  CLASSIFICATION_OF_EXCEPTIONS = 'classification_of_exceptions',
+  TYPE_OF_EXCEPTION = 'type_of_exception',
+  SEVERITY_LEVEL = 'severity_level',
+  MANUAL_STATUS = 'manual_status',
+  SYS_TYPE = 'sys_type',
+  SWITCH_STATUS = 'switch_status',
+  KEY_STATUS = 'key_status',
+  PADLOCK_STATUS = 'padlock_status',
+  KEY_REASON = 'key_reason',
+  PADLOCK_REASON = 'padlock_reason',
+  JOB_CARD_STATUS = 'job_card_status',
+  JOB_CARD_REASON = 'job_card_reason',
+  ISONLINE_STATUS = 'isOnline_status',
+  CANBINET_STATUS = 'canbinet_status',
+  RFID_TOKEN_TYPE = 'rfid_token_type',
+  RFID_TOKEN_STATUS = 'rfid_token_status',
+  ISOCCUPIED_STATUS = 'isOccupied_status',
+  SLOT_TYPE = 'slot_type',
+  SLOT_STATUS = 'slot_status',
 }

+ 172 - 0
src/views/Basicdata/configuration/ConfigForm.vue

@@ -0,0 +1,172 @@
+<template>
+  <Dialog v-model="dialogVisible" :title="dialogTitle" width="800">
+    <el-form
+      ref="formRef"
+      v-loading="formLoading"
+      :model="formData"
+      :rules="formRules"
+      label-width="80px"
+    >
+      <el-form-item label="参数名称" prop="sysAttrName">
+        <el-input v-model="formData.sysAttrName" placeholder="请输入参数名称" />
+      </el-form-item>
+      <el-form-item label="参数键名" prop="sysAttrKey">
+        <el-input v-model="formData.sysAttrKey" placeholder="请输入参数键名" />
+      </el-form-item>
+      <el-form-item label="参数类型" prop="sysAttrType">
+        <el-select v-model="formData.sysAttrType" clearable placeholder="请选择参数类型">
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.SYS_TYPE)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="键值类型" prop="inputType">
+        <el-radio-group v-model="inputType" @change="handleInputTypeChange">
+          <el-radio label="text">文字</el-radio>
+          <el-radio label="image">图片</el-radio>
+        </el-radio-group>
+      </el-form-item>
+      <el-form-item label="参数键值" prop="sysAttrValue">
+        <UploadImg
+          v-if="inputType === 'image'"
+          :limit="1"
+          :value="formData.sysAttrValue"
+          :fileSize="5"
+          @onUploaded="handleImgUploaded"
+          @onRemoved="handleImgRemoved"
+        />
+        <el-input
+          v-else
+          type="textarea"
+          :rows="4"
+          v-model="formData.sysAttrValue"
+          placeholder="请输入参数键值"
+        />
+      </el-form-item>
+      <el-form-item label="备注" prop="remark">
+        <el-input v-model="formData.remark" placeholder="请输入备注" />
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import * as ConfigApi from '@/api/basic/configuration'
+
+
+defineOptions({ name: 'SystemConfigForm' })
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const dialogTitle = ref('') // 弹窗的标题
+const formLoading = ref(false) // 表单的加载中
+const formType = ref('') // 表单的类型:create - 新增;update - 修改
+const inputType = ref('text') // 输入类型:text - 文字;image - 图片
+const formData = ref({
+  sysAttrId: undefined,
+  sysAttrName: '',
+  sysAttrKey: '',
+  sysAttrType: undefined,
+  sysAttrValue: '',
+  remark: ''
+})
+const formRules = reactive({
+  sysAttrName: [{ required: true, message: '参数名称不能为空', trigger: 'blur' }],
+  sysAttrKey: [{ required: true, message: '参数键名不能为空', trigger: 'blur' }],
+  sysAttrValue: [{ 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 ConfigApi.selectIsSystemAttributeById(id)
+      // 如果值是图片,设置输入类型为图片
+      if (isImageUrl(formData.value.sysAttrValue)) {
+        inputType.value = 'image'
+      }
+    } finally {
+      formLoading.value = false
+    }
+  }
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 校验表单
+  if (!formRef) return
+  const valid = await formRef.value.validate()
+  if (!valid) return
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = formData.value
+    if (formType.value === 'create') {
+      await ConfigApi.insertIsSystemAttribute(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await ConfigApi.updateIsSystemAttribute(data)
+      message.success(t('common.updateSuccess'))
+    }
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    sysAttrId: undefined,
+    sysAttrName: '',
+    sysAttrKey: '',
+    sysAttrType: undefined,
+    sysAttrValue: '',
+    remark: ''
+  }
+  inputType.value = 'text'
+  formRef.value?.resetFields()
+}
+
+/** 处理输入类型变更 */
+const handleInputTypeChange = () => {
+  formData.value.sysAttrValue = ''
+}
+
+/** 处理图片上传 */
+const handleImgUploaded = (imgUrl: any[]) => {
+  formData.value.sysAttrValue = imgUrl[0].url
+}
+
+/** 处理图片移除 */
+const handleImgRemoved = () => {
+  formData.value.sysAttrValue = ''
+}
+
+/** 判断是否为图片URL */
+const isImageUrl = (url: string) => {
+  if (typeof url !== 'string') return false
+  return /\.(jpg|jpeg|png|gif|webp|bmp)$/i.test(url)
+}
+</script>

+ 250 - 0
src/views/Basicdata/configuration/index.vue

@@ -0,0 +1,250 @@
+<template>
+  <ContentWrap>
+    <!-- 搜索工作栏 -->
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="68px"
+    >
+      <el-form-item label="参数名称" prop="sysAttrName">
+        <el-input
+          v-model="queryParams.sysAttrName"
+          placeholder="请输入参数名称"
+          clearable
+          class="!w-240px"
+          @keyup.enter="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="参数键名" prop="sysAttrKey">
+        <el-input
+          v-model="queryParams.sysAttrKey"
+          placeholder="请输入参数键名"
+          clearable
+          class="!w-240px"
+          @keyup.enter="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="参数键值" prop="sysAttrValue">
+        <el-input
+          v-model="queryParams.sysAttrValue"
+          placeholder="请输入参数键值"
+          clearable
+          class="!w-240px"
+          @keyup.enter="handleQuery"
+        />
+      </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="['iscs:attribute:create']"
+        >
+          <Icon icon="ep:plus" class="mr-5px" /> 新增
+        </el-button>
+        <el-button
+          type="danger"
+          plain
+          @click="handleRefreshCache"
+          v-hasPermi="['iscs:attribute:refresh']"
+        >
+          <Icon icon="ep:refresh" class="mr-5px" /> 刷新缓存
+        </el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table v-loading="loading" :data="list" @selection-change="handleSelectionChange">
+      <el-table-column type="selection" width="55" align="center" />
+      <el-table-column label="参数名称" width="210px" prop="sysAttrName" />
+      <el-table-column label="参数键名" prop="sysAttrKey" :show-overflow-tooltip="true" />
+      <el-table-column label="参数类型" width="90" prop="sysAttrType" :show-overflow-tooltip="true">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.SYS_TYPE" :value="scope.row.sysAttrType" />
+        </template>
+      </el-table-column>
+      <el-table-column label="参数键值" prop="sysAttrValue" :show-overflow-tooltip="true">
+        <template #default="scope">
+          <div class="img-box" v-if="isImageUrl(scope.row.sysAttrValue)">
+            <el-image
+              style="width: 50px; height: 50px"
+              :preview-teleported="true"
+              class="images"
+              :hide-on-click-modal="true"
+              :src="scope.row.sysAttrValue"
+              :zoom-rate="1.2"
+              :preview-src-list="[scope.row.sysAttrValue]"
+              :initial-index="1"
+            />
+            <Icon icon="ep:zoom-in" class="eye-icon" />
+          </div>
+          <div v-else>{{ scope.row.sysAttrValue }}</div>
+        </template>
+      </el-table-column>
+      <el-table-column label="备注" prop="remark" width="300" />
+      <el-table-column
+        label="创建时间"
+        align="center"
+        prop="createTime"
+        width="180"
+        :formatter="dateFormatter"
+      />
+      <el-table-column label="操作" align="center">
+        <template #default="scope">
+          <el-button
+            link
+            type="primary"
+            @click="openForm('update', scope.row.sysAttrId)"
+            v-hasPermi="['iscs:attribute:update']"
+          >
+            编辑
+          </el-button>
+          <el-button
+            link
+            type="danger"
+            @click="handleDelete(scope.row.sysAttrId)"
+            v-hasPermi="['iscs:attribute: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>
+
+  <!-- 表单弹窗:添加/修改 -->
+  <ConfigForm ref="formRef" @success="getList" />
+</template>
+
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import * as ConfigApi from '@/api/basic/configuration'
+import ConfigForm from './ConfigForm.vue'
+
+
+defineOptions({ name: 'SystemConfig' })
+
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
+
+const loading = ref(true) // 列表的加载中
+const total = ref(0) // 列表的总页数
+const list = ref([]) // 列表的数据
+const ids = ref<number[]>([]) // 选中的数据
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  sysAttrName: undefined,
+  sysAttrKey: undefined,
+  sysAttrValue: undefined
+})
+const queryFormRef = ref() // 搜索的表单
+
+/** 查询配置列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await ConfigApi.getIsSystemAttributePage(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 ConfigApi.deleteIsSystemAttributeByIds(id)
+    message.success(t('common.delSuccess'))
+    await getList()
+  } catch {}
+}
+
+/** 刷新缓存操作 */
+const handleRefreshCache = async () => {
+  try {
+    await ConfigApi.refreshAttrCache()
+    message.success('刷新成功')
+  } catch {}
+}
+
+/** 多选框选中数据 */
+const handleSelectionChange = (selection: any[]) => {
+  ids.value = selection.map(item => item.sysAttrId)
+}
+
+/** 判断是否为图片URL */
+const isImageUrl = (url: string) => {
+  if (typeof url !== 'string') return false
+  return /\.(jpg|jpeg|png|gif|webp|bmp)$/i.test(url)
+}
+
+/** 初始化 **/
+onMounted(() => {
+  getList()
+})
+</script>
+
+<style lang="scss" scoped>
+.img-box {
+  width: 50px;
+  height: 50px;
+  position: relative;
+
+  .eye-icon {
+    display: none;
+  }
+
+  &:hover {
+    background: #000;
+
+    .images {
+      opacity: 0.6;
+    }
+
+    .eye-icon {
+      display: block;
+      position: absolute;
+      top: 40%;
+      left: 30%;
+      z-index: 100;
+      color: white;
+      pointer-events: none;
+    }
+  }
+}
+</style>

+ 138 - 0
src/views/Basicdata/mapconfig/MapConfigForm.vue

@@ -0,0 +1,138 @@
+<template>
+  <Dialog v-model="dialogVisible" :title="dialogTitle" width="800">
+    <el-form
+      ref="formRef"
+      v-loading="formLoading"
+      :model="formData"
+      :rules="formRules"
+      label-width="80px"
+    >
+      <el-form-item label="地图名称" prop="name">
+        <el-input v-model="formData.name" placeholder="请输入地图名称" />
+      </el-form-item>
+      <el-form-item label="地图图片" prop="imageUrl">
+        <UploadImg
+          :limit="1"
+          :value="formData.imageUrl"
+          :fileSize="5"
+          @onUploaded="handleImgUploaded"
+          @onRemoved="handleImgRemoved"
+        />
+      </el-form-item>
+      <el-form-item label="图片宽度" prop="width">
+        <el-input v-model="formData.width" placeholder="请输入图片宽度" />
+      </el-form-item>
+      <el-form-item label="图片高度" prop="height">
+        <el-input v-model="formData.height" placeholder="请输入图片高度" />
+      </el-form-item>
+      <el-form-item label="横坐标" prop="x">
+        <el-input v-model="formData.x" placeholder="请输入横坐标" />
+      </el-form-item>
+      <el-form-item label="纵坐标" prop="y">
+        <el-input v-model="formData.y" placeholder="请输入纵坐标" />
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+
+<script lang="ts" setup>
+import * as MapConfigApi from '@/api/basic/mapconfig'
+
+
+
+defineOptions({ name: 'SystemMapForm' })
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const dialogTitle = ref('') // 弹窗的标题
+const formLoading = ref(false) // 表单的加载中
+const formType = ref('') // 表单的类型:create - 新增;update - 修改
+const formData = ref({
+  id: undefined,
+  name: '',
+  imageUrl: '',
+  width: undefined,
+  height: undefined,
+  x: undefined,
+  y: undefined
+})
+const formRules = reactive({
+  name: [{ required: true, message: '地图名称不能为空', trigger: 'blur' }],
+  imageUrl: [{ 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 MapConfigApi.selectIsMapById(id)
+    } finally {
+      formLoading.value = false
+    }
+  }
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 校验表单
+  if (!formRef) return
+  const valid = await formRef.value.validate()
+  if (!valid) return
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = formData.value
+    if (formType.value === 'create') {
+      await MapConfigApi.insertIsMap(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await MapConfigApi.updateIsMap(data)
+      message.success(t('common.updateSuccess'))
+    }
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: undefined,
+    name: '',
+    imageUrl: '',
+    width: undefined,
+    height: undefined,
+    x: undefined,
+    y: undefined
+  }
+  formRef.value?.resetFields()
+}
+
+/** 处理图片上传 */
+const handleImgUploaded = (imgUrl: any[]) => {
+  formData.value.imageUrl = imgUrl[0].url
+}
+
+/** 处理图片移除 */
+const handleImgRemoved = () => {
+  formData.value.imageUrl = ''
+}
+</script>

+ 217 - 0
src/views/Basicdata/mapconfig/index.vue

@@ -0,0 +1,217 @@
+<template>
+  <ContentWrap>
+    <!-- 搜索工作栏 -->
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="68px"
+    >
+      <el-form-item label="地图名称" prop="name">
+        <el-input
+          v-model="queryParams.name"
+          placeholder="请输入地图名称"
+          clearable
+          class="!w-240px"
+          @keyup.enter="handleQuery"
+        />
+      </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="['iscs:map:create']"
+        >
+          <Icon icon="ep:plus" class="mr-5px" /> 新增
+        </el-button>
+        <el-button
+          type="danger"
+          plain
+          :disabled="multiple"
+          @click="handleDelete"
+          v-hasPermi="['iscs:map:delete']"
+        >
+          <Icon icon="ep:delete" class="mr-5px" /> 删除
+        </el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table v-loading="loading" :data="list" @selection-change="handleSelectionChange">
+      <el-table-column type="selection" width="55" align="center" />
+      <el-table-column label="地图编号" align="center" prop="id" />
+      <el-table-column label="地图名称" align="center" prop="name" />
+      <el-table-column label="地图图片" align="left" prop="imageUrl" :show-overflow-tooltip="true">
+        <template #default="scope">
+          <div class="img-box" v-if="scope.row.imageUrl">
+            <el-image
+              style="width: 75px; height: 75px"
+              :preview-teleported="true"
+              class="images"
+              :hide-on-click-modal="true"
+              :src="scope.row.imageUrl"
+              :zoom-rate="1.2"
+              :preview-src-list="[scope.row.imageUrl]"
+              :initial-index="1"
+            />
+            <Icon icon="ep:zoom-in" class="eye-icon" />
+          </div>
+          <span v-else>-</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="地图宽度" align="center" prop="width" />
+      <el-table-column label="地图高度" align="center" prop="height" />
+      <el-table-column label="横坐标" align="center" prop="x" />
+      <el-table-column label="纵坐标" align="center" prop="y" />
+      <el-table-column
+        label="创建时间"
+        align="center"
+        prop="createTime"
+        width="180"
+        :formatter="dateFormatter"
+      />
+      <el-table-column label="操作" align="center">
+        <template #default="scope">
+          <el-button
+            link
+            type="primary"
+            @click="openForm('update', scope.row.id)"
+            v-hasPermi="['iscs:map:update']"
+          >
+            编辑
+          </el-button>
+          <el-button
+            link
+            type="danger"
+            @click="handleDelete(scope.row.id)"
+            v-hasPermi="['iscs:map: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>
+
+  <!-- 表单弹窗:添加/修改 -->
+  <MapConfigForm ref="formRef" @success="getList" />
+</template>
+
+<script lang="ts" setup>
+import { dateFormatter } from '@/utils/formatTime'
+import * as MapConfigApi from '@/api/basic/mapconfig'
+import {deleteIsMapByIds, getIsMapPage} from "@/api/basic/mapconfig";
+
+
+
+defineOptions({ name: 'SystemMap' })
+
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
+
+const loading = ref(true) // 列表的加载中
+const total = ref(0) // 列表的总页数
+const list = ref([]) // 列表的数据
+const ids = ref<number[]>([]) // 选中的数据
+const multiple = ref(true) // 非多个禁用
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  name: undefined
+})
+const queryFormRef = ref() // 搜索的表单
+
+/** 查询地图列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await MapConfigApi.getIsMapPage(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 MapConfigApi.deleteIsMapByIds(id)
+    message.success(t('common.delSuccess'))
+    await getList()
+  } catch {}
+}
+
+/** 多选框选中数据 */
+const handleSelectionChange = (selection: any[]) => {
+  ids.value = selection.map(item => item.id)
+  multiple.value = !selection.length
+}
+
+/** 初始化 **/
+onMounted(() => {
+  getList()
+})
+</script>
+
+<style lang="scss" scoped>
+.img-box {
+  width: 75px;
+  height: 75px;
+  position: relative;
+
+  .eye-icon {
+    display: none;
+  }
+
+  &:hover {
+    background: #000;
+
+    .images {
+      opacity: 0.6;
+    }
+
+    .eye-icon {
+      display: block;
+      position: absolute;
+      top: 40%;
+      left: 40%;
+      z-index: 100;
+      color: white;
+      pointer-events: none;
+    }
+  }
+}
+</style>

+ 171 - 0
src/views/Basicdata/mappoint/MapPointForm.vue

@@ -0,0 +1,171 @@
+<template>
+  <Dialog v-model="dialogVisible" :title="dialogTitle" width="800">
+    <el-form
+      ref="formRef"
+      v-loading="formLoading"
+      :model="formData"
+      :rules="formRules"
+      label-width="80px"
+    >
+      <el-form-item label="地图名称" prop="mapId">
+        <el-select v-model="formData.mapId" placeholder="地图名称" class="!w-280px">
+          <el-option
+            v-for="item in mapOptions"
+            :key="item.id"
+            :label="item.name"
+            :value="item.id"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="地图类型" prop="mapType">
+        <el-select v-model="formData.mapType" placeholder="地图类型" class="!w-280px">
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.MAP_TYPE)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item v-if="formData.mapType === '1'" label="实体" prop="entityId">
+        <el-tree-select
+          v-model="formData.entityId"
+          :data="deptOptions"
+          :props="{
+            value: 'workstationId',
+            label: 'workstationName',
+            children: 'children'
+          }"
+          placeholder="选择实体"
+          class="!w-280px"
+          @change="handleWorkstationChange"
+        />
+      </el-form-item>
+      <el-form-item v-if="formData.mapType === '2'" label="实体" prop="entityId">
+        <el-select v-model="formData.entityId" placeholder="实体" class="!w-280px">
+          <el-option
+            v-for="item in spmOptions"
+            :key="item.pointId"
+            :label="item.pointName"
+            :value="item.pointId"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="横坐标" prop="x">
+        <el-input v-model="formData.x" placeholder="请输入横坐标" />
+      </el-form-item>
+      <el-form-item label="纵坐标" prop="y">
+        <el-input v-model="formData.y" placeholder="请输入纵坐标" />
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import * as MapPointApi from '@/api/basic/mappoint'
+import {insertIsMapPoint, selectIsMapPointById, updateIsMapPoint} from "@/api/basic/mappoint";
+
+defineOptions({ name: 'SystemMapPointForm' })
+
+interface DeptOption {
+  workstationId: number
+  workstationName: string
+  children?: DeptOption[]
+}
+
+interface Props {
+  mapOptions: Array<{ id: number; name: string }>
+  deptOptions: DeptOption[]
+  spmOptions: Array<{ pointId: number; pointName: string }>
+}
+
+const props = defineProps<Props>()
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const dialogTitle = ref('') // 弹窗的标题
+const formLoading = ref(false) // 表单的加载中
+const formType = ref('') // 表单的类型:create - 新增;update - 修改
+const formData = ref({
+  id: undefined as number | undefined,
+  mapId: undefined as number | undefined,
+  mapType: undefined as string | undefined,
+  entityId: undefined as number | undefined,
+  x: undefined as number | undefined,
+  y: undefined as number | undefined
+})
+const formRules = reactive({
+  mapId: [{ required: true, message: '地图名称不能为空', trigger: 'blur' }],
+  mapType: [{ 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 MapPointApi.selectIsMapPointById(id)
+    } finally {
+      formLoading.value = false
+    }
+  }
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 校验表单
+  if (!formRef) return
+  const valid = await formRef.value.validate()
+  if (!valid) return
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = formData.value
+    if (formType.value === 'create') {
+      await MapPointApi.insertIsMapPoint(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await MapPointApi.updateIsMapPoint(data)
+      message.success(t('common.updateSuccess'))
+    }
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: undefined,
+    mapId: undefined,
+    mapType: undefined,
+    entityId: undefined,
+    x: undefined,
+    y: undefined
+  }
+  formRef.value?.resetFields()
+}
+
+/** 处理岗位变更 */
+const handleWorkstationChange = (value: number) => {
+  formData.value.entityId = value
+}
+</script>

+ 207 - 0
src/views/Basicdata/mappoint/index.vue

@@ -0,0 +1,207 @@
+<template>
+  <ContentWrap>
+    <!-- 搜索工作栏 -->
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="68px"
+    >
+      <el-form-item label="地图名称" prop="mapId">
+        <el-select v-model="queryParams.mapId" placeholder="地图名称" class="!w-240px">
+          <el-option
+            v-for="item in mapOptions"
+            :key="item.id"
+            :label="item.name"
+            :value="item.id"
+          />
+        </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="['iscs:map-point:create']"
+        >
+          <Icon icon="ep:plus" class="mr-5px" /> 新增
+        </el-button>
+        <el-button
+          type="danger"
+          plain
+          :disabled="multiple"
+          @click="handleDelete"
+          v-hasPermi="['iscs:map-point:delete']"
+        >
+          <Icon icon="ep:delete" class="mr-5px" /> 删除
+        </el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table v-loading="loading" :data="list" @selection-change="handleSelectionChange">
+      <el-table-column type="selection" width="55" align="center" />
+      <el-table-column label="地图名称" align="center" width="210px" prop="mapName" />
+      <el-table-column label="地图类型" align="center" prop="mapType" :show-overflow-tooltip="true">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.MAP_TYPE" :value="scope.row.mapType" />
+        </template>
+      </el-table-column>
+      <el-table-column label="实体名称" align="center" width="210px" prop="entityName" />
+      <el-table-column label="横坐标" align="center" prop="x" width="180" />
+      <el-table-column label="纵坐标" align="center" prop="y" width="180" />
+      <el-table-column
+        label="创建时间"
+        align="center"
+        prop="createTime"
+        width="180"
+        :formatter="dateFormatter"
+      />
+      <el-table-column label="操作" align="center">
+        <template #default="scope">
+          <el-button
+            link
+            type="primary"
+            @click="openForm('update', scope.row.id)"
+            v-hasPermi="['iscs:map-point:update']"
+          >
+            编辑
+          </el-button>
+          <el-button
+            link
+            type="danger"
+            @click="handleDelete(scope.row.id)"
+            v-hasPermi="['iscs:map-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>
+
+  <!-- 表单弹窗:添加/修改 -->
+  <MapPointForm
+    ref="formRef"
+    :map-options="mapOptions"
+    :dept-options="deptOptions"
+    :spm-options="spmOptions"
+    @success="getList"
+  />
+</template>
+
+<script lang="ts" setup>
+import { DICT_TYPE } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import * as MapPointApi from '@/api/basic/mappoint'
+import * as MapApi from '@/api/basic/mapconfig'
+import * as PostApi from '@/api/system/post'
+import * as SpmApi from '@/api/dv/spm/index'
+import MapPointForm from './MapPointForm.vue'
+
+
+
+defineOptions({ name: 'SystemMapPoint' })
+
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
+
+const loading = ref(true) // 列表的加载中
+const total = ref(0) // 列表的总页数
+const list = ref([]) // 列表的数据
+const ids = ref<number[]>([]) // 选中的数据
+const multiple = ref(true) // 非多个禁用
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  mapId: undefined
+})
+
+// 下拉选项数据
+const mapOptions = ref([]) // 地图选项
+const deptOptions = ref([]) // 岗位选项
+const spmOptions = ref([]) // 隔离点选项
+
+const queryFormRef = ref() // 搜索的表单
+
+/** 查询地图点位列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await MapPointApi.getIsMapPointPage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 获取其他列表数据 */
+const getOtherList = async () => {
+  const params = { current: 1, size: -1 }
+  try {
+    const [mapRes, deptRes, spmRes] = await Promise.all([
+      MapApi.getIsMapPage(params),
+      MarsDeptApi.listMarsDept(params),
+      SpmApi.getIsIsolationPointPage(params)
+    ])
+    mapOptions.value = mapRes.data.records
+    deptOptions.value = handleTree(deptRes.data.records, 'workstationId', 'parentId')
+    spmOptions.value = spmRes.data.records
+  } catch (error) {
+    console.error('获取下拉选项数据失败:', error)
+  }
+}
+
+/** 搜索按钮操作 */
+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 MapPointApi.deleteIsMapPointByIds(id)
+    message.success(t('common.delSuccess'))
+    await getList()
+  } catch {}
+}
+
+/** 多选框选中数据 */
+const handleSelectionChange = (selection: any[]) => {
+  ids.value = selection.map(item => item.id)
+  multiple.value = !selection.length
+}
+
+/** 初始化 **/
+onMounted(() => {
+  getList()
+  getOtherList()
+})
+</script>

+ 40 - 0
src/views/dv/lotoStation/LookDetail.vue

@@ -0,0 +1,40 @@
+<template>
+  <ContentWrap>
+    <el-radio-group v-model="tabPosition" class="mb-15px">
+      <el-radio-button label="first">锁定站</el-radio-button>
+      <el-radio-button label="second">开关状态</el-radio-button>
+      <el-radio-button label="third">隔离点列表</el-radio-button>
+    </el-radio-group>
+
+    <component :is="currentComponent" :loto-id="lotoId" />
+  </ContentWrap>
+</template>
+
+<script lang="ts" setup>
+import { ref, computed } from 'vue'
+import { useRoute } from 'vue-router'
+import MapData from './MapData.vue'
+import PointList from './PointList.vue'
+import SwitchStatus from './SwitchStatus.vue'
+
+defineOptions({ name: 'LotoStationDetail' })
+
+const route = useRoute()
+const tabPosition = ref('first')
+const lotoId = ref(route.query.lotoId as string)
+
+const currentComponent = computed(() => {
+  const components = {
+    third: PointList,
+    first: MapData,
+    second: SwitchStatus
+  }
+  return components[tabPosition.value]
+})
+</script>
+
+<style scoped lang="scss">
+.mb-15px {
+  margin-bottom: 15px;
+}
+</style>

+ 188 - 0
src/views/dv/lotoStation/LotoStationForm.vue

@@ -0,0 +1,188 @@
+<template>
+  <Dialog v-model="dialogVisible" :title="dialogTitle" width="450">
+    <el-form
+      ref="formRef"
+      v-loading="formLoading"
+      :model="formData"
+      :rules="formRules"
+      label-width="120px"
+    >
+      <el-form-item label="锁定站名称" prop="lotoName">
+        <el-input
+          v-model="formData.lotoName"
+          placeholder="请输入锁定站名称"
+        />
+      </el-form-item>
+      <el-form-item label="排序" prop="orderNum">
+        <el-input-number v-model="formData.orderNum" />
+      </el-form-item>
+      <el-form-item label="岗位" prop="workstationId">
+        <el-tree-select
+          v-model="formData.workstationId"
+          :data="marsOptions"
+          :props="{ label: 'workstationName', value: 'workstationId', children: 'children' }"
+          placeholder="选择岗位"
+        />
+      </el-form-item>
+      <el-form-item label="所属硬件" prop="lotoSerialNumber">
+        <el-select
+          v-model="formData.lotoSerialNumber"
+          placeholder="请选择所属硬件"
+          clearable
+        >
+          <el-option
+            v-for="dict in hardWareList"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="地图名称" prop="mapId">
+        <el-select
+          v-model="formData.mapId"
+          placeholder="地图名称"
+        >
+          <el-option
+            v-for="item in mapOptions"
+            :key="item.id"
+            :label="item.name"
+            :value="item.id"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="锁定站信息" prop="map">
+        <el-input
+          v-model="formData.map"
+          placeholder="请输入锁定站信息"
+        />
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+
+<script lang="ts" setup>
+import { DICT_TYPE } from '@/utils/dict'
+import * as LotoStationApi from '@/api/dv/lotoStation/index'
+// import { genCode } from '@/api/system/autocode/rule'
+import { getIsMapPage } from '@/api/basic/mapconfig/index'
+import { listHardware } from '@/api/hw/hardware/information/index'
+// import { listMarsDept } from '@/api/system/marsdept'
+
+defineOptions({ name: 'LotoStationForm' })
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const dialogTitle = ref('') // 弹窗的标题
+const formLoading = ref(false) // 表单的加载中
+const formType = ref('') // 表单的类型:create - 新增;update - 修改
+const autoGenFlag = ref(false) // 是否自动生成编码
+
+const formData = ref({
+  lotoId: undefined,
+  lotoName: '',
+  orderNum: 0,
+  workstationId: undefined,
+  lotoSerialNumber: undefined,
+  mapId: undefined,
+  map: ''
+})
+
+const formRules = reactive({
+  lotoName: [{ required: true, message: '锁定站名称不能为空', trigger: 'blur' }],
+  workstationId: [{ required: true, message: '岗位不能为空', trigger: 'blur' }]
+})
+
+const formRef = ref() // 表单 Ref
+const marsOptions = ref([]) // 岗位树选项
+const mapOptions = ref([]) // 地图选项
+const hardWareList = 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 LotoStationApi.selectIsLotoStationById(id)
+    } finally {
+      formLoading.value = false
+    }
+  }
+  // 获取岗位数据
+  const deptRes = await listMarsDept({ current: 1, size: -1 })
+  marsOptions.value = handleTree(deptRes.data.records, 'workstationId', 'parentId')
+
+  // 获取地图数据
+  const mapRes = await getIsMapPage({ current: 1, size: -1 })
+  mapOptions.value = mapRes.data.records
+
+  // 获取硬件数据
+  const hwRes = await listHardware({ current: 1, size: -1 })
+  hardWareList.value = hwRes.data.records.map(item => ({
+    value: item.serialNumber,
+    label: item.hardwareName
+  }))
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 校验表单
+  if (!formRef) return
+  const valid = await formRef.value.validate()
+  if (!valid) return
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = formData.value
+    if (formType.value === 'create') {
+      await LotoStationApi.addLoto(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await LotoStationApi.updateLoto(data)
+      message.success(t('common.updateSuccess'))
+    }
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    lotoId: undefined,
+    lotoName: '',
+    orderNum: 0,
+    workstationId: undefined,
+    lotoSerialNumber: undefined,
+    mapId: undefined,
+    map: ''
+  }
+  autoGenFlag.value = false
+  formRef.value?.resetFields()
+}
+
+/** 自动生成编码 */
+const handleAutoGenChange = async (value: boolean) => {
+  if (value) {
+    formData.value.lotoCode = await genCode('LOTO_STATION_CODE')
+  } else {
+    formData.value.lotoCode = ''
+  }
+}
+</script>

+ 234 - 0
src/views/dv/lotoStation/MapData.vue

@@ -0,0 +1,234 @@
+<template>
+  <ContentWrap>
+    <div class="map-container">
+      <v-stage
+        ref="stage"
+        :config="stageConfig"
+        @mousedown="handleMouseDown"
+        @mousemove="handleMouseMove"
+        @mouseup="handleMouseUp"
+      >
+        <v-layer ref="layer">
+          <!-- 背景图 -->
+          <v-image
+            v-if="backgroundImage"
+            :config="{
+              x: x,
+              y: y,
+              image: backgroundImage,
+              width: width,
+              height: height
+            }"
+          />
+
+          <!-- 网格 -->
+          <v-line
+            v-for="(line, index) in gridLines"
+            :key="'grid-' + index"
+            :config="line"
+          />
+
+          <!-- 点位 -->
+          <v-group
+            v-for="point in points"
+            :key="point.id"
+            :config="{
+              x: point.x * 50,
+              y: point.y * 50,
+              draggable: true
+            }"
+            @dragend="handleDragEnd($event, point)"
+          >
+            <v-rect
+              :config="{
+                x: 0,
+                y: 0,
+                width: 50,
+                height: 70,
+                cornerRadius: 5,
+                stroke: 'red',
+                strokeWidth: 2,
+                fill: 'white'
+              }"
+            />
+            <v-image
+              :config="{
+                x: 1,
+                y: 5,
+                image: point.image,
+                width: 45,
+                height: 45
+              }"
+            />
+            <v-text
+              :config="{
+                x: 8,
+                y: 50,
+                text: point.name,
+                fontSize: 17,
+                fill: 'red'
+              }"
+            />
+          </v-group>
+        </v-layer>
+      </v-stage>
+
+      <!-- 操作按钮 -->
+      <div class="operation-buttons">
+        <el-button type="primary" @click="handleSave">
+          <Icon icon="ep:check" class="mr-5px" /> 保存
+        </el-button>
+        <el-button type="primary" @click="handleReset">
+          <Icon icon="ep:refresh" class="mr-5px" /> 重置
+        </el-button>
+      </div>
+    </div>
+  </ContentWrap>
+</template>
+
+<script lang="ts" setup>
+import { ref, onMounted, reactive } from 'vue'
+import { useRoute } from 'vue-router'
+import { useMessage } from '@/hooks/web/useMessage'
+import { useI18n } from '@/hooks/web/useI18n'
+import * as LotoStationApi from '@/api/dv/lotoStation/index'
+import * as MapApi from '@/api/basic/mapconfig'
+import * as PointApi from '@/api/dv/spm/index'
+
+
+defineOptions({ name: 'LotoStationMap' })
+
+const { t } = useI18n()
+const message = useMessage()
+const route = useRoute()
+
+// 状态定义
+const stage = ref(null)
+const layer = ref(null)
+const backgroundImage = ref(null)
+const points = ref([])
+const gridLines = ref([])
+
+// 配置
+const stageConfig = reactive({
+  width: 1600,
+  height: 860
+})
+
+// 数据
+const mapData = reactive({
+  x: 0,
+  y: 0,
+  width: 0,
+  height: 0,
+  mapId: null,
+  mapType: 2
+})
+
+// 方法
+const initGrid = () => {
+  const lines = []
+  const cellSize = 50
+
+  // 绘制竖线
+  for (let i = 0; i <= stageConfig.width; i += cellSize) {
+    lines.push({
+      points: [i, 0, i, stageConfig.height],
+      stroke: '#e0e0e0',
+      strokeWidth: 1
+    })
+  }
+
+  // 绘制横线
+  for (let j = 0; j <= stageConfig.height; j += cellSize) {
+    lines.push({
+      points: [0, j, stageConfig.width, j],
+      stroke: '#e0e0e0',
+      strokeWidth: 1
+    })
+  }
+
+  gridLines.value = lines
+}
+
+const loadMapData = async () => {
+  const lotoId = route.query.lotoId
+  try {
+    // 获取锁定站信息
+    const lotoRes = await LotoStationApi.selectIsLotoStationById(lotoId)
+    mapData.mapId = lotoRes.data.mapId
+
+    // 获取地图信息
+    const mapRes = await MapApi.selectIsMapById(mapData.mapId)
+    const mapData = mapRes.data
+    backgroundImage.value = await loadImage(mapData.imageUrl)
+    mapData.x = mapData.x
+    mapData.y = mapData.y
+    mapData.width = mapData.width
+    mapData.height = mapData.height
+
+    // 获取点位信息
+    const pointsRes = await PointApi.getIsIsolationPointPage({ mapId: mapData.mapId })
+    points.value = pointsRes.data.map(point => ({
+      ...point,
+      image: await loadImage(point.pointIcon)
+    }))
+
+    initGrid()
+  } catch (error) {
+    message.error(t('common.loadError'))
+  }
+}
+
+const loadImage = (url: string): Promise<HTMLImageElement> => {
+  return new Promise((resolve, reject) => {
+    const img = new Image()
+    img.onload = () => resolve(img)
+    img.onerror = reject
+    img.src = url
+  })
+}
+
+const handleDragEnd = (e: any, point: any) => {
+  // 处理拖拽结束逻辑
+  const pos = e.target.position()
+  point.x = Math.round(pos.x / 50)
+  point.y = Math.round(pos.y / 50)
+}
+
+const handleSave = async () => {
+  try {
+    await LotoStationApi.updateLoto({
+      lotoId: route.query.lotoId,
+      points: points.value
+    })
+    message.success(t('common.saveSuccess'))
+  } catch (error) {
+    message.error(t('common.saveError'))
+  }
+}
+
+const handleReset = () => {
+  loadMapData()
+}
+
+// 生命周期
+onMounted(() => {
+  loadMapData()
+})
+</script>
+
+<style scoped lang="scss">
+.map-container {
+  position: relative;
+  width: 100%;
+  height: 700px;
+
+  .operation-buttons {
+    position: absolute;
+    top: 20px;
+    right: 20px;
+    z-index: 1;
+  }
+}
+</style>

+ 259 - 0
src/views/dv/lotoStation/PointList.vue

@@ -0,0 +1,259 @@
+<template>
+  <div class="app-container">
+    <el-table
+      height="740"
+      v-loading="loading"
+      :data="isolationList"
+      @selection-change="handleSelectionChange"
+    >
+      <el-table-column type="selection" width="55" align="center"/>
+      <el-table-column label="隔离点编号" align="center" prop="pointCode" width="100">
+        <template #default="scope">
+          <el-button
+            v-no-more-click
+            type="text"
+            @click="handleView(scope.row)"
+            v-hasPermi="['iscs:point:list']"
+          >{{ scope.row.pointCode }}
+          </el-button>
+        </template>
+      </el-table-column>
+      <el-table-column label="隔离点名称" align="center" prop="pointName"/>
+      <el-table-column
+        label="隔离点图标"
+        align="center"
+        prop="pointIcon"
+        width="90"
+      >
+        <template #default="scope">
+          <img
+            v-if="scope.row.pointIcon"
+            :src="scope.row.pointIcon"
+            alt=""
+            style="width: 50px; height: 50px"
+          />
+          <span v-else>-</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="开关状态" align="center" prop="switchStatus">
+        <template #default="scope">
+          <el-switch
+            style="pointer-events: none;"
+            v-if="scope.row.switchStatus!==null"
+            v-model="scope.row.switchStatus"
+            active-value="1"
+            inactive-value="0"
+            active-color="#13ce66"
+            inactive-color="grey"
+          ></el-switch>
+          <span v-else>-</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="隔离点NFC" align="center" prop="pointNfc" />
+      <el-table-column label="岗位" align="center" prop="workstationName" />
+      <el-table-column label="设备/工艺" align="center" prop="machineryName" />
+      <el-table-column label="锁定站" align="center" prop="lotoName" />
+      <el-table-column label="隔离点序列号" align="center" prop="pointSerialNumber" />
+      <el-table-column label="作用" align="center" prop="remark" />
+      <el-table-column
+        label="隔离点图片"
+        align="center"
+        prop="pointPicture"
+        width="90"
+      >
+        <template #default="scope">
+          <img
+            v-if="scope.row.pointPicture"
+            :src="scope.row.pointPicture"
+            alt=""
+            style="width: 50px; height: 50px"
+          />
+          <span v-else>-</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="能量源" align="center" prop="powerType">
+        <template #default="scope">
+          <dict-tag
+            :options="dict.type.power_type"
+            :value="scope.row.powerType"
+          />
+        </template>
+      </el-table-column>
+    </el-table>
+    <pagination
+      v-show="total > 0"
+      :total="total"
+      v-model:page="queryParams.current"
+      v-model:limit="queryParams.size"
+      @pagination="getList"
+    />
+  </div>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted, watch } from 'vue'
+import { useDict } from '@/utils/dict'
+import {
+  getIsIsolationPointPage,
+  selectIsIsolationPointById,
+  addinsertIsIsolationPoint,
+  updateIsIsolationPoint,
+  deleteIsIsolationPointByPointIds,
+  getWorkshopList,
+  getWorkareaList
+} from '@/api/dv/spm/index'
+import { genCode } from '@/api/system/autocode/rule'
+import { listWorkarea } from '@/api/mes/wa/workarea'
+import { listLockType } from '@/api/mes/locktype/locktype'
+import { listPadpadLockTypeApi } from '@/api/mes/padLockType/padLockType'
+import { listLoto } from '@/api/dv/lotoStation/index'
+import { listMarsDept } from '@/api/system/marsdept'
+import { listTechnology } from '@/api/system/machinery'
+import { getIsSystemAttributeByKey } from '@/api/basic/mappoint/index'
+
+const props = defineProps({
+  lotoId: {
+    type: String,
+    required: true
+  }
+})
+
+// 字典数据
+const { dict } = useDict('power_type', 'point_type', 'lock_type', 'switch_status')
+
+// 数据定义
+const loading = ref(true)
+const total = ref(0)
+const isolationList = ref([])
+const ids = ref([])
+const codes = ref([])
+const single = ref(true)
+const multiple = ref(true)
+const deptOptions = ref([])
+const showSearch = ref(true)
+const workshopList = ref([])
+const workareaList = ref([])
+const pointId = ref(null)
+const autoGenFlag = ref(false)
+const optType = ref(undefined)
+const selectedImageIndex = ref(-1)
+
+// 查询参数
+const queryParams = reactive({
+  current: 1,
+  size: 10,
+  pointCode: '',
+  pointName: '',
+  delFlag: '',
+  pointType: '',
+  powerType: '',
+  startTime: '',
+  endTime: '',
+  lotoId: null
+})
+
+// 表单参数
+const form = reactive({})
+
+// 图片映射
+const imageMap = reactive({
+  0: '', // 电能
+  1: '', // 阀门
+  2: '', // 空气能
+  3: '' // 急停开关
+})
+
+// 监听 lotoId 变化
+watch(() => props.lotoId, (newVal) => {
+  console.log('lotoId changed:', newVal)
+  queryParams.lotoId = newVal
+  getList()
+})
+
+// 方法定义
+const getList = async () => {
+  loading.value = true
+  try {
+    const response = await getIsIsolationPointPage(queryParams)
+    isolationList.value = response.data.records
+    total.value = response.data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+const handleSelectionChange = (selection) => {
+  ids.value = selection.map(item => item.pointId)
+  codes.value = selection.map(item => item.pointCode)
+  single.value = selection.length !== 1
+  multiple.value = !selection.length
+}
+
+const handleView = (row) => {
+  // 实现查看详情逻辑
+}
+
+// 生命周期钩子
+onMounted(() => {
+  getList()
+  getworkShop()
+  getworkArea()
+})
+
+// 获取车间数据
+const getworkShop = async () => {
+  try {
+    const response = await workshoplistAll()
+    workshopList.value = response.data.map(item => ({
+      label: item.workshopName,
+      value: item.workshopId,
+      key: item.workshopCode
+    }))
+  } catch (error) {
+    console.error('获取车间数据失败:', error)
+  }
+}
+
+// 获取作业区域数据
+const getworkArea = async () => {
+  const workshopId = form.workshopId
+  if (workshopId) {
+    try {
+      const response = await getIsWorkareaList(workshopId)
+      workareaList.value = response.data.map(item => ({
+        label: item.workareaName,
+        value: item.workareaId,
+        key: item.workareaCode
+      }))
+    } catch (error) {
+      console.error('获取作业区域数据失败:', error)
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.el-input-width {
+  width: 380px !important;
+}
+
+.image-grid {
+  display: flex;
+  flex-wrap: wrap;
+}
+
+.image-item {
+  margin: 5px;
+  height: 55px;
+  cursor: pointer;
+  border: 2px solid transparent;
+  transition: border-color 0.3s;
+  position: relative;
+
+  &.selected {
+    height: 55px;
+    border-color: rgb(2, 86, 255);
+    border-width: 2px;
+  }
+}
+</style>

+ 176 - 0
src/views/dv/lotoStation/SwitchStatus.vue

@@ -0,0 +1,176 @@
+<template>
+  <div class="mapdata">
+    <div id="container" ref="container" style="width: 1600px"></div>
+  </div>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted, onBeforeUnmount } from 'vue'
+import { useRoute } from 'vue-router'
+import Konva from 'konva'
+import {
+  selectLotoMapById,
+  selectIsLotoStationById,
+  updateLoto,
+  updatePointsBindingLoto
+} from '@/api/mes/lotoStation/lotoStation'
+import { getIsIsolationPointPage } from '@/api/mes/spm/segregationPoint'
+import { getIsMapPointPage, selectIsMapPointById, updateMapPointList } from '@/api/system/mappoint'
+import { selectIsMapById } from '@/api/system/mapconfig'
+
+const route = useRoute()
+const container = ref(null)
+const stage = ref(null)
+const layer = ref(null)
+const blinkLights = ref([])
+const globalBlinkTimer = ref(null)
+
+// 数据定义
+const form = reactive({})
+const imageUrl = ref('')
+const width = ref('')
+const height = ref('')
+const x = ref('')
+const y = ref('')
+const mapId = ref(null)
+const mapType = ref(2)
+const pointList = ref(null)
+const newbindingPoints = ref([])
+const newmovePoints = ref([])
+const newunbindingPoints = ref([])
+
+// 初始化 Konva
+const initKonva = () => {
+  stage.value = new Konva.Stage({
+    container: container.value,
+    width: 1600,
+    height: 860
+  })
+
+  layer.value = new Konva.Layer()
+  drawGrid(50, 50, '#e0e0e0')
+
+  const bgImage = new Image()
+  const imageConfig = {
+    x: x.value,
+    y: y.value,
+    width: width.value,
+    height: height.value,
+    draggable: false
+  }
+
+  bgImage.src = imageUrl.value
+  bgImage.onload = () => {
+    const knovaImage = new Konva.Image({
+      ...imageConfig,
+      image: bgImage
+    })
+    layer.value.add(knovaImage)
+    renderGrid()
+    stage.value.add(layer.value)
+    layer.value.draw()
+  }
+
+  stage.value.draggable(false)
+}
+
+// 绘制网格
+const drawGrid = (cellWidth, cellHeight, gridColor) => {
+  const width = 1600
+  const height = 860
+
+  for (let i = 0; i <= width; i += cellWidth) {
+    const verticalLine = new Konva.Line({
+      points: [i, 0, i, height],
+      stroke: gridColor,
+      strokeWidth: 1
+    })
+    layer.value.add(verticalLine)
+  }
+
+  for (let j = 0; j <= height; j += cellHeight) {
+    const horizontalLine = new Konva.Line({
+      points: [0, j, width, j],
+      stroke: gridColor,
+      strokeWidth: 1
+    })
+    layer.value.add(horizontalLine)
+  }
+
+  layer.value.draw()
+}
+
+// 全局控制闪烁频率同步函数
+const startGlobalBlinkTimer = () => {
+  if (globalBlinkTimer.value) return
+
+  globalBlinkTimer.value = setInterval(() => {
+    const currentSecond = Math.floor(Date.now() / 200) % 2
+    const isOn = currentSecond === 1
+
+    blinkLights.value.forEach(light => {
+      light.opacity(isOn ? 1 : 0.6)
+      light.scale({ x: isOn ? 1 : 1.1, y: isOn ? 1 : 1.1 })
+    })
+
+    if (blinkLights.value.length > 0) {
+      blinkLights.value[0].getLayer().batchDraw()
+    }
+  }, 50)
+}
+
+// 添加闪烁动画
+const addBlinkAnimation = (light, isRed, isGrey) => {
+  if (!isGrey && !isRed) {
+    if (!blinkLights.value.includes(light)) {
+      blinkLights.value.push(light)
+    }
+    startGlobalBlinkTimer()
+  }
+}
+
+// 获取信息
+const getInfo = async () => {
+  const lotoId = route.query.lotoId
+  try {
+    const response = await selectIsLotoStationById(lotoId)
+    Object.assign(form, response.data)
+    mapId.value = response.data.mapId
+
+    const mapResponse = await selectIsMapById(response.data.mapId)
+    imageUrl.value = mapResponse.data.imageUrl
+    width.value = mapResponse.data.width
+    height.value = mapResponse.data.height
+    x.value = mapResponse.data.x
+    y.value = mapResponse.data.y
+    pointList.value = mapResponse.data.pointList
+    initKonva()
+  } catch (error) {
+    console.error('获取信息失败:', error)
+  }
+}
+
+// 生命周期钩子
+onMounted(() => {
+  getInfo()
+})
+
+onBeforeUnmount(() => {
+  if (globalBlinkTimer.value) {
+    clearInterval(globalBlinkTimer.value)
+  }
+})
+</script>
+
+<style scoped lang="scss">
+#container {
+  width: 100%;
+  height: 100%;
+}
+
+.mapdata {
+  width: 100%;
+  height: 100%;
+  display: flex;
+}
+</style>

+ 211 - 0
src/views/dv/lotoStation/index.vue

@@ -0,0 +1,211 @@
+<template>
+  <ContentWrap>
+    <!-- 搜索工作栏 -->
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="100px"
+    >
+      <el-form-item label="锁定站名称" prop="lotoName">
+        <el-input
+          v-model="queryParams.lotoName"
+          placeholder="请输入锁定站名称"
+          clearable
+          class="!w-240px"
+          @keyup.enter="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="岗位" prop="workstationId">
+        <el-tree-select
+          v-model="queryParams.workstationId"
+          :data="marsOptions"
+          :props="{ label: 'workstationName', value: 'workstationId', children: 'children' }"
+          placeholder="选择岗位"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="设备/工艺" prop="lotoId">
+        <el-tree-select
+          v-model="queryParams.lotoId"
+          :data="machineryOptions"
+          :props="{ label: 'machineryName', value: 'machineryId', children: 'children' }"
+          placeholder="选择设备/工艺"
+          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="['iscs:station:create']"
+        >
+          <Icon icon="ep:plus" class="mr-5px" /> 新增
+        </el-button>
+        <el-button
+          type="danger"
+          plain
+          :disabled="multiple"
+          @click="handleDelete()"
+          v-hasPermi="['iscs:station:delete']"
+        >
+          <Icon icon="ep:delete" class="mr-5px" /> 批量删除
+        </el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table v-loading="loading" :data="list" @selection-change="handleSelectionChange">
+      <el-table-column type="selection" width="55" align="center" />
+      <el-table-column label="锁定站名称" align="center" prop="lotoName" />
+      <el-table-column label="排序" align="center" prop="orderNum" />
+      <el-table-column label="岗位" align="center" prop="workstationName" />
+      <el-table-column label="地图名称" align="center" prop="mapName" />
+      <el-table-column label="所属硬件序列号" align="center" prop="lotoSerialNumber" />
+      <el-table-column label="锁定站详情" align="center" width="120">
+        <template #default="scope">
+          <el-button
+            link
+            type="primary"
+            @click="handleView(scope.row)"
+            v-hasPermi="['iscs:station:query']"
+          >
+            查看
+          </el-button>
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" align="center" width="120">
+        <template #default="scope">
+          <el-button
+            link
+            type="primary"
+            @click="openForm('update', scope.row.lotoId)"
+            v-hasPermi="['iscs:station:update']"
+          >
+            修改
+          </el-button>
+          <el-button
+            link
+            type="danger"
+            @click="handleDelete(scope.row.lotoId)"
+            v-hasPermi="['iscs:station:delete']"
+          >
+            删除
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.current"
+      v-model:limit="queryParams.size"
+      @pagination="getList"
+    />
+  </ContentWrap>
+
+  <!-- 表单弹窗:添加/修改 -->
+  <LotoStationForm ref="formRef" @success="getList" />
+</template>
+
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import * as LotoStationApi from '@/api/dv/lotoStation/index'
+import LotoStationForm from './LotoStationForm.vue'
+
+
+defineOptions({ name: 'LotoStation' })
+
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
+
+const loading = ref(true) // 列表的加载中
+const total = ref(0) // 列表的总页数
+const list = ref([]) // 列表的数据
+const ids = ref<number[]>([]) // 选中的数据
+const multiple = ref(true) // 非多个禁用
+const marsOptions = ref([]) // 岗位树选项
+const machineryOptions = ref([]) // 设备工艺树选项
+
+const queryParams = reactive({
+  current: 1,
+  size: 10,
+  lotoName: undefined,
+  workstationId: undefined,
+  lotoId: undefined
+})
+
+const queryFormRef = ref() // 搜索的表单
+
+/** 查询锁定站列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await LotoStationApi.listLoto(queryParams)
+    list.value = data.records
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.current = 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) => {
+  const lotoIds = id || ids.value
+  try {
+    await message.delConfirm()
+    await LotoStationApi.delLoto(lotoIds)
+    message.success(t('common.delSuccess'))
+    await getList()
+  } catch {}
+}
+
+/** 多选框选中数据 */
+const handleSelectionChange = (selection: any[]) => {
+  ids.value = selection.map(item => item.lotoId)
+  multiple.value = !selection.length
+}
+
+/** 查看按钮操作 */
+const handleView = (row: any) => {
+  const lotoId = row.lotoId
+  router.push(`/mes/hw/lotoStation/index/LookDetail?lotoId=${lotoId}`)
+}
+
+/** 初始化 **/
+onMounted(async () => {
+  await getList()
+  // 获取岗位数据
+  const deptRes = await listMarsDept({ current: 1, size: -1 })
+  marsOptions.value = handleTree(deptRes.data.records, 'workstationId', 'parentId')
+
+  // 获取设备/工艺数据
+  const techRes = await listTechnology({ current: 1, size: -1 })
+  const data = techRes.data.records.filter(item => item.machineryType == '工艺')
+  machineryOptions.value = handleTree(data, 'machineryId', 'parentId')
+})
+</script>

+ 343 - 0
src/views/dv/spm/SegregationPointForm.vue

@@ -0,0 +1,343 @@
+<template>
+  <Dialog v-model="dialogVisible" :title="dialogTitle" width="960">
+    <el-form
+      ref="formRef"
+      v-loading="formLoading"
+      :model="formData"
+      :rules="formRules"
+      label-width="110px"
+    >
+      <el-row>
+        <el-col :span="7">
+          <el-form-item label="隔离点编号" prop="pointCode">
+            <el-input
+              v-model="formData.pointCode"
+              placeholder="请输入隔离点编号"
+            />
+          </el-form-item>
+        </el-col>
+        <el-col :span="4">
+          <el-form-item label-width="80">
+            <el-switch
+              v-model="autoGenFlag"
+              active-color="#13ce66"
+              active-text="自动生成"
+              @change="handleAutoGenChange"
+              v-if="formType !== 'view'"
+            />
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="隔离点名称" prop="pointName">
+            <el-input
+              v-model="formData.pointName"
+              placeholder="请输入隔离点名称"
+            />
+          </el-form-item>
+        </el-col>
+      </el-row>
+
+      <el-row>
+        <el-col :span="11">
+          <el-form-item label="岗位" prop="workstationId">
+            <el-tree-select
+              v-model="formData.workstationId"
+              :data="deptOptions"
+              :props="{ label: 'workstationName', value: 'workstationId', children: 'children' }"
+              placeholder="选择岗位"
+            />
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="隔离点NFC" prop="pointNfc">
+            <el-select v-model="formData.pointNfc">
+              <el-option
+                v-for="dict in rfidTokenData"
+                :key="dict.value"
+                :label="dict.label"
+                :value="dict.value"
+              />
+            </el-select>
+          </el-form-item>
+        </el-col>
+      </el-row>
+
+      <el-row>
+        <el-col :span="11">
+          <el-form-item label="锁定站" prop="lotoId">
+            <el-select
+              v-model="formData.lotoId"
+              placeholder="请选择锁定站"
+            >
+              <el-option
+                v-for="dict in lotoOptions"
+                :key="dict.value"
+                :label="dict.label"
+                :value="dict.value"
+              />
+            </el-select>
+          </el-form-item>
+        </el-col>
+        <el-col :span="11">
+          <el-form-item label="作用" prop="remark">
+            <el-input
+              v-model="formData.remark"
+              placeholder="请输入作用"
+            />
+          </el-form-item>
+        </el-col>
+      </el-row>
+
+      <el-row>
+        <el-col :span="11">
+          <el-form-item label="能量源" prop="powerType">
+            <el-select
+              v-model="formData.powerType"
+              placeholder="请选择能量源"
+            >
+              <el-option
+                v-for="dict in powerTypeOptions"
+                :key="dict.value"
+                :label="dict.label"
+                :value="dict.value"
+              />
+            </el-select>
+          </el-form-item>
+        </el-col>
+        <el-col :span="11">
+          <el-form-item label="隔离点序列号" prop="pointSerialNumber">
+            <el-input
+              v-model="formData.pointSerialNumber"
+              placeholder="请输入隔离点序列号"
+            />
+          </el-form-item>
+        </el-col>
+      </el-row>
+
+      <el-row>
+        <el-col :span="12">
+          <el-form-item label="隔离点图标" prop="pointIcon">
+            <div class="image-grid">
+              <div
+                v-for="(imageUrl, index) in imageMap"
+                :key="index"
+                class="image-item"
+                :class="{ 'selected': selectedImageIndex === index }"
+                @click="selectIcon(imageUrl, index)"
+              >
+                <img :src="imageUrl" alt="Isolation Icon" style="width: 50px; height: 50px;" />
+              </div>
+            </div>
+          </el-form-item>
+        </el-col>
+        <el-col :span="8">
+          <el-form-item label="隔离点图片" prop="pointPicture">
+            <UploadImg
+              v-model="formData.pointPicture"
+              :limit="1"
+              :is-show-tip="false"
+            />
+          </el-form-item>
+        </el-col>
+      </el-row>
+    </el-form>
+    <template #footer>
+      <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import * as SegregationPointApi from '@/api/dv/spm/index'
+import { genCode } from '@/api/system/autocode/rule'
+import { getIsSystemAttributeByKey } from '@/api/basic/configuration/index'
+import { getIsRfidTokenPage } from '@/api/mes/rfid_token'
+import { listMarsDept } from '@/api/system/marsdept'
+import { listLoto } from '@/api/dv/lotoStation/index'
+
+
+defineOptions({ name: 'SegregationPointForm' })
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const dialogTitle = ref('') // 弹窗的标题
+const formLoading = ref(false) // 表单的加载中
+const formType = ref('') // 表单的类型:create - 新增;update - 修改
+const autoGenFlag = ref(false) // 是否自动生成编码
+const selectedImageIndex = ref(-1) // 选中的图片索引
+
+const formData = ref({
+  pointId: undefined,
+  pointCode: '',
+  pointName: '',
+  pointIcon: '',
+  pointPicture: '',
+  pointNfc: '',
+  workstationId: undefined,
+  lotoId: undefined,
+  powerType: undefined,
+  pointSerialNumber: '',
+  remark: ''
+})
+
+const formRules = reactive({
+  pointCode: [{ required: true, message: '隔离点编号不能为空', trigger: 'blur' }],
+  pointName: [{ required: true, message: '隔离点名称不能为空', trigger: 'blur' }],
+  workstationId: [{ required: true, message: '岗位不能为空', trigger: 'blur' }],
+  lotoId: [{ required: true, message: '锁定站不能为空', trigger: 'blur' }],
+  pointNfc: [{ required: true, message: '隔离点NFC不能为空', trigger: 'blur' }]
+})
+
+const formRef = ref() // 表单 Ref
+const deptOptions = ref([]) // 部门树选项
+const lotoOptions = ref([]) // 锁定站选项
+const rfidTokenData = ref([]) // RFID Token数据
+const imageMap = ref({}) // 图片映射
+const powerTypeOptions = 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 SegregationPointApi.selectIsIsolationPointById(id)
+      // 确定选中的图片索引
+      selectedImageIndex.value = getImageIndexByIcon(formData.value.pointIcon)
+    } finally {
+      formLoading.value = false
+    }
+  }
+  // 获取岗位数据
+  const deptRes = await listMarsDept({ current: 1, size: -1 })
+  deptOptions.value = handleTree(deptRes.data.records, 'workstationId', 'parentId')
+
+  // 获取锁定站数据
+  const lotoRes = await listLoto({ current: 1, size: -1 })
+  lotoOptions.value = lotoRes.data.records.map(item => ({
+    value: item.lotoId,
+    label: item.lotoName
+  }))
+
+  // 获取RFID Token数据
+  const rfidRes = await getIsRfidTokenPage({ current: 1, size: -1 })
+  rfidTokenData.value = rfidRes.data.records.map(record => ({
+    value: record.rfidId,
+    label: record.rfid
+  }))
+
+  // 获取能量源字典数据
+  powerTypeOptions.value = await getIntDictOptions(DICT_TYPE.POWER_TYPE)
+
+  // 获取隔离点图标
+  const sysAttrKey1 = 'sys.icon_set.isolation'
+  const iconRes = await getIsSystemAttributeByKey(sysAttrKey1)
+  const values = iconRes.data.sysAttrValue.split(',').map(value => value.trim())
+  const iconPromises = values.map(value => getIsSystemAttributeByKey(value))
+  const iconResponses = await Promise.all(iconPromises)
+  iconResponses.forEach((response, index) => {
+    imageMap.value[index] = response.data.sysAttrValue
+  })
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 校验表单
+  if (!formRef) return
+  const valid = await formRef.value.validate()
+  if (!valid) return
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = formData.value
+    if (formType.value === 'create') {
+      await SegregationPointApi.addinsertIsIsolationPoint(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await SegregationPointApi.updateIsIsolationPoint(data)
+      message.success(t('common.updateSuccess'))
+    }
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    pointId: undefined,
+    pointCode: '',
+    pointName: '',
+    pointIcon: '',
+    pointPicture: '',
+    pointNfc: '',
+    workstationId: undefined,
+    lotoId: undefined,
+    powerType: undefined,
+    pointSerialNumber: '',
+    remark: ''
+  }
+  autoGenFlag.value = false
+  selectedImageIndex.value = -1
+  formRef.value?.resetFields()
+}
+
+/** 自动生成编码 */
+const handleAutoGenChange = async (value: boolean) => {
+  if (value) {
+    formData.value.pointCode = await genCode('ISOLATION_POINT_CODE')
+  } else {
+    formData.value.pointCode = ''
+  }
+}
+
+/** 选择隔离点图标 */
+const selectIcon = (imageUrl: string, index: number) => {
+  formData.value.pointIcon = imageUrl
+  selectedImageIndex.value = index
+}
+
+/** 获取图片索引 */
+const getImageIndexByIcon = (iconUrl: string) => {
+  for (const [index, imageUrl] of Object.entries(imageMap.value)) {
+    if (imageUrl === iconUrl) {
+      return index
+    }
+  }
+  return -1
+}
+</script>
+
+<style lang="scss" scoped>
+.image-grid {
+  display: flex;
+  flex-wrap: wrap;
+}
+
+.image-item {
+  margin: 5px;
+  height: 55px;
+  cursor: pointer;
+  border: 2px solid transparent;
+  transition: border-color 0.3s;
+  position: relative;
+
+  &.selected {
+    border-color: rgb(2, 86, 255);
+    border-width: 2px;
+  }
+}
+</style>

+ 294 - 0
src/views/dv/spm/index.vue

@@ -0,0 +1,294 @@
+<template>
+  <ContentWrap>
+    <!-- 搜索工作栏 -->
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="100px"
+    >
+      <el-form-item label="隔离点名称" prop="pointName">
+        <el-input
+          v-model="queryParams.pointName"
+          placeholder="请输入隔离点名称"
+          clearable
+          class="!w-240px"
+          @keyup.enter="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="岗位" prop="workstationId">
+        <el-tree-select
+          v-model="queryParams.workstationId"
+          :data="deptOptions"
+          :props="{ label: 'workstationName', value: 'workstationId', children: 'children' }"
+          placeholder="选择岗位"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="设备/工艺" prop="machineryId">
+        <el-tree-select
+          v-model="queryParams.machineryId"
+          :data="machineryOptions"
+          :props="{ label: 'machineryName', value: 'machineryId', children: 'children' }"
+          placeholder="选择设备/工艺"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="锁定站" prop="lotoId">
+        <el-select
+          v-model="queryParams.lotoId"
+          placeholder="请选择锁定站"
+          class="!w-240px"
+        >
+          <el-option
+            v-for="dict in lotoOptions"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="能量源" prop="powerType">
+        <el-select
+          v-model="queryParams.powerType"
+          placeholder="请选择能量源"
+          class="!w-240px"
+        >
+          <el-option
+            v-for="dict in powerTypeOptions"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.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="['iscs:point:create']"
+        >
+          <Icon icon="ep:plus" class="mr-5px" /> 新增
+        </el-button>
+        <el-button
+          type="danger"
+          plain
+          :disabled="multiple"
+          @click="handleDelete()"
+          v-hasPermi="['iscs:point:delete']"
+        >
+          <Icon icon="ep:delete" class="mr-5px" /> 批量删除
+        </el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table v-loading="loading" :data="list" @selection-change="handleSelectionChange">
+      <el-table-column type="selection" width="55" align="center" />
+      <el-table-column label="隔离点编号" align="center" prop="pointCode" width="100">
+        <template #default="scope">
+          <el-button
+            link
+            type="primary"
+            @click="handleView(scope.row)"
+            v-hasPermi="['iscs:point:list']"
+          >{{ scope.row.pointCode }}</el-button>
+        </template>
+      </el-table-column>
+      <el-table-column label="隔离点名称" align="center" prop="pointName" />
+      <el-table-column label="隔离点图标" align="center" prop="pointIcon" width="90">
+        <template #default="scope">
+          <el-image
+            v-if="scope.row.pointIcon"
+            style="width: 50px; height: 50px"
+            :src="scope.row.pointIcon"
+            :preview-src-list="[scope.row.pointIcon]"
+          />
+          <span v-else>-</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="开关状态" align="center" prop="switchStatus">
+        <template #default="scope">
+          <el-switch
+            style="pointer-events: none"
+            v-if="scope.row.switchStatus!==null"
+            v-model="scope.row.switchStatus"
+            active-value="1"
+            inactive-value="0"
+            active-text="ON"
+            inactive-text="OFF"
+            active-color="#13ce66"
+            inactive-color="#ff4949"
+          />
+          <span v-else>-</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="隔离点NFC" align="center" prop="pointNfc" />
+      <el-table-column label="岗位" align="center" prop="workstationName" />
+      <el-table-column label="设备/工艺" align="center" prop="machineryName" />
+      <el-table-column label="锁定站" align="center" prop="lotoName" />
+      <el-table-column label="隔离点序列号" align="center" prop="pointSerialNumber" />
+      <el-table-column label="作用" align="center" prop="remark" />
+      <el-table-column label="隔离点图片" align="center" prop="pointPicture" width="90">
+        <template #default="scope">
+          <el-image
+            v-if="scope.row.pointPicture"
+            style="width: 50px; height: 50px"
+            :src="scope.row.pointPicture"
+            :preview-src-list="[scope.row.pointPicture]"
+          />
+          <span v-else>-</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="能量源" align="center" prop="powerType">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.POWER_TYPE" :value="scope.row.powerType" />
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" align="center" width="120">
+        <template #default="scope">
+          <el-button
+            link
+            type="primary"
+            @click="openForm('update', scope.row.pointId)"
+            v-hasPermi="['iscs:point:update']"
+          >
+            修改
+          </el-button>
+          <el-button
+            link
+            type="danger"
+            @click="handleDelete(scope.row.pointId)"
+            v-hasPermi="['iscs:point:delete']"
+          >
+            删除
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.current"
+      v-model:limit="queryParams.size"
+      @pagination="getList"
+    />
+  </ContentWrap>
+
+  <!-- 表单弹窗:添加/修改 -->
+  <SegregationPointForm ref="formRef" @success="getList" />
+</template>
+
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import * as SegregationPointApi from '@/api/dv/spm/index'
+import SegregationPointForm from './SegregationPointForm.vue'
+
+defineOptions({ name: 'SegregationPoint' })
+
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
+
+const loading = ref(true) // 列表的加载中
+const total = ref(0) // 列表的总页数
+const list = ref([]) // 列表的数据
+const ids = ref<number[]>([]) // 选中的数据
+const multiple = ref(true) // 非多个禁用
+const deptOptions = ref([]) // 部门树选项
+const machineryOptions = ref([]) // 工艺树选项
+const lotoOptions = ref([]) // 锁定站选项
+const powerTypeOptions = ref([]) // 能量源选项
+
+const queryParams = reactive({
+  current: 1,
+  size: 10,
+  pointName: undefined,
+  workstationId: undefined,
+  machineryId: undefined,
+  lotoId: undefined,
+  powerType: undefined
+})
+
+const queryFormRef = ref() // 搜索的表单
+
+/** 查询隔离点列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await SegregationPointApi.getIsIsolationPointPage(queryParams)
+    list.value = data.records
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.current = 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) => {
+  const pointIds = id || ids.value
+  try {
+    await message.delConfirm()
+    await SegregationPointApi.deleteIsIsolationPointByPointIds(pointIds)
+    message.success(t('common.delSuccess'))
+    await getList()
+  } catch {}
+}
+
+/** 多选框选中数据 */
+const handleSelectionChange = (selection: any[]) => {
+  ids.value = selection.map(item => item.pointId)
+  multiple.value = !selection.length
+}
+
+/** 查看按钮操作 */
+const handleView = (row: any) => {
+  openForm('view', row.pointId)
+}
+
+/** 初始化 **/
+onMounted(async () => {
+  await getList()
+  // 获取岗位数据
+  const deptRes = await listMarsDept({ current: 1, size: -1 })
+  deptOptions.value = handleTree(deptRes.data.records, 'workstationId', 'parentId')
+
+  // 获取设备/工艺数据
+  const techRes = await listTechnology({ current: 1, size: -1 })
+  const data = techRes.data.records.filter(item => item.machineryType == '工艺')
+  machineryOptions.value = handleTree(data, 'machineryId', 'parentId')
+
+  // 获取锁定站数据
+  const lotoRes = await listLoto({ current: 1, size: -1 })
+  lotoOptions.value = lotoRes.data.records.map(item => ({
+    value: item.lotoId,
+    label: item.lotoName
+  }))
+
+  // 获取能量源字典数据
+  powerTypeOptions.value = await getIntDictOptions(DICT_TYPE.POWER_TYPE)
+})
+</script>

+ 167 - 0
src/views/dv/technology/TechnologyForm.vue

@@ -0,0 +1,167 @@
+<template>
+  <Dialog v-model="dialogVisible" :title="dialogTitle" width="800">
+    <el-form
+      ref="formRef"
+      v-loading="formLoading"
+      :model="formData"
+      :rules="formRules"
+      label-width="120px"
+    >
+      <el-form-item :label="tabPosition.value === 'craft' ? '工艺名称' : '设备名称'" prop="machineryName">
+        <el-input v-model="formData.machineryName" placeholder="请输入名称" />
+      </el-form-item>
+      <el-form-item label="岗位" prop="workstationId">
+        <el-tree-select
+          v-model="formData.workstationId"
+          :data="workstationOptions"
+          :props="defaultProps"
+          placeholder="请选择岗位"
+        />
+      </el-form-item>
+      <el-form-item label="所属电柜" prop="lotoId">
+        <el-select v-model="formData.lotoId" placeholder="请选择所属电柜" class="!w-300px">
+          <el-option
+            v-for="dict in lotoOptions"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item :label="tabPosition.value === 'craft' ? '工艺图' : '设备图'" prop="machineryImg">
+        <UploadImg v-model="formData.machineryImg" :limit="1" height="75px" width="75px" />
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+
+<script lang="ts" setup>
+import { ref, reactive, watch, onMounted } from 'vue'
+import { handleTree } from '@/utils/tree'
+import * as TechnologyApi from '@/api/dv/technology'
+// import * as MarsDeptApi from '@/api/system/marsdept'
+import * as LotoApi from '@/api/dv/lotoStation'
+
+defineOptions({ name: 'TechnologyForm' })
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const dialogTitle = ref('') // 弹窗的标题
+const formLoading = ref(false) // 表单的加载中
+const formType = ref('') // 表单的类型:create - 新增;update - 修改
+const tabPosition = ref('craft') // 当前选中的标签页
+
+const formData = ref({
+  machineryId: undefined,
+  parentId: undefined,
+  machineryName: '',
+  machineryType: undefined,
+  workstationId: undefined,
+  lotoId: undefined,
+  machineryImg: ''
+})
+
+const formRules = reactive({
+  machineryName: [{ required: true, message: '名称不能为空', trigger: 'blur' }],
+  workstationId: [{ required: true, message: '岗位不能为空', trigger: 'change' }],
+  lotoId: [{ required: true, message: '电柜不能为空', trigger: 'change' }]
+})
+
+const formRef = ref() // 表单 Ref
+const workstationOptions = ref([]) // 岗位树选项
+const lotoOptions = ref([]) // 电柜选项
+
+// 树形配置
+const defaultProps = {
+  children: 'children',
+  label: 'label'
+}
+
+/** 打开弹窗 */
+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 TechnologyApi.getTechnologyInfo(id)
+    } finally {
+      formLoading.value = false
+    }
+  }
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 校验表单
+  if (!formRef.value) return
+  const valid = await formRef.value.validate()
+  if (!valid) return
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = formData.value
+    if (formType.value === 'create') {
+      await TechnologyApi.addTechnology(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await TechnologyApi.updateTechnology(data)
+      message.success(t('common.updateSuccess'))
+    }
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    machineryId: undefined,
+    parentId: undefined,
+    machineryName: '',
+    machineryType: undefined,
+    workstationId: undefined,
+    lotoId: undefined,
+    machineryImg: ''
+  }
+  formRef.value?.resetFields()
+}
+
+// 监听岗位变化
+watch(() => formData.value.workstationId, async (newVal) => {
+  if (newVal) {
+    const data = { pageNo: 1, pageSize: -1, workstationId: newVal }
+    const response = await LotoApi.listLoto(data)
+    lotoOptions.value = response.records.map((item) => ({
+      value: item.lotoId,
+      label: item.lotoName
+    }))
+  }
+})
+
+// 获取岗位树数据
+const getWorkstationTree = async () => {
+  const data = { pageNo: 1, pageSize: -1 }
+  const response = await MarsDeptApi.listMarsDept(data)
+  workstationOptions.value = handleTree(response.records)
+}
+
+// 初始化
+onMounted(() => {
+  getWorkstationTree()
+})
+</script>

+ 347 - 0
src/views/dv/technology/index.vue

@@ -0,0 +1,347 @@
+<template>
+  <div class="app-container">
+    <!-- 左侧岗位树 -->
+    <ContentWrap class="left">
+      <div class="deptTree">
+        <div class="head-container">
+          <el-input
+            v-model="queryParams.workstationName"
+            placeholder="请在下方选择岗位名称"
+            clearable
+            size="small"
+            prefix-icon="ep:search"
+            class="mb-20px"
+            @input="handleInputChange"
+            @clear="handleClear"
+          />
+        </div>
+        <div class="head-container">
+          <el-tree
+            :data="workstationOptions"
+            :props="defaultProps"
+            :expand-on-click-node="false"
+            :filter-node-method="filterNode"
+            ref="treeDataRef"
+            node-key="id"
+            default-expand-all
+            @node-click="handleNodeClick"
+            highlight-current
+          />
+        </div>
+      </div>
+    </ContentWrap>
+
+    <!-- 右侧内容区 -->
+    <ContentWrap class="right">
+      <el-radio-group v-model="tabPosition" class="mb-30px">
+        <el-radio-button label="craft">工艺</el-radio-button>
+        <el-radio-button label="device">设备</el-radio-button>
+      </el-radio-group>
+
+      <div v-if="tabPosition === 'craft' || tabPosition === 'device'">
+        <!-- 搜索工作栏 -->
+        <el-form
+          class="-mb-15px"
+          :model="queryParams"
+          ref="queryFormRef"
+          :inline="true"
+          label-width="68px"
+        >
+          <el-form-item :label="tabPosition === 'craft' ? '工艺名称' : '设备名称'" prop="machineryName">
+            <el-input
+              v-model="queryParams.machineryName"
+              :placeholder="tabPosition === 'craft' ? '请输入工艺名称' : '请输入设备名称'"
+              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="['iscs:machinery:create']"
+            >
+              <Icon icon="ep:plus" class="mr-5px" /> 新增
+            </el-button>
+            <el-button
+              type="danger"
+              plain
+              :disabled="multiple"
+              @click="handleDelete"
+              v-hasPermi="['iscs:machinery:delete']"
+            >
+              <Icon icon="ep:delete" class="mr-5px" /> 批量删除
+            </el-button>
+          </el-form-item>
+        </el-form>
+
+        <!-- 列表 -->
+        <el-table
+          v-loading="loading"
+          :data="list"
+          row-key="machineryId"
+          :default-expand-all="isExpandAll"
+          v-if="refreshTable"
+          :tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
+          @selection-change="handleSelectionChange"
+        >
+          <el-table-column type="selection" width="55" align="center" />
+          <el-table-column
+            prop="machineryId"
+            :label="tabPosition === 'craft' ? '工艺编号' : '设备编号'"
+            width="130"
+          />
+          <el-table-column
+            prop="machineryName"
+            :label="tabPosition === 'craft' ? '工艺名称' : '设备名称'"
+            width="260"
+          />
+          <el-table-column prop="workstationName" label="岗位" width="260" />
+          <el-table-column
+            prop="machineryImg"
+            :label="tabPosition === 'craft' ? '工艺图' : '设备图'"
+            width="260"
+          >
+            <template #default="scope">
+              <UploadImg v-model="scope.row.machineryImg" :limit="1" height="75px" width="75px" />
+            </template>
+          </el-table-column>
+          <el-table-column
+            label="详情"
+            align="center"
+            class-name="small-padding fixed-width"
+          >
+            <template #default="scope">
+              <el-button
+                link
+                type="primary"
+                v-throttle
+                @click="handleLook(scope.row)"
+              >
+                <Icon icon="ep:view" class="mr-5px" /> 查看
+              </el-button>
+            </template>
+          </el-table-column>
+          <el-table-column label="操作" align="center" width="260">
+            <template #default="scope">
+              <el-button
+                link
+                type="primary"
+                @click="handleLook(scope.row)"
+              >
+                <Icon icon="ep:view" class="mr-5px" /> 查看
+              </el-button>
+              <el-button
+                link
+                type="primary"
+                @click="openForm('update', scope.row.machineryId)"
+                v-hasPermi="['iscs:machinery:update']"
+              >
+                <Icon icon="ep:edit" class="mr-5px" /> 修改
+              </el-button>
+              <el-button
+                link
+                type="danger"
+                @click="handleDelete(scope.row.machineryId)"
+                v-hasPermi="['iscs:machinery:delete']"
+              >
+                <Icon icon="ep:delete" class="mr-5px" /> 删除
+              </el-button>
+            </template>
+          </el-table-column>
+        </el-table>
+
+        <!-- 分页 -->
+        <pagination
+          v-show="total > 0"
+          v-model:total="total"
+          v-model:page="queryParams.pageNo"
+          v-model:limit="queryParams.pageSize"
+          @pagination="getList"
+        />
+      </div>
+    </ContentWrap>
+
+    <!-- 表单弹窗:添加/修改 -->
+    <TechnologyForm ref="formRef" @success="getList" />
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { ref, reactive, onMounted, watch } from 'vue'
+import { useRouter } from 'vue-router'
+import { handleTree } from '@/utils/tree'
+import * as TechnologyApi from '@/api/dv/technology'
+// import * as MarsDeptApi from '@/api/system/marsdept'
+import * as LotoApi from '@/api/dv/lotoStation'
+import TechnologyForm from './TechnologyForm.vue'
+
+
+defineOptions({ name: 'Technology' })
+
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
+const router = useRouter()
+
+// 数据定义
+const loading = ref(true) // 列表的加载中
+const list = ref() // 列表的数据
+const total = ref(0) // 总条数
+const tabPosition = ref('craft') // 当前选中的标签页
+const multiple = ref(true) // 是否多选
+const isExpandAll = ref(true) // 是否展开,默认全部展开
+const refreshTable = ref(true) // 重新渲染表格状态
+const treeDataRef = ref() // 树形组件引用
+const workstationOptions = ref([]) // 岗位树选项
+
+// 查询参数
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  machineryName: undefined,
+  machineryCode: undefined,
+  machineryType: undefined,
+  workstationName: undefined,
+  workstationId: undefined
+})
+
+// 树形配置
+const defaultProps = {
+  children: 'children',
+  label: 'label'
+}
+
+// 监听标签页变化
+watch(() => tabPosition.value, (newVal) => {
+  if (newVal) {
+    getList()
+  }
+})
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const type = tabPosition.value === 'craft' ? '工艺' : '设备'
+    queryParams.machineryType = type
+    const data = await TechnologyApi.listTechnology(queryParams)
+    list.value = handleTree(data.records)
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 获取其他数据 */
+const getOtherList = async () => {
+  const data = { pageNo: 1, pageSize: -1 }
+  try {
+    const [lotoResponse, marsResponse] = await Promise.all([
+      LotoApi.listLoto(data),
+      MarsDeptApi.listMarsDept(data)
+    ])
+    workstationOptions.value = handleTree(marsResponse.records)
+  } catch (error) {
+    console.error('获取数据失败:', error)
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryParams.pageNo = 1
+  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 TechnologyApi.delTechnology(id)
+    message.success(t('common.delSuccess'))
+    // 刷新列表
+    await getList()
+  } catch {}
+}
+
+/** 树节点点击事件 */
+const handleNodeClick = (data: any) => {
+  queryParams.workstationId = data.id
+  queryParams.workstationName = data.label
+  getList()
+}
+
+/** 搜索框输入事件 */
+const handleInputChange = (val: string) => {
+  treeDataRef.value?.filter(val)
+}
+
+/** 搜索框清空事件 */
+const handleClear = () => {
+  queryParams.workstationName = undefined
+  queryParams.workstationId = undefined
+  getList()
+}
+
+/** 树节点过滤方法 */
+const filterNode = (value: string, data: any) => {
+  if (!value) return true
+  return data.label.indexOf(value) !== -1
+}
+
+/** 查看详情 */
+const handleLook = (row: any) => {
+  if (tabPosition.value === 'craft') {
+    router.push(`/dv/technology/technologyDetail/CraftDetail?machineryId=${row.machineryId}`)
+  } else {
+    router.push(`/dv/technology/technologyDetail/DeviceDetail?machineryId=${row.machineryId}`)
+  }
+}
+
+/** 初始化 **/
+onMounted(async () => {
+  await getList()
+  await getOtherList()
+})
+</script>
+
+<style scoped lang="scss">
+.app-container {
+  width: 100%;
+  height: 100%;
+  display: flex;
+}
+
+.left {
+  width: 15%;
+  height: 100%;
+  margin-right: 10px;
+}
+
+.deptTree {
+  width: 100%;
+  height: 90%;
+}
+
+.right {
+  width: 83%;
+  height: 100%;
+}
+</style>

+ 641 - 0
src/views/dv/technology/technologyDetail/CraftDetail.vue

@@ -0,0 +1,641 @@
+<template>
+  <div class="app-container">
+    <el-radio-group v-model="tabPosition" class="mb-30px">
+      <el-radio-button label="craftInfo">工艺信息</el-radio-button>
+      <el-radio-button label="deviceList">设备列表</el-radio-button>
+      <el-radio-button label="Loto">锁定站</el-radio-button>
+      <el-radio-button label="sopList">SOP列表</el-radio-button>
+    </el-radio-group>
+
+    <!-- 工艺信息 -->
+    <div v-if="tabPosition === 'craftInfo'">
+      <Tinymce />
+    </div>
+
+    <!-- 设备列表 -->
+    <div v-if="tabPosition === 'deviceList'">
+      <el-row :gutter="10" class="mb8">
+        <el-col :span="1.5">
+          <el-button
+            v-hasPermi="['iscs:machinery:add']"
+            type="primary"
+            plain
+            @click="handleAdd"
+          >
+            <Icon icon="ep:plus" />
+            新增
+          </el-button>
+        </el-col>
+        <el-col :span="1.5">
+          <el-button
+            v-hasPermi="['iscs:machinery:remove']"
+            type="danger"
+            plain
+            :disabled="multiple"
+            @click="handleDelete"
+          >
+            <Icon icon="ep:delete" />
+            批量删除
+          </el-button>
+        </el-col>
+        <right-toolbar v-model:showSearch="showSearch" @queryTable="getList" />
+      </el-row>
+
+      <el-table
+        v-loading="loading"
+        :data="deviceList"
+        row-key="machineryId"
+        :default-expand-all="isExpandAll"
+        :tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
+        @selection-change="handleSelectionChange"
+      >
+        <el-table-column type="selection" width="55" align="center" />
+        <el-table-column prop="machineryCode" label="设备编码" />
+        <el-table-column prop="machineryName" label="设备名称" />
+        <el-table-column prop="machineryImg" label="设备图">
+          <template #default="{ row }">
+            <el-image
+              :src="row.machineryImg"
+              :preview-src-list="[row.machineryImg]"
+              fit="cover"
+              class="w-50px h-50px"
+            />
+          </template>
+        </el-table-column>
+        <el-table-column label="操作" align="center" width="200">
+          <template #default="{ row }">
+            <el-button
+              v-hasPermi="['iscs:machinery:edit']"
+              type="primary"
+              link
+              @click="handleUpdate(row)"
+            >
+              <Icon icon="ep:edit" />
+              编辑
+            </el-button>
+            <el-button
+              v-hasPermi="['iscs:machinery:remove']"
+              type="danger"
+              link
+              @click="handleDelete(row)"
+            >
+              <Icon icon="ep:delete" />
+              删除
+            </el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <pagination
+        v-if="total > 0"
+        v-model:total="total"
+        v-model:page="queryParams.pageNo"
+        v-model:limit="queryParams.pageSize"
+        @pagination="getList"
+      />
+    </div>
+
+    <!-- LOTO站 -->
+    <div v-if="tabPosition === 'Loto'">
+      <MapData :machinery-id="route.query.machineryId" />
+    </div>
+
+    <!-- SOP列表 -->
+    <div v-if="tabPosition === 'sopList'">
+      <el-row :gutter="10" class="mb8">
+        <el-col :span="1.5">
+          <el-button
+            v-hasPermi="['iscs:machinery:add']"
+            type="primary"
+            plain
+            @click="handleAddSop"
+          >
+            <Icon icon="ep:plus" />
+            新增
+          </el-button>
+        </el-col>
+        <el-col :span="1.5">
+          <el-button
+            v-hasPermi="['iscs:machinery:remove']"
+            type="danger"
+            plain
+            :disabled="multiple"
+            @click="handleSopDelete"
+          >
+            <Icon icon="ep:delete" />
+            批量删除
+          </el-button>
+        </el-col>
+      </el-row>
+
+      <el-table
+        v-loading="loading"
+        :data="sopList"
+        row-key="sopId"
+        @selection-change="handleSopSelectionChange"
+      >
+        <el-table-column type="selection" width="55" align="center" />
+        <el-table-column prop="sopName" label="SOP名称" />
+        <el-table-column prop="machineryName" label="工作内容" />
+        <el-table-column label="操作" align="center" width="200">
+          <template #default="{ row }">
+            <el-button
+              v-hasPermi="['iscs:machinery:edit']"
+              type="primary"
+              link
+              @click="handleSopUpdate(row)"
+            >
+              <Icon icon="ep:edit" />
+              编辑
+            </el-button>
+            <el-button
+              v-hasPermi="['iscs:machinery:remove']"
+              type="danger"
+              link
+              @click="handleSopDelete(row)"
+            >
+              <Icon icon="ep:delete" />
+              删除
+            </el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <pagination
+        v-if="sopTotal > 0"
+        v-model:total="sopTotal"
+        v-model:page="queryParams.pageNo"
+        v-model:limit="queryParams.pageSize"
+        @pagination="getSopList"
+      />
+    </div>
+
+    <!-- 设备表单弹窗 -->
+    <Dialog v-model="dialogVisible" :title="dialogTitle" width="800">
+      <el-form
+        ref="formRef"
+        v-loading="formLoading"
+        :model="formData"
+        :rules="formRules"
+        label-width="120px"
+      >
+        <el-form-item label="设备名称" prop="machineryName">
+          <el-input v-model="formData.machineryName" placeholder="请输入设备名称" />
+        </el-form-item>
+
+        <el-row>
+          <el-col :span="17">
+            <el-form-item label="设备编号" prop="machineryCode">
+              <el-input v-model="formData.machineryCode" placeholder="请输入设备编号" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="7">
+            <el-form-item label-width="30">
+              <el-switch
+                v-model="autoGenFlag"
+                active-text="自动生成"
+                @change="handleAutoGenChange"
+              />
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <el-form-item label="岗位" prop="workstationId">
+          <el-tree-select
+            v-model="formData.workstationId"
+            :data="workstationOptions"
+            :props="defaultProps"
+            placeholder="请选择岗位"
+            disabled
+          />
+        </el-form-item>
+
+        <el-form-item label="所属电柜" prop="lotoId">
+          <el-select
+            v-model="formData.lotoId"
+            placeholder="请选择所属电柜"
+            class="!w-300px"
+            disabled
+          >
+            <el-option
+              v-for="dict in lotoOptions"
+              :key="dict.value"
+              :label="dict.label"
+              :value="dict.value"
+            />
+          </el-select>
+        </el-form-item>
+
+        <el-form-item label="设备图" prop="machineryImg">
+          <ImageUpload
+            v-model="formData.machineryImg"
+            :limit="1"
+            :file-size="5"
+            @onUploaded="handleImgUploaded"
+            @onRemoved="handleImgRemoved"
+          />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
+        <el-button @click="dialogVisible = false">取 消</el-button>
+      </template>
+    </Dialog>
+
+    <!-- SOP表单弹窗 -->
+    <Dialog v-model="sopDialogVisible" :title="sopDialogTitle" width="800">
+      <el-form
+        ref="sopFormRef"
+        v-loading="formLoading"
+        :model="sopFormData"
+        :rules="sopFormRules"
+        label-width="100px"
+      >
+        <el-form-item label="所属岗位" prop="workstationId">
+          <el-tree-select
+            v-model="sopFormData.workstationId"
+            :data="workstationOptions"
+            :props="defaultProps"
+            placeholder="选择岗位"
+            disabled
+          />
+        </el-form-item>
+
+        <el-form-item label="设备/工艺" prop="machineryId">
+          <el-tree-select
+            v-model="sopFormData.machineryId"
+            :data="machineryOptions"
+            :props="defaultProps"
+            placeholder="选择设备/工艺"
+            disabled
+          />
+        </el-form-item>
+
+        <el-form-item label="SOP类型" prop="sopType">
+          <el-select v-model="sopFormData.sopType" placeholder="请选择SOP类型" clearable>
+            <el-option
+              v-for="dict in getIntDictOptions(DICT_TYPE.SOP_TYPE)"
+              :key="dict.value"
+              :label="dict.label"
+              :value="dict.value"
+            />
+          </el-select>
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button :disabled="formLoading" type="primary" @click="submitSopForm">确 定</el-button>
+        <el-button @click="sopDialogVisible = false">取 消</el-button>
+      </template>
+    </Dialog>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { ref, reactive, onMounted } from 'vue'
+import { useRoute } from 'vue-router'
+import { useI18n } from 'vue-i18n'
+import { useMessage } from '@/hooks/web/useMessage'
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import * as TechnologyApi from '@/api/dv/technology'
+import * as LotoApi from '@/api/dv/lotoStation'
+import * as SopApi from '@/api/sop/sopindex'
+import { genCode } from '@/api/system/autocode/rule'
+import MapData from './MapData.vue'
+import Tinymce from '@/components/tinymce/example/Index.vue'
+
+defineOptions({ name: 'TechnologyDetail' })
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+const route = useRoute()
+
+// 数据列表相关
+const loading = ref(false)
+const deviceList = ref([])
+const sopList = ref([])
+const total = ref(0)
+const sopTotal = ref(0)
+const multiple = ref(true)
+const showSearch = ref(true)
+const isExpandAll = ref(true)
+const tabPosition = ref('craftInfo')
+
+// 查询参数
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  machineryName: undefined,
+  machineryCode: undefined,
+  machineryId: undefined
+})
+
+// 表单相关
+const dialogVisible = ref(false)
+const dialogTitle = ref('')
+const formLoading = ref(false)
+const formRef = ref()
+const formData = ref({
+  machineryId: undefined,
+  parentId: undefined,
+  machineryName: '',
+  machineryCode: '',
+  workstationId: undefined,
+  lotoId: undefined,
+  machineryImg: undefined,
+  machineryType: '设备'
+})
+
+// 表单校验规则
+const formRules = reactive({
+  machineryName: [{ required: true, message: '设备名称不能为空', trigger: 'blur' }],
+  machineryCode: [{ required: true, message: '设备编号不能为空', trigger: 'blur' }],
+  workstationId: [{ required: true, message: '岗位不能为空', trigger: 'blur' }],
+  lotoId: [{ required: true, message: '电柜不能为空', trigger: 'blur' }]
+})
+
+// SOP表单相关
+const sopDialogVisible = ref(false)
+const sopDialogTitle = ref('')
+const sopFormRef = ref()
+const sopFormData = ref({
+  sopId: undefined,
+  workstationId: undefined,
+  machineryId: undefined,
+  sopType: undefined
+})
+
+// SOP表单校验规则
+const sopFormRules = reactive({
+  sopType: [{ required: true, message: 'SOP类型不能为空', trigger: 'change' }]
+})
+
+// 选项数据
+const workstationOptions = ref([])
+const machineryOptions = ref([])
+const lotoOptions = ref([])
+const defaultProps = {
+  children: 'children',
+  label: 'label'
+}
+
+// 自动生成编码
+const autoGenFlag = ref(false)
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = {
+      ...queryParams,
+      parentId: route.query.machineryId,
+      machineryType: '设备'
+    }
+    const res = await TechnologyApi.listTechnology(data)
+    deviceList.value = res.data.records
+    total.value = res.data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 查询SOP列表 */
+const getSopList = async () => {
+  loading.value = true
+  try {
+    const data = {
+      ...queryParams,
+      machineryId: route.query.machineryId
+    }
+    const res = await SopApi.getIsMarsSopPage(data)
+    sopList.value = res.data.records
+    sopTotal.value = res.data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 获取选项数据 */
+const getOptions = async () => {
+  // 获取岗位树
+  const marsRes = await TechnologyApi.listMarsDept({ pageSize: -1 })
+  workstationOptions.value = marsRes.data.records
+
+  // 获取设备工艺树
+  const techRes = await TechnologyApi.listTechnology({ pageSize: -1 })
+  machineryOptions.value = techRes.data.records
+
+  // 获取电柜列表
+  const lotoRes = await LotoApi.listLoto({ pageSize: -1 })
+  lotoOptions.value = lotoRes.data.records.map(item => ({
+    value: item.lotoId,
+    label: item.lotoName
+  }))
+}
+
+/** 打开设备表单 */
+const open = async (type: string, id?: number) => {
+  dialogVisible.value = true
+  dialogTitle.value = t('action.' + type)
+  resetForm()
+  if (id) {
+    formLoading.value = true
+    try {
+      formData.value = await TechnologyApi.getTechnologyInfo(id)
+    } finally {
+      formLoading.value = false
+    }
+  }
+}
+
+/** 打开SOP表单 */
+const openSop = async (type: string, id?: number) => {
+  sopDialogVisible.value = true
+  sopDialogTitle.value = t('action.' + type)
+  resetSopForm()
+  if (id) {
+    formLoading.value = true
+    try {
+      sopFormData.value = await SopApi.selectIsMarsSopById(id)
+    } finally {
+      formLoading.value = false
+    }
+  }
+}
+
+/** 新增设备 */
+const handleAdd = () => {
+  formData.value.parentId = route.query.machineryId
+  open('create')
+}
+
+/** 新增SOP */
+const handleAddSop = () => {
+  openSop('create')
+}
+
+/** 修改设备 */
+const handleUpdate = (row: any) => {
+  open('update', row.machineryId)
+}
+
+/** 修改SOP */
+const handleSopUpdate = (row: any) => {
+  openSop('update', row.sopId)
+}
+
+/** 删除设备 */
+const handleDelete = async (row?: any) => {
+  const ids = row?.machineryId || selectedIds.value
+  await message.confirm('确认删除数据项?')
+  try {
+    await TechnologyApi.delTechnology(ids)
+    message.success(t('common.delSuccess'))
+    getList()
+  } catch {}
+}
+
+/** 删除SOP */
+const handleSopDelete = async (row?: any) => {
+  const ids = row?.sopId || selectedSopIds.value
+  await message.confirm('确认删除数据项?')
+  try {
+    await SopApi.deleteIsMarsSopByMarsSopIds(ids)
+    message.success(t('common.delSuccess'))
+    getSopList()
+  } catch {}
+}
+
+/** 提交设备表单 */
+const submitForm = async () => {
+  if (!formRef.value) return
+  const valid = await formRef.value.validate()
+  if (!valid) return
+  formLoading.value = true
+  try {
+    const data = formData.value
+    if (data.machineryId) {
+      await TechnologyApi.updateTechnology(data)
+      message.success(t('common.updateSuccess'))
+    } else {
+      await TechnologyApi.addTechnology(data)
+      message.success(t('common.createSuccess'))
+    }
+    dialogVisible.value = false
+    getList()
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 提交SOP表单 */
+const submitSopForm = async () => {
+  if (!sopFormRef.value) return
+  const valid = await sopFormRef.value.validate()
+  if (!valid) return
+  formLoading.value = true
+  try {
+    const data = sopFormData.value
+    if (data.sopId) {
+      await SopApi.updateIsMarsSop(data)
+      message.success(t('common.updateSuccess'))
+    } else {
+      await SopApi.addinsertIsMarsSop(data)
+      message.success(t('common.createSuccess'))
+    }
+    sopDialogVisible.value = false
+    getSopList()
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 自动生成编码 */
+const handleAutoGenChange = async (val: boolean) => {
+  if (val) {
+    const res = await genCode('TECHNOLOGY_CODE')
+    formData.value.machineryCode = res.data
+  } else {
+    formData.value.machineryCode = ''
+  }
+}
+
+/** 图片上传成功 */
+const handleImgUploaded = (urls: string[]) => {
+  formData.value.machineryImg = urls[0]
+}
+
+/** 图片移除 */
+const handleImgRemoved = () => {
+  formData.value.machineryImg = undefined
+}
+
+/** 重置设备表单 */
+const resetForm = () => {
+  formData.value = {
+    machineryId: undefined,
+    parentId: undefined,
+    machineryName: '',
+    machineryCode: '',
+    workstationId: undefined,
+    lotoId: undefined,
+    machineryImg: undefined,
+    machineryType: '设备'
+  }
+  formRef.value?.resetFields()
+  autoGenFlag.value = false
+}
+
+/** 重置SOP表单 */
+const resetSopForm = () => {
+  sopFormData.value = {
+    sopId: undefined,
+    workstationId: undefined,
+    machineryId: undefined,
+    sopType: undefined
+  }
+  sopFormRef.value?.resetFields()
+}
+
+/** 表格选择 */
+const selectedIds = ref<number[]>([])
+const handleSelectionChange = (selection: any[]) => {
+  selectedIds.value = selection.map(item => item.machineryId)
+  multiple.value = !selection.length
+}
+
+/** SOP表格选择 */
+const selectedSopIds = ref<number[]>([])
+const handleSopSelectionChange = (selection: any[]) => {
+  selectedSopIds.value = selection.map(item => item.sopId)
+  multiple.value = !selection.length
+}
+
+onMounted(() => {
+  getList()
+  getSopList()
+  getOptions()
+})
+</script>
+
+<style scoped>
+.app-container {
+  width: 100%;
+  height: 100%;
+}
+
+.mb-30px {
+  margin-bottom: 30px;
+}
+
+.w-50px {
+  width: 50px;
+}
+
+.h-50px {
+  height: 50px;
+}
+
+.w-300px {
+  width: 300px;
+}
+</style>

+ 312 - 0
src/views/dv/technology/technologyDetail/DeviceDetail.vue

@@ -0,0 +1,312 @@
+<template>
+  <div class="app-container">
+    <el-radio-group v-model="tabPosition" class="mb-30px">
+      <el-radio-button label="deviceInfo">设备信息</el-radio-button>
+      <el-radio-button label="Loto">锁定站</el-radio-button>
+    </el-radio-group>
+
+    <!-- 设备信息 -->
+    <div v-if="tabPosition === 'deviceInfo'">
+      <Tinymce />
+    </div>
+
+    <!-- LOTO站 -->
+    <div v-if="tabPosition === 'Loto'">
+      <MapData :machinery-id="route.query.machineryId" />
+    </div>
+
+    <!-- 设备表单弹窗 -->
+    <Dialog v-model="dialogVisible" :title="dialogTitle" width="800">
+      <el-form
+        ref="formRef"
+        v-loading="formLoading"
+        :model="formData"
+        :rules="formRules"
+        label-width="120px"
+      >
+        <el-form-item label="上级" prop="parentId">
+          <el-tree-select
+            v-model="formData.parentId"
+            :data="machineryOptions"
+            :props="defaultProps"
+            placeholder="选择上级"
+          />
+        </el-form-item>
+
+        <el-form-item label="设备/工艺名称" prop="machineryName">
+          <el-input v-model="formData.machineryName" placeholder="请输入设备/工艺名称" />
+        </el-form-item>
+
+        <el-row>
+          <el-col :span="18">
+            <el-form-item label="设备/工艺编号" prop="machineryCode">
+              <el-input v-model="formData.machineryCode" placeholder="请输入设备/工艺编号" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="6">
+            <el-form-item label-width="30">
+              <el-switch
+                v-model="autoGenFlag"
+                active-text="自动生成"
+                @change="handleAutoGenChange"
+              />
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <el-form-item label="岗位" prop="workstationId">
+          <el-tree-select
+            v-model="formData.workstationId"
+            :data="workstationOptions"
+            :props="defaultProps"
+            placeholder="请选择岗位"
+          />
+        </el-form-item>
+
+        <el-form-item label="所属电柜" prop="lotoId">
+          <el-select
+            v-model="formData.lotoId"
+            placeholder="请选择所属电柜"
+            class="!w-300px"
+          >
+            <el-option
+              v-for="dict in lotoOptions"
+              :key="dict.value"
+              :label="dict.label"
+              :value="dict.value"
+            />
+          </el-select>
+        </el-form-item>
+
+        <el-form-item label="设备/工艺类型" prop="machineryType">
+          <el-input v-model="formData.machineryType" placeholder="请输入设备/工艺类型" />
+        </el-form-item>
+
+        <el-form-item label="工艺图" prop="machineryImg">
+          <ImageUpload
+            v-model="formData.machineryImg"
+            :limit="1"
+            :file-size="5"
+            @onUploaded="handleImgUploaded"
+            @onRemoved="handleImgRemoved"
+          />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
+        <el-button @click="dialogVisible = false">取 消</el-button>
+      </template>
+    </Dialog>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { ref, reactive, onMounted, watch } from 'vue'
+import { useRoute } from 'vue-router'
+import { useI18n } from 'vue-i18n'
+import { useMessage } from '@/hooks/web/useMessage'
+import * as TechnologyApi from '@/api/dv/technology'
+import * as LotoApi from '@/api/dv/lotoStation'
+import { genCode } from '@/api/system/autocode/rule'
+import MapData from './MapData.vue'
+import Tinymce from '@/components/tinymce/example/Index.vue'
+
+defineOptions({ name: 'DeviceDetail' })
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+const route = useRoute()
+
+// 数据列表相关
+const loading = ref(false)
+const tabPosition = ref('deviceInfo')
+const multiple = ref(true)
+const showSearch = ref(true)
+const isExpandAll = ref(true)
+
+// 表单相关
+const dialogVisible = ref(false)
+const dialogTitle = ref('')
+const formLoading = ref(false)
+const formRef = ref()
+const formData = ref({
+  machineryId: undefined,
+  parentId: undefined,
+  machineryName: '',
+  machineryCode: '',
+  workstationId: undefined,
+  lotoId: undefined,
+  machineryType: '',
+  machineryImg: undefined
+})
+
+// 表单校验规则
+const formRules = reactive({
+  machineryName: [{ required: true, message: '设备/工艺名称不能为空', trigger: 'blur' }],
+  machineryCode: [{ required: true, message: '设备/工艺编号不能为空', trigger: 'blur' }],
+  workstationId: [{ required: true, message: '岗位不能为空', trigger: 'blur' }],
+  lotoId: [{ required: true, message: '电柜不能为空', trigger: 'blur' }]
+})
+
+// 选项数据
+const workstationOptions = ref([])
+const machineryOptions = ref([])
+const lotoOptions = ref([])
+const defaultProps = {
+  children: 'children',
+  label: 'label'
+}
+
+// 自动生成编码
+const autoGenFlag = ref(false)
+
+// 监听岗位变化
+watch(() => formData.value.workstationId, (newVal) => {
+  if (newVal) {
+    getLotoList()
+  }
+})
+
+/** 获取电柜列表 */
+const getLotoList = async () => {
+  try {
+    const res = await LotoApi.listLoto({
+      pageSize: -1,
+      workstationId: formData.value.workstationId
+    })
+    lotoOptions.value = res.data.records.map(item => ({
+      value: item.lotoId,
+      label: item.lotoName
+    }))
+  } catch (error) {
+    console.error('获取电柜列表失败:', error)
+  }
+}
+
+/** 获取选项数据 */
+const getOptions = async () => {
+  // 获取岗位树
+  const marsRes = await TechnologyApi.listMarsDept({ pageSize: -1 })
+  workstationOptions.value = marsRes.data.records
+
+  // 获取设备工艺树
+  const techRes = await TechnologyApi.listTechnology({ pageSize: -1 })
+  machineryOptions.value = techRes.data.records
+
+  // 获取电柜列表
+  await getLotoList()
+}
+
+/** 打开表单 */
+const open = async (type: string, id?: number) => {
+  dialogVisible.value = true
+  dialogTitle.value = t('action.' + type)
+  resetForm()
+  if (id) {
+    formLoading.value = true
+    try {
+      formData.value = await TechnologyApi.getTechnologyInfo(id)
+    } finally {
+      formLoading.value = false
+    }
+  }
+}
+
+/** 新增 */
+const handleAdd = (row?: any) => {
+  formData.value.parentId = row?.machineryId || 0
+  open('create')
+}
+
+/** 修改 */
+const handleUpdate = (row: any) => {
+  open('update', row.machineryId)
+}
+
+/** 删除 */
+const handleDelete = async (row: any) => {
+  await message.confirm('确认删除数据项?')
+  try {
+    await TechnologyApi.delTechnology(row.machineryId)
+    message.success(t('common.delSuccess'))
+    getOptions()
+  } catch {}
+}
+
+/** 提交表单 */
+const submitForm = async () => {
+  if (!formRef.value) return
+  const valid = await formRef.value.validate()
+  if (!valid) return
+  formLoading.value = true
+  try {
+    const data = formData.value
+    if (data.machineryId) {
+      await TechnologyApi.updateTechnology(data)
+      message.success(t('common.updateSuccess'))
+    } else {
+      await TechnologyApi.addTechnology(data)
+      message.success(t('common.createSuccess'))
+    }
+    dialogVisible.value = false
+    getOptions()
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 自动生成编码 */
+const handleAutoGenChange = async (val: boolean) => {
+  if (val) {
+    const res = await genCode('TECHNOLOGY_CODE')
+    formData.value.machineryCode = res.data
+  } else {
+    formData.value.machineryCode = ''
+  }
+}
+
+/** 图片上传成功 */
+const handleImgUploaded = (urls: string[]) => {
+  formData.value.machineryImg = urls[0]
+}
+
+/** 图片移除 */
+const handleImgRemoved = () => {
+  formData.value.machineryImg = undefined
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    machineryId: undefined,
+    parentId: undefined,
+    machineryName: '',
+    machineryCode: '',
+    workstationId: undefined,
+    lotoId: undefined,
+    machineryType: '',
+    machineryImg: undefined
+  }
+  formRef.value?.resetFields()
+  autoGenFlag.value = false
+}
+
+onMounted(() => {
+  getOptions()
+})
+</script>
+
+<style scoped>
+.app-container {
+  width: 100%;
+  height: 100%;
+}
+
+.mb-30px {
+  margin-bottom: 30px;
+}
+
+.w-300px {
+  width: 300px;
+}
+</style>

+ 335 - 0
src/views/dv/technology/technologyDetail/MapData.vue

@@ -0,0 +1,335 @@
+<template>
+  <div class="mapdata">
+    <div id="container" ref="container" class="w-full h-full" />
+    <div class="left">
+      <div class="bottombtn w-full h-35px">
+        <el-button
+          type="primary"
+          @click="handleSave"
+        >
+          <Icon icon="ep:edit" />
+          保存
+        </el-button>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { ref, onMounted } from 'vue'
+import { useRoute } from 'vue-router'
+import { useI18n } from 'vue-i18n'
+import { useMessage } from '@/hooks/web/useMessage'
+import Konva from 'konva'
+import * as TechnologyApi from '@/api/dv/technology'
+import * as LotoApi from '@/api/dv/lotoStation'
+import * as MapApi from '@/api/basic/mapconfig'
+
+defineOptions({ name: 'MapData' })
+
+const props = defineProps({
+  machineryId: {
+    type: String,
+    default: ''
+  }
+})
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+const route = useRoute()
+
+// Konva 相关
+const container = ref<HTMLElement>()
+const stage = ref<Konva.Stage>()
+const layer = ref<Konva.Layer>()
+const backgroundLayer = ref<Konva.Layer>()
+const gridLayer = ref<Konva.Layer>()
+
+// 数据相关
+const selectedStates = ref<boolean[]>([])
+const selectedText = ref<string[]>([])
+const rects = ref<Record<string, Konva.Rect>>({})
+const texts = ref<Record<string, Konva.Text>>({})
+const redrects = ref<Record<string, Konva.Rect>>({})
+const redtexts = ref<Record<string, Konva.Text>>({})
+const pointIdList = ref<number[]>([])
+const selectPoints = ref<number[]>([])
+
+// 表单数据
+const formData = ref({
+  map: null,
+  mapId: undefined,
+  imageUrl: '',
+  width: 0,
+  height: 0,
+  x: 0,
+  y: 0,
+  pointList: []
+})
+
+/** 初始化数据 */
+const initData = async () => {
+  try {
+    // 获取设备工艺信息
+    const techRes = await TechnologyApi.getTechnologyInfo(props.machineryId)
+    const lotoId = techRes.data.lotoId
+    selectPoints.value = techRes.data.pointIdList
+
+    // 获取电柜地图信息
+    const mapRes = await LotoApi.selectLotoMapById(lotoId, '', '')
+    formData.value.map = mapRes.data
+
+    // 获取电柜信息
+    const lotoRes = await LotoApi.selectIsLotoStationById(lotoId)
+    formData.value = { ...formData.value, ...lotoRes.data }
+
+    // 获取地图配置
+    const configRes = await MapApi.selectIsMapById(lotoRes.data.mapId)
+    formData.value = {
+      ...formData.value,
+      imageUrl: configRes.data.imageUrl,
+      width: configRes.data.width,
+      height: configRes.data.height,
+      x: configRes.data.x,
+      y: configRes.data.y,
+      pointList: configRes.data.pointList
+    }
+
+    initKonva()
+  } catch (error) {
+    console.error('初始化数据失败:', error)
+  }
+}
+
+/** 初始化 Konva */
+const initKonva = () => {
+  if (!container.value) return
+
+  // 创建舞台
+  stage.value = new Konva.Stage({
+    container: container.value,
+    width: 1600,
+    height: 860
+  })
+
+  // 创建网格图层
+  gridLayer.value = new Konva.Layer()
+  stage.value.add(gridLayer.value)
+  drawGrid(50, 50, '#e0e0e0', gridLayer.value)
+
+  // 创建底图图层
+  backgroundLayer.value = new Konva.Layer()
+  stage.value.add(backgroundLayer.value)
+
+  // 加载底图
+  const bgImage = new Image()
+  bgImage.src = formData.value.imageUrl
+  bgImage.onload = () => {
+    const konvaImage = new Konva.Image({
+      x: formData.value.x || 0,
+      y: formData.value.y || 0,
+      image: bgImage,
+      width: formData.value.width || 1200,
+      height: formData.value.height || 860,
+      draggable: false
+    })
+    backgroundLayer.value?.add(konvaImage)
+    backgroundLayer.value?.draw()
+  }
+
+  // 创建主图层
+  layer.value = new Konva.Layer()
+  stage.value.add(layer.value)
+
+  // 渲染隔离点
+  renderGrid()
+
+  // 禁止舞台拖拽
+  stage.value.draggable(false)
+}
+
+/** 绘制网格 */
+const drawGrid = (cellWidth: number, cellHeight: number, gridColor: string, layer: Konva.Layer) => {
+  const width = 1600
+  const height = 860
+
+  // 绘制竖线
+  for (let i = 0; i <= width; i += cellWidth) {
+    const verticalLine = new Konva.Line({
+      points: [i, 0, i, height],
+      stroke: gridColor,
+      strokeWidth: 1
+    })
+    layer.add(verticalLine)
+  }
+
+  // 绘制横线
+  for (let j = 0; j <= height; j += cellHeight) {
+    const horizontalLine = new Konva.Line({
+      points: [0, j, width, j],
+      stroke: gridColor,
+      strokeWidth: 1
+    })
+    layer.add(horizontalLine)
+  }
+
+  layer.draw()
+}
+
+/** 渲染隔离点 */
+const renderGrid = () => {
+  if (!layer.value) return
+
+  // 重置数据
+  selectedStates.value = []
+  rects.value = {}
+  texts.value = {}
+  redrects.value = {}
+  redtexts.value = {}
+  selectedText.value = []
+  pointIdList.value = []
+
+  // 渲染点位
+  formData.value.pointList.forEach((pos) => {
+    const x = pos.x * 50
+    const y = pos.y * 50
+    const labelText = pos.entityName
+
+    const point = new Image()
+    point.src = pos.pointIcon
+    point.onload = () => {
+      // 创建点位图片
+      const konvaImage = new Konva.Image({
+        x: x + 2,
+        y: y,
+        image: point,
+        width: 45,
+        height: 45,
+        draggable: false
+      })
+
+      // 添加点击事件
+      konvaImage.on('click', () => {
+        const isCurrentlySelected = redrects.value[labelText]?.visible()
+
+        if (isCurrentlySelected) {
+          redrects.value[labelText]?.visible(false)
+          redtexts.value[labelText]?.visible(false)
+          pointIdList.value = pointIdList.value.filter(id => id !== pos.entityId)
+        } else {
+          redrects.value[labelText]?.visible(true)
+          redtexts.value[labelText]?.visible(true)
+          pointIdList.value.push(pos.entityId)
+        }
+
+        layer.value?.batchDraw()
+      })
+
+      // 创建背景矩形
+      const bgrect = new Konva.Rect({
+        x: x - 2,
+        y: y - 5,
+        width: 50,
+        height: 78,
+        cornerRadius: 5,
+        stroke: 'white',
+        strokeWidth: 2,
+        fill: 'white'
+      })
+      layer.value?.add(bgrect)
+      rects.value[labelText] = bgrect
+
+      // 添加图片
+      layer.value?.add(konvaImage)
+
+      // 创建文本
+      const text = new Konva.Text({
+        x: x + 8,
+        y: y + 50,
+        fontSize: 17,
+        text: labelText,
+        fontFamily: 'Calibri',
+        fill: 'red'
+      })
+      layer.value?.add(text)
+      texts.value[labelText] = text
+
+      // 创建选中状态覆盖层
+      const redrect = new Konva.Rect({
+        x: x - 3,
+        y: y - 6,
+        width: 52,
+        height: 80,
+        cornerRadius: 5,
+        fill: 'rgba(97, 97, 97, 0.5)',
+        visible: false,
+        listening: false
+      })
+      layer.value?.add(redrect)
+      redrects.value[labelText] = redrect
+
+      // 创建选中状态对号
+      const redtext = new Konva.Text({
+        x: x - 8 + 42 / 2,
+        y: y + 50 / 2,
+        fontSize: 24,
+        text: '✔',
+        fontFamily: 'Arial',
+        fill: 'white',
+        align: 'center',
+        verticalAlign: 'middle',
+        visible: false,
+        listening: false
+      })
+      layer.value?.add(redtext)
+      redtexts.value[labelText] = redtext
+
+      // 设置初始选中状态
+      if (selectPoints.value?.includes(pos.entityId)) {
+        redrects.value[labelText]?.visible(true)
+        redtexts.value[labelText]?.visible(true)
+        pointIdList.value.push(pos.entityId)
+      }
+
+      layer.value?.draw()
+    }
+  })
+}
+
+/** 保存 */
+const handleSave = async () => {
+  await message.confirm('请确认是否保存修改内容')
+  try {
+    const data = {
+      machineryId: route.query.machineryId,
+      pointIdList: pointIdList.value
+    }
+    await TechnologyApi.saveMachineryPoints(data)
+    message.success(t('common.saveSuccess'))
+  } catch (error) {
+    console.error('保存失败:', error)
+  }
+}
+
+onMounted(() => {
+  initData()
+})
+</script>
+
+<style scoped lang="scss">
+.mapdata {
+  width: 100%;
+  height: 100%;
+  display: flex;
+}
+
+.left {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+}
+
+.h-35px {
+  height: 35px;
+}
+</style>

+ 237 - 0
src/views/email/emailNotify/EmailNotifyForm.vue

@@ -0,0 +1,237 @@
+<template>
+  <Dialog v-model="dialogVisible" :title="dialogTitle" width="550">
+    <el-form
+      ref="formRef"
+      v-loading="formLoading"
+      :model="formData"
+      :rules="formRules"
+      label-width="100px"
+    >
+      <el-form-item label="提醒事项" prop="name">
+        <el-input v-model="formData.name" placeholder="请输入提醒事项" />
+      </el-form-item>
+      <el-form-item label="邮件模板" prop="templateCode">
+        <el-select v-model="formData.templateCode">
+          <el-option
+            v-for="item in templatesList"
+            :key="item.templateCode"
+            :label="item.templateName"
+            :value="item.templateCode"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="提醒时长" prop="reminderTime">
+        <div class="time-picker">
+          <div
+            class="time-unit"
+            v-for="(unit, index) in timeUnits"
+            :key="index"
+          >
+            <select
+              v-model="timeValues.reminderTime[unit.name]"
+              :id="unit.name"
+            >
+              <option
+                v-for="option in unit.options"
+                :key="option"
+                :value="option"
+              >
+                {{ option }}
+              </option>
+            </select>
+            <label :for="unit.name">{{ unit.label }}</label>
+          </div>
+        </div>
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+
+<script lang="ts" setup>
+import { CommonStatusEnum } from '@/utils/constants'
+import * as EmailNotifyApi from '@/api/email/notify/index'
+import * as EmailTemplateApi from '@/api/email/templates/index'
+
+defineOptions({ name: 'EmailNotifyForm' })
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const dialogTitle = ref('') // 弹窗的标题
+const formLoading = ref(false) // 表单的加载中
+const formType = ref('') // 表单的类型:create - 新增;update - 修改
+const formData = ref({
+  configId: undefined,
+  name: '',
+  templateCode: undefined,
+  reminderTime: 0,
+  status: CommonStatusEnum.ENABLE
+})
+
+const formRules = reactive({
+  name: [{ required: true, message: '提醒事项不能为空', trigger: 'blur' }],
+  templateCode: [{ required: true, message: '邮件模板不能为空', trigger: 'change' }]
+})
+
+const formRef = ref() // 表单 Ref
+
+// 时间单位配置
+const timeUnits = [
+  {
+    name: 'days',
+    label: '天',
+    options: Array.from({ length: 31 }, (_, i) => i)
+  },
+  {
+    name: 'hours',
+    label: '小时',
+    options: Array.from({ length: 24 }, (_, i) => i)
+  },
+  {
+    name: 'minutes',
+    label: '分钟',
+    options: Array.from({ length: 60 }, (_, i) => i)
+  },
+  {
+    name: 'seconds',
+    label: '秒',
+    options: Array.from({ length: 60 }, (_, i) => i)
+  }
+]
+
+// 时间值
+const timeValues = reactive({
+  reminderTime: {
+    days: 0,
+    hours: 0,
+    minutes: 0,
+    seconds: 0
+  }
+})
+
+// 邮件模板列表
+const templatesList = ref([])
+
+/** 打开弹窗 */
+const open = async (type: string, id?: number) => {
+  dialogVisible.value = true
+  dialogTitle.value = t('action.' + type)
+  formType.value = type
+  resetForm()
+  // 获取邮件模板列表
+  const data = {
+    pageNo: 1,
+    pageSize: -1
+  }
+  const res = await EmailTemplateApi.listEmailTemplates(data)
+  templatesList.value = res.records
+  // 修改时,设置数据
+  if (id) {
+    formLoading.value = true
+    try {
+      const data = await EmailNotifyApi.getIsMailNotifyConfigById(id)
+      formData.value = data
+      // 转换时间
+      const convertTime = (time: number) => ({
+        days: Math.floor(time / (24 * 60 * 60)),
+        hours: Math.floor((time % (24 * 60 * 60)) / (60 * 60)),
+        minutes: Math.floor((time % (60 * 60)) / 60),
+        seconds: time % 60
+      })
+      timeValues.reminderTime = convertTime(data.reminderTime)
+    } finally {
+      formLoading.value = false
+    }
+  }
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 校验表单
+  if (!formRef) return
+  const valid = await formRef.value.validate()
+  if (!valid) return
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = { ...formData.value }
+    // 计算总秒数
+    const calculateTotalSeconds = ({ days, hours, minutes, seconds }) =>
+      ((days * 24 + hours) * 60 + minutes) * 60 + seconds
+    data.reminderTime = calculateTotalSeconds(timeValues.reminderTime)
+
+    if (formType.value === 'create') {
+      await EmailNotifyApi.addIsMailNotifyConfig(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await EmailNotifyApi.updateIsMailNotifyConfig(data)
+      message.success(t('common.updateSuccess'))
+    }
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    configId: undefined,
+    name: '',
+    templateCode: undefined,
+    reminderTime: 0,
+    status: CommonStatusEnum.ENABLE
+  }
+  // 重置时间值
+  timeValues.reminderTime = {
+    days: 0,
+    hours: 0,
+    minutes: 0,
+    seconds: 0
+  }
+  formRef.value?.resetFields()
+}
+</script>
+
+<style lang="scss" scoped>
+.time-picker {
+  display: flex;
+  gap: 15px;
+  align-items: center;
+  padding: 0 10px;
+  border-radius: 8px;
+}
+
+.time-unit {
+  display: flex;
+}
+
+label {
+  font-size: 14px;
+  color: #333;
+  margin-left: 10px;
+}
+
+select {
+  padding: 8px;
+  margin-left: -10px;
+  font-size: 14px;
+  border: 1px solid #ddd;
+  border-radius: 4px;
+  transition: border-color 0.3s ease;
+}
+
+select:focus {
+  border-color: #007bff;
+  outline: none;
+}
+</style>

+ 188 - 0
src/views/email/emailNotify/index.vue

@@ -0,0 +1,188 @@
+<template>
+  <!-- 搜索工作栏 -->
+  <ContentWrap>
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="68px"
+    >
+      <el-form-item label="提醒事项" prop="name">
+        <el-input
+          v-model="queryParams.name"
+          placeholder="请输入提醒事项"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="邮件模板" prop="templateName">
+        <el-input
+          v-model="queryParams.templateName"
+          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="['iscs:notify:create']"
+        >
+          <Icon icon="ep:plus" class="mr-5px" /> 新增
+        </el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table
+      v-loading="loading"
+      :data="templatesNotifyList"
+      @selection-change="handleSelectionChange"
+    >
+      <el-table-column type="selection" width="55" align="center" />
+      <el-table-column label="提醒事项" prop="name" />
+      <el-table-column label="是否激活" prop="status">
+        <template #default="{ row }">
+          <el-switch
+            v-model="row.status"
+            :active-value="'1'"
+            :inactive-value="'0'"
+            @change="handleFrozenChange(row)"
+          />
+        </template>
+      </el-table-column>
+      <el-table-column prop="templateName" label="邮件模板" />
+      <el-table-column prop="reminderTime" label="提醒时长">
+        <template #default="{ row }">
+          {{ formattedTime(row.reminderTime) }}
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" align="center" width="150">
+        <template #default="{ row }">
+          <el-button
+            link
+            type="primary"
+            @click="openForm('update', row.configId)"
+            v-hasPermi="['iscs:notify:update']"
+          >
+            修改
+          </el-button>
+          <el-button
+            link
+            type="danger"
+            @click="handleDelete(row.configId)"
+            v-hasPermi="['iscs:notify:delete']"
+          >
+            删除
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+  </ContentWrap>
+
+  <!-- 表单弹窗:添加/修改 -->
+  <EmailNotifyForm ref="formRef" @success="getList" />
+</template>
+
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import * as EmailNotifyApi from '@/api/email/notify/index'
+import EmailNotifyForm from './EmailNotifyForm.vue'
+
+defineOptions({ name: 'EmailNotify' })
+
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
+
+const loading = ref(true) // 列表的加载中
+const templatesNotifyList = ref([]) // 列表的数据
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  name: undefined,
+  templateName: undefined
+})
+const queryFormRef = ref() // 搜索的表单
+const refreshTable = ref(true) // 重新渲染表格状态
+
+/** 查询邮件提醒列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await EmailNotifyApi.listIsMailNotifyConfigPage(queryParams)
+    templatesNotifyList.value = data.records
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryParams.pageNo = 1
+  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 EmailNotifyApi.deleteIsMailNotifyConfig(id)
+    message.success(t('common.delSuccess'))
+    // 刷新列表
+    await getList()
+  } catch {}
+}
+
+/** 是否激活状态修改 */
+const handleFrozenChange = async (row: any) => {
+  try {
+    await EmailNotifyApi.updateIsMailNotifyConfig(row)
+    message.success(row.status === '1' ? '激活成功' : '取消激活')
+  } catch {}
+}
+
+/** 格式化时间 */
+const formattedTime = (totalSeconds: number) => {
+  const days = Math.floor(totalSeconds / (24 * 60 * 60))
+  const hours = Math.floor((totalSeconds % (24 * 60 * 60)) / (60 * 60))
+  const minutes = Math.floor((totalSeconds % (60 * 60)) / 60)
+  const seconds = totalSeconds % 60
+
+  const parts = []
+  if (days > 0) parts.push(`${days}天`)
+  if (hours > 0) parts.push(`${hours}小时`)
+  if (minutes > 0) parts.push(`${minutes}分钟`)
+  if (seconds > 0) parts.push(`${seconds}秒`)
+
+  return parts.join(' ')
+}
+
+/** 初始化 **/
+onMounted(async () => {
+  await getList()
+})
+</script>

+ 131 - 0
src/views/email/emailTemplates/EmailTemplateForm.vue

@@ -0,0 +1,131 @@
+<template>
+  <Dialog v-model="dialogVisible" :title="dialogTitle" width="600">
+    <el-form
+      ref="formRef"
+      v-loading="formLoading"
+      :model="formData"
+      :rules="formRules"
+      label-width="120px"
+    >
+      <el-form-item label="邮件模板编号" prop="templateCode">
+        <el-input
+          :disabled="formType === 'update'"
+          v-model="formData.templateCode"
+          placeholder="请输入邮件模板编号"
+        />
+      </el-form-item>
+      <el-form-item label="邮件模板名称" prop="templateName">
+        <el-input
+          v-model="formData.templateName"
+          placeholder="请输入邮件模板名称"
+        />
+      </el-form-item>
+      <el-form-item label="邮件模板标题" prop="templateTitle">
+        <el-input
+          v-model="formData.templateTitle"
+          placeholder="请输入邮件模板标题"
+        />
+      </el-form-item>
+      <el-form-item label="邮件模板内容" prop="templateContent">
+        <el-input
+          type="textarea"
+          :rows="20"
+          v-model="formData.templateContent"
+          placeholder="请输入邮件模板内容"
+        />
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+
+<script lang="ts" setup>
+import { CommonStatusEnum } from '@/utils/constants'
+import * as EmailTemplateApi from '@/api/email/templates/index'
+
+defineOptions({ name: 'EmailTemplateForm' })
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const dialogTitle = ref('') // 弹窗的标题
+const formLoading = ref(false) // 表单的加载中
+const formType = ref('') // 表单的类型:create - 新增;update - 修改
+const formData = ref({
+  templateId: undefined,
+  templateCode: '',
+  templateName: '',
+  templateTitle: '',
+  templateContent: ''
+})
+
+const formRules = reactive({
+  templateCode: [{ required: true, message: '邮件模板编号不能为空', trigger: 'blur' }],
+  templateName: [{ required: true, message: '邮件模板名称不能为空', trigger: 'blur' }],
+  templateTitle: [{ required: true, message: '邮件模板标题不能为空', trigger: 'blur' }],
+  templateContent: [{ 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 {
+      const data = await EmailTemplateApi.getEmailTemplatesInfo(id)
+      formData.value = data
+    } finally {
+      formLoading.value = false
+    }
+  }
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 校验表单
+  if (!formRef) return
+  const valid = await formRef.value.validate()
+  if (!valid) return
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = formData.value
+    if (formType.value === 'create') {
+      await EmailTemplateApi.addEmailTemplates(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await EmailTemplateApi.updateEmailTemplates(data)
+      message.success(t('common.updateSuccess'))
+    }
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    templateId: undefined,
+    templateCode: '',
+    templateName: '',
+    templateTitle: '',
+    templateContent: ''
+  }
+  formRef.value?.resetFields()
+}
+</script>

+ 188 - 0
src/views/email/emailTemplates/index.vue

@@ -0,0 +1,188 @@
+<template>
+  <!-- 搜索工作栏 -->
+  <ContentWrap>
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="100px"
+    >
+      <el-form-item label="邮件模板名称" prop="templateName">
+        <el-input
+          v-model="queryParams.templateName"
+          placeholder="请输入邮件模板名称"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="邮件模板标题" prop="templateTitle">
+        <el-input
+          v-model="queryParams.templateTitle"
+          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="['iscs:template:create']"
+        >
+          <Icon icon="ep:plus" class="mr-5px" /> 新增
+        </el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table
+      v-loading="loading"
+      :data="templatesList"
+      @selection-change="handleSelectionChange"
+    >
+      <el-table-column type="selection" width="55" align="center" />
+      <el-table-column prop="templateCode" label="邮件模板编号" />
+      <el-table-column prop="templateName" label="邮件模板名称" />
+      <el-table-column prop="templateTitle" label="邮件模板标题" />
+      <el-table-column prop="templateContent" label="内容" width="120">
+        <template #default="{ row }">
+          <el-button link type="primary" @click="handleCheck(row)">查看</el-button>
+        </template>
+      </el-table-column>
+      <el-table-column
+        label="新增时间"
+        align="center"
+        prop="createTime"
+        width="150"
+      >
+        <template #default="{ row }">
+          <span>{{ row.createTime }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" align="center" width="150">
+        <template #default="{ row }">
+          <el-button
+            link
+            type="primary"
+            @click="openForm('update', row.templateId)"
+            v-hasPermi="['iscs:template:update']"
+          >
+            修改
+          </el-button>
+          <el-button
+            link
+            type="danger"
+            @click="handleDelete(row.templateId)"
+            v-hasPermi="['iscs:template:delete']"
+          >
+            删除
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+  </ContentWrap>
+
+  <!-- 表单弹窗:添加/修改 -->
+  <EmailTemplateForm ref="formRef" @success="getList" />
+
+  <!-- 查看内容弹窗 -->
+  <Dialog v-model="checkDialogVisible" title="内容查看" width="600">
+    <el-input
+      type="textarea"
+      v-model="checkContent"
+      :rows="20"
+      readonly
+    />
+    <template #footer>
+      <el-button @click="checkDialogVisible = false">关 闭</el-button>
+    </template>
+  </Dialog>
+</template>
+
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import * as EmailTemplateApi from '@/api/email/templates/index'
+import EmailTemplateForm from './EmailTemplateForm.vue'
+
+defineOptions({ name: 'EmailTemplate' })
+
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
+
+const loading = ref(true) // 列表的加载中
+const templatesList = ref([]) // 列表的数据
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  templateName: undefined,
+  templateTitle: undefined
+})
+const queryFormRef = ref() // 搜索的表单
+const refreshTable = ref(true) // 重新渲染表格状态
+
+// 查看内容相关
+const checkDialogVisible = ref(false)
+const checkContent = ref('')
+
+/** 查询邮件模板列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await EmailTemplateApi.listEmailTemplates(queryParams)
+    templatesList.value = data.records
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryParams.pageNo = 1
+  queryFormRef.value.resetFields()
+  handleQuery()
+}
+
+/** 添加/修改操作 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+  formRef.value.open(type, id)
+}
+
+/** 查看内容操作 */
+const handleCheck = (row: any) => {
+  checkContent.value = row.templateContent
+  checkDialogVisible.value = true
+}
+
+/** 删除按钮操作 */
+const handleDelete = async (id: number) => {
+  try {
+    // 删除的二次确认
+    await message.delConfirm()
+    // 发起删除
+    await EmailTemplateApi.delEmailTemplates(id)
+    message.success(t('common.delSuccess'))
+    // 刷新列表
+    await getList()
+  } catch {}
+}
+
+/** 初始化 **/
+onMounted(async () => {
+  await getList()
+})
+</script>

+ 321 - 0
src/views/hw/hardware/information/HardwareForm.vue

@@ -0,0 +1,321 @@
+<template>
+  <Dialog v-model="dialogVisible" :title="dialogTitle" width="800">
+    <el-form
+      ref="formRef"
+      v-loading="formLoading"
+      :model="formData"
+      :rules="formRules"
+      label-width="120px"
+    >
+      <el-row>
+        <el-col :span="8">
+          <el-form-item label="硬件编码" prop="hardwareCode">
+            <el-input
+              v-model="formData.hardwareCode"
+              placeholder="请输入硬件编码"
+              style="width: 100%"
+            />
+          </el-form-item>
+        </el-col>
+        <el-col :span="3">
+          <el-form-item label-width="80">
+            <el-switch
+              v-model="autoGenFlag"
+              active-color="#13ce66"
+              active-text="自动生成"
+              @change="handleAutoGenChange"
+              v-if="formType !== 'view'"
+            />
+          </el-form-item>
+        </el-col>
+        <el-col :span="11">
+          <el-form-item label="硬件类型" prop="hardwareTypeId">
+            <el-tree-select
+              v-model="formData.hardwareTypeId"
+              :data="hardwareTypeOption"
+              :props="defaultProps"
+              @change="onSelectChange"
+              placeholder="请选择硬件类型"
+            />
+          </el-form-item>
+        </el-col>
+      </el-row>
+      <el-row>
+        <el-col :span="11">
+          <el-form-item label="硬件名称" prop="hardwareName">
+            <el-input
+              v-model="formData.hardwareName"
+              placeholder="请输入硬件名称"
+            />
+          </el-form-item>
+        </el-col>
+        <el-col :span="11">
+          <el-form-item label="规格型号" prop="hardwareSpec">
+            <el-input
+              v-model="formData.hardwareSpec"
+              placeholder="请输入规格型号"
+            />
+          </el-form-item>
+        </el-col>
+      </el-row>
+      <el-row>
+        <el-col :span="11">
+          <el-form-item label="序列号" prop="serialNumber">
+            <el-input
+              v-model="formData.serialNumber"
+              placeholder="请输入序列号"
+            />
+          </el-form-item>
+        </el-col>
+      </el-row>
+      <el-row>
+        <el-col :span="11">
+          <el-form-item label="可用次数" prop="availableTimes">
+            <el-input
+              v-model="formData.availableTimes"
+              placeholder="请输入可用次数"
+              disabled
+            />
+          </el-form-item>
+        </el-col>
+        <el-col :span="11">
+          <el-form-item label="已用次数" prop="usedTimes">
+            <el-input
+              v-model="formData.usedTimes"
+              placeholder="请输入已用次数"
+              disabled
+            />
+          </el-form-item>
+        </el-col>
+      </el-row>
+      <el-row>
+        <el-col :span="11">
+          <el-form-item label="可用寿命" prop="availableLife">
+            <el-input
+              v-model="formData.availableLife"
+              placeholder="请输入可用寿命"
+            />
+          </el-form-item>
+        </el-col>
+        <el-col :span="11">
+          <el-form-item label="已用寿命" prop="usedLife">
+            <el-input
+              v-model="formData.usedLife"
+              placeholder="请输入已用寿命"
+            />
+          </el-form-item>
+        </el-col>
+      </el-row>
+      <el-row>
+        <el-col :span="11">
+          <el-form-item label="启用日期" prop="activationTime">
+            <el-date-picker
+              v-model="formData.activationTime"
+              type="date"
+              value-format="YYYY-MM-DD"
+              placeholder="请选择启用日期"
+              style="width: 100%"
+            />
+          </el-form-item>
+        </el-col>
+        <el-col :span="11">
+          <el-form-item label="状态" prop="status">
+            <el-radio-group v-model="formData.status">
+              <el-radio label="1">
+                <img src="@/assets/images/success.png" alt="" class="imgstatus" />
+                在线
+              </el-radio>
+              <el-radio label="2">
+                <img src="@/assets/images/error.png" alt="" class="imgstatus" />
+                离线
+              </el-radio>
+              <el-radio label="3">
+                <img src="@/assets/images/warn.png" alt="" class="imgstatus" />
+                异常
+              </el-radio>
+            </el-radio-group>
+          </el-form-item>
+        </el-col>
+      </el-row>
+    </el-form>
+    <template #footer>
+      <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+
+<script lang="ts" setup>
+import { ref, reactive } from 'vue'
+import { handleTree } from '@/utils/tree'
+import * as HardwareApi from '@/api/hw/hardware/information/index'
+import * as HardwareTypeApi from '@/api/hw/type/hardwaretype/index'
+// import * as AutocodeApi from '@/api/system/autocode/rule'
+
+defineOptions({ name: 'HardwareForm' })
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const dialogTitle = ref('') // 弹窗的标题
+const formLoading = ref(false) // 表单的加载中
+const formType = ref('') // 表单的类型:create - 新增;update - 修改
+const autoGenFlag = ref(false) // 是否自动生成编码
+
+const formData = ref({
+  hardwareId: undefined,
+  hardwareCode: '',
+  hardwareName: '',
+  hardwareTypeId: undefined,
+  hardwareTypeName: '',
+  hardwareSpec: '',
+  serialNumber: '',
+  availableTimes: 0,
+  usedTimes: 0,
+  availableLife: '',
+  usedLife: '',
+  activationTime: '',
+  status: '1'
+})
+
+const formRules = reactive({
+  hardwareCode: [{ required: true, message: '硬件编码不能为空', trigger: 'blur' }],
+  hardwareName: [{ required: true, message: '硬件名称不能为空', trigger: 'blur' }],
+  hardwareTypeId: [{ required: true, message: '硬件类型不能为空', trigger: 'change' }]
+})
+
+const formRef = ref() // 表单 Ref
+const hardwareTypeOption = ref([]) // 硬件类型选项
+
+// 树形配置
+const defaultProps = {
+  children: 'children',
+  label: 'label'
+}
+
+/** 打开弹窗 */
+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 HardwareApi.getHardwareInfo(id)
+    } finally {
+      formLoading.value = false
+    }
+  }
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 校验表单
+  if (!formRef.value) return
+  const valid = await formRef.value.validate()
+  if (!valid) return
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = formData.value
+    if (formType.value === 'create') {
+      await HardwareApi.addHardware(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await HardwareApi.updateHardware(data)
+      message.success(t('common.updateSuccess'))
+    }
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    hardwareId: undefined,
+    hardwareCode: '',
+    hardwareName: '',
+    hardwareTypeId: undefined,
+    hardwareTypeName: '',
+    hardwareSpec: '',
+    serialNumber: '',
+    availableTimes: 0,
+    usedTimes: 0,
+    availableLife: '',
+    usedLife: '',
+    activationTime: '',
+    status: '1'
+  }
+  autoGenFlag.value = false
+  formRef.value?.resetFields()
+}
+
+/** 处理选择变化 */
+const onSelectChange = (selectedId: number) => {
+  formData.value.hardwareTypeId = selectedId
+  // 查找对应的 hardwareTypeName
+  const selectedOption = hardwareTypeOption.value.find(
+    (option: any) => option.id === selectedId
+  )
+  if (selectedOption) {
+    formData.value.hardwareTypeName = selectedOption.hardwareTypeName
+  }
+}
+
+/** 自动生成编码 */
+const handleAutoGenChange = async (value: boolean) => {
+  if (value) {
+    formData.value.hardwareCode = await AutocodeApi.genCode('HARDWARE_CODE')
+  } else {
+    formData.value.hardwareCode = ''
+  }
+}
+
+/** 获取硬件类型树形数据 */
+const getHardwareTypeTree = async () => {
+  const data = { pageNo: 1, pageSize: 10000 }
+  const response = await HardwareTypeApi.listHanrwareType(data)
+  hardwareTypeOption.value = handleTree(response.records)
+}
+
+/** 初始化 **/
+onMounted(async () => {
+  await getHardwareTypeTree()
+})
+</script>
+
+<style scoped>
+.imgstatus {
+  position: relative;
+  top: 1px;
+  left: 0px;
+}
+:deep(.el-radio__inner) {
+  border-radius: 2px;
+}
+:deep(.el-radio__input.is-checked .el-radio__inner::after) {
+  content: "";
+  width: 8px;
+  height: 3px;
+  border: 1px solid white;
+  border-top: transparent;
+  border-right: transparent;
+  text-align: center;
+  display: block;
+  position: absolute;
+  top: 3px;
+  left: 2px;
+  transform: rotate(-45deg);
+  border-radius: 0pc;
+  background: none;
+}
+</style>

+ 277 - 0
src/views/hw/hardware/information/index.vue

@@ -0,0 +1,277 @@
+<template>
+  <ContentWrap>
+    <!-- 搜索工作栏 -->
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="100px"
+    >
+      <el-form-item label="硬件编码" prop="hardwareCode">
+        <el-input
+          v-model="queryParams.hardwareCode"
+          placeholder="请输入硬件编码"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="硬件名称" prop="hardwareName">
+        <el-input
+          v-model="queryParams.hardwareName"
+          placeholder="请输入硬件名称"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="状态" prop="status">
+        <el-select
+          v-model="queryParams.status"
+          placeholder="请选择硬件状态"
+          clearable
+          class="!w-240px"
+        >
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.HARDWARE_STATUS)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="硬件类型" prop="hardwareTypeId">
+        <el-tree-select
+          v-model="queryParams.hardwareTypeId"
+          :data="hardwareTypeOption"
+          :props="defaultProps"
+          placeholder="请选择硬件类型"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="规格型号" prop="hardwareSpec">
+        <el-input
+          v-model="queryParams.hardwareSpec"
+          placeholder="请输入规格型号"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="创建时间" prop="createTime">
+        <el-date-picker
+          v-model="createTime"
+          type="datetimerange"
+          range-separator="-"
+          start-placeholder="开始日期"
+          end-placeholder="结束日期"
+          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="['mes:hw:information:create']"
+        >
+          <Icon icon="ep:plus" class="mr-5px" />
+          新增
+        </el-button>
+
+        <el-button
+          type="danger"
+          plain
+          :disabled="!selectedIds.length"
+          @click="handleDelete()"
+          v-hasPermi="['mes:hw:information:delete']"
+        >
+          <Icon icon="ep:delete" class="mr-5px" />
+          批量删除
+        </el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table v-loading="loading" :data="list" @selection-change="handleSelectionChange">
+      <el-table-column type="selection" width="55" align="center" />
+      <el-table-column label="硬件编码" prop="hardwareCode" width="120" />
+      <el-table-column
+        label="硬件名称"
+        prop="hardwareName"
+        width="150"
+        :show-overflow-tooltip="true"
+      />
+      <el-table-column label="硬件类型" prop="hardwareTypeName" />
+      <el-table-column label="规格型号" prop="hardwareSpec" />
+      <el-table-column label="序列号" prop="serialNumber" />
+      <el-table-column label="硬件状态" prop="status">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.HARDWARE_STATUS" :value="scope.row.status" />
+        </template>
+      </el-table-column>
+      <el-table-column label="创建时间" prop="createTime" width="180" :formatter="dateFormatter" />
+      <el-table-column
+        label="启用时间"
+        prop="activationTime"
+        width="120"
+        :formatter="dateFormatter"
+      />
+      <el-table-column label="可用次数" prop="availableTimes" />
+      <el-table-column label="已用次数" prop="usedTimes" />
+      <el-table-column label="可用寿命" prop="availableLife" />
+      <el-table-column label="已用寿命" prop="usedLife" />
+      <el-table-column label="操作" align="center" width="150">
+        <template #default="scope">
+          <el-button
+            link
+            type="primary"
+            @click="openForm('update', scope.row.id)"
+            v-hasPermi="['mes:hw:information:update']"
+          >
+            编辑
+          </el-button>
+          <el-button
+            link
+            type="danger"
+            @click="handleDelete(scope.row.id)"
+            v-hasPermi="['mes:hw:information:delete']"
+          >
+            删除
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <Pagination
+      v-model:total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </ContentWrap>
+
+  <!-- 表单弹窗 -->
+  <HardwareForm ref="formRef" @success="getList" />
+</template>
+
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import { handleTree } from '@/utils/tree'
+import * as HardwareApi from '@/api/hw/hardware/information/index'
+import * as HardwareTypeApi from '@/api//hw/type/hardwaretype/index'
+import HardwareForm from './HardwareForm.vue'
+
+defineOptions({ name: 'HardwareInfo' })
+
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
+
+const loading = ref(true) // 列表的加载中
+const list = ref([]) // 列表的数据
+const total = ref(0) // 列表的总页数
+const showSearch = ref(true) // 是否显示搜索工作栏
+const selectedIds = ref<number[]>([]) // 选中的数据编号
+const createTime = ref<[Date, Date] | null>(null) // 创建时间范围
+
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  hardwareCode: undefined,
+  hardwareName: undefined,
+  status: undefined,
+  hardwareTypeId: undefined,
+  hardwareSpec: undefined,
+  startTime: undefined,
+  endTime: undefined
+})
+
+const queryFormRef = ref() // 搜索表单
+const hardwareTypeOption = ref([]) // 硬件类型选项
+
+// 树形配置
+const defaultProps = {
+  children: 'children',
+  label: 'label'
+}
+
+/** 查询硬件列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    // 处理时间范围
+    if (createTime.value) {
+      queryParams.startTime = dateFormatter(createTime.value[0])
+      queryParams.endTime = dateFormatter(createTime.value[1])
+    }
+    const data = await HardwareApi.listHardware(queryParams)
+    list.value = data.records
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  createTime.value = null
+  queryFormRef.value?.resetFields()
+  handleQuery()
+}
+
+/** 多选框选中数据 */
+const handleSelectionChange = (selection: any[]) => {
+  selectedIds.value = selection.map((item) => item.id)
+}
+
+/** 添加/修改操作 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+  formRef.value.open(type, id)
+}
+
+/** 删除按钮操作 */
+const handleDelete = async (id?: number) => {
+  const ids = [id || selectedIds.value].join(',')
+  try {
+    // 删除的二次确认
+    await message.delConfirm()
+    // 发起删除
+    await HardwareApi.delHardware(ids)
+    message.success(t('common.delSuccess'))
+    // 刷新列表
+    await getList()
+  } catch {}
+}
+
+/** 获取硬件类型树形数据 */
+const getHardwareTypeTree = async () => {
+  const data = { pageNo: 1, pageSize: 10000 }
+  const response = await HardwareTypeApi.listHanrwareType(data)
+  hardwareTypeOption.value = handleTree(response.records)
+}
+
+/** 初始化 **/
+onMounted(async () => {
+  await getList()
+  await getHardwareTypeTree()
+})
+</script>

+ 226 - 0
src/views/hw/hardware/keys/KeyForm.vue

@@ -0,0 +1,226 @@
+<template>
+  <Dialog v-model="dialogVisible" :title="dialogTitle" width="450">
+    <el-form
+      ref="formRef"
+      v-loading="formLoading"
+      :model="formData"
+      :rules="formRules"
+      label-width="120px"
+    >
+      <el-form-item label="所属硬件" prop="hardwareId">
+        <el-select
+          v-model="formData.hardwareId"
+          placeholder="请选择所属硬件"
+          clearable
+          class="!w-240px"
+        >
+          <el-option
+            v-for="dict in hardwareOptions"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-row>
+        <el-col :span="15">
+          <el-form-item label="钥匙编码" prop="keyCode">
+            <el-input
+              v-model="formData.keyCode"
+              placeholder="请输入钥匙编码"
+              class="!w-240px"
+            />
+          </el-form-item>
+        </el-col>
+        <el-col :span="8">
+          <el-form-item label-width="80">
+            <el-switch
+              v-model="autoGenFlag"
+              active-color="#13ce66"
+              active-text="自动生成"
+              @change="handleAutoGenChange"
+              v-if="formType !== 'view'"
+            />
+          </el-form-item>
+        </el-col>
+      </el-row>
+      <el-form-item label="钥匙名称" prop="keyName">
+        <el-input
+          v-model="formData.keyName"
+          placeholder="请输入钥匙名称"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="钥匙NFC" prop="keyNfc">
+        <el-input
+          v-model="formData.keyNfc"
+          placeholder="请输入钥匙Nfc"
+          maxlength="16"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="MAC地址" prop="macAddress">
+        <el-input
+          v-model="formData.macAddress"
+          placeholder="请输入Mac地址"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="钥匙型号" prop="keySpec">
+        <el-input
+          v-model="formData.keySpec"
+          placeholder="请输入钥匙型号"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="状态" prop="exStatus">
+        <el-radio-group v-model="formData.exStatus">
+          <el-radio
+            v-for="dict in getIntDictOptions(DICT_TYPE.KEY_STATUS)"
+            :key="dict.value"
+            :label="dict.value"
+          >
+            {{ dict.label }}
+          </el-radio>
+        </el-radio-group>
+      </el-form-item>
+      <el-form-item label="备注" prop="exRemark">
+        <el-input
+          v-model="formData.exRemark"
+          placeholder="请输入备注"
+          class="!w-240px"
+        />
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+
+<script lang="ts" setup>
+import { ref, reactive } from 'vue'
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import * as KeyApi from '@/api/hw/hardware/keys/index'
+import * as HardwareApi from '@/api/hw/hardware/information/index'
+// import * as AutocodeApi from '@/api/system/autocode/rule'
+
+defineOptions({ name: 'KeyForm' })
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const dialogTitle = ref('') // 弹窗的标题
+const formLoading = ref(false) // 表单的加载中
+const formType = ref('') // 表单的类型:create - 新增;update - 修改
+const autoGenFlag = ref(false) // 是否自动生成编码
+
+const formData = ref({
+  keyId: undefined,
+  hardwareId: undefined,
+  keyCode: '',
+  keyName: '',
+  keyNfc: '',
+  macAddress: '',
+  keySpec: '',
+  exStatus: '1',
+  exRemark: ''
+})
+
+const formRules = reactive({
+  keyCode: [{ required: true, message: '钥匙编码不能为空', trigger: 'blur' }],
+  keyName: [{ required: true, message: '钥匙名称不能为空', trigger: 'blur' }],
+  keyNfc: [{ required: true, message: '钥匙NFC不能为空', trigger: 'blur' }],
+  macAddress: [{ required: true, message: 'MAC地址不能为空', trigger: 'blur' }]
+})
+
+const formRef = ref() // 表单 Ref
+const hardwareOptions = 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 KeyApi.getKeyInfoAPI(id)
+    } finally {
+      formLoading.value = false
+    }
+  }
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 校验表单
+  if (!formRef.value) return
+  const valid = await formRef.value.validate()
+  if (!valid) return
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = formData.value
+    if (formType.value === 'create') {
+      await KeyApi.addKeyAPI(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await KeyApi.updateKeyAPI(data)
+      message.success(t('common.updateSuccess'))
+    }
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    keyId: undefined,
+    hardwareId: undefined,
+    keyCode: '',
+    keyName: '',
+    keyNfc: '',
+    macAddress: '',
+    keySpec: '',
+    exStatus: '1',
+    exRemark: ''
+  }
+  autoGenFlag.value = false
+  formRef.value?.resetFields()
+}
+
+/** 自动生成编码 */
+const handleAutoGenChange = async (value: boolean) => {
+  if (value) {
+    formData.value.keyCode = await AutocodeApi.genCode('KEY_CODE')
+  } else {
+    formData.value.keyCode = ''
+  }
+}
+
+/** 获取硬件列表 */
+const getHardwareList = async () => {
+  const data = { pageNo: 1, pageSize: -1 }
+  const response = await HardwareApi.listHardware(data)
+  hardwareOptions.value = response.records.map(item => ({
+    value: item.id,
+    label: item.hardwareName
+  }))
+}
+
+/** 初始化 **/
+onMounted(async () => {
+  await getHardwareList()
+})
+</script>

+ 197 - 0
src/views/hw/hardware/keys/index.vue

@@ -0,0 +1,197 @@
+<template>
+  <ContentWrap>
+    <!-- 搜索工作栏 -->
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="100px"
+    >
+      <el-form-item label="钥匙编码" prop="keyCode">
+        <el-input
+          v-model="queryParams.keyCode"
+          placeholder="请输入钥匙编码"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="钥匙名称" prop="keyName">
+        <el-input
+          v-model="queryParams.keyName"
+          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="['mes:hw:key:create']"
+        >
+          <Icon icon="ep:plus" class="mr-5px" /> 新增
+        </el-button>
+        <el-button
+          type="danger"
+          plain
+          :disabled="!selectedIds.length"
+          @click="handleDelete()"
+          v-hasPermi="['mes:hw:key:delete']"
+        >
+          <Icon icon="ep:delete" class="mr-5px" /> 批量删除
+        </el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table
+      v-loading="loading"
+      :data="list"
+      @selection-change="handleSelectionChange"
+    >
+      <el-table-column type="selection" width="55" align="center" />
+      <el-table-column label="钥匙编码" prop="keyCode" width="150" />
+      <el-table-column label="钥匙名称" prop="keyName" width="180" />
+      <el-table-column label="钥匙NFC" prop="keyNfc" width="180" :show-overflow-tooltip="true" />
+      <el-table-column label="钥匙型号" prop="keySpec" width="180" :show-overflow-tooltip="true" />
+      <el-table-column label="MAC地址" prop="macAddress" />
+      <el-table-column label="状态" prop="exStatus">
+        <template #default="scope">
+          <el-switch
+            v-if="scope.row.exStatus !== null"
+            v-model="scope.row.exStatus"
+            active-value="1"
+            inactive-value="0"
+            active-color="#13ce66"
+            inactive-color="grey"
+            disabled
+          />
+          <span v-else>-</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="备注" prop="exRemark" />
+      <el-table-column label="所属硬件" prop="hardwareName" />
+      <el-table-column label="创建时间" prop="createTime" width="180" :formatter="dateFormatter" />
+      <el-table-column label="操作" align="center" width="150">
+        <template #default="scope">
+          <el-button
+            link
+            type="primary"
+            @click="openForm('update', scope.row.keyId)"
+            v-hasPermi="['mes:hw:key:update']"
+          >
+            编辑
+          </el-button>
+          <el-button
+            link
+            type="danger"
+            @click="handleDelete(scope.row.keyId)"
+            v-hasPermi="['mes:hw:key:delete']"
+          >
+            删除
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <Pagination
+      v-model:total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </ContentWrap>
+
+  <!-- 表单弹窗 -->
+  <KeyForm ref="formRef" @success="getList" />
+</template>
+
+<script lang="ts" setup>
+import { dateFormatter } from '@/utils/formatTime'
+import * as KeyApi from '@/api/hw/hardware/keys/index'
+import KeyForm from './KeyForm.vue'
+
+defineOptions({ name: 'KeyInfo' })
+
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
+
+const loading = ref(true) // 列表的加载中
+const list = ref([]) // 列表的数据
+const total = ref(0) // 列表的总页数
+const showSearch = ref(true) // 是否显示搜索工作栏
+const selectedIds = ref<number[]>([]) // 选中的数据编号
+
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  keyCode: undefined,
+  keyName: undefined,
+  keyNfc: undefined,
+  macAddress: undefined
+})
+
+const queryFormRef = ref() // 搜索表单
+
+/** 查询钥匙列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await KeyApi.listKeyAPI(queryParams)
+    list.value = data.records
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value?.resetFields()
+  handleQuery()
+}
+
+/** 多选框选中数据 */
+const handleSelectionChange = (selection: any[]) => {
+  selectedIds.value = selection.map(item => item.keyId)
+}
+
+/** 添加/修改操作 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+  formRef.value.open(type, id)
+}
+
+/** 删除按钮操作 */
+const handleDelete = async (id?: number) => {
+  const ids = [id || selectedIds.value].join(',')
+  try {
+    // 删除的二次确认
+    await message.delConfirm()
+    // 发起删除
+    await KeyApi.delKeyAPI(ids)
+    message.success(t('common.delSuccess'))
+    // 刷新列表
+    await getList()
+  } catch {}
+}
+
+/** 初始化 **/
+onMounted(async () => {
+  await getList()
+})
+</script>

+ 225 - 0
src/views/hw/hardware/lockset/LocksetForm.vue

@@ -0,0 +1,225 @@
+<template>
+  <Dialog v-model="dialogVisible" :title="dialogTitle" width="450">
+    <el-form
+      ref="formRef"
+      v-loading="formLoading"
+      :model="formData"
+      :rules="formRules"
+      label-width="120px"
+    >
+      <el-form-item label="锁具机构类型" prop="locksetTypeId">
+        <el-tree-select
+          v-model="formData.locksetTypeId"
+          :data="locksetTypeOptions"
+          :props="{ label: 'locksetTypeName', value: 'locksetTypeId', children: 'children' }"
+          placeholder="请选择锁具机构类型"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-row>
+        <el-col :span="15">
+          <el-form-item label="锁具机构编码" prop="locksetCode">
+            <el-input
+              v-model="formData.locksetCode"
+              placeholder="请输入锁具机构编码"
+              class="!w-240px"
+            />
+          </el-form-item>
+        </el-col>
+        <el-col :span="8">
+          <el-form-item label-width="80">
+            <el-switch
+              v-model="autoGenFlag"
+              active-color="#13ce66"
+              active-text="自动生成"
+              @change="handleAutoGenChange"
+              v-if="formType !== 'view'"
+            />
+          </el-form-item>
+        </el-col>
+      </el-row>
+      <el-form-item label="锁具机构名称" prop="locksetName">
+        <el-input
+          v-model="formData.locksetName"
+          placeholder="请输入锁具机构名称"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="锁具机构NFC" prop="locksetNfc">
+        <el-input
+          v-model="formData.locksetNfc"
+          placeholder="请输入锁具机构Nfc"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="锁具机构RFID" prop="locksetRfid">
+        <el-input
+          v-model="formData.locksetRfid"
+          placeholder="请输入锁具机构RFID"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="物资名称" prop="materialsId">
+        <el-select
+          v-model="formData.materialsId"
+          placeholder="请选择物资名称"
+          clearable
+          class="!w-240px"
+        >
+          <el-option
+            v-for="dict in materialsOptions"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="锁具机构型号" prop="locksetSpec">
+        <el-input
+          v-model="formData.locksetSpec"
+          placeholder="请输入锁具机构型号"
+          maxlength="16"
+          class="!w-240px"
+        />
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+
+<script lang="ts" setup>
+import { ref, reactive } from 'vue'
+import { handleTree } from '@/utils/tree'
+import * as LocksetApi from '@/api/hw/hardware/lockset/index'
+import * as LocksetTypeApi from '@/api/hw/type/locksettype/index'
+// import * as MaterialApi from '@/api/mes/material/information'
+// import * as AutocodeApi from '@/api/system/autocode/rule'
+
+defineOptions({ name: 'LockForm' })
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const dialogTitle = ref('') // 弹窗的标题
+const formLoading = ref(false) // 表单的加载中
+const formType = ref('') // 表单的类型:create - 新增;update - 修改
+const autoGenFlag = ref(false) // 是否自动生成编码
+
+const formData = ref({
+  locksetId: undefined,
+  locksetTypeId: undefined,
+  locksetCode: '',
+  locksetName: '',
+  locksetNfc: '',
+  locksetRfid: '',
+  materialsId: undefined,
+  locksetSpec: ''
+})
+
+const formRules = reactive({
+  locksetCode: [{ required: true, message: '锁具机构编码不能为空', trigger: 'blur' }],
+  locksetName: [{ required: true, message: '锁具机构名称不能为空', trigger: 'blur' }],
+  locksetNfc: [{ required: true, message: '锁具机构NFC不能为空', trigger: 'blur' }],
+  locksetRfid: [{ required: true, message: '锁具机构RFID不能为空', trigger: 'blur' }]
+})
+
+const formRef = ref() // 表单 Ref
+const locksetTypeOptions = ref([]) // 锁具机构类型选项
+const materialsOptions = 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 LocksetApi.getLockInfoAPI(id)
+    } finally {
+      formLoading.value = false
+    }
+  }
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 校验表单
+  if (!formRef.value) return
+  const valid = await formRef.value.validate()
+  if (!valid) return
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = formData.value
+    if (formType.value === 'create') {
+      await LocksetApi.addLocksetApi(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await LocksetApi.updateLocksetApi(data)
+      message.success(t('common.updateSuccess'))
+    }
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    locksetId: undefined,
+    locksetTypeId: undefined,
+    locksetCode: '',
+    locksetName: '',
+    locksetNfc: '',
+    locksetRfid: '',
+    materialsId: undefined,
+    locksetSpec: ''
+  }
+  autoGenFlag.value = false
+  formRef.value?.resetFields()
+}
+
+/** 自动生成编码 */
+const handleAutoGenChange = async (value: boolean) => {
+  if (value) {
+    formData.value.locksetCode = await AutocodeApi.genCode('LOCKSET_CODE')
+  } else {
+    formData.value.locksetCode = ''
+  }
+}
+
+/** 获取锁具机构类型列表 */
+const getLockTypeList = async () => {
+  const data = { pageNo: 1, pageSize: -1 }
+  const response = await LocksetTypeApi.listLockType(data)
+  locksetTypeOptions.value = handleTree(response.records, 'locksetTypeId', 'parentTypeId', 'children')
+}
+
+/** 获取物资列表 */
+const getMaterialList = async () => {
+  const data = { pageNo: 1, pageSize: -1 }
+  const response = await MaterialApi.listMaterials(data)
+  materialsOptions.value = response.records.map(item => ({
+    value: item.materialsCabinetId,
+    label: item.materialsName
+  }))
+}
+
+/** 初始化 **/
+onMounted(async () => {
+  await getLockTypeList()
+  await getMaterialList()
+})
+</script>

+ 207 - 0
src/views/hw/hardware/lockset/index.vue

@@ -0,0 +1,207 @@
+<template>
+  <ContentWrap>
+    <!-- 搜索工作栏 -->
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="100px"
+    >
+      <el-form-item label="锁具机构编码" prop="locksetCode">
+        <el-input
+          v-model="queryParams.locksetCode"
+          placeholder="请输入锁具机构编码"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="锁具机构名称" prop="locksetName">
+        <el-input
+          v-model="queryParams.locksetName"
+          placeholder="请输入锁具机构名称"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="锁具机构类型" prop="locksetTypeId">
+        <el-tree-select
+          v-model="queryParams.locksetTypeId"
+          :data="locksetTypeOptions"
+          :props="{ label: 'locksetTypeName', value: 'locksetTypeId', children: 'children' }"
+          placeholder="请选择锁具机构类型"
+          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="['mes:hw:lk:create']"
+        >
+          <Icon icon="ep:plus" class="mr-5px" /> 新增
+        </el-button>
+        <el-button
+          type="danger"
+          plain
+          :disabled="!selectedIds.length"
+          @click="handleDelete()"
+          v-hasPermi="['mes:hw:lk:delete']"
+        >
+          <Icon icon="ep:delete" class="mr-5px" /> 批量删除
+        </el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+
+    <el-table
+      v-loading="loading"
+      :data="list"
+      @selection-change="handleSelectionChange"
+    >
+      <el-table-column type="selection" width="55" align="center" />
+      <el-table-column label="锁具机构编码" prop="locksetCode" width="150" />
+      <el-table-column label="锁具机构名称" prop="locksetName" width="180" :show-overflow-tooltip="true" />
+      <el-table-column label="锁具机构类型" prop="locksetTypeName" />
+      <el-table-column label="锁具机构NFC" prop="locksetNfc" />
+      <el-table-column label="锁具机构RFID" prop="locksetRfid" />
+      <el-table-column label="物资名称" prop="materialsName" />
+      <el-table-column label="锁具机构型号" prop="locksetSpec" />
+      <el-table-column label="创建时间" prop="createTime" width="180" :formatter="dateFormatter" />
+      <el-table-column label="操作" align="center" width="150">
+        <template #default="scope">
+          <el-button
+            link
+            type="primary"
+            @click="openForm('update', scope.row.locksetId)"
+            v-hasPermi="['mes:hw:lk:update']"
+          >
+            编辑
+          </el-button>
+          <el-button
+            link
+            type="danger"
+            @click="handleDelete(scope.row.locksetId)"
+            v-hasPermi="['mes:hw:lk:delete']"
+          >
+            删除
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <Pagination
+      v-model:total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </ContentWrap>
+
+  <!-- 表单弹窗 -->
+  <LockForm ref="formRef" @success="getList" />
+</template>
+
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import { handleTree } from '@/utils/tree'
+import * as LocksetApi from '@/api/hw/hardware/lockset/index'
+import * as LocksetTypeApi from '@/api/hw/type/locksettype/index'
+// import * as MaterialApi from '@/api/mes/material/information'
+import LockForm from './LockForm.vue'
+import {listLocksetType} from "@/api/hw/type/locksettype/index";
+import {delLocksetAPI, listLocksetAPI} from "@/api/hw/hardware/lockset/index";
+
+defineOptions({ name: 'LockInfo' })
+
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
+
+const loading = ref(true) // 列表的加载中
+const list = ref([]) // 列表的数据
+const total = ref(0) // 列表的总页数
+const showSearch = ref(true) // 是否显示搜索工作栏
+const selectedIds = ref<number[]>([]) // 选中的数据编号
+const locksetTypeOptions = ref([]) // 锁具机构类型选项
+
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  locksetCode: undefined,
+  locksetName: undefined,
+  locksetTypeId: undefined
+})
+
+const queryFormRef = ref() // 搜索表单
+
+/** 查询锁具机构列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await LocksetApi.listLocksetAPI(queryParams)
+    list.value = data.records
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 获取锁具机构类型列表 */
+const getLockTypeList = async () => {
+  const data = { pageNo: 1, pageSize: -1 }
+  const response = await LocksetTypeApi.listLocksetType(data)
+  locksetTypeOptions.value = handleTree(response.records, 'locksetTypeId', 'parentTypeId', 'children')
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value?.resetFields()
+  handleQuery()
+}
+
+/** 多选框选中数据 */
+const handleSelectionChange = (selection: any[]) => {
+  selectedIds.value = selection.map(item => item.locksetId)
+}
+
+/** 添加/修改操作 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+  formRef.value.open(type, id)
+}
+
+/** 删除按钮操作 */
+const handleDelete = async (id?: number) => {
+  const ids = [id || selectedIds.value].join(',')
+  try {
+    // 删除的二次确认
+    await message.delConfirm()
+    // 发起删除
+    await LocksetApi.delLocksetAPI(ids)
+    message.success(t('common.delSuccess'))
+    // 刷新列表
+    await getList()
+  } catch {}
+}
+
+/** 初始化 **/
+onMounted(async () => {
+  await getList()
+  await getLockTypeList()
+})
+</script>

+ 237 - 0
src/views/hw/hardware/padLocks/PadLockForm.vue

@@ -0,0 +1,237 @@
+<template>
+  <Dialog v-model="dialogVisible" :title="dialogTitle" width="450">
+    <el-form
+      ref="formRef"
+      v-loading="formLoading"
+      :model="formData"
+      :rules="formRules"
+      label-width="120px"
+    >
+      <el-form-item label="所属硬件" prop="hardwareId">
+        <el-select
+          v-model="formData.hardwareId"
+          placeholder="请选择所属硬件"
+          clearable
+          class="!w-240px"
+        >
+          <el-option
+            v-for="dict in hardwareOptions"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="挂锁类型" prop="lockTypeId">
+        <el-tree-select
+          v-model="formData.lockTypeId"
+          :data="lockTypeOptions"
+          :props="{ label: 'lockTypeName', value: 'lockTypeId', children: 'children' }"
+          placeholder="请选择挂锁类型"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-row>
+        <el-col :span="15">
+          <el-form-item label="挂锁编码" prop="lockCode">
+            <el-input
+              v-model="formData.lockCode"
+              placeholder="请输入挂锁编码"
+              class="!w-240px"
+            />
+          </el-form-item>
+        </el-col>
+        <el-col :span="8">
+          <el-form-item label-width="80">
+            <el-switch
+              v-model="autoGenFlag"
+              active-color="#13ce66"
+              active-text="自动生成"
+              @change="handleAutoGenChange"
+              v-if="formType !== 'view'"
+            />
+          </el-form-item>
+        </el-col>
+      </el-row>
+      <el-form-item label="挂锁名称" prop="lockName">
+        <el-input
+          v-model="formData.lockName"
+          placeholder="请输入挂锁名称"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="挂锁NFC" prop="lockNfc">
+        <el-input
+          v-model="formData.lockNfc"
+          placeholder="请输入挂锁Nfc"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="挂锁型号" prop="lockSpec">
+        <el-input
+          v-model="formData.lockSpec"
+          placeholder="请输入挂锁型号"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="状态" prop="exStatus">
+        <el-radio-group v-model="formData.exStatus">
+          <el-radio
+            v-for="dict in getIntDictOptions(DICT_TYPE.PADLOCK_STATUS)"
+            :key="dict.value"
+            :label="dict.value"
+          >
+            {{ dict.label }}
+          </el-radio>
+        </el-radio-group>
+      </el-form-item>
+      <el-form-item label="备注" prop="exRemark">
+        <el-input
+          v-model="formData.exRemark"
+          placeholder="请输入备注"
+          class="!w-240px"
+        />
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+
+<script lang="ts" setup>
+import { ref, reactive } from 'vue'
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { handleTree } from '@/utils/tree'
+import * as PadLockApi from '@/api/hw/hardware/padLock/index'
+import * as HardwareApi from '@/api/hw/hardware/information/index'
+import * as PadLockTypeApi from '@/api/hw/type/padLockType/index'
+// import * as AutocodeApi from '@/api/system/autocode/rule'
+
+defineOptions({ name: 'PadLockForm' })
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const dialogTitle = ref('') // 弹窗的标题
+const formLoading = ref(false) // 表单的加载中
+const formType = ref('') // 表单的类型:create - 新增;update - 修改
+const autoGenFlag = ref(false) // 是否自动生成编码
+
+const formData = ref({
+  lockId: undefined,
+  hardwareId: undefined,
+  lockTypeId: undefined,
+  lockCode: '',
+  lockName: '',
+  lockNfc: '',
+  lockSpec: '',
+  exStatus: '1',
+  exRemark: ''
+})
+
+const formRules = reactive({
+  lockCode: [{ required: true, message: '挂锁编码不能为空', trigger: 'blur' }],
+  lockName: [{ required: true, message: '挂锁名称不能为空', trigger: 'blur' }],
+  lockNfc: [{ required: true, message: '挂锁NFC不能为空', trigger: 'blur' }]
+})
+
+const formRef = ref() // 表单 Ref
+const hardwareOptions = ref([]) // 硬件选项
+const lockTypeOptions = 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 PadLockApi.getPadLockInfoAPI(id)
+    } finally {
+      formLoading.value = false
+    }
+  }
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 校验表单
+  if (!formRef.value) return
+  const valid = await formRef.value.validate()
+  if (!valid) return
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = formData.value
+    if (formType.value === 'create') {
+      await PadLockApi.addPadLockAPI(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await PadLockApi.updatePadLockAPI(data)
+      message.success(t('common.updateSuccess'))
+    }
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    lockId: undefined,
+    hardwareId: undefined,
+    lockTypeId: undefined,
+    lockCode: '',
+    lockName: '',
+    lockNfc: '',
+    lockSpec: '',
+    exStatus: '1',
+    exRemark: ''
+  }
+  autoGenFlag.value = false
+  formRef.value?.resetFields()
+}
+
+/** 自动生成编码 */
+const handleAutoGenChange = async (value: boolean) => {
+  if (value) {
+    formData.value.lockCode = await AutocodeApi.genCode('LOCK_CODE')
+  } else {
+    formData.value.lockCode = ''
+  }
+}
+
+/** 获取硬件列表 */
+const getHardwareList = async () => {
+  const data = { pageNo: 1, pageSize: -1 }
+  const response = await HardwareApi.listHardware(data)
+  hardwareOptions.value = response.records.map(item => ({
+    value: item.id,
+    label: item.hardwareName
+  }))
+}
+
+/** 获取挂锁类型列表 */
+const getLockTypeList = async () => {
+  const data = { pageNo: 1, pageSize: -1 }
+  const response = await PadLockTypeApi.listpadLockTypeApi(data)
+  lockTypeOptions.value = handleTree(response.records, 'lockTypeId', 'parentTypeId', 'children')
+}
+
+/** 初始化 **/
+onMounted(async () => {
+  await getHardwareList()
+  await getLockTypeList()
+})
+</script>

+ 198 - 0
src/views/hw/hardware/padLocks/index.vue

@@ -0,0 +1,198 @@
+<template>
+  <ContentWrap>
+    <!-- 搜索工作栏 -->
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="100px"
+    >
+      <el-form-item label="挂锁编码" prop="lockCode">
+        <el-input
+          v-model="queryParams.lockCode"
+          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>
+        <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="['mes:hw:plk:create']"
+        >
+          <Icon icon="ep:plus" class="mr-5px" /> 新增
+        </el-button>
+        <el-button
+          type="danger"
+          plain
+          :disabled="!selectedIds.length"
+          @click="handleDelete()"
+          v-hasPermi="['mes:hw:plk:delete']"
+        >
+          <Icon icon="ep:delete" class="mr-5px" /> 批量删除
+        </el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table
+      v-loading="loading"
+      :data="list"
+      @selection-change="handleSelectionChange"
+    >
+      <el-table-column type="selection" width="55" align="center" />
+      <el-table-column label="挂锁编码" prop="lockCode" width="150" />
+      <el-table-column label="挂锁名称" prop="lockName" width="200" />
+      <el-table-column label="挂锁NFC" prop="lockNfc" width="180" :show-overflow-tooltip="true" />
+      <el-table-column label="所属硬件" prop="hardwareName" />
+      <el-table-column label="挂锁类型" prop="lockTypeName" />
+      <el-table-column label="挂锁型号" prop="lockSpec" />
+      <el-table-column label="状态" prop="exStatus">
+        <template #default="scope">
+          <el-switch
+            v-if="scope.row.exStatus !== null"
+            v-model="scope.row.exStatus"
+            active-value="1"
+            inactive-value="0"
+            active-color="#13ce66"
+            inactive-color="grey"
+            disabled
+          />
+          <span v-else>-</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="备注" prop="exRemark" />
+      <el-table-column label="创建时间" prop="createTime" width="180" :formatter="dateFormatter" />
+      <el-table-column label="操作" align="center" width="150">
+        <template #default="scope">
+          <el-button
+            link
+            type="primary"
+            @click="openForm('update', scope.row.lockId)"
+            v-hasPermi="['mes:hw:plk:update']"
+          >
+            编辑
+          </el-button>
+          <el-button
+            link
+            type="danger"
+            @click="handleDelete(scope.row.lockId)"
+            v-hasPermi="['mes:hw:plk:delete']"
+          >
+            删除
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <Pagination
+      v-model:total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </ContentWrap>
+
+  <!-- 表单弹窗 -->
+  <PadLockForm ref="formRef" @success="getList" />
+</template>
+
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import * as PadLockApi from '@/api/hw/hardware/padLock/index'
+import PadLockForm from './PadLockForm.vue'
+
+defineOptions({ name: 'PadLockInfo' })
+
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
+
+const loading = ref(true) // 列表的加载中
+const list = ref([]) // 列表的数据
+const total = ref(0) // 列表的总页数
+const showSearch = ref(true) // 是否显示搜索工作栏
+const selectedIds = ref<number[]>([]) // 选中的数据编号
+
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  lockCode: undefined,
+  lockName: undefined,
+  lockNfc: undefined,
+  lockTypeId: undefined
+})
+
+const queryFormRef = ref() // 搜索表单
+
+/** 查询挂锁列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await PadLockApi.listPadLockAPI(queryParams)
+    list.value = data.records
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value?.resetFields()
+  handleQuery()
+}
+
+/** 多选框选中数据 */
+const handleSelectionChange = (selection: any[]) => {
+  selectedIds.value = selection.map(item => item.lockId)
+}
+
+/** 添加/修改操作 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+  formRef.value.open(type, id)
+}
+
+/** 删除按钮操作 */
+const handleDelete = async (id?: number) => {
+  const ids = [id || selectedIds.value].join(',')
+  try {
+    // 删除的二次确认
+    await message.delConfirm()
+    // 发起删除
+    await PadLockApi.delPadLockAPI(ids)
+    message.success(t('common.delSuccess'))
+    // 刷新列表
+    await getList()
+  } catch {}
+}
+
+/** 初始化 **/
+onMounted(async () => {
+  await getList()
+})
+</script>

+ 265 - 0
src/views/hw/lockCabinet/LockCabinetForm.vue

@@ -0,0 +1,265 @@
+<template>
+  <Dialog v-model="dialogVisible" :title="dialogTitle" width="650">
+    <el-form
+      ref="formRef"
+      v-loading="formLoading"
+      :model="formData"
+      :rules="formRules"
+      label-width="120px"
+    >
+      <el-row>
+        <el-col :span="15">
+          <el-form-item label="锁柜编号" prop="cabinetCode">
+            <el-input
+              v-model="formData.cabinetCode"
+              placeholder="请输入锁柜编号"
+              class="!w-240px"
+            />
+          </el-form-item>
+        </el-col>
+        <el-col :span="8">
+          <el-form-item label-width="80">
+            <el-switch
+              v-model="autoGenFlag"
+              active-color="#13ce66"
+              active-text="自动生成"
+              @change="handleAutoGenChange"
+              v-if="formType !== 'view'"
+            />
+          </el-form-item>
+        </el-col>
+      </el-row>
+      <el-form-item label="锁柜名称" prop="cabinetName">
+        <el-input
+          v-model="formData.cabinetName"
+          placeholder="请输入锁柜名称"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="岗位序号" prop="workstationId">
+        <el-tree-select
+          v-model="formData.workstationId"
+          :data="workstationOptions"
+          :props="{ label: 'workstationName', value: 'workstationId', children: 'children' }"
+          placeholder="请选择岗位"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="硬件ID" prop="hardwareId">
+        <el-select
+          v-model="formData.hardwareId"
+          placeholder="请选择硬件ID"
+          clearable
+          class="!w-240px"
+          @change="handleSelectHardware"
+        >
+          <el-option
+            v-for="dict in hardwareOptions"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="是否在线" prop="isOnline">
+        <el-radio-group v-model="formData.isOnline">
+          <el-radio
+            v-for="dict in getIntDictOptions(DICT_TYPE.IS_ONLINE_STATUS)"
+            :key="dict.value"
+            :label="dict.value"
+          >
+            {{ dict.label }}
+          </el-radio>
+        </el-radio-group>
+      </el-form-item>
+      <el-form-item label="状态" prop="status">
+        <el-radio-group v-model="formData.status">
+          <el-radio
+            v-for="dict in getIntDictOptions(DICT_TYPE.CABINET_STATUS)"
+            :key="dict.value"
+            :label="dict.value"
+          >
+            {{ dict.label }}
+          </el-radio>
+        </el-radio-group>
+      </el-form-item>
+      <el-row>
+        <el-col :span="10">
+          <el-form-item label="图标" prop="cabinetIcon">
+            <UploadImg
+              v-model="formData.cabinetIcon"
+              :limit="1"
+              height="64px"
+              width="64px"
+            />
+          </el-form-item>
+        </el-col>
+        <el-col :span="10">
+          <el-form-item label="图片" prop="cabinetPicture">
+            <UploadImg
+              v-model="formData.cabinetPicture"
+              :limit="1"
+              height="64px"
+              width="64px"
+            />
+          </el-form-item>
+        </el-col>
+      </el-row>
+      <el-form-item label="备注" prop="remark">
+        <el-input
+          v-model="formData.remark"
+          placeholder="请输入备注"
+          class="!w-240px"
+        />
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+
+<script lang="ts" setup>
+import { ref, reactive } from 'vue'
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { handleTree } from '@/utils/tree'
+import * as LockCabinetApi from '@/api/hw/hardware/lockCabinet/index'
+import * as HardwareApi from '@/api/hw/hardware/information/index'
+// import * as MarsDeptApi from '@/api/system/marsdept'
+// import * as AutocodeApi from '@/api/system/autocode/rule'
+
+defineOptions({ name: 'LockCabinetForm' })
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const dialogTitle = ref('') // 弹窗的标题
+const formLoading = ref(false) // 表单的加载中
+const formType = ref('') // 表单的类型:create - 新增;update - 修改
+const autoGenFlag = ref(false) // 是否自动生成编码
+
+const formData = ref({
+  cabinetId: undefined,
+  cabinetCode: '',
+  cabinetName: '',
+  workstationId: undefined,
+  hardwareId: undefined,
+  isOnline: '1',
+  status: '1',
+  cabinetIcon: '',
+  cabinetPicture: '',
+  remark: ''
+})
+
+const formRules = reactive({
+  cabinetCode: [{ required: true, message: '锁柜编码不能为空', trigger: 'blur' }],
+  cabinetName: [{ required: true, message: '锁柜名称不能为空', trigger: 'blur' }],
+  hardwareId: [{ required: true, message: '硬件ID不能为空', trigger: 'blur' }],
+  workstationId: [{ required: true, message: '岗位序号不能为空', trigger: 'blur' }]
+})
+
+const formRef = ref() // 表单 Ref
+const hardwareOptions = ref([]) // 硬件选项
+const workstationOptions = 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 LockCabinetApi.selectIsLockCabinetById(id)
+    } finally {
+      formLoading.value = false
+    }
+  }
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 校验表单
+  if (!formRef.value) return
+  const valid = await formRef.value.validate()
+  if (!valid) return
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = formData.value
+    if (formType.value === 'create') {
+      await LockCabinetApi.insertIsLockCabinet(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await LockCabinetApi.updateIsLockCabinet(data)
+      message.success(t('common.updateSuccess'))
+    }
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    cabinetId: undefined,
+    cabinetCode: '',
+    cabinetName: '',
+    workstationId: undefined,
+    hardwareId: undefined,
+    isOnline: '1',
+    status: '1',
+    cabinetIcon: '',
+    cabinetPicture: '',
+    remark: ''
+  }
+  autoGenFlag.value = false
+  formRef.value?.resetFields()
+}
+
+/** 自动生成编码 */
+const handleAutoGenChange = async (value: boolean) => {
+  if (value) {
+    formData.value.cabinetCode = await AutocodeApi.genCode('CABINET_CODE')
+  } else {
+    formData.value.cabinetCode = ''
+  }
+}
+
+/** 选择硬件 */
+const handleSelectHardware = (value: string) => {
+  formData.value.hardwareId = value
+}
+
+/** 获取硬件列表 */
+const getHardwareList = async () => {
+  const data = { pageNo: 1, pageSize: -1 }
+  const response = await HardwareApi.listHardware(data)
+  hardwareOptions.value = response.records.map(item => ({
+    value: item.id,
+    label: item.hardwareName
+  }))
+}
+
+/** 获取岗位列表 */
+const getWorkstationList = async () => {
+  const data = { pageNo: 1, pageSize: -1 }
+  const response = await MarsDeptApi.listMarsDept(data)
+  workstationOptions.value = handleTree(response.records, 'workstationId', 'parentId')
+}
+
+/** 初始化 **/
+onMounted(async () => {
+  await getHardwareList()
+  await getWorkstationList()
+})
+</script>

+ 225 - 0
src/views/hw/lockCabinet/LookList.vue

@@ -0,0 +1,225 @@
+<template>
+  <ContentWrap>
+    <!-- 搜索工作栏 -->
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="100px"
+    >
+      <el-form-item label="仓位编码" prop="slotCode">
+        <el-input
+          v-model="queryParams.slotCode"
+          placeholder="请输入仓位编码"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="仓位类型" prop="slotType">
+        <el-select
+          v-model="queryParams.slotType"
+          placeholder="请选择仓位类型"
+          clearable
+          class="!w-240px"
+        >
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.SLOT_TYPE)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="状态" prop="status">
+        <el-select
+          v-model="queryParams.status"
+          placeholder="请选择状态"
+          clearable
+          class="!w-240px"
+        >
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.SLOT_STATUS)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.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="['mes:hw:work:create']"
+        >
+          <Icon icon="ep:plus" class="mr-5px" /> 新增
+        </el-button>
+        <el-button
+          type="danger"
+          plain
+          :disabled="!selectedIds.length"
+          @click="handleDelete()"
+          v-hasPermi="['mes:hw:work:delete']"
+        >
+          <Icon icon="ep:delete" class="mr-5px" /> 批量删除
+        </el-button>
+      </el-form-item>
+    </el-form>
+</ContentWrap>
+
+<ContentWrap>
+    <el-table
+      v-loading="loading"
+      :data="list"
+      @selection-change="handleSelectionChange"
+    >
+      <el-table-column type="selection" width="55" align="center" />
+      <el-table-column label="仓位编码" prop="slotCode" width="150" />
+      <el-table-column label="仓位类型" prop="slotType">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.SLOT_TYPE" :value="scope.row.slotType" />
+        </template>
+      </el-table-column>
+      <el-table-column label="行" prop="row" />
+      <el-table-column label="列" prop="col" />
+      <el-table-column label="是否被占用" prop="isOccupied">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.IS_OCCUPIED_STATUS" :value="scope.row.isOccupied" />
+        </template>
+      </el-table-column>
+      <el-table-column label="占用仓位的硬件ID" prop="hardwareId" />
+      <el-table-column label="状态" prop="status">
+        <template #default="scope">
+          <el-switch
+            v-if="scope.row.status !== null"
+            v-model="scope.row.status"
+            active-value="0"
+            inactive-value="1"
+            active-color="#13ce66"
+            inactive-color="grey"
+            disabled
+          />
+          <span v-else>-</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="备注" prop="remark" />
+      <el-table-column label="创建时间" prop="createTime" :formatter="dateFormatter" />
+      <el-table-column label="操作" align="center" width="150">
+        <template #default="scope">
+          <el-button
+            link
+            type="primary"
+            @click="openForm('update', scope.row.slotId)"
+            v-hasPermi="['mes:hw:work:update']"
+          >
+            编辑
+          </el-button>
+          <el-button
+            link
+            type="danger"
+            @click="handleDelete(scope.row.slotId)"
+            v-hasPermi="['mes:hw:work:delete']"
+          >
+            删除
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <Pagination
+      v-model:total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </ContentWrap>
+
+  <!-- 表单弹窗 -->
+  <SlotForm ref="formRef" @success="getList" />
+</template>
+
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import * as SlotApi from '@/api/hw/hardware/lockCabinet/slots'
+import SlotForm from './SlotForm.vue'
+
+defineOptions({ name: 'LockCabinetSlotList' })
+
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
+
+const loading = ref(true) // 列表的加载中
+const list = ref([]) // 列表的数据
+const total = ref(0) // 列表的总页数
+const showSearch = ref(true) // 是否显示搜索工作栏
+const selectedIds = ref<number[]>([]) // 选中的数据编号
+
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  slotCode: undefined,
+  slotType: undefined,
+  status: undefined
+})
+
+const queryFormRef = ref() // 搜索表单
+
+/** 查询仓位列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await SlotApi.getIsLockCabinetSlotsPage(queryParams)
+    list.value = data.records
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value?.resetFields()
+  handleQuery()
+}
+
+/** 多选框选中数据 */
+const handleSelectionChange = (selection: any[]) => {
+  selectedIds.value = selection.map(item => item.slotId)
+}
+
+/** 添加/修改操作 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+  formRef.value.open(type, id)
+}
+
+/** 删除按钮操作 */
+const handleDelete = async (id?: number) => {
+  const ids = [id || selectedIds.value].join(',')
+  try {
+    // 删除的二次确认
+    await message.delConfirm()
+    // 发起删除
+    await SlotApi.deleteIsLockCabinetSlotsBySlotIds(ids)
+    message.success(t('common.delSuccess'))
+    // 刷新列表
+    await getList()
+  } catch {}
+}
+
+/** 初始化 **/
+onMounted(async () => {
+  await getList()
+})
+</script>

+ 256 - 0
src/views/hw/lockCabinet/MapData.vue

@@ -0,0 +1,256 @@
+<template>
+  <ContentWrap>
+    <div class="time-card">
+      <Icon icon="ep:time" class="mr-5px" />
+      <div class="time-content">
+        <div class="label">最后更新</div>
+        <div class="time">{{ updateTime }}</div>
+      </div>
+    </div>
+    <div ref="container" class="map-container"></div>
+
+    <Dialog v-model="dialogVisible" title="异常信息" width="400">
+      <h4 class="text-center font-bold">{{ errorInfo }}</h4>
+      <template #footer>
+        <el-button @click="dialogVisible = false">关 闭</el-button>
+      </template>
+    </Dialog>
+  </ContentWrap>
+</template>
+
+<script lang="ts" setup>
+import { ref, onMounted } from 'vue'
+import Konva from 'konva'
+import { getIsSystemAttributeByKey } from '@/api/system/configuration'
+import { getIsLockCabinetSlotsPage } from '@/api/mes/lockCabinet/slots'
+
+defineOptions({ name: 'LockCabinetMap' })
+
+const props = defineProps<{
+  cabinetId: string
+}>()
+
+const stage = ref<Konva.Stage | null>(null)
+const layer = ref<Konva.Layer | null>(null)
+const cachedResults = ref<Record<string, string>>({})
+const cachedImages = ref<Record<string, Konva.Image>>({})
+const slotData = ref<any[]>([])
+const dialogVisible = ref(false)
+const errorInfo = ref('')
+const updateTime = ref<string | null>(null)
+const container = ref<HTMLElement>()
+
+/** 初始化 Konva */
+const initKonva = () => {
+  if (!container.value) return
+  stage.value = new Konva.Stage({
+    container: container.value,
+    width: 900,
+    height: 800
+  })
+  layer.value = new Konva.Layer()
+  stage.value.add(layer.value)
+}
+
+/** 获取数据 */
+const getData = async () => {
+  const data = {
+    pageNo: 1,
+    pageSize: -1,
+    cabinetId: props.cabinetId
+  }
+  try {
+    const res = await getIsLockCabinetSlotsPage(data)
+    updateTime.value = res.records[0]?.updateTime
+    slotData.value = res.records || []
+
+    const icons = [
+      'icon.locker.normal',
+      'icon.locker.out',
+      'icon.padlock.normal',
+      'icon.padlock.out',
+      'icon.locker.exception'
+    ]
+    const results = await Promise.all(icons.map(key => getIsSystemAttributeByKey(key)))
+    cachedResults.value = icons.reduce((map, key, idx) => {
+      map[key] = results[idx].data?.sysAttrValue || ''
+      return map
+    }, {} as Record<string, string>)
+
+    await preloadImages()
+    renderSlots()
+  } catch (err) {
+    console.error('获取数据失败:', err)
+  }
+}
+
+/** 显示错误对话框 */
+const showErrorDialog = (slot: any) => {
+  errorInfo.value = slot.remark || '未知异常'
+  dialogVisible.value = true
+}
+
+/** 预加载图片 */
+const preloadImages = async () => {
+  const urls = Object.values(cachedResults.value)
+  const promises = urls.map(url => loadImageOnce(url))
+  await Promise.all(promises)
+}
+
+/** 渲染仓位 */
+const renderSlots = async () => {
+  if (!layer.value) return
+  layer.value.destroyChildren()
+
+  const grouped: Record<string, any[]> = {}
+  for (const slot of slotData.value) {
+    const key = `${slot.row}`
+    if (!grouped[key]) grouped[key] = []
+    grouped[key].push(slot)
+  }
+
+  const rows = Object.keys(grouped).sort((a, b) => Number(a) - Number(b))
+  const startY = 20
+  const rowHeight = 120
+  const rowGap = 20
+
+  for (let i = 0; i < rows.length; i++) {
+    const rowSlots = grouped[rows[i]]
+    await renderSlotRow(rowSlots, startY + i * (rowHeight + rowGap), rowHeight)
+  }
+
+  layer.value.draw()
+}
+
+/** 渲染仓位行 */
+const renderSlotRow = async (slots: any[], boxTopY: number, boxHeight: number) => {
+  if (!layer.value || !stage.value) return
+
+  const padding = 20
+  const boxWidth = 860
+  const centerX = stage.value.width() / 2
+  const boxStartX = centerX - boxWidth / 2
+
+  const box = new Konva.Rect({
+    x: boxStartX,
+    y: boxTopY,
+    width: boxWidth,
+    height: boxHeight,
+    stroke: 'black',
+    strokeWidth: 2
+  })
+  layer.value.add(box)
+
+  const loadedImages = []
+  for (const slot of slots) {
+    const { slotType, isOccupied } = slot
+    let baseKey = ''
+    if (slotType === '0') {
+      baseKey = isOccupied === '1' ? 'icon.locker.normal' : 'icon.locker.out'
+    } else {
+      baseKey = isOccupied === '1' ? 'icon.padlock.normal' : 'icon.padlock.out'
+    }
+    const baseUrl = cachedResults.value[baseKey]
+    if (!baseUrl || !cachedImages.value[baseUrl]) continue
+
+    const imageNode = cachedImages.value[baseUrl].clone()
+    const width = slotType === '0' ? 110 : 40
+    const height = 90
+    imageNode.setAttrs({ width, height })
+
+    loadedImages.push({ imageNode, slot, width, height })
+  }
+
+  const totalSlots = loadedImages.length
+  const spacing = totalSlots > 1
+    ? (boxWidth - 2 * padding - totalSlots * loadedImages[0].width) / (totalSlots - 1)
+    : 0
+  let currentX = boxStartX + padding
+
+  for (const { imageNode, slot, width, height } of loadedImages) {
+    imageNode.setAttrs({
+      x: currentX,
+      y: boxTopY + (boxHeight - height) / 2
+    })
+    layer.value.add(imageNode)
+
+    if (slot.status === '1') {
+      const exUrl = cachedResults.value['icon.locker.exception']
+      if (exUrl && cachedImages.value[exUrl]) {
+        const exImage = cachedImages.value[exUrl].clone()
+        const exWidth = 30
+        const exHeight = 30
+        exImage.setAttrs({
+          x: currentX + (width - exWidth) / 2,
+          y: boxTopY + (boxHeight - exHeight) / 2,
+          width: exWidth,
+          height: exHeight
+        })
+        exImage.on('click', () => {
+          showErrorDialog(slot)
+        })
+        layer.value.add(exImage)
+      }
+    }
+
+    currentX += width + spacing
+  }
+}
+
+/** 加载图片 */
+const loadImageOnce = (url: string) => {
+  if (cachedImages.value[url]) return Promise.resolve(cachedImages.value[url])
+
+  return new Promise<Konva.Image>((resolve, reject) => {
+    const img = new window.Image()
+    img.crossOrigin = 'Anonymous'
+    img.src = url
+    img.onload = () => {
+      const konvaImage = new Konva.Image({ image: img })
+      cachedImages.value[url] = konvaImage
+      resolve(konvaImage)
+    }
+    img.onerror = reject
+  })
+}
+
+/** 初始化 **/
+onMounted(async () => {
+  initKonva()
+  await getData()
+})
+</script>
+
+<style scoped lang="scss">
+.time-card {
+  display: inline-flex;
+  align-items: center;
+  padding: 12px 20px;
+  background: linear-gradient(135deg, #f5f7fa 0%, #e4e8eb 100%);
+  border-radius: 8px;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+  transition: all 0.3s;
+
+  &.glow {
+    box-shadow: 0 0 15px rgba(64, 158, 255, 0.5);
+  }
+
+  .time-content {
+    .label {
+      font-size: 12px;
+      color: #909399;
+    }
+    .time {
+      font-size: 16px;
+      font-weight: bold;
+      color: #303133;
+    }
+  }
+}
+
+.map-container {
+  width: 900px;
+  height: 400px;
+  margin: 0 auto;
+}
+</style>

+ 273 - 0
src/views/hw/lockCabinet/SlotForm.vue

@@ -0,0 +1,273 @@
+<template>
+  <Dialog v-model="dialogVisible" :title="dialogTitle" width="500">
+    <el-form
+      ref="formRef"
+      v-loading="formLoading"
+      :model="formData"
+      :rules="formRules"
+      label-width="140px"
+    >
+      <el-form-item label="锁柜序号" prop="cabinetId">
+        <el-select
+          v-model="formData.cabinetId"
+          placeholder="请选择锁柜"
+          class="!w-240px"
+        >
+          <el-option
+            v-for="dict in cabinetOptions"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-row>
+        <el-col :span="15">
+          <el-form-item label="仓位编号" prop="slotCode">
+            <el-input
+              v-model="formData.slotCode"
+              placeholder="请输入仓位编号"
+              class="!w-240px"
+            />
+          </el-form-item>
+        </el-col>
+        <el-col :span="8">
+          <el-form-item label-width="80">
+            <el-switch
+              v-model="autoGenFlag"
+              active-color="#13ce66"
+              active-text="自动生成"
+              @change="handleAutoGenChange"
+            />
+          </el-form-item>
+        </el-col>
+      </el-row>
+      <el-form-item label="仓位类型" prop="slotType">
+        <el-select
+          v-model="formData.slotType"
+          placeholder="请选择仓位类型"
+          class="!w-240px"
+        >
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.SLOT_TYPE)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="行" prop="row">
+        <el-input
+          v-model="formData.row"
+          placeholder="请输入行"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="列" prop="col">
+        <el-input
+          v-model="formData.col"
+          placeholder="请输入列"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="是否被占用" prop="isOccupied">
+        <el-radio-group v-model="formData.isOccupied">
+          <el-radio
+            v-for="dict in getIntDictOptions(DICT_TYPE.IS_OCCUPIED_STATUS)"
+            :key="dict.value"
+            :label="dict.value"
+          >
+            {{ dict.label }}
+          </el-radio>
+        </el-radio-group>
+      </el-form-item>
+      <el-form-item
+        label="占用仓位的硬件Id"
+        prop="hardwareId"
+        v-if="formData.isOccupied === '1'"
+      >
+        <el-select
+          v-model="formData.hardwareId"
+          placeholder="请选择硬件ID"
+          clearable
+          class="!w-240px"
+          @change="handleSelectHardware"
+        >
+          <el-option
+            v-for="dict in hardwareOptions"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="状态" prop="status">
+        <el-radio-group v-model="formData.status">
+          <el-radio
+            v-for="dict in getIntDictOptions(DICT_TYPE.SLOT_STATUS)"
+            :key="dict.value"
+            :label="dict.value"
+          >
+            {{ dict.label }}
+          </el-radio>
+        </el-radio-group>
+      </el-form-item>
+      <el-form-item label="备注" prop="remark">
+        <el-input
+          v-model="formData.remark"
+          placeholder="请输入备注"
+          class="!w-240px"
+        />
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+
+<script lang="ts" setup>
+import { ref, reactive } from 'vue'
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import * as SlotApi from '@/api/hw/hardware/lockCabinet/slots'
+import * as lockCabinetApi from '@/api/hw/hardware/lockCabinet/index'
+import * as HardwareApi from '@/api/hw/hardware/information/index'
+// import * as AutocodeApi from '@/api/system/autocode/rule'
+
+defineOptions({ name: 'LockCabinetSlotForm' })
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const dialogTitle = ref('') // 弹窗的标题
+const formLoading = ref(false) // 表单的加载中
+const formType = ref('') // 表单的类型:create - 新增;update - 修改
+const autoGenFlag = ref(false) // 是否自动生成编码
+
+const formData = ref({
+  slotId: undefined,
+  slotCode: '',
+  slotType: undefined,
+  row: undefined,
+  col: undefined,
+  isOccupied: '0',
+  hardwareId: undefined,
+  cabinetId: undefined,
+  status: '0',
+  remark: ''
+})
+
+const formRules = reactive({
+  slotCode: [{ required: true, message: '仓位编号不能为空', trigger: 'blur' }],
+  slotType: [{ required: true, message: '仓位类型不能为空', trigger: 'blur' }],
+  cabinetId: [{ required: true, message: '锁柜序号不能为空', trigger: 'blur' }]
+})
+
+const formRef = ref() // 表单 Ref
+const cabinetOptions = ref([]) // 锁柜选项
+const hardwareOptions = 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 SlotApi.selectIsLockCabinetSlotsById(id)
+    } finally {
+      formLoading.value = false
+    }
+  }
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 校验表单
+  if (!formRef.value) return
+  const valid = await formRef.value.validate()
+  if (!valid) return
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = formData.value
+    if (formType.value === 'create') {
+      await SlotApi.insertIsLockCabinetSlots(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await SlotApi.updateIsLockCabinetSlots(data)
+      message.success(t('common.updateSuccess'))
+    }
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    slotId: undefined,
+    slotCode: '',
+    slotType: undefined,
+    row: undefined,
+    col: undefined,
+    isOccupied: '0',
+    hardwareId: undefined,
+    cabinetId: undefined,
+    status: '0',
+    remark: ''
+  }
+  autoGenFlag.value = false
+  formRef.value?.resetFields()
+}
+
+/** 自动生成编码 */
+const handleAutoGenChange = async (value: boolean) => {
+  if (value) {
+    formData.value.slotCode = await AutocodeApi.genCode('SLOT_CODE')
+  } else {
+    formData.value.slotCode = ''
+  }
+}
+
+/** 选择硬件 */
+const handleSelectHardware = (value: string) => {
+  formData.value.hardwareId = value
+}
+
+/** 获取锁柜列表 */
+const getCabinetList = async () => {
+  const data = { pageNo: 1, pageSize: -1 }
+  const response = await lockCabinetApi.getIsLockCabinetPage(data)
+  cabinetOptions.value = response.records.map(item => ({
+    value: item.cabinetId,
+    label: item.cabinetName
+  }))
+}
+
+/** 获取硬件列表 */
+const getHardwareList = async () => {
+  const data = { pageNo: 1, pageSize: -1 }
+  const response = await HardwareApi.listHardware(data)
+  hardwareOptions.value = response.records.map(item => ({
+    value: item.id,
+    label: item.hardwareName
+  }))
+}
+
+/** 初始化 **/
+onMounted(async () => {
+  await getCabinetList()
+  await getHardwareList()
+})
+</script>

+ 304 - 0
src/views/hw/lockCabinet/index.vue

@@ -0,0 +1,304 @@
+<template>
+  <ContentWrap>
+    <!-- 搜索工作栏 -->
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="100px"
+    >
+      <el-form-item label="锁柜名称" prop="cabinetName">
+        <el-input
+          v-model="queryParams.cabinetName"
+          placeholder="请输入锁柜名称"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="是否在线" prop="isOnline">
+        <el-select
+          v-model="queryParams.isOnline"
+          placeholder="请选择是否在线"
+          clearable
+          class="!w-240px"
+        >
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.IS_ONLINE_STATUS)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="状态" prop="status">
+        <el-select
+          v-model="queryParams.status"
+          placeholder="请选择状态"
+          clearable
+          class="!w-240px"
+        >
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.CABINET_STATUS)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.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="['mes:hw:work:create']"
+        >
+          <Icon icon="ep:plus" class="mr-5px" /> 新增
+        </el-button>
+        <el-button
+          type="danger"
+          plain
+          :disabled="!selectedIds.length"
+          @click="handleDelete()"
+          v-hasPermi="['mes:hw:work:delete']"
+        >
+          <Icon icon="ep:delete" class="mr-5px" /> 批量删除
+        </el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table
+      v-loading="loading"
+      :data="list"
+      @selection-change="handleSelectionChange"
+    >
+      <el-table-column type="selection" width="55" align="center" />
+      <el-table-column label="锁柜编码" prop="cabinetCode" width="150" />
+      <el-table-column label="锁柜名称" prop="cabinetName" />
+      <el-table-column label="硬件ID" prop="hardwareId" />
+      <el-table-column label="硬件序列号" prop="serialNumber" />
+      <el-table-column label="岗位" prop="workstationName" />
+      <el-table-column label="图片" prop="cabinetPicture">
+        <template #default="scope">
+          <div class="img-box" v-if="scope.row.cabinetPicture">
+            <el-image
+              style="width: 50px; height: 50px"
+              :preview-teleported="true"
+              class="images"
+              :hide-on-click-modal="true"
+              :src="scope.row.cabinetPicture"
+              :zoom-rate="1.2"
+              :preview-src-list="[scope.row.cabinetPicture]"
+              :initial-index="1"
+            />
+            <Icon icon="ep:zoom-in" class="eye-icon" />
+          </div>
+          <span v-else>-</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="图标" prop="cabinetIcon">
+        <template #default="scope">
+          <div class="img-box" v-if="scope.row.cabinetIcon">
+            <el-image
+              style="width: 50px; height: 50px"
+              :preview-teleported="true"
+              class="images"
+              :hide-on-click-modal="true"
+              :src="scope.row.cabinetIcon"
+              :zoom-rate="1.2"
+              :preview-src-list="[scope.row.cabinetIcon]"
+              :initial-index="1"
+            />
+            <Icon icon="ep:zoom-in" class="eye-icon" />
+          </div>
+          <span v-else>-</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="是否在线" prop="isOnline">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.IS_ONLINE_STATUS" :value="scope.row.isOnline" />
+        </template>
+      </el-table-column>
+      <el-table-column label="状态" prop="status">
+        <template #default="scope">
+          <el-switch
+            v-if="scope.row.status !== null"
+            v-model="scope.row.status"
+            active-value="1"
+            inactive-value="0"
+            active-color="#13ce66"
+            inactive-color="grey"
+            disabled
+          />
+          <span v-else>-</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="备注" prop="remark" />
+      <el-table-column label="创建时间" prop="createTime" :formatter="dateFormatter" />
+      <el-table-column label="详情" align="center" width="80">
+        <template #default="scope">
+          <el-button link type="primary" @click="lookDetail(scope.row)">查看</el-button>
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" align="center" width="150">
+        <template #default="scope">
+          <el-button
+            link
+            type="primary"
+            @click="openForm('update', scope.row.cabinetId)"
+            v-hasPermi="['mes:hw:work:update']"
+          >
+            编辑
+          </el-button>
+          <el-button
+            link
+            type="danger"
+            @click="handleDelete(scope.row.cabinetId)"
+            v-hasPermi="['mes:hw:work:delete']"
+          >
+            删除
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <Pagination
+      v-model:total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </ContentWrap>
+
+  <!-- 表单弹窗 -->
+  <LockCabinetForm ref="formRef" @success="getList" />
+</template>
+
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import * as LockCabinetApi from '@/api/hw/hardware/lockCabinet/index'
+import LockCabinetForm from './LockCabinetForm.vue'
+
+defineOptions({ name: 'LockCabinetInfo' })
+
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
+const router = useRouter() // 路由
+
+const loading = ref(true) // 列表的加载中
+const list = ref([]) // 列表的数据
+const total = ref(0) // 列表的总页数
+const showSearch = ref(true) // 是否显示搜索工作栏
+const selectedIds = ref<number[]>([]) // 选中的数据编号
+
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  cabinetName: undefined,
+  isOnline: undefined,
+  status: undefined
+})
+
+const queryFormRef = ref() // 搜索表单
+
+/** 查询锁柜列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await LockCabinetApi.getIsLockCabinetPage(queryParams)
+    list.value = data.records
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value?.resetFields()
+  handleQuery()
+}
+
+/** 多选框选中数据 */
+const handleSelectionChange = (selection: any[]) => {
+  selectedIds.value = selection.map(item => item.cabinetId)
+}
+
+/** 添加/修改操作 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+  formRef.value.open(type, id)
+}
+
+/** 查看详情 */
+const lookDetail = (row: any) => {
+  router.push({
+    path: '/mes/hw/lockCabinet/LookDetail',
+    query: {
+      cabinetId: row.cabinetId
+    }
+  })
+}
+
+/** 删除按钮操作 */
+const handleDelete = async (id?: number) => {
+  const ids = [id || selectedIds.value].join(',')
+  try {
+    // 删除的二次确认
+    await message.delConfirm()
+    // 发起删除
+    await LockCabinetApi.deleteIsLockCabinetByCabinetIds(ids)
+    message.success(t('common.delSuccess'))
+    // 刷新列表
+    await getList()
+  } catch {}
+}
+
+/** 初始化 **/
+onMounted(async () => {
+  await getList()
+})
+</script>
+
+<style scoped lang="scss">
+.img-box {
+  width: 50px;
+  height: 50px;
+  position: relative;
+
+  .eye-icon {
+    display: none;
+  }
+
+  &:hover {
+    background: #000;
+
+    .images {
+      opacity: 0.6;
+    }
+
+    .eye-icon {
+      display: block;
+      position: absolute;
+      top: 40%;
+      left: 30%;
+      z-index: 100;
+      color: white;
+      pointer-events: none;
+    }
+  }
+}
+</style>

+ 30 - 0
src/views/hw/lockCabinet/lookDetail.vue

@@ -0,0 +1,30 @@
+<template>
+  <ContentWrap>
+    <el-radio-group v-model="tabPosition" class="mb-15px">
+      <el-radio-button label="first">锁柜视图</el-radio-button>
+      <el-radio-button label="second">列表视图</el-radio-button>
+    </el-radio-group>
+    <component :is="currentComponent" :cabinetId="cabinetId" />
+  </ContentWrap>
+</template>
+
+<script lang="ts" setup>
+import { ref, computed } from 'vue'
+import { useRoute } from 'vue-router'
+import LookList from './LookList.vue'
+import MapData from './MapData.vue'
+
+defineOptions({ name: 'LockCabinetDetail' })
+
+const route = useRoute()
+const tabPosition = ref('first')
+const cabinetId = ref(route.query.cabinetId as string)
+
+const currentComponent = computed(() => {
+  const components = {
+    first: MapData,
+    second: LookList
+  }
+  return components[tabPosition.value]
+})
+</script>

Beberapa file tidak ditampilkan karena terlalu banyak file yang berubah dalam diff ini