zouyu
2025-11-14 5067cad099f9e3ec0a006a5913c28850f5a13391
Merge branch 'dev_7004' into dev_tide

# Conflicts:
# package.json
# src/router/index.js
# vite.config.js
已添加29个文件
已修改85个文件
13485 ■■■■■ 文件已修改
.env.development 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.env.production 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.env.staging 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
bin/build.bat 22 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
index.html 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
package.json 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
public/HYSNico.ico 补丁 | 查看 | 原始文档 | blame | 历史
src/api/collaborativeApproval/noticeManagement.js 69 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/collaborativeApproval/rpaManagement.js 77 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/energyManagement/index.js 115 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/energyManagement/waterManagement.js 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/indexViews/HYSNLogo.png 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/indexViews/HYSNView.png 补丁 | 查看 | 原始文档 | blame | 历史
src/components/PIMTable/PIMTable.vue 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main.js 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/router/index.js 29 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/basicData/customerFile/index.vue 37 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/chatHome/chatHomeIndex/MobileChat.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/approvalProcess/components/infoFormDia.vue 82 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/approvalProcess/fileList.vue 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/approvalProcess/index.vue 32 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/approvalProcess/index1.vue 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/approvalProcess/index2.vue 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/approvalProcess/index3.vue 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/approvalProcess/index4.vue 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/attendanceManagement/index.vue 714 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/knowledgeBase/index.vue 848 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/meetingBoard/index.vue 498 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/noticeManagement/index.vue 705 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/notificationManagement/index.vue 1187 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/rpaManagement/index.vue 400 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/warningSystem/index.vue 307 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/demo/fakePage/index.vue 248 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/energyManagement/dynamicEnergySaving/index.vue 659 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/energyManagement/energyArea/index.vue 511 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/energyManagement/energyCockpit/index.vue 1380 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/energyManagement/energyPeriodTime/index.vue 444 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/energyManagement/energyPower/components/formDia.vue 55 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/energyManagement/gasManagement/index.vue 624 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/energyManagement/meterCollection/index.vue 556 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/energyManagement/waterManagement/components/formDia.vue 62 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/energyManagement/waterManagement/index.vue 20 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/iotMonitor/index.vue 317 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/iotMonitor/indexWD.vue 317 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/ledger/Form.vue 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/ledger/index.vue 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/measurementEquipment/components/calibrationDia.vue 34 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/measurementEquipment/components/formDia.vue 34 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/measurementEquipment/filesDia.vue 202 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/measurementEquipment/index.vue 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/repair/Modal/MaintainModal.vue 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/repair/index.vue 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/upkeep/Modal/MaintenanceModal.vue 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/upkeep/index.vue 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/financialManagement/expenseManagement/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/financialManagement/revenueManagement/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/index.vue 51 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inventoryManagement/dispatchLog/index.vue 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inventoryManagement/issueManagement/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inventoryManagement/receiptManagement/index.vue 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inventoryManagement/stockManagement/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inventoryManagement/stockWarning/index.vue 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/login.vue 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/monitor/job/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/monitor/job/log.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/monitor/logininfor/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/monitor/online/index.vue 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/monitor/operlog/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/contractManagement/filesDia.vue 202 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/contractManagement/index.vue 160 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/onboarding/components/formDia.vue 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/onboarding/index.vue 66 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/procurementManagement/invoiceEntry/index.vue 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/procurementManagement/invoiceEntry/indexOld.vue 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/procurementManagement/paymentEntry/index.vue 47 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/procurementManagement/paymentHistory/index.vue 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/procurementManagement/paymentLedger/index.vue 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/procurementManagement/procurementInvoiceLedger/Form/EditForm.vue 13 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/procurementManagement/procurementInvoiceLedger/index.vue 18 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/procurementManagement/procurementInvoiceLedger/indexOld.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/procurementManagement/procurementLedger/index.vue 428 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/operationScheduling/components/formDia.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionReporting/index.vue 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/safetyMonitoring/index.vue 873 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/qualityManagement/finalInspection/components/formDia.vue 79 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/qualityManagement/finalInspection/index.vue 142 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/qualityManagement/processInspection/components/filesDia.vue 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/qualityManagement/processInspection/components/formDia.vue 97 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/qualityManagement/processInspection/index.vue 137 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/qualityManagement/rawMaterialInspection/components/filesDia.vue 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/qualityManagement/rawMaterialInspection/components/formDia.vue 42 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/qualityManagement/rawMaterialInspection/index.vue 30 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/projectProfit/index.vue 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/invoiceLedger/index.vue 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/invoiceRegistration/index.vue 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/receiptPayment/index.vue 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/receiptPaymentLedger/index.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/salesLedger/fileList.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/salesLedger/index.vue 18 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/config/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/dept/index.vue 37 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/dict/data.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/dict/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/menu/index.vue 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/notice/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/post/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/role/authUser.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/role/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/role/selectUser.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/user/authRole.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/user/index.vue 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/tool/gen/editTable.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/tool/gen/importTable.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/tool/gen/index.vue 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.env.development
@@ -1,8 +1,8 @@
# é¡µé¢æ ‡é¢˜
VITE_APP_TITLE = MIS系统(管理信息系统)
VITE_APP_TITLE = èŠ¯å¯¼äº‘ï¼ˆç®¡ç†ä¿¡æ¯ç³»ç»Ÿï¼‰
# å¼€å‘环境配置
VITE_APP_ENV = 'development'
# MIS系统(管理信息系统)/开发环境
# èŠ¯å¯¼äº‘ï¼ˆç®¡ç†ä¿¡æ¯ç³»ç»Ÿï¼‰/开发环境
VITE_APP_BASE_API = '/dev-api'
.env.production
@@ -1,10 +1,10 @@
# é¡µé¢æ ‡é¢˜
VITE_APP_TITLE = MIS系统(管理信息系统)
VITE_APP_TITLE = èŠ¯å¯¼äº‘ï¼ˆç®¡ç†ä¿¡æ¯ç³»ç»Ÿï¼‰
# ç”Ÿäº§çŽ¯å¢ƒé…ç½®
VITE_APP_ENV = 'production'
# MIS系统(管理信息系统)/生产环境
# èŠ¯å¯¼äº‘ï¼ˆç®¡ç†ä¿¡æ¯ç³»ç»Ÿï¼‰/生产环境
VITE_APP_BASE_API = '/prod-api'
# æ˜¯å¦åœ¨æ‰“包时开启压缩,支持 gzip å’Œ brotli
.env.staging
@@ -1,10 +1,10 @@
# é¡µé¢æ ‡é¢˜
VITE_APP_TITLE = MIS系统(管理信息系统)
VITE_APP_TITLE = èŠ¯å¯¼äº‘ï¼ˆç®¡ç†ä¿¡æ¯ç³»ç»Ÿï¼‰
# ç”Ÿäº§çŽ¯å¢ƒé…ç½®
VITE_APP_ENV = 'staging'
# MIS系统(管理信息系统)/生产环境
# èŠ¯å¯¼äº‘ï¼ˆç®¡ç†ä¿¡æ¯ç³»ç»Ÿï¼‰/生产环境
VITE_APP_BASE_API = '/stage-api'
# æ˜¯å¦åœ¨æ‰“包时开启压缩,支持 gzip å’Œ brotli
bin/build.bat
@@ -1,12 +1,12 @@
@echo off
echo.
echo [信息] æ‰“包Web工程,生成dist文件。
echo.
%~d0
cd %~dp0
cd ..
yarn build:prod
@echo off
echo.
echo [��Ϣ] ï¿½ï¿½ï¿½Web���̣�����dist���
echo.
%~d0
cd %~dp0
cd ..
yarn build:prod
pause
index.html
@@ -7,7 +7,7 @@
  <meta name="renderer" content="webkit">
  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
  <link rel="icon" href="/favicon.ico">
  <title>MIS系统(管理信息系统)</title>
  <title>芯导云(管理信息系统)</title>
  <!--[if lt IE 11]><script>window.location.href='/html/ie.html';</script><![endif]-->
  <style>
    html,
package.json
@@ -1,7 +1,7 @@
{
  "name": "ruoyi",
  "version": "3.8.9",
  "description": "MES",
  "description": "MIS系统(管理信息系统)",
  "author": "若依",
  "license": "MIT",
  "type": "module",
public/HYSNico.ico
src/api/collaborativeApproval/noticeManagement.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,69 @@
import request from '@/utils/request'
// æŸ¥è¯¢å…¬å‘Šåˆ—表
export function listNotice(query) {
  return request({
    url: '/collaborativeApproval/notice/list',
    method: 'get',
    params: query
  })
}
// æŸ¥è¯¢å…¬å‘Šè¯¦ç»†
export function getNotice(noticeId) {
  return request({
    url: '/collaborativeApproval/notice/' + noticeId,
    method: 'get'
  })
}
// æ–°å¢žå…¬å‘Š
export function addNotice(data) {
  return request({
    url: '/collaborativeApproval/notice',
    method: 'post',
    data: data
  })
}
// ä¿®æ”¹å…¬å‘Š
export function updateNotice(data) {
  return request({
    url: '/collaborativeApproval/notice',
    method: 'put',
    data: data
  })
}
// åˆ é™¤å…¬å‘Š
export function delNotice(noticeId) {
  return request({
    url: '/collaborativeApproval/notice/' + noticeId,
    method: 'delete'
  })
}
// æ‰¹é‡åˆ é™¤å…¬å‘Š
export function delNoticeBatch(noticeIds) {
  return request({
    url: '/collaborativeApproval/notice/batch',
    method: 'delete',
    data: noticeIds
  })
}
// å‘布公告
export function publishNotice(noticeId) {
  return request({
    url: '/collaborativeApproval/notice/publish/' + noticeId,
    method: 'put'
  })
}
// ä¸‹çº¿å…¬å‘Š
export function offlineNotice(noticeId) {
  return request({
    url: '/collaborativeApproval/notice/offline/' + noticeId,
    method: 'put'
  })
}
src/api/collaborativeApproval/rpaManagement.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,77 @@
import request from "@/utils/request";
// æŸ¥è¯¢RPA列表
export function listRpa(query) {
  return request({
    url: "/collaborativeApproval/rpa/list",
    method: "get",
    params: query,
  });
}
// æŸ¥è¯¢RPA详细
export function getRpa(rpaId) {
  return request({
    url: "/collaborativeApproval/rpa/" + rpaId,
    method: "get",
  });
}
// æ–°å¢žRPA
export function addRpa(data) {
  return request({
    url: "/collaborativeApproval/rpa",
    method: "post",
    data: data,
  });
}
// ä¿®æ”¹RPA
export function updateRpa(data) {
  return request({
    url: "/collaborativeApproval/rpa",
    method: "put",
    data: data,
  });
}
// åˆ é™¤RPA
export function delRpa(rpaId) {
  return request({
    url: "/collaborativeApproval/rpa/" + rpaId,
    method: "delete",
  });
}
// æ‰¹é‡åˆ é™¤RPA
export function delRpaBatch(rpaIds) {
  return request({
    url: "/collaborativeApproval/rpa/batch",
    method: "delete",
    data: rpaIds,
  });
}
// å¯åЍRPA
export function startRpa(rpaId) {
  return request({
    url: "/collaborativeApproval/rpa/start/" + rpaId,
    method: "post",
  });
}
// åœæ­¢RPA
export function stopRpa(rpaId) {
  return request({
    url: "/collaborativeApproval/rpa/stop/" + rpaId,
    method: "post",
  });
}
// èŽ·å–RPA状态
export function getRpaStatus(rpaId) {
  return request({
    url: "/collaborativeApproval/rpa/status/" + rpaId,
    method: "get",
  });
}
src/api/energyManagement/index.js
@@ -4,47 +4,124 @@
// è®¾å¤‡èƒ½è€—-分页查询
export function equipmentEnergyListPage(query) {
  return request({
    url: '/equipmentEnergyConsumption/listPage',
    method: 'get',
    url: "/equipmentEnergyConsumption/listPage",
    method: "get",
    params: query,
  })
  });
}
// -能源趋势-分页查询
export function listPageByTrend(query) {
  return request({
    url: '/equipmentEnergyConsumption/listPageByTrend',
    method: 'get',
    url: "/equipmentEnergyConsumption/listPageByTrend",
    method: "get",
    params: query,
  })
  });
}
// åŒºåŸŸ-分页查询
export function areaListPage(query) {
  return request({
    url: "/electricityConsumptionArea/listPage",
    method: "get",
    params: query,
  });
}
// åŒºåŸŸ-树
export function areaListTree(query) {
  return request({
    url: "/electricityConsumptionArea/list",
    method: "get",
    params: query,
  });
}
// æ—¶é—´å‘¨æœŸ-分页查询
export function periodListPage(query) {
  return request({
    url: "/energyPeriod/listPage",
    method: "get",
    params: query,
  });
}
// è®¾å¤‡èƒ½è€—-删除
export function equipmentEnergyDelete(query) {
  return request({
    url: '/equipmentEnergyConsumption/delete',
    method: 'delete',
    url: "/equipmentEnergyConsumption/delete",
    method: "delete",
    data: query,
  })
  });
}
// åŒºåŸŸ-删除
export function areaDelete(query) {
  return request({
    url: "/electricityConsumptionArea/delete",
    method: "delete",
    data: query,
  });
}
// æ—¶é—´å‘¨æœŸ-删除
export function periodDelete(query) {
  return request({
    url: "/energyPeriod/delete",
    method: "delete",
    data: query,
  });
}
// è®¾å¤‡èƒ½è€—-新增
export function equipmentEnergyAdd(query) {
  return request({
    url: '/equipmentEnergyConsumption/add',
    method: 'post',
    url: "/equipmentEnergyConsumption/add",
    method: "post",
    data: query,
  })
  });
}
// åŒºåŸŸ-新增
export function areaAdd(query) {
  return request({
    url: "/electricityConsumptionArea/add",
    method: "post",
    data: query,
  });
}
// æ—¶é—´å‘¨æœŸ-新增
export function periodAdd(query) {
  return request({
    url: "/energyPeriod/add",
    method: "post",
    data: query,
  });
}
// è®¾å¤‡èƒ½è€—-修改
export function equipmentEnergyUpdate(query) {
  return request({
    url: '/equipmentEnergyConsumption/update',
    method: 'post',
    url: "/equipmentEnergyConsumption/update",
    method: "post",
    data: query,
  })
  });
}
//区域-修改
export function areaUpdate(query) {
  return request({
    url: "/electricityConsumptionArea/update",
    method: "post",
    data: query,
  });
}
// æ—¶é—´å‘¨æœŸ-修改
export function periodUpdate(query) {
  return request({
    url: "/energyPeriod/update",
    method: "post",
    data: query,
  });
}
// è®¾å¤‡ä¸‹æ‹‰æ¡†æŸ¥è¯¢
export function deviceList(query) {
  return request({
    url: '/equipmentEnergyConsumption/deviceList',
    method: 'get',
  })
}
    url: "/equipmentEnergyConsumption/deviceList",
    method: "get",
  });
}
src/api/energyManagement/waterManagement.js
@@ -4,7 +4,7 @@
// ç”¨æ°´è®¾å¤‡-分页查询
export function waterEquipmentListPage(query) {
  return request({
    url: '/waterEquipmentConsumption/listPage',
    url: '/waterRecord/listPage',
    method: 'get',
    params: query,
  })
@@ -13,7 +13,7 @@
// ç”¨æ°´è¶‹åŠ¿-分页查询
export function listPageByWaterTrend(query) {
  return request({
    url: '/waterEquipmentConsumption/listPageByTrend',
    url: '/waterRecord/listPageByTrend',
    method: 'get',
    params: query,
  })
@@ -22,7 +22,7 @@
// ç”¨æ°´è®¾å¤‡-删除
export function waterEquipmentDelete(query) {
  return request({
    url: '/waterEquipmentConsumption/delete',
    url: '/waterRecord/delete',
    method: 'delete',
    data: query,
  })
@@ -31,7 +31,7 @@
// ç”¨æ°´è®¾å¤‡-新增
export function waterEquipmentAdd(query) {
  return request({
    url: '/waterEquipmentConsumption/add',
    url: '/waterRecord/add',
    method: 'post',
    data: query,
  })
@@ -40,7 +40,7 @@
// ç”¨æ°´è®¾å¤‡-修改
export function waterEquipmentUpdate(query) {
  return request({
    url: '/waterEquipmentConsumption/update',
    url: '/waterRecord/update',
    method: 'post',
    data: query,
  })
@@ -49,8 +49,9 @@
// ç”¨æ°´è®¾å¤‡ä¸‹æ‹‰æ¡†æŸ¥è¯¢
export function waterDeviceList(query) {
  return request({
    url: '/waterEquipmentConsumption/deviceList',
    url: '/device/ledger/page',
    method: 'get',
    params: query,
  })
}
@@ -89,3 +90,4 @@
    data: query,
  })
}
src/assets/indexViews/HYSNLogo.png
src/assets/indexViews/HYSNView.png
src/components/PIMTable/PIMTable.vue
@@ -15,6 +15,7 @@
    :expand-row-keys="expandRowKeys"
    :show-summary="isShowSummary"
    :summary-method="summaryMethod"
    stripe
    @row-click="rowClick"
    @current-change="currentChange"
    @selection-change="handleSelectionChange"
src/main.js
@@ -76,7 +76,7 @@
app.config.globalProperties.addDateRange = addDateRange;
app.config.globalProperties.selectDictLabel = selectDictLabel;
app.config.globalProperties.selectDictLabels = selectDictLabels;
app.config.globalProperties.javaApi = "http://114.132.189.42:8099";
app.config.globalProperties.javaApi = "http://114.132.189.42:7004";
app.config.globalProperties.HaveJson = (val) => {
  return JSON.parse(JSON.stringify(val));
};
src/router/index.js
@@ -77,20 +77,6 @@
    ]
  },
  {
    path: '/main/MobileChat',
    component: Layout,
    redirect: '',
    hidden: true,
    children: [
      {
        path: '',
        component: () => import('@/views/chatHome/chatHomeIndex/MobileChat'),
        name: 'MobileChat',
        meta: { title: 'AI对话', icon: 'dashboard', affix: true}
      }
    ]
  },
  {
    path: '/user',
    component: Layout,
    hidden: true,
@@ -123,6 +109,21 @@
    ]
  },
  {
    path: '/main/MobileChat',
    component: Layout,
    redirect: '',
    hidden: true,
    permissions: ['MobileChat:edit'],
    children: [
      {
        path: '',
        component: () => import('@/views/chatHome/chatHomeIndex/MobileChat'),
        name: 'MobileChat',
        meta: { title: 'AI对话', activeMenu: '/chatHome/chatHomeIndex'}
      }
    ]
  },
  {
    path: '/system/role-auth',
    component: Layout,
    hidden: true,
src/views/basicData/customerFile/index.vue
@@ -92,25 +92,6 @@
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30" v-for="(contact, index) in formYYs.contactList" :key="index">
          <el-col :span="12">
            <el-form-item label="联系人:" prop="contactPerson">
              <el-input v-model="contact.contactPerson" placeholder="请输入" clearable  />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="联系电话:" prop="contactPhone">
              <div style="display: flex; align-items: center;width: 100%;">
                <el-input v-model="contact.contactPhone" placeholder="请输入" clearable />
                <el-button   @click="removeContact(index)" type="danger" circle style="margin-left: 5px;">
                  <el-icon><Close /></el-icon>
                </el-button>
              </div>
            </el-form-item>
          </el-col>
        </el-row>
        <el-button @click="addNewContact" style="margin-bottom: 10px;">+ æ–°å¢žè”系人</el-button>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="银行基本户:" prop="basicBankAccount">
@@ -142,6 +123,24 @@
            </el-form-item>
          </el-col>
        </el-row>
                <el-row :gutter="30" v-for="(contact, index) in formYYs.contactList" :key="index">
                    <el-col :span="12">
                        <el-form-item label="联系人:" prop="contactPerson">
                            <el-input v-model="contact.contactPerson" placeholder="请输入" clearable  />
                        </el-form-item>
                    </el-col>
                    <el-col :span="12">
                        <el-form-item label="联系电话:" prop="contactPhone">
                            <div style="display: flex; align-items: center;width: 100%;">
                                <el-input v-model="contact.contactPhone" placeholder="请输入" clearable />
                                <el-button   @click="removeContact(index)" type="danger" circle style="margin-left: 5px;">
                                    <el-icon><Close /></el-icon>
                                </el-button>
                            </div>
                        </el-form-item>
                    </el-col>
                </el-row>
                <el-button @click="addNewContact" style="margin-bottom: 10px;">+ æ–°å¢žè”系人</el-button>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="维护人:" prop="maintainer">
src/views/chatHome/chatHomeIndex/MobileChat.vue
@@ -203,7 +203,7 @@
  }
  chatList.value.push(replyMsg)
  scrollBottom()
  loading.value = false
  // å¦‚果有查询关键字,则模拟流式输出
  if (route.query.keyWord) {
    simulateStreamingOutput(replyMsg, route.query.keyWord)
src/views/collaborativeApproval/approvalProcess/components/infoFormDia.vue
@@ -112,6 +112,23 @@
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="24">
            <el-form-item label="附件材料:" prop="remark">
              <el-upload v-model:file-list="fileList" :action="upload.url" multiple ref="fileUpload" auto-upload
                         :headers="upload.headers" :before-upload="handleBeforeUpload" :on-error="handleUploadError"
                         :on-success="handleUploadSuccess" :on-remove="handleRemove">
                <el-button type="primary" v-if="operationType !== 'view'">上传</el-button>
                <template #tip v-if="operationType !== 'view'">
                  <div class="el-upload__tip">
                    æ–‡ä»¶æ ¼å¼æ”¯æŒ
                    doc,docx,xls,xlsx,ppt,pptx,pdf,txt,xml,jpg,jpeg,png,gif,bmp,rar,zip,7z
                  </div>
                </template>
              </el-upload>
            </el-form-item>
          </el-col>
        </el-row>
      </el-form>
      <template #footer>
        <div class="dialog-footer">
@@ -130,7 +147,11 @@
  approveProcessUpdate,
  getDept
} from "@/api/collaborativeApproval/approvalProcess.js";
import {
  delLedgerFile,
} from "@/api/salesManagement/salesLedger.js";
import {userListNoPageByTenantId} from "@/api/system/user.js";
import { getToken } from "@/utils/auth";
const { proxy } = getCurrentInstance()
const emit = defineEmits(['close'])
import useUserStore from "@/store/modules/user";
@@ -138,6 +159,13 @@
const dialogFormVisible = ref(false);
const operationType = ref('')
const fileList = ref([]);
const upload = reactive({
  // ä¸Šä¼ çš„地址
  url: import.meta.env.VITE_APP_BASE_API + "/file/upload",
  // è®¾ç½®ä¸Šä¼ çš„请求头部
  headers: { Authorization: "Bearer " + getToken() },
});
const data = reactive({
  form: {
    approveTime: "",
@@ -146,6 +174,7 @@
        approveDeptId: "",
    approveReason: "",
    checkResult: "",
    tempFileIds: [],
    approverList: [] // æ–°å¢žå­—段,存储所有节点的审批人id
  },
  rules: {
@@ -160,6 +189,12 @@
const { form, rules } = toRefs(data);
const productOptions = ref([]);
const currentApproveStatus = ref(0)
const props = defineProps({
  approveType: {
    type: [Number, String],
    default: 0
  }
})
// å®¡æ‰¹äººèŠ‚ç‚¹ç›¸å…³
const approverNodes = ref([
@@ -176,6 +211,7 @@
// æ‰“开弹框
const openDialog = (type, row) => {
  console.log('openDialog', type, row)
  operationType.value = type;
  dialogFormVisible.value = true;
    userListNoPageByTenantId().then((res) => {
@@ -192,6 +228,8 @@
  // èŽ·å–å½“å‰ç”¨æˆ·ä¿¡æ¯å¹¶è®¾ç½®éƒ¨é—¨ID
  form.value.approveDeptId = userStore.currentDeptId
  if (operationType.value === 'edit') {
    fileList.value = row.commonFileList
    form.value.tempFileIds = fileList.value.map(file => file.id)
        currentApproveStatus.value = row.approveStatus
    approveProcessGetInfo({id: row.approveId,approveReason: '1'}).then(res => {
            form.value = {...res.data}
@@ -233,6 +271,7 @@
const submitForm = () => {
  // æ”¶é›†æ‰€æœ‰èŠ‚ç‚¹çš„å®¡æ‰¹äººid
  form.value.approveUserIds = approverNodes.value.map(node => node.userId).join(',')
  form.value.approveType = props.approveType
  // å®¡æ‰¹äººå¿…填校验
  const hasEmptyApprover = approverNodes.value.some(node => !node.userId)
  if (hasEmptyApprover) {
@@ -257,6 +296,7 @@
}
// å…³é—­å¼¹æ¡†
const closeDia = () => {
  fileList.value = []
  proxy.resetForm("formRef");
  dialogFormVisible.value = false;
  emit('close')
@@ -269,6 +309,48 @@
  const day = String(today.getDate()).padStart(2, "0");
  return `${year}-${month}-${day}`;
}
// ä¸Šä¼ å‰æ ¡æ£€
function handleBeforeUpload(file) {
  // æ ¡æ£€æ–‡ä»¶å¤§å°
  // if (file.size > 1024 * 1024 * 10) {
  //   proxy.$modal.msgError("上传文件大小不能超过10MB!");
  //   return false;
  // }
  proxy.$modal.loading("正在上传文件,请稍候...");
  return true;
}
// ä¸Šä¼ å¤±è´¥
function handleUploadError(err) {
  proxy.$modal.msgError("上传文件失败");
  proxy.$modal.closeLoading();
}
// ä¸Šä¼ æˆåŠŸå›žè°ƒ
function handleUploadSuccess(res, file, uploadFiles) {
  proxy.$modal.closeLoading();
  if (res.code === 200) {
    // ç¡®ä¿ tempFileIds å­˜åœ¨ä¸”为数组
    if (!form.value.tempFileIds) {
      form.value.tempFileIds = [];
    }
    form.value.tempFileIds.push(res.data.tempId);
    proxy.$modal.msgSuccess("上传成功");
  } else {
    proxy.$modal.msgError(res.msg);
    proxy.$refs.fileUpload.handleRemove(file);
  }
}
// ç§»é™¤æ–‡ä»¶
function handleRemove(file) {
  if (operationType.value === "edit") {
    let ids = [];
    ids.push(file.id);
    delLedgerFile(ids).then((res) => {
      proxy.$modal.msgSuccess("删除成功");
    });
  }
}
defineExpose({
  openDialog,
});
src/views/collaborativeApproval/approvalProcess/fileList.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,43 @@
<template>
  <el-dialog v-model="dialogVisible" title="附件" width="40%" :before-close="handleClose">
    <el-table :data="tableData" border height="40vh" stripe>
      <el-table-column label="附件名称" prop="name" min-width="400" show-overflow-tooltip />
      <el-table-column fixed="right" label="操作" width="100" align="center">
        <template #default="scope">
          <el-button link type="primary" size="small" @click="downLoadFile(scope.row)">下载</el-button>
          <el-button link type="primary" size="small" @click="lookFile(scope.row)">预览</el-button>
        </template>
      </el-table-column>
    </el-table>
  </el-dialog>
  <filePreview ref="filePreviewRef" />
</template>
<script setup>
import { ref } from 'vue'
import filePreview from '@/components/filePreview/index.vue'
const dialogVisible = ref(false)
const tableData = ref([])
const { proxy } = getCurrentInstance();
const filePreviewRef = ref()
const handleClose = () => {
  dialogVisible.value = false
}
const open = (list) => {
  dialogVisible.value = true
  tableData.value = list
}
const downLoadFile = (row) => {
  proxy.$download.name(row.url);
}
const lookFile = (row) => {
  filePreviewRef.value.open(row.url)
}
defineExpose({
  open
})
</script>
<style></style>
src/views/collaborativeApproval/approvalProcess/index.vue
@@ -42,12 +42,14 @@
          :total="page.total"
      ></PIMTable>
    </div>
    <info-form-dia ref="infoFormDia" @close="handleQuery"></info-form-dia>
    <info-form-dia ref="infoFormDia" @close="handleQuery" :approveType="approveType"></info-form-dia>
    <approval-dia ref="approvalDia" @close="handleQuery"></approval-dia>
    <FileList ref="fileListRef" />
  </div>
</template>
<script setup>
import FileList from "./fileList.vue";
import { Search } from "@element-plus/icons-vue";
import {onMounted, ref} from "vue";
import {ElMessageBox} from "element-plus";
@@ -55,6 +57,15 @@
import ApprovalDia from "@/views/collaborativeApproval/approvalProcess/components/approvalDia.vue";
import {approveProcessDelete, approveProcessListPage} from "@/api/collaborativeApproval/approvalProcess.js";
import useUserStore from "@/store/modules/user";
// å®šä¹‰ç»„件接收的props
const props = defineProps({
  approveType: {
    type: [Number, String],
    default: 0
  }
});
const userStore = useUserStore();
@@ -116,11 +127,12 @@
  {
    label: "申请人",
    prop: "approveUserName",
    width: 120
  },
  {
    label: "申请日期",
    prop: "approveTime",
        width: 120
        width: 200
  },
  {
    label: "结束日期",
@@ -137,7 +149,7 @@
    label: "操作",
    align: "center",
    fixed: "right",
    width: 150,
    width: 230,
    operation: [
      {
        name: "编辑",
@@ -162,6 +174,13 @@
          openApprovalDia('view', row);
        },
      },
      {
        name: "附件",
        type: "text",
        clickFun: (row) => {
          downLoadFile(row);
        },
      },
    ],
  },
]);
@@ -183,6 +202,11 @@
  page.current = 1;
  getList();
};
const fileListRef = ref(null)
const downLoadFile = (row) => {
  fileListRef.value.open(row.commonFileList)
}
const pagination = (obj) => {
  page.current = obj.page;
  page.size = obj.limit;
@@ -190,7 +214,7 @@
};
const getList = () => {
  tableLoading.value = true;
  approveProcessListPage({...page, ...searchForm.value,}).then(res => {
  approveProcessListPage({...page, ...searchForm.value,approveType:props.approveType}).then(res => {
    tableLoading.value = false;
    tableData.value = res.data.records
    page.total = res.data.total;
src/views/collaborativeApproval/approvalProcess/index1.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,22 @@
<template>
  <div class="container">
    <!-- å¼•å…¥index.vue组件并传递参数 -->
    <ApprovalProcessIndex :approveType="1" />
  </div>
</template>
<script setup>
import ApprovalProcessIndex from './index.vue'
// å®šä¹‰ç»„件名称
defineOptions({
  name: 'ApprovalProcessIndex1'
})
</script>
<style scoped>
.container {
  width: 100%;
  height: 100%;
}
</style>
src/views/collaborativeApproval/approvalProcess/index2.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,22 @@
<template>
  <div class="container">
    <!-- å¼•å…¥index.vue组件并传递参数 -->
    <ApprovalProcessIndex :approveType="2" />
  </div>
</template>
<script setup>
import ApprovalProcessIndex from './index.vue'
// å®šä¹‰ç»„件名称
defineOptions({
  name: 'ApprovalProcessIndex1'
})
</script>
<style scoped>
.container {
  width: 100%;
  height: 100%;
}
</style>
src/views/collaborativeApproval/approvalProcess/index3.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,22 @@
<template>
  <div class="container">
    <!-- å¼•å…¥index.vue组件并传递参数 -->
    <ApprovalProcessIndex :approveType="3" />
  </div>
</template>
<script setup>
import ApprovalProcessIndex from './index.vue'
// å®šä¹‰ç»„件名称
defineOptions({
  name: 'ApprovalProcessIndex1'
})
</script>
<style scoped>
.container {
  width: 100%;
  height: 100%;
}
</style>
src/views/collaborativeApproval/approvalProcess/index4.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,22 @@
<template>
  <div class="container">
    <!-- å¼•å…¥index.vue组件并传递参数 -->
    <ApprovalProcessIndex :approveType="4" />
  </div>
</template>
<script setup>
import ApprovalProcessIndex from './index.vue'
// å®šä¹‰ç»„件名称
defineOptions({
  name: 'ApprovalProcessIndex1'
})
</script>
<style scoped>
.container {
  width: 100%;
  height: 100%;
}
</style>
src/views/collaborativeApproval/attendanceManagement/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,714 @@
<template>
  <div class="app-container">
    <el-tabs v-model="activeTab" type="border-card">
      <!-- å‡æœŸè®¾ç½® -->
      <el-tab-pane label="假期设置" name="holiday">
        <div class="tab-content">
          <el-button type="primary" @click="openDialog('holiday', 'add')">新增假期</el-button>
          <el-table :data="holidayData" border style="width: 100%; margin-top: 20px;" stripe>
            <el-table-column prop="name" label="假期名称" />
            <el-table-column prop="type" label="假期类型">
              <template #default="scope">
                <el-tag :type="getTagType(scope.row.type)">{{ getTypeLabel(scope.row.type) }}</el-tag>
              </template>
            </el-table-column>
            <el-table-column prop="startDate" label="开始日期"  />
            <el-table-column prop="endDate" label="结束日期"  />
            <el-table-column prop="days" label="天数"  align="center" />
            <el-table-column prop="status" label="状态" >
              <template #default="scope">
                <el-tag :type="scope.row.status === 'active' ? 'success' : 'info'">
                  {{ scope.row.status === 'active' ? '启用' : '停用' }}
                </el-tag>
              </template>
            </el-table-column>
            <el-table-column label="操作" fixed="right">
              <template #default="scope">
                <el-button type="primary" size="small" @click="openDialog('holiday', 'edit', scope.row)">编辑</el-button>
                <el-button type="danger" size="small" @click="deleteItem('holiday', scope.row)">删除</el-button>
              </template>
            </el-table-column>
          </el-table>
        </div>
      </el-tab-pane>
      <!-- å¹´å‡è®¾ç½® -->
      <el-tab-pane label="年假设置" name="annual">
        <div class="tab-content">
          <el-button type="primary" @click="openDialog('annual', 'add')">新增年假规则</el-button>
          <el-table :data="annualData" border style="width: 100%; margin-top: 20px;" stripe>
            <el-table-column prop="employeeType" label="员工类型"/>
            <el-table-column prop="workYears" label="工作年限" />
            <el-table-column prop="annualDays" label="年假天数" align="center" />
            <el-table-column prop="maxCarryOver" label="最大结转天数" align="center" />
            <el-table-column prop="status" label="状态">
              <template #default="scope">
                <el-tag :type="scope.row.status === 'active' ? 'success' : 'info'">
                  {{ scope.row.status === 'active' ? '启用' : '停用' }}
                </el-tag>
              </template>
            </el-table-column>
            <el-table-column label="操作" fixed="right">
              <template #default="scope">
                <el-button type="primary" size="small" @click="openDialog('annual', 'edit', scope.row)">编辑</el-button>
                <el-button type="danger" size="small" @click="deleteItem('annual', scope.row)">删除</el-button>
              </template>
            </el-table-column>
          </el-table>
        </div>
      </el-tab-pane>
      <!-- åŠ ç­è®¾ç½® -->
      <el-tab-pane label="加班设置" name="overtime">
        <div class="tab-content">
          <el-button type="primary" @click="openDialog('overtime', 'add')">新增加班规则</el-button>
          <el-table :data="overtimeData" border style="width: 100%; margin-top: 20px;" stripe>
            <el-table-column prop="name" label="规则名称" />
            <el-table-column prop="type" label="加班类型" >
              <template #default="scope">
                <el-tag :type="getTagType(scope.row.type)">{{ getTypeLabel(scope.row.type) }}</el-tag>
              </template>
            </el-table-column>
            <el-table-column prop="startTime" label="开始时间"  />
            <el-table-column prop="endTime" label="结束时间"  />
            <el-table-column prop="rate" label="倍率" align="center" />
            <el-table-column prop="status" label="状态" >
              <template #default="scope">
                <el-tag :type="scope.row.status === 'active' ? 'success' : 'info'">
                  {{ scope.row.status === 'active' ? '启用' : '停用' }}
                </el-tag>
              </template>
            </el-table-column>
            <el-table-column label="操作" fixed="right">
              <template #default="scope">
                <el-button type="primary" size="small" @click="openDialog('overtime', 'edit', scope.row)">编辑</el-button>
                <el-button type="danger" size="small" @click="deleteItem('overtime', scope.row)">删除</el-button>
              </template>
            </el-table-column>
          </el-table>
        </div>
      </el-tab-pane>
      <!-- ä¸Šç­æ—¶é—´è®¾ç½® -->
      <el-tab-pane label="上班时间设置" name="worktime">
        <div class="tab-content">
          <el-button type="primary" @click="openDialog('worktime', 'add')">新增时间段</el-button>
          <el-table :data="worktimeData" border style="width: 100%; margin-top: 20px;" stripe>
            <el-table-column prop="name" label="时间段名称"  />
            <el-table-column prop="startTime" label="上班时间"/>
            <el-table-column prop="endTime" label="下班时间" />
            <el-table-column prop="flexibleStart" label="弹性上班">
              <template #default="scope">
                <el-tag :type="scope.row.flexibleStart ? 'success' : 'info'">
                  {{ scope.row.flexibleStart ? '是' : '否' }}
                </el-tag>
              </template>
            </el-table-column>
            <el-table-column prop="flexibleMinutes" label="弹性时间(分钟)" width="120" align="center" />
            <el-table-column prop="status" label="状态" >
              <template #default="scope">
                <el-tag :type="scope.row.status === 'active' ? 'success' : 'info'">
                  {{ scope.row.status === 'active' ? '启用' : '停用' }}
                </el-tag>
              </template>
            </el-table-column>
            <el-table-column label="操作" fixed="right">
              <template #default="scope">
                <el-button type="primary" size="small" @click="openDialog('worktime', 'edit', scope.row)">编辑</el-button>
                <el-button type="danger" size="small" @click="deleteItem('worktime', scope.row)">删除</el-button>
              </template>
            </el-table-column>
          </el-table>
        </div>
      </el-tab-pane>
    </el-tabs>
    <!-- é€šç”¨å¼¹çª— -->
    <el-dialog v-model="dialogVisible" :title="dialogTitle" width="600px">
      <el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
        <el-form-item label="名称" prop="name" v-if="currentType !== 'annual'">
          <el-input v-model="form.name" placeholder="请输入名称" />
        </el-form-item>
        <el-form-item label="类型" prop="type" v-if="currentType === 'holiday' || currentType === 'overtime'">
          <el-select v-model="form.type" placeholder="请选择类型" style="width: 100%">
            <el-option
              v-for="option in getTypeOptions()"
              :key="option.value"
              :label="option.label"
              :value="option.value"
            />
          </el-select>
        </el-form-item>
        <el-form-item label="员工类型" prop="employeeType" v-if="currentType === 'annual'">
          <el-select v-model="form.employeeType" placeholder="请选择员工类型" style="width: 100%">
            <el-option label="正式员工" value="regular" />
            <el-option label="试用期员工" value="probation" />
            <el-option label="实习生" value="intern" />
          </el-select>
        </el-form-item>
        <el-form-item label="工作年限" prop="workYears" v-if="currentType === 'annual'">
          <el-input v-model="form.workYears" placeholder="如:1-3年、3-5年等" />
        </el-form-item>
        <el-form-item label="年假天数" prop="annualDays" v-if="currentType === 'annual'">
          <el-input-number v-model="form.annualDays" :min="0" :max="365" style="width: 100%" />
        </el-form-item>
        <el-form-item label="最大结转天数" prop="maxCarryOver" v-if="currentType === 'annual'">
          <el-input-number v-model="form.maxCarryOver" :min="0" :max="30" style="width: 100%" />
        </el-form-item>
                          <el-form-item label="日期范围" prop="dateRange" v-if="currentType === 'holiday'">
           <el-date-picker
             v-model="form.dateRange"
             type="daterange"
             range-separator="至"
             start-placeholder="开始日期"
             end-placeholder="结束日期"
             style="width: 100%"
             @change="calculateDays"
           />
         </el-form-item>
        <el-form-item label="天数" prop="days" v-if="currentType === 'holiday'">
          <el-input-number v-model="form.days" :min="0" style="width: 100%" />
        </el-form-item>
                 <el-form-item label="开始时间" prop="startTime" v-if="currentType === 'overtime'">
           <el-time-picker
             v-model="form.startTime"
             placeholder="开始时间"
             format="HH:mm"
             value-format="HH:mm"
             style="width: 100%"
             @change="validateTimeField('startTime')"
           />
         </el-form-item>
         <el-form-item label="结束时间" prop="endTime" v-if="currentType === 'overtime'">
           <el-time-picker
             v-model="form.endTime"
             placeholder="结束时间"
             format="HH:mm"
             value-format="HH:mm"
             style="width: 100%"
             @change="validateTimeField('endTime')"
           />
         </el-form-item>
        <el-form-item label="倍率" prop="rate" v-if="currentType === 'overtime'">
          <el-input-number v-model="form.rate" :min="1" :max="3" :step="0.5" style="width: 100%" />
        </el-form-item>
                 <el-form-item label="上班时间" prop="workStartTime" v-if="currentType === 'worktime'">
           <el-time-picker
             v-model="form.workStartTime"
             placeholder="上班时间"
             format="HH:mm"
             value-format="HH:mm"
             style="width: 100%"
             @change="validateTimeField('workStartTime')"
           />
         </el-form-item>
         <el-form-item label="下班时间" prop="workEndTime" v-if="currentType === 'worktime'">
           <el-time-picker
             v-model="form.workEndTime"
             placeholder="下班时间"
             format="HH:mm"
             value-format="HH:mm"
             style="width: 100%"
             @change="validateTimeField('workEndTime')"
           />
         </el-form-item>
        <el-form-item label="弹性上班" prop="flexibleStart" v-if="currentType === 'worktime'">
          <el-switch v-model="form.flexibleStart" />
        </el-form-item>
        <el-form-item label="弹性时间(分钟)" prop="flexibleMinutes" v-if="currentType === 'worktime' && form.flexibleStart">
          <el-input-number v-model="form.flexibleMinutes" :min="0" :max="120" style="width: 100%" />
        </el-form-item>
                 <el-form-item label="状态" prop="status">
           <el-radio-group v-model="form.status">
             <el-radio value="active">启用</el-radio>
             <el-radio value="inactive">停用</el-radio>
           </el-radio-group>
         </el-form-item>
      </el-form>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="dialogVisible = false">取消</el-button>
          <el-button type="primary" @click="submitForm">确定</el-button>
        </span>
      </template>
    </el-dialog>
  </div>
</template>
<script setup>
import { ref, reactive, onMounted, onUnmounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
// å½“前激活的标签页
const activeTab = ref('holiday')
// å¼¹çª—相关
const dialogVisible = ref(false)
const dialogTitle = ref('')
const currentType = ref('')
const currentAction = ref('')
const currentEditId = ref('')
const formRef = ref()
// è¡¨å•数据
const form = reactive({
  name: '',
  type: '',
  dateRange: [],
  days: 0,
  employeeType: '',
  workYears: '',
  annualDays: 0,
  maxCarryOver: 0,
  startTime: '', // åŠ ç­å¼€å§‹æ—¶é—´
  endTime: '',   // åŠ ç­ç»“æŸæ—¶é—´
  workStartTime: '', // ä¸Šç­æ—¶é—´
  workEndTime: '',   // ä¸‹ç­æ—¶é—´
  rate: 1.5,
  flexibleStart: false,
  flexibleMinutes: 30,
  status: 'active'
})
// è¡¨å•验证规则
const rules = {
  name: [{ required: true, message: '请输入名称', trigger: 'blur' }],
  type: [{ required: true, message: '请选择类型', trigger: 'change' }],
  dateRange: [{ required: true, message: '请选择日期范围', trigger: 'change' }],
  days: [{ required: true, message: '请输入天数', trigger: 'blur' }],
  employeeType: [{ required: true, message: '请选择员工类型', trigger: 'change' }],
  workYears: [{ required: true, message: '请输入工作年限', trigger: 'blur' }],
  annualDays: [{ required: true, message: '请输入年假天数', trigger: 'blur' }],
  maxCarryOver: [{ required: true, message: '请输入最大结转天数', trigger: 'blur' }],
  startTime: [{
    required: true,
    message: '请选择开始时间',
    trigger: 'change',
    validator: (rule, value, callback) => {
      if (!value) {
        callback(new Error('请选择开始时间'))
      } else {
        callback()
      }
    }
  }],
  endTime: [{
    required: true,
    message: '请选择结束时间',
    trigger: 'change',
    validator: (rule, value, callback) => {
      if (!value) {
        callback(new Error('请选择结束时间'))
      } else {
        callback()
      }
    }
  }],
  workStartTime: [{
    required: true,
    message: '请选择上班时间',
    trigger: 'change',
    validator: (rule, value, callback) => {
      if (!value) {
        callback(new Error('请选择上班时间'))
      } else {
        callback()
      }
    }
  }],
  workEndTime: [{
    required: true,
    message: '请选择下班时间',
    trigger: 'change',
    validator: (rule, value, callback) => {
      if (!value) {
        callback(new Error('请选择下班时间'))
      } else {
        callback()
      }
    }
  }],
  rate: [{ required: true, message: '请输入倍率', trigger: 'blur' }]
}
// æ¨¡æ‹Ÿæ•°æ®
const holidayData = ref([
  { id: '1', name: '春节', type: 'legal', startDate: '2024-02-10', endDate: '2024-02-17', days: 8, status: 'active' },
  { id: '2', name: '清明节', type: 'legal', startDate: '2024-04-05', endDate: '2024-04-05', days: 1, status: 'active' },
  { id: '3', name: '劳动节', type: 'legal', startDate: '2024-05-01', endDate: '2024-05-05', days: 5, status: 'active' }
])
const annualData = ref([
  { id: '1', employeeType: 'regular', workYears: '1-3å¹´', annualDays: 5, maxCarryOver: 2, status: 'active' },
  { id: '2', employeeType: 'regular', workYears: '3-5å¹´', annualDays: 10, maxCarryOver: 5, status: 'active' },
  { id: '3', employeeType: 'regular', workYears: '5年以上', annualDays: 15, maxCarryOver: 10, status: 'active' }
])
const overtimeData = ref([
  { id: '1', name: '工作日加班', type: 'weekday', startTime: '18:00', endTime: '22:00', rate: 1.5, status: 'active' },
  { id: '2', name: '周末加班', type: 'weekend', startTime: '09:00', endTime: '18:00', rate: 2.0, status: 'active' },
  { id: '3', name: '深夜加班', type: 'night', startTime: '22:00', endTime: '06:00', rate: 2.5, status: 'active' }
])
const worktimeData = ref([
  { id: '1', name: '标准工作时间', startTime: '09:00', endTime: '18:00', flexibleStart: true, flexibleMinutes: 30, status: 'active' },
  { id: '2', name: '早班时间', startTime: '08:00', endTime: '17:00', flexibleStart: false, flexibleMinutes: 0, status: 'active' },
  { id: '3', name: '晚班时间', startTime: '14:00', endTime: '23:00', flexibleStart: false, flexibleMinutes: 0, status: 'active' }
])
// å·¥å…·å‡½æ•°
const getTagType = (type) => {
  const tagMap = {
    legal: 'success', adjustment: 'warning', special: 'info', company: 'primary',
    weekday: 'primary', weekend: 'warning', holiday: 'danger', night: 'info'
  }
  return tagMap[type] || 'info'
}
const getTypeLabel = (type) => {
  const labelMap = {
    legal: '法定节假日', adjustment: '调休日', special: '特殊假期', company: '公司假期',
    weekday: '工作日加班', weekend: '周末加班', holiday: '节假日加班', night: '深夜加班'
  }
  return labelMap[type] || type
}
const getTypeOptions = () => {
  if (currentType.value === 'holiday') {
    return [
      { label: '法定节假日', value: 'legal' },
      { label: '调休日', value: 'adjustment' },
      { label: '特殊假期', value: 'special' },
      { label: '公司假期', value: 'company' }
    ]
  } else if (currentType.value === 'overtime') {
    return [
      { label: '工作日加班', value: 'weekday' },
      { label: '周末加班', value: 'weekend' },
      { label: '节假日加班', value: 'holiday' },
      { label: '深夜加班', value: 'night' }
    ]
  }
  return []
}
// è®¡ç®—假期天数
const calculateDays = () => {
  try {
    if (form.dateRange && form.dateRange.length === 2 && form.dateRange[0] && form.dateRange[1]) {
      const start = new Date(form.dateRange[0])
      const end = new Date(form.dateRange[1])
      if (isNaN(start.getTime()) || isNaN(end.getTime())) {
        console.warn('无效的日期格式')
        return
      }
      const diffTime = Math.abs(end - start)
      const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)) + 1
      form.days = diffDays
    }
  } catch (error) {
    console.error('计算天数失败:', error)
  }
}
// éªŒè¯æ—¶é—´æ ¼å¼
const validateTime = (time) => {
  if (!time) return ''
  if (typeof time === 'string') return time
  if (time instanceof Date) {
    return time.toTimeString().slice(0, 5)
  }
  return ''
}
// éªŒè¯æ—¶é—´å­—段
const validateTimeField = (fieldName) => {
  try {
    const value = form[fieldName]
    if (value && typeof value === 'object' && value.hour !== undefined) {
      // å¦‚果是时间对象,转换为字符串格式
      const hours = value.hour.toString().padStart(2, '0')
      const minutes = value.minute.toString().padStart(2, '0')
      form[fieldName] = `${hours}:${minutes}`
    }
  } catch (error) {
    console.error(`验证时间字段 ${fieldName} å¤±è´¥:`, error)
    form[fieldName] = ''
  }
}
// æ‰“开弹窗
const openDialog = (type, action, row = null) => {
  try {
    currentType.value = type
    currentAction.value = action
    if (action === 'add') {
      dialogTitle.value = `新增${getTypeName(type)}`
      currentEditId.value = ''
      resetForm()
    } else if (action === 'edit' && row) {
      dialogTitle.value = `编辑${getTypeName(type)}`
      currentEditId.value = row.id
      fillForm(row)
    }
    dialogVisible.value = true
  } catch (error) {
    console.error('打开弹窗失败:', error)
    ElMessage.error('打开弹窗失败,请重试')
  }
}
const getTypeName = (type) => {
  const nameMap = {
    holiday: '假期',
    annual: '年假规则',
    overtime: '加班规则',
    worktime: '时间段'
  }
  return nameMap[type] || ''
}
const resetForm = () => {
  Object.assign(form, {
    name: '',
    type: '',
    dateRange: [],
    days: 0,
    employeeType: '',
    workYears: '',
    annualDays: 0,
    maxCarryOver: 0,
    startTime: '',
    endTime: '',
    workStartTime: '',
    workEndTime: '',
    rate: 1.5,
    flexibleStart: false,
    flexibleMinutes: 30,
    status: 'active'
  })
}
const fillForm = (row) => {
  if (currentType.value === 'holiday') {
    Object.assign(form, {
      name: row.name,
      type: row.type,
      dateRange: [new Date(row.startDate), new Date(row.endDate)],
      days: row.days,
      status: row.status
    })
  } else if (currentType.value === 'annual') {
    Object.assign(form, {
      employeeType: row.employeeType,
      workYears: row.workYears,
      annualDays: row.annualDays,
      maxCarryOver: row.maxCarryOver,
      status: row.status
    })
  } else if (currentType.value === 'overtime') {
    Object.assign(form, {
      name: row.name,
      type: row.type,
      startTime: row.startTime || '',
      endTime: row.endTime || '',
      rate: row.rate,
      status: row.status
    })
  } else if (currentType.value === 'worktime') {
    Object.assign(form, {
      name: row.name,
      workStartTime: row.startTime || '',
      workEndTime: row.endTime || '',
      flexibleStart: row.flexibleStart,
      flexibleMinutes: row.flexibleMinutes,
      status: row.status
    })
  }
}
// æäº¤è¡¨å•
const submitForm = async () => {
  try {
    if (!formRef.value) {
      ElMessage.error('表单引用不存在')
      return
    }
    await formRef.value.validate()
    if (currentAction.value === 'add') {
      addItem()
    } else if (currentAction.value === 'edit') {
      editItem()
    }
    dialogVisible.value = false
    ElMessage.success('操作成功')
  } catch (error) {
    console.error('表单验证失败:', error)
    ElMessage.error('表单验证失败,请检查输入')
  }
}
const addItem = () => {
  const newItem = { ...form, id: Date.now().toString() }
  if (currentType.value === 'holiday') {
    newItem.startDate = form.dateRange[0].toISOString().split('T')[0]
    newItem.endDate = form.dateRange[1].toISOString().split('T')[0]
    holidayData.value.push(newItem)
  } else if (currentType.value === 'annual') {
    annualData.value.push(newItem)
  } else if (currentType.value === 'overtime') {
    newItem.startTime = form.startTime || ''
    newItem.endTime = form.endTime || ''
    overtimeData.value.push(newItem)
  } else if (currentType.value === 'worktime') {
    newItem.startTime = form.workStartTime || ''
    newItem.endTime = form.workEndTime || ''
    worktimeData.value.push(newItem)
  }
}
const editItem = () => {
  let dataArray
  let index
  if (currentType.value === 'holiday') {
    dataArray = holidayData.value
    index = dataArray.findIndex(item => item.id === currentEditId.value)
    if (index > -1) {
      dataArray[index] = {
        ...dataArray[index],
        name: form.name,
        type: form.type,
        startDate: form.dateRange[0].toISOString().split('T')[0],
        endDate: form.dateRange[1].toISOString().split('T')[0],
        days: form.days,
        status: form.status
      }
    }
  } else if (currentType.value === 'annual') {
    dataArray = annualData.value
    index = dataArray.findIndex(item => item.id === currentEditId.value)
    if (index > -1) {
      dataArray[index] = {
        ...dataArray[index],
        employeeType: form.employeeType,
        workYears: form.workYears,
        annualDays: form.annualDays,
        maxCarryOver: form.maxCarryOver,
        status: form.status
      }
    }
  } else if (currentType.value === 'overtime') {
    dataArray = overtimeData.value
    index = dataArray.findIndex(item => item.id === currentEditId.value)
    if (index > -1) {
      dataArray[index] = {
        ...dataArray[index],
        name: form.name,
        type: form.type,
        startTime: form.startTime || '',
        endTime: form.endTime || '',
        rate: form.rate,
        status: form.status
      }
    }
  } else if (currentType.value === 'worktime') {
    dataArray = worktimeData.value
    index = dataArray.findIndex(item => item.id === currentEditId.value)
    if (index > -1) {
      dataArray[index] = {
        ...dataArray[index],
        name: form.name,
        startTime: form.workStartTime || '',
        endTime: form.workEndTime || '',
        flexibleStart: form.flexibleStart,
        flexibleMinutes: form.flexibleMinutes,
        status: form.status
      }
    }
  }
}
// åˆ é™¤é¡¹ç›®
const deleteItem = (type, row) => {
  ElMessageBox.confirm('确定要删除这个项目吗?', '提示', {
    confirmButtonText: '确定',
    cancelButtonText: '取消',
    type: 'warning'
  }).then(() => {
    let dataArray
    if (type === 'holiday') dataArray = holidayData.value
    else if (type === 'annual') dataArray = annualData.value
    else if (type === 'overtime') dataArray = overtimeData.value
    else if (type === 'worktime') dataArray = worktimeData.value
    const index = dataArray.findIndex(item => item.id === row.id)
    if (index > -1) {
      dataArray.splice(index, 1)
      ElMessage.success('删除成功')
    }
  })
}
onMounted(() => {
  console.log('考勤管理页面加载完成')
})
onUnmounted(() => {
  // æ¸…理工作
  dialogVisible.value = false
  currentType.value = ''
  currentAction.value = ''
  currentEditId.value = ''
})
</script>
<style scoped>
.app-container {
  padding: 20px;
}
.tab-content {
  padding: 20px 0;
}
.dialog-footer {
  text-align: right;
}
:deep(.el-tabs__content) {
  padding: 20px;
}
:deep(.el-form-item) {
  margin-bottom: 20px;
}
</style>
src/views/collaborativeApproval/knowledgeBase/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,848 @@
<template>
  <div class="app-container">
    <div class="search_form">
      <div>
        <span class="search_title">知识标题:</span>
        <el-input
          v-model="searchForm.title"
          style="width: 240px"
          placeholder="请输入知识标题搜索"
          @change="handleQuery"
          clearable
          :prefix-icon="Search"
        />
        <span class="search_title ml10">知识类型:</span>
        <el-select v-model="searchForm.type" clearable @change="handleQuery" style="width: 240px">
          <el-option label="合同特批" :value="'contract'" />
          <el-option label="审批案例" :value="'approval'" />
          <el-option label="解决方案" :value="'solution'" />
          <el-option label="经验总结" :value="'experience'" />
          <el-option label="操作指南" :value="'guide'" />
        </el-select>
        <el-button type="primary" @click="handleQuery" style="margin-left: 10px">
          æœç´¢
        </el-button>
      </div>
      <div>
        <el-button type="primary" @click="openForm('add')">新增知识</el-button>
        <el-button type="danger" plain @click="handleDelete">删除</el-button>
      </div>
    </div>
    <div class="table_list">
      <PIMTable
        rowKey="id"
        :column="tableColumn"
        :tableData="tableData"
        :page="page"
        :isSelection="true"
        @selection-change="handleSelectionChange"
        :tableLoading="tableLoading"
        @pagination="pagination"
        :total="page.total"
      ></PIMTable>
    </div>
    <!-- æ–°å¢ž/编辑知识弹窗 -->
    <el-dialog
      v-model="dialogVisible"
      :title="dialogTitle"
      width="800px"
      :close-on-click-modal="false"
    >
      <el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="知识标题" prop="title">
              <el-input v-model="form.title" placeholder="请输入知识标题" />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="知识类型" prop="type">
              <el-select v-model="form.type" placeholder="请选择知识类型" style="width: 100%">
                <el-option label="合同特批" value="contract" />
                <el-option label="审批案例" value="approval" />
                <el-option label="解决方案" value="solution" />
                <el-option label="经验总结" value="experience" />
                <el-option label="操作指南" value="guide" />
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="适用场景" prop="scenario">
              <el-input v-model="form.scenario" placeholder="请输入适用场景" />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="解决效率" prop="efficiency">
              <el-select v-model="form.efficiency" placeholder="请选择解决效率" style="width: 100%">
                <el-option label="显著提升" value="high" />
                <el-option label="一般提升" value="medium" />
                <el-option label="轻微提升" value="low" />
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>
        <el-form-item label="问题描述" prop="problem">
          <el-input
            v-model="form.problem"
            type="textarea"
            :rows="3"
            placeholder="请描述遇到的问题"
          />
        </el-form-item>
        <el-form-item label="解决方案" prop="solution">
          <el-input
            v-model="form.solution"
            type="textarea"
            :rows="4"
            placeholder="请详细描述解决方案"
          />
        </el-form-item>
        <el-form-item label="关键要点" prop="keyPoints">
          <el-input
            v-model="form.keyPoints"
            type="textarea"
            :rows="3"
            placeholder="请输入关键要点,用逗号分隔"
          />
        </el-form-item>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="创建人" prop="creator">
              <el-input v-model="form.creator" placeholder="请输入创建人" />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="使用次数" prop="usageCount">
              <el-input-number v-model="form.usageCount" :min="0" style="width: 100%" />
            </el-form-item>
          </el-col>
        </el-row>
      </el-form>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="dialogVisible = false">取消</el-button>
          <el-button type="primary" @click="submitForm">确定</el-button>
        </span>
      </template>
    </el-dialog>
    <!-- æŸ¥çœ‹çŸ¥è¯†è¯¦æƒ…弹窗 -->
    <el-dialog
      v-model="viewDialogVisible"
      title="知识详情"
      width="900px"
      :close-on-click-modal="false"
    >
      <div class="knowledge-detail">
        <el-descriptions :column="2" border>
          <el-descriptions-item label="知识标题" :span="2">
            <span class="detail-title">{{ currentKnowledge.title }}</span>
          </el-descriptions-item>
          <el-descriptions-item label="知识类型">
            <el-tag :type="getTypeTagType(currentKnowledge.type)">
              {{ getTypeLabel(currentKnowledge.type) }}
            </el-tag>
          </el-descriptions-item>
          <el-descriptions-item label="适用场景">
            {{ currentKnowledge.scenario }}
          </el-descriptions-item>
          <el-descriptions-item label="解决效率">
            <el-tag :type="getEfficiencyTagType(currentKnowledge.efficiency)">
              {{ getEfficiencyLabel(currentKnowledge.efficiency) }}
            </el-tag>
          </el-descriptions-item>
          <el-descriptions-item label="使用次数">
            <el-tag type="info">{{ currentKnowledge.usageCount }} æ¬¡</el-tag>
          </el-descriptions-item>
          <el-descriptions-item label="创建人">
            {{ currentKnowledge.creator }}
          </el-descriptions-item>
          <el-descriptions-item label="创建时间">
            {{ currentKnowledge.createTime }}
          </el-descriptions-item>
        </el-descriptions>
        <div class="detail-section">
          <h4>问题描述</h4>
          <div class="detail-content">{{ currentKnowledge.problem }}</div>
        </div>
        <div class="detail-section">
          <h4>解决方案</h4>
          <div class="detail-content">{{ currentKnowledge.solution }}</div>
        </div>
        <div class="detail-section">
          <h4>关键要点</h4>
          <div class="key-points">
            <el-tag
              v-for="(point, index) in currentKnowledge.keyPoints.split(',')"
              :key="index"
              type="success"
              style="margin-right: 8px; margin-bottom: 8px;"
            >
              {{ point.trim() }}
            </el-tag>
          </div>
        </div>
        <div class="detail-section">
          <h4>使用统计</h4>
          <div class="usage-stats">
            <el-row :gutter="20">
              <el-col :span="8">
                <div class="stat-item">
                  <div class="stat-number">{{ currentKnowledge.usageCount }}</div>
                  <div class="stat-label">使用次数</div>
                </div>
              </el-col>
              <el-col :span="8">
                <div class="stat-item">
                  <div class="stat-number">{{ getEfficiencyScore(currentKnowledge.efficiency) }}%</div>
                  <div class="stat-label">效率提升</div>
                </div>
              </el-col>
              <el-col :span="8">
                <div class="stat-item">
                  <div class="stat-number">{{ getTimeSaved(currentKnowledge.efficiency) }}</div>
                  <div class="stat-label">平均节省时间</div>
                </div>
              </el-col>
            </el-row>
          </div>
        </div>
      </div>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="viewDialogVisible = false">关闭</el-button>
          <el-button type="primary" @click="copyKnowledge">复制知识</el-button>
          <el-button type="success" @click="markAsFavorite">收藏</el-button>
        </span>
      </template>
    </el-dialog>
  </div>
</template>
<script setup>
import { Search } from "@element-plus/icons-vue";
import { onMounted, ref, reactive, toRefs } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import PIMTable from "@/components/PIMTable/PIMTable.vue";
// è¡¨å•验证规则
const rules = {
  title: [
    { required: true, message: "请输入知识标题", trigger: "blur" }
  ],
  type: [
    { required: true, message: "请选择知识类型", trigger: "change" }
  ],
  problem: [
    { required: true, message: "请描述遇到的问题", trigger: "blur" }
  ],
  solution: [
    { required: true, message: "请详细描述解决方案", trigger: "blur" }
  ]
};
// å“åº”式数据
const data = reactive({
  searchForm: {
    title: "",
    type: "",
  },
  tableLoading: false,
  page: {
    current: 1,
    size: 100,
    total: 0,
  },
  tableData: [],
  selectedIds: [],
  form: {
    title: "",
    type: "",
    scenario: "",
    efficiency: "medium",
    problem: "",
    solution: "",
    keyPoints: "",
    creator: "",
    usageCount: 0
  },
  dialogVisible: false,
  dialogTitle: "",
  dialogType: "add",
  viewDialogVisible: false,
  currentKnowledge: {}
});
const {
  searchForm,
  tableLoading,
  page,
  tableData,
  selectedIds,
  form,
  dialogVisible,
  dialogTitle,
  dialogType,
  viewDialogVisible,
  currentKnowledge
} = toRefs(data);
// è¡¨å•引用
const formRef = ref();
// è¡¨æ ¼åˆ—配置
const tableColumn = ref([
  {
    label: "知识标题",
    prop: "title",
    showOverflowTooltip: true,
  },
  {
    label: "知识类型",
    prop: "type",
    dataType: "tag",
    formatData: (params) => {
      const typeMap = {
        contract: "合同特批",
        approval: "审批案例",
        solution: "解决方案",
        experience: "经验总结",
        guide: "操作指南"
      };
      return typeMap[params] || params;
    },
    formatType: (params) => {
      const typeMap = {
        contract: "success",
        approval: "warning",
        solution: "primary",
        experience: "info",
        guide: "danger"
      };
      return typeMap[params] || "info";
    }
  },
  {
    label: "适用场景",
    prop: "scenario",
    width: 150,
    showOverflowTooltip: true,
  },
  {
    label: "解决效率",
    prop: "efficiency",
    dataType: "tag",
    formatData: (params) => {
      const efficiencyMap = {
        high: "显著提升",
        medium: "一般提升",
        low: "轻微提升"
      };
      return efficiencyMap[params] || params;
    },
    formatType: (params) => {
      const typeMap = {
        high: "success",
        medium: "warning",
        low: "info"
      };
      return typeMap[params] || "info";
    }
  },
  {
    label: "使用次数",
    prop: "usageCount",
    width: 100,
    align: "center"
  },
  {
    label: "创建人",
    prop: "creator",
    width: 120,
  },
  {
    label: "创建时间",
    prop: "createTime",
    width: 180,
  },
  {
    dataType: "action",
    label: "操作",
    align: "center",
    fixed: "right",
    width: 200,
    operation: [
      {
        name: "编辑",
        type: "text",
        clickFun: (row) => {
          openForm("edit", row);
        }
      },
      {
        name: "查看",
        type: "text",
        clickFun: (row) => {
          viewKnowledge(row);
        }
      }
    ]
  }
]);
// æ¨¡æ‹Ÿæ•°æ®
let mockData = [
  {
    id: "1",
    title: "特殊合同审批流程优化方案",
    type: "contract",
    scenario: "大额合同快速审批",
    efficiency: "high",
    problem: "大额合同审批流程复杂,审批时间长,影响业务进展",
    solution: "建立绿色通道,对符合条件的合同采用简化审批流程,由部门负责人直接审批,平均审批时间从3天缩短至1天",
    keyPoints: "绿色通道条件,简化流程,审批权限,时间控制",
    creator: "张经理",
    usageCount: 15,
    createTime: "2024-01-15 10:30:00"
  },
  {
    id: "2",
    title: "跨部门协作审批经验总结",
    type: "experience",
    scenario: "多部门协作项目",
    efficiency: "medium",
    problem: "跨部门项目审批时,各部门意见不统一,审批进度缓慢",
    solution: "建立项目协调机制,指定项目负责人,定期召开协调会议,统一各方意见后再进行审批",
    keyPoints: "项目协调,定期会议,统一意见,负责人制度",
    creator: "李主管",
    usageCount: 8,
    createTime: "2024-01-14 15:20:00"
  },
  {
    id: "3",
    title: "紧急采购审批操作指南",
    type: "guide",
    scenario: "紧急采购需求",
    efficiency: "high",
    problem: "紧急采购时审批流程复杂,无法满足紧急需求",
    solution: "制定紧急采购审批标准,明确紧急程度分级,不同级别采用不同审批流程,确保紧急需求得到及时处理",
    keyPoints: "紧急分级,标准制定,流程简化,及时处理",
    creator: "王专员",
    usageCount: 12,
    createTime: "2024-01-13 09:15:00"
  }
];
// çŸ¥è¯†æ ‡é¢˜æ¨¡æ¿
const titleTemplates = [
  "{type}审批流程优化方案",
  "{scenario}处理经验总结",
  "{type}特殊情况处理指南",
  "{scenario}快速审批方案",
  "{type}标准化操作流程",
  "{scenario}问题解决方案",
  "{type}最佳实践总结",
  "{scenario}效率提升方案"
];
// çŸ¥è¯†ç±»åž‹é…ç½®
const knowledgeTypes = [
  { type: "contract", label: "合同特批", efficiency: "high" },
  { type: "approval", label: "审批案例", efficiency: "medium" },
  { type: "solution", label: "解决方案", efficiency: "high" },
  { type: "experience", label: "经验总结", efficiency: "medium" },
  { type: "guide", label: "操作指南", efficiency: "low" }
];
// åœºæ™¯åˆ—表
const scenarios = ["大额合同审批", "跨部门协作", "紧急采购", "特殊申请", "流程优化", "问题处理", "标准化建设", "效率提升"];
// è‡ªåŠ¨ç”Ÿæˆæ–°æ•°æ®
const generateNewData = () => {
  const newId = (mockData.length + 1).toString();
  const now = new Date();
  const randomType = knowledgeTypes[Math.floor(Math.random() * knowledgeTypes.length)];
  const randomScenario = scenarios[Math.floor(Math.random() * scenarios.length)];
  // ç”Ÿæˆéšæœºæ ‡é¢˜
  let title = titleTemplates[Math.floor(Math.random() * titleTemplates.length)];
  title = title
    .replace('{type}', randomType.label)
    .replace('{scenario}', randomScenario);
  const newKnowledge = {
    id: newId,
    title: title,
    type: randomType.type,
    scenario: randomScenario,
    efficiency: randomType.efficiency,
    problem: `在${randomScenario}过程中遇到的问题描述...`,
    solution: `针对${randomScenario}的解决方案和操作步骤...`,
    keyPoints: "关键要点1,关键要点2,关键要点3,关键要点4",
    creator: ["张经理", "李主管", "王专员", "刘总监"][Math.floor(Math.random() * 4)],
    usageCount: Math.floor(Math.random() * 20) + 1,
    createTime: now.toLocaleString()
  };
  // æ·»åŠ åˆ°æ•°æ®å¼€å¤´
  mockData.unshift(newKnowledge);
  // ä¿æŒæ•°æ®é‡åœ¨åˆç†èŒƒå›´å†…(最多保留30条)
  if (mockData.length > 30) {
    mockData = mockData.slice(0, 30);
  }
  console.log(`[${new Date().toLocaleString()}] è‡ªåŠ¨ç”Ÿæˆæ–°çŸ¥è¯†: ${title}`);
};
// ç”Ÿå‘½å‘¨æœŸ
onMounted(() => {
  getList();
  startAutoRefresh();
});
// å¼€å§‹è‡ªåŠ¨åˆ·æ–°
const startAutoRefresh = () => {
  setInterval(() => {
    generateNewData();
    getList();
  }, 600000); // 10分钟刷新一次 (10 * 60 * 1000 = 600000ms)
};
// æŸ¥è¯¢æ•°æ®
const handleQuery = () => {
  page.value.current = 1;
  getList();
};
const getList = () => {
  tableLoading.value = true;
  setTimeout(() => {
    let filteredData = [...mockData];
    if (searchForm.value.title) {
      filteredData = filteredData.filter(item =>
        item.title.toLowerCase().includes(searchForm.value.title.toLowerCase())
      );
    }
    if (searchForm.value.type) {
      filteredData = filteredData.filter(item => item.type === searchForm.value.type);
    }
    tableData.value = filteredData;
    page.value.total = filteredData.length;
    tableLoading.value = false;
  }, 500);
};
// åˆ†é¡µå¤„理
const pagination = (obj) => {
  page.value.current = obj.page;
  page.value.size = obj.limit;
  handleQuery();
};
// é€‰æ‹©å˜åŒ–处理
const handleSelectionChange = (selection) => {
  selectedIds.value = selection.map(item => item.id);
};
// æ‰“开表单
const openForm = (type, row = null) => {
  dialogType.value = type;
  if (type === "add") {
    dialogTitle.value = "新增知识";
    // é‡ç½®è¡¨å•
    Object.assign(form.value, {
      title: "",
      type: "",
      scenario: "",
      efficiency: "medium",
      problem: "",
      solution: "",
      keyPoints: "",
      creator: "",
      usageCount: 0
    });
  } else if (type === "edit" && row) {
    dialogTitle.value = "编辑知识";
    Object.assign(form.value, {
      title: row.title,
      type: row.type,
      scenario: row.scenario,
      efficiency: row.efficiency,
      problem: row.problem,
      solution: row.solution,
      keyPoints: row.keyPoints,
      creator: row.creator,
      usageCount: row.usageCount
    });
  }
  dialogVisible.value = true;
};
// æŸ¥çœ‹çŸ¥è¯†è¯¦æƒ…
const viewKnowledge = (row) => {
  currentKnowledge.value = { ...row };
  viewDialogVisible.value = true;
};
// èŽ·å–ç±»åž‹æ ‡ç­¾ç±»åž‹
const getTypeTagType = (type) => {
  const typeMap = {
    contract: "success",
    approval: "warning",
    solution: "primary",
    experience: "info",
    guide: "danger"
  };
  return typeMap[type] || "info";
};
// èŽ·å–ç±»åž‹æ ‡ç­¾æ–‡æœ¬
const getTypeLabel = (type) => {
  const typeMap = {
    contract: "合同特批",
    approval: "审批案例",
    solution: "解决方案",
    experience: "经验总结",
    guide: "操作指南"
  };
  return typeMap[type] || type;
};
// èŽ·å–æ•ˆçŽ‡æ ‡ç­¾ç±»åž‹
const getEfficiencyTagType = (efficiency) => {
  const typeMap = {
    high: "success",
    medium: "warning",
    low: "info"
  };
  return typeMap[efficiency] || "info";
};
// èŽ·å–æ•ˆçŽ‡æ ‡ç­¾æ–‡æœ¬
const getEfficiencyLabel = (efficiency) => {
  const efficiencyMap = {
    high: "显著提升",
    medium: "一般提升",
    low: "轻微提升"
  };
  return efficiencyMap[efficiency] || efficiency;
};
// èŽ·å–æ•ˆçŽ‡æå‡ç™¾åˆ†æ¯”
const getEfficiencyScore = (efficiency) => {
  const scoreMap = {
    high: 40,
    medium: 25,
    low: 15
  };
  return scoreMap[efficiency] || 0;
};
// èŽ·å–å¹³å‡èŠ‚çœæ—¶é—´
const getTimeSaved = (efficiency) => {
  const timeMap = {
    high: "2-3天",
    medium: "1-2天",
    low: "0.5-1天"
  };
  return timeMap[efficiency] || "未知";
};
// å¤åˆ¶çŸ¥è¯†
const copyKnowledge = () => {
  const knowledgeText = `
知识标题:${currentKnowledge.value.title}
知识类型:${getTypeLabel(currentKnowledge.value.type)}
适用场景:${currentKnowledge.value.scenario}
问题描述:${currentKnowledge.value.problem}
解决方案:${currentKnowledge.value.solution}
关键要点:${currentKnowledge.value.keyPoints}
创建人:${currentKnowledge.value.creator}
  `.trim();
  // å¤åˆ¶åˆ°å‰ªè´´æ¿
  navigator.clipboard.writeText(knowledgeText).then(() => {
    ElMessage.success("知识内容已复制到剪贴板");
  }).catch(() => {
    ElMessage.error("复制失败,请手动复制");
  });
};
// æ”¶è—çŸ¥è¯†
const markAsFavorite = () => {
  // å¢žåŠ ä½¿ç”¨æ¬¡æ•°
  const index = mockData.findIndex(item => item.id === currentKnowledge.value.id);
  if (index !== -1) {
    mockData[index].usageCount += 1;
    currentKnowledge.value.usageCount += 1;
  }
  ElMessage.success("已收藏,使用次数+1");
};
// æäº¤çŸ¥è¯†è¡¨å•
const submitForm = async () => {
  try {
    await formRef.value.validate();
    if (dialogType.value === "add") {
      // æ–°å¢žçŸ¥è¯†
      const newKnowledge = {
        id: (mockData.length + 1).toString(),
        title: form.value.title,
        type: form.value.type,
        scenario: form.value.scenario,
        efficiency: form.value.efficiency,
        problem: form.value.problem,
        solution: form.value.solution,
        keyPoints: form.value.keyPoints,
        creator: form.value.creator,
        usageCount: form.value.usageCount,
        createTime: new Date().toLocaleString()
      };
      mockData.unshift(newKnowledge);
      ElMessage.success("知识创建成功");
    } else {
      // ç¼–辑知识
      const index = mockData.findIndex(item => item.id === selectedIds.value[0]);
      if (index !== -1) {
        Object.assign(mockData[index], {
          title: form.value.title,
          type: form.value.type,
          scenario: form.value.scenario,
          efficiency: form.value.efficiency,
          problem: form.value.problem,
          solution: form.value.solution,
          keyPoints: form.value.keyPoints,
          creator: form.value.creator,
          usageCount: form.value.usageCount
        });
        ElMessage.success("知识更新成功");
      }
    }
    dialogVisible.value = false;
    getList();
  } catch (error) {
    console.error("表单验证失败:", error);
  }
};
// åˆ é™¤çŸ¥è¯†
const handleDelete = () => {
  if (selectedIds.value.length === 0) {
    ElMessage.warning("请选择要删除的知识");
    return;
  }
  ElMessageBox.confirm("选中的内容将被删除,是否确认删除?", "删除", {
    confirmButtonText: "确认",
    cancelButtonText: "取消",
    type: "warning",
  }).then(() => {
    // ä»ŽmockData中删除选中的项
    selectedIds.value.forEach(id => {
      const index = mockData.findIndex(item => item.id === id);
      if (index !== -1) {
        mockData.splice(index, 1);
      }
    });
    ElMessage.success("删除成功");
    selectedIds.value = [];
    getList();
  }).catch(() => {
    // ç”¨æˆ·å–消
  });
};
</script>
<style scoped>
.auto-refresh-info {
  margin-bottom: 15px;
}
.auto-refresh-info .el-alert {
  border-radius: 8px;
}
.dialog-footer {
  text-align: right;
}
.knowledge-detail {
  padding: 20px 0;
}
.detail-title {
  font-size: 18px;
  font-weight: bold;
  color: #303133;
}
.detail-section {
  margin-top: 24px;
}
.detail-section h4 {
  margin: 0 0 12px 0;
  font-size: 16px;
  font-weight: 600;
  color: #303133;
  border-left: 4px solid #409eff;
  padding-left: 12px;
}
.detail-content {
  background: #f8f9fa;
  padding: 16px;
  border-radius: 6px;
  line-height: 1.6;
  color: #606266;
  white-space: pre-wrap;
}
.key-points {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}
.usage-stats {
  margin-top: 16px;
}
.stat-item {
  text-align: center;
  padding: 20px;
  background: #f8f9fa;
  border-radius: 8px;
}
.stat-number {
  font-size: 24px;
  font-weight: bold;
  color: #409eff;
  margin-bottom: 8px;
}
.stat-label {
  font-size: 14px;
  color: #909399;
}
</style>
src/views/collaborativeApproval/meetingBoard/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,498 @@
<template>
  <div class="app-container">
    <!-- é¡µé¢æ ‡é¢˜ -->
    <div class="page-header">
      <h2>会议看板</h2>
<!--      <el-button type="primary" @click="createMeeting">创建会议</el-button>-->
    </div>
    <!-- ä¼šè®®ç»Ÿè®¡å¡ç‰‡ -->
    <div class="stats-cards">
      <el-card class="stat-card">
        <div class="stat-content">
          <div class="stat-number">{{ stats.total }}</div>
          <div class="stat-label">总会议数</div>
        </div>
      </el-card>
      <el-card class="stat-card">
        <div class="stat-content">
          <div class="stat-number">{{ stats.ongoing }}</div>
          <div class="stat-label">进行中</div>
        </div>
      </el-card>
      <el-card class="stat-card">
        <div class="stat-content">
          <div class="stat-number">{{ stats.completed }}</div>
          <div class="stat-label">已完成</div>
        </div>
      </el-card>
      <el-card class="stat-card">
        <div class="stat-content">
          <div class="stat-number">{{ stats.upcoming }}</div>
          <div class="stat-label">即将开始</div>
        </div>
      </el-card>
    </div>
    <!-- ä¼šè®®åˆ—表 -->
    <div class="meeting-list">
      <el-card v-for="meeting in meetings" :key="meeting.id" class="meeting-card">
        <div class="meeting-header">
          <div class="meeting-title">
            <h3>{{ meeting.title }}</h3>
            <el-tag :type="getStatusType(meeting.status)" size="small">
              {{ getStatusText(meeting.status) }}
            </el-tag>
          </div>
          <div class="meeting-time">
            <el-icon><Clock /></el-icon>
            {{ formatTime(meeting.startTime) }} - {{ formatTime(meeting.endTime) }}
          </div>
        </div>
        <div class="meeting-info">
          <div class="info-item">
            <el-icon><Location /></el-icon>
            <span>{{ meeting.location }}</span>
          </div>
          <div class="info-item">
            <el-icon><User /></el-icon>
            <span>主持人: {{ meeting.host }}</span>
          </div>
          <div class="info-item">
            <el-icon><UserFilled /></el-icon>
            <span>参会人数: {{ meeting.participants.length }}人</span>
          </div>
        </div>
        <div class="meeting-agenda">
          <h4>议程安排</h4>
          <div class="agenda-list">
            <div
              v-for="(agenda, index) in meeting.agenda"
              :key="index"
              class="agenda-item"
              :class="{ 'active': agenda.status === 'active', 'completed': agenda.status === 'completed' }"
            >
              <span class="agenda-time">{{ agenda.time }}</span>
              <span class="agenda-content">{{ agenda.content }}</span>
              <el-tag
                :type="getAgendaStatusType(agenda.status)"
                size="small"
              >
                {{ getAgendaStatusText(agenda.status) }}
              </el-tag>
            </div>
          </div>
        </div>
<!--        <div class="meeting-actions">-->
<!--          <el-button type="primary" size="small" @click="joinMeeting(meeting)">-->
<!--            åŠ å…¥ä¼šè®®-->
<!--          </el-button>-->
<!--          <el-button type="info" size="small" @click="viewDetails(meeting)">-->
<!--            æŸ¥çœ‹è¯¦æƒ…-->
<!--          </el-button>-->
<!--          <el-button type="warning" size="small" @click="editMeeting(meeting)">-->
<!--            ç¼–辑-->
<!--          </el-button>-->
<!--        </div>-->
      </el-card>
    </div>
    <!-- åˆ›å»ºä¼šè®®å¯¹è¯æ¡† -->
    <el-dialog v-model="dialogVisible" title="创建会议" width="600px">
      <el-form :model="meetingForm" label-width="100px">
        <el-form-item label="会议标题">
          <el-input v-model="meetingForm.title" placeholder="请输入会议标题" />
        </el-form-item>
        <el-form-item label="会议时间">
          <el-date-picker
            v-model="meetingForm.timeRange"
            type="datetimerange"
            range-separator="至"
            start-placeholder="开始时间"
            end-placeholder="结束时间"
            format="YYYY-MM-DD HH:mm"
            value-format="YYYY-MM-DD HH:mm:ss"
          />
        </el-form-item>
        <el-form-item label="会议地点">
          <el-input v-model="meetingForm.location" placeholder="请输入会议地点" />
        </el-form-item>
        <el-form-item label="主持人">
          <el-input v-model="meetingForm.host" placeholder="请输入主持人姓名" />
        </el-form-item>
        <el-form-item label="会议描述">
          <el-input
            v-model="meetingForm.description"
            type="textarea"
            :rows="3"
            placeholder="请输入会议描述"
          />
        </el-form-item>
      </el-form>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="dialogVisible = false">取消</el-button>
          <el-button type="primary" @click="submitMeeting">确定</el-button>
        </span>
      </template>
    </el-dialog>
  </div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { Clock, Location, User, UserFilled } from '@element-plus/icons-vue'
// ç»Ÿè®¡æ•°æ®
const stats = reactive({
  total: 12,
  ongoing: 3,
  completed: 7,
  upcoming: 2
})
// ä¼šè®®æ•°æ®
const meetings = ref([
  {
    id: 1,
    title: '产品开发周会',
    status: 'ongoing',
    startTime: '2024-01-15 09:00:00',
    endTime: '2024-01-15 10:30:00',
    location: '会议室A',
    host: '张经理',
    participants: ['张经理', '李工程师', '王设计师', '赵测试员'],
    agenda: [
      { time: '09:00-09:15', content: '上周工作总结', status: 'completed' },
      { time: '09:15-09:45', content: '本周开发计划', status: 'active' },
      { time: '09:45-10:00', content: '技术难点讨论', status: 'pending' },
      { time: '10:00-10:30', content: '问题反馈与解决', status: 'pending' }
    ]
  },
  {
    id: 2,
    title: '客户需求评审会',
    status: 'upcoming',
    startTime: '2024-01-15 14:00:00',
    endTime: '2024-01-15 15:00:00',
    location: '线上会议',
    host: '陈总监',
    participants: ['陈总监', '刘产品经理', '孙客户经理', '客户代表'],
    agenda: [
      { time: '14:00-14:20', content: '需求背景介绍', status: 'pending' },
      { time: '14:20-14:40', content: '功能需求分析', status: 'pending' },
      { time: '14:40-15:00', content: '技术可行性评估', status: 'pending' }
    ]
  },
  {
    id: 3,
    title: '团队建设活动',
    status: 'completed',
    startTime: '2024-01-14 16:00:00',
    endTime: '2024-01-14 18:00:00',
    location: '公司大厅',
    host: '人事部',
    participants: ['全体员工'],
    agenda: [
      { time: '16:00-16:30', content: '团队游戏', status: 'completed' },
      { time: '16:30-17:00', content: '经验分享', status: 'completed' },
      { time: '17:00-18:00', content: '自由交流', status: 'completed' }
    ]
  }
])
// å¯¹è¯æ¡†ç›¸å…³
const dialogVisible = ref(false)
const meetingForm = reactive({
  title: '',
  timeRange: [],
  location: '',
  host: '',
  description: ''
})
// èŽ·å–çŠ¶æ€ç±»åž‹
const getStatusType = (status) => {
  const statusMap = {
    'ongoing': 'success',
    'upcoming': 'warning',
    'completed': 'info'
  }
  return statusMap[status] || 'info'
}
// èŽ·å–çŠ¶æ€æ–‡æœ¬
const getStatusText = (status) => {
  const statusMap = {
    'ongoing': '进行中',
    'upcoming': '即将开始',
    'completed': '已完成'
  }
  return statusMap[status] || '未知'
}
// èŽ·å–è®®ç¨‹çŠ¶æ€ç±»åž‹
const getAgendaStatusType = (status) => {
  const statusMap = {
    'completed': 'success',
    'active': 'warning',
    'pending': 'info'
  }
  return statusMap[status] || 'info'
}
// èŽ·å–è®®ç¨‹çŠ¶æ€æ–‡æœ¬
const getAgendaStatusText = (status) => {
  const statusMap = {
    'completed': '已完成',
    'active': '进行中',
    'pending': '待开始'
  }
  return statusMap[status] || '未知'
}
// æ ¼å¼åŒ–æ—¶é—´
const formatTime = (timeStr) => {
  const date = new Date(timeStr)
  return date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })
}
// åˆ›å»ºä¼šè®®
const createMeeting = () => {
  dialogVisible.value = true
  // é‡ç½®è¡¨å•
  Object.assign(meetingForm, {
    title: '',
    timeRange: [],
    location: '',
    host: '',
    description: ''
  })
}
// æäº¤ä¼šè®®
const submitMeeting = () => {
  if (!meetingForm.title || !meetingForm.timeRange.length || !meetingForm.location || !meetingForm.host) {
    ElMessage.warning('请填写完整的会议信息')
    return
  }
  // åˆ›å»ºæ–°ä¼šè®®
  const newMeeting = {
    id: Date.now(),
    title: meetingForm.title,
    status: 'upcoming',
    startTime: meetingForm.timeRange[0],
    endTime: meetingForm.timeRange[1],
    location: meetingForm.location,
    host: meetingForm.host,
    participants: [meetingForm.host],
    agenda: [
      { time: '待定', content: '议程待定', status: 'pending' }
    ]
  }
  meetings.value.unshift(newMeeting)
  stats.total++
  stats.upcoming++
  ElMessage.success('会议创建成功')
  dialogVisible.value = false
}
// åŠ å…¥ä¼šè®®
const joinMeeting = (meeting) => {
  ElMessage.success(`已加入会议:${meeting.title}`)
}
// æŸ¥çœ‹è¯¦æƒ…
const viewDetails = (meeting) => {
  ElMessage.info(`查看会议详情:${meeting.title}`)
}
// ç¼–辑会议
const editMeeting = (meeting) => {
  ElMessage.info(`编辑会议:${meeting.title}`)
}
onMounted(() => {
  console.log('会议看板页面加载完成')
})
</script>
<style scoped>
.app-container {
  padding: 20px;
}
.page-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 20px;
}
.page-header h2 {
  margin: 0;
  color: #303133;
}
.stats-cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 20px;
  margin-bottom: 30px;
}
.stat-card {
  text-align: center;
}
.stat-content {
  padding: 10px;
}
.stat-number {
  font-size: 32px;
  font-weight: bold;
  color: #409eff;
  margin-bottom: 8px;
}
.stat-label {
  font-size: 14px;
  color: #606266;
}
.meeting-list {
  display: grid;
  gap: 20px;
}
.meeting-card {
  border-radius: 8px;
}
.meeting-header {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  margin-bottom: 15px;
}
.meeting-title {
  display: flex;
  align-items: center;
  gap: 10px;
}
.meeting-title h3 {
  margin: 0;
  color: #303133;
}
.meeting-time {
  display: flex;
  align-items: center;
  gap: 5px;
  color: #606266;
  font-size: 14px;
}
.meeting-info {
  display: flex;
  gap: 20px;
  margin-bottom: 20px;
  flex-wrap: wrap;
}
.info-item {
  display: flex;
  align-items: center;
  gap: 5px;
  color: #606266;
  font-size: 14px;
}
.meeting-agenda {
  margin-bottom: 20px;
}
.meeting-agenda h4 {
  margin: 0 0 15px 0;
  color: #303133;
  font-size: 16px;
}
.agenda-list {
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.agenda-item {
  display: flex;
  align-items: center;
  gap: 15px;
  padding: 10px;
  border-radius: 6px;
  background-color: #f5f7fa;
}
.agenda-item.active {
  background-color: #fdf6ec;
  border-left: 3px solid #e6a23c;
}
.agenda-item.completed {
  background-color: #f0f9ff;
  border-left: 3px solid #409eff;
}
.agenda-time {
  font-weight: bold;
  color: #606266;
  min-width: 80px;
}
.agenda-content {
  flex: 1;
  color: #303133;
}
.meeting-actions {
  display: flex;
  gap: 10px;
  justify-content: flex-end;
}
.dialog-footer {
  display: flex;
  justify-content: flex-end;
  gap: 10px;
}
@media (max-width: 768px) {
  .stats-cards {
    grid-template-columns: repeat(2, 1fr);
  }
  .meeting-header {
    flex-direction: column;
    gap: 10px;
  }
  .meeting-info {
    flex-direction: column;
    gap: 10px;
  }
  .meeting-actions {
    flex-direction: column;
  }
}
</style>
src/views/collaborativeApproval/noticeManagement/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,705 @@
<template>
  <div class="app-container">
    <!-- æœç´¢è¡¨å• -->
    <div class="search_form">
      <div>
        <span class="search_title">公告标题:</span>
        <el-input
          v-model="searchForm.noticeTitle"
          style="width: 240px"
          placeholder="请输入公告标题搜索"
          @change="handleQuery"
          clearable
          :prefix-icon="Search"
        />
        <span class="search_title ml10">公告类型:</span>
        <el-select v-model="searchForm.noticeType" clearable @change="handleQuery" style="width: 240px">
          <el-option label="放假通知" value="1" />
          <el-option label="设备维修通知" value="2" />
        </el-select>
        <span class="search_title ml10">状态:</span>
        <el-select v-model="searchForm.status" clearable @change="handleQuery" style="width: 240px">
          <el-option label="草稿" value="0" />
          <el-option label="已发布" value="1" />
          <el-option label="已下线" value="2" />
        </el-select>
        <el-button type="primary" @click="handleQuery" style="margin-left: 10px">搜索</el-button>
        <el-button @click="resetQuery" style="margin-left: 10px">重置</el-button>
      </div>
      <div>
        <el-button type="primary" @click="openForm('add')">新增公告</el-button>
        <el-button type="danger" plain @click="handleDelete" :disabled="!selectedIds.length">删除</el-button>
      </div>
    </div>
    <!-- é€šçŸ¥å…¬å‘Šæ¿ -->
    <div class="notice-board">
      <!-- æ”¾å‡é€šçŸ¥åŒºåŸŸ -->
      <div class="notice-section" v-if="holidayNotices.length > 0">
        <div class="section-header">
          <h3>📅 æ”¾å‡é€šçŸ¥</h3>
          <span class="section-count">{{ holidayNotices.length }}条</span>
        </div>
        <div class="notice-cards">
          <div
            v-for="notice in holidayNotices"
            :key="notice.id"
            class="notice-card holiday-card"
            :class="{ 'urgent': notice.priority === '3' }"
          >
            <div class="card-header">
              <div class="card-title">
                <el-icon class="holiday-icon"><Calendar /></el-icon>
                {{ notice.noticeTitle }}
              </div>
              <div class="card-actions">
                <el-button link type="primary" @click="handleEdit(notice)">编辑</el-button>
                <el-button link type="danger" @click="handleDelete(notice.id)">删除</el-button>
              </div>
            </div>
            <div class="card-content">
              <p>{{ notice.noticeContent }}</p>
            </div>
            <div class="card-footer">
              <div class="card-meta">
                <span class="priority" :class="'priority-' + notice.priority">
                  {{ getPriorityText(notice.priority) }}
                </span>
                <span class="status" :class="'status-' + notice.status">
                  {{ getStatusText(notice.status) }}
                </span>
              </div>
              <div class="card-info">
                <span class="creator">{{ notice.createBy }}</span>
                <span class="time">{{ notice.createTime }}</span>
              </div>
            </div>
            <div class="card-remark" v-if="notice.remark">
              <el-icon><InfoFilled /></el-icon>
              <span>{{ notice.remark }}</span>
            </div>
          </div>
        </div>
      </div>
      <!-- è®¾å¤‡ç»´ä¿®é€šçŸ¥åŒºåŸŸ -->
      <div class="notice-section" v-if="maintenanceNotices.length > 0">
        <div class="section-header">
          <h3>🔧 è®¾å¤‡ç»´ä¿®é€šçŸ¥</h3>
          <span class="section-count">{{ maintenanceNotices.length }}条</span>
        </div>
        <div class="notice-cards">
          <div
            v-for="notice in maintenanceNotices"
            :key="notice.id"
            class="notice-card maintenance-card"
            :class="{ 'urgent': notice.priority === '3' }"
          >
            <div class="card-header">
              <div class="card-title">
                <el-icon class="maintenance-icon"><Tools /></el-icon>
                {{ notice.noticeTitle }}
              </div>
              <div class="card-actions">
                <el-button link type="primary" @click="handleEdit(notice)">编辑</el-button>
                <el-button link type="danger" @click="handleDelete(notice.id)">删除</el-button>
              </div>
            </div>
            <div class="card-content">
              <p>{{ notice.noticeContent }}</p>
            </div>
            <div class="card-footer">
              <div class="card-meta">
                <span class="priority" :class="'priority-' + notice.priority">
                  {{ getPriorityText(notice.priority) }}
                </span>
                <span class="status" :class="'status-' + notice.status">
                  {{ getStatusText(notice.status) }}
                </span>
              </div>
              <div class="card-info">
                <span class="creator">{{ notice.createBy }}</span>
                <span class="time">{{ notice.createTime }}</span>
              </div>
            </div>
            <div class="card-remark" v-if="notice.remark">
              <el-icon><InfoFilled /></el-icon>
              <span>{{ notice.remark }}</span>
            </div>
          </div>
        </div>
      </div>
      <!-- ç©ºçŠ¶æ€ -->
      <div class="empty-state" v-if="filteredNotices.length === 0">
        <el-empty description="暂无通知公告" />
      </div>
    </div>
    <!-- æ–°å¢ž/编辑对话框 -->
    <el-dialog
      :title="dialogTitle"
      v-model="dialogVisible"
      width="800px"
      append-to-body
      @close="resetForm"
    >
      <el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
        <el-row>
          <el-col :span="12">
            <el-form-item label="公告标题" prop="noticeTitle">
              <el-input v-model="form.noticeTitle" placeholder="请输入公告标题" />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="公告类型" prop="noticeType">
              <el-select v-model="form.noticeType" placeholder="请选择公告类型" style="width: 100%">
                <el-option label="放假通知" value="1" />
                <el-option label="设备维修通知" value="2" />
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row>
          <el-col :span="12">
            <el-form-item label="状态">
              <el-radio-group v-model="form.status">
                <el-radio value="0">草稿</el-radio>
                <el-radio value="1">已发布</el-radio>
                <el-radio value="2">已下线</el-radio>
              </el-radio-group>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="优先级">
              <el-select v-model="form.priority" placeholder="请选择优先级" style="width: 100%">
                <el-option label="普通" value="1" />
                <el-option label="重要" value="2" />
                <el-option label="紧急" value="3" />
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row>
          <el-col :span="24">
            <el-form-item label="公告内容" prop="noticeContent">
              <el-input
                v-model="form.noticeContent"
                type="textarea"
                :rows="6"
                placeholder="请输入公告内容"
                maxlength="500"
                show-word-limit
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row>
          <el-col :span="24">
            <el-form-item label="备注">
              <el-input
                v-model="form.remark"
                type="textarea"
                :rows="3"
                placeholder="请输入备注信息"
                maxlength="200"
                show-word-limit
              />
            </el-form-item>
          </el-col>
        </el-row>
      </el-form>
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary" @click="submitForm">ç¡® å®š</el-button>
          <el-button @click="dialogVisible = false">取 æ¶ˆ</el-button>
        </div>
      </template>
    </el-dialog>
  </div>
</template>
<script setup>
import { Search, Calendar, Tools, InfoFilled } from "@element-plus/icons-vue";
import { onMounted, ref, reactive, toRefs, computed } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import useUserStore from "@/store/modules/user";
const userStore = useUserStore();
// å“åº”式数据
const data = reactive({
  searchForm: {
    noticeTitle: "",
    noticeType: "",
    status: "",
  },
  form: {
    id: undefined,
    noticeTitle: "",
    noticeType: "",
    noticeContent: "",
    status: "0",
    priority: "1",
    remark: "",
    createBy: "",
    createTime: "",
  },
  rules: {
    noticeTitle: [
      { required: true, message: "公告标题不能为空", trigger: "blur" }
    ],
    noticeType: [
      { required: true, message: "请选择公告类型", trigger: "change" }
    ],
    noticeContent: [
      { required: true, message: "公告内容不能为空", trigger: "blur" }
    ]
  }
});
const { searchForm, form, rules } = toRefs(data);
// é¡µé¢çŠ¶æ€
const dialogVisible = ref(false);
const dialogTitle = ref("");
const selectedIds = ref([]);
const formRef = ref();
// æ¨¡æ‹Ÿæ•°æ® - æ ¹æ®æ³•定节假日设计
const mockData = [
  {
    id: 1,
    noticeTitle: "2024年春节放假通知",
    noticeType: "1",
    priority: "2",
    status: "1",
    noticeContent: "根据国务院办公厅通知,2024年春节放假安排如下:2月10日(初一)至2月17日(初八)放假调休,共8天。2月4日(星期日)、2月18日(星期日)上班。请各部门提前做好工作安排。",
    remark: "放假期间请保持手机畅通,如有紧急事务及时联系",
    createBy: "人事部",
    createTime: "2024-01-15 10:30:00"
  },
  {
    id: 2,
    noticeTitle: "2024年清明节放假通知",
    noticeType: "1",
    priority: "1",
    status: "1",
    noticeContent: "根据国务院办公厅通知,2024年清明节放假安排如下:4月4日(星期四)至4月6日(星期六)放假调休,共3天。4月7日(星期日)上班。",
    remark: "请各部门做好值班安排,确保节日期间各项工作正常运转",
    createBy: "行政部",
    createTime: "2024-01-14 14:20:00"
  },
  {
    id: 3,
    noticeTitle: "2024年劳动节放假通知",
    noticeType: "1",
    priority: "1",
    status: "1",
    noticeContent: "根据国务院办公厅通知,2024年劳动节放假安排如下:5月1日(星期三)至5月5日(星期日)放假调休,共5天。4月28日(星期日)、5月11日(星期六)上班。",
    remark: "放假前请关闭电源,锁好门窗,注意安全",
    createBy: "行政部",
    createTime: "2024-01-13 09:15:00"
  },
  {
    id: 4,
    noticeTitle: "2024年端午节放假通知",
    noticeType: "1",
    priority: "1",
    status: "1",
    noticeContent: "根据国务院办公厅通知,2024年端午节放假安排如下:6月8日(星期六)至6月10日(星期一)放假调休,共3天。6月11日(星期二)上班。",
    remark: "祝大家端午节快乐,阖家幸福!",
    createBy: "行政部",
    createTime: "2024-01-12 16:30:00"
  },
  {
    id: 5,
    noticeTitle: "2024年中秋节放假通知",
    noticeType: "1",
    priority: "1",
    status: "1",
    noticeContent: "根据国务院办公厅通知,2024年中秋节放假安排如下:9月15日(星期日)至9月17日(星期二)放假调休,共3天。9月14日(星期六)上班。",
    remark: "中秋佳节,祝大家团圆美满,幸福安康!",
    createBy: "行政部",
    createTime: "2024-01-11 11:20:00"
  },
  {
    id: 6,
    noticeTitle: "2024年国庆节放假通知",
    noticeType: "1",
    priority: "2",
    status: "1",
    noticeContent: "根据国务院办公厅通知,2024年国庆节放假安排如下:10月1日(星期二)至10月7日(星期一)放假调休,共7天。9月29日(星期日)、10月12日(星期六)上班。",
    remark: "国庆期间请各部门做好值班安排,确保安全稳定",
    createBy: "行政部",
    createTime: "2024-01-10 15:45:00"
  },
  {
    id: 7,
    noticeTitle: "A车间生产线年度检修通知",
    noticeType: "2",
    priority: "2",
    status: "1",
    noticeContent: "A车间生产线将于2024å¹´1月20日(周六)进行年度检修维护,预计停工8小时。检修内容包括:设备清洁、润滑保养、安全装置检查等。请生产部门提前调整生产计划。",
    remark: "维修期间请相关人员配合,确保检修工作安全顺利进行",
    createBy: "设备部",
    createTime: "2024-01-14 14:20:00"
  },
  {
    id: 8,
    noticeTitle: "B车间设备预防性维护通知",
    noticeType: "2",
    priority: "1",
    status: "1",
    noticeContent: "B车间关键设备将于2024å¹´1月25日进行预防性维护,预计停工4小时。维护内容包括:设备检查、零件更换、性能测试等。请相关部门配合。",
    remark: "维护完成后将进行试运行,确保设备正常运行",
    createBy: "设备部",
    createTime: "2024-01-13 09:15:00"
  }
];
// è®¡ç®—属性
const filteredNotices = computed(() => {
  let filtered = [...mockData];
  if (searchForm.value.noticeTitle) {
    filtered = filtered.filter(item =>
      item.noticeTitle.includes(searchForm.value.noticeTitle)
    );
  }
  if (searchForm.value.noticeType) {
    filtered = filtered.filter(item =>
      item.noticeType === searchForm.value.noticeType
    );
  }
  if (searchForm.value.status !== "") {
    filtered = filtered.filter(item =>
      item.status === searchForm.value.status
    );
  }
  return filtered;
});
const holidayNotices = computed(() => {
  return filteredNotices.value.filter(notice => notice.noticeType === "1");
});
const maintenanceNotices = computed(() => {
  return filteredNotices.value.filter(notice => notice.noticeType === "2");
});
// æ–¹æ³•定义
const handleQuery = () => {
  // æœç´¢åŠŸèƒ½ä¿æŒä¸å˜ï¼Œä½†æ•°æ®é€šè¿‡è®¡ç®—å±žæ€§è‡ªåŠ¨è¿‡æ»¤
};
const resetQuery = () => {
  searchForm.value = {
    noticeTitle: "",
    noticeType: "",
    status: ""
  };
};
const getPriorityText = (priority) => {
  const priorityMap = { "1": "普通", "2": "重要", "3": "紧急" };
  return priorityMap[priority] || "普通";
};
const getStatusText = (status) => {
  const statusMap = { "0": "草稿", "1": "已发布", "2": "已下线" };
  return statusMap[status] || "未知";
};
const openForm = (type) => {
  if (type === 'add') {
    dialogTitle.value = "新增公告";
    form.value = {
      id: undefined,
      noticeTitle: "",
      noticeType: "",
      noticeContent: "",
      status: "0",
      priority: "1",
      remark: "",
      createBy: userStore.name || "当前用户",
      createTime: new Date().toLocaleString()
    };
  }
  dialogVisible.value = true;
};
const handleEdit = (row) => {
  dialogTitle.value = "编辑公告";
  form.value = { ...row };
  dialogVisible.value = true;
};
const handleSelectionChange = (selection) => {
  selectedIds.value = selection.map(item => item.id);
};
const handleDelete = (id) => {
  ElMessageBox.confirm(
    "确认删除这条公告吗?",
    "提示",
    {
      confirmButtonText: "确定",
      cancelButtonText: "取消",
      type: "warning"
    }
  ).then(() => {
    const index = mockData.findIndex(item => item.id === id);
    if (index > -1) {
      mockData.splice(index, 1);
      ElMessage.success("删除成功");
    }
  });
};
const submitForm = () => {
  formRef.value.validate((valid) => {
    if (valid) {
      if (form.value.id) {
        // ç¼–辑模式
        const index = mockData.findIndex(item => item.id === form.value.id);
        if (index > -1) {
          mockData[index] = { ...form.value };
        }
        ElMessage.success("修改成功");
      } else {
        // æ–°å¢žæ¨¡å¼
        const newId = Math.max(...mockData.map(item => item.id)) + 1;
        const newNotice = {
          ...form.value,
          id: newId,
          createTime: new Date().toLocaleString()
        };
        mockData.unshift(newNotice);
        ElMessage.success("新增成功");
      }
      dialogVisible.value = false;
    }
  });
};
const resetForm = () => {
  formRef.value?.resetFields();
};
// ç”Ÿå‘½å‘¨æœŸ
onMounted(() => {
  // é¡µé¢åŠ è½½å®Œæˆ
});
</script>
<style scoped>
.search_form {
  background: #fff;
  padding: 20px;
  margin-bottom: 20px;
  border-radius: 8px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  display: flex;
  justify-content: space-between;
  align-items: center;
}
.search_title {
  font-weight: 500;
  color: #333;
  margin-right: 8px;
}
.ml10 {
  margin-left: 10px;
}
.notice-board {
  background: #f5f7fa;
  padding: 20px;
  border-radius: 8px;
}
.notice-section {
  margin-bottom: 30px;
}
.section-header {
  display: flex;
  align-items: center;
  margin-bottom: 20px;
  padding: 0 10px;
}
.section-header h3 {
  margin: 0;
  color: #303133;
  font-size: 18px;
  font-weight: 600;
}
.section-count {
  margin-left: 10px;
  background: #409eff;
  color: white;
  padding: 2px 8px;
  border-radius: 12px;
  font-size: 12px;
}
.notice-cards {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
  gap: 20px;
}
.notice-card {
  background: white;
  border-radius: 12px;
  padding: 20px;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
  transition: all 0.3s ease;
  border-left: 4px solid transparent;
}
.notice-card:hover {
  transform: translateY(-2px);
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
}
.holiday-card {
  border-left-color: #67c23a;
}
.maintenance-card {
  border-left-color: #e6a23c;
}
.urgent {
  border-left-color: #f56c6c;
  background: linear-gradient(135deg, #fff5f5 0%, #ffffff 100%);
}
.card-header {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  margin-bottom: 15px;
}
.card-title {
  display: flex;
  align-items: center;
  font-size: 16px;
  font-weight: 600;
  color: #303133;
  flex: 1;
}
.holiday-icon {
  color: #67c23a;
  margin-right: 8px;
  font-size: 18px;
}
.maintenance-icon {
  color: #e6a23c;
  margin-right: 8px;
  font-size: 18px;
}
.card-actions {
  display: flex;
  gap: 8px;
}
.card-content {
  margin-bottom: 15px;
}
.card-content p {
  margin: 0;
  color: #606266;
  line-height: 1.6;
  font-size: 14px;
}
.card-footer {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 10px;
}
.card-meta {
  display: flex;
  gap: 8px;
}
.priority, .status {
  padding: 2px 8px;
  border-radius: 12px;
  font-size: 12px;
  font-weight: 500;
}
.priority-1 { background: #f0f9ff; color: #0369a1; }
.priority-2 { background: #fef3c7; color: #d97706; }
.priority-3 { background: #fef2f2; color: #dc2626; }
.status-0 { background: #f3f4f6; color: #6b7280; }
.status-1 { background: #d1fae5; color: #059669; }
.status-2 { background: #fef3c7; color: #d97706; }
.card-info {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  font-size: 12px;
  color: #909399;
}
.creator {
  font-weight: 500;
  margin-bottom: 2px;
}
.card-remark {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 8px 12px;
  background: #f8f9fa;
  border-radius: 6px;
  font-size: 12px;
  color: #606266;
  border-left: 3px solid #409eff;
}
.empty-state {
  text-align: center;
  padding: 60px 20px;
}
.dialog-footer {
  text-align: right;
}
/* å“åº”式设计 */
@media (max-width: 768px) {
  .notice-cards {
    grid-template-columns: 1fr;
  }
  .search_form {
    flex-direction: column;
    gap: 15px;
  }
  .search_form > div {
    width: 100%;
  }
}
</style>
src/views/collaborativeApproval/notificationManagement/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,1187 @@
<template>
  <div class="app-container">
    <div class="search_form">
      <div>
        <span class="search_title">通知标题:</span>
        <el-input
          v-model="searchForm.title"
          style="width: 240px"
          placeholder="请输入通知标题搜索"
          @change="handleQuery"
          clearable
          :prefix-icon="Search"
        />
        <span class="search_title ml10">通知类型:</span>
        <el-select v-model="searchForm.type" clearable @change="handleQuery" style="width: 240px">
          <el-option label="放假通知" :value="'holiday'" />
          <el-option label="处罚通知" :value="'penalty'" />
          <el-option label="开会通知" :value="'meeting'" />
          <el-option label="临时通知" :value="'temporary'" />
          <el-option label="正式通知" :value="'formal'" />
        </el-select>
        <el-button type="primary" @click="handleQuery" style="margin-left: 10px">
          æœç´¢
        </el-button>
      </div>
      <div>
        <el-button type="primary" @click="openForm('add')">新增通知</el-button>
        <el-button type="success" @click="openMeetingDialog">在线会议</el-button>
        <el-button type="warning" @click="openFileShareDialog">文件共享</el-button>
        <!-- <el-button type="info" @click="refreshEmployees">刷新员工</el-button> -->
        <el-button type="danger" plain @click="handleDelete">删除</el-button>
      </div>
    </div>
    <div class="table_list">
      <PIMTable
        rowKey="id"
        :column="tableColumn"
        :tableData="tableData"
        :page="page"
        :isSelection="true"
        @selection-change="handleSelectionChange"
        :tableLoading="tableLoading"
        @pagination="pagination"
        :total="page.total"
      ></PIMTable>
    </div>
    <!-- æ–°å¢ž/编辑通知弹窗 -->
    <el-dialog
      v-model="dialogVisible"
      :title="dialogTitle"
      width="800px"
      :close-on-click-modal="false"
    >
      <el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="通知标题" prop="title">
              <el-input v-model="form.title" placeholder="请输入通知标题" />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="通知类型" prop="type">
              <el-select v-model="form.type" placeholder="请选择通知类型" style="width: 100%">
                <el-option label="放假通知" value="holiday" />
                <el-option label="处罚通知" value="penalty" />
                <el-option label="开会通知" value="meeting" />
                <el-option label="临时通知" value="temporary" />
                <el-option label="正式通知" value="formal" />
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="优先级" prop="priority">
              <el-select v-model="form.priority" placeholder="请选择优先级" style="width: 100%">
                <el-option label="普通" value="low" />
                <el-option label="重要" value="medium" />
                <el-option label="紧急" value="high" />
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="有效期至" prop="expireDate">
              <el-date-picker
                v-model="form.expireDate"
                type="date"
                placeholder="请选择有效期"
                style="width: 100%"
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-form-item label="接收部门" prop="departments">
          <el-select
            v-model="form.departments"
            multiple
            placeholder="请选择接收部门"
            style="width: 100%"
          >
            <el-option
              v-for="dept in departments"
              :key="dept"
              :label="dept"
              :value="dept"
            />
          </el-select>
        </el-form-item>
        <el-form-item label="同步方式" prop="syncMethods">
          <el-checkbox-group v-model="form.syncMethods">
            <el-checkbox
              v-for="method in syncMethods"
              :key="method.value"
              :label="method.value"
            >
              {{ method.label }}
            </el-checkbox>
          </el-checkbox-group>
        </el-form-item>
        <el-form-item label="通知内容" prop="content">
          <el-input
            v-model="form.content"
            type="textarea"
            :rows="4"
            placeholder="请输入通知内容"
          />
        </el-form-item>
      </el-form>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="dialogVisible = false">取消</el-button>
          <el-button type="primary" @click="submitForm">确定</el-button>
        </span>
      </template>
    </el-dialog>
    <!-- åœ¨çº¿ä¼šè®®å¼¹çª— -->
    <el-dialog
      v-model="meetingDialogVisible"
      title="创建在线会议"
      width="700px"
      :close-on-click-modal="false"
    >
      <el-form ref="meetingFormRef" :model="meetingForm" :rules="meetingRules" label-width="120px">
        <el-form-item label="会议标题" prop="title">
          <el-input v-model="meetingForm.title" placeholder="请输入会议标题" />
        </el-form-item>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="开始时间" prop="startTime">
              <el-date-picker
                v-model="meetingForm.startTime"
                type="datetime"
                placeholder="请选择开始时间"
                style="width: 100%"
              />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="会议时长" prop="duration">
              <el-input-number
                v-model="meetingForm.duration"
                :min="15"
                :max="480"
                :step="15"
                style="width: 100%"
              />
              <span style="margin-left: 10px">分钟</span>
            </el-form-item>
          </el-col>
        </el-row>
        <el-form-item label="会议平台" prop="platform">
          <el-select v-model="meetingForm.platform" placeholder="请选择会议平台" style="width: 100%">
            <el-option
              v-for="platform in meetingPlatforms"
              :key="platform.value"
              :label="platform.label"
              :value="platform.value"
            />
          </el-select>
        </el-form-item>
        <el-form-item label="参会人员" prop="participants">
          <el-select
            v-model="meetingForm.participants"
            multiple
            filterable
            remote
            :remote-method="filterEmployees"
            :loading="employeesLoading"
            placeholder="请选择参会人员"
            style="width: 100%"
          >
            <el-option-group
              v-for="group in employeeGroups"
              :key="group.label"
              :label="group.label"
            >
                             <el-option
                 v-for="employee in group.options"
                 :key="employee.value"
                 :label="`${employee.label} (${employee.dept})`"
                 :value="employee.value"
               >
                 <div style="display: flex; justify-content: space-between; align-items: center;">
                   <div>
                     <div style="font-weight: 500;">{{ employee.label }}</div>
                     <div style="color: #909399; font-size: 12px;">{{ employee.dept }}</div>
                   </div>
                   <div style="text-align: right; font-size: 12px; color: #909399;">
                     <div v-if="employee.phone">{{ employee.phone }}</div>
                     <div v-if="employee.email">{{ employee.email }}</div>
                   </div>
                 </div>
               </el-option>
            </el-option-group>
          </el-select>
          <div style="margin-top: 8px; color: #909399; font-size: 12px;">
            å·²é€‰æ‹© {{ meetingForm.participants.length }} äºº
          </div>
          <!-- å·²é€‰æ‹©äººå‘˜è¯¦æƒ… -->
          <div v-if="meetingForm.participants.length > 0" style="margin-top: 10px;">
            <el-tag
              v-for="participantId in meetingForm.participants"
              :key="participantId"
              closable
              @close="removeParticipant(participantId)"
              style="margin-right: 8px; margin-bottom: 8px;"
            >
              {{ getEmployeeName(participantId) }}
            </el-tag>
          </div>
        </el-form-item>
        <el-form-item label="会议描述" prop="description">
          <el-input
            v-model="meetingForm.description"
            type="textarea"
            :rows="3"
            placeholder="请输入会议描述"
          />
        </el-form-item>
      </el-form>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="meetingDialogVisible = false">取消</el-button>
          <el-button type="primary" @click="createMeeting">创建会议</el-button>
        </span>
      </template>
    </el-dialog>
    <!-- æ–‡ä»¶å…±äº«å¼¹çª— -->
    <el-dialog
      v-model="fileShareDialogVisible"
      title="文件共享"
      width="700px"
      :close-on-click-modal="false"
    >
      <el-form ref="fileShareFormRef" :model="fileShareForm" :rules="fileShareRules" label-width="120px">
        <el-form-item label="共享标题" prop="title">
          <el-input v-model="fileShareForm.title" placeholder="请输入共享标题" />
        </el-form-item>
        <el-form-item label="共享描述" prop="description">
          <el-input
            v-model="fileShareForm.description"
            type="textarea"
            :rows="3"
            placeholder="请输入共享描述"
          />
        </el-form-item>
        <el-form-item label="接收部门" prop="departments">
          <el-select
            v-model="fileShareForm.departments"
            multiple
            placeholder="请选择接收部门"
            style="width: 100%"
          >
            <el-option
              v-for="dept in departments"
              :key="dept"
              :label="dept"
              :value="dept"
            />
          </el-select>
        </el-form-item>
        <el-form-item label="上传文件" prop="files">
          <el-upload
            ref="uploadRef"
            :auto-upload="false"
            :on-change="handleFileChange"
            :on-remove="removeFile"
            :file-list="fileList"
            multiple
            :limit="10"
            accept=".doc,.docx,.pdf,.xls,.xlsx,.ppt,.pptx,.txt,.jpg,.jpeg,.png,.gif"
          >
            <el-button type="primary">选择文件</el-button>
            <template #tip>
              <div class="el-upload__tip">
                æ”¯æŒä¸Šä¼ æ–‡æ¡£ã€å›¾ç‰‡ç­‰æ ¼å¼ï¼Œå•个文件不超过10MB,最多10个文件
              </div>
            </template>
          </el-upload>
        </el-form-item>
      </el-form>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="fileShareDialogVisible = false">取消</el-button>
          <el-button type="primary" @click="shareFiles">共享文件</el-button>
        </span>
      </template>
    </el-dialog>
  </div>
</template>
<script setup>
import { Search } from "@element-plus/icons-vue";
import { onMounted, ref, reactive, toRefs, computed } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import PIMTable from "@/components/PIMTable/PIMTable.vue";
import { userListNoPageByTenantId } from "@/api/system/user.js";
import { staffOnJobListPage } from "@/api/personnelManagement/employeeRecord.js";
// è¡¨å•验证规则
const rules = {
  title: [
    { required: true, message: "请输入通知标题", trigger: "blur" }
  ],
  type: [
    { required: true, message: "请选择通知类型", trigger: "change" }
  ],
  content: [
    { required: true, message: "请输入通知内容", trigger: "blur" }
  ]
};
const meetingRules = {
  title: [
    { required: true, message: "请输入会议标题", trigger: "blur" }
  ],
  startTime: [
    { required: true, message: "请选择会议开始时间", trigger: "change" }
  ],
  participants: [
    { required: true, message: "请选择参会人员", trigger: "change" }
  ]
};
const fileShareRules = {
  title: [
    { required: true, message: "请输入共享标题", trigger: "blur" }
  ],
  description: [
    { required: true, message: "请输入共享描述", trigger: "blur" }
  ]
};
// å“åº”式数据
const data = reactive({
  searchForm: {
    title: "",
    type: "",
    status: "",
  },
  tableLoading: false,
  page: {
    current: 1,
    size: 100,
    total: 0,
  },
  tableData: [],
  selectedIds: [],
  // æ–°å¢žé€šçŸ¥ç›¸å…³
  form: {
    title: "",
    type: "",
    priority: "medium",
    content: "",
    departments: [],
    expireDate: "",
    syncMethods: []
  },
  dialogVisible: false,
  dialogTitle: "",
  dialogType: "add",
  // åœ¨çº¿ä¼šè®®ç›¸å…³
  meetingDialogVisible: false,
  meetingForm: {
    title: "",
    startTime: "",
    duration: 60,
    participants: [],
    description: "",
    platform: "wechat"
  },
  // æ–‡ä»¶å…±äº«ç›¸å…³
  fileShareDialogVisible: false,
  fileShareForm: {
    title: "",
    description: "",
    departments: [],
    files: []
  },
  fileList: []
});
const {
  searchForm,
  tableLoading,
  page,
  tableData,
  selectedIds,
  form,
  dialogVisible,
  dialogTitle,
  dialogType,
  meetingDialogVisible,
  meetingForm,
  fileShareDialogVisible,
  fileShareForm,
  fileList
} = toRefs(data);
// è¡¨å•引用
const formRef = ref();
const meetingFormRef = ref();
const fileShareFormRef = ref();
// è¡¨æ ¼åˆ—配置
const tableColumn = ref([
  {
    label: "通知标题",
    prop: "title",
    showOverflowTooltip: true,
  },
  {
    label: "通知类型",
    prop: "type",
    dataType: "tag",
    formatData: (params) => {
      const typeMap = {
        holiday: "放假通知",
        penalty: "处罚通知",
        meeting: "开会通知",
        temporary: "临时通知",
        formal: "正式通知"
      };
      return typeMap[params] || params;
    },
    formatType: (params) => {
      const typeMap = {
        holiday: "success",
        penalty: "danger",
        meeting: "warning",
        temporary: "info",
        formal: "primary"
      };
      return typeMap[params] || "info";
    }
  },
  {
    label: "优先级",
    prop: "priority",
    dataType: "tag",
    formatData: (params) => {
      const priorityMap = {
        low: "普通",
        medium: "重要",
        high: "紧急"
      };
      return priorityMap[params] || params;
    },
    formatType: (params) => {
      const typeMap = {
        low: "info",
        medium: "warning",
        high: "danger"
      };
      return typeMap[params] || "info";
    }
  },
  {
    label: "状态",
    prop: "status",
    dataType: "tag",
    formatData: (params) => {
      const statusMap = {
        draft: "草稿",
        published: "已发布",
        expired: "已过期"
      };
      return statusMap[params] || params;
    },
    formatType: (params) => {
      const typeMap = {
        draft: "info",
        published: "success",
        expired: "danger"
      };
      return typeMap[params] || "info";
    }
  },
  {
    label: "接收部门",
    prop: "departments",
    width: 150,
    showOverflowTooltip: true,
    formatData: (params) => {
      if (!params || params.length === 0) return "全部部门";
      return params.join(", ");
    }
  },
  {
    label: "有效期至",
    prop: "expireDate",
    width: 150,
    formatData: (params) => {
      if (!params) return "永久有效";
      return params;
    }
  },
  {
    label: "创建时间",
    prop: "createTime",
    width: 180,
  },
  {
    dataType: "action",
    label: "操作",
    align: "center",
    fixed: "right",
    width: 280,
    operation: [
      {
        name: "编辑",
        type: "text",
        clickFun: (row) => {
          openForm("edit", row);
        }
      },
      {
        name: "发布",
        type: "text",
        clickFun: (row) => {
          publishNotification(row);
        },
        // disabled: (row) => row.status === "published"
      },
      {
        name: "撤回",
        type: "text",
        clickFun: (row) => {
          revokeNotification(row);
        },
        // disabled: (row) => row.status !== "published"
      }
    ]
  }
]);
// æ¨¡æ‹Ÿæ•°æ®
let mockData = [
  {
    id: "1",
    title: "2024年春节放假通知",
    type: "holiday",
    priority: "high",
    status: "published",
    content: "根据国家规定,结合公司实际情况,现将2024年春节放假安排通知如下...",
    departments: ["技术部", "销售部", "人事部", "财务部", "运营部", "市场部", "客服部"],
    expireDate: "2024-02-15",
    syncMethods: ["wechat", "dingtalk", "email"],
    createTime: "2024-01-15 10:30:00"
  },
  {
    id: "2",
    title: "技术部周例会通知",
    type: "meeting",
    priority: "medium",
    status: "published",
    content: "技术部定于每周五下午2点召开周例会,请各位同事准时参加...",
    departments: ["技术部"],
    expireDate: "2024-01-20",
    syncMethods: ["wechat", "dingtalk"],
    createTime: "2024-01-14 15:20:00"
  },
  {
    id: "3",
    title: "员工行为规范处罚通知",
    type: "penalty",
    priority: "high",
    status: "draft",
    content: "为维护公司正常秩序,规范员工行为,现对违反公司规定的行为进行处罚...",
    departments: ["人事部", "技术部", "销售部"],
    expireDate: "2024-02-13",
    syncMethods: ["wechat", "email"],
    createTime: "2024-01-13 09:15:00"
  }
];
// é€šçŸ¥æ ‡é¢˜æ¨¡æ¿
const titleTemplates = [
  "关于{year}å¹´{holiday}放假安排的通知",
  "{dept}部门{meeting}会议通知",
  "员工{behavior}行为规范提醒",
  "{company}重要事项通知",
  "{dept}部门工作安排通知",
  "关于{project}项目进度的通知",
  "{dept}部门人员调整通知",
  "公司{policy}政策更新通知"
];
// é€šçŸ¥ç±»åž‹é…ç½®
const notificationTypes = [
  { type: "holiday", label: "放假通知", priority: "high" },
  { type: "meeting", label: "开会通知", priority: "medium" },
  { type: "penalty", label: "处罚通知", priority: "high" },
  { type: "temporary", label: "临时通知", priority: "low" },
  { type: "formal", label: "正式通知", priority: "medium" }
];
// éƒ¨é—¨åˆ—表
const departments = ["技术部", "销售部", "人事部", "财务部", "运营部", "市场部", "客服部"];
// äººå‘˜åˆ—表
const employees = ref([]);
const employeesLoading = ref(false);
// èŽ·å–åœ¨èŒå‘˜å·¥åˆ—è¡¨
const getEmployeesList = async () => {
  try {
    employeesLoading.value = true;
    // ä¼˜å…ˆä½¿ç”¨ç³»ç»Ÿç”¨æˆ·æŽ¥å£ï¼ˆæŒ‰ç§Ÿæˆ·èŽ·å–ï¼‰
    const userResponse = await userListNoPageByTenantId();
    if (userResponse.data) {
      employees.value = userResponse.data.map(user => ({
        label: user.nickName || user.userName || '未知姓名',
        value: user.userId || user.id,
        dept: user.dept?.deptName || '未知部门',
        phone: user.phonenumber || '',
        email: user.email || '',
        status: user.status || '0'
      })).filter(user => user.status === '0'); // åªæ˜¾ç¤ºæ­£å¸¸çŠ¶æ€çš„ç”¨æˆ·
    } else {
      // å¦‚果系统用户接口失败,使用员工台账接口
      const response = await staffOnJobListPage({
        pageNum: 1,
        pageSize: 1000,
        staffState: 1 // åœ¨èŒçŠ¶æ€
      });
      if (response.data && response.data.records) {
        employees.value = response.data.records.map(employee => ({
          label: employee.staffName || employee.name || '未知姓名',
          value: employee.staffNo || employee.id || employee.staffId,
          dept: employee.deptName || employee.department || '未知部门',
          phone: employee.phone || employee.mobile || '',
          email: employee.email || '',
          status: '0'
        }));
      }
    }
  } catch (error) {
    console.error('获取员工列表失败:', error);
    // å¦‚果接口都失败,使用默认数据
    employees.value = [
      { label: "张三", value: "001", dept: "技术部", phone: "13800138001", email: "zhangsan@company.com", status: "0" },
      { label: "李四", value: "002", dept: "销售部", phone: "13800138002", email: "lisi@company.com", status: "0" },
      { label: "王五", value: "003", dept: "人事部", phone: "13800138003", email: "wangwu@company.com", status: "0" }
    ];
  } finally {
    employeesLoading.value = false;
  }
};
// å‘˜å·¥åˆ†ç»„
const employeeGroups = computed(() => {
  const groups = {};
  employees.value.forEach(employee => {
    const dept = employee.dept || '其他部门';
    if (!groups[dept]) {
      groups[dept] = [];
    }
    groups[dept].push(employee);
  });
  // æŒ‰éƒ¨é—¨åç§°æŽ’序,确保显示顺序一致
  return Object.keys(groups)
    .sort()
    .map(dept => ({
      label: dept,
      options: groups[dept].sort((a, b) => a.label.localeCompare(b.label, 'zh-CN'))
    }));
});
// è¿‡æ»¤å‘˜å·¥ï¼ˆè¿œç¨‹æœç´¢ï¼‰
const filterEmployees = (query) => {
  if (query !== '') {
    const lowerQuery = query.toLowerCase();
    return employees.value.filter(employee =>
      employee.label.toLowerCase().includes(lowerQuery) ||
      employee.dept.toLowerCase().includes(lowerQuery) ||
      (employee.phone && employee.phone.includes(query)) ||
      (employee.email && employee.email.toLowerCase().includes(lowerQuery))
    );
  } else {
    return employees.value;
  }
};
// åˆ·æ–°å‘˜å·¥åˆ—表
const refreshEmployees = async () => {
  ElMessage.info("正在刷新员工列表...");
  await getEmployeesList();
  // ç»Ÿè®¡å„部门人数
  const deptStats = {};
  employees.value.forEach(emp => {
    const dept = emp.dept || '其他部门';
    deptStats[dept] = (deptStats[dept] || 0) + 1;
  });
  const deptInfo = Object.entries(deptStats)
    .map(([dept, count]) => `${dept}: ${count}人`)
    .join(', ');
  ElMessage.success(`员工列表刷新完成,共 ${employees.value.length} äºº (${deptInfo})`);
};
// èŽ·å–å‘˜å·¥å§“å
const getEmployeeName = (employeeId) => {
  const employee = employees.value.find(emp => emp.value === employeeId);
  return employee ? employee.label : '未知人员';
};
// èŽ·å–å‘˜å·¥è¯¦ç»†ä¿¡æ¯
const getEmployeeInfo = (employeeId) => {
  const employee = employees.value.find(emp => emp.value === employeeId);
  if (!employee) return null;
  return {
    name: employee.label,
    dept: employee.dept,
    phone: employee.phone,
    email: employee.email
  };
};
// ç§»é™¤å‚会人员
const removeParticipant = (participantId) => {
  const index = meetingForm.value.participants.indexOf(participantId);
  if (index > -1) {
    meetingForm.value.participants.splice(index, 1);
  }
};
// åŒæ­¥æ–¹å¼é€‰é¡¹
const syncMethods = [
  { label: "企业微信", value: "wechat" },
  { label: "钉钉", value: "dingtalk" },
  { label: "邮件", value: "email" },
  { label: "短信", value: "sms" }
];
// ä¼šè®®å¹³å°é€‰é¡¹
const meetingPlatforms = [
  { label: "企业微信会议", value: "wechat" },
  { label: "钉钉会议", value: "dingtalk" },
  { label: "腾讯会议", value: "tencent" },
  { label: "Zoom", value: "zoom" }
];
// è‡ªåŠ¨ç”Ÿæˆæ–°æ•°æ®
const generateNewData = () => {
  const newId = (mockData.length + 1).toString();
  const now = new Date();
  const randomType = notificationTypes[Math.floor(Math.random() * notificationTypes.length)];
  const randomDept = departments[Math.floor(Math.random() * departments.length)];
  // ç”Ÿæˆéšæœºæ ‡é¢˜
  let title = titleTemplates[Math.floor(Math.random() * titleTemplates.length)];
  title = title
    .replace('{year}', now.getFullYear())
    .replace('{holiday}', ['春节', '国庆', '中秋', '元旦'][Math.floor(Math.random() * 4)])
    .replace('{dept}', randomDept)
    .replace('{meeting}', ['周例会', '月度总结', '项目评审', '培训会议'][Math.floor(Math.random() * 4)])
    .replace('{behavior}', ['考勤', '着装', '工作态度', '团队协作'][Math.floor(Math.random() * 4)])
    .replace('{company}', ['公司', '集团', '总部'][Math.floor(Math.random() * 4)])
    .replace('{project}', ['数字化转型', '产品升级', '市场拓展', '人才培养'][Math.floor(Math.random() * 4)])
    .replace('{policy}', ['考勤', '薪酬', '福利', '晋升'][Math.floor(Math.random() * 4)]);
  // éšæœºçŠ¶æ€
  const statuses = ['draft', 'published'];
  const randomStatus = statuses[Math.floor(Math.random() * statuses.length)];
  // éšæœºä¼˜å…ˆçº§
  const priorities = ['low', 'medium', 'high'];
  const randomPriority = priorities[Math.floor(Math.random() * priorities.length)];
  const newNotification = {
    id: newId,
    title: title,
    type: randomType.type,
    priority: randomPriority,
    status: randomStatus,
    content: `这是${title}的详细内容,请相关人员注意查看...`,
    departments: [randomDept],
    expireDate: new Date(now.getTime() + 30 * 24 * 60 * 60 * 1000).toISOString().split('T')[0], // 30天后过期
    syncMethods: ["wechat", "dingtalk"],
    createTime: now.toLocaleString()
  };
  // æ·»åŠ åˆ°æ•°æ®å¼€å¤´
  mockData.unshift(newNotification);
  // ä¿æŒæ•°æ®é‡åœ¨åˆç†èŒƒå›´å†…(最多保留20条)
  if (mockData.length > 20) {
    mockData = mockData.slice(0, 20);
  }
  console.log(`[${new Date().toLocaleString()}] è‡ªåŠ¨ç”Ÿæˆæ–°é€šçŸ¥: ${title}`);
};
// ç”Ÿå‘½å‘¨æœŸ
onMounted(() => {
  getList();
  getEmployeesList(); // èŽ·å–å‘˜å·¥åˆ—è¡¨
  startAutoRefresh();
});
// å¼€å§‹è‡ªåŠ¨åˆ·æ–°
const startAutoRefresh = () => {
  setInterval(() => {
    generateNewData();
    getList();
  }, 600000); // 10分钟刷新一次 (10 * 60 * 1000 = 600000ms)
};
// æŸ¥è¯¢æ•°æ®
const handleQuery = () => {
  page.value.current = 1;
  getList();
};
const getList = () => {
  tableLoading.value = true;
  setTimeout(() => {
    let filteredData = [...mockData];
    if (searchForm.value.title) {
      filteredData = filteredData.filter(item =>
        item.title.toLowerCase().includes(searchForm.value.title.toLowerCase())
      );
    }
    if (searchForm.value.type) {
      filteredData = filteredData.filter(item => item.type === searchForm.value.type);
    }
    tableData.value = filteredData;
    page.value.total = filteredData.length;
    tableLoading.value = false;
  }, 500);
};
// åˆ†é¡µå¤„理
const pagination = (obj) => {
  page.value.current = obj.page;
  page.value.size = obj.limit;
  handleQuery();
};
// é€‰æ‹©å˜åŒ–处理
const handleSelectionChange = (selection) => {
  selectedIds.value = selection.map(item => item.id);
};
// æ‰“开表单
const openForm = (type, row = null) => {
  dialogType.value = type;
  if (type === "add") {
    dialogTitle.value = "新增通知";
    // é‡ç½®è¡¨å•
    Object.assign(form.value, {
      title: "",
      type: "",
      priority: "medium",
      content: "",
      departments: [],
      expireDate: "",
      syncMethods: []
    });
  } else if (type === "edit" && row) {
    dialogTitle.value = "编辑通知";
    Object.assign(form.value, {
      title: row.title,
      type: row.type,
      priority: row.priority,
      content: row.content || "",
      departments: row.departments || [],
      expireDate: row.expireDate || "",
      syncMethods: row.syncMethods || []
    });
  }
  dialogVisible.value = true;
};
// æ‰“开在线会议弹窗
const openMeetingDialog = () => {
  // é‡ç½®è¡¨å•
  Object.assign(meetingForm.value, {
    title: "",
    startTime: "",
    duration: 60,
    participants: [],
    description: "",
    platform: "wechat"
  });
  meetingDialogVisible.value = true;
};
// æ‰“开文件共享弹窗
const openFileShareDialog = () => {
  // é‡ç½®è¡¨å•
  Object.assign(fileShareForm.value, {
    title: "",
    description: "",
    departments: [],
    files: []
  });
  fileList.value = [];
  fileShareDialogVisible.value = true;
};
// æ‰‹åŠ¨åˆ·æ–°æ•°æ®
const manualRefresh = () => {
  generateNewData();
  getList();
  ElMessage.success("手动刷新完成,已生成新通知");
};
// æäº¤é€šçŸ¥è¡¨å•
const submitForm = async () => {
  try {
    await formRef.value.validate();
    if (dialogType.value === "add") {
      // æ–°å¢žé€šçŸ¥
      const newNotification = {
        id: (mockData.length + 1).toString(),
        title: form.value.title,
        type: form.value.type,
        priority: form.value.priority,
        status: "draft",
        content: form.value.content,
        departments: form.value.departments,
        expireDate: form.value.expireDate,
        syncMethods: form.value.syncMethods,
        createTime: new Date().toLocaleString()
      };
      mockData.unshift(newNotification);
      ElMessage.success("通知创建成功");
    } else {
      // ç¼–辑通知
      const index = mockData.findIndex(item => item.id === selectedIds.value[0]);
      if (index !== -1) {
        Object.assign(mockData[index], {
          title: form.value.title,
          type: form.value.type,
          priority: form.value.priority,
          content: form.value.content,
          departments: form.value.departments,
          expireDate: form.value.expireDate,
          syncMethods: form.value.syncMethods
        });
        ElMessage.success("通知更新成功");
      }
    }
    dialogVisible.value = false;
    getList();
  } catch (error) {
    console.error("表单验证失败:", error);
  }
};
// åˆ›å»ºä¼šè®®
const createMeeting = async () => {
  try {
    await meetingFormRef.value.validate();
    // æ¨¡æ‹Ÿåˆ›å»ºä¼šè®®
    const meetingInfo = {
      title: meetingForm.value.title,
      startTime: meetingForm.value.startTime,
      duration: meetingForm.value.duration,
      participants: meetingForm.value.participants,
      description: meetingForm.value.description,
      platform: meetingForm.value.platform,
      meetingId: `MTG${Date.now()}`
    };
    // æ¨¡æ‹Ÿå‘送到企业微信/钉钉
    const platformName = meetingPlatforms.find(p => p.value === meetingForm.value.platform)?.label || "未知平台";
    ElMessage.success(`会议创建成功!会议ID: ${meetingInfo.meetingId},将通过${platformName}发送通知`);
    meetingDialogVisible.value = false;
         // èŽ·å–å‚ä¼šäººå‘˜ä¿¡æ¯
     const participantNames = meetingForm.value.participants.map(participantId => {
       const employee = employees.value.find(emp => emp.value === participantId);
       return employee ? employee.label : '未知人员';
     }).join('、');
     // èŽ·å–å‚ä¼šäººå‘˜è¯¦ç»†ä¿¡æ¯
     const participantDetails = meetingForm.value.participants.map(participantId => {
       const employee = employees.value.find(emp => emp.value === participantId);
       return employee ? {
         name: employee.label,
         dept: employee.dept,
         phone: employee.phone,
         email: employee.email
       } : null;
     }).filter(Boolean);
    // å°†ä¼šè®®ä¿¡æ¯æ·»åŠ åˆ°é€šçŸ¥åˆ—è¡¨
    const meetingNotification = {
      id: (mockData.length + 1).toString(),
      title: `[会议通知] ${meetingInfo.title}`,
      type: "meeting",
      priority: "high",
      status: "published",
             content: `会议时间: ${meetingInfo.startTime},时长: ${meetingInfo.duration}分钟,平台: ${meetingPlatforms.find(p => p.value === meetingForm.value.platform)?.label || "未知平台"},参会人员: ${participantNames},共${participantDetails.length}人`,
      departments: [],
      expireDate: "",
      syncMethods: [meetingForm.value.platform],
      createTime: new Date().toLocaleString()
    };
    mockData.unshift(meetingNotification);
    getList();
  } catch (error) {
    console.error("会议表单验证失败:", error);
  }
};
// æ–‡ä»¶ä¸Šä¼ å¤„理
const handleFileChange = (file) => {
  const isLt10M = file.size / 1024 / 1024 < 10;
  if (!isLt10M) {
    ElMessage.error("上传文件大小不能超过 10MB!");
    return false;
  }
  const fileInfo = {
    name: file.name,
    size: file.size,
    type: file.type,
    uid: file.uid
  };
  fileList.value.push(fileInfo);
  fileShareForm.value.files.push(fileInfo);
  return false; // é˜»æ­¢è‡ªåŠ¨ä¸Šä¼ 
};
// ç§»é™¤æ–‡ä»¶
const removeFile = (file) => {
  const index = fileList.value.findIndex(item => item.uid === file.uid);
  if (index !== -1) {
    const index2 = fileShareForm.value.files.findIndex(item => item.uid === file.uid);
    if (index2 !== -1) {
      fileShareForm.value.files.splice(index2, 1);
    }
    fileList.value.splice(index, 1);
  }
};
// å…±äº«æ–‡ä»¶
const shareFiles = async () => {
  try {
    await fileShareFormRef.value.validate();
    if (fileShareForm.value.files.length === 0) {
      ElMessage.warning("请至少选择一个文件");
      return;
    }
    // æ¨¡æ‹Ÿæ–‡ä»¶å…±äº«
    const shareInfo = {
      title: fileShareForm.value.title,
      description: fileShareForm.value.description,
      departments: fileShareForm.value.departments,
      files: fileShareForm.value.files,
      shareId: `FILE${Date.now()}`
    };
    ElMessage.success(`文件共享成功!共享ID: ${shareInfo.shareId},已通知相关部门`);
    fileShareDialogVisible.value = false;
    // å°†æ–‡ä»¶å…±äº«ä¿¡æ¯æ·»åŠ åˆ°é€šçŸ¥åˆ—è¡¨
    const fileShareNotification = {
      id: (mockData.length + 1).toString(),
      title: `[文件共享] ${shareInfo.title}`,
      type: "temporary",
      priority: "medium",
      status: "published",
      content: `共享描述: ${shareInfo.description},文件数量: ${shareInfo.files.length}个`,
      departments: shareInfo.departments,
      expireDate: "",
      syncMethods: ["wechat", "dingtalk"],
      createTime: new Date().toLocaleString()
    };
    mockData.unshift(fileShareNotification);
    getList();
  } catch (error) {
    console.error("文件共享表单验证失败:", error);
  }
};
// å‘布通知
const publishNotification = (row) => {
  row.status = "published";
  ElMessage.success("通知发布成功");
  getList();
};
// æ’¤å›žé€šçŸ¥
const revokeNotification = (row) => {
  row.status = "draft";
  ElMessage.success("通知已撤回");
  getList();
};
// åˆ é™¤é€šçŸ¥
const handleDelete = () => {
  if (selectedIds.value.length === 0) {
    ElMessage.warning("请选择要删除的通知");
    return;
  }
  ElMessageBox.confirm("选中的内容将被删除,是否确认删除?", "删除", {
    confirmButtonText: "确认",
    cancelButtonText: "取消",
    type: "warning",
  }).then(() => {
    ElMessage.success("删除成功");
    selectedIds.value = [];
    getList();
  }).catch(() => {
    // ç”¨æˆ·å–消
  });
};
</script>
<style scoped>
.auto-refresh-info {
  margin-bottom: 15px;
}
.auto-refresh-info .el-alert {
  border-radius: 8px;
}
.dialog-footer {
  text-align: right;
}
.el-upload__tip {
  color: #909399;
  font-size: 12px;
  margin-top: 8px;
}
.el-checkbox-group {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
}
.el-checkbox {
  margin-right: 0;
}
</style>
src/views/collaborativeApproval/rpaManagement/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,400 @@
<template>
  <div class="app-container">
    <div class="search_form">
      <div>
        <span class="search_title">程序名:</span>
        <el-input
          v-model="searchForm.programName"
          style="width: 240px"
          placeholder="请输入程序名搜索"
          @change="handleQuery"
          clearable
          :prefix-icon="Search"
        />
        <span class="search_title ml10">执行状态:</span>
        <el-select v-model="searchForm.status" clearable @change="handleQuery" style="width: 240px">
          <el-option label="运行中" :value="'running'" />
          <el-option label="已停止" :value="'stopped'" />
          <el-option label="异常" :value="'error'" />
        </el-select>
        <el-button type="primary" @click="handleQuery" style="margin-left: 10px">
          æœç´¢
        </el-button>
      </div>
      <div>
        <el-button type="primary" @click="openForm('add')">新增</el-button>
        <el-button type="danger" plain @click="handleDelete">删除</el-button>
      </div>
    </div>
    <div class="table_list">
      <PIMTable
        rowKey="id"
        :column="tableColumn"
        :tableData="tableData"
        :page="page"
        :isSelection="true"
        @selection-change="handleSelectionChange"
        :tableLoading="tableLoading"
        @pagination="pagination"
        :total="page.total"
      ></PIMTable>
    </div>
    <!-- RPA表单弹窗 -->
    <el-dialog
      v-model="dialogVisible"
      :title="dialogTitle"
      width="500px"
      :close-on-click-modal="false"
    >
      <el-form
        ref="formRef"
        :model="form"
        :rules="rules"
        label-width="100px"
      >
        <el-form-item label="程序名" prop="programName">
          <el-input
            v-model="form.programName"
            placeholder="请输入程序名"
            clearable
          />
        </el-form-item>
        <el-form-item label="执行状态" prop="status">
          <el-select v-model="form.status" placeholder="请选择执行状态" style="width: 100%">
            <el-option label="运行中" value="running" />
            <el-option label="已停止" value="stopped" />
            <el-option label="异常" value="error" />
          </el-select>
        </el-form-item>
        <el-form-item label="描述" prop="description">
          <el-input
            v-model="form.description"
            type="textarea"
            :rows="3"
            placeholder="请输入RPA程序描述"
            clearable
          />
        </el-form-item>
      </el-form>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="dialogVisible = false">取消</el-button>
          <el-button type="primary" @click="submitForm">确定</el-button>
        </span>
      </template>
    </el-dialog>
  </div>
</template>
<script setup>
import { Search } from "@element-plus/icons-vue";
import { onMounted, ref, reactive, toRefs } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import PIMTable from "@/components/PIMTable/PIMTable.vue";
// å“åº”式数据
const data = reactive({
  searchForm: {
    programName: "",
    status: "",
  },
  form: {
    id: "",
    programName: "",
    status: "stopped",
    description: "",
    createTime: "",
  },
  dialogVisible: false,
  dialogTitle: "",
  dialogType: "add",
  selectedIds: [],
  tableLoading: false,
  page: {
    current: 1,
    size: 100,
    total: 0,
  },
  tableData: [],
});
const { searchForm, form, dialogVisible, dialogTitle, dialogType, selectedIds, tableLoading, page, tableData } = toRefs(data);
// è¡¨å•引用
const formRef = ref();
// è¡¨å•验证规则
const rules = {
  programName: [
    { required: true, message: "请输入程序名", trigger: "blur" }
  ],
  status: [
    { required: true, message: "请选择执行状态", trigger: "change" }
  ]
};
// è¡¨æ ¼åˆ—配置
const tableColumn = ref([
  {
    label: "程序名",
    prop: "programName",
    // width: 200,
  },
  {
    label: "执行状态",
    prop: "status",
    dataType: "tag",
    // width: 120,
    formatData: (params) => {
      const statusMap = {
        running: "运行中",
        stopped: "已停止",
        error: "异常"
      };
      return statusMap[params] || params;
    },
    formatType: (params) => {
      const typeMap = {
        running: "success",
        stopped: "info",
        error: "danger"
      };
      return typeMap[params] || "info";
    }
  },
  {
    label: "描述",
    prop: "description",
    // width: 300,
    showOverflowTooltip: true,
  },
  {
    label: "创建时间",
    prop: "createTime",
    // width: 180,
  },
  {
    dataType: "action",
    label: "操作",
    align: "center",
    fixed: "right",
    width: 230,
    operation: [
      {
        name: "编辑",
        type: "text",
        clickFun: (row) => {
          openForm("edit", row);
        }
      },
      {
        name: "开始",
        type: "text",
        clickFun: (row) => {
          handleStart(row);
        },
        disabled: (row) => row.status !== 'stopped'
      },
      {
        name: "停止",
        type: "text",
        clickFun: (row) => {
          handleStop(row);
        },
        disabled: (row) => row.status === 'stopped'
      }
    ]
  }
]);
// æ¨¡æ‹Ÿæ•°æ®
const mockData = [
  {
    id: "1",
    programName: "订单处理RPA",
    status: "running",
    description: "自动处理客户订单,包括验证、分配和确认",
    createTime: "2024-01-15 10:30:00"
  },
  {
    id: "2",
    programName: "库存同步RPA",
    status: "stopped",
    description: "同步多个仓库的库存数据,确保数据一致性",
    createTime: "2024-01-14 15:20:00"
  },
  {
    id: "3",
    programName: "报表生成RPA",
    status: "error",
    description: "自动生成每日销售报表和库存报表",
    createTime: "2024-01-13 09:15:00"
  }
];
// ç”Ÿå‘½å‘¨æœŸ
onMounted(() => {
  getList();
});
// æŸ¥è¯¢æ•°æ®
const handleQuery = () => {
  page.value.current = 1;
  getList();
};
const getList = () => {
  tableLoading.value = true;
  // æ¨¡æ‹ŸAPI调用延迟
  setTimeout(() => {
    let filteredData = [...mockData];
    // æ ¹æ®æœç´¢æ¡ä»¶è¿‡æ»¤æ•°æ®
    if (searchForm.value.programName) {
      filteredData = filteredData.filter(item =>
        item.programName.toLowerCase().includes(searchForm.value.programName.toLowerCase())
      );
    }
    if (searchForm.value.status) {
      filteredData = filteredData.filter(item => item.status === searchForm.value.status);
    }
    tableData.value = filteredData;
    page.value.total = filteredData.length;
    tableLoading.value = false;
  }, 500);
};
// åˆ†é¡µå¤„理
const pagination = (obj) => {
  page.value.current = obj.page;
  page.value.size = obj.limit;
  handleQuery();
};
// é€‰æ‹©å˜åŒ–处理
const handleSelectionChange = (selection) => {
  selectedIds.value = selection.map(item => item.id);
};
// æ‰“开表单
const openForm = (type, row) => {
  dialogType.value = type;
  dialogVisible.value = true;
  if (type === "add") {
    dialogTitle.value = "添加RPA";
    form.value = {
      id: "",
      programName: "",
      status: "stopped",
      description: "",
      createTime: "",
    };
  } else {
    dialogTitle.value = "编辑RPA";
    form.value = { ...row };
  }
};
// æäº¤è¡¨å•
const submitForm = async () => {
  if (!formRef.value) return;
  try {
    await formRef.value.validate();
    if (dialogType.value === "add") {
      // æ·»åŠ æ–°RPA
      const newRPA = {
        id: Date.now().toString(),
        programName: form.value.programName,
        status: form.value.status,
        description: form.value.description,
        createTime: new Date().toLocaleString(),
      };
      mockData.unshift(newRPA);
      ElMessage.success("RPA添加成功");
    } else {
      // ç¼–辑RPA
      const index = mockData.findIndex(item => item.id === form.value.id);
      if (index !== -1) {
        mockData[index] = { ...form.value };
        ElMessage.success("RPA更新成功");
      }
    }
    dialogVisible.value = false;
    getList();
  } catch (error) {
    console.error("表单验证失败:", error);
  }
};
// å¼€å§‹RPA
const handleStart = (row) => {
  ElMessageBox.confirm(`确定要启动RPA程序"${row.programName}"吗?`, "提示", {
    confirmButtonText: "确认",
    cancelButtonText: "取消",
    type: "warning",
  }).then(() => {
    row.status = "running";
    ElMessage.success("RPA启动成功");
    getList();
  }).catch(() => {
    // ç”¨æˆ·å–消
  });
};
// åœæ­¢RPA
const handleStop = (row) => {
  ElMessageBox.confirm(`确定要停止RPA程序"${row.programName}"吗?`, "提示", {
    confirmButtonText: "确认",
    cancelButtonText: "取消",
    type: "warning",
  }).then(() => {
    row.status = "stopped";
    ElMessage.success("RPA停止成功");
    getList();
  }).catch(() => {
    // ç”¨æˆ·å–消
  });
};
// åˆ é™¤RPA
const handleDelete = () => {
  let ids = [];
  if (selectedIds.value.length > 0) {
    ids = selectedIds.value.map((item) => item.id);
  } else {
    ElMessage.warning("请选择要删除的RPA");
    return;
  }
  ElMessageBox.confirm("选中的内容将被删除,是否确认删除?", "删除", {
    confirmButtonText: "确认",
    cancelButtonText: "取消",
    type: "warning",
  }).then(() => {
    // ä»Žæ¨¡æ‹Ÿæ•°æ®ä¸­åˆ é™¤é€‰ä¸­çš„项
    ids.forEach(id => {
      const index = mockData.findIndex(item => item.id === id);
      if (index !== -1) {
        mockData.splice(index, 1);
      }
    });
    ElMessage.success("删除成功");
    selectedIds.value = [];
    getList();
  }).catch(() => {
    // ç”¨æˆ·å–消
  });
};
</script>
<style scoped></style>
src/views/collaborativeApproval/warningSystem/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,307 @@
<template>
  <div class="warning-system">
    <h2>预警联动机制</h2>
    <!-- ç»Ÿè®¡å¡ç‰‡ -->
    <div class="stats">
      <div class="stat-card red">
        <span class="number">2</span>
        <span class="label">红色预警</span>
      </div>
      <div class="stat-card orange">
        <span class="number">1</span>
        <span class="label">橙色预警</span>
      </div>
      <div class="stat-card yellow">
        <span class="number">1</span>
        <span class="label">黄色预警</span>
      </div>
      <div class="stat-card green">
        <span class="number">1</span>
        <span class="label">绿色预警</span>
      </div>
    </div>
    <!-- é¢„警列表 -->
    <div class="warning-list">
      <h3>预警列表</h3>
      <table>
        <thead>
          <tr>
            <th>编号</th>
            <th>标题</th>
            <th>类型</th>
            <th>等级</th>
            <th>状态</th>
            <th>责任人</th>
            <th>操作</th>
          </tr>
        </thead>
        <tbody>
          <tr v-for="warning in warnings" :key="warning.id">
            <td>{{ warning.id }}</td>
            <td>{{ warning.title }}</td>
            <td>{{ warning.type }}</td>
            <td>
              <span :class="['level-tag', warning.level]">
                {{ warning.levelText }}
              </span>
            </td>
            <td>
              <span :class="['status-tag', warning.status]">
                {{ warning.statusText }}
              </span>
            </td>
            <td>{{ warning.responsible }}</td>
            <td>
              <button @click="viewDetail(warning)">查看详情</button>
            </td>
          </tr>
        </tbody>
      </table>
    </div>
    <!-- è¯¦æƒ…对话框 -->
    <div v-if="showDetail" class="modal">
      <div class="modal-content">
        <h3>预警详情</h3>
        <div v-if="currentWarning">
          <p><strong>编号:</strong>{{ currentWarning.id }}</p>
          <p><strong>标题:</strong>{{ currentWarning.title }}</p>
          <p><strong>类型:</strong>{{ currentWarning.type }}</p>
          <p><strong>等级:</strong>{{ currentWarning.levelText }}</p>
          <p><strong>描述:</strong>{{ currentWarning.description }}</p>
          <p><strong>影响:</strong>{{ currentWarning.impact }}</p>
          <p><strong>建议:</strong>{{ currentWarning.suggestions }}</p>
        </div>
        <button @click="showDetail = false">关闭</button>
      </div>
    </div>
  </div>
</template>
<script>
export default {
  name: 'WarningSystem',
  data() {
    return {
      showDetail: false,
      currentWarning: null,
      warnings: [
        {
          id: 'W001',
          title: '项目预算超支预警',
          type: '财务预警',
          level: 'red',
          levelText: '红色预警',
          status: 'pending',
          statusText: '待处理',
          responsible: '张经理',
          description: 'A项目预算执行率已达95%,预计将超出预算范围。',
          impact: '影响项目整体财务指标,可能导致项目亏损',
          suggestions: '暂停非必要支出,优化资源配置,申请预算调整'
        },
        {
          id: 'W002',
          title: '合同到期预警',
          type: '合规预警',
          level: 'orange',
          levelText: '橙色预警',
          status: 'processing',
          statusText: '处理中',
          responsible: '李主管',
          description: '与供应商B的合同将于2024å¹´1月25日到期。',
          impact: '影响供应链稳定性,可能导致服务中断',
          suggestions: '评估供应商表现,准备续签材料,制定备选方案'
        },
        {
          id: 'W003',
          title: '设备维护预警',
          type: '运营预警',
          level: 'yellow',
          levelText: '黄色预警',
          status: 'pending',
          statusText: '待处理',
          responsible: '王工程师',
          description: '生产线设备C已运行8000小时,接近维护周期。',
          impact: '可能影响生产效率和产品质量',
          suggestions: '安排维护时间,准备备件,制定维护计划'
        },
        {
          id: 'W004',
          title: '人员配置预警',
          type: '运营预警',
          level: 'green',
          levelText: '绿色预警',
          status: 'resolved',
          statusText: '已解决',
          responsible: 'èµµHR',
          description: '技术部门人员配置充足,项目进度正常。',
          impact: '无负面影响',
          suggestions: '继续监控人员配置情况'
        },
        {
          id: 'W005',
          title: '质量事故预警',
          type: '运营预警',
          level: 'red',
          levelText: '红色预警',
          status: 'pending',
          statusText: '待处理',
          responsible: '陈总监',
          description: '产品D在客户现场出现质量问题。',
          impact: '影响客户满意度,可能造成经济损失',
          suggestions: '立即召回问题产品,分析原因,制定改进措施'
        }
      ]
    }
  },
  methods: {
    viewDetail(warning) {
      this.currentWarning = warning
      this.showDetail = true
    }
  }
}
</script>
<style scoped>
.warning-system {
  padding: 20px;
  max-width: 1200px;
  margin: 0 auto;
}
h2 {
  color: #333;
  margin-bottom: 30px;
}
.stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 20px;
  margin-bottom: 30px;
}
.stat-card {
  padding: 20px;
  border-radius: 8px;
  color: white;
  text-align: center;
  box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.stat-card.red { background: linear-gradient(135deg, #ff6b6b, #ee5a52); }
.stat-card.orange { background: linear-gradient(135deg, #ffa726, #ff9800); }
.stat-card.yellow { background: linear-gradient(135deg, #ffd54f, #ffc107); }
.stat-card.green { background: linear-gradient(135deg, #66bb6a, #4caf50); }
.stat-card .number {
  display: block;
  font-size: 32px;
  font-weight: bold;
  margin-bottom: 8px;
}
.stat-card .label {
  font-size: 14px;
  opacity: 0.9;
}
.warning-list h3 {
  margin-bottom: 20px;
  color: #333;
}
table {
  width: 100%;
  border-collapse: collapse;
  background: white;
  border-radius: 8px;
  overflow: hidden;
  box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
th, td {
  padding: 12px;
  text-align: left;
  border-bottom: 1px solid #eee;
}
th {
  background: #f8f9fa;
  font-weight: 600;
  color: #333;
}
.level-tag, .status-tag {
  padding: 4px 8px;
  border-radius: 4px;
  font-size: 12px;
  color: white;
}
.level-tag.red { background: #f56c6c; }
.level-tag.orange { background: #e6a23c; }
.level-tag.yellow { background: #e6a23c; }
.level-tag.green { background: #67c23a; }
.status-tag.pending { background: #f56c6c; }
.status-tag.processing { background: #e6a23c; }
.status-tag.resolved { background: #67c23a; }
button {
  padding: 6px 12px;
  margin: 0 4px;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 12px;
  background: #409eff;
  color: white;
}
.modal {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: rgba(0,0,0,0.5);
  display: flex;
  align-items: center;
  justify-content: center;
}
.modal-content {
  background: white;
  padding: 30px;
  border-radius: 8px;
  max-width: 600px;
  width: 90%;
  max-height: 80vh;
  overflow-y: auto;
}
.modal-content h3 {
  margin-bottom: 20px;
  color: #333;
}
.modal-content p {
  margin-bottom: 15px;
  line-height: 1.6;
}
.modal-content strong {
  color: #333;
}
.modal-content button {
  background: #409eff;
  color: white;
  padding: 10px 20px;
  font-size: 14px;
}
</style>
src/views/demo/fakePage/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,248 @@
<template>
  <div class="app-container">
    <el-card shadow="never">
      <div class="toolbar">
        <el-input
          v-model="query.keyword"
          placeholder="搜索名称/类别"
          clearable
          style="width: 240px"
          @keyup.enter="handleSearch"
        />
        <el-select
          v-model="query.status"
          placeholder="状态"
          clearable
          style="width: 140px; margin-left: 12px"
        >
          <el-option label="启用" value="启用" />
          <el-option label="停用" value="停用" />
        </el-select>
        <el-button type="primary" style="margin-left: 12px" @click="handleSearch">查询</el-button>
        <el-button @click="resetQuery">重置</el-button>
        <el-button type="success" plain style="float: right" @click="openCreate">新增</el-button>
      </div>
      <el-table :data="pagedList" border style="width: 100%" height="480" stripe>
        <el-table-column prop="id" label="编号" width="90" sortable />
        <el-table-column prop="name" label="名称" min-width="140" />
        <el-table-column prop="category" label="类别" width="120" />
        <el-table-column prop="stock" label="库存" width="100" sortable />
        <el-table-column prop="price" label="单价(Â¥)" width="120">
          <template #default="scope">{{ formatPrice(scope.row.price) }}</template>
        </el-table-column>
        <el-table-column label="状态" width="120">
          <template #default="scope">
            <el-tag :type="scope.row.status === '启用' ? 'success' : 'info'">{{ scope.row.status }}</el-tag>
          </template>
        </el-table-column>
        <el-table-column prop="updatedAt" label="更新时间" min-width="160" />
        <el-table-column label="操作" width="180" fixed="right">
          <template #default="scope">
            <el-button link type="primary" @click="openEdit(scope.row)">编辑</el-button>
            <el-button link type="danger" @click="handleDelete(scope.row)">删除</el-button>
          </template>
        </el-table-column>
      </el-table>
      <div class="pagination">
        <el-pagination
          background
          layout="total, sizes, prev, pager, next, jumper"
          :total="filteredList.length"
          :page-sizes="[5, 10, 20, 50]"
          :page-size="pager.pageSize"
          :current-page="pager.pageNum"
          @size-change="handleSizeChange"
          @current-change="handleCurrentChange"
        />
      </div>
    </el-card>
    <el-dialog v-model="dialogVisible" :title="isEdit ? '编辑' : '新增'" width="520px">
      <el-form :model="form" :rules="rules" ref="formRef" label-width="90px">
        <el-form-item label="名称" prop="name">
          <el-input v-model="form.name" placeholder="请输入名称" />
        </el-form-item>
        <el-form-item label="类别" prop="category">
          <el-select v-model="form.category" placeholder="请选择类别" style="width: 100%">
            <el-option label="原料" value="原料" />
            <el-option label="半成品" value="半成品" />
            <el-option label="成品" value="成品" />
          </el-select>
        </el-form-item>
        <el-form-item label="库存" prop="stock">
          <el-input v-model.number="form.stock" type="number" min="0" />
        </el-form-item>
        <el-form-item label="单价(Â¥)" prop="price">
          <el-input v-model.number="form.price" type="number" min="0" step="0.01" />
        </el-form-item>
        <el-form-item label="状态" prop="status">
          <el-radio-group v-model="form.status">
            <el-radio label="启用">启用</el-radio>
            <el-radio label="停用">停用</el-radio>
          </el-radio-group>
        </el-form-item>
      </el-form>
      <template #footer>
        <el-button @click="dialogVisible = false">取 æ¶ˆ</el-button>
        <el-button type="primary" @click="submitForm">ç¡® å®š</el-button>
      </template>
    </el-dialog>
  </div>
</template>
<script setup>
import { ref, reactive, computed, nextTick } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
defineOptions({ name: 'FakePage' })
const query = reactive({
  keyword: '',
  status: ''
})
const pager = reactive({
  pageNum: 1,
  pageSize: 10
})
const allList = ref(generateMockData())
const filteredList = computed(() => {
  const keyword = (query.keyword || '').trim()
  const status = query.status
  return allList.value.filter(item => {
    const hitKeyword = !keyword || item.name.includes(keyword) || item.category.includes(keyword)
    const hitStatus = !status || item.status === status
    return hitKeyword && hitStatus
  })
})
const pagedList = computed(() => {
  const start = (pager.pageNum - 1) * pager.pageSize
  const end = start + pager.pageSize
  return filteredList.value.slice(start, end)
})
function handleSearch() {
  pager.pageNum = 1
}
function resetQuery() {
  query.keyword = ''
  query.status = ''
  pager.pageNum = 1
}
function handleSizeChange(size) {
  pager.pageSize = size
  pager.pageNum = 1
}
function handleCurrentChange(page) {
  pager.pageNum = page
}
function formatPrice(val) {
  return Number(val || 0).toFixed(2)
}
// æ–°å¢ž/编辑
const dialogVisible = ref(false)
const isEdit = ref(false)
const formRef = ref()
const form = reactive({ id: null, name: '', category: '', stock: 0, price: 0, status: '启用' })
const rules = {
  name: [{ required: true, message: '请输入名称', trigger: 'blur' }],
  category: [{ required: true, message: '请选择类别', trigger: 'change' }],
  stock: [{ required: true, message: '请输入库存', trigger: 'blur' }],
  price: [{ required: true, message: '请输入单价', trigger: 'blur' }]
}
function openCreate() {
  isEdit.value = false
  Object.assign(form, { id: null, name: '', category: '', stock: 0, price: 0, status: '启用' })
  dialogVisible.value = true
  nextTick(() => formRef.value?.clearValidate?.())
}
function openEdit(row) {
  isEdit.value = true
  Object.assign(form, JSON.parse(JSON.stringify(row)))
  dialogVisible.value = true
  nextTick(() => formRef.value?.clearValidate?.())
}
function submitForm() {
  formRef.value?.validate?.((valid) => {
    if (!valid) return
    if (isEdit.value) {
      const index = allList.value.findIndex(x => x.id === form.id)
      if (index > -1) {
        allList.value[index] = { ...form, updatedAt: nowString() }
        ElMessage.success('已保存')
      }
    } else {
      const newId = Date.now()
      allList.value.unshift({ ...form, id: newId, updatedAt: nowString() })
      ElMessage.success('已新增')
    }
    dialogVisible.value = false
  })
}
function handleDelete(row) {
  ElMessageBox.confirm(`确认删除【${row.name}】吗?`, '提示', { type: 'warning' })
    .then(() => {
      allList.value = allList.value.filter(x => x.id !== row.id)
      ElMessage.success('已删除')
    })
    .catch(() => {})
}
function generateMockData() {
  const categories = ['原料', '半成品', '成品']
  const statusOptions = ['启用', '停用']
  const list = []
  for (let i = 1; i <= 36; i++) {
    list.push({
      id: i,
      name: `物料-${i.toString().padStart(3, '0')}`,
      category: categories[i % categories.length],
      stock: Math.floor(Math.random() * 1000),
      price: (Math.random() * 500 + 10).toFixed(2),
      status: statusOptions[i % 2],
      updatedAt: nowString()
    })
  }
  return list
}
function nowString() {
  const d = new Date()
  const yyyy = d.getFullYear()
  const MM = String(d.getMonth() + 1).padStart(2, '0')
  const dd = String(d.getDate()).padStart(2, '0')
  const hh = String(d.getHours()).padStart(2, '0')
  const mm = String(d.getMinutes()).padStart(2, '0')
  const ss = String(d.getSeconds()).padStart(2, '0')
  return `${yyyy}-${MM}-${dd} ${hh}:${mm}:${ss}`
}
</script>
<style scoped>
.toolbar {
  margin-bottom: 12px;
}
.pagination {
  margin-top: 12px;
  text-align: right;
}
</style>
src/views/energyManagement/dynamicEnergySaving/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,659 @@
<template>
  <div class="app-container">
    <!-- è¾¹ç¼˜è®¡ç®—状态监控 -->
    <el-row :gutter="20" class="status-section">
      <el-col :span="8">
        <el-card class="status-card">
          <div class="status-item">
            <div class="status-icon">
              <el-icon><Monitor /></el-icon>
            </div>
            <div class="status-info">
              <div class="status-title">边缘服务器状态</div>
              <div class="status-value" :class="edgeServerStatus.status">
                {{ edgeServerStatus.status === 'online' ? '在线' : '离线' }}
              </div>
              <div class="status-detail">最后心跳: {{ formatTime(edgeServerStatus.lastHeartbeat) }}</div>
            </div>
          </div>
        </el-card>
      </el-col>
      <el-col :span="8">
        <el-card class="status-card">
          <div class="status-item">
            <div class="status-icon">
              <el-icon><Cpu /></el-icon>
            </div>
            <div class="status-info">
              <div class="status-title">模型运行状态</div>
              <div class="status-value" :class="modelStatus.status">
                {{ modelStatus.status === 'running' ? '运行中' : '已停止' }}
              </div>
              <div class="status-detail">运行模型: {{ modelStatus.modelCount }}个</div>
            </div>
          </div>
        </el-card>
      </el-col>
      <el-col :span="8">
        <el-card class="status-card">
          <div class="status-item">
            <div class="status-icon">
              <el-icon><TrendCharts /></el-icon>
            </div>
            <div class="status-info">
              <div class="status-title">节能效果</div>
              <div class="status-value success">{{ energySavingRate.toFixed(1) }}%</div>
              <div class="status-detail">累计节能: {{ totalEnergySaved.toFixed(1) }}kWh</div>
            </div>
          </div>
        </el-card>
      </el-col>
    </el-row>
    <!-- æ³¨æ°´æ³µé¢‘率优化控制 -->
    <el-card class="control-section">
      <template #header>
        <span>注水泵频率优化控制</span>
      </template>
      <el-row :gutter="20">
        <el-col :span="12">
          <div class="pump-control">
            <h4>实时参数监控</h4>
            <el-form label-width="120px">
              <el-form-item label="地层压力 (MPa)">
                <el-input v-model="pumpData.formationPressure" readonly>
                  <template #append>MPa</template>
                </el-input>
              </el-form-item>
              <el-form-item label="当前泵速 (Hz)">
                <el-input v-model="pumpData.currentFrequency" readonly>
                  <template #append>Hz</template>
                </el-input>
              </el-form-item>
              <el-form-item label="优化后泵速 (Hz)">
                <el-input v-model="pumpData.optimizedFrequency" readonly>
                  <template #append>Hz</template>
                </el-input>
              </el-form-item>
              <el-form-item label="能耗降低">
                <el-progress
                  :percentage="pumpData.energyReduction"
                  :color="getProgressColor"
                  :format="format => `${format}%`"
                />
              </el-form-item>
              <el-form-item label="流量 (m³/h)">
                <el-input v-model="pumpData.flowRate" readonly>
                  <template #append>m³/h</template>
                </el-input>
              </el-form-item>
              <el-form-item label="功率 (kW)">
                <el-input v-model="pumpData.power" readonly>
                  <template #append>kW</template>
                </el-input>
              </el-form-item>
            </el-form>
          </div>
        </el-col>
        <el-col :span="12">
          <div class="pump-chart">
            <h4>频率优化趋势</h4>
            <div ref="frequencyChart" style="height: 300px;"></div>
          </div>
        </el-col>
      </el-row>
      <el-row :gutter="20" class="control-buttons">
        <el-col :span="24">
          <el-button
            type="primary"
            :disabled="!canControl"
            @click="applyOptimization"
          >
            åº”用优化设置
          </el-button>
          <el-button
            type="warning"
            :disabled="!canControl"
            @click="emergencyStop"
          >
            ç´§æ€¥åœæ­¢
          </el-button>
          <el-button
            type="info"
            @click="showOptimizationHistory"
          >
            ä¼˜åŒ–历史
          </el-button>
          <el-button
            type="success"
            @click="toggleAutoRefresh"
          >
            {{ autoRefreshStatus ? '停止自动刷新' : '开启自动刷新' }}
          </el-button>
        </el-col>
      </el-row>
    </el-card>
    <!-- è¾¹ç¼˜è®¡ç®—模型配置 -->
    <el-card class="model-section">
      <template #header>
        <span>边缘计算模型配置</span>
      </template>
      <el-table :data="modelConfigs" style="width: 100%" stripe>
        <el-table-column prop="modelName" label="模型名称" />
        <el-table-column prop="version" label="版本" />
        <el-table-column prop="status" label="状态">
          <template #default="scope">
            <el-tag :type="scope.row.status === 'active' ? 'success' : 'info'">
              {{ scope.row.status === 'active' ? '激活' : '待机' }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column prop="accuracy" label="准确率" />
        <el-table-column prop="lastUpdate" label="最后更新" />
        <el-table-column label="操作">
          <template #default="scope">
            <el-button
              size="small"
              @click="updateModel(scope.row)"
            >
              æ›´æ–°æ¨¡åž‹
            </el-button>
            <el-button
              size="small"
              type="danger"
              @click="deleteModel(scope.row)"
            >
              åˆ é™¤
            </el-button>
          </template>
        </el-table-column>
      </el-table>
    </el-card>
    <!-- èƒ½è€—分析图表 -->
    <el-card class="analysis-section">
      <template #header>
        <span>能耗分析</span>
      </template>
      <el-row :gutter="20">
        <el-col :span="12">
          <div ref="energyChart" style="height: 400px;"></div>
        </el-col>
        <el-col :span="12">
          <div ref="savingChart" style="height: 400px;"></div>
        </el-col>
      </el-row>
    </el-card>
    <!-- ä¼˜åŒ–历史对话框 -->
    <el-dialog v-model="historyDialogVisible" title="优化历史记录" width="80%">
      <el-table :data="optimizationHistory" style="width: 100%" stripe>
        <el-table-column prop="timestamp" label="时间" />
        <el-table-column prop="formationPressure" label="地层压力 (MPa)" />
        <el-table-column prop="oldFrequency" label="原频率 (Hz)" />
        <el-table-column prop="newFrequency" label="新频率 (Hz)" />
        <el-table-column prop="energySaved" label="节能 (kWh)" />
        <el-table-column prop="status" label="状态">
          <template #default="scope">
            <el-tag :type="scope.row.status === 'success' ? 'success' : 'warning'">
              {{ scope.row.status === 'success' ? '成功' : '失败' }}
            </el-tag>
          </template>
        </el-table-column>
      </el-table>
    </el-dialog>
  </div>
</template>
<script setup>
import { ref, onMounted, onUnmounted, computed } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Monitor, Cpu, TrendCharts } from '@element-plus/icons-vue'
import * as echarts from 'echarts'
// å“åº”式数据
const edgeServerStatus = ref({ status: 'online', lastHeartbeat: Date.now() })
const modelStatus = ref({ status: 'running', modelCount: 3 })
const energySavingRate = ref(15.8)
const totalEnergySaved = ref(1250.5)
const pumpData = ref({
  formationPressure: 25.6,
  currentFrequency: 45.2,
  optimizedFrequency: 42.1,
  energyReduction: 23,
  flowRate: 180.5,
  power: 85.3
})
const modelConfigs = ref([
  {
    modelName: '注水泵频率优化模型',
    version: 'v2.1.0',
    status: 'active',
    accuracy: '94.2%',
    lastUpdate: '2024-01-15 14:30:00'
  },
  {
    modelName: '地层压力预测模型',
    version: 'v1.8.5',
    status: 'active',
    accuracy: '91.7%',
    lastUpdate: '2024-01-14 09:15:00'
  },
  {
    modelName: '能耗分析模型',
    version: 'v2.0.3',
    status: 'standby',
    accuracy: '89.3%',
    lastUpdate: '2024-01-13 16:45:00'
  }
])
const historyDialogVisible = ref(false)
const optimizationHistory = ref([])
// å›¾è¡¨å¼•用
const frequencyChart = ref(null)
const energyChart = ref(null)
const savingChart = ref(null)
// è‡ªåŠ¨åˆ·æ–°ç›¸å…³
const autoRefreshStatus = ref(true)
const autoRefreshTimer = ref(null)
const chartInstances = ref([])
// è®¡ç®—属性
const canControl = computed(() => {
  return edgeServerStatus.value.status === 'online' && modelStatus.value.status === 'running'
})
const getProgressColor = computed(() => {
  return (percentage) => {
    if (percentage < 20) return '#909399'
    if (percentage < 40) return '#E6A23C'
    if (percentage < 60) return '#409EFF'
    return '#67C23A'
  }
})
// ç”Ÿæˆæ¨¡æ‹Ÿæ•°æ®
const generateMockData = () => {
  // ç”Ÿæˆéšæœºåœ°å±‚压力 (20-30 MPa)
  const formationPressure = 20 + Math.random() * 10
  // æ ¹æ®åœ°å±‚压力计算优化频率
  const baseFrequency = 40 + (formationPressure - 25) * 2
  const currentFrequency = baseFrequency + (Math.random() - 0.5) * 4
  const optimizedFrequency = Math.max(35, baseFrequency - Math.random() * 3)
  // è®¡ç®—能耗降低
  const energyReduction = Math.round((currentFrequency - optimizedFrequency) / currentFrequency * 100)
  // è®¡ç®—流量和功率
  const flowRate = 150 + Math.random() * 60
  const power = 70 + Math.random() * 30
  // æ›´æ–°æ³µæ•°æ®
  pumpData.value = {
    formationPressure: parseFloat(formationPressure.toFixed(1)),
    currentFrequency: parseFloat(currentFrequency.toFixed(1)),
    optimizedFrequency: parseFloat(optimizedFrequency.toFixed(1)),
    energyReduction: Math.min(energyReduction, 35),
    flowRate: parseFloat(flowRate.toFixed(1)),
    power: parseFloat(power.toFixed(1))
  }
  // æ›´æ–°èŠ‚èƒ½æ•ˆæžœ
  energySavingRate.value = 12 + Math.random() * 8
  totalEnergySaved.value += Math.random() * 2
  // æ›´æ–°è¾¹ç¼˜æœåŠ¡å™¨çŠ¶æ€
  edgeServerStatus.value.lastHeartbeat = Date.now()
  // éšæœºæ›´æ–°æ¨¡åž‹çŠ¶æ€
  if (Math.random() > 0.95) {
    modelStatus.value.modelCount = Math.max(1, modelStatus.value.modelCount + (Math.random() > 0.5 ? 1 : -1))
  }
  // æ·»åŠ ä¼˜åŒ–åŽ†å²è®°å½•
  if (Math.random() > 0.7) {
    addOptimizationHistory()
  }
  // æ›´æ–°å›¾è¡¨æ•°æ®
  updateCharts()
}
// æ·»åŠ ä¼˜åŒ–åŽ†å²è®°å½•
const addOptimizationHistory = () => {
  const timestamp = new Date().toLocaleString()
  const record = {
    timestamp,
    formationPressure: pumpData.value.formationPressure,
    oldFrequency: pumpData.value.currentFrequency,
    newFrequency: pumpData.value.optimizedFrequency,
    energySaved: parseFloat((Math.random() * 5 + 1).toFixed(2)),
    status: Math.random() > 0.1 ? 'success' : 'failed'
  }
  optimizationHistory.value.unshift(record)
  // ä¿æŒæœ€å¤š100条记录
  if (optimizationHistory.value.length > 100) {
    optimizationHistory.value = optimizationHistory.value.slice(0, 100)
  }
}
// æ›´æ–°å›¾è¡¨æ•°æ®
const updateCharts = () => {
  chartInstances.value.forEach(instance => {
    if (instance && instance.setOption) {
      // è¿™é‡Œå¯ä»¥æ›´æ–°å›¾è¡¨æ•°æ®
      // ä¸ºäº†ç®€åŒ–,我们只是重新初始化图表
    }
  })
}
// æ–¹æ³•
const refreshData = () => {
  generateMockData()
  ElMessage.success('数据刷新成功')
}
const applyOptimization = async () => {
  try {
    await ElMessageBox.confirm('确定要应用当前的优化设置吗?', '确认操作', {
      confirmButtonText: '确定',
      cancelButtonText: '取消',
      type: 'warning'
    })
    // åº”用优化设置
    pumpData.value.currentFrequency = pumpData.value.optimizedFrequency
    ElMessage.success('优化设置应用成功')
    refreshData()
  } catch (error) {
    if (error !== 'cancel') {
      ElMessage.error('应用优化设置失败')
    }
  }
}
const emergencyStop = async () => {
  try {
    await ElMessageBox.confirm('确定要紧急停止所有注水泵吗?', '紧急操作', {
      confirmButtonText: '确定',
      cancelButtonText: '取消',
      type: 'error'
    })
    // æ‰§è¡Œç´§æ€¥åœæ­¢é€»è¾‘
    pumpData.value.currentFrequency = 0
    pumpData.value.optimizedFrequency = 0
    ElMessage.success('紧急停止执行成功')
  } catch (error) {
    if (error !== 'cancel') {
      ElMessage.error('紧急停止执行失败')
    }
  }
}
const showOptimizationHistory = () => {
  historyDialogVisible.value = true
}
const updateModel = (model) => {
  ElMessage.info(`更新模型: ${model.modelName}`)
}
const deleteModel = async (model) => {
  try {
    await ElMessageBox.confirm(`确定要删除模型 ${model.modelName} å—?`, '确认删除', {
      confirmButtonText: '确定',
      cancelButtonText: '取消',
      type: 'warning'
    })
    const index = modelConfigs.value.findIndex(m => m.modelName === model.modelName)
    if (index > -1) {
      modelConfigs.value.splice(index, 1)
      ElMessage.success('模型删除成功')
    }
  } catch (error) {
    if (error !== 'cancel') {
      ElMessage.error('模型删除失败')
    }
  }
}
const toggleAutoRefresh = () => {
  autoRefreshStatus.value = !autoRefreshStatus.value
  if (autoRefreshStatus.value) {
    startAutoRefresh()
    ElMessage.success('自动刷新已开启')
  } else {
    stopAutoRefresh()
    ElMessage.info('自动刷新已关闭')
  }
}
const startAutoRefresh = () => {
  stopAutoRefresh() // å…ˆåœæ­¢ä¹‹å‰çš„定时器
  autoRefreshTimer.value = setInterval(() => {
    generateMockData()
  }, 60000) // 1分钟 = 60000毫秒
}
const stopAutoRefresh = () => {
  if (autoRefreshTimer.value) {
    clearInterval(autoRefreshTimer.value)
    autoRefreshTimer.value = null
  }
}
const formatTime = (timestamp) => {
  return new Date(timestamp).toLocaleTimeString()
}
// åˆå§‹åŒ–图表
const initCharts = () => {
  // é¢‘率优化趋势图
  const frequencyChartInstance = echarts.init(frequencyChart.value)
  const frequencyOption = {
    title: { text: '泵频率优化趋势' },
    tooltip: { trigger: 'axis' },
    legend: { data: ['当前频率', '优化频率', '地层压力'] },
    xAxis: { type: 'category', data: ['00:00', '04:00', '08:00', '12:00', '16:00', '20:00'] },
    yAxis: [
      { type: 'value', name: '频率 (Hz)' },
      { type: 'value', name: '压力 (MPa)' }
    ],
    series: [
      {
        name: '当前频率',
        type: 'line',
        data: [45.2, 44.8, 45.5, 45.1, 44.9, 45.2]
      },
      {
        name: '优化频率',
        type: 'line',
        data: [42.1, 41.8, 42.3, 41.9, 41.7, 42.1]
      },
      {
        name: '地层压力',
        type: 'line',
        yAxisIndex: 1,
        data: [25.6, 25.8, 26.1, 25.9, 25.7, 25.6]
      }
    ]
  }
  frequencyChartInstance.setOption(frequencyOption)
  chartInstances.value.push(frequencyChartInstance)
  // èƒ½è€—分析图
  const energyChartInstance = echarts.init(energyChart.value)
  const energyOption = {
    title: { text: '日能耗对比' },
    tooltip: { trigger: 'item' },
    legend: { orient: 'vertical', left: 'left',top: 'center' },
    series: [
      {
        name: '能耗分布',
        type: 'pie',
        radius: '50%',
        data: [
          { value: 45, name: '注水泵' },
          { value: 25, name: '照明系统' },
          { value: 20, name: '通风系统' },
          { value: 10, name: '其他设备' }
        ]
      }
    ]
  }
  energyChartInstance.setOption(energyOption)
  chartInstances.value.push(energyChartInstance)
  // èŠ‚èƒ½æ•ˆæžœå›¾
  const savingChartInstance = echarts.init(savingChart.value)
  const savingOption = {
    title: { text: '节能效果趋势' },
    tooltip: { trigger: 'axis' },
    xAxis: { type: 'category', data: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'] },
    yAxis: { type: 'value', name: '节能率 (%)' },
    series: [
      {
        name: '节能率',
        type: 'bar',
        data: [12.5, 15.2, 18.7, 16.3, 19.1, 17.8, 15.8]
      }
    ]
  }
  savingChartInstance.setOption(savingOption)
  chartInstances.value.push(savingChartInstance)
}
// ç”Ÿæˆåˆå§‹åŽ†å²æ•°æ®
const generateInitialHistory = () => {
  for (let i = 0; i < 20; i++) {
    const timestamp = new Date(Date.now() - i * 3600000).toLocaleString()
    const record = {
      timestamp,
      formationPressure: parseFloat((20 + Math.random() * 10).toFixed(1)),
      oldFrequency: parseFloat((40 + Math.random() * 10).toFixed(1)),
      newFrequency: parseFloat((35 + Math.random() * 8).toFixed(1)),
      energySaved: parseFloat((Math.random() * 5 + 1).toFixed(2)),
      status: Math.random() > 0.1 ? 'success' : 'failed'
    }
    optimizationHistory.value.push(record)
  }
}
// ç”Ÿå‘½å‘¨æœŸ
onMounted(() => {
  initCharts()
  generateInitialHistory()
  refreshData()
  if (autoRefreshStatus.value) {
    startAutoRefresh()
  }
})
onUnmounted(() => {
  stopAutoRefresh()
  chartInstances.value.forEach(instance => {
    if (instance && instance.dispose) {
      instance.dispose()
    }
  })
})
</script>
<style scoped>
.app-container {
  padding: 20px;
}
.status-section {
  margin-bottom: 20px;
}
.status-card {
  height: 140px;
}
.status-item {
  display: flex;
  align-items: center;
  height: 100%;
}
.status-icon {
  font-size: 48px;
  margin-right: 20px;
  color: #409EFF;
}
.status-info {
  flex: 1;
}
.status-title {
  font-size: 14px;
  color: #909399;
  margin-bottom: 8px;
}
.status-value {
  font-size: 24px;
  font-weight: bold;
  margin-bottom: 8px;
}
.status-detail {
  font-size: 12px;
  color: #909399;
}
.status-value.online,
.status-value.running {
  color: #67C23A;
}
.status-value.offline,
.status-value.stopped {
  color: #F56C6C;
}
.status-value.success {
  color: #67C23A;
}
.control-section,
.model-section,
.analysis-section {
  margin-bottom: 20px;
}
.pump-control h4,
.pump-chart h4 {
  margin-bottom: 20px;
  color: #303133;
}
.control-buttons {
  margin-top: 20px;
  text-align: center;
}
.control-buttons .el-button {
  margin: 0 10px;
}
</style>
src/views/energyManagement/energyArea/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,511 @@
<template>
  <div class="app-container product-view">
    <div class="left">
      <div>
        <el-input
          v-model="search"
          style="width: 210px"
          placeholder="输入关键字进行搜索"
          @change="searchFilter"
          @clear="searchFilter"
          clearable
          prefix-icon="Search"
        />
        <el-button
          type="primary"
          @click="openProDia('addOne')"
          style="margin-left: 10px"
          >新增父区域</el-button
        >
      </div>
      <div ref="containerRef">
        <el-tree
          ref="tree"
          v-loading="treeLoad"
          :data="list"
          @node-click="handleNodeClick"
          :expand-on-click-node="false"
          default-expand-all
          :default-expanded-keys="expandedKeys"
          :draggable="true"
          :filter-node-method="filterNode"
          :props="{ children: 'children', label: 'label' }"
          highlight-current
          node-key="id"
          style="
            height: calc(100vh - 190px);
            overflow-y: scroll;
            scrollbar-width: none;
            margin-top: 10px;
          "
        >
          <template #default="{ node, data }">
            <div class="custom-tree-node">
              <span class="tree-node-content">
                <el-icon class="orange-icon">
                  <component :is="data.children && data.children.length > 0
                  ? node.expanded ? 'FolderOpened' : 'Folder' : 'Tickets'" />
                </el-icon>
                {{ data.label }}
              </span>
              <div>
                <el-button
                  type="primary"
                  link
                  @click="openProDia('edit', data)"
                >
                  ç¼–辑
                </el-button>
                <el-button type="primary" link @click="openModelDia('add','', data.id)">
                  æ·»åŠ å­åŒºåŸŸ
                </el-button>
                <el-button
                  v-if="!node.childNodes.length"
                  style="margin-left: 4px"
                  type="danger"
                  link
                  @click="remove(node, data)"
                >
                  åˆ é™¤
                </el-button>
              </div>
            </div>
          </template>
        </el-tree>
      </div>
    </div>
    <div class="right">
      <div style="margin-bottom: 10px" v-if="isShowButton">
        <el-button type="primary" @click="openModelDia('add')">
          æ–°å¢žå­åŒºåŸŸ
        </el-button>
        <el-button
          type="danger"
          @click="handleDelete"
          style="margin-left: 10px"
          plain
        >
          åˆ é™¤
        </el-button>
      </div>
      <PIMTable
        rowKey="id"
        :column="tableColumn"
        :tableData="tableData"
        :page="page"
        :isSelection="true"
        @selection-change="handleSelectionChange"
        :tableLoading="tableLoading"
        @pagination="pagination"
      ></PIMTable>
    </div>
    <el-dialog v-model="productDia" title="区域" width="400px" @keydown.enter.prevent>
      <el-form
        :model="form"
        label-width="140px"
        label-position="top"
        :rules="rules"
        ref="formRef"
      >
        <el-row :gutter="30">
          <el-col :span="24">
            <el-form-item label="区域名称:" prop="areaName">
              <el-input
                v-model="form.areaName"
                placeholder="请输入产品名称"
                clearable
                @keydown.enter.prevent
              />
            </el-form-item>
          </el-col>
        </el-row>
      </el-form>
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary" @click="submitForm">确认</el-button>
          <el-button @click="closeProDia">取消</el-button>
        </div>
      </template>
    </el-dialog>
    <el-dialog
      v-model="modelDia"
      title="子区域"
      width="400px"
      @close="closeModelDia"
      @keydown.enter.prevent
    >
      <el-form
        :model="modelForm"
        label-width="140px"
        label-position="top"
        :rules="modelRules"
        ref="modelFormRef"
      >
      <el-form-item label="父区域:" prop="fuId">
        <el-cascader v-model="modelForm.fuId" :options="list" :props="{
          value: 'id',
          label: 'label',
          children: 'children',
          checkStrictly: true,
        }" />
        </el-form-item>
        <el-form-item label="区域类型:" prop="areaType">
          <el-select v-model="modelForm.areaType" placeholder="请选择">
            <el-option v-for="item in area_type" :key="item.value" :label="item.label" :value="item.value" />
          </el-select>
        </el-form-item>
        <el-form-item label="区域名称:" prop="areaName">
          <el-input
            v-model="modelForm.areaName"
            placeholder="请输入单位"
            clearable
            @keydown.enter.prevent
          />
        </el-form-item>
      </el-form>
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary" @click="submitModelForm">确认</el-button>
          <el-button @click="closeModelDia">取消</el-button>
        </div>
      </template>
    </el-dialog>
  </div>
</template>
<script setup>
import { ref } from "vue";
import { ElMessageBox } from "element-plus";
import {
  areaAdd,
  areaDelete,
  areaListPage,
  areaListTree,
} from "@/api/energyManagement/index.js";
const { proxy } = getCurrentInstance();
const tree = ref(null);
const containerRef = ref(null);
const productDia = ref(false);
const modelDia = ref(false);
const modelOperationType = ref("");
const search = ref("");
const currentId = ref("");
const currentParentId = ref("");
const operationType = ref("");
const treeLoad = ref(false);
const list = ref([]);
const expandedKeys = ref([]);
const {area_type} = proxy.useDict("area_type")
const tableColumn = ref([
  {
    label: "区域名称",
    prop: "areaName",
  },
  {
    label: "区域类型",
    prop: "areaType",
    dataType: "tag",
    formatData: (row) => {
      return area_type.value.find(item => item.value == row)?.label;
    }
  },
  {
    dataType: "action",
    label: "操作",
    align: "center",
    operation: [
      {
        name: "编辑",
        type: "text",
        clickFun: (row) => {
          openModelDia("edit", row);
        },
      },
    ],
  },
]);
const tableData = ref([]);
const tableLoading = ref(false);
const isShowButton = ref(false);
const selectedRows = ref([]);
const page = reactive({
  current: 1,
  size: 10,
  total: 0,
});
const data = reactive({
  form: {
    areaName: "",
  },
  rules: {
    areaName: [{ required: true, message: "请输入", trigger: "blur" }],
  },
  modelForm: {
    areaName: "",
    fuId: "",
  },
  modelRules: {
    areaName: [{ required: true, message: "请输入", trigger: "blur" }],
    fuId: [{ required: true, message: "请输入", trigger: "change" }],
  },
});
const { form, rules, modelForm, modelRules } = toRefs(data);
// æŸ¥è¯¢äº§å“æ ‘
const getProductTreeList = () => {
  treeLoad.value = true;
  areaListTree()
    .then((res) => {
      list.value = res;
      list.value.forEach((a) => {
        expandedKeys.value.push(a.label);
      });
      treeLoad.value = false;
    })
    .catch((err) => {
      treeLoad.value = false;
    });
};
// è¿‡æ»¤äº§å“æ ‘
const searchFilter = () => {
  proxy.$refs.tree.filter(search.value);
};
// æ‰“开产品弹框
const openProDia = (type, data) => {
  operationType.value = type;
  productDia.value = true;
  form.value.areaName = "";
  if (type === "edit") {
    form.value.areaName = data.areaName;
  }
};
// æ‰“开规格型号弹框
const openModelDia = (type, data,fatherId) => {
  modelOperationType.value = type;
  modelDia.value = true;
  modelForm.value.fuId = "";
  modelForm.value.areaType = "";
  modelForm.value.areaName = "";
  modelForm.value.id = "";
  modelForm.value.fuId = fatherId;
  if (type === "edit") {
    modelForm.value = { ...data };
  }
};
// æäº¤äº§å“åç§°ä¿®æ”¹
const submitForm = () => {
  proxy.$refs.formRef.validate((valid) => {
    if (valid) {
      if (operationType.value === "add") {
        form.value.parentId = currentId.value;
        form.value.id = "";
      } else if (operationType.value === "addOne") {
        form.value.id = "";
        form.value.parentId = "";
      } else {
        form.value.id = currentId.value;
        form.value.parentId = "";
      }
      areaAdd(form.value).then((res) => {
        proxy.$modal.msgSuccess("提交成功");
        closeProDia();
        getProductTreeList();
      });
    }
  });
};
// å…³é—­äº§å“å¼¹æ¡†
const closeProDia = () => {
  proxy.$refs.formRef.resetFields();
  productDia.value = false;
};
// åˆ é™¤äº§å“
const remove = (node, data) => {
  let ids = [];
  ids.push(data.id);
  ElMessageBox.confirm("选中的内容将被删除,是否确认删除?", "删除提示", {
    confirmButtonText: "确认",
    cancelButtonText: "取消",
    type: "warning",
  })
    .then(() => {
      tableLoading.value = true;
      areaDelete(ids)
        .then((res) => {
          proxy.$modal.msgSuccess("删除成功");
          getProductTreeList();
        })
        .finally(() => {
          tableLoading.value = false;
        });
    })
    .catch(() => {
      proxy.$modal.msg("已取消");
    });
};
// é€‰æ‹©äº§å“
const handleNodeClick = (val, node, el) => {
  // åˆ¤æ–­æ˜¯å¦ä¸ºå¶å­èŠ‚ç‚¹
  isShowButton.value = !(val.children && val.children.length > 0);
  // åªæœ‰å¶å­èŠ‚ç‚¹æ‰æ‰§è¡Œä»¥ä¸‹é€»è¾‘
  currentId.value = val.id;
  currentParentId.value = val.parentId;
  getModelList(true);
};
// æäº¤è§„格型号修改
const submitModelForm = () => {
  proxy.$refs.modelFormRef.validate((valid) => {
    if (valid) {
      modelForm.value.fuId = currentId.value;
      areaAdd(modelForm.value).then((res) => {
        proxy.$modal.msgSuccess("提交成功");
        closeModelDia();
        getModelList();
        getProductTreeList();
      });
    }
  });
};
// å…³é—­åž‹å·å¼¹æ¡†
const closeModelDia = () => {
  proxy.$refs.modelFormRef.resetFields();
  modelDia.value = false;
};
// è¡¨æ ¼é€‰æ‹©æ•°æ®
const handleSelectionChange = (selection) => {
  selectedRows.value = selection;
};
// æŸ¥è¯¢è§„格型号
const pagination = (obj) => {
  page.current = obj.page;
  page.size = obj.limit;
  getModelList();
};
const getModelList = (val = false) => {
  tableLoading.value = true;
  let obj = {
    id: currentId.value,
    fuId:currentId.value,
    current: page.current,
    size: page.size
  }
  if(val){
    delete obj.id;
  }else{
    delete obj.fuId
  }
  areaListPage(obj).then((res) => {
    console.log("res", res);
    tableData.value = res.data.records;
    page.total = res.data.total;
    tableLoading.value = false;
  });
};
// åˆ é™¤è§„格型号
const handleDelete = () => {
  let ids = [];
  if (selectedRows.value.length > 0) {
    ids = selectedRows.value.map((item) => item.id);
  } else {
    proxy.$modal.msgWarning("请选择数据");
    return;
  }
  ElMessageBox.confirm("选中的内容将被删除,是否确认删除?", "删除提示", {
    confirmButtonText: "确认",
    cancelButtonText: "取消",
    type: "warning",
  })
    .then(() => {
      tableLoading.value = true;
      areaDelete(ids)
        .then((res) => {
          proxy.$modal.msgSuccess("删除成功");
          getModelList();
          getProductTreeList();
        })
        .finally(() => {
          tableLoading.value = false;
        });
    })
    .catch(() => {
      proxy.$modal.msg("已取消");
    });
};
// è°ƒç”¨tree过滤方法 ä¸­æ–‡è‹±è¿‡æ»¤
const filterNode = (value, data, node) => {
  if (!value) {
    //如果数据为空,则返回true,显示所有的数据项
    return true;
  }
  // æŸ¥è¯¢åˆ—表是否有匹配数据,将值小写,匹配英文数据
  let val = value.toLowerCase();
  return chooseNode(val, data, node); // è°ƒç”¨è¿‡æ»¤äºŒå±‚方法
};
// è¿‡æ»¤çˆ¶èŠ‚ç‚¹ / å­èŠ‚ç‚¹ (如果输入的参数是父节点且能匹配,则返回该节点以及其下的所有子节点;如果参数是子节点,则返回该节点的父节点。name是中文字符,enName是英文字符.
const chooseNode = (value, data, node) => {
  if (data.label.indexOf(value) !== -1) {
    return true;
  }
  const level = node.level;
  // å¦‚果传入的节点本身就是一级节点就不用校验了
  if (level === 1) {
    return false;
  }
  // å…ˆå–当前节点的父节点
  let parentData = node.parent;
  // éåŽ†å½“å‰èŠ‚ç‚¹çš„çˆ¶èŠ‚ç‚¹
  let index = 0;
  while (index < level - 1) {
    // å¦‚果匹配到直接返回,此处name值是中文字符,enName是英文字符。判断匹配中英文过滤
    if (parentData.data.label.indexOf(value) !== -1) {
      return true;
    }
    // å¦åˆ™çš„话再往上一层做匹配
    parentData = parentData.parent;
    index++;
  }
  // æ²¡åŒ¹é…åˆ°è¿”回false
  return false;
};
getProductTreeList();
</script>
<style scoped>
.product-view {
  display: flex;
}
.left {
  width: 380px;
  padding: 16px;
  background: #ffffff;
}
.right {
  width: calc(100% - 380px);
  padding: 16px;
  margin-left: 20px;
  background: #ffffff;
}
.custom-tree-node {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: space-between;
  font-size: 14px;
  padding-right: 8px;
}
.tree-node-content {
  display: flex;
  align-items: center; /* åž‚直居中 */
  height: 100%;
}
.orange-icon {
  color: orange;
  font-size: 18px;
  margin-right: 8px; /* å›¾æ ‡ä¸Žæ–‡å­—之间加点间距 */
}
</style>
src/views/energyManagement/energyCockpit/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,1380 @@
<template>
  <div class="app-container">
    <!-- é¡µé¢æ ‡é¢˜ -->
    <div class="page-header">
      <h2>能源驾驶舱</h2>
      <div class="header-info">
        <span class="update-time">最后更新:{{ lastUpdateTime }}</span>
        <el-button type="primary" size="small" @click="refreshData">
          <el-icon><Refresh /></el-icon>
          åˆ·æ–°æ•°æ®
        </el-button>
      </div>
    </div>
    <!-- å®žæ—¶èƒ½è€—监控 -->
    <div class="real-time-monitor">
      <el-row :gutter="20">
        <el-col :span="8">
          <el-card class="monitor-card">
            <template #header>
              <div class="card-header">
                <span>电力消耗</span>
                <el-tag type="success" size="small">实时</el-tag>
              </div>
            </template>
            <div class="monitor-content">
              <div class="monitor-value">
                <span class="value">{{ electricityConsumption }}</span>
                <span class="unit">kW·h</span>
              </div>
              <div class="monitor-trend">
                <span class="trend-label">趋势:</span>
                <el-tag :type="getTrendType(electricityTrend)" size="small">
                  {{ electricityTrend > 0 ? '↑' : '↓' }} {{ Math.abs(electricityTrend) }}%
                </el-tag>
              </div>
            </div>
          </el-card>
        </el-col>
        <el-col :span="8">
          <el-card class="monitor-card">
            <template #header>
              <div class="card-header">
                <span>水消耗</span>
                <el-tag type="primary" size="small">实时</el-tag>
              </div>
            </template>
            <div class="monitor-content">
              <div class="monitor-value">
                <span class="value">{{ waterConsumption }}</span>
                <span class="unit">m³</span>
              </div>
              <div class="monitor-trend">
                <span class="trend-label">趋势:</span>
                <el-tag :type="getTrendType(waterTrend)" size="small">
                  {{ waterTrend > 0 ? '↑' : '↓' }} {{ Math.abs(waterTrend) }}%
                </el-tag>
              </div>
            </div>
          </el-card>
        </el-col>
        <el-col :span="8">
          <el-card class="monitor-card">
            <template #header>
              <div class="card-header">
                <span>气体消耗</span>
                <el-tag type="warning" size="small">实时</el-tag>
              </div>
            </template>
            <div class="monitor-content">
              <div class="monitor-value">
                <span class="value">{{ gasConsumption }}</span>
                <span class="unit">m³</span>
              </div>
              <div class="monitor-trend">
                <span class="trend-label">趋势:</span>
                <el-tag :type="getTrendType(gasTrend)" size="small">
                  {{ gasTrend > 0 ? '↑' : '↓' }} {{ Math.abs(gasTrend) }}%
                </el-tag>
              </div>
            </div>
          </el-card>
        </el-col>
      </el-row>
    </div>
    <!-- èƒ½è€—趋势分析 -->
    <div class="trend-analysis">
      <el-card>
        <template #header>
          <div class="card-header">
            <span>能耗趋势分析</span>
            <div class="time-selector">
              <el-radio-group v-model="trendTimeUnit" @change="handleTrendTimeChange">
                <el-radio value="hour">小时</el-radio>
                <el-radio value="day">日</el-radio>
                <el-radio value="week">周</el-radio>
                <el-radio value="month">月</el-radio>
                <el-radio value="year">å¹´</el-radio>
              </el-radio-group>
            </div>
          </div>
        </template>
        <div class="chart-container">
          <div ref="trendChart" style="width: 100%; height: 400px;"></div>
        </div>
      </el-card>
    </div>
    <!-- èƒ½è€—统计与排名 -->
    <div class="statistics-ranking">
      <el-row :gutter="20">
        <el-col :span="12">
          <el-card class="statistics-card">
            <template #header>
              <div class="card-header">
                <span class="card-title">能耗统计报表</span>
                <div class="header-actions">
                  <el-select v-model="statisticsPeriod" @change="handleStatisticsChange" size="small" style="width: 100px;">
                    <el-option label="日统计" value="day" />
                    <el-option label="周统计" value="week" />
                    <el-option label="月统计" value="month" />
                    <el-option label="年统计" value="year" />
                  </el-select>
                </div>
              </div>
            </template>
            <div class="statistics-content">
              <div class="statistics-item">
                <span class="label">总能耗:</span>
                <span class="value">{{ totalEnergyConsumption }} kW·h</span>
              </div>
              <div class="statistics-item">
                <span class="label">同比:</span>
                <span class="value" :class="getComparisonClass(yearOverYear)">
                  {{ yearOverYear > 0 ? '+' : '' }}{{ yearOverYear }}%
                </span>
              </div>
              <div class="statistics-item">
                <span class="label">环比:</span>
                <span class="value" :class="getComparisonClass(monthOverMonth)">
                  {{ monthOverMonth > 0 ? '+' : '' }}{{ monthOverMonth }}%
                </span>
              </div>
              <div class="statistics-item">
                <span class="label">节能率:</span>
                <span class="value success">{{ energySavingRate }}%</span>
              </div>
            </div>
          </el-card>
        </el-col>
        <el-col :span="12">
          <el-card class="ranking-card">
            <template #header>
              <div class="card-header">
                <span class="card-title">能耗排名</span>
                <el-select v-model="rankingType" @change="handleRankingChange" size="small" style="width: 120px;">
                  <el-option label="部门排名" value="department" />
                  <el-option label="车间排名" value="workshop" />
                  <el-option label="设备排名" value="equipment" />
                </el-select>
              </div>
            </template>
            <div class="ranking-list">
              <div v-for="(item, index) in rankingList" :key="index" class="ranking-item">
                <div class="ranking-number" :class="getRankingClass(index + 1)">{{ index + 1 }}</div>
                <div class="ranking-info">
                  <div class="ranking-name">{{ item.name }}</div>
                  <div class="ranking-value">{{ item.value }} kW·h</div>
                </div>
                <div class="ranking-trend">
                  <el-tag :type="getTrendType(item.trend)" size="small">
                    {{ item.trend > 0 ? '↑' : '↓' }} {{ Math.abs(item.trend) }}%
                  </el-tag>
                </div>
              </div>
            </div>
          </el-card>
        </el-col>
      </el-row>
    </div>
    <!-- å¼‚常分析与智能控制 -->
    <div class="analysis-control">
      <el-row :gutter="20">
        <el-col :span="12">
          <el-card class="abnormal-card">
            <template #header>
              <div class="card-header">
                <span class="card-title">异常分析</span>
                <el-tag type="danger" size="small">{{ abnormalCount }}个异常</el-tag>
              </div>
            </template>
            <div class="abnormal-list">
              <div v-for="(item, index) in abnormalList" :key="index" class="abnormal-item">
                <div class="abnormal-icon">
                  <el-icon :color="getAbnormalColor(item.level)">
                    <Warning v-if="item.level === 'warning'" />
                    <CircleClose v-else />
                  </el-icon>
                </div>
                <div class="abnormal-content">
                  <div class="abnormal-title">{{ item.title }}</div>
                  <div class="abnormal-desc">{{ item.description }}</div>
                  <div class="abnormal-time">{{ item.time }}</div>
                </div>
                <div class="abnormal-action">
                  <el-button link size="small" @click="handleAbnormal(item)">处理</el-button>
                </div>
              </div>
            </div>
          </el-card>
        </el-col>
        <el-col :span="12">
          <el-card class="control-card">
            <template #header>
              <div class="card-header">
                <span class="card-title">智能控制系统</span>
                <el-switch v-model="autoControlEnabled" @change="handleAutoControlChange" />
              </div>
            </template>
            <div class="control-content">
              <div class="control-item">
                <span class="label">峰谷平电价管理:</span>
                <el-tag :type="getPriceType(currentPriceType)" size="small">
                  {{ getPriceTypeText(currentPriceType) }}
                </el-tag>
              </div>
              <div class="control-item">
                <span class="label">负荷预测:</span>
                <span class="value">{{ loadForecast }} kW</span>
              </div>
              <div class="control-item">
                <span class="label">自动启停:</span>
                <el-tag :type="autoStartStop ? 'success' : 'info'" size="small">
                  {{ autoStartStop ? '已启用' : '已禁用' }}
                </el-tag>
              </div>
              <div class="control-item">
                <span class="label">智能调节:</span>
                <el-progress :percentage="intelligentAdjustment" :color="getProgressColor" />
              </div>
            </div>
          </el-card>
        </el-col>
      </el-row>
    </div>
    <!-- çŽ¯ä¿æŒ‡æ ‡ -->
    <div class="environmental-indicators">
      <el-card>
        <template #header>
          <div class="card-header">
            <span>环保指标监控</span>
          </div>
        </template>
        <el-row :gutter="20">
          <el-col :span="8">
            <div class="indicator-item">
              <div class="indicator-title">碳排放量</div>
              <div class="indicator-value">{{ carbonEmission }} kg</div>
              <div class="indicator-trend">
                <span>同比:</span>
                <span :class="getComparisonClass(carbonEmissionTrend)">
                  {{ carbonEmissionTrend > 0 ? '+' : '' }}{{ carbonEmissionTrend }}%
                </span>
              </div>
            </div>
          </el-col>
          <el-col :span="8">
            <div class="indicator-item">
              <div class="indicator-title">环保达标率</div>
              <div class="indicator-value">{{ environmentalCompliance }}%</div>
              <div class="indicator-trend">
                <span>目标:</span>
                <span class="success">95%</span>
              </div>
            </div>
          </el-col>
          <el-col :span="8">
            <div class="indicator-item">
              <div class="indicator-title">绿色能源占比</div>
              <div class="indicator-value">{{ greenEnergyRatio }}%</div>
              <div class="indicator-trend">
                <span>目标:</span>
                <span class="success">30%</span>
              </div>
            </div>
          </el-col>
        </el-row>
      </el-card>
    </div>
    <!-- å¤šç»´åº¦æŠ¥è¡¨ -->
    <div class="multi-dimensional-reports">
      <el-card>
        <template #header>
          <div class="card-header">
            <span>多维度报表</span>
          </div>
        </template>
        <div class="report-filters">
          <el-row :gutter="20">
            <el-col :span="6">
              <el-form-item label="时间维度">
                <el-select v-model="reportTimeDimension" placeholder="选择时间维度">
                  <el-option label="小时" value="hour" />
                  <el-option label="日" value="day" />
                  <el-option label="周" value="week" />
                  <el-option label="月" value="month" />
                  <el-option label="å¹´" value="year" />
                </el-select>
              </el-form-item>
            </el-col>
            <el-col :span="6">
              <el-form-item label="部门维度">
                <el-select v-model="reportDepartmentDimension" placeholder="选择部门">
                  <el-option label="全部部门" value="all" />
                  <el-option label="生产部" value="production" />
                  <el-option label="技术部" value="technology" />
                  <el-option label="行政部" value="administration" />
                </el-select>
              </el-form-item>
            </el-col>
            <el-col :span="6">
              <el-form-item label="设备维度">
                <el-select v-model="reportEquipmentDimension" placeholder="选择设备类型">
                  <el-option label="全部设备" value="all" />
                  <el-option label="电力设备" value="electricity" />
                  <el-option label="水处理设备" value="water" />
                  <el-option label="气体设备" value="gas" />
                </el-select>
              </el-form-item>
            </el-col>
            <el-col :span="6">
              <el-form-item>
                <el-button type="primary" @click="generateReport">生成报表</el-button>
              </el-form-item>
            </el-col>
          </el-row>
        </div>
        <div class="report-preview">
          <div class="report-data">
            <el-row :gutter="20">
              <el-col :span="8">
                <div class="data-card">
                  <div class="data-title">电力消耗</div>
                  <div class="data-value">{{ reportData.electricity }} kW·h</div>
                  <div class="data-trend">
                    <span :class="getTrendClass(reportData.electricityTrend)">
                      {{ reportData.electricityTrend > 0 ? '↑' : '↓' }} {{ Math.abs(reportData.electricityTrend) }}%
                    </span>
                  </div>
                </div>
              </el-col>
              <el-col :span="8">
                <div class="data-card">
                  <div class="data-title">水消耗</div>
                  <div class="data-value">{{ reportData.water }} m³</div>
                  <div class="data-trend">
                    <span :class="getTrendClass(reportData.waterTrend)">
                      {{ reportData.waterTrend > 0 ? '↑' : '↓' }} {{ Math.abs(reportData.waterTrend) }}%
                    </span>
                  </div>
                </div>
              </el-col>
              <el-col :span="8">
                <div class="data-card">
                  <div class="data-title">气体消耗</div>
                  <div class="data-value">{{ reportData.gas }} m³</div>
                  <div class="data-trend">
                    <span :class="getTrendClass(reportData.gasTrend)">
                      {{ reportData.gasTrend > 0 ? '↑' : '↓' }} {{ Math.abs(reportData.gasTrend) }}%
                    </span>
                  </div>
                </div>
              </el-col>
            </el-row>
            <div class="report-chart">
              <div class="chart-title">能耗趋势图</div>
              <div class="chart-bars">
                <div v-for="(item, index) in reportData.chartData" :key="index" class="chart-bar">
                  <div class="bar-label">{{ item.label }}</div>
                  <div class="bar-container">
                    <div class="bar-fill" :style="{ height: item.percentage + '%', backgroundColor: item.color }"></div>
                  </div>
                  <div class="bar-value">{{ item.value }}</div>
                </div>
              </div>
            </div>
            <div class="report-summary">
              <div class="summary-item">
                <span class="summary-label">总能耗:</span>
                <span class="summary-value">{{ reportData.totalEnergy }} kW·h</span>
              </div>
              <div class="summary-item">
                <span class="summary-label">平均能耗:</span>
                <span class="summary-value">{{ reportData.averageEnergy }} kW·h</span>
              </div>
              <div class="summary-item">
                <span class="summary-label">能耗效率:</span>
                <span class="summary-value">{{ reportData.efficiency }}%</span>
              </div>
            </div>
          </div>
        </div>
      </el-card>
    </div>
  </div>
</template>
<script setup>
import { ref, reactive, onMounted, onUnmounted, nextTick } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import * as echarts from 'echarts'
import {
  Refresh,
  Download,
  Warning,
  CircleClose,
  Document,
  Edit,
  Bell
} from '@element-plus/icons-vue'
// å“åº”式数据
const lastUpdateTime = ref('')
const electricityConsumption = ref(0)
const waterConsumption = ref(0)
const gasConsumption = ref(0)
const electricityTrend = ref(0)
const waterTrend = ref(0)
const gasTrend = ref(0)
// è¶‹åŠ¿åˆ†æž
const trendTimeUnit = ref('day')
const trendChart = ref(null)
let chartInstance = null
// ç»Ÿè®¡æŠ¥è¡¨
const statisticsPeriod = ref('month')
const totalEnergyConsumption = ref(0)
const yearOverYear = ref(0)
const monthOverMonth = ref(0)
const energySavingRate = ref(0)
// èƒ½è€—排名
const rankingType = ref('department')
const rankingList = ref([])
// å¼‚常分析
const abnormalCount = ref(0)
const abnormalList = ref([])
// æ™ºèƒ½æŽ§åˆ¶
const autoControlEnabled = ref(true)
const currentPriceType = ref('peak')
const loadForecast = ref(0)
const autoStartStop = ref(true)
const intelligentAdjustment = ref(0)
// çŽ¯ä¿æŒ‡æ ‡
const carbonEmission = ref(0)
const carbonEmissionTrend = ref(0)
const environmentalCompliance = ref(0)
const greenEnergyRatio = ref(0)
// å¤šç»´åº¦æŠ¥è¡¨
const reportTimeDimension = ref('month')
const reportDepartmentDimension = ref('all')
const reportEquipmentDimension = ref('all')
const reportData = ref({
  electricity: 0,
  water: 0,
  gas: 0,
  electricityTrend: 0,
  waterTrend: 0,
  gasTrend: 0,
  totalEnergy: 0,
  averageEnergy: 0,
  efficiency: 0,
  chartData: []
})
// å®šæ—¶å™¨
let updateTimer = null
// èŽ·å–è¶‹åŠ¿ç±»åž‹æ ·å¼
const getTrendType = (trend) => {
  if (trend > 0) return 'danger'
  if (trend < 0) return 'success'
  return 'info'
}
// èŽ·å–å¯¹æ¯”ç±»åž‹æ ·å¼
const getComparisonClass = (value) => {
  if (value > 0) return 'danger'
  if (value < 0) return 'success'
  return 'info'
}
// èŽ·å–æŽ’åæ ·å¼
const getRankingClass = (rank) => {
  if (rank === 1) return 'ranking-first'
  if (rank === 2) return 'ranking-second'
  if (rank === 3) return 'ranking-third'
  return 'ranking-normal'
}
// èŽ·å–å¼‚å¸¸é¢œè‰²
const getAbnormalColor = (level) => {
  return level === 'warning' ? '#E6A23C' : '#F56C6C'
}
// èŽ·å–ç”µä»·ç±»åž‹æ ·å¼
const getPriceType = (type) => {
  const typeMap = {
    peak: 'danger',
    normal: 'warning',
    valley: 'success'
  }
  return typeMap[type] || 'info'
}
// èŽ·å–ç”µä»·ç±»åž‹æ–‡æœ¬
const getPriceTypeText = (type) => {
  const typeMap = {
    peak: '峰时',
    normal: '平时',
    valley: '谷时'
  }
  return typeMap[type] || '未知'
}
// èŽ·å–è¿›åº¦æ¡é¢œè‰²
const getProgressColor = (percentage) => {
  if (percentage < 50) return '#67C23A'
  if (percentage < 80) return '#E6A23C'
  return '#F56C6C'
}
// èŽ·å–è¶‹åŠ¿æ ·å¼
const getTrendClass = (trend) => {
  if (trend > 0) return 'trend-up'
  if (trend < 0) return 'trend-down'
  return 'trend-stable'
}
// æ¨¡æ‹Ÿæ•°æ®ç”Ÿæˆ
const generateMockData = () => {
  // å®žæ—¶èƒ½è€—数据
  electricityConsumption.value = Math.floor(Math.random() * 1000) + 2000
  waterConsumption.value = Math.floor(Math.random() * 100) + 150
  gasConsumption.value = Math.floor(Math.random() * 50) + 80
  // è¶‹åŠ¿æ•°æ®
  electricityTrend.value = (Math.random() * 20 - 10).toFixed(1)
  waterTrend.value = (Math.random() * 15 - 7.5).toFixed(1)
  gasTrend.value = (Math.random() * 12 - 6).toFixed(1)
  // ç»Ÿè®¡æ•°æ®
  totalEnergyConsumption.value = Math.floor(Math.random() * 50000) + 100000
  yearOverYear.value = (Math.random() * 20 - 10).toFixed(1)
  monthOverMonth.value = (Math.random() * 15 - 7.5).toFixed(1)
  energySavingRate.value = (Math.random() * 10 + 5).toFixed(1)
  // æŽ’名数据
  rankingList.value = [
    { name: '生产车间A', value: Math.floor(Math.random() * 5000) + 10000, trend: (Math.random() * 20 - 10).toFixed(1) },
    { name: '生产车间B', value: Math.floor(Math.random() * 4000) + 8000, trend: (Math.random() * 20 - 10).toFixed(1) },
    { name: '技术研发部', value: Math.floor(Math.random() * 3000) + 6000, trend: (Math.random() * 20 - 10).toFixed(1) },
    { name: '行政办公区', value: Math.floor(Math.random() * 2000) + 4000, trend: (Math.random() * 20 - 10).toFixed(1) },
    { name: '后勤保障区', value: Math.floor(Math.random() * 1500) + 3000, trend: (Math.random() * 20 - 10).toFixed(1) }
  ].sort((a, b) => b.value - a.value)
  // å¼‚常数据
  abnormalCount.value = Math.floor(Math.random() * 5) + 1
  abnormalList.value = [
    { level: 'warning', title: '电力负荷过高', description: '生产车间A电力负荷达到85%,建议检查设备运行状态', time: '2分钟前' },
    { level: 'error', title: '水压异常', description: '水处理设备压力异常,当前压力0.3MPa,低于正常范围', time: '5分钟前' }
  ]
  // æ™ºèƒ½æŽ§åˆ¶æ•°æ®
  loadForecast.value = Math.floor(Math.random() * 500) + 1500
  intelligentAdjustment.value = Math.floor(Math.random() * 30) + 60
  // çŽ¯ä¿æŒ‡æ ‡
  carbonEmission.value = Math.floor(Math.random() * 1000) + 5000
  carbonEmissionTrend.value = (Math.random() * 15 - 7.5).toFixed(1)
  environmentalCompliance.value = (Math.random() * 5 + 95).toFixed(1)
  greenEnergyRatio.value = (Math.random() * 10 + 25).toFixed(1)
  // æ›´æ–°æœ€åŽæ›´æ–°æ—¶é—´
  lastUpdateTime.value = new Date().toLocaleString()
  // åŒæ—¶æ›´æ–°æŠ¥è¡¨æ•°æ®
  generateReportData()
}
// åˆå§‹åŒ–趋势图表
const initTrendChart = () => {
  if (chartInstance) {
    chartInstance.dispose()
  }
  chartInstance = echarts.init(trendChart.value)
  const option = {
    title: {
      text: '能耗趋势分析',
      left: 'center'
    },
    tooltip: {
      trigger: 'axis'
    },
    legend: {
      data: ['电力', 'æ°´', '气体'],
      bottom: 10
    },
    xAxis: {
      type: 'category',
      data: generateTimeData()
    },
    yAxis: {
      type: 'value',
      name: '消耗量'
    },
    series: [
      {
        name: '电力',
        type: 'line',
        data: generateSeriesData(),
        smooth: true
      },
      {
        name: 'æ°´',
        type: 'line',
        data: generateSeriesData(),
        smooth: true
      },
      {
        name: '气体',
        type: 'line',
        data: generateSeriesData(),
        smooth: true
      }
    ]
  }
  chartInstance.setOption(option)
}
// ç”Ÿæˆæ—¶é—´æ•°æ®
const generateTimeData = () => {
  const data = []
  const now = new Date()
  switch (trendTimeUnit.value) {
    case 'hour':
      for (let i = 23; i >= 0; i--) {
        const time = new Date(now.getTime() - i * 60 * 60 * 1000)
        data.unshift(time.getHours() + ':00')
      }
      break
    case 'day':
      for (let i = 29; i >= 0; i--) {
        const time = new Date(now.getTime() - i * 24 * 60 * 60 * 1000)
        data.unshift(time.getDate() + '日')
      }
      break
    case 'week':
      for (let i = 11; i >= 0; i--) {
        data.unshift(`第${12 - i}周`)
      }
      break
    case 'month':
      for (let i = 11; i >= 0; i--) {
        const month = (12 - i) % 12 || 12
        data.unshift(`${month}月`)
      }
      break
    case 'year':
      for (let i = 4; i >= 0; i--) {
        const year = new Date().getFullYear() - i
        data.unshift(`${year}å¹´`)
      }
      break
  }
  return data
}
// ç”Ÿæˆç³»åˆ—数据
const generateSeriesData = () => {
  const data = []
  const count = trendTimeUnit.value === 'hour' ? 24 :
                trendTimeUnit.value === 'day' ? 30 :
                trendTimeUnit.value === 'week' ? 12 :
                trendTimeUnit.value === 'month' ? 12 : 5
  for (let i = 0; i < count; i++) {
    data.push(Math.floor(Math.random() * 1000) + 500)
  }
  return data
}
// å¤„理趋势时间变化
const handleTrendTimeChange = () => {
  nextTick(() => {
    initTrendChart()
  })
}
// å¤„理统计周期变化
const handleStatisticsChange = () => {
  generateMockData()
}
// å¤„理排名类型变化
const handleRankingChange = () => {
  // æ ¹æ®ç±»åž‹é‡æ–°ç”ŸæˆæŽ’名数据
  generateMockData()
}
// å¤„理自动控制变化
const handleAutoControlChange = (value) => {
  ElMessage.success(`智能控制系统已${value ? '启用' : '禁用'}`)
}
// å¤„理异常
const handleAbnormal = (item) => {
  ElMessage.info(`正在处理异常:${item.title}`)
}
// åˆ·æ–°æ•°æ®
const refreshData = () => {
  generateMockData()
  if (chartInstance) {
    initTrendChart()
  }
  ElMessage.success('数据已刷新')
}
// å¯¼å‡ºç»Ÿè®¡
const exportStatistics = () => {
  ElMessage.success('统计数据导出成功')
}
// å¯¼å‡ºçŽ¯ä¿æŠ¥å‘Š
const exportEnvironmentalReport = () => {
  ElMessage.success('环保报告导出成功')
}
// ç”Ÿæˆè‡ªå®šä¹‰æŠ¥è¡¨
const generateCustomReport = () => {
  ElMessage.info('自定义报表功能开发中...')
}
// è®¢é˜…报表
const subscribeReport = () => {
  ElMessage.info('报表订阅功能开发中...')
}
// ç”ŸæˆæŠ¥è¡¨æ•°æ®
const generateReportData = () => {
  // ç”ŸæˆåŸºç¡€æ•°æ®
  reportData.value.electricity = Math.floor(Math.random() * 5000) + 8000
  reportData.value.water = Math.floor(Math.random() * 200) + 300
  reportData.value.gas = Math.floor(Math.random() * 100) + 150
  // ç”Ÿæˆè¶‹åŠ¿æ•°æ®
  reportData.value.electricityTrend = (Math.random() * 20 - 10).toFixed(1)
  reportData.value.waterTrend = (Math.random() * 15 - 7.5).toFixed(1)
  reportData.value.gasTrend = (Math.random() * 12 - 6).toFixed(1)
  // è®¡ç®—总能耗和平均能耗
  reportData.value.totalEnergy = reportData.value.electricity + reportData.value.water * 0.1 + reportData.value.gas * 0.05
  reportData.value.averageEnergy = Math.floor(reportData.value.totalEnergy / 3)
  reportData.value.efficiency = (Math.random() * 20 + 80).toFixed(1)
  // ç”Ÿæˆå›¾è¡¨æ•°æ®
  const labels = ['周一', '周二', '周三', '周四', '周五', '周六', '周日']
  const colors = ['#409eff', '#67c23a', '#e6a23c', '#f56c6c', '#909399', '#9c27b0', '#ff9800']
  reportData.value.chartData = labels.map((label, index) => ({
    label,
    value: Math.floor(Math.random() * 1000) + 500,
    percentage: Math.floor(Math.random() * 40) + 30,
    color: colors[index]
  }))
}
// ç”ŸæˆæŠ¥è¡¨
const generateReport = () => {
  generateReportData()
  ElMessage.success('报表生成成功')
}
// å¯åŠ¨å®šæ—¶æ›´æ–°
const startAutoUpdate = () => {
  updateTimer = setInterval(() => {
    generateMockData()
    if (chartInstance) {
      initTrendChart()
    }
  }, 60000) // æ¯åˆ†é’Ÿæ›´æ–°ä¸€æ¬¡
}
// åœæ­¢å®šæ—¶æ›´æ–°
const stopAutoUpdate = () => {
  if (updateTimer) {
    clearInterval(updateTimer)
    updateTimer = null
  }
}
// ç»„件挂载
onMounted(() => {
  generateMockData()
  nextTick(() => {
    initTrendChart()
  })
  startAutoUpdate()
})
// ç»„件卸载
onUnmounted(() => {
  stopAutoUpdate()
  if (chartInstance) {
    chartInstance.dispose()
  }
})
</script>
<style lang="scss" scoped>
.app-container {
  padding: 12px;
  background: #f5f5f5;
  min-height: 100vh;
}
.page-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 12px;
  padding: 16px;
  background: white;
  border-radius: 8px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  h2 {
    margin: 0;
    color: #303133;
    font-size: 22px;
  }
  .header-info {
    display: flex;
    align-items: center;
    gap: 12px;
    .update-time {
      color: #909399;
      font-size: 14px;
    }
  }
}
.real-time-monitor {
  margin-bottom: 12px;
  .monitor-card {
    .monitor-content {
      text-align: center;
      padding: 16px 0;
      .monitor-value {
        margin-bottom: 12px;
        .value {
          font-size: 28px;
          font-weight: bold;
          color: #409eff;
        }
        .unit {
          font-size: 14px;
          color: #909399;
          margin-left: 4px;
        }
      }
      .monitor-trend {
        .trend-label {
          font-size: 14px;
          color: #606266;
          margin-right: 6px;
        }
      }
    }
  }
}
.trend-analysis {
  margin-bottom: 12px;
  .card-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    .time-selector {
      .el-radio-group {
        .el-radio {
          margin-right: 4px;
        }
      }
    }
  }
  .chart-container {
    padding: 16px 0;
  }
}
.statistics-ranking {
  margin-bottom: 12px;
  .statistics-card, .ranking-card {
    height: 100%;
    box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
    border-radius: 8px;
    transition: all 0.3s ease;
    &:hover {
      transform: translateY(-2px);
      box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
    }
  }
  .card-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 12px 16px;
    border-bottom: 1px solid #f0f0f0;
    background: #fafafa;
    .card-title {
      font-size: 15px;
      font-weight: 600;
      color: #303133;
    }
    .header-actions {
      display: flex;
      gap: 8px;
      align-items: center;
    }
  }
  .statistics-content {
    padding: 16px;
    .statistics-item {
      display: flex;
      justify-content: space-between;
      align-items: center;
      margin-bottom: 12px;
      padding: 10px 12px;
      background: #f8f9fa;
      border-radius: 6px;
      transition: background-color 0.3s ease;
      &:hover {
        background: #e9ecef;
      }
      &:last-child {
        margin-bottom: 0;
      }
      .label {
        color: #606266;
        font-size: 14px;
        font-weight: 500;
      }
      .value {
        font-weight: bold;
        font-size: 15px;
        &.success {
          color: #67c23a;
        }
      }
    }
  }
  .ranking-list {
    padding: 16px;
    .ranking-item {
      display: flex;
      align-items: center;
      padding: 12px;
      margin-bottom: 6px;
      background: #f8f9fa;
      border-radius: 6px;
      transition: all 0.3s ease;
      &:hover {
        background: #e9ecef;
        transform: translateX(4px);
      }
      &:last-child {
        margin-bottom: 0;
      }
      .ranking-number {
        width: 32px;
        height: 32px;
        border-radius: 50%;
        display: flex;
        align-items: center;
        justify-content: center;
        font-weight: bold;
        font-size: 14px;
        margin-right: 12px;
        box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
        &.ranking-first {
          background: linear-gradient(135deg, #ffd700 0%, #ffed4e 100%);
          color: #fff;
        }
        &.ranking-second {
          background: linear-gradient(135deg, #c0c0c0 0%, #d4d4d4 100%);
          color: #fff;
        }
        &.ranking-third {
          background: linear-gradient(135deg, #cd7f32 0%, #daa520 100%);
          color: #fff;
        }
        &.ranking-normal {
          background: linear-gradient(135deg, #f5f5f5 0%, #e9ecef 100%);
          color: #909399;
        }
      }
      .ranking-info {
        flex: 1;
        .ranking-name {
          font-weight: 600;
          color: #303133;
          margin-bottom: 4px;
          font-size: 14px;
        }
        .ranking-value {
          color: #606266;
          font-size: 13px;
          font-weight: 500;
        }
      }
      .ranking-trend {
        margin-left: 12px;
      }
    }
  }
}
.analysis-control {
  margin-bottom: 20px;
  .card-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
  }
  .abnormal-list {
    .abnormal-item {
      display: flex;
      align-items: flex-start;
      padding: 15px 0;
      border-bottom: 1px solid #f0f0f0;
      &:last-child {
        border-bottom: none;
      }
      .abnormal-icon {
        margin-right: 15px;
        margin-top: 2px;
      }
      .abnormal-content {
        flex: 1;
        .abnormal-title {
          font-weight: bold;
          color: #303133;
          margin-bottom: 5px;
        }
        .abnormal-desc {
          color: #606266;
          font-size: 14px;
          margin-bottom: 5px;
        }
        .abnormal-time {
          color: #909399;
          font-size: 12px;
        }
      }
      .abnormal-action {
        margin-left: 15px;
      }
    }
  }
  .control-content {
    .control-item {
      display: flex;
      justify-content: space-between;
      align-items: center;
      margin-bottom: 20px;
      &:last-child {
        margin-bottom: 0;
      }
      .label {
        color: #606266;
        font-size: 14px;
      }
      .value {
        font-weight: bold;
        color: #303133;
      }
    }
  }
}
.environmental-indicators {
  margin-bottom: 20px;
  .card-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    .header-actions {
      display: flex;
      gap: 10px;
    }
  }
  .indicator-item {
    text-align: center;
    padding: 20px 0;
    .indicator-title {
      color: #606266;
      font-size: 14px;
      margin-bottom: 10px;
    }
    .indicator-value {
      font-size: 24px;
      font-weight: bold;
      color: #409eff;
      margin-bottom: 10px;
    }
    .indicator-trend {
      font-size: 12px;
      color: #909399;
      .success {
        color: #67c23a;
      }
    }
  }
}
.multi-dimensional-reports {
  .card-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    .header-actions {
      display: flex;
      gap: 10px;
    }
  }
  .report-filters {
    padding: 20px 0;
    border-bottom: 1px solid #f0f0f0;
    margin-bottom: 20px;
  }
  .report-preview {
    .report-data {
      padding: 20px 0;
      .data-card {
        text-align: center;
        padding: 16px;
        background: #f8f9fa;
        border-radius: 8px;
        margin-bottom: 16px;
        .data-title {
          color: #606266;
          font-size: 14px;
          margin-bottom: 8px;
        }
        .data-value {
          font-size: 20px;
          font-weight: bold;
          color: #303133;
          margin-bottom: 8px;
        }
        .data-trend {
          font-size: 12px;
          .trend-up {
            color: #f56c6c;
          }
          .trend-down {
            color: #67c23a;
          }
          .trend-stable {
            color: #909399;
          }
        }
      }
      .report-chart {
        margin: 20px 0;
        padding: 20px;
        background: #f8f9fa;
        border-radius: 8px;
        .chart-title {
          text-align: center;
          font-size: 16px;
          font-weight: 600;
          color: #303133;
          margin-bottom: 16px;
        }
        .chart-bars {
          display: flex;
          justify-content: space-around;
          align-items: flex-end;
          height: 120px;
          .chart-bar {
            text-align: center;
            flex: 1;
            margin: 0 8px;
            .bar-label {
              font-size: 12px;
              color: #606266;
              margin-bottom: 8px;
            }
            .bar-container {
              height: 80px;
              background: #e9ecef;
              border-radius: 4px;
              position: relative;
              margin-bottom: 8px;
            }
            .bar-fill {
              position: absolute;
              bottom: 0;
              left: 0;
              right: 0;
              border-radius: 4px;
              transition: height 0.3s ease;
            }
            .bar-value {
              font-size: 12px;
              color: #303133;
              font-weight: 500;
            }
          }
        }
      }
      .report-summary {
        display: flex;
        justify-content: space-around;
        padding: 20px;
        background: #f8f9fa;
        border-radius: 8px;
        .summary-item {
          text-align: center;
          .summary-label {
            display: block;
            color: #606266;
            font-size: 14px;
            margin-bottom: 8px;
          }
          .summary-value {
            font-size: 18px;
            font-weight: bold;
            color: #303133;
          }
        }
      }
    }
  }
}
// é€šç”¨æ ·å¼
.card-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
}
.success {
  color: #67c23a;
}
.danger {
  color: #f56c6c;
}
.warning {
  color: #e6a23c;
}
.info {
  color: #909399;
}
</style>
src/views/energyManagement/energyPeriodTime/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,444 @@
<template>
  <div class="app-container">
    <div class="search_form">
      <div>
        <span class="search_title">日期:</span>
        <!-- <el-time-picker
            style="width: 240px;margin-right: 10px"
            v-model="searchForm.startTime"
            value-format="HH:mm:ss"
            format="HH:mm:ss"
            type="time"
            placeholder="请选择开始时间"
            clearable
        /> -->
        <el-date-picker
            v-model="searchForm.date"
            type="date"
            placeholder="请选择日期"
            :size="size"
        />
        <!-- <el-time-picker
            v-model="searchForm.timeRange"
            is-range
            arrow-control
            range-separator="To"
            start-placeholder="选择结束时间"
            end-placeholder="选择结束时间"
        /> -->
        <span class="search_title">电价(元/度):</span>
        <el-input
            v-model="searchForm.price"
            style="width: 240px"
            placeholder="请输入电价"
            @change="handleQuery"
            clearable
            :prefix-icon="Search"
        />
        <el-button type="primary" @click="handleQuery" style="margin-left: 10px"
        >搜索</el-button>
        <el-button @click="resetFilters">重置</el-button>
      </div>
      <div>
        <el-button type="primary" @click="openForm('add')">新增</el-button>
        <el-button type="danger" plain @click="handleDelete">删除</el-button>
      </div>
    </div>
    <div class="table_list">
      <PIMTable
          rowKey="id"
          :column="tableColumn"
          :tableData="tableData"
          :page="page"
          :isSelection="true"
          @selection-change="handleSelectionChange"
          :tableLoading="tableLoading"
          @pagination="pagination"
      ></PIMTable>
    </div>
    <el-dialog
        v-model="dialogFormVisible"
        title="用电时段管理"
        width="70%"
        @close="closeDia"
    >
      <el-form
          :model="form"
          label-width="140px"
          label-position="top"
          :rules="rules"
          ref="formRef"
      >
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="日期:" prop="date">
              <el-date-picker
                  style="width: 100%"
                  v-model="form.date"
                  value-format="YYYY-MM-DD"
                  format="YYYY-MM-DD"
                  type="date"
                  placeholder="请选择日期"
                  clearable
              />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="电价(元/度):" prop="price">
              <el-input
                  v-model="form.price"
                  placeholder="请输入电价"
                  clearable
                  type="number"
                  step="0.01"
                  min="0"
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
         <el-col :span="12">
           <el-form-item label="峰段:" prop="peak">
             <el-input
                 v-model="form.peak"
                 placeholder="请输入峰段"
                 clearable
                 type="number"
                 step="0.01"
                 min="0"
             />
           </el-form-item>
         </el-col>
          <el-col :span="12">
            <el-form-item label="谷段:" prop="valley">
              <el-input
                  v-model="form.valley"
                  placeholder="请输入谷段"
                  clearable
                  type="number"
                  step="0.01"
                  min="0"
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
         <el-col :span="12">
           <el-form-item label="平段:" prop="flat">
             <el-input
                 v-model="form.flat"
                 placeholder="请输入平段"
                 clearable
                 type="number"
                 step="0.01"
                 min="0"
             />
           </el-form-item>
         </el-col>
          <el-col :span="12">
            <el-form-item label="尖段:" prop="sharp">
              <el-input
                  v-model="form.sharp"
                  placeholder="请输入尖段"
                  clearable
                  type="number"
                  step="0.01"
                  min="0"
              />
            </el-form-item>
          </el-col>
        </el-row>
      </el-form>
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary" @click="submitForm">确认</el-button>
          <el-button @click="closeDia">取消</el-button>
        </div>
      </template>
    </el-dialog>
  </div>
</template>
<script setup>
import {Search} from "@element-plus/icons-vue";
import {onMounted, ref} from "vue";
import {ElMessageBox} from "element-plus";
import {getToken} from "@/utils/auth.js";
import {periodListPage,periodDelete,periodAdd,periodUpdate} from "@/api/energyManagement/index.js";
const { proxy } = getCurrentInstance();
const data = reactive({
    searchForm: {
        date: "",
        price: ""
    },
    form: {
        date: "",
        price: "",
        peak: "",
        valley: "",
        flat: "",
        sharp: ""
    }
});
const { searchForm,form } = toRefs(data);
const page = ref({
  current: 1,
  size: 10,
  total: 0
});
const dialogFormVisible = ref(false);
const selectedRows = ref([]);
const operationType = ref('');
const tableData = ref([]);
const emit = defineEmits(['close'])
const tableLoading = ref(false);
const tableColumn = ref([
    // {
    //     label: "时段名称",
    //     prop: "timeName",
    //     width: 200,
    // },
    {
        label: "日期",
        prop: "date",
        width: 200,
    },
    {
        label: "电价(元/度)",
        prop: "price",
        width: 200,
    },
        {
        label: "峰段",
        prop: "peak",
    },
    {
        label: "谷段",
        prop: "valley",
    },
    {
        label: "平段",
        prop: "flat",
    },
    {
        label: "尖段",
        prop: "sharp",
    },
    {
        dataType: "action",
        label: "操作",
        align: "center",
        fixed: 'right',
        operation: [
            {
                name: "编辑",
                type: "text",
                clickFun: (row) => {
                    openForm("edit", row);
                },
            },
        ],
    },
]);
// è¡¨æ ¼é€‰æ‹©æ•°æ®
const handleSelectionChange = (selection) => {
    selectedRows.value = selection;
};
const formDia = ref()
const upload = reactive({
    // æ˜¯å¦æ˜¾ç¤ºå¼¹å‡ºå±‚(客户导入)
    open: false,
    // å¼¹å‡ºå±‚标题(客户导入)
    title: "",
    // æ˜¯å¦ç¦ç”¨ä¸Šä¼ 
    isUploading: false,
    // è®¾ç½®ä¸Šä¼ çš„请求头部
    headers: { Authorization: "Bearer " + getToken() },
    // ä¸Šä¼ çš„地址
    url: import.meta.env.VITE_APP_BASE_API + "/equipmentEnergyConsumption/importData",
    // æ–‡ä»¶ä¸Šä¼ å‰çš„回调
    beforeUpload: (file) => {
        console.log('文件即将上传', file);
        // å¯ä»¥åœ¨æ­¤å¤„做文件类型或大小校验
        const isValid = file.type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' || file.name.endsWith('.xlsx') || file.name.endsWith('.xls');
        if (!isValid) {
            proxy.$modal.msgError("只能上传 Excel æ–‡ä»¶");
        }
        return isValid;
    },
    // æ–‡ä»¶çŠ¶æ€æ”¹å˜æ—¶çš„å›žè°ƒ
    onChange: (file, fileList) => {
        console.log('文件状态改变', file, fileList);
    },
    // æ–‡ä»¶ä¸Šä¼ æˆåŠŸæ—¶çš„å›žè°ƒ
    onSuccess: (response, file, fileList) => {
        console.log('上传成功', response, file, fileList);
        if(response.code === 200){
            proxy.$modal.msgSuccess("文件上传成功");
        }else if(response.code === 500){
            ElMessageBox.error(response.msg);
        }else{
            ElMessageBox.warning(response.msg);
        }
    },
    // æ–‡ä»¶ä¸Šä¼ å¤±è´¥æ—¶çš„回调
    onError: (error, file, fileList) => {
        console.error('上传失败', error, file, fileList);
        ElMessageBox.error("文件上传失败");
    },
    // æ–‡ä»¶ä¸Šä¼ è¿›åº¦å›žè°ƒ
    onProgress: (event, file, fileList) => {
        console.log('上传中...', event.percent);
    }
});
// æŸ¥è¯¢åˆ—表
/** æœç´¢æŒ‰é’®æ“ä½œ */
const handleQuery = () => {
    page.current = 1;
    getList();
};
//重置
const resetFilters = () => {
    searchForm.value = {
        date: "",
        price: ""
    };
  getList();
};
const pagination = (obj) => {
    page.current = obj.page;
    page.size = obj.limit;
    getList();
};
const getList = () => {
    tableLoading.value = true;
    periodListPage({ ...searchForm, ...page.value }).then((res) => {
            tableLoading.value = false;
            if (res && res.data) {
                tableData.value = res.data.records || [];
                page.total = res.data.total || 0;
            } else {
                tableData.value = [];
                page.total = 0;
                ElMessageBox.warning('未获取到数据');
            }
        })
        .catch((err) => {
            tableLoading.value = false;
            console.error('数据加载失败:', err);
            ElMessageBox.error('数据加载失败,请重试');
        });
};
// æ‰“开弹框
const openDialog = (type, row) => {
  operationType.value = type;
  dialogFormVisible.value = true;
    // form.value.maintainer = userStore.nickName;
    // form.value.maintenanceTime = getCurrentDate();
    form.value = {}
    proxy.resetForm("formRef");
    periodListPage().then((res) => {
        codeList.value = res.data;
    });
    if (type === "edit") {
        form.value = {...row}
    }
}
// æ‰“开弹框
const openForm = (type, row) => {
    openDialog(type, row)
};
// æäº¤è¡¨å•
const submitForm = () => {
    proxy.$refs["formRef"].validate(valid => {
        if (valid) {
            if (operationType.value === "add") {
                periodAdd(form.value).then(response => {
                    proxy.$modal.msgSuccess("新增成功")
                    closeDia()
                    getList()
                })
            } else {
                periodUpdate(form.value).then(response => {
                    proxy.$modal.msgSuccess("修改成功")
                    closeDia()
                    getList()
                })
            }
        }
    })
}
// å…³é—­å¼¹æ¡†
const closeDia = () => {
    proxy.resetForm("formRef");
    dialogFormVisible.value = false;
    emit('close')
};
/** å¯¼å…¥æŒ‰é’®æ“ä½œ */
function handleImport() {
    upload.title = "设备能耗";
    upload.open = true;
    // æ¸…空上次上传的文件列表
    nextTick(() => {
        proxy.$refs["uploadRef"]?.clearFiles();
    });
}
function importTemplate() {
    proxy.download(
        "/equipmentEnergyConsumption/export",
        {},
        '设备能耗导入模版.xlsx'
    );
}
/** æäº¤ä¸Šä¼ æ–‡ä»¶ */
function submitFileForm() {
    proxy.$refs["uploadRef"].submit();
}
/** å¼¹æ¡†å…³é—­æ—¶æ¸…空文件列表 */
function handleDialogClose() {
    nextTick(() => {
        proxy.$refs["uploadRef"]?.clearFiles();
    });
}
const handleDelete = () => {
    let ids = [];
    if (selectedRows.value.length > 0) {
        ids = selectedRows.value.map((item) => item.id);
    } else {
        proxy.$modal.msgWarning("请选择数据");
        return;
    }
    ElMessageBox.confirm("选中的内容将被删除,是否确认删除?", "删除提示", {
        confirmButtonText: "确认",
        cancelButtonText: "取消",
        type: "warning",
    })
    .then(() => {
        tableLoading.value = true;
        periodDelete(ids)
            .then((res) => {
                proxy.$modal.msgSuccess("删除成功");
                getList();
            })
            .finally(() => {
                tableLoading.value = false;
            });
    })
    .catch(() => {
        proxy.$modal.msg("已取消");
    });
};
onMounted(() => {
    getList();
});
</script>
<style scoped>
</style>
src/views/energyManagement/energyPower/components/formDia.vue
@@ -35,6 +35,25 @@
                        </el-form-item>
                    </el-col>
                    <el-col :span="12">
                        <el-form-item label="用电消耗区域:" prop="electricityConsumptionAreaId">
                            <el-cascader
                                v-model="form.electricityConsumptionAreaId"
                                :options="areaList"
                                :props="{
                                    value: 'id',
                                    label: 'label',
                                    children: 'children',
                                    checkStrictly: true,
                                }"
                                placeholder="请选择区域"
                                clearable
                                style="width: 100%"
                            />
                        </el-form-item>
                    </el-col>
                </el-row>
                <el-row :gutter="30">
                    <el-col :span="12">
                        <el-form-item label="每日限制电量:" prop="everyNum">
                            <el-input
                                v-model="form.everyNum"
@@ -43,8 +62,6 @@
                            />
                        </el-form-item>
                    </el-col>
                </el-row>
                <el-row :gutter="30">
                    <el-col :span="12">
                        <el-form-item label="额定功率:" prop="powerRating">
                            <el-input
@@ -54,6 +71,8 @@
                            />
                        </el-form-item>
                    </el-col>
                </el-row>
                <el-row :gutter="30">
                    <el-col :span="12">
                        <el-form-item label="实际功率:" prop="powerActual">
                            <el-input
@@ -63,8 +82,6 @@
                            />
                        </el-form-item>
                    </el-col>
                </el-row>
                <el-row :gutter="30">
                    <el-col :span="12">
                        <el-form-item label="运行时间:" prop="runDate">
                            <el-date-picker
@@ -78,6 +95,8 @@
                            />
                        </el-form-item>
                    </el-col>
                </el-row>
                <el-row :gutter="30">
                    <el-col :span="12">
                        <el-form-item label="当日用电量:" prop="dayNum">
                            <el-input
@@ -102,7 +121,7 @@
<script setup>
import {ref} from "vue";
import useUserStore from "@/store/modules/user.js";
import {deviceList, equipmentEnergyAdd, equipmentEnergyUpdate} from "@/api/energyManagement/index.js";
import {deviceList, equipmentEnergyAdd, equipmentEnergyUpdate, areaListTree} from "@/api/energyManagement/index.js";
const { proxy } = getCurrentInstance()
const emit = defineEmits(['close'])
const dialogFormVisible = ref(false);
@@ -118,6 +137,7 @@
        powerActual: "",
        runDate: "",
        dayNum: "",
        electricityConsumptionAreaId: "",
    },
    rules: {
        code: [{ required: true, message: "请选择", trigger: "change" }],
@@ -126,10 +146,12 @@
        powerRating: [{ required: true, message: "请输入", trigger: "blur" }],
        powerActual: [{ required: true, message: "请输入", trigger: "blur" }],
        dayNum: [{ required: true, message: "请输入", trigger: "blur" }],
        electricityConsumptionAreaId: [{ required: true, message: "请选择区域", trigger: "change" }],
    },
})
const { form, rules } = toRefs(data);
const codeList = ref([])
const areaList = ref([])
// æ‰“开弹框
const openDialog = (type, row) => {
@@ -139,11 +161,24 @@
    // form.value.maintenanceTime = getCurrentDate();
    form.value = {}
    proxy.resetForm("formRef");
    // èŽ·å–è®¾å¤‡åˆ—è¡¨
    deviceList().then((res) => {
        codeList.value = res.data;
    });
    // èŽ·å–åŒºåŸŸåˆ—è¡¨
    areaListTree().then((res) => {
        areaList.value = res;
        console.log("areaList", res);
    });
    if (type === "edit") {
        form.value = {...row}
        // ç¼–辑时,将单个ID转换为数组格式用于回显
        if (row.electricityConsumptionAreaId) {
            form.value.electricityConsumptionAreaId = [row.electricityConsumptionAreaId];
        }
    }
}
const setName = (code) => {
@@ -156,13 +191,19 @@
const submitForm = () => {
    proxy.$refs["formRef"].validate(valid => {
        if (valid) {
            // æäº¤å‰å¤„理 electricityConsumptionAreaId,取数组的最后一个值
            const submitData = { ...form.value };
            if (Array.isArray(submitData.electricityConsumptionAreaId) && submitData.electricityConsumptionAreaId.length > 0) {
                submitData.electricityConsumptionAreaId = submitData.electricityConsumptionAreaId[submitData.electricityConsumptionAreaId.length - 1];
            }
            if (operationType.value === "add") {
                equipmentEnergyAdd(form.value).then(response => {
                equipmentEnergyAdd(submitData).then(response => {
                    proxy.$modal.msgSuccess("新增成功")
                    closeDia()
                })
            } else {
                equipmentEnergyUpdate(form.value).then(response => {
                equipmentEnergyUpdate(submitData).then(response => {
                    proxy.$modal.msgSuccess("修改成功")
                    closeDia()
                })
src/views/energyManagement/gasManagement/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,624 @@
<template>
  <div class="app-container">
    <!-- é¡µé¢æ ‡é¢˜ -->
    <div class="page-header">
      <h2>用气管理系统</h2>
      <div class="header-info">
        <span class="update-time">最后更新:{{ lastUpdateTime }}</span>
        <el-button type="primary" size="small" @click="refreshData">
          <el-icon><Refresh /></el-icon>
          åˆ·æ–°æ•°æ®
        </el-button>
      </div>
    </div>
    <!-- ç»Ÿè®¡å¡ç‰‡åŒºåŸŸ -->
    <div class="stats-cards">
      <el-row :gutter="20">
        <el-col :span="6">
          <el-card class="stat-card">
            <div class="stat-content">
              <div class="stat-icon gas-device">
                <el-icon size="32"><Box /></el-icon>
              </div>
              <div class="stat-info">
                <div class="stat-value">{{ totalDevices }}</div>
                <div class="stat-label">在用设备</div>
              </div>
            </div>
          </el-card>
        </el-col>
        <el-col :span="6">
          <el-card class="stat-card">
            <div class="stat-content">
              <div class="stat-icon daily-consumption">
                <el-icon size="32"><TrendCharts /></el-icon>
              </div>
              <div class="stat-info">
                <div class="stat-value">{{ dailyConsumption }} m³</div>
                <div class="stat-label">日耗量</div>
              </div>
            </div>
          </el-card>
        </el-col>
        <el-col :span="6">
          <el-card class="stat-card">
            <div class="stat-content">
              <div class="stat-icon monthly-consumption">
                <el-icon size="32"><DataLine /></el-icon>
              </div>
              <div class="stat-info">
                <div class="stat-value">{{ monthlyConsumption }} m³</div>
                <div class="stat-label">月耗量</div>
              </div>
            </div>
          </el-card>
        </el-col>
        <el-col :span="6">
          <el-card class="stat-card">
            <div class="stat-content">
              <div class="stat-icon gas-price">
                <el-icon size="32"><Money /></el-icon>
              </div>
              <div class="stat-info">
                <div class="stat-value">Â¥{{ gasUnitPrice }}</div>
                <div class="stat-label">气体单价</div>
              </div>
            </div>
          </el-card>
        </el-col>
      </el-row>
    </div>
    <!-- è´¹ç”¨ç»Ÿè®¡åŒºåŸŸ -->
    <div class="cost-stats">
      <el-row :gutter="20">
        <el-col :span="12">
          <el-card class="cost-card">
            <template #header>
              <div class="card-header">
                <span>日费用统计</span>
                <el-tag type="success" size="small">今日</el-tag>
              </div>
            </template>
            <div class="cost-content">
              <div class="cost-main">
                <span class="cost-amount">Â¥{{ dailyTotalCost.toFixed(2) }}</span>
                <span class="cost-unit">元</span>
              </div>
              <div class="cost-details">
                <div class="cost-item">
                  <span>消耗量:</span>
                  <span>{{ dailyConsumption }} m³</span>
                </div>
                <div class="cost-item">
                  <span>单价:</span>
                  <span>Â¥{{ gasUnitPrice }}/m³</span>
                </div>
              </div>
            </div>
          </el-card>
        </el-col>
        <el-col :span="12">
          <el-card class="cost-card">
            <template #header>
              <div class="card-header">
                <span>月费用统计</span>
                <el-tag type="primary" size="small">本月</el-tag>
              </div>
            </template>
            <div class="cost-content">
              <div class="cost-main">
                <span class="cost-amount">Â¥{{ monthlyTotalCost.toFixed(2) }}</span>
                <span class="cost-unit">元</span>
              </div>
              <div class="cost-details">
                <div class="cost-item">
                  <span>消耗量:</span>
                  <span>{{ monthlyConsumption }} m³</span>
                </div>
                <div class="cost-item">
                  <span>平均单价:</span>
                  <span>Â¥{{ gasUnitPrice }}/m³</span>
                </div>
              </div>
            </div>
          </el-card>
        </el-col>
      </el-row>
    </div>
    <!-- è®¾å¤‡åˆ—表区域 -->
    <div class="device-section">
      <el-card>
        <template #header>
                     <div class="card-header">
             <span>设备监控</span>
             <div class="header-actions">
               <el-button type="primary" size="small" @click="addDevice">
                 <el-icon><Plus /></el-icon>
                 æ·»åŠ è®¾å¤‡
               </el-button>
             </div>
           </div>
        </template>
        <el-table :data="deviceList" border style="width: 100%" v-loading="tableLoading" stripe>
          <el-table-column align="center" label="序号" type="index" width="60" />
          <el-table-column label="设备编号" prop="deviceCode" width="120" show-overflow-tooltip />
          <el-table-column label="设备名称" prop="deviceName" width="150" show-overflow-tooltip />
          <el-table-column label="设备类型" prop="deviceType" width="120" show-overflow-tooltip />
          <el-table-column label="规格型号" prop="specification" width="150" show-overflow-tooltip />
          <el-table-column label="当前压力(MPa)" prop="currentPressure" width="130" show-overflow-tooltip>
            <template #default="scope">
              <span :class="getPressureClass(scope.row.currentPressure)">
                {{ scope.row.currentPressure }}
              </span>
            </template>
          </el-table-column>
          <el-table-column label="当前温度(℃)" prop="currentTemperature" width="130" show-overflow-tooltip>
            <template #default="scope">
              <span :class="getTemperatureClass(scope.row.currentTemperature)">
                {{ scope.row.currentTemperature }}
              </span>
            </template>
          </el-table-column>
          <el-table-column label="气体浓度(ppm)" prop="gasConcentration" width="140" show-overflow-tooltip>
            <template #default="scope">
              <span :class="getConcentrationClass(scope.row.gasConcentration)">
                {{ scope.row.gasConcentration }}
              </span>
            </template>
          </el-table-column>
          <el-table-column label="运行状态" prop="status" width="100" show-overflow-tooltip>
            <template #default="scope">
              <el-tag :type="getStatusType(scope.row.status)" size="small">
                {{ getStatusText(scope.row.status) }}
              </el-tag>
            </template>
          </el-table-column>
          <el-table-column label="最后更新" prop="lastUpdate" width="160" show-overflow-tooltip />
                     <el-table-column label="操作" align="center" width="100" fixed="right">
             <template #default="scope">
               <el-button link size="small" @click="editDevice(scope.row)">
                 ç¼–辑
               </el-button>
             </template>
           </el-table-column>
        </el-table>
      </el-card>
    </div>
    <!-- æ·»åŠ /编辑设备弹窗 -->
    <el-dialog v-model="deviceDialogVisible" :title="dialogTitle" width="600px">
      <el-form :model="deviceForm" :rules="deviceRules" ref="deviceFormRef" label-width="120px">
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="设备编号" prop="deviceCode">
              <el-input v-model="deviceForm.deviceCode" placeholder="请输入设备编号" />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="设备名称" prop="deviceName">
              <el-input v-model="deviceForm.deviceName" placeholder="请输入设备名称" />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="设备类型" prop="deviceType">
              <el-select v-model="deviceForm.deviceType" placeholder="请选择设备类型" style="width: 100%">
                <el-option label="液化气储罐" value="液化气储罐" />
                <el-option label="压缩气储罐" value="压缩气储罐" />
                <el-option label="天然气储罐" value="天然气储罐" />
                <el-option label="氧气储罐" value="氧气储罐" />
                <el-option label="其他" value="其他" />
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="规格型号" prop="specification">
              <el-input v-model="deviceForm.specification" placeholder="请输入规格型号" />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="设计压力(MPa)" prop="designPressure">
              <el-input-number v-model="deviceForm.designPressure" :min="0" :precision="2" style="width: 100%" />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="容积(m³)" prop="volume">
              <el-input-number v-model="deviceForm.volume" :min="0" :precision="2" style="width: 100%" />
            </el-form-item>
          </el-col>
        </el-row>
      </el-form>
      <template #footer>
        <el-button @click="deviceDialogVisible = false">取消</el-button>
        <el-button type="primary" @click="saveDevice">保存</el-button>
      </template>
    </el-dialog>
  </div>
</template>
<script setup>
import { ref, reactive, onMounted, onUnmounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
  Refresh,
  Box,
  TrendCharts,
  DataLine,
  Money,
  Plus
} from '@element-plus/icons-vue'
// å“åº”式数据
const lastUpdateTime = ref('')
const totalDevices = ref(0)
const dailyConsumption = ref(0)
const monthlyConsumption = ref(0)
const gasUnitPrice = ref(0)
const dailyTotalCost = ref(0)
const monthlyTotalCost = ref(0)
const deviceList = ref([])
const tableLoading = ref(false)
const deviceDialogVisible = ref(false)
const dialogTitle = ref('')
const deviceFormRef = ref()
// è®¾å¤‡è¡¨å•数据
const deviceForm = reactive({
  deviceCode: '',
  deviceName: '',
  deviceType: '',
  specification: '',
  designPressure: 0,
  volume: 0
})
// è¡¨å•验证规则
const deviceRules = {
  deviceCode: [{ required: true, message: '请输入设备编号', trigger: 'blur' }],
  deviceName: [{ required: true, message: '请输入设备名称', trigger: 'blur' }],
  deviceType: [{ required: true, message: '请选择设备类型', trigger: 'change' }],
  specification: [{ required: true, message: '请输入规格型号', trigger: 'blur' }],
  designPressure: [{ required: true, message: '请输入设计压力', trigger: 'blur' }],
  volume: [{ required: true, message: '请输入容积', trigger: 'blur' }]
}
// å®šæ—¶å™¨
let updateTimer = null
// æ¨¡æ‹Ÿæ•°æ®ç”Ÿæˆ
const generateMockData = () => {
  // æ›´æ–°ç»Ÿè®¡æ•°æ®
  totalDevices.value = Math.floor(Math.random() * 10) + 15 // 15-25台设备
  dailyConsumption.value = Math.floor(Math.random() * 100) + 200 // 200-300 m³
  monthlyConsumption.value = Math.floor(Math.random() * 2000) + 5000 // 5000-7000 m³
  gasUnitPrice.value = (Math.random() * 2 + 3).toFixed(2) // 3-5元/m³
  // è®¡ç®—费用
  dailyTotalCost.value = dailyConsumption.value * gasUnitPrice.value
  monthlyTotalCost.value = monthlyConsumption.value * gasUnitPrice.value
  // æ›´æ–°è®¾å¤‡åˆ—表数据
  deviceList.value = Array.from({ length: totalDevices.value }, (_, index) => ({
    id: index + 1,
    deviceCode: `GT${String(index + 1).padStart(3, '0')}`,
    deviceName: `储气罐${index + 1}`,
    deviceType: ['液化气储罐', '压缩气储罐', '天然气储罐', '氧气储罐'][Math.floor(Math.random() * 4)],
    specification: `${Math.floor(Math.random() * 50) + 50}m³`,
    currentPressure: (Math.random() * 2 + 0.5).toFixed(2),
    currentTemperature: (Math.random() * 20 + 15).toFixed(1),
    gasConcentration: (Math.random() * 10).toFixed(2),
    status: ['running', 'stopped', 'warning', 'error'][Math.floor(Math.random() * 4)],
    lastUpdate: new Date().toLocaleString()
  }))
  // æ›´æ–°æœ€åŽæ›´æ–°æ—¶é—´
  lastUpdateTime.value = new Date().toLocaleString()
}
// èŽ·å–åŽ‹åŠ›çŠ¶æ€æ ·å¼
const getPressureClass = (pressure) => {
  const p = parseFloat(pressure)
  if (p < 0.8) return 'pressure-low'
  if (p > 1.5) return 'pressure-high'
  return 'pressure-normal'
}
// èŽ·å–æ¸©åº¦çŠ¶æ€æ ·å¼
const getTemperatureClass = (temperature) => {
  const t = parseFloat(temperature)
  if (t < 10 || t > 35) return 'temperature-warning'
  return 'temperature-normal'
}
// èŽ·å–æµ“åº¦çŠ¶æ€æ ·å¼
const getConcentrationClass = (concentration) => {
  const c = parseFloat(concentration)
  if (c > 5) return 'concentration-warning'
  return 'concentration-normal'
}
// èŽ·å–çŠ¶æ€ç±»åž‹
const getStatusType = (status) => {
  const statusMap = {
    running: 'success',
    stopped: 'info',
    warning: 'warning',
    error: 'danger'
  }
  return statusMap[status] || 'info'
}
// èŽ·å–çŠ¶æ€æ–‡æœ¬
const getStatusText = (status) => {
  const statusMap = {
    running: '运行中',
    stopped: '已停止',
    warning: '警告',
    error: '故障'
  }
  return statusMap[status] || '未知'
}
// åˆ·æ–°æ•°æ®
const refreshData = () => {
  generateMockData()
  ElMessage.success('数据已刷新')
}
// æ·»åŠ è®¾å¤‡
const addDevice = () => {
  dialogTitle.value = '添加设备'
  Object.keys(deviceForm).forEach(key => {
    deviceForm[key] = key === 'designPressure' || key === 'volume' ? 0 : ''
  })
  deviceDialogVisible.value = true
}
// ç¼–辑设备
const editDevice = (row) => {
  dialogTitle.value = '编辑设备'
  Object.keys(deviceForm).forEach(key => {
    if (row[key] !== undefined) {
      deviceForm[key] = row[key]
    }
  })
  deviceDialogVisible.value = true
}
// ä¿å­˜è®¾å¤‡
const saveDevice = () => {
  deviceFormRef.value.validate((valid) => {
    if (valid) {
      ElMessage.success('保存成功')
      deviceDialogVisible.value = false
      refreshData()
    }
  })
}
// å¯åŠ¨å®šæ—¶æ›´æ–°
const startAutoUpdate = () => {
  updateTimer = setInterval(() => {
    generateMockData()
  }, 60000) // æ¯åˆ†é’Ÿæ›´æ–°ä¸€æ¬¡
}
// åœæ­¢å®šæ—¶æ›´æ–°
const stopAutoUpdate = () => {
  if (updateTimer) {
    clearInterval(updateTimer)
    updateTimer = null
  }
}
// ç»„件挂载
onMounted(() => {
  generateMockData()
  startAutoUpdate()
})
// ç»„件卸载
onUnmounted(() => {
  stopAutoUpdate()
})
</script>
<style lang="scss" scoped>
.app-container {
  padding: 20px;
  background: #f5f5f5;
  min-height: 100vh;
}
.page-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 20px;
  padding: 20px;
  background: white;
  border-radius: 8px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  h2 {
    margin: 0;
    color: #303133;
    font-size: 24px;
  }
  .header-info {
    display: flex;
    align-items: center;
    gap: 15px;
    .update-time {
      color: #909399;
      font-size: 14px;
    }
  }
}
.stats-cards {
  margin-bottom: 20px;
  .stat-card {
    .stat-content {
      display: flex;
      align-items: center;
      padding: 10px;
      .stat-icon {
        width: 60px;
        height: 60px;
        border-radius: 50%;
        display: flex;
        align-items: center;
        justify-content: center;
        margin-right: 15px;
        &.gas-device {
          background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
          color: white;
        }
        &.daily-consumption {
          background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
          color: white;
        }
        &.monthly-consumption {
          background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
          color: white;
        }
        &.gas-price {
          background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
          color: white;
        }
      }
      .stat-info {
        .stat-value {
          font-size: 24px;
          font-weight: bold;
          color: #303133;
          line-height: 1;
        }
        .stat-label {
          font-size: 14px;
          color: #909399;
          margin-top: 5px;
        }
      }
    }
  }
}
.cost-stats {
  margin-bottom: 20px;
  .cost-card {
    .card-header {
      display: flex;
      justify-content: space-between;
      align-items: center;
    }
    .cost-content {
      text-align: center;
      padding: 20px 0;
      .cost-main {
        margin-bottom: 15px;
        .cost-amount {
          font-size: 36px;
          font-weight: bold;
          color: #409eff;
        }
        .cost-unit {
          font-size: 16px;
          color: #909399;
          margin-left: 5px;
        }
      }
      .cost-details {
        .cost-item {
          display: flex;
          justify-content: space-between;
          margin-bottom: 8px;
          font-size: 14px;
          color: #606266;
          &:last-child {
            margin-bottom: 0;
          }
        }
      }
    }
  }
}
.device-section {
  .card-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    .header-actions {
      display: flex;
      gap: 10px;
    }
  }
}
// çŠ¶æ€æ ·å¼
.pressure-low {
  color: #e6a23c;
  font-weight: bold;
}
.pressure-normal {
  color: #67c23a;
  font-weight: bold;
}
.pressure-high {
  color: #f56c6c;
  font-weight: bold;
}
.temperature-normal {
  color: #67c23a;
  font-weight: bold;
}
.temperature-warning {
  color: #e6a23c;
  font-weight: bold;
}
.concentration-normal {
  color: #67c23a;
  font-weight: bold;
}
.concentration-warning {
  color: #f56c6c;
  font-weight: bold;
}
</style>
src/views/energyManagement/meterCollection/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,556 @@
<template>
  <div class="app-container">
    <el-card class="box-card">
      <div slot="header" class="clearfix">
        <span>电表采集管理</span>
        <el-button style="float: right; padding: 3px 0" link @click="refreshData">
          <i class="el-icon-refresh"></i> åˆ·æ–°
        </el-button>
      </div>
             <!-- æµ‹è¯•按钮 -->
       <el-row :gutter="20" style="margin-bottom: 15px;">
         <el-col :span="24">
           <el-button @click="addTestData" type="primary" size="small">添加测试数据</el-button>
           <el-button @click="clearData" type="danger" size="small">清空数据</el-button>
           <el-button @click="testChart" type="success" size="small">测试图表</el-button>
         </el-col>
       </el-row>
       <!-- æœç´¢åŒºåŸŸ -->
       <el-row :gutter="20" class="search-row">
        <el-col :span="6">
          <el-input
            v-model="searchForm.meterNo"
            placeholder="请输入电表编号"
            clearable
            @keyup.enter.native="handleSearch"
          >
            <i slot="prefix" class="el-input__icon el-icon-search"></i>
          </el-input>
        </el-col>
        <el-col :span="6">
          <el-select v-model="searchForm.location" placeholder="请选择位置" clearable>
            <el-option label="生产车间A" value="车间A"></el-option>
            <el-option label="生产车间B" value="车间B"></el-option>
            <el-option label="办公区域" value="办公区"></el-option>
            <el-option label="配电室" value="配电室"></el-option>
          </el-select>
        </el-col>
        <el-col :span="6">
          <el-date-picker
            v-model="searchForm.dateRange"
            type="daterange"
            range-separator="至"
            start-placeholder="开始日期"
            end-placeholder="结束日期"
            format="yyyy-MM-dd"
            value-format="yyyy-MM-dd"
          />
        </el-col>
        <el-col :span="6">
          <el-button type="primary" @click="handleSearch">搜索</el-button>
          <el-button @click="resetSearch">重置</el-button>
        </el-col>
      </el-row>
      <!-- ç”µè¡¨åˆ—表 -->
      <el-table
        :data="meterList"
        style="width: 100%"
        v-loading="loading"
        border
        stripe
        height="calc(100vh - 22em)"
      >
        <el-table-column prop="meterNo" label="电表编号" width="120" />
        <el-table-column prop="location" label="安装位置" width="120" />
        <el-table-column prop="meterType" label="电表类型" width="120" />
        <el-table-column prop="voltage" label="电压等级" width="100" />
        <el-table-column prop="currentReading" label="当前读数(kWh)" width="140" />
        <el-table-column prop="lastReading" label="上次读数(kWh)" width="140" />
        <el-table-column prop="consumption" label="用电量(kWh)" width="120" />
        <el-table-column prop="power" label="功率(kW)" width="100" />
        <el-table-column prop="powerFactor" label="功率因数" width="100" />
                          <el-table-column prop="status" label="状态" width="80">
           <template #default="scope">
             <el-tag :type="scope.row.status === '正常' ? 'success' : 'danger'">
               {{ scope.row.status }}
             </el-tag>
           </template>
         </el-table-column>
         <el-table-column prop="lastUpdateTime" label="最后更新时间" width="160" />
         <el-table-column label="操作" width="180" fixed="right" align="center">
           <template #default="scope">
             <el-button link @click="viewDetails(scope.row)">
               æŸ¥çœ‹è¯¦æƒ…
             </el-button>
             <el-button link @click="manualCollection(scope.row)">
               æ‰‹åЍ采集
             </el-button>
           </template>
         </el-table-column>
      </el-table>
      <!-- åˆ†é¡µ -->
            <pagination
                :total="pagination.total"
                layout="total, sizes, prev, pager, next, jumper"
                :page="pagination.currentPage"
                :limit="pagination.pageSize"
                @pagination="handleCurrentChange"
            />
    </el-card>
              <!-- è¯¦æƒ…对话框 -->
      <el-dialog
        title="电表详情"
        v-model="detailDialogVisible"
        width="60%"
        @opened="onDialogOpened"
      >
       <el-row :gutter="20">
         <el-col :span="12">
           <div class="detail-item">
             <label>电表编号:</label>
             <span>{{ currentMeter.meterNo }}</span>
           </div>
         </el-col>
         <el-col :span="12">
           <div class="detail-item">
             <label>安装位置:</label>
             <span>{{ currentMeter.location }}</span>
           </div>
         </el-col>
         <el-col :span="12">
           <div class="detail-item">
             <label>电表类型:</label>
             <span>{{ currentMeter.meterType }}</span>
           </div>
         </el-col>
         <el-col :span="12">
           <div class="detail-item">
             <label>电压等级:</label>
             <span>{{ currentMeter.voltage }}</span>
           </div>
         </el-col>
         <el-col :span="12">
           <div class="detail-item">
             <label>当前读数:</label>
             <span>{{ currentMeter.currentReading }} kWh</span>
           </div>
         </el-col>
         <el-col :span="12">
           <div class="detail-item">
             <label>上次读数:</label>
             <span>{{ currentMeter.lastReading }} kWh</span>
           </div>
         </el-col>
         <el-col :span="12">
           <div class="detail-item">
             <label>用电量:</label>
             <span>{{ currentMeter.consumption }} kWh</span>
           </div>
         </el-col>
         <el-col :span="12">
           <div class="detail-item">
             <label>功率:</label>
             <span>{{ currentMeter.power }} kW</span>
           </div>
         </el-col>
         <el-col :span="12">
           <div class="detail-item">
             <label>功率因数:</label>
             <span>{{ currentMeter.powerFactor }}</span>
           </div>
         </el-col>
         <el-col :span="12">
           <div class="detail-item">
             <label>状态:</label>
             <el-tag :type="currentMeter.status === '正常' ? 'success' : 'danger'">
               {{ currentMeter.status }}
             </el-tag>
           </div>
         </el-col>
         <el-col :span="12">
           <div class="detail-item">
             <label>最后更新时间:</label>
             <span>{{ currentMeter.lastUpdateTime }}</span>
           </div>
         </el-col>
       </el-row>
      <!-- ç”¨ç”µè¶‹åŠ¿å›¾ -->
      <div style="margin-top: 20px;">
        <h4>24小时用电趋势</h4>
        <div ref="chartContainer" style="height: 300px;"></div>
      </div>
    </el-dialog>
  </div>
</template>
<script>
import * as echarts from 'echarts'
export default {
  name: 'MeterCollection',
  data() {
    return {
      loading: false,
      searchForm: {
        meterNo: '',
        location: '',
        dateRange: []
      },
      meterList: [],
      pagination: {
        currentPage: 1,
        pageSize: 10,
        total: 0
      },
      detailDialogVisible: false,
      currentMeter: {},
      chart: null
    }
  },
  created() {
    // ç«‹å³ç”Ÿæˆä¸€äº›æµ‹è¯•数据
    this.meterList = [
      {
        id: 1,
        meterNo: 'M001',
        location: '车间A',
        meterType: '智能电表',
        voltage: '380V',
        currentReading: 8500,
        lastReading: 8400,
        consumption: 100,
        power: '75.5',
        powerFactor: '0.85',
        status: '正常',
        lastUpdateTime: '2024-01-15 10:30:00'
      },
      {
        id: 2,
        meterNo: 'M002',
        location: '车间B',
        meterType: '多功能电表',
        voltage: '220V',
        currentReading: 6200,
        lastReading: 6100,
        consumption: 100,
        power: '45.2',
        powerFactor: '0.92',
        status: '正常',
        lastUpdateTime: '2024-01-15 10:25:00'
      }
    ]
    this.pagination.total = this.meterList.length
  },
  mounted() {
    // å»¶è¿Ÿä¸€ç‚¹æ—¶é—´å†è°ƒç”¨ï¼Œç¡®ä¿DOM已经渲染
    this.$nextTick(() => {
      this.getMeterList()
    })
  },
  watch: {
    meterList: {
      handler(newVal) {
        console.log('meterList数据变化:', newVal)
      },
      deep: true,
      immediate: true
    }
  },
  methods: {
    // èŽ·å–ç”µè¡¨åˆ—è¡¨
    getMeterList() {
      this.loading = true
      // æ¨¡æ‹ŸAPI调用
      setTimeout(() => {
        const mockData = this.generateMockData()
        this.meterList = mockData
        this.pagination.total = this.meterList.length
        this.loading = false
      }, 500)
    },
    // ç”Ÿæˆæ¨¡æ‹Ÿæ•°æ®
    generateMockData() {
      const locations = ['车间A', '车间B', '办公区', '配电室']
      const meterTypes = ['智能电表', '多功能电表', '普通电表']
      const voltages = ['220V', '380V', '10kV']
      const statuses = ['正常', '异常']
      const data = []
      for (let i = 1; i <= 25; i++) {
        const currentReading = Math.floor(Math.random() * 10000) + 5000
        const lastReading = currentReading - Math.floor(Math.random() * 100) - 10
        const consumption = currentReading - lastReading
        const power = Math.random() * 100 + 20
        const powerFactor = (Math.random() * 0.3 + 0.7).toFixed(2)
        data.push({
          id: i,
          meterNo: `M${String(i).padStart(3, '0')}`,
          location: locations[Math.floor(Math.random() * locations.length)],
          meterType: meterTypes[Math.floor(Math.random() * meterTypes.length)],
          voltage: voltages[Math.floor(Math.random() * voltages.length)],
          currentReading: currentReading,
          lastReading: lastReading,
          consumption: consumption,
          power: power.toFixed(2),
          powerFactor: powerFactor,
          status: statuses[Math.floor(Math.random() * statuses.length)],
          lastUpdateTime: this.formatDate(new Date(Date.now() - Math.random() * 86400000))
        })
      }
      return data
    },
    // æ ¼å¼åŒ–日期
    formatDate(date) {
      const year = date.getFullYear()
      const month = String(date.getMonth() + 1).padStart(2, '0')
      const day = String(date.getDate()).padStart(2, '0')
      const hours = String(date.getHours()).padStart(2, '0')
      const minutes = String(date.getMinutes()).padStart(2, '0')
      return `${year}-${month}-${day} ${hours}:${minutes}`
    },
    // æœç´¢
    handleSearch() {
      this.pagination.currentPage = 1
      this.getMeterList()
    },
    // é‡ç½®æœç´¢
    resetSearch() {
      this.searchForm = {
        meterNo: '',
        location: '',
        dateRange: []
      }
      this.handleSearch()
    },
    // æŸ¥çœ‹è¯¦æƒ…
    viewDetails(row) {
      this.currentMeter = row
      this.detailDialogVisible = true
    },
    // å¯¹è¯æ¡†æ‰“开后初始化图表
    onDialogOpened() {
      this.$nextTick(() => {
        setTimeout(() => {
          this.initChart()
        }, 100)
      })
    },
    // æ‰‹åЍ采集
    manualCollection(row) {
      this.$message.success(`正在采集电表 ${row.meterNo} çš„æ•°æ®...`)
      // æ¨¡æ‹Ÿé‡‡é›†è¿‡ç¨‹
      setTimeout(() => {
        row.currentReading = Math.floor(Math.random() * 100) + row.currentReading
        row.lastUpdateTime = this.formatDate(new Date())
        this.$message.success('数据采集完成')
      }, 1000)
    },
    // åˆ·æ–°æ•°æ®
    refreshData() {
      this.getMeterList()
      this.$message.success('数据已刷新')
    },
    // æ·»åŠ æµ‹è¯•æ•°æ®
    addTestData() {
      const testData = {
        id: Date.now(),
        meterNo: `M${String(this.meterList.length + 1).padStart(3, '0')}`,
        location: '测试位置',
        meterType: '测试电表',
        voltage: '220V',
        currentReading: Math.floor(Math.random() * 10000) + 1000,
        lastReading: Math.floor(Math.random() * 5000) + 500,
        consumption: Math.floor(Math.random() * 100) + 10,
        power: (Math.random() * 100 + 10).toFixed(2),
        powerFactor: (Math.random() * 0.3 + 0.7).toFixed(2),
        status: '正常',
        lastUpdateTime: this.formatDate(new Date())
      }
      this.meterList.push(testData)
      this.pagination.total = this.meterList.length
      this.$message.success('测试数据已添加')
    },
    // æ¸…空数据
    clearData() {
      this.meterList = []
      this.pagination.total = 0
      this.$message.success('数据已清空')
    },
    // æµ‹è¯•图表
    testChart() {
      this.$message.info('图表测试功能')
      // åˆ›å»ºä¸€ä¸ªæµ‹è¯•对话框来测试图表
      this.currentMeter = {
        meterNo: 'TEST001',
        location: '测试位置',
        meterType: '测试电表',
        voltage: '220V',
        currentReading: 1000,
        lastReading: 900,
        consumption: 100,
        power: '50.0',
        powerFactor: '0.85',
        status: '正常',
        lastUpdateTime: '2024-01-15 12:00:00'
      }
      this.detailDialogVisible = true
    },
    // åˆ†é¡µå¤§å°æ”¹å˜
    handleSizeChange(val) {
      this.pagination.pageSize = val
      this.getMeterList()
    },
    // å½“前页改变
    handleCurrentChange(val) {
      this.pagination.pageSize = val.limit
      this.pagination.currentPage = val.page
      this.getMeterList()
    },
    // åˆå§‹åŒ–图表
    initChart() {
      try {
        if (this.chart) {
          this.chart.dispose()
          this.chart = null
        }
        // ç¡®ä¿DOM元素存在
        if (!this.$refs.chartContainer) {
          console.error('图表容器不存在,等待DOM更新...')
          // å¦‚果容器不存在,等待一下再试
          setTimeout(() => {
            this.initChart()
          }, 100)
          return
        }
        // æ£€æŸ¥å®¹å™¨å°ºå¯¸
        const container = this.$refs.chartContainer
        if (container.offsetWidth === 0 || container.offsetHeight === 0) {
          setTimeout(() => {
            this.initChart()
          }, 100)
          return
        }
        this.chart = echarts.init(container)
        // ç”Ÿæˆ24小时模拟数据
        const hours = []
        const consumption = []
        for (let i = 0; i < 24; i++) {
          hours.push(`${i}:00`)
          consumption.push(Math.floor(Math.random() * 50) + 20)
        }
        const option = {
          title: {
            text: '24小时用电量趋势',
            left: 'center'
          },
          tooltip: {
            trigger: 'axis',
            formatter: '{b}<br/>用电量: {c} kWh'
          },
          xAxis: {
            type: 'category',
            data: hours,
            axisLabel: {
              rotate: 45
            }
          },
          yAxis: {
            type: 'value',
            name: '用电量 (kWh)'
          },
          series: [{
            data: consumption,
            type: 'line',
            smooth: true,
            areaStyle: {
              opacity: 0.3
            },
            itemStyle: {
              color: '#409EFF'
            }
          }]
        }
        this.chart.setOption(option)
      } catch (error) {
        console.error('图表初始化失败:', error)
        this.$message.error('图表初始化失败: ' + error.message)
      }
    }
  },
  beforeUnmount() {
    if (this.chart) {
      try {
        this.chart.dispose()
        this.chart = null
      } catch (error) {
        console.error('清理图表失败:', error)
      }
    }
  }
}
</script>
<style scoped>
.search-row {
  margin-bottom: 20px;
}
.pagination {
  margin-top: 20px;
  text-align: right;
}
.el-table {
  margin-top: 20px;
}
.detail-item {
  margin-bottom: 15px;
  padding: 10px;
  border: 1px solid #ebeef5;
  border-radius: 4px;
  background-color: #fafafa;
}
.detail-item label {
  font-weight: bold;
  color: #606266;
  margin-right: 10px;
  min-width: 100px;
  display: inline-block;
}
.detail-item span {
  color: #303133;
}
.detail-item .el-tag {
  margin-left: 0;
}
</style>
src/views/energyManagement/waterManagement/components/formDia.vue
@@ -15,9 +15,9 @@
            >
                <el-row :gutter="30">
                    <el-col :span="12">
                        <el-form-item label="设备:" prop="code">
                        <el-form-item label="设备:" prop="deviceModel">
                            <el-select
                                v-model="form.code"
                                v-model="form.deviceModel"
                                placeholder="请选择"
                                clearable
                                @change="setName"
@@ -35,9 +35,9 @@
                        </el-form-item>
                    </el-col>
                    <el-col :span="12">
                        <el-form-item label="每日限制水量:" prop="everyNum">
                        <el-form-item label="每日限制水量:" prop="waterDayLimit">
                            <el-input
                                v-model="form.everyNum"
                                v-model="form.waterDayLimit"
                                placeholder="请输入"
                                clearable
                            />
@@ -46,18 +46,18 @@
                </el-row>
                <el-row :gutter="30">
                    <el-col :span="12">
                        <el-form-item label="额定流量:" prop="flowRating">
                        <el-form-item label="额定流量:" prop="ratedRate">
                            <el-input
                                v-model="form.flowRating"
                                v-model="form.ratedRate"
                                placeholder="请输入"
                                clearable
                            />
                        </el-form-item>
                    </el-col>
                    <el-col :span="12">
                        <el-form-item label="实际流量:" prop="flowActual">
                        <el-form-item label="实际流量:" prop="actualTraffic">
                            <el-input
                                v-model="form.flowActual"
                                v-model="form.actualTraffic"
                                placeholder="请输入"
                                clearable
                            />
@@ -66,10 +66,10 @@
                </el-row>
                <el-row :gutter="30">
                    <el-col :span="12">
                        <el-form-item label="运行时间:" prop="runDate">
                        <el-form-item label="运行时间:" prop="runTime">
                            <el-date-picker
                                style="width: 100%"
                                v-model="form.runDate"
                                v-model="form.runTime"
                                value-format="YYYY-MM-DD"
                                format="YYYY-MM-DD"
                                type="date"
@@ -79,9 +79,9 @@
                        </el-form-item>
                    </el-col>
                    <el-col :span="12">
                        <el-form-item label="当日用水量:" prop="dayNum">
                        <el-form-item label="当日用水量:" prop="waterDay">
                            <el-input
                                v-model="form.dayNum"
                                v-model="form.waterDay"
                                placeholder="请输入"
                                clearable
                            />
@@ -99,9 +99,9 @@
                        </el-form-item>
                    </el-col>
                    <el-col :span="12">
                        <el-form-item label="用水类型:" prop="waterType">
                        <el-form-item label="用水类型:" prop="type">
                            <el-select
                                v-model="form.waterType"
                                v-model="form.type"
                                placeholder="请选择"
                                clearable
                            >
@@ -136,25 +136,25 @@
const data = reactive({
    form: {
        name: "",
        code: "",
        everyNum: "",
        flowRating: "",
        flowActual: "",
        runDate: "",
        dayNum: "",
    deviceName: "",
    deviceModel: "",
    waterDayLimit: "",
    ratedRate: "",
    actualTraffic: "",
    runTime: "",
    waterDay: "",
        waterPrice: "",
        waterType: "",
    type: "",
    },
    rules: {
        code: [{ required: true, message: "请选择", trigger: "change" }],
        runDate: [{ required: true, message: "请选择", trigger: "change" }],
        everyNum: [{ required: true, message: "请输入", trigger: "blur" }],
        flowRating: [{ required: true, message: "请输入", trigger: "blur" }],
        flowActual: [{ required: true, message: "请输入", trigger: "blur" }],
        dayNum: [{ required: true, message: "请输入", trigger: "blur" }],
    deviceModel: [{ required: true, message: "请选择", trigger: "change" }],
        runTime: [{ required: true, message: "请选择", trigger: "change" }],
    waterDayLimit: [{ required: true, message: "请输入", trigger: "blur" }],
    ratedRate: [{ required: true, message: "请输入", trigger: "blur" }],
    actualTraffic: [{ required: true, message: "请输入", trigger: "blur" }],
    waterDay: [{ required: true, message: "请输入", trigger: "blur" }],
        waterPrice: [{ required: true, message: "请输入", trigger: "blur" }],
        waterType: [{ required: true, message: "请选择", trigger: "change" }],
    type: [{ required: true, message: "请选择", trigger: "change" }],
    },
})
const { form, rules } = toRefs(data);
@@ -166,8 +166,8 @@
  dialogFormVisible.value = true;
    form.value = {}
    proxy.resetForm("formRef");
    waterDeviceList().then((res) => {
        codeList.value = res.data;
    waterDeviceList({size: -1}).then((res) => {
        codeList.value = res.data.records;
    });
    if (type === "edit") {
        form.value = {...row}
src/views/energyManagement/waterManagement/index.vue
@@ -4,7 +4,7 @@
            <div>
                <span class="search_title">设备名称:</span>
                <el-input
                    v-model="searchForm.name"
                    v-model="searchForm.deviceName"
                    style="width: 240px"
                    placeholder="请输入"
                    @change="handleQuery"
@@ -101,35 +101,35 @@
const tableColumn = ref([
    {
        label: "设备名称",
        prop: "name",
        prop: "deviceName",
        width: 200,
    },
    {
        label: "规格型号",
        prop: "code",
        prop: "deviceModel",
        width: 200,
    },
    {
        label: "额定流量",
        prop: "flowRating",
        prop: "ratedRate",
    },
    {
        label: "实际流量",
        prop: "flowActual",
        prop: "actualTraffic",
    },
    {
        label: "运行时间",
        prop: "runDate",
        prop: "runTime",
        width:150
    },
    {
        label: "当日用水量",
        prop: "dayNum",
        prop: "waterDay",
        width: 150,
    },
    {
        label: "每日限制水量",
        prop: "everyNum",
        prop: "waterDayLimit",
        width:220
    },
    {
@@ -175,7 +175,7 @@
    // è®¾ç½®ä¸Šä¼ çš„请求头部
    headers: { Authorization: "Bearer " + getToken() },
    // ä¸Šä¼ çš„地址
    url: import.meta.env.VITE_APP_BASE_API + "/waterEquipmentConsumption/importData",
    url: import.meta.env.VITE_APP_BASE_API + "/waterRecord/importData",
    // æ–‡ä»¶ä¸Šä¼ å‰çš„回调
    beforeUpload: (file) => {
        console.log('文件即将上传', file);
@@ -257,7 +257,7 @@
}
function importTemplate() {
    proxy.download(
        "/waterEquipmentConsumption/export",
        "/waterRecord/export",
        {},
        '用水设备导入模版.xlsx'
    );
src/views/equipmentManagement/iotMonitor/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,317 @@
<template>
  <div class="app-container iot-monitor">
    <div class="header">
      <div class="title">实时工况监控(IoT)</div>
      <div class="actions">
        <el-button type="primary" @click="toggleCollecting">{{ collecting ? '暂停采集' : '启动采集' }}</el-button>
        <el-button @click="resetAll">重置</el-button>
        <span class="ts">上次更新时间:{{ lastUpdatedDisplay }}</span>
      </div>
    </div>
<!--    <el-alert-->
<!--      title="边缘预警规则:轴承磨损-振动值偏离基线±5%触发告警;温度/压力越界触发提醒"-->
<!--      type="info"-->
<!--      :closable="false"-->
<!--      show-icon-->
<!--      class="rule-alert"-->
<!--    />-->
    <el-row :gutter="16">
      <el-col v-for="dev in devices" :key="dev.id" :span="12">
        <el-card :class="['device-card', dev.hasAlert ? 'is-alert' : '']">
          <template #header>
            <div class="card-header">
              <div class="card-title">
                <span class="device-name">{{ dev.name }}</span>
                <el-tag :type="dev.hasAlert ? 'danger' : 'success'" size="small">{{ dev.hasAlert ? '告警' : '正常' }}</el-tag>
              </div>
              <div class="meta">类型:{{ dev.type }}|基线振动:{{ dev.baseline.vibration.toFixed(2) }} mm/s</div>
            </div>
          </template>
          <div class="metrics">
            <div class="metric" :class="{ 'metric-alert': dev.alerts.vibration }">
              <div class="metric-head">
                <span>振动(mm/s)</span>
                <el-tag :type="dev.alerts.vibration ? 'danger' : 'info'" size="small">{{ dev.alerts.vibration ? '±5%越界' : '基线±5%' }}</el-tag>
              </div>
              <div class="metric-value">{{ currentValue(dev.series.vibration).toFixed(2) }}</div>
              <Echarts
                :xAxis="[{ type: 'category', data: xAxisLabels }]"
                :yAxis="[{ type: 'value', name: 'mm/s' }]"
                :series="[{ type: 'line', smooth: true, showSymbol: false, data: dev.series.vibration }]"
                :tooltip="{ trigger: 'axis' }"
                :grid="{ left: 40, right: 10, top: 10, bottom: 20 }"
                :chartStyle="{ height: '160px', width: '100%' }"
                :lineColors="['#409EFF']"
              />
            </div>
            <div class="metric" :class="{ 'metric-alert': dev.alerts.temperature }">
              <div class="metric-head">
                <span>温度(°C)</span>
                <el-tag :type="dev.alerts.temperature ? 'warning' : 'info'" size="small">{{ dev.alerts.temperature ? '越界' : '20~80' }}</el-tag>
              </div>
              <div class="metric-value">{{ currentValue(dev.series.temperature).toFixed(1) }}</div>
              <Echarts
                :xAxis="[{ type: 'category', data: xAxisLabels }]"
                :yAxis="[{ type: 'value', name: '°C' }]"
                :series="[{ type: 'line', smooth: true, showSymbol: false, data: dev.series.temperature }]"
                :tooltip="{ trigger: 'axis' }"
                :grid="{ left: 40, right: 10, top: 10, bottom: 20 }"
                :chartStyle="{ height: '160px', width: '100%' }"
                :lineColors="['#E6A23C']"
              />
            </div>
            <div class="metric" :class="{ 'metric-alert': dev.alerts.pressure }">
              <div class="metric-head">
                <span>压力(MPa)</span>
                <el-tag :type="dev.alerts.pressure ? 'warning' : 'info'" size="small">{{ dev.alerts.pressure ? '越界' : '0.2~1.5' }}</el-tag>
              </div>
              <div class="metric-value">{{ currentValue(dev.series.pressure).toFixed(2) }}</div>
              <Echarts
                :xAxis="[{ type: 'category', data: xAxisLabels }]"
                :yAxis="[{ type: 'value', name: 'MPa' }]"
                :series="[{ type: 'line', smooth: true, showSymbol: false, data: dev.series.pressure }]"
                :tooltip="{ trigger: 'axis' }"
                :grid="{ left: 40, right: 10, top: 10, bottom: 20 }"
                :chartStyle="{ height: '160px', width: '100%' }"
                :lineColors="['#67C23A']"
              />
            </div>
          </div>
        </el-card>
      </el-col>
    </el-row>
  </div>
</template>
<script setup>
import { ref, reactive, onMounted, onBeforeUnmount, computed } from 'vue'
import { ElNotification } from 'element-plus'
import Echarts from '@/components/Echarts/echarts.vue'
defineOptions({ name: 'IoTMonitor' })
const windowSize = 30
const collecting = ref(true)
const lastUpdated = ref(Date.now())
const lastUpdatedDisplay = computed(() => new Date(lastUpdated.value).toLocaleTimeString())
const xAxisLabels = ref(Array.from({ length: windowSize }, (_, i) => i - (windowSize - 1)).map(n => `${n}s`))
function makeSeries(fill, decimals = 2) {
  return Array.from({ length: windowSize }, () => Number(fill.toFixed(decimals)))
}
const devices = reactive([
  {
    id: 'water-pump',
    name: '注水泵1',
    type: '移动装备',
    baseline: { vibration: 9 },
    initial: { temperature: 40, pressure: 0.70 },
    alerts: { vibration: false, temperature: false, pressure: false },
    hasAlert: false,
    series: {
      vibration: makeSeries(9),
      temperature: makeSeries(40, 1),
      pressure: makeSeries(0.7, 2),
    },
  },
  {
    id: 'fluid-supply-truck',
    name: '注水泵2',
    type: '移动装备',
    baseline: { vibration: 7 },
    initial: { temperature: 30, pressure: 0.60 },
    alerts: { vibration: false, temperature: false, pressure: false },
    hasAlert: false,
    series: {
      vibration: makeSeries(7),
      temperature: makeSeries(30, 1),
      pressure: makeSeries(0.6, 2),
    },
  },
  {
    id: 'fracturing-truck',
    name: '注水泵3',
    type: '移动装备',
    baseline: { vibration: 12 },
    initial: { temperature: 65, pressure: 1.40 },
    alerts: { vibration: false, temperature: false, pressure: false },
    hasAlert: false,
    series: {
      vibration: makeSeries(12),
      temperature: makeSeries(65, 1),
      pressure: makeSeries(1.4, 2),
    },
  },
  {
    id: 'oil-tank-truck',
    name: '注水泵4',
    type: '移动装备',
    baseline: { vibration: 6 },
    initial: { temperature: 28, pressure: 0.50 },
    alerts: { vibration: false, temperature: false, pressure: false },
    hasAlert: false,
    series: {
      vibration: makeSeries(6),
      temperature: makeSeries(28, 1),
      pressure: makeSeries(0.5, 2),
    },
  },
])
function currentValue(arr) {
  return arr[arr.length - 1] ?? 0
}
function pushWindow(arr, val) {
  if (arr.length >= windowSize) arr.shift()
  arr.push(val)
}
function clamp(val, min, max) { return Math.max(min, Math.min(max, val)) }
function tickDevice(dev) {
  const vibBase = dev.baseline.vibration
  // æŒ¯åŠ¨ï¼šåŸºçº¿Â±2%随机波动;5%概率触发8%~12%尖峰模拟告警
  const spike = Math.random() < 0.05
  const vibNoise = vibBase * (spike ? (1 + (Math.random() * 0.08 + 0.04) * (Math.random() < 0.5 ? -1 : 1)) : (1 + (Math.random() - 0.5) * 0.04))
  const vibVal = Number(vibNoise.toFixed(2))
  pushWindow(dev.series.vibration, vibVal)
  // æ¸©åº¦ï¼šç¼“慢随机游走,并添加偶发高温偏移
  const tPrev = currentValue(dev.series.temperature)
  const tDrift = tPrev + (Math.random() - 0.5) * 0.8 + (Math.random() < 0.02 ? 6 : 0)
  const tVal = Number(clamp(tDrift, 15, 95).toFixed(1))
  pushWindow(dev.series.temperature, tVal)
  // åŽ‹åŠ›ï¼šå°å¹…æ³¢åŠ¨ï¼Œå¶å‘ä½ŽåŽ‹/高压
  const pPrev = currentValue(dev.series.pressure)
  const pDrift = pPrev + (Math.random() - 0.5) * 0.05 + (Math.random() < 0.02 ? (Math.random() < 0.5 ? -0.3 : 0.3) : 0)
  const pVal = Number(clamp(pDrift, 0.05, 2.0).toFixed(2))
  pushWindow(dev.series.pressure, pVal)
  // è¾¹ç¼˜è®¡ç®—阈值判断
  const vibDelta = Math.abs(vibVal - vibBase) / vibBase
  const vibAlert = vibDelta > 0.05
  const tAlert = tVal < 20 || tVal > 80
  const pAlert = pVal < 0.2 || pVal > 1.5
  const prevHasAlert = dev.hasAlert
  dev.alerts.vibration = vibAlert
  dev.alerts.temperature = tAlert
  dev.alerts.pressure = pAlert
  dev.hasAlert = vibAlert || tAlert || pAlert
  if (dev.hasAlert && !prevHasAlert) {
    const reasons = []
    if (vibAlert) reasons.push(`振动偏离±5% (当前 ${vibVal} / åŸºçº¿ ${vibBase})`)
    if (tAlert) reasons.push(`温度越界 (当前 ${tVal}°C, æœŸæœ› 20~80°C) `)
    if (pAlert) reasons.push(`压力越界 (当前 ${pVal}MPa, æœŸæœ› 0.2~1.5MPa) `)
    ElNotification({
      title: `${dev.name} å‘Šè­¦`,
      message: reasons.join(';'),
      type: vibAlert ? 'error' : 'warning',
      duration: 5000,
    })
  }
}
let timer = null
function start() {
  if (timer) return
  timer = setInterval(() => {
    if (!collecting.value) return
    devices.forEach(tickDevice)
    lastUpdated.value = Date.now()
  }, 10000)
}
function stop() {
  if (timer) {
    clearInterval(timer)
    timer = null
  }
}
function toggleCollecting() { collecting.value = !collecting.value }
function resetAll() {
  devices.forEach(dev => {
    dev.series.vibration = makeSeries(dev.baseline.vibration)
    const t0 = dev.initial?.temperature ?? 45
    const p0 = dev.initial?.pressure ?? 0.8
    dev.series.temperature = makeSeries(t0, 1)
    dev.series.pressure = makeSeries(p0, 2)
    dev.alerts.vibration = false
    dev.alerts.temperature = false
    dev.alerts.pressure = false
    dev.hasAlert = false
  })
  lastUpdated.value = Date.now()
}
onMounted(() => {
  start()
})
onBeforeUnmount(() => {
  stop()
})
</script>
<style lang="scss" scoped>
.iot-monitor {
  .header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 12px;
    .title { font-size: 18px; font-weight: 600; }
    .actions { display: flex; align-items: center; gap: 8px; }
    .ts { color: #909399; font-size: 12px; }
  }
  .rule-alert { margin-bottom: 12px; }
}
.device-card {
  margin-bottom: 16px;
  transition: border-color 0.2s ease, box-shadow 0.2s ease;
  &.is-alert { border-color: #F56C6C; box-shadow: 0 0 0 2px rgba(245,108,108,0.2) inset; }
  .card-header {
    display: flex; flex-direction: column; gap: 4px;
    .card-title { display: flex; align-items: center; gap: 8px; font-weight: 600; }
    .meta { color: #909399; font-size: 12px; }
  }
  .metrics {
    display: grid;
    grid-template-columns: 1fr;
    gap: 12px;
  }
}
.metric {
  border: 1px solid #ebeef5;
  border-radius: 6px;
  padding: 8px 8px 0 8px;
  &-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 4px; font-size: 13px; color: #606266; }
  &-value { font-size: 20px; font-weight: 600; margin: 2px 0 6px 0; }
}
.metric-alert {
  border-color: #F56C6C;
  background: #FFF6F6;
}
@media (min-width: 1200px) {
  .device-card .metrics { grid-template-columns: 1fr 1fr 1fr; }
}
</style>
src/views/equipmentManagement/iotMonitor/indexWD.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,317 @@
<template>
  <div class="app-container iot-monitor">
    <div class="header">
      <div class="title">实时工况监控(IoT)</div>
      <div class="actions">
        <el-button type="primary" @click="toggleCollecting">{{ collecting ? '暂停采集' : '启动采集' }}</el-button>
        <el-button @click="resetAll">重置</el-button>
        <span class="ts">上次更新时间:{{ lastUpdatedDisplay }}</span>
      </div>
    </div>
<!--    <el-alert-->
<!--      title="边缘预警规则:轴承磨损-振动值偏离基线±5%触发告警;温度/压力越界触发提醒"-->
<!--      type="info"-->
<!--      :closable="false"-->
<!--      show-icon-->
<!--      class="rule-alert"-->
<!--    />-->
    <el-row :gutter="16">
      <el-col v-for="dev in devices" :key="dev.id" :span="12">
        <el-card :class="['device-card', dev.hasAlert ? 'is-alert' : '']">
          <template #header>
            <div class="card-header">
              <div class="card-title">
                <span class="device-name">{{ dev.name }}</span>
                <el-tag :type="dev.hasAlert ? 'danger' : 'success'" size="small">{{ dev.hasAlert ? '告警' : '正常' }}</el-tag>
              </div>
              <div class="meta">类型:{{ dev.type }}|基线振动:{{ dev.baseline.vibration.toFixed(2) }} mm/s</div>
            </div>
          </template>
          <div class="metrics">
            <div class="metric" :class="{ 'metric-alert': dev.alerts.vibration }">
              <div class="metric-head">
                <span>振动(mm/s)</span>
                <el-tag :type="dev.alerts.vibration ? 'danger' : 'info'" size="small">{{ dev.alerts.vibration ? '±5%越界' : '基线±5%' }}</el-tag>
              </div>
              <div class="metric-value">{{ currentValue(dev.series.vibration).toFixed(2) }}</div>
              <Echarts
                :xAxis="[{ type: 'category', data: xAxisLabels }]"
                :yAxis="[{ type: 'value', name: 'mm/s' }]"
                :series="[{ type: 'line', smooth: true, showSymbol: false, data: dev.series.vibration }]"
                :tooltip="{ trigger: 'axis' }"
                :grid="{ left: 40, right: 10, top: 10, bottom: 20 }"
                :chartStyle="{ height: '160px', width: '100%' }"
                :lineColors="['#409EFF']"
              />
            </div>
            <div class="metric" :class="{ 'metric-alert': dev.alerts.temperature }">
              <div class="metric-head">
                <span>温度(°C)</span>
                <el-tag :type="dev.alerts.temperature ? 'warning' : 'info'" size="small">{{ dev.alerts.temperature ? '越界' : '20~80' }}</el-tag>
              </div>
              <div class="metric-value">{{ currentValue(dev.series.temperature).toFixed(1) }}</div>
              <Echarts
                :xAxis="[{ type: 'category', data: xAxisLabels }]"
                :yAxis="[{ type: 'value', name: '°C' }]"
                :series="[{ type: 'line', smooth: true, showSymbol: false, data: dev.series.temperature }]"
                :tooltip="{ trigger: 'axis' }"
                :grid="{ left: 40, right: 10, top: 10, bottom: 20 }"
                :chartStyle="{ height: '160px', width: '100%' }"
                :lineColors="['#E6A23C']"
              />
            </div>
            <div class="metric" :class="{ 'metric-alert': dev.alerts.pressure }">
              <div class="metric-head">
                <span>压力(MPa)</span>
                <el-tag :type="dev.alerts.pressure ? 'warning' : 'info'" size="small">{{ dev.alerts.pressure ? '越界' : '0.2~1.5' }}</el-tag>
              </div>
              <div class="metric-value">{{ currentValue(dev.series.pressure).toFixed(2) }}</div>
              <Echarts
                :xAxis="[{ type: 'category', data: xAxisLabels }]"
                :yAxis="[{ type: 'value', name: 'MPa' }]"
                :series="[{ type: 'line', smooth: true, showSymbol: false, data: dev.series.pressure }]"
                :tooltip="{ trigger: 'axis' }"
                :grid="{ left: 40, right: 10, top: 10, bottom: 20 }"
                :chartStyle="{ height: '160px', width: '100%' }"
                :lineColors="['#67C23A']"
              />
            </div>
          </div>
        </el-card>
      </el-col>
    </el-row>
  </div>
</template>
<script setup>
import { ref, reactive, onMounted, onBeforeUnmount, computed } from 'vue'
import { ElNotification } from 'element-plus'
import Echarts from '@/components/Echarts/echarts.vue'
defineOptions({ name: 'IoTMonitor' })
const windowSize = 30
const collecting = ref(true)
const lastUpdated = ref(Date.now())
const lastUpdatedDisplay = computed(() => new Date(lastUpdated.value).toLocaleTimeString())
const xAxisLabels = ref(Array.from({ length: windowSize }, (_, i) => i - (windowSize - 1)).map(n => `${n}s`))
function makeSeries(fill, decimals = 2) {
  return Array.from({ length: windowSize }, () => Number(fill.toFixed(decimals)))
}
const devices = reactive([
  {
    id: 'hydrocyclone-desander',
    name: '旋流除砂器',
    type: '分离设备',
    baseline: { vibration: 8 },
    initial: { temperature: 35, pressure: 0.85 },
    alerts: { vibration: false, temperature: false, pressure: false },
    hasAlert: false,
    series: {
      vibration: makeSeries(8),
      temperature: makeSeries(35, 1),
      pressure: makeSeries(0.85, 2),
    },
  },
  {
    id: 'high-pressure-separator',
    name: '高压分离器撬',
    type: '分离设备',
    baseline: { vibration: 6 },
    initial: { temperature: 45, pressure: 1.20 },
    alerts: { vibration: false, temperature: false, pressure: false },
    hasAlert: false,
    series: {
      vibration: makeSeries(6),
      temperature: makeSeries(45, 1),
      pressure: makeSeries(1.2, 2),
    },
  },
  {
    id: 'heating-throttle-pressure',
    name: '组合式加热节流调压',
    type: '调压设备',
    baseline: { vibration: 10 },
    initial: { temperature: 75, pressure: 1.80 },
    alerts: { vibration: false, temperature: false, pressure: false },
    hasAlert: false,
    series: {
      vibration: makeSeries(10),
      temperature: makeSeries(75, 1),
      pressure: makeSeries(1.8, 2),
    },
  },
  {
    id: 'three-phase-separator',
    name: '三相分离器',
    type: '分离设备',
    baseline: { vibration: 7 },
    initial: { temperature: 38, pressure: 0.95 },
    alerts: { vibration: false, temperature: false, pressure: false },
    hasAlert: false,
    series: {
      vibration: makeSeries(7),
      temperature: makeSeries(38, 1),
      pressure: makeSeries(0.95, 2),
    },
  },
])
function currentValue(arr) {
  return arr[arr.length - 1] ?? 0
}
function pushWindow(arr, val) {
  if (arr.length >= windowSize) arr.shift()
  arr.push(val)
}
function clamp(val, min, max) { return Math.max(min, Math.min(max, val)) }
function tickDevice(dev) {
  const vibBase = dev.baseline.vibration
  // æŒ¯åŠ¨ï¼šåŸºçº¿Â±2%随机波动;5%概率触发8%~12%尖峰模拟告警
  const spike = Math.random() < 0.05
  const vibNoise = vibBase * (spike ? (1 + (Math.random() * 0.08 + 0.04) * (Math.random() < 0.5 ? -1 : 1)) : (1 + (Math.random() - 0.5) * 0.04))
  const vibVal = Number(vibNoise.toFixed(2))
  pushWindow(dev.series.vibration, vibVal)
  // æ¸©åº¦ï¼šç¼“慢随机游走,并添加偶发高温偏移
  const tPrev = currentValue(dev.series.temperature)
  const tDrift = tPrev + (Math.random() - 0.5) * 0.8 + (Math.random() < 0.02 ? 6 : 0)
  const tVal = Number(clamp(tDrift, 15, 95).toFixed(1))
  pushWindow(dev.series.temperature, tVal)
  // åŽ‹åŠ›ï¼šå°å¹…æ³¢åŠ¨ï¼Œå¶å‘ä½ŽåŽ‹/高压
  const pPrev = currentValue(dev.series.pressure)
  const pDrift = pPrev + (Math.random() - 0.5) * 0.05 + (Math.random() < 0.02 ? (Math.random() < 0.5 ? -0.3 : 0.3) : 0)
  const pVal = Number(clamp(pDrift, 0.05, 2.0).toFixed(2))
  pushWindow(dev.series.pressure, pVal)
  // è¾¹ç¼˜è®¡ç®—阈值判断
  const vibDelta = Math.abs(vibVal - vibBase) / vibBase
  const vibAlert = vibDelta > 0.05
  const tAlert = tVal < 20 || tVal > 80
  const pAlert = pVal < 0.2 || pVal > 1.5
  const prevHasAlert = dev.hasAlert
  dev.alerts.vibration = vibAlert
  dev.alerts.temperature = tAlert
  dev.alerts.pressure = pAlert
  dev.hasAlert = vibAlert || tAlert || pAlert
  if (dev.hasAlert && !prevHasAlert) {
    const reasons = []
    if (vibAlert) reasons.push(`振动偏离±5% (当前 ${vibVal} / åŸºçº¿ ${vibBase})`)
    if (tAlert) reasons.push(`温度越界 (当前 ${tVal}°C, æœŸæœ› 20~80°C) `)
    if (pAlert) reasons.push(`压力越界 (当前 ${pVal}MPa, æœŸæœ› 0.2~1.5MPa) `)
    ElNotification({
      title: `${dev.name} å‘Šè­¦`,
      message: reasons.join(';'),
      type: vibAlert ? 'error' : 'warning',
      duration: 5000,
    })
  }
}
let timer = null
function start() {
  if (timer) return
  timer = setInterval(() => {
    if (!collecting.value) return
    devices.forEach(tickDevice)
    lastUpdated.value = Date.now()
  }, 10000)
}
function stop() {
  if (timer) {
    clearInterval(timer)
    timer = null
  }
}
function toggleCollecting() { collecting.value = !collecting.value }
function resetAll() {
  devices.forEach(dev => {
    dev.series.vibration = makeSeries(dev.baseline.vibration)
    const t0 = dev.initial?.temperature ?? 45
    const p0 = dev.initial?.pressure ?? 0.8
    dev.series.temperature = makeSeries(t0, 1)
    dev.series.pressure = makeSeries(p0, 2)
    dev.alerts.vibration = false
    dev.alerts.temperature = false
    dev.alerts.pressure = false
    dev.hasAlert = false
  })
  lastUpdated.value = Date.now()
}
onMounted(() => {
  start()
})
onBeforeUnmount(() => {
  stop()
})
</script>
<style lang="scss" scoped>
.iot-monitor {
  .header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 12px;
    .title { font-size: 18px; font-weight: 600; }
    .actions { display: flex; align-items: center; gap: 8px; }
    .ts { color: #909399; font-size: 12px; }
  }
  .rule-alert { margin-bottom: 12px; }
}
.device-card {
  margin-bottom: 16px;
  transition: border-color 0.2s ease, box-shadow 0.2s ease;
  &.is-alert { border-color: #F56C6C; box-shadow: 0 0 0 2px rgba(245,108,108,0.2) inset; }
  .card-header {
    display: flex; flex-direction: column; gap: 4px;
    .card-title { display: flex; align-items: center; gap: 8px; font-weight: 600; }
    .meta { color: #909399; font-size: 12px; }
  }
  .metrics {
    display: grid;
    grid-template-columns: 1fr;
    gap: 12px;
  }
}
.metric {
  border: 1px solid #ebeef5;
  border-radius: 6px;
  padding: 8px 8px 0 8px;
  &-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 4px; font-size: 13px; color: #606266; }
  &-value { font-size: 20px; font-weight: 600; margin: 2px 0 6px 0; }
}
.metric-alert {
  border-color: #F56C6C;
  background: #FFF6F6;
}
@media (min-width: 1200px) {
  .device-card .metrics { grid-template-columns: 1fr 1fr 1fr; }
}
</style>
src/views/equipmentManagement/ledger/Form.vue
@@ -8,7 +8,7 @@
      </el-col>
      <el-col :span="12">
        <el-form-item label="规格型号" prop="deviceModel">
          <el-input v-model="form.deviceModel" placeholder="请输入规格型号" />
          <el-input v-model="form.deviceModel" :disabled="(form.deviceModel != null && operationType === 'edit')" placeholder="请输入规格型号" />
        </el-form-item>
      </el-col>
      <el-col :span="12">
@@ -119,6 +119,7 @@
  name: "设备台账表单",
});
const formRef = ref(null);
const operationType = ref('');
const formRules = {
    deviceName: [{ required: true, trigger: "blur", message: "请输入" }],
    deviceModel: [{ required: true, trigger: "blur", message: "请输入" }],
@@ -144,6 +145,9 @@
});
const loadForm = async (id) => {
    if (id) {
        operationType.value = 'edit'
    }
  const { code, data } = await getLedgerById(id);
  if (code == 200) {
    form.deviceName = data.deviceName;
src/views/equipmentManagement/ledger/index.vue
@@ -76,6 +76,8 @@
          size: pagination.pageSize,
          total: pagination.total,
        }"
        :isShowSummary="true"
        :summaryMethod="summaryMethod"
        @selection-change="handleSelectionChange"
        @pagination="changePage"
      >
@@ -103,6 +105,8 @@
import dayjs from "dayjs";
import QRCode from "qrcode";
import { ref } from "vue";
import { summarizeTable } from "@/utils/summarizeTable";
import {Search} from "@element-plus/icons-vue";
defineOptions({
  name: "设备台账",
@@ -127,7 +131,12 @@
} = usePaginationApi(
  getLedgerPage,
  {
    searchText: undefined,
    deviceName: undefined,
    deviceModel: undefined,
    supplierName: undefined,
    unit: undefined,
    entryDateStart: undefined,
    entryDateEnd: undefined,
  },
  [
    {
@@ -227,6 +236,20 @@
    pagination.pageSize = limit;
  onCurrentChange(page);
};
// åˆè®¡æ–¹æ³•
const summaryMethod = (param) => {
  return summarizeTable(
    param,
    ['number', 'taxIncludingPriceTotal', 'unTaxIncludingPriceTotal', 'taxIncludingPriceUnit'],
    {
      number: { noDecimal: true },
      taxIncludingPriceTotal: { decimalPlaces: 2 },
      unTaxIncludingPriceTotal: { decimalPlaces: 2 }
    }
  );
};
const deleteRow = (id) => {
  ElMessageBox.confirm("此操作将永久删除该文件, æ˜¯å¦ç»§ç»­?", "提示", {
    confirmButtonText: "确定",
src/views/equipmentManagement/measurementEquipment/components/calibrationDia.vue
@@ -94,23 +94,23 @@
                        </el-form-item>
                    </el-col>
                </el-row>
                <el-row :gutter="30">
                    <el-col :span="24">
                        <el-form-item label="附件材料:" prop="remark">
                            <el-upload v-model:file-list="fileList" :action="upload.url" multiple ref="fileUpload" auto-upload
                                                 :headers="upload.headers" :before-upload="handleBeforeUpload" :on-error="handleUploadError"
                                                 :on-success="handleUploadSuccess" :on-remove="handleRemove">
                                <el-button type="primary" v-if="operationType !== 'view'">上传</el-button>
                                <template #tip v-if="operationType !== 'view'">
                                    <div class="el-upload__tip">
                                        æ–‡ä»¶æ ¼å¼æ”¯æŒ
                                        doc,docx,xls,xlsx,ppt,pptx,pdf,txt,xml,jpg,jpeg,png,gif,bmp,rar,zip,7z
                                    </div>
                                </template>
                            </el-upload>
                        </el-form-item>
                    </el-col>
                </el-row>
<!--                <el-row :gutter="30">-->
<!--                    <el-col :span="24">-->
<!--                        <el-form-item label="附件材料:" prop="remark">-->
<!--                            <el-upload v-model:file-list="fileList" :action="upload.url" multiple ref="fileUpload" auto-upload-->
<!--                                                 :headers="upload.headers" :before-upload="handleBeforeUpload" :on-error="handleUploadError"-->
<!--                                                 :on-success="handleUploadSuccess" :on-remove="handleRemove">-->
<!--                                <el-button type="primary" v-if="operationType !== 'view'">上传</el-button>-->
<!--                                <template #tip v-if="operationType !== 'view'">-->
<!--                                    <div class="el-upload__tip">-->
<!--                                        æ–‡ä»¶æ ¼å¼æ”¯æŒ-->
<!--                                        doc,docx,xls,xlsx,ppt,pptx,pdf,txt,xml,jpg,jpeg,png,gif,bmp,rar,zip,7z-->
<!--                                    </div>-->
<!--                                </template>-->
<!--                            </el-upload>-->
<!--                        </el-form-item>-->
<!--                    </el-col>-->
<!--                </el-row>-->
            </el-form>
            <template #footer>
                <div class="dialog-footer">
src/views/equipmentManagement/measurementEquipment/components/formDia.vue
@@ -90,23 +90,23 @@
                        </el-form-item>
                    </el-col>
                </el-row>
                <el-row :gutter="30">
                    <el-col :span="24">
                        <el-form-item label="附件材料:" prop="remark">
                            <el-upload v-model:file-list="fileList" :action="upload.url" multiple ref="fileUpload" auto-upload
                                                 :headers="upload.headers" :before-upload="handleBeforeUpload" :on-error="handleUploadError"
                                                 :on-success="handleUploadSuccess" :on-remove="handleRemove">
                                <el-button type="primary" v-if="operationType !== 'view'">上传</el-button>
                                <template #tip v-if="operationType !== 'view'">
                                    <div class="el-upload__tip">
                                        æ–‡ä»¶æ ¼å¼æ”¯æŒ
                                        doc,docx,xls,xlsx,ppt,pptx,pdf,txt,xml,jpg,jpeg,png,gif,bmp,rar,zip,7z
                                    </div>
                                </template>
                            </el-upload>
                        </el-form-item>
                    </el-col>
                </el-row>
<!--                <el-row :gutter="30">-->
<!--                    <el-col :span="24">-->
<!--                        <el-form-item label="附件材料:" prop="remark">-->
<!--                            <el-upload v-model:file-list="fileList" :action="upload.url" multiple ref="fileUpload" auto-upload-->
<!--                                                 :headers="upload.headers" :before-upload="handleBeforeUpload" :on-error="handleUploadError"-->
<!--                                                 :on-success="handleUploadSuccess" :on-remove="handleRemove">-->
<!--                                <el-button type="primary" v-if="operationType !== 'view'">上传</el-button>-->
<!--                                <template #tip v-if="operationType !== 'view'">-->
<!--                                    <div class="el-upload__tip">-->
<!--                                        æ–‡ä»¶æ ¼å¼æ”¯æŒ-->
<!--                                        doc,docx,xls,xlsx,ppt,pptx,pdf,txt,xml,jpg,jpeg,png,gif,bmp,rar,zip,7z-->
<!--                                    </div>-->
<!--                                </template>-->
<!--                            </el-upload>-->
<!--                        </el-form-item>-->
<!--                    </el-col>-->
<!--                </el-row>-->
            </el-form>
            <template #footer>
                <div class="dialog-footer">
src/views/equipmentManagement/measurementEquipment/filesDia.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,202 @@
<template>
  <div>
    <el-dialog
        v-model="dialogFormVisible"
        title="上传附件"
        width="50%"
        @close="closeDia"
    >
      <div style="margin-bottom: 10px;text-align: right">
        <el-upload
            v-model:file-list="fileList"
            class="upload-demo"
            :action="uploadUrl"
            :on-success="handleUploadSuccess"
            :on-error="handleUploadError"
            name="file"
            :show-file-list="false"
            :headers="headers"
            style="display: inline;margin-right: 10px"
        >
          <el-button type="primary">上传附件</el-button>
        </el-upload>
        <el-button type="danger" plain @click="handleDelete">删除</el-button>
      </div>
      <PIMTable
          rowKey="id"
          :column="tableColumn"
          :tableData="tableData"
          :tableLoading="tableLoading"
          :isSelection="true"
          @selection-change="handleSelectionChange"
          height="500"
      >
      </PIMTable>
            <pagination
                style="margin: 10px 0"
                v-show="total > 0"
                @pagination="paginationSearch"
                :total="total"
                :page="page.current"
                :limit="page.size"
            />
      <template #footer>
        <div class="dialog-footer">
          <el-button @click="closeDia">取消</el-button>
        </div>
      </template>
    </el-dialog>
    <filePreview ref="filePreviewRef" />
  </div>
</template>
<script setup>
import {ref} from "vue";
import {ElMessageBox} from "element-plus";
import {getToken} from "@/utils/auth.js";
import filePreview from '@/components/filePreview/index.vue'
import {
  fileAdd,
  fileDel,
  fileListPage
} from "@/api/financialManagement/revenueManagement.js";
import Pagination from "@/components/PIMTable/Pagination.vue";
const { proxy } = getCurrentInstance()
const emit = defineEmits(['close'])
const dialogFormVisible = ref(false);
const currentId = ref('')
const selectedRows = ref([]);
const filePreviewRef = ref()
const tableColumn = ref([
  {
    label: "文件名称",
    prop: "name",
  },
  {
    dataType: "action",
    label: "操作",
    align: "center",
    operation: [
      {
        name: "下载",
        type: "text",
        clickFun: (row) => {
          downLoadFile(row);
        },
      },
      {
        name: "预览",
        type: "text",
        clickFun: (row) => {
          lookFile(row);
        },
      }
    ],
  },
]);
const page = reactive({
    current: 1,
    size: 100,
});
const total = ref(0);
const tableData = ref([]);
const fileList = ref([]);
const tableLoading = ref(false);
const accountType = ref('')
const headers = ref({
  Authorization: "Bearer " + getToken(),
});
const uploadUrl = ref(import.meta.env.VITE_APP_BASE_API + "/file/upload"); // ä¸Šä¼ çš„图片服务器地址
// æ‰“开弹框
const openDialog = (row,type) => {
  accountType.value = type;
  dialogFormVisible.value = true;
  currentId.value = row.id;
  getList()
}
const paginationSearch = (obj) => {
    page.current = obj.page;
    page.size = obj.limit;
    getList();
};
const getList = () => {
  fileListPage({accountId: currentId.value,accountType:accountType.value, ...page}).then(res => {
    tableData.value = res.data.records;
        total.value = res.data.total;
  })
}
// è¡¨æ ¼é€‰æ‹©æ•°æ®
const handleSelectionChange = (selection) => {
  selectedRows.value = selection;
};
// å…³é—­å¼¹æ¡†
const closeDia = () => {
  dialogFormVisible.value = false;
  emit('close')
};
// ä¸Šä¼ æˆåŠŸå¤„ç†
function handleUploadSuccess(res, file) {
  // å¦‚果上传成功
  if (res.code == 200) {
    const fileRow = {}
    fileRow.name = res.data.originalName
    fileRow.url = res.data.tempPath
    uploadFile(fileRow)
  } else {
    proxy.$modal.msgError("文件上传失败");
  }
}
function uploadFile(file) {
  file.accountId = currentId.value;
  file.accountType = accountType.value;
  fileAdd(file).then(res => {
    proxy.$modal.msgSuccess("文件上传成功");
    getList()
  })
}
// ä¸Šä¼ å¤±è´¥å¤„理
function handleUploadError() {
  proxy.$modal.msgError("文件上传失败");
}
// ä¸‹è½½é™„ä»¶
const downLoadFile = (row) => {
  proxy.$download.name(row.url);
}
// åˆ é™¤
const handleDelete = () => {
  let ids = [];
  if (selectedRows.value.length > 0) {
    ids = selectedRows.value.map((item) => item.id);
  } else {
    proxy.$modal.msgWarning("请选择数据");
    return;
  }
  ElMessageBox.confirm("选中的内容将被删除,是否确认删除?", "导出", {
    confirmButtonText: "确认",
    cancelButtonText: "取消",
    type: "warning",
  }).then(() => {
    fileDel(ids).then((res) => {
      proxy.$modal.msgSuccess("删除成功");
      getList();
    });
  }).catch(() => {
    proxy.$modal.msg("已取消");
  });
};
// é¢„览附件
const lookFile = (row) => {
  filePreviewRef.value.open(row.url)
}
defineExpose({
  openDialog,
});
</script>
<style scoped>
</style>
src/views/equipmentManagement/measurementEquipment/index.vue
@@ -44,6 +44,7 @@
        </div>
        <form-dia ref="formDia" @close="handleQuery"></form-dia>
        <calibration-dia ref="calibrationDia" @close="handleQuery"></calibration-dia>
    <files-dia ref="filesDia"></files-dia>
    </div>
</template>
@@ -57,6 +58,7 @@
    measuringInstrumentDelete,
    measuringInstrumentListPage
} from "@/api/equipmentManagement/measurementEquipment.js";
import FilesDia from "./filesDia.vue";
const { proxy } = getCurrentInstance();
const userStore = useUserStore()
@@ -136,6 +138,7 @@
        dataType: "action",
        label: "操作",
        align: "center",
        width: '130',
        fixed: 'right',
        operation: [
            {
@@ -149,7 +152,7 @@
                name: "附件",
                type: "text",
                clickFun: (row) => {
                    openCalibrationDia("add", row);
          openFilesFormDia(row);
                },
            },
        ],
@@ -157,6 +160,7 @@
]);
const tableData = ref([]);
const tableLoading = ref(false);
const filesDia = ref()
const page = reactive({
    current: 1,
    size: 100,
@@ -164,6 +168,14 @@
});
const selectedRows = ref([]);
// æ‰“开附件弹框
const openFilesFormDia = (row) => {
  console.log(row)
  nextTick(() => {
    filesDia.value?.openDialog( row,'计量器具台账')
  })
};
// è¡¨æ ¼é€‰æ‹©æ•°æ®
const handleSelectionChange = (selection) => {
    selectedRows.value = selection;
src/views/equipmentManagement/repair/Modal/MaintainModal.vue
@@ -1,5 +1,5 @@
<template>
  <el-drawer v-model="visible" :title="modalOptions.title" direction="ltr">
  <el-dialog v-model="visible" :title="modalOptions.title" direction="ltr">
    <MaintainForm ref="maintainFormRef" />
    <template #footer>
            <el-button type="primary" @click="sendForm" :loading="loading">
@@ -7,7 +7,7 @@
            </el-button>
      <el-button @click="closeModal">{{ modalOptions.cancelText }}</el-button>
    </template>
  </el-drawer>
  </el-dialog>
</template>
<script setup>
src/views/equipmentManagement/repair/index.vue
@@ -139,6 +139,7 @@
import { ElMessageBox, ElMessage } from "element-plus";
import dayjs from "dayjs";
import MaintainModal from "./Modal/MaintainModal.vue";
import {Search} from "@element-plus/icons-vue";
defineOptions({
  name: "设备报修",
@@ -163,7 +164,12 @@
} = usePaginationApi(
  getRepairPage,
  {
    searchText: undefined,
    deviceName: undefined,
    deviceModel: undefined,
    remark: undefined,
    maintenanceName: undefined,
    repairTimeStr: undefined,
    maintenanceTimeStr: undefined,
  },
  [
    {
src/views/equipmentManagement/upkeep/Modal/MaintenanceModal.vue
@@ -1,5 +1,5 @@
<template>
  <el-drawer v-model="visible" :title="modalOptions.title" direction="ltr">
  <el-dialog v-model="visible" :title="modalOptions.title" direction="ltr">
    <MaintenanceForm ref="maintenanceFormRef" />
    <template #footer>
            <el-button type="primary" @click="sendForm" :loading="loading">
@@ -7,7 +7,7 @@
            </el-button>
      <el-button @click="closeModal">{{ modalOptions.cancelText }}</el-button>
    </template>
  </el-drawer>
  </el-dialog>
</template>
<script setup>
src/views/equipmentManagement/upkeep/index.vue
@@ -127,6 +127,7 @@
import MaintenanceModal from "./Modal/MaintenanceModal.vue";
import dayjs from "dayjs";
import { ElMessageBox, ElMessage } from "element-plus";
import {Search} from "@element-plus/icons-vue";
defineOptions({
  name: "设备保养",
@@ -154,7 +155,12 @@
  getTableData,
  resetFilters,
  onCurrentChange,
} = usePaginationApi(getUpkeepPage, {}, [
} = usePaginationApi(getUpkeepPage, {
  deviceName: undefined,
  maintenancePlanTime: undefined,
  maintenanceActuallyTime: undefined,
  maintenanceActuallyName: undefined,
}, [
  {
    label: "设备名称",
    align: "center",
src/views/financialManagement/expenseManagement/index.vue
@@ -1,7 +1,7 @@
<template>
  <div class="app-container">
    <el-form :model="filters" :inline="true">
      <el-form-item label="录入日期:">
      <el-form-item label="支出日期:">
        <el-date-picker v-model="filters.entryDate" value-format="YYYY-MM-DD" format="YYYY-MM-DD" type="daterange"
                        placeholder="请选择" clearable @change="changeDaterange" />
      </el-form-item>
src/views/financialManagement/revenueManagement/index.vue
@@ -1,7 +1,7 @@
<template>
  <div class="app-container">
    <el-form :model="filters" :inline="true">
      <el-form-item label="录入日期:">
      <el-form-item label="收入日期:">
        <el-date-picker v-model="filters.entryDate" value-format="YYYY-MM-DD" format="YYYY-MM-DD" type="daterange"
                        placeholder="请选择" clearable @change="changeDaterange" />
      </el-form-item>
src/views/index.vue
@@ -118,11 +118,11 @@
            <div class="main-panel">
                <div style="display: flex;justify-content: space-between;">
                    <div class="section-title">应收应付统计</div>
                    <el-radio-group v-model="radio1" size="large" @change="statisticsReceivable">
                        <el-radio-button label="按周" :value="1" />
                        <el-radio-button label="按月" :value="2" />
                        <el-radio-button label="按季度" :value="3" />
                    </el-radio-group>
<!--                    <el-radio-group v-model="radio1" size="large" @change="statisticsReceivable">-->
<!--                        <el-radio-button label="按周" :value="1" />-->
<!--                        <el-radio-button label="按月" :value="2" />-->
<!--                        <el-radio-button label="按季度" :value="3" />-->
<!--                    </el-radio-group>-->
                </div>
                <Echarts ref="chart"
                                 :color="barColors2"
@@ -138,23 +138,23 @@
        
        <!-- åº•部横向两栏 -->
        <div class="dashboard-row">
            <div class="main-panel">
                <div class="section-title">质量统计</div>
                <div class="quality-cards">
                    <div class="quality-card one">原材料已检测数 <span>{{qualityStatisticsObject.supplierNum}}ä»¶</span></div>
                    <div class="quality-card two">过程检验数量 <span>{{qualityStatisticsObject.processNum}}ä»¶</span></div>
                    <div class="quality-card three">出厂已检数量 <span>{{qualityStatisticsObject.factoryNum}}ä»¶</span></div>
                </div>
                <Echarts ref="chart"
                                 :chartStyle="chartStyle"
                                 :grid="grid"
                                 :legend="barLegend"
                                 :series="barSeries1"
                                 :tooltip="tooltip"
                                 :xAxis="xAxis1"
                                 :yAxis="yAxis1"
                                 style="height: 260px"></Echarts>
            </div>
<!--            <div class="main-panel">-->
<!--                <div class="section-title">质量统计</div>-->
<!--                <div class="quality-cards">-->
<!--                    <div class="quality-card one">原材料已检测数 <span>{{qualityStatisticsObject.supplierNum}}ä»¶</span></div>-->
<!--                    <div class="quality-card two">过程检验数量 <span>{{qualityStatisticsObject.processNum}}ä»¶</span></div>-->
<!--                    <div class="quality-card three">出厂已检数量 <span>{{qualityStatisticsObject.factoryNum}}ä»¶</span></div>-->
<!--                </div>-->
<!--                <Echarts ref="chart"-->
<!--                                 :chartStyle="chartStyle"-->
<!--                                 :grid="grid"-->
<!--                                 :legend="barLegend"-->
<!--                                 :series="barSeries1"-->
<!--                                 :tooltip="tooltip"-->
<!--                                 :xAxis="xAxis1"-->
<!--                                 :yAxis="yAxis1"-->
<!--                                 style="height: 260px"></Echarts>-->
<!--            </div>-->
            <div class="main-panel">
                <div class="section-title">回款与开票分析</div>
                <Echarts ref="chart" :chartStyle="chartStyle" :grid="grid" :legend="lineLegend" :series="lineSeries"
@@ -744,15 +744,18 @@
    color: #666;
    list-style: none;
    padding: 0;
    height: 190px;
    overflow-y: auto;
    width: 460px;
}
.line {
    position: relative;
    width: 250px;
    width: 230px;
}
.line::after {
    content: '';
    position: absolute;
    right: 12px;
    right: 2px;
    top: 0;
    bottom: 0;
    width: 1px;
src/views/inventoryManagement/dispatchLog/index.vue
@@ -33,13 +33,14 @@
        style="width: 100%"
        :summary-method="summarizeMainTable"
        height="calc(100vh - 18.5em)"
        stripe
      >
        <el-table-column align="center" type="selection" width="55" />
        <el-table-column align="center" label="序号" type="index" width="60" />
        <el-table-column
          label="出库日期"
          prop="createTime"
          min-width="250"
          min-width="130"
          show-overflow-tooltip
        />
        <el-table-column
@@ -75,13 +76,13 @@
        <el-table-column
          label="含税单价(元)"
          prop="taxInclusiveUnitPrice"
          width="100"
          width="200"
          show-overflow-tooltip
        />
        <el-table-column
          label="含税总价(元)"
          prop="taxInclusiveTotalPrice"
          width="100"
          width="200"
          show-overflow-tooltip
        />
        <el-table-column
src/views/inventoryManagement/issueManagement/index.vue
@@ -16,7 +16,7 @@
    <div class="table_list">
      <el-table :data="tableData" border v-loading="tableLoading" @selection-change="handleSelectionChange"
        :expand-row-keys="expandedRowKeys" :row-key="row => row.id" show-summary style="width: 100%"
        :summary-method="summarizeMainTable" height="calc(100vh - 18.5em)">
        :summary-method="summarizeMainTable" height="calc(100vh - 18.5em)" stripe>
        <el-table-column align="center" type="selection" width="55" />
        <el-table-column align="center" label="序号" type="index" width="60" />
        <el-table-column label="入库时间" prop="createTime" width="100" show-overflow-tooltip />
src/views/inventoryManagement/receiptManagement/index.vue
@@ -16,7 +16,7 @@
    <div class="table_list">
      <el-table :data="tableData" border v-loading="tableLoading" @selection-change="handleSelectionChange"
        :expand-row-keys="expandedRowKeys" :row-key="row => row.id" show-summary style="width: 100%"
        :summary-method="summarizeMainTable" height="calc(100vh - 18.5em)">
        :summary-method="summarizeMainTable" height="calc(100vh - 18.5em)" stripe>
        <el-table-column align="center" type="selection" width="55" />
        <el-table-column align="center" label="序号" type="index" width="60" />
        <el-table-column label="入库时间" prop="createTime" width="100" show-overflow-tooltip />
@@ -66,6 +66,7 @@
          border
          v-loading="loadingProducts"
          @selection-change="handleSelectionChange"
          stripe
        >
          <el-table-column align="center" type="selection" width="55" />
          <el-table-column
src/views/inventoryManagement/stockManagement/index.vue
@@ -16,7 +16,7 @@
    <div class="table_list">
      <el-table :data="tableData" border v-loading="tableLoading" @selection-change="handleSelectionChange"
        :expand-row-keys="expandedRowKeys" :row-key="row => row.id" show-summary style="width: 100%"
        :summary-method="summarizeMainTable" height="calc(100vh - 18.5em)">
        :summary-method="summarizeMainTable" height="calc(100vh - 18.5em)" stripe>
        <el-table-column align="center" type="selection" width="55" />
        <el-table-column align="center" label="序号" type="index" width="60" />
        <el-table-column label="库存日期" prop="createTime" width="100" show-overflow-tooltip />
src/views/inventoryManagement/stockWarning/index.vue
@@ -51,6 +51,7 @@
        @selection-change="handleSelectionChange"
        style="width: 100%"
        height="calc(100vh - 280px)"
        stripe
      >
        <el-table-column align="center" type="selection" width="55" />
        <el-table-column align="center" label="序号" type="index" width="60" />
src/views/login.vue
@@ -86,8 +86,8 @@
const { proxy } = getCurrentInstance()
const loginForm = ref({
  username: "admin",
  password: "admin123",
  username: "",
  password: "",
  rememberMe: false,
  currentFatoryId:'',
})
@@ -181,7 +181,7 @@
<style lang='scss' scoped>
.login {
  height: 100%;
  background-image: url("../assets/indexViews/JZYJView.png");
  background-image: url("../assets/images/login-background.png");
  background-size: cover;
  position: relative;
}
src/views/monitor/job/index.vue
@@ -87,7 +87,7 @@
         <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
      </el-row>
      <el-table v-loading="loading" :data="jobList" @selection-change="handleSelectionChange">
      <el-table v-loading="loading" :data="jobList" @selection-change="handleSelectionChange" stripe>
         <el-table-column type="selection" width="55" align="center" />
         <el-table-column label="任务编号" width="100" align="center" prop="jobId" />
         <el-table-column label="任务名称" align="center" prop="jobName" :show-overflow-tooltip="true" />
src/views/monitor/job/log.vue
@@ -96,7 +96,7 @@
         <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
      </el-row>
      <el-table v-loading="loading" :data="jobLogList" @selection-change="handleSelectionChange">
      <el-table v-loading="loading" :data="jobLogList" @selection-change="handleSelectionChange" stripe>
         <el-table-column type="selection" width="55" align="center" />
         <el-table-column label="日志编号" width="80" align="center" prop="jobLogId" />
         <el-table-column label="任务名称" align="center" prop="jobName" :show-overflow-tooltip="true" />
src/views/monitor/logininfor/index.vue
@@ -93,7 +93,7 @@
         <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
      </el-row>
      <el-table ref="logininforRef" v-loading="loading" :data="logininforList" @selection-change="handleSelectionChange" :default-sort="defaultSort" @sort-change="handleSortChange">
      <el-table ref="logininforRef" v-loading="loading" :data="logininforList" @selection-change="handleSelectionChange" :default-sort="defaultSort" @sort-change="handleSortChange" stripe>
         <el-table-column type="selection" width="55" align="center" />
         <el-table-column label="访问编号" align="center" prop="infoId" />
         <el-table-column label="用户名称" align="center" prop="userName" :show-overflow-tooltip="true" sortable="custom" :sort-orders="['descending', 'ascending']" />
src/views/monitor/online/index.vue
@@ -28,6 +28,7 @@
         v-loading="loading"
         :data="onlineList.slice((pageNum - 1) * pageSize, pageNum * pageSize)"
         style="width: 100%;"
         stripe
      >
         <el-table-column label="序号" width="50" type="index" align="center">
            <template #default="scope">
src/views/monitor/operlog/index.vue
@@ -107,7 +107,7 @@
         <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
      </el-row>
      <el-table ref="operlogRef" v-loading="loading" :data="operlogList" @selection-change="handleSelectionChange" :default-sort="defaultSort" @sort-change="handleSortChange">
      <el-table ref="operlogRef" v-loading="loading" :data="operlogList" @selection-change="handleSelectionChange" :default-sort="defaultSort" @sort-change="handleSortChange" stripe>
         <el-table-column type="selection" width="50" align="center" />
         <el-table-column label="日志编号" align="center" prop="operId" />
         <el-table-column label="系统模块" align="center" prop="title" :show-overflow-tooltip="true" />
src/views/personnelManagement/contractManagement/filesDia.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,202 @@
<template>
  <div>
    <el-dialog
        v-model="dialogFormVisible"
        title="上传附件"
        width="50%"
        @close="closeDia"
    >
      <div style="margin-bottom: 10px;text-align: right">
        <el-upload
            v-model:file-list="fileList"
            class="upload-demo"
            :action="uploadUrl"
            :on-success="handleUploadSuccess"
            :on-error="handleUploadError"
            name="file"
            :show-file-list="false"
            :headers="headers"
            style="display: inline;margin-right: 10px"
        >
          <el-button type="primary">上传附件</el-button>
        </el-upload>
        <el-button type="danger" plain @click="handleDelete">删除</el-button>
      </div>
      <PIMTable
          rowKey="id"
          :column="tableColumn"
          :tableData="tableData"
          :tableLoading="tableLoading"
          :isSelection="true"
          @selection-change="handleSelectionChange"
          height="500"
      >
      </PIMTable>
            <pagination
                style="margin: 10px 0"
                v-show="total > 0"
                @pagination="paginationSearch"
                :total="total"
                :page="page.current"
                :limit="page.size"
            />
      <template #footer>
        <div class="dialog-footer">
          <el-button @click="closeDia">取消</el-button>
        </div>
      </template>
    </el-dialog>
    <filePreview ref="filePreviewRef" />
  </div>
</template>
<script setup>
import {ref} from "vue";
import {ElMessageBox} from "element-plus";
import {getToken} from "@/utils/auth.js";
import filePreview from '@/components/filePreview/index.vue'
import {
  fileAdd,
  fileDel,
  fileListPage
} from "@/api/financialManagement/revenueManagement.js";
import Pagination from "@/components/PIMTable/Pagination.vue";
const { proxy } = getCurrentInstance()
const emit = defineEmits(['close'])
const dialogFormVisible = ref(false);
const currentId = ref('')
const selectedRows = ref([]);
const filePreviewRef = ref()
const tableColumn = ref([
  {
    label: "文件名称",
    prop: "name",
  },
  {
    dataType: "action",
    label: "操作",
    align: "center",
    operation: [
      {
        name: "下载",
        type: "text",
        clickFun: (row) => {
          downLoadFile(row);
        },
      },
      {
        name: "预览",
        type: "text",
        clickFun: (row) => {
          lookFile(row);
        },
      }
    ],
  },
]);
const page = reactive({
    current: 1,
    size: 100,
});
const total = ref(0);
const tableData = ref([]);
const fileList = ref([]);
const tableLoading = ref(false);
const accountType = ref('')
const headers = ref({
  Authorization: "Bearer " + getToken(),
});
const uploadUrl = ref(import.meta.env.VITE_APP_BASE_API + "/file/upload"); // ä¸Šä¼ çš„图片服务器地址
// æ‰“开弹框
const openDialog = (row,type) => {
  accountType.value = type;
  dialogFormVisible.value = true;
  currentId.value = row.id;
  getList()
}
const paginationSearch = (obj) => {
    page.current = obj.page;
    page.size = obj.limit;
    getList();
};
const getList = () => {
  fileListPage({accountId: currentId.value,accountType:accountType.value, ...page}).then(res => {
    tableData.value = res.data.records;
        total.value = res.data.total;
  })
}
// è¡¨æ ¼é€‰æ‹©æ•°æ®
const handleSelectionChange = (selection) => {
  selectedRows.value = selection;
};
// å…³é—­å¼¹æ¡†
const closeDia = () => {
  dialogFormVisible.value = false;
  emit('close')
};
// ä¸Šä¼ æˆåŠŸå¤„ç†
function handleUploadSuccess(res, file) {
  // å¦‚果上传成功
  if (res.code == 200) {
    const fileRow = {}
    fileRow.name = res.data.originalName
    fileRow.url = res.data.tempPath
    uploadFile(fileRow)
  } else {
    proxy.$modal.msgError("文件上传失败");
  }
}
function uploadFile(file) {
  file.accountId = currentId.value;
  file.accountType = accountType.value;
  fileAdd(file).then(res => {
    proxy.$modal.msgSuccess("文件上传成功");
    getList()
  })
}
// ä¸Šä¼ å¤±è´¥å¤„理
function handleUploadError() {
  proxy.$modal.msgError("文件上传失败");
}
// ä¸‹è½½é™„ä»¶
const downLoadFile = (row) => {
  proxy.$download.name(row.url);
}
// åˆ é™¤
const handleDelete = () => {
  let ids = [];
  if (selectedRows.value.length > 0) {
    ids = selectedRows.value.map((item) => item.id);
  } else {
    proxy.$modal.msgWarning("请选择数据");
    return;
  }
  ElMessageBox.confirm("选中的内容将被删除,是否确认删除?", "导出", {
    confirmButtonText: "确认",
    cancelButtonText: "取消",
    type: "warning",
  }).then(() => {
    fileDel(ids).then((res) => {
      proxy.$modal.msgSuccess("删除成功");
      getList();
    });
  }).catch(() => {
    proxy.$modal.msg("已取消");
  });
};
// é¢„览附件
const lookFile = (row) => {
  filePreviewRef.value.open(row.url)
}
defineExpose({
  openDialog,
});
</script>
<style scoped>
</style>
src/views/personnelManagement/contractManagement/index.vue
@@ -3,52 +3,81 @@
    <div class="search_form">
      <div>
        <span class="search_title">姓名:</span>
        <el-input
            v-model="searchForm.staffName"
            style="width: 240px"
            placeholder="请输入姓名搜索"
            @change="handleQuery"
            clearable
            :prefix-icon="Search"
        />
        <span  style="margin-left: 10px" class="search_title">合同结束日期:</span>
        <el-date-picker  v-model="searchForm.entryDate" value-format="YYYY-MM-DD" format="YYYY-MM-DD" type="daterange"
                         placeholder="请选择" clearable @change="changeDaterange" />
        <el-button type="primary" @click="handleQuery" style="margin-left: 10px"
        >搜索</el-button
        >
        <el-input v-model="searchForm.staffName" style="width: 240px" placeholder="请输入姓名搜索" @change="handleQuery"
          clearable :prefix-icon="Search" />
        <span style="margin-left: 10px" class="search_title">合同结束日期:</span>
        <el-date-picker v-model="searchForm.entryDate" value-format="YYYY-MM-DD" format="YYYY-MM-DD" type="daterange"
          placeholder="请选择" clearable @change="changeDaterange" />
        <el-button type="primary" @click="handleQuery" style="margin-left: 10px">搜索</el-button>
      </div>
      <div>
        <!--        <el-button type="primary" @click="openForm('add')">新增入职</el-button>-->
<!--        <el-button type="info" @click="handleImport">导入</el-button>-->
        <el-button @click="handleOut">导出</el-button>
        <!--        <el-button type="danger" plain @click="handleDelete">删除</el-button>-->
      </div>
    </div>
    <div class="table_list">
      <PIMTable
          rowKey="id"
          :column="tableColumn"
          :tableData="tableData"
          :page="page"
          :isSelection="true"
          @selection-change="handleSelectionChange"
          :tableLoading="tableLoading"
          @pagination="pagination"
          :total="page.total"
      ></PIMTable>
      <PIMTable rowKey="id" :column="tableColumn" :tableData="tableData" :page="page" :isSelection="true"
        @selection-change="handleSelectionChange" :tableLoading="tableLoading" @pagination="pagination"
        :total="page.total"></PIMTable>
    </div>
    <form-dia ref="formDia" @close="handleQuery"></form-dia>
    <!-- åˆåŒå¯¼å…¥å¯¹è¯æ¡† -->
    <el-dialog
      :title="upload.title"
      v-model="upload.open"
      width="400px"
      append-to-body
    >
      <el-upload
        ref="uploadRef"
        :limit="1"
        accept=".xlsx, .xls"
        :headers="upload.headers"
        :action="upload.url + '?updateSupport=' + upload.updateSupport"
        :disabled="upload.isUploading"
        :on-progress="handleFileUploadProgress"
        :on-success="handleFileSuccess"
        :auto-upload="false"
        drag
      >
        <el-icon class="el-icon--upload"><upload-filled /></el-icon>
        <div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
        <template #tip>
          <div class="el-upload__tip text-center">
            <span>仅允许导入xls、xlsx格式文件。</span>
            <!-- <el-link
              type="primary"
              :underline="false"
              style="font-size: 12px; vertical-align: baseline"
              @click="importTemplate"
              >下载模板</el-link
            > -->
          </div>
        </template>
      </el-upload>
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary" @click="submitFileForm">ç¡® å®š</el-button>
          <el-button @click="upload.open = false">取 æ¶ˆ</el-button>
        </div>
      </template>
    </el-dialog>
    <files-dia ref="filesDia"></files-dia>
  </div>
</template>
<script setup>
import { Search } from "@element-plus/icons-vue";
import {onMounted, ref} from "vue";
import { onMounted, ref } from "vue";
import FormDia from "@/views/personnelManagement/contractManagement/components/formDia.vue";
import {ElMessageBox} from "element-plus";
import {staffOnJobListPage} from "@/api/personnelManagement/employeeRecord.js";
import { ElMessageBox } from "element-plus";
import { staffOnJobListPage } from "@/api/personnelManagement/employeeRecord.js";
import dayjs from "dayjs";
import { getToken } from "@/utils/auth.js";
import FilesDia from "./filesDia.vue";
const data = reactive({
  searchForm: {
    staffName: "",
@@ -108,7 +137,7 @@
  {
    label: "家庭住址",
    prop: "adress",
    width:200
    width: 200
  },
  {
    label: "第一学历",
@@ -117,12 +146,12 @@
  {
    label: "专业",
    prop: "profession",
    width:100
    width: 100
  },
  {
    label: "身份证号",
    prop: "identityCard",
    width:200
    width: 200
  },
  {
    label: "年龄",
@@ -131,7 +160,7 @@
  {
    label: "联系电话",
    prop: "phone",
    width:150
    width: 150
  },
  {
    label: "紧急联系人",
@@ -141,7 +170,7 @@
  {
    label: "紧急联系人电话",
    prop: "emergencyContactPhone",
    width:150
    width: 150
  },
  {
    label: "合同年限",
@@ -162,6 +191,7 @@
    label: "操作",
    align: "center",
    fixed: 'right',
    width: 120,
    operation: [
      {
        name: "详情",
@@ -170,9 +200,17 @@
          openForm("edit", row);
        },
      },
      {
        name: "附件",
        type: "text",
        clickFun: (row) => {
          openFilesFormDia(row);
        },
      },
    ],
  },
]);
const filesDia = ref()
const tableData = ref([]);
const selectedRows = ref([]);
const tableLoading = ref(false);
@@ -192,6 +230,13 @@
    searchForm.value.entryDateEnd = dayjs(value[1]).format("YYYY-MM-DD");
  }
  getList();
};
// æ‰“开附件弹框
const openFilesFormDia = (row) => {
  console.log(row)
  nextTick(() => {
    filesDia.value?.openDialog( row,'合同')
  })
};
// æŸ¥è¯¢åˆ—表
/** æœç´¢æŒ‰é’®æ“ä½œ */
@@ -234,12 +279,47 @@
    cancelButtonText: "取消",
    type: "warning",
  })
      .then(() => {
        proxy.download("/staff/staffOnJob/export", {}, "合同管理.xlsx");
      })
      .catch(() => {
        proxy.$modal.msg("已取消");
      });
    .then(() => {
      proxy.download("/staff/staffOnJob/export", {}, "合同管理.xlsx");
    })
    .catch(() => {
      proxy.$modal.msg("已取消");
    });
};
const upload = reactive({
  // æ˜¯å¦æ˜¾ç¤ºå¼¹å‡ºå±‚(合同导入)
  open: false,
  // å¼¹å‡ºå±‚标题(合同导入)
  title: "",
  // æ˜¯å¦ç¦ç”¨ä¸Šä¼ 
  isUploading: false,
  // æ˜¯å¦æ›´æ–°å·²ç»å­˜åœ¨çš„用户数据
  updateSupport: 1,
  // è®¾ç½®ä¸Šä¼ çš„请求头部
  headers: { Authorization: "Bearer " + getToken() },
  // ä¸Šä¼ çš„地址
  url: import.meta.env.VITE_APP_BASE_API + "/staff/staffOnJob/import",
});
/** å¯¼å…¥æŒ‰é’®æ“ä½œ */
function handleImport() {
  upload.title = "合同导入";
  upload.open = true;
}
/** æäº¤ä¸Šä¼ æ–‡ä»¶ */
function submitFileForm() {
  console.log(upload.url + '?updateSupport=' + upload.updateSupport)
  proxy.$refs["uploadRef"].submit();
}
/**文件上传中处理 */
const handleFileUploadProgress = (event, file, fileList) => {
  upload.isUploading = true;
};
/** æ–‡ä»¶ä¸Šä¼ æˆåŠŸå¤„ç† */
const handleFileSuccess = (response, file, fileList) => {
  upload.open = false;
  upload.isUploading = false;
  proxy.$refs["uploadRef"].handleRemove(file);
  getList();
};
onMounted(() => {
  getList();
src/views/personnelManagement/onboarding/components/formDia.vue
@@ -124,6 +124,26 @@
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="试用期(月):" prop="probationPeriod">
              <el-input-number v-model="form.probationPeriod" :precision="0" :step="1" min="0" style="width: 100%"/>
            </el-form-item>
          </el-col>
          <!-- <el-col :span="12">
            <el-form-item label="入职日期:" prop="entryDate">
              <el-date-picker
                  v-model="form.entryDate"
                  type="date"
                  placeholder="请选择日期"
                  value-format="YYYY-MM-DD"
                  format="YYYY-MM-DD"
                  clearable
                  style="width: 100%"
              />
            </el-form-item>
          </el-col> -->
        </el-row>
      </el-form>
      <template #footer>
        <div class="dialog-footer">
@@ -136,7 +156,7 @@
</template>
<script setup>
import {ref} from "vue";
import {ref, reactive, toRefs} from "vue";
import {getStaffJoinInfo, staffJoinAdd, staffJoinUpdate} from "@/api/personnelManagement/onboarding.js";
const { proxy } = getCurrentInstance()
const emit = defineEmits(['close'])
@@ -162,6 +182,7 @@
    contractStartTime: "",
    contractEndTime: "",
    staffState: "",
    probationPeriod: 3, // é»˜è®¤è¯•用期3个月
  },
  rules: {
    staffNo: [{ required: true, message: "请输入", trigger: "blur" },],
src/views/personnelManagement/onboarding/index.vue
@@ -151,8 +151,39 @@
    width:150
  },
  {
    label: "合同年限",
    label: "试用期(月)",
    prop: "probationPeriod",
    width: 120,
  },
  // {
  //   label: "转正日期",
  //   prop: "probationEndDate",
  //   width: 120,
  //   formatData: (row) => {
  //     // ä¿®æ”¹ä¸ºä½¿ç”¨åˆåŒå¼€å§‹æ—¥æœŸè®¡ç®—转正日期
  //     if (row.contractStartTime && row.probationPeriod) {
  //       // è®¡ç®—转正日期(合同开始日期加上试用期月数)
  //       return dayjs(row.contractStartTime).add(row.probationPeriod, 'month').format('YYYY-MM-DD');
  //     }
  //     return '';
  //   },
  //   formatType: (row) => {
  //     // ä¿®æ”¹ä¸ºä½¿ç”¨åˆåŒå¼€å§‹æ—¥æœŸæ£€æŸ¥æ˜¯å¦ä¸´è¿‘转正(7天内)
  //     if (row.contractStartTime && row.probationPeriod) {
  //       const probationEndDate = dayjs(row.contractStartTime).add(row.probationPeriod, 'month');
  //       const daysUntilProbationEnd = probationEndDate.diff(dayjs(), 'day');
  //       if (daysUntilProbationEnd >= 0 && daysUntilProbationEnd <= 7) {
  //         return 'warning'; // ä½¿ç”¨è­¦å‘Šæ ·å¼æ ‡è®°ä¸´è¿‘转正的员工
  //       }
  //     }
  //     return '';
  //   }
  // },
  {
    label: "合同年限(年)",
    prop: "contractTerm",
    width: 120,
  },
  {
    label: "合同开始日期",
@@ -222,10 +253,43 @@
    tableLoading.value = false;
    tableData.value = res.data.records
    page.total = res.data.total;
    // æ£€æŸ¥æ˜¯å¦æœ‰ä¸´è¿‘转正的员工并提醒
    checkProbationEnding(tableData.value);
  }).catch(err => {
    tableLoading.value = false;
  })
};
// æ£€æŸ¥ä¸´è¿‘转正的员工并提醒
const checkProbationEnding = (data) => {
  const probationEndingSoon = [];
  data.forEach(item => {
    // ä¿®æ”¹ä¸ºä½¿ç”¨åˆåŒå¼€å§‹æ—¥æœŸæ£€æŸ¥
    if (item.contractStartTime && item.probationPeriod) {
      const probationEndDate = dayjs(item.contractStartTime).add(item.probationPeriod, 'month');
      const daysUntilProbationEnd = probationEndDate.diff(dayjs(), 'day');
      if (daysUntilProbationEnd >= 0 && daysUntilProbationEnd <= 7) {
        probationEndingSoon.push({
          staffName: item.staffName,
          probationEndDate: probationEndDate.format('YYYY-MM-DD'),
          daysLeft: daysUntilProbationEnd
        });
      }
    }
  });
  if (probationEndingSoon.length > 0) {
    let message = '以下员工将在7天内转正:\n';
    probationEndingSoon.forEach(item => {
      message += `${item.staffName}(${item.probationEndDate},还有${item.daysLeft}天)\n`;
    });
    // æ˜¾ç¤ºæé†’消息
    proxy.$modal.msgInfo(message);
  }
};
// è¡¨æ ¼é€‰æ‹©æ•°æ®
const handleSelectionChange = (selection) => {
  selectedRows.value = selection;
src/views/procurementManagement/invoiceEntry/index.vue
@@ -165,7 +165,7 @@
      },
    },
    {
      label: "已开票金额(元)",
      label: "已来票金额(元)",
      prop: "receiptPaymentAmount",
      width:200,
      formatData: (val) => {
@@ -173,7 +173,7 @@
      },
    },
    {
      label: "待开票金额(元)",
      label: "待来票金额(元)",
      prop: "unReceiptPaymentAmount",
      width:200,
      formatData: (val) => {
src/views/procurementManagement/invoiceEntry/indexOld.vue
@@ -37,6 +37,7 @@
        :summary-method="summarizeMainTable"
        @expand-change="expandChange"
        height="calc(100vh - 18.5em)"
        stripe
      >
        <el-table-column align="center" type="selection" width="55" />
        <el-table-column type="expand">
@@ -46,6 +47,7 @@
              border
              show-summary
              :summary-method="summarizeChildrenTable"
              stripe
            >
              <el-table-column
                align="center"
@@ -278,6 +280,7 @@
          :data="productData"
          border
          @selection-change="productSelected"
          stripe
          show-summary
          style="width: 100%"
          :summary-method="summarizeChildrenTable"
src/views/procurementManagement/paymentEntry/index.vue
@@ -61,6 +61,7 @@
                        show-summary
                        v-loading="childrenLoading"
                        :summary-method="summarizeMainTable2"
                        stripe
                    >
                        <el-table-column
                            align="center"
@@ -221,31 +222,31 @@
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="登记人:" prop="registrant">
              <el-input
                v-model="form.registrant"
                placeholder="请输入"
                clearable
                disabled
              />
            </el-form-item>
          </el-col>
                    <el-col :span="12">
                        <el-form-item label="付款日期:" prop="paymentDate">
                            <el-date-picker
                                style="width: 100%"
                                v-model="form.paymentDate"
                                value-format="YYYY-MM-DD"
                                format="YYYY-MM-DD"
                                type="date"
                                placeholder="请选择"
                                clearable
                            />
                        </el-form-item>
                    </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="付款日期:" prop="paymentDate">
              <el-date-picker
                style="width: 100%"
                v-model="form.paymentDate"
                value-format="YYYY-MM-DD"
                format="YYYY-MM-DD"
                type="date"
                placeholder="请选择"
                clearable
              />
            </el-form-item>
          </el-col>
                    <el-col :span="12">
                        <el-form-item label="登记人:" prop="registrant">
                            <el-input
                                v-model="form.registrant"
                                placeholder="请输入"
                                clearable
                                disabled
                            />
                        </el-form-item>
                    </el-col>
          <el-col :span="12">
            <el-form-item label="登记日期:" prop="registrationtDate">
              <el-input
src/views/procurementManagement/paymentHistory/index.vue
@@ -63,6 +63,10 @@
const isShowSummarySon = ref(true);
const tableColumn = ref([
  {
    label: "采购合同号",
    prop: "purchaseContractNumber",
  },
  {
    label: "付款日期",
    prop: "paymentDate",
  },
src/views/procurementManagement/paymentLedger/index.vue
@@ -29,6 +29,7 @@
            height="calc(100vh - 18.5em)"
            :highlight-current-row="true"
            style="width: 100%"
            stripe
            tooltip-effect="dark"
            @row-click="rowClick"
            :show-summary="isShowSummary"
src/views/procurementManagement/procurementInvoiceLedger/Form/EditForm.vue
@@ -53,7 +53,7 @@
defineOptions({
  name: "来票台账表单",
});
const temFutureTickets = ref(0)
const { form, resetForm } = useFormData({
  id: undefined,
  purchaseContractNumber: undefined, // é‡‡è´­åˆåŒå·
@@ -77,6 +77,7 @@
    form.ticketsAmount = data.ticketsAmount.toFixed(2);
    form.taxInclusiveUnitPrice = data.taxInclusiveUnitPrice;
    form.futureTickets = data.futureTickets;
    temFutureTickets.value = data.futureTickets;
  }
};
@@ -86,16 +87,14 @@
        proxy.$modal.msgWarning("含税单价不能为零或未定义");
        return;
    }
    if (Number(form.ticketsNum) > Number(form.futureTickets)) {
    if (Number(form.ticketsNum) > Number(temFutureTickets.value)) {
        proxy.$modal.msgWarning("开票数不得大于未开票数");
        form.ticketsNum = form.futureTickets
        return;
        form.ticketsNum = temFutureTickets.value
    }
    
    // ç¡®ä¿æ‰€æœ‰æ•°å€¼éƒ½è½¬æ¢ä¸ºæ•°å­—类型进行计算
    const ticketsAmount = Number(val) * Number(form.taxInclusiveUnitPrice);
    const futureTickets = Number(form.futureTickets) - Number(val);
    const ticketsAmount = Number(form.ticketsNum) * Number(form.taxInclusiveUnitPrice);
    const futureTickets = Number(temFutureTickets.value) - Number(form.ticketsNum);
    form.futureTickets = Number(futureTickets.toFixed(2));
    form.ticketsAmount = Number(ticketsAmount.toFixed(2));
};
src/views/procurementManagement/procurementInvoiceLedger/index.vue
@@ -162,14 +162,19 @@
      width: 150,
    },
    {
      label: "客户名称",
      prop: "customerName",
      label: "项目名称",
      prop: "projectName",
      width: 240,
    },
    {
      label: "供应商名称",
      prop: "supplierName",
      width: 240,
    },
    {
      label: "产品大类",
      prop: "productCategory",
      width: 150,
    },
    {
      label: "规格型号",
@@ -190,12 +195,17 @@
      },
    },
    {
      label: "开票日期",
      label: "本次来票数",
      prop: "ticketsNum",
      width: 110,
    },
    {
      label: "来票日期",
      prop: "createdAt",
      width: 110,
    },
    {
      label: "开票金额",
      label: "来票金额(元)",
      prop: "ticketsAmount",
      width: 200,
      formatData: (cell) => {
src/views/procurementManagement/procurementInvoiceLedger/indexOld.vue
@@ -52,6 +52,7 @@
        :summary-method="summarizeMainTable"
        @expand-change="expandChange"
        height="calc(100vh - 18.5em)"
        stripe
      >
        <el-table-column align="center" label="序号" type="index" width="55" />
        <el-table-column type="expand">
@@ -61,6 +62,7 @@
              border
              show-summary
              :summary-method="summarizeChildrenTable"
              stripe
            >
              <el-table-column
                align="center"
src/views/procurementManagement/procurementLedger/index.vue
@@ -39,6 +39,7 @@
    <div class="table_list">
      <div style="display: flex;justify-content: flex-end;margin-bottom: 20px;">
        <el-button type="primary" @click="openForm('add')">新增台账</el-button>
        <el-button type="success" @click="openScanAddDialog">扫码新增</el-button>
        <el-button @click="handleOut">导出</el-button>
        <el-button type="danger" plain @click="handleDelete">删除</el-button>
      </div>
@@ -53,6 +54,7 @@
        :summary-method="summarizeMainTable"
        @expand-change="expandChange"
        height="calc(100vh - 18.5em)"
        stripe
      >
        <el-table-column align="center" type="selection" width="55" />
        <el-table-column type="expand">
@@ -62,6 +64,7 @@
              border
              show-summary
              :summary-method="summarizeChildrenTable"
              stripe
            >
              <el-table-column
                align="center"
@@ -145,7 +148,7 @@
        <el-table-column
          fixed="right"
          label="操作"
          min-width="60"
          min-width="150"
          align="center"
        >
          <template #default="scope">
@@ -157,6 +160,14 @@
                            :disabled="scope.row.receiptPaymentAmount>0 || scope.row.recorderName !== userStore.nickName"
              >编辑</el-button
            >
            <el-button
              link
              type="success"
              size="small"
              @click="showQRCode(scope.row)"
              >生成二维码</el-button
            >
          </template>
        </el-table-column>
      </el-table>
@@ -296,6 +307,7 @@
          border
          @selection-change="productSelected"
          show-summary
          stripe
          :summary-method="summarizeProTable"
        >
          <el-table-column align="center" type="selection" width="55" />
@@ -539,13 +551,228 @@
        </div>
      </template>
    </el-dialog>
    <!-- äºŒç»´ç æ˜¾ç¤ºå¯¹è¯æ¡† -->
    <el-dialog
      v-model="qrCodeDialogVisible"
      title="采购合同号二维码"
      width="400px"
      center
    >
      <div style="text-align: center;">
        <img :src="qrCodeUrl" alt="二维码" style="width:200px;height:200px;" />
        <div style="margin: 20px;">
          <el-button type="primary" @click="downloadQRCode">下载二维码图片</el-button>
        </div>
      </div>
    </el-dialog>
    <!-- æ‰«ç æ–°å¢žå¯¹è¯æ¡† -->
    <el-dialog
      v-model="scanAddDialogVisible"
      title="扫码新增采购台账"
      width="70%"
      @close="closeScanAddDialog"
    >
      <el-form
        :model="scanAddForm"
        label-width="140px"
        label-position="top"
        :rules="scanAddRules"
        ref="scanAddFormRef"
      >
        <el-row :gutter="20">
          <el-col :span="24">
            <el-form-item label="扫码内容:">
              <el-input
                v-model="scanAddForm.scanContent"
                type="textarea"
                :rows="3"
                placeholder="请扫描二维码或手动输入采购合同信息"
                @input="parseScanContent"
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="采购合同号:" prop="purchaseContractNumber">
              <el-input
                v-model="scanAddForm.purchaseContractNumber"
                placeholder="请输入"
                clearable
              />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="供应商名称:" prop="supplierName">
              <el-input
                v-model="scanAddForm.supplierName"
                placeholder="请输入"
                clearable
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="项目名称:" prop="projectName">
              <el-input
                v-model="scanAddForm.projectName"
                placeholder="请输入"
                clearable
              />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="合同金额(元):" prop="contractAmount">
              <el-input-number
                v-model="scanAddForm.contractAmount"
                :precision="2"
                :step="0.1"
                clearable
                style="width: 100%"
                placeholder="请输入"
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="付款方式:">
              <el-input
                v-model="scanAddForm.paymentMethod"
                placeholder="请输入"
                clearable
              />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="录入人:">
              <el-input v-model="scanAddForm.recorderName" disabled />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="24">
            <el-form-item label="备注:">
              <el-input
                v-model="scanAddForm.remark"
                type="textarea"
                :rows="2"
                placeholder="请输入备注信息"
                clearable
              />
            </el-form-item>
          </el-col>
        </el-row>
      </el-form>
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary" @click="submitScanAdd">确认新增</el-button>
          <el-button @click="closeScanAddDialog">取消</el-button>
        </div>
      </template>
    </el-dialog>
    <!-- æ‰«ç ç™»è®°å¯¹è¯æ¡† -->
    <el-dialog
      v-model="scanDialogVisible"
      title="扫码登记"
      width="60%"
      @close="closeScanDialog"
    >
      <el-form
        :model="scanForm"
        label-width="120px"
        label-position="left"
        :rules="scanRules"
        ref="scanFormRef"
      >
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="采购合同号:">
              <el-input v-model="scanForm.purchaseContractNumber" disabled />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="供应商名称:">
              <el-input v-model="scanForm.supplierName" disabled />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="项目名称:">
              <el-input v-model="scanForm.projectName" disabled />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="扫码时间:">
              <el-input v-model="scanForm.scanTime" disabled />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="扫码人:">
              <el-input v-model="scanForm.scannerName" disabled />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="扫码状态:">
              <el-tag :type="scanForm.scanStatus === '已扫码' ? 'success' : 'warning'">
                {{ scanForm.scanStatus }}
              </el-tag>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="24">
            <el-form-item label="扫码备注:">
              <el-input
                v-model="scanForm.scanRemark"
                type="textarea"
                :rows="3"
                placeholder="请输入扫码备注信息"
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="24">
            <el-form-item label="扫码记录:">
              <el-table :data="scanRecords" border style="width: 100%" stripe>
                <el-table-column label="序号" type="index" width="60" align="center" />
                <el-table-column label="扫码时间" prop="scanTime" width="180" />
                <el-table-column label="扫码人" prop="scannerName" width="120" />
                <el-table-column label="扫码状态" prop="scanStatus" width="100">
                  <template #default="scope">
                    <el-tag :type="scope.row.scanStatus === '已扫码' ? 'success' : 'warning'">
                      {{ scope.row.scanStatus }}
                    </el-tag>
                  </template>
                </el-table-column>
                <el-table-column label="备注" prop="scanRemark" />
              </el-table>
            </el-form-item>
          </el-col>
        </el-row>
      </el-form>
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary" @click="submitScan">确认扫码</el-button>
          <el-button @click="closeScanDialog">取消</el-button>
        </div>
      </template>
    </el-dialog>
  </div>
</template>
<script setup>
import { getToken } from "@/utils/auth";
import pagination from "@/components/PIMTable/Pagination.vue";
import { ref, onMounted } from "vue";
import { ref, onMounted, reactive, toRefs, getCurrentInstance, nextTick } from "vue";
import { Search } from "@element-plus/icons-vue";
import { ElMessageBox } from "element-plus";
import { userListNoPage } from "@/api/system/user.js";
@@ -567,6 +794,7 @@
  createPurchaseNo,
} from "@/api/procurementManagement/procurementLedger.js";
import useFormData from "@/hooks/useFormData.js";
import QRCode from "qrcode";
const { proxy } = getCurrentInstance();
const tableData = ref([]);
const productData = ref([]);
@@ -589,6 +817,10 @@
import dayjs from "dayjs";
const userStore = useUserStore();
// äºŒç»´ç ç›¸å…³å˜é‡
const qrCodeDialogVisible = ref(false);
const qrCodeUrl = ref("");
// ç”¨æˆ·ä¿¡æ¯è¡¨å•弹框数据
const operationType = ref("");
@@ -812,8 +1044,8 @@
// ä¸Šä¼ å‰æ ¡æ£€
function handleBeforeUpload(file) {
  // æ ¡æ£€æ–‡ä»¶å¤§å°
  if (file.size > 1024 * 1024 * 10) {
    proxy.$modal.msgError("上传文件大小不能超过10MB!");
  if (file.size > 1024 * 1024 * 50) {
    proxy.$modal.msgError("上传文件大小不能超过50MB!");
    return false;
  }
  proxy.$modal.loading("正在上传文件,请稍候...");
@@ -1152,6 +1384,194 @@
  }
};
// æ˜¾ç¤ºäºŒç»´ç 
const showQRCode = async (row) => {
  try {
    // æž„建二维码内容,只包含采购合同号(纯文本)
    const qrContent = row.purchaseContractNumber || '';
    // æ£€æŸ¥å†…容是否为空
    if (!qrContent || qrContent.trim() === '') {
      proxy.$modal.msgWarning("该行没有采购合同号,无法生成二维码");
      return;
    }
    qrCodeUrl.value = await QRCode.toDataURL(qrContent, {
      width: 200,
      margin: 2,
      color: {
        dark: '#000000',
        light: '#FFFFFF'
      }
    });
    qrCodeDialogVisible.value = true;
  } catch (error) {
    console.error('生成二维码失败:', error);
    proxy.$modal.msgError("生成二维码失败:" + error.message);
  }
};
// ä¸‹è½½äºŒç»´ç 
const downloadQRCode = () => {
  if (!qrCodeUrl.value) {
    proxy.$modal.msgWarning("二维码未生成");
    return;
  }
  const a = document.createElement('a');
  a.href = qrCodeUrl.value;
  a.download = `采购合同号二维码_${new Date().getTime()}.png`;
  document.body.appendChild(a);
  a.click();
  document.body.removeChild(a);
  proxy.$modal.msgSuccess("下载成功");
};
// æ‰«ç æ–°å¢žå¯¹è¯æ¡†ç›¸å…³å˜é‡
const scanAddDialogVisible = ref(false);
const scanAddForm = reactive({
  scanContent: "",
  purchaseContractNumber: "",
  supplierName: "",
  projectName: "",
  contractAmount: "",
  paymentMethod: "",
  recorderName: "",
  scanRemark: "",
});
const scanAddRules = {
  purchaseContractNumber: [{ required: true, message: "请输入采购合同号", trigger: "blur" }],
  supplierName: [{ required: true, message: "请输入供应商名称", trigger: "blur" }],
  projectName: [{ required: true, message: "请输入项目名称", trigger: "blur" }],
};
// æ‰«ç ç™»è®°å¯¹è¯æ¡†ç›¸å…³å˜é‡
const scanDialogVisible = ref(false);
const scanForm = reactive({
  purchaseContractNumber: "",
  supplierName: "",
  projectName: "",
  scanTime: "",
  scannerName: "",
  scanStatus: "未扫码",
  scanRemark: "",
});
const scanRules = {
  scanRemark: [{ required: true, message: "请输入扫码备注", trigger: "blur" }],
};
const scanRecords = ref([]);
// æ‰“开扫码新增对话框
const openScanAddDialog = () => {
  scanAddForm.scanContent = "";
  scanAddForm.purchaseContractNumber = "";
  scanAddForm.supplierName = "";
  scanAddForm.projectName = "";
  scanAddForm.contractAmount = "";
  scanAddForm.paymentMethod = "";
  scanAddForm.recorderName = userStore.nickName;
  scanAddForm.scanRemark = "";
  scanAddDialogVisible.value = true;
};
// è§£æžæ‰«ç å†…容(模拟解析二维码数据)
const parseScanContent = (content) => {
  if (!content) return;
  // æ¨¡æ‹Ÿè§£æžäºŒç»´ç å†…容,这里可以根据实际需求调整解析逻辑
  // å‡è®¾æ‰«ç å†…容格式为:合同号|供应商|项目|金额|付款方式
  const parts = content.split('|');
  if (parts.length >= 3) {
    scanAddForm.purchaseContractNumber = parts[0] || "";
    scanAddForm.supplierName = parts[1] || "";
    scanAddForm.projectName = parts[2] || "";
    scanAddForm.contractAmount = parts[3] || "";
    scanAddForm.paymentMethod = parts[4] || "";
  }
};
// å…³é—­æ‰«ç æ–°å¢žå¯¹è¯æ¡†
const closeScanAddDialog = () => {
  scanAddDialogVisible.value = false;
  proxy.resetForm("scanAddFormRef");
};
// æäº¤æ‰«ç æ–°å¢ž
const submitScanAdd = () => {
  proxy.$refs["scanAddFormRef"].validate((valid) => {
    if (valid) {
      // æž„建新增数据
      const newData = {
        purchaseContractNumber: scanAddForm.purchaseContractNumber,
        supplierName: scanAddForm.supplierName,
        projectName: scanAddForm.projectName,
        contractAmount: scanAddForm.contractAmount,
        paymentMethod: scanAddForm.paymentMethod,
        recorderName: scanAddForm.recorderName,
        entryDate: getCurrentDate(),
        remark: scanAddForm.scanRemark,
        type: 2
      };
      // æ¨¡æ‹Ÿæ–°å¢žæˆåŠŸ
      proxy.$modal.msgSuccess("扫码新增成功!");
      closeScanAddDialog();
      // å¯ä»¥é€‰æ‹©æ˜¯å¦åˆ·æ–°åˆ—表
      // getList();
    }
  });
};
// æ‰“开扫码登记对话框
const openScanDialog = (row) => {
  scanForm.purchaseContractNumber = row.purchaseContractNumber;
  scanForm.supplierName = row.supplierName;
  scanForm.projectName = row.projectName;
  scanForm.scanTime = getCurrentDateTime();
  scanForm.scannerName = userStore.nickName;
  scanForm.scanStatus = "未扫码";
  scanForm.scanRemark = "";
  scanRecords.value = [];
  scanDialogVisible.value = true;
};
// å…³é—­æ‰«ç ç™»è®°å¯¹è¯æ¡†
const closeScanDialog = () => {
  scanDialogVisible.value = false;
  proxy.resetForm("scanFormRef");
};
// æäº¤æ‰«ç ç™»è®°
const submitScan = () => {
  proxy.$refs["scanFormRef"].validate((valid) => {
    if (valid) {
      // æ·»åŠ æ‰«ç è®°å½•
      scanRecords.value.push({
        ...scanForm,
        id: Date.now(), // æ¨¡æ‹ŸID
        scanTime: getCurrentDateTime(),
      });
      scanForm.scanStatus = "已扫码";
      scanForm.scanRemark = scanForm.scanRemark || "无";
      proxy.$modal.msgSuccess("扫码登记成功!");
      closeScanDialog();
    }
  });
};
// èŽ·å–å½“å‰æ—¥æœŸæ—¶é—´
function getCurrentDateTime() {
  const now = new Date();
  const year = now.getFullYear();
  const month = String(now.getMonth() + 1).padStart(2, "0");
  const day = String(now.getDate()).padStart(2, "0");
  const hours = String(now.getHours()).padStart(2, "0");
  const minutes = String(now.getMinutes()).padStart(2, "0");
  const seconds = String(now.getSeconds()).padStart(2, "0");
  return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
}
onMounted(() => {
  getList();
});
src/views/productionManagement/operationScheduling/components/formDia.vue
@@ -8,7 +8,7 @@
    >
      <el-button type="primary" @click="addRow" style="margin-bottom: 10px;">新增</el-button>
            <span style="font-size: 18px;margin-left: 10px">待排产数量:{{pendingNum}}</span>
      <el-table :data="tableData" border style="width: 100%" :summary-method="summarizeMainTable" show-summary :row-key="row => row.id">
      <el-table :data="tableData" border style="width: 100%" :summary-method="summarizeMainTable" show-summary :row-key="row => row.id" stripe>
        <el-table-column label="序号" width="60">
          <template #default="scope">
            {{ scope.$index + 1 }}
src/views/productionManagement/productionReporting/index.vue
@@ -50,6 +50,7 @@
                        :data="expandData"
                        border
                        show-summary
                        stripe
                        :summary-method="summarizeMainTable"
                        v-loading="childrenLoading"
                    >
src/views/productionManagement/safetyMonitoring/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,873 @@
<template>
    <div class="safety-monitoring">
        <el-row :gutter="20">
            <!-- å·¦ä¾§ï¼šå®žæ—¶ç›‘控区域 -->
            <el-col :span="16">
                <el-card class="monitoring-card">
                    <div slot="header" class="card-header">
                        <span>实时气体浓度监控</span>
                        <el-tag :type="systemStatus === 'normal' ? 'success' : 'danger'">
                            {{ systemStatus === 'normal' ? '系统正常' : '系统告警' }}
                        </el-tag>
                    </div>
                    <!-- å‚¨ç½åŒºç›‘控 -->
                    <div class="monitoring-section">
                        <h3>储罐区监控</h3>
                        <div class="sensor-grid">
                            <div class="sensor-item" v-for="sensor in tankSensors" :key="sensor.id">
                                <div class="sensor-header">
                                    <span>{{ sensor.name }}</span>
                                    <el-tag :type="sensor.status === 'normal' ? 'success' : 'danger'" size="small">
                                        {{ sensor.status === 'normal' ? '正常' : '超标' }}
                                    </el-tag>
                                </div>
                                <div class="sensor-data">
                                    <div class="data-item">
                                        <span>甲烷: {{ sensor.methane.toFixed(2) }}%</span>
                                        <el-progress
                                            :percentage="Math.min(Math.round(sensor.methane * 40 * 100) / 100, 100)"
                                            :color="getProgressColor(Math.min(Math.round(sensor.methane * 40 * 100) / 100, 100), 80)"
                                            :format="formatProgress"
                                            :stroke-width="8"
                                        />
                                    </div>
                                    <div class="data-item">
                                        <span>硫化氢: {{ sensor.h2s.toFixed(2) }}ppm</span>
                                        <el-progress
                                            :percentage="Math.min(Math.round((sensor.h2s / 20) * 100 * 100) / 100, 100)"
                                            :color="getProgressColor(Math.min(Math.round((sensor.h2s / 20) * 100 * 100) / 100, 100), 80)"
                                            :format="formatProgress"
                                            :stroke-width="8"
                                        />
                                    </div>
                                </div>
                            </div>
                        </div>
                    </div>
                    <!-- äº•口压缩机监控 -->
                    <div class="monitoring-section">
                        <h3>井口压缩机监控</h3>
                        <div class="sensor-grid">
                            <div class="sensor-item" v-for="sensor in compressorSensors" :key="sensor.id">
                                <div class="sensor-header">
                                    <span>{{ sensor.name }}</span>
                                    <el-tag :type="sensor.status === 'normal' ? 'success' : 'danger'" size="small">
                                        {{ sensor.status === 'normal' ? '正常' : '超标' }}
                                    </el-tag>
                                </div>
                                <div class="sensor-data">
                                    <div class="data-item">
                                        <span>甲烷: {{ sensor.methane.toFixed(2) }}%</span>
                                        <el-progress
                                            :percentage="Math.min(Math.round(sensor.methane * 40 * 100) / 100, 100)"
                                            :color="getProgressColor(sensor.methane, 2.5)"
                                            :format="formatProgress"
                                            :stroke-width="8"
                                        />
                                    </div>
                                    <div class="data-item">
                                        <span>硫化氢: {{ sensor.h2s.toFixed(2) }}ppm</span>
                                        <el-progress
                                            :percentage="Math.min(Math.round((sensor.h2s / 20) * 100 * 100) / 100, 100)"
                                            :color="getProgressColor(sensor.h2s, 10)"
                                            :format="formatProgress"
                                            :stroke-width="8"
                                        />
                                    </div>
                                </div>
                            </div>
                        </div>
                    </div>
                    <!-- å®žæ—¶æ›²çº¿å›¾ -->
                    <div class="chart-section">
                        <h3>实时浓度曲线</h3>
                        <div class="chart-container">
                            <div ref="chart" class="chart"></div>
                        </div>
                    </div>
                </el-card>
            </el-col>
            <!-- å³ä¾§ï¼šæŽ§åˆ¶é¢æ¿ -->
            <el-col :span="8">
                <el-card class="control-card">
                    <div slot="header" class="card-header">
                        <span>应急控制面板</span>
                    </div>
                    <!-- å–·æ·‹çŠ¶æ€ -->
                    <div class="control-section">
                        <h4>喷淋系统状态</h4>
                        <div class="status-grid">
                            <div class="status-item" v-for="sprinkler in sprinklerSystems" :key="sprinkler.id">
                                <div class="status-indicator" :class="sprinkler.status">
                                    <i class="el-icon-circle-check" v-if="sprinkler.status === 'active'"></i>
                                    <i class="el-icon-circle-close" v-else></i>
                                </div>
                                <span>{{ sprinkler.name }}</span>
                                <el-tag :type="sprinkler.status === 'active' ? 'success' : 'info'" size="small">
                                    {{ sprinkler.status === 'active' ? '运行中' : '待机' }}
                                </el-tag>
                            </div>
                        </div>
                    </div>
                    <!-- åº”急记录按钮 -->
                    <h4>应急管理</h4>
                    <div class="control-section1">
                        <el-button type="primary" @click="showEmergencyRecords" style="margin-bottom: 10px;">
                            åº”急记录
                        </el-button>
                        <el-button type="warning" @click="triggerEmergency" :disabled="!hasEmergency">
                            è§¦å‘应急响应
                        </el-button>
                    </div>
                    <!-- ç³»ç»Ÿæ—¥å¿— -->
                    <div class="control-section">
                        <h4>系统日志</h4>
                        <div class="log-container">
                            <div class="log-item" v-for="log in systemLogs" :key="log.id">
                                <span class="log-time">{{ log.time }}</span>
                                <span class="log-content">{{ log.content }}</span>
                            </div>
                        </div>
                    </div>
                </el-card>
            </el-col>
        </el-row>
        <!-- æ³„漏预警弹窗 -->
        <el-dialog
            title="⚠️ æ³„漏预警"
            :visible.sync="leakWarningVisible"
            width="500px"
            :close-on-click-modal="false"
            :close-on-press-escape="false"
            class="leak-warning-dialog"
        >
            <div class="warning-content">
                <div class="warning-icon">
                    <i class="el-icon-warning"></i>
                </div>
                <div class="warning-text">
                    <h3>检测到气体浓度超标!</h3>
                    <p>位置:{{ currentWarning.location }}</p>
                    <p>超标气体:{{ currentWarning.gas }}</p>
                    <p>当前浓度:{{ currentWarning.value }}</p>
                </div>
            </div>
            <div slot="footer" class="dialog-footer">
                <el-button type="danger" @click="acknowledgeWarning">确认告警</el-button>
                <el-button type="primary" @click="viewDetails">查看详情</el-button>
            </div>
        </el-dialog>
        <!-- åº”急记录弹窗 -->
        <el-dialog
            title="应急记录"
            :visible.sync="emergencyRecordsVisible"
            width="800px"
        >
            <el-table :data="emergencyRecords" style="width: 100%" stripe>
                <el-table-column prop="time" label="时间" width="180"></el-table-column>
                <el-table-column prop="location" label="位置" width="150"></el-table-column>
                <el-table-column prop="type" label="类型" width="120"></el-table-column>
                <el-table-column prop="status" label="状态" width="100">
                    <template slot-scope="scope">
                        <el-tag :type="scope.row.status === 'resolved' ? 'success' : 'warning'">
                            {{ scope.row.status === 'resolved' ? '已解决' : '处理中' }}
                        </el-tag>
                    </template>
                </el-table-column>
                <el-table-column prop="description" label="描述"></el-table-column>
                <el-table-column label="操作" width="120">
                    <template slot-scope="scope">
                        <el-button type="text" @click="viewBlockchainDetails(scope.row)">
                            åŒºå—链详情
                        </el-button>
                    </template>
                </el-table-column>
            </el-table>
        </el-dialog>
        <!-- åŒºå—链存证详情弹窗 -->
        <el-dialog
            title="区块链存证详情"
            :visible.sync="blockchainDetailsVisible"
            width="900px"
        >
            <div class="blockchain-details">
                <el-descriptions :column="2" border>
                    <el-descriptions-item label="事件ID">{{ currentEvent.id }}</el-descriptions-item>
                    <el-descriptions-item label="时间戳">{{ currentEvent.timestamp }}</el-descriptions-item>
                    <el-descriptions-item label="位置">{{ currentEvent.location }}</el-descriptions-item>
                    <el-descriptions-item label="事件类型">{{ currentEvent.type }}</el-descriptions-item>
                </el-descriptions>
                <div class="sensor-data-section">
                    <h4>传感器数据</h4>
                    <el-table :data="currentEvent.sensorData" style="width: 100%" stripe>
                        <el-table-column prop="sensor" label="传感器"></el-table-column>
                        <el-table-column prop="methane" label="甲烷浓度"></el-table-column>
                        <el-table-column prop="h2s" label="硫化氢浓度"></el-table-column>
                        <el-table-column prop="timestamp" label="记录时间"></el-table-column>
                    </el-table>
                </div>
                <div class="action-log-section">
                    <h4>处置动作记录</h4>
                    <el-timeline>
                        <el-timeline-item
                            v-for="action in currentEvent.actions"
                            :key="action.id"
                            :timestamp="action.timestamp"
                            :type="action.type === 'emergency' ? 'danger' : 'primary'"
                        >
                            {{ action.description }}
                        </el-timeline-item>
                    </el-timeline>
                </div>
                <div class="blockchain-info">
                    <h4>区块链信息</h4>
                    <el-descriptions :column="1" border>
                        <el-descriptions-item label="区块哈希">{{ currentEvent.blockHash }}</el-descriptions-item>
                        <el-descriptions-item label="交易哈希">{{ currentEvent.txHash }}</el-descriptions-item>
                        <el-descriptions-item label="确认数">{{ currentEvent.confirmations }}</el-descriptions-item>
                    </el-descriptions>
                </div>
            </div>
        </el-dialog>
    </div>
</template>
<script>
import * as echarts from 'echarts'
export default {
    name: 'SafetyMonitoring',
    data() {
        return {
            systemStatus: 'normal',
            leakWarningVisible: false,
            emergencyRecordsVisible: false,
            blockchainDetailsVisible: false,
            currentWarning: {},
            currentEvent: {},
            hasEmergency: false,
            // å‚¨ç½åŒºä¼ æ„Ÿå™¨æ•°æ®
            tankSensors: [
                { id: 1, name: '储罐T-001', methane: 1.20, h2s: 2.10, status: 'normal' },
                { id: 2, name: '储罐T-002', methane: 0.80, h2s: 1.50, status: 'normal' },
                { id: 3, name: '储罐T-003', methane: 3.20, h2s: 8.50, status: 'warning' },
                { id: 4, name: '储罐T-004', methane: 0.60, h2s: 0.80, status: 'normal' }
            ],
            // äº•口压缩机传感器数据
            compressorSensors: [
                { id: 5, name: '压缩机C-001', methane: 2.10, h2s: 3.20, status: 'normal' },
                { id: 6, name: '压缩机C-002', methane: 4.80, h2s: 12.50, status: 'warning' },
                { id: 7, name: '压缩机C-003', methane: 1.80, h2s: 2.80, status: 'normal' }
            ],
            // å–·æ·‹ç³»ç»ŸçŠ¶æ€
            sprinklerSystems: [
                { id: 1, name: '储罐区喷淋', status: 'active' },
                { id: 2, name: '压缩机区喷淋', status: 'standby' },
                { id: 3, name: '紧急喷淋', status: 'standby' }
            ],
            // ç³»ç»Ÿæ—¥å¿—
            systemLogs: [
                { id: 1, time: '14:30:25', content: '系统启动完成,所有传感器正常' },
                { id: 2, time: '14:35:12', content: '储罐T-003甲烷浓度超标,触发预警' },
                { id: 3, time: '14:35:15', content: '启动储罐区喷淋系统' },
                { id: 4, time: '14:35:20', content: '发送紧急疏散广播' }
            ],
            // åº”急记录
            emergencyRecords: [
                {
                    id: 'EM001',
                    time: '2024-01-15 14:35:12',
                    location: '储罐T-003',
                    type: '甲烷超标',
                    status: 'resolved',
                    description: '储罐T-003甲烷浓度达到3.2%,超过安全阈值2.5%'
                },
                {
                    id: 'EM002',
                    time: '2024-01-15 14:35:15',
                    location: '压缩机C-002',
                    type: '硫化氢超标',
                    status: 'processing',
                    description: '压缩机C-002硫化氢浓度达到12.5ppm,超过安全阈值10ppm'
                }
            ],
            // å›¾è¡¨å®žä¾‹
            chart: null,
            // å®šæ—¶å™¨
            timer: null
        }
    },
    mounted() {
        this.initChart()
        this.startDataRefresh()
        this.checkEmergencyStatus()
    },
    beforeDestroy() {
        if (this.timer) {
            clearInterval(this.timer)
        }
        if (this.chart) {
            this.chart.dispose()
        }
    },
    methods: {
        // ç»Ÿä¸€è¿›åº¦æ¡æ ¼å¼åŒ–为两位小数,避免浮点误差显示
        formatProgress(percentage) {
            if (percentage == null || isNaN(percentage)) return '0.00%'
            const val = Math.round(Number(percentage) * 100) / 100
            return `${val.toFixed(2)}%`
        },
        // åˆå§‹åŒ–图表
        initChart() {
            this.chart = echarts.init(this.$refs.chart)
            this.updateChart()
        },
        // æ›´æ–°å›¾è¡¨æ•°æ®
        updateChart() {
            const option = {
                title: {
                    text: '实时气体浓度监控',
                    left: 'center'
                },
                tooltip: {
                    trigger: 'axis',
                    axisPointer: {
                        type: 'cross'
                    }
                },
                legend: {
                    data: ['储罐区甲烷', '储罐区硫化氢', '压缩机甲烷', '压缩机硫化氢'],
                    top: 30
                },
                grid: {
                    left: '3%',
                    right: '4%',
                    bottom: '3%',
                    top: '15%',
                    containLabel: true
                },
                xAxis: {
                    type: 'category',
                    data: this.generateTimeData()
                },
                yAxis: [
                    {
                        type: 'value',
                        name: '甲烷浓度(%)',
                        position: 'left'
                    },
                    {
                        type: 'value',
                        name: '硫化氢浓度(ppm)',
                        position: 'right'
                    }
                ],
                series: [
                    {
                        name: '储罐区甲烷',
                        type: 'line',
                        data: this.generateRandomData(20, 0.5, 3.5),
                        smooth: true,
                        yAxisIndex: 0
                    },
                    {
                        name: '储罐区硫化氢',
                        type: 'line',
                        data: this.generateRandomData(20, 0.5, 12),
                        smooth: true,
                        yAxisIndex: 1
                    },
                    {
                        name: '压缩机甲烷',
                        type: 'line',
                        data: this.generateRandomData(20, 1.0, 5.0),
                        smooth: true,
                        yAxisIndex: 0
                    },
                    {
                        name: '压缩机硫化氢',
                        type: 'line',
                        data: this.generateRandomData(20, 1.0, 15),
                        smooth: true,
                        yAxisIndex: 1
                    }
                ]
            }
            this.chart.setOption(option)
        },
        // ç”Ÿæˆæ—¶é—´æ•°æ®
        generateTimeData() {
            const times = []
            const now = new Date()
            for (let i = 19; i >= 0; i--) {
                const time = new Date(now.getTime() - i * 5 * 60 * 1000)
                times.push(time.toLocaleTimeString('zh-CN', { hour12: false }))
            }
            return times
        },
        // ç”Ÿæˆéšæœºæ•°æ®
        generateRandomData(count, min, max) {
            const data = []
            for (let i = 0; i < count; i++) {
                data.push(+(Math.random() * (max - min) + min).toFixed(2))
            }
            return data
        },
        // å¼€å§‹æ•°æ®åˆ·æ–°
        startDataRefresh() {
            this.timer = setInterval(() => {
                this.refreshSensorData()
                this.updateChart()
                this.checkEmergencyStatus()
            }, 5000) // æ¯5秒刷新一次
        },
        // åˆ·æ–°ä¼ æ„Ÿå™¨æ•°æ®
        refreshSensorData() {
            // æ›´æ–°å‚¨ç½åŒºä¼ æ„Ÿå™¨æ•°æ®
            this.tankSensors.forEach(sensor => {
                sensor.methane = +(Math.random() * 4).toFixed(2)
                sensor.h2s = +(Math.random() * 15).toFixed(2)
                sensor.status = this.getSensorStatus(sensor.methane, sensor.h2s)
            })
            // æ›´æ–°åŽ‹ç¼©æœºä¼ æ„Ÿå™¨æ•°æ®
            this.compressorSensors.forEach(sensor => {
                sensor.methane = +(Math.random() * 6).toFixed(2)
                sensor.h2s = +(Math.random() * 20).toFixed(2)
                sensor.status = this.getSensorStatus(sensor.methane, sensor.h2s)
            })
            // æ£€æŸ¥æ˜¯å¦éœ€è¦è§¦å‘预警
            this.checkLeakWarning()
        },
        // èŽ·å–ä¼ æ„Ÿå™¨çŠ¶æ€
        getSensorStatus(methane, h2s) {
            const methanePct = Math.min(Math.round(methane * 40 * 100) / 100, 100)
            const h2sPct = Math.min(Math.round((h2s / 20) * 100 * 100) / 100, 100)
            if (methanePct >= 80 || h2sPct >= 80) {
                return 'warning'
            }
            return 'normal'
        },
        // æ£€æŸ¥æ³„漏预警
        checkLeakWarning() {
            const allSensors = [...this.tankSensors, ...this.compressorSensors]
            const warningSensor = allSensors.find(sensor => this.getSensorStatus(sensor.methane, sensor.h2s) === 'warning')
            if (warningSensor && !this.leakWarningVisible) {
                this.triggerLeakWarning(warningSensor)
            }
        },
        // è§¦å‘泄漏预警
        triggerLeakWarning(sensor) {
            const methanePct = Math.min(Math.round(sensor.methane * 40 * 100) / 100, 100)
            const h2sPct = Math.min(Math.round((sensor.h2s / 20) * 100 * 100) / 100, 100)
            const isMethaneMajor = methanePct >= h2sPct
            const overGas = isMethaneMajor ? '甲烷' : '硫化氢'
            const percent = (isMethaneMajor ? methanePct : h2sPct).toFixed(2)
            this.currentWarning = {
                location: sensor.name,
                gas: overGas,
                value: `${percent}%`
            }
            this.leakWarningVisible = true
            this.hasEmergency = true
            // è‡ªåŠ¨è§¦å‘åº”æ€¥å“åº”
            this.autoEmergencyResponse(sensor)
            // æ·»åŠ ç³»ç»Ÿæ—¥å¿—
            this.addSystemLog(`检测到${sensor.name}气体浓度超标,触发泄漏预警`)
        },
        // è‡ªåŠ¨åº”æ€¥å“åº”
        autoEmergencyResponse(sensor) {
            // å¯åŠ¨å–·æ·‹ç³»ç»Ÿ
            if (sensor.name.includes('储罐')) {
                this.sprinklerSystems[0].status = 'active'
            } else if (sensor.name.includes('压缩机')) {
                this.sprinklerSystems[1].status = 'active'
            }
            // æ·»åŠ ç³»ç»Ÿæ—¥å¿—
            this.addSystemLog(`启动${sensor.name}区域喷淋系统`)
            this.addSystemLog(`发送紧急疏散广播`)
            // åˆ›å»ºåº”急记录
            this.createEmergencyRecord(sensor)
        },
        // æ·»åŠ ç³»ç»Ÿæ—¥å¿—
        addSystemLog(content) {
            const now = new Date()
            const time = now.toLocaleTimeString('zh-CN', { hour12: false })
            this.systemLogs.unshift({
                id: Date.now(),
                time: time,
                content: content
            })
            // ä¿æŒæœ€å¤š20条日志
            if (this.systemLogs.length > 20) {
                this.systemLogs = this.systemLogs.slice(0, 20)
            }
        },
        // åˆ›å»ºåº”急记录
        createEmergencyRecord(sensor) {
            const now = new Date()
            const record = {
                id: `EM${Date.now()}`,
                time: now.toLocaleString('zh-CN'),
                location: sensor.name,
                type: sensor.methane > 2.5 ? '甲烷超标' : '硫化氢超标',
                status: 'processing',
                description: `${sensor.name}检测到${sensor.methane > 2.5 ? '甲烷' : '硫化氢'}浓度超标`
            }
            this.emergencyRecords.unshift(record)
        },
        // èŽ·å–è¿›åº¦æ¡é¢œè‰²
        getProgressColor(value, threshold) {
            if (value > threshold) {
                return '#F56C6C'
            } else if (value > threshold * 0.8) {
                return '#E6A23C'
            }
            return '#67C23A'
        },
        // æ£€æŸ¥åº”急状态
        checkEmergencyStatus() {
            const allSensors = [...this.tankSensors, ...this.compressorSensors]
            const has = allSensors.some(sensor => this.getSensorStatus(sensor.methane, sensor.h2s) === 'warning')
            this.hasEmergency = has
            this.systemStatus = has ? 'warning' : 'normal'
        },
        // ç¡®è®¤å‘Šè­¦
        acknowledgeWarning() {
            this.leakWarningVisible = false
            this.addSystemLog('泄漏预警已确认')
        },
        // æŸ¥çœ‹è¯¦æƒ…
        viewDetails() {
            this.leakWarningVisible = false
            // è¿™é‡Œå¯ä»¥è·³è½¬åˆ°è¯¦ç»†é¡µé¢æˆ–显示更多信息
        },
        // æ˜¾ç¤ºåº”急记录
        showEmergencyRecords() {
            this.emergencyRecordsVisible = true
        },
        // æŸ¥çœ‹åŒºå—链详情
        viewBlockchainDetails(record) {
            this.currentEvent = {
                id: record.id,
                timestamp: record.time,
                location: record.location,
                type: record.type,
                sensorData: [
                    {
                        sensor: '甲烷传感器',
                        methane: '3.2%',
                        h2s: '8.5ppm',
                        timestamp: record.time
                    },
                    {
                        sensor: '硫化氢传感器',
                        methane: '2.8%',
                        h2s: '12.5ppm',
                        timestamp: record.time
                    }
                ],
                actions: [
                    {
                        id: 1,
                        timestamp: record.time,
                        type: 'emergency',
                        description: '检测到气体浓度超标,触发预警'
                    },
                    {
                        id: 2,
                        timestamp: new Date(new Date(record.time).getTime() + 3000).toLocaleString('zh-CN'),
                        type: 'action',
                        description: '启动喷淋系统降温'
                    },
                    {
                        id: 3,
                        timestamp: new Date(new Date(record.time).getTime() + 5000).toLocaleString('zh-CN'),
                        type: 'action',
                        description: '发送紧急疏散广播'
                    }
                ],
                blockHash: '0x1234567890abcdef...',
                txHash: '0xabcdef1234567890...',
                confirmations: 12
            }
            this.emergencyRecordsVisible = false
            this.blockchainDetailsVisible = true
        },
        // è§¦å‘应急响应
        triggerEmergency() {
            this.$message.success('应急响应已触发')
            this.addSystemLog('手动触发应急响应')
        }
    }
}
</script>
<style scoped>
.safety-monitoring {
    padding: 20px;
    background-color: #f5f7fa;
    min-height: calc(100vh - 84px);
}
.monitoring-card, .control-card {
    margin-bottom: 20px;
}
.card-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
}
.monitoring-section {
    margin-bottom: 30px;
}
.monitoring-section h3 {
    color: #303133;
    margin-bottom: 15px;
    padding-bottom: 8px;
    border-bottom: 2px solid #409EFF;
}
.sensor-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
    gap: 15px;
}
.sensor-item {
    background: #fff;
    border: 1px solid #e4e7ed;
    border-radius: 8px;
    padding: 15px;
    box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.sensor-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 15px;
    font-weight: bold;
}
.sensor-data .data-item {
    margin-bottom: 12px;
}
.sensor-data .data-item span {
    display: block;
    margin-bottom: 5px;
    font-size: 14px;
    color: #606266;
}
.chart-section {
    margin-top: 30px;
}
.chart-section h3 {
    color: #303133;
    margin-bottom: 15px;
    padding-bottom: 8px;
    border-bottom: 2px solid #409EFF;
}
.chart-container {
    background: #fff;
    border-radius: 8px;
    padding: 20px;
}
.chart {
    width: 100%;
    height: 400px;
}
.control-section {
    margin-bottom: 25px;
}
.control-section1 {
    display: flex;
}
.control-section h4 {
    color: #303133;
    margin-bottom: 15px;
    font-size: 16px;
}
.status-grid {
    display: grid;
    gap: 10px;
}
.status-item {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 10px;
    background: #f8f9fa;
    border-radius: 6px;
}
.status-indicator {
    width: 20px;
    height: 20px;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
}
.status-indicator.active {
    color: #67C23A;
}
.status-indicator.standby {
    color: #909399;
}
.log-container {
    max-height: 200px;
    overflow-y: auto;
    background: #f8f9fa;
    border-radius: 6px;
    padding: 10px;
}
.log-item {
    display: flex;
    gap: 10px;
    margin-bottom: 8px;
    font-size: 12px;
}
.log-time {
    color: #909399;
    min-width: 60px;
}
.log-content {
    color: #606266;
}
/* æ³„漏预警弹窗样式 */
.leak-warning-dialog {
    background: #fff5f5;
}
.warning-content {
    text-align: center;
    padding: 20px 0;
}
.warning-icon {
    font-size: 60px;
    color: #F56C6C;
    margin-bottom: 20px;
}
.warning-text h3 {
    color: #F56C6C;
    margin-bottom: 15px;
}
.warning-text p {
    margin: 8px 0;
    color: #606266;
}
/* åŒºå—链详情样式 */
.blockchain-details {
    padding: 20px 0;
}
.sensor-data-section, .action-log-section, .blockchain-info {
    margin-top: 25px;
}
.sensor-data-section h4, .action-log-section h4, .blockchain-info h4 {
    color: #303133;
    margin-bottom: 15px;
    padding-bottom: 8px;
    border-bottom: 1px solid #e4e7ed;
}
/* å“åº”式设计 */
@media (max-width: 1200px) {
    .sensor-grid {
        grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
    }
}
@media (max-width: 768px) {
    .safety-monitoring {
        padding: 10px;
    }
    .sensor-grid {
        grid-template-columns: 1fr;
    }
    .chart {
        height: 300px;
    }
}
</style>
src/views/qualityManagement/finalInspection/components/formDia.vue
@@ -58,8 +58,10 @@
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="检验员:" prop="checkName">
              <el-input v-model="form.checkName" placeholder="请输入" clearable/>
                            <el-select v-model="form.checkName" placeholder="请选择" clearable>
                                <el-option v-for="item in userList" :key="item.nickName" :label="item.nickName"
                                                     :value="item.nickName"/>
                            </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="12">
@@ -77,6 +79,17 @@
          </el-col>
        </el-row>
      </el-form>
            <PIMTable
                rowKey="id"
                :column="tableColumn"
                :tableData="tableData"
                :tableLoading="tableLoading"
                height="400"
            >
                <template #slot="{ row }">
                    <el-input v-model="row.testValue" clearable/>
                </template>
            </PIMTable>
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary" @click="submitForm">确认</el-button>
@@ -92,6 +105,9 @@
import {getOptions} from "@/api/procurementManagement/procurementLedger.js";
import {productTreeList} from "@/api/basicData/product.js";
import {qualityInspectAdd, qualityInspectUpdate} from "@/api/qualityManagement/rawMaterialInspection.js";
import {userListNoPage} from "@/api/system/user.js";
import {qualityInspectDetailByProductId} from "@/api/qualityManagement/metricMaintenance.js";
import {qualityInspectParamInfo} from "@/api/qualityManagement/qualityInspectParam.js";
const { proxy } = getCurrentInstance()
const emit = defineEmits(['close'])
@@ -125,17 +141,50 @@
const { form, rules } = toRefs(data);
const supplierList = ref([]);
const productOptions = ref([]);
const tableColumn = ref([
    {
        label: "指标",
        prop: "parameterItem",
    },
    {
        label: "单位",
        prop: "unit",
    },
    {
        label: "标准值",
        prop: "standardValue",
    },
    {
        label: "内控值",
        prop: "controlValue",
    },
    {
        label: "检验值",
        prop: "testValue",
        dataType: 'slot',
        slot: 'slot',
    },
]);
const tableData = ref([]);
const tableLoading = ref(false);
const userList = ref([]);
const currentProductId = ref(0);
// æ‰“开弹框
const openDialog = (type, row) => {
const openDialog = async (type, row) => {
  operationType.value = type;
  dialogFormVisible.value = true;
  getOptions().then((res) => {
    supplierList.value = res.data;
  });
    let userLists = await userListNoPage();
    userList.value = userLists.data;
    form.value = {}
  getProductOptions();
  if (operationType.value === 'edit') {
    form.value = {...row}
        currentProductId.value = row.productId || 0
        getQualityInspectParamList(row.id)
  }
}
const getProductOptions = () => {
@@ -144,7 +193,11 @@
  });
};
const getModels = (value) => {
    currentProductId.value = value
  form.value.productName = findNodeById(productOptions.value, value);
    if (currentProductId) {
        getList();
    }
};
const findNodeById = (nodes, productId) => {
  for (let i = 0; i < nodes.length; i++) {
@@ -179,13 +232,19 @@
  proxy.$refs.formRef.validate(valid => {
    if (valid) {
      form.value.inspectType = 2
            if (operationType.value === "add") {
                tableData.value.forEach((item) => {
                    delete item.id
                })
            }
            const data = {...form.value, qualityInspectParams: tableData.value}
      if (operationType.value === "add") {
        qualityInspectAdd(form.value).then(res => {
        qualityInspectAdd(data).then(res => {
          proxy.$modal.msgSuccess("提交成功");
          closeDia();
        })
      } else {
        qualityInspectUpdate(form.value).then(res => {
        qualityInspectUpdate(data).then(res => {
          proxy.$modal.msgSuccess("提交成功");
          closeDia();
        })
@@ -193,6 +252,16 @@
    }
  })
}
const getList = () => {
    qualityInspectDetailByProductId(currentProductId.value).then(res => {
        tableData.value = res.data;
    })
}
const getQualityInspectParamList = (id) => {
    qualityInspectParamInfo(id).then(res => {
        tableData.value = res.data;
    })
}
// å…³é—­å¼¹æ¡†
const closeDia = () => {
  proxy.resetForm("formRef");
src/views/qualityManagement/finalInspection/index.vue
@@ -40,6 +40,23 @@
    <InspectionFormDia ref="inspectionFormDia" @close="handleQuery"></InspectionFormDia>
    <FormDia ref="formDia" @close="handleQuery"></FormDia>
    <files-dia ref="filesDia" @close="handleQuery"></files-dia>
        <el-dialog v-model="dialogFormVisible" title="编辑检验员" width="30%"
                             @close="closeDia">
            <el-form :model="form" label-width="140px" label-position="top" :rules="rules" ref="formRef">
                <el-form-item label="检验员:" prop="checkName">
                    <el-select v-model="form.checkName" placeholder="请选择" clearable>
                        <el-option v-for="item in userList" :key="item.nickName" :label="item.nickName"
                                             :value="item.nickName"/>
                    </el-select>
                </el-form-item>
            </el-form>
            <template #footer>
                <div class="dialog-footer">
                    <el-button type="primary" @click="submitForm">确认</el-button>
                    <el-button @click="closeDia">取消</el-button>
                </div>
            </template>
        </el-dialog>
  </div>
</template>
@@ -49,9 +66,15 @@
import InspectionFormDia from "@/views/qualityManagement/finalInspection/components/inspectionFormDia.vue";
import FormDia from "@/views/qualityManagement/finalInspection/components/formDia.vue";
import {ElMessageBox} from "element-plus";
import {qualityInspectDel, qualityInspectListPage} from "@/api/qualityManagement/rawMaterialInspection.js";
import {
    downloadQualityInspect,
    qualityInspectDel,
    qualityInspectListPage, qualityInspectUpdate,
    submitQualityInspect
} from "@/api/qualityManagement/rawMaterialInspection.js";
import FilesDia from "@/views/qualityManagement/finalInspection/components/filesDia.vue";
import dayjs from "dayjs";
import {userListNoPage} from "@/api/system/user.js";
const data = reactive({
  searchForm: {
@@ -63,6 +86,9 @@
    entryDateStart: dayjs().format("YYYY-MM-DD"),
    entryDateEnd: dayjs().add(1, "day").format("YYYY-MM-DD"),
  },
    rules: {
        checkName: [{required: true, message: "请选择", trigger: "change"}],
    },
});
const { searchForm } = toRefs(data);
const tableColumn = ref([
@@ -111,12 +137,23 @@
      }
    },
  },
    {
        label: "提交状态",
        prop: "inspectState",
        formatData: (params) => {
            if (params) {
                return "已提交";
            } else {
                return "未提交";
            }
        },
    },
  {
    dataType: "action",
    label: "操作",
    align: "center",
    fixed: "right",
    width: 190,
    width: 280,
    operation: [
      {
        name: "编辑",
@@ -124,13 +161,9 @@
        clickFun: (row) => {
          openForm("edit", row);
        },
      },
      {
        name: "新增检验记录",
        type: "text",
        clickFun: (row) => {
          openInspectionForm("edit", row);
        },
                disabled: (row) => {
                    return row.inspectState == 1;
                }
      },
      {
        name: "附件",
@@ -139,12 +172,44 @@
          openFilesFormDia(row);
        },
      },
            {
                name: "提交",
                type: "text",
                clickFun: (row) => {
                    submit(row.id);
                },
                disabled: (row) => {
                    return row.inspectState == 1;
                }
            },
            {
                name: "分配检验员",
                type: "text",
                clickFun: (row) => {
                    if (!row.checkName) {
                        open(row)
                    } else {
                        proxy.$modal.msgError("检验员已存在");
                    }
                },
                disabled: (row) => {
                    return row.inspectState == 1 || row.checkName;
                }
            },
            {
                name: "下载",
                type: "text",
                clickFun: (row) => {
                    downLoadFile(row);
                },
            },
    ],
  },
]);
const tableData = ref([]);
const selectedRows = ref([]);
const tableLoading = ref(false);
const currentRow = ref(null)
const page = reactive({
  current: 1,
  size: 100,
@@ -154,6 +219,11 @@
const filesDia = ref()
const inspectionFormDia = ref()
const { proxy } = getCurrentInstance()
const userList = ref([]);
const form = ref({
    checkName: ""
});
const dialogFormVisible = ref(false);
const changeDaterange = (value) => {
  searchForm.value.entryDateStart = undefined;
@@ -249,6 +319,60 @@
        proxy.$modal.msg("已取消");
      });
};
// æä»·
const submit = async (id) => {
    const res = await submitQualityInspect({id: id})
    if (res.code === 200) {
        proxy.$modal.msgSuccess("提交成功");
        getList();
    }
}
// å…³é—­å¼¹æ¡†
const closeDia = () => {
    proxy.resetForm("formRef");
    dialogFormVisible.value = false;
};
const submitForm = () => {
    if (currentRow.value) {
        const data = {
            ...form.value,
            id: currentRow.value.id
        }
        qualityInspectUpdate(data).then(res => {
            proxy.$modal.msgSuccess("提交成功");
            closeDia();
            getList();
        })
    }
};
const open = async (row) => {
    let userLists = await userListNoPage();
    userList.value = userLists.data;
    currentRow.value = row
    dialogFormVisible.value = true
}
const downLoadFile = (row) => {
    downloadQualityInspect({ id: row.id }).then((blobData) => {
        const blob = new Blob([blobData], {
            type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
        })
        const downloadUrl = window.URL.createObjectURL(blob)
        const link = document.createElement('a')
        link.href = downloadUrl
        link.download = '原材料检验报告.docx'
        document.body.appendChild(link)
        link.click()
        document.body.removeChild(link)
        window.URL.revokeObjectURL(downloadUrl)
    })
};
onMounted(() => {
  getList();
});
src/views/qualityManagement/processInspection/components/filesDia.vue
@@ -46,18 +46,13 @@
        </div>
      </template>
    </el-dialog>
        <filePreview ref="filePreviewRef" />
  </div>
</template>
<script setup>
import {ref} from "vue";
import {getStaffJoinInfo, staffJoinAdd, staffJoinUpdate} from "@/api/personnelManagement/onboarding.js";
import {Search} from "@element-plus/icons-vue";
import {
  qualityInspectParamDel,
  qualityInspectParamInfo,
  qualityInspectParamUpdate
} from "@/api/qualityManagement/qualityInspectParam.js";
import filePreview from '@/components/filePreview/index.vue'
import {ElMessageBox} from "element-plus";
import {getToken} from "@/utils/auth.js";
import {
@@ -72,6 +67,7 @@
const dialogFormVisible = ref(false);
const currentId = ref('')
const selectedRows = ref([]);
const filePreviewRef = ref()
const tableColumn = ref([
  {
    label: "文件名称",
@@ -88,7 +84,14 @@
        clickFun: (row) => {
          downLoadFile(row);
        },
      }
      },
            {
                name: "预览",
                type: "text",
                clickFun: (row) => {
                    lookFile(row);
                },
            }
    ],
  },
]);
@@ -158,6 +161,10 @@
function handleUploadError() {
  proxy.$modal.msgError("文件上传失败");
}
// é¢„览附件
const lookFile = (row) => {
    filePreviewRef.value.open(row.url)
}
// åˆ é™¤
const handleDelete = () => {
  let ids = [];
src/views/qualityManagement/processInspection/components/formDia.vue
@@ -65,8 +65,10 @@
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="检验员:" prop="checkName">
              <el-input v-model="form.checkName" placeholder="请输入" clearable/>
                            <el-select v-model="form.checkName" placeholder="请选择" clearable>
                                <el-option v-for="item in userList" :key="item.nickName" :label="item.nickName"
                                                     :value="item.nickName"/>
                            </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="12">
@@ -84,6 +86,17 @@
          </el-col>
        </el-row>
      </el-form>
            <PIMTable
                rowKey="id"
                :column="tableColumn"
                :tableData="tableData"
                :tableLoading="tableLoading"
                height="400"
            >
                <template #slot="{ row }">
                    <el-input v-model="row.testValue" clearable/>
                </template>
            </PIMTable>
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary" @click="submitForm">确认</el-button>
@@ -99,6 +112,9 @@
import {getOptions} from "@/api/procurementManagement/procurementLedger.js";
import {productTreeList} from "@/api/basicData/product.js";
import {qualityInspectAdd, qualityInspectUpdate} from "@/api/qualityManagement/rawMaterialInspection.js";
import {qualityInspectDetailByProductId} from "@/api/qualityManagement/metricMaintenance.js";
import {userListNoPage} from "@/api/system/user.js";
import {qualityInspectParamInfo} from "@/api/qualityManagement/qualityInspectParam.js";
const { proxy } = getCurrentInstance()
const emit = defineEmits(['close'])
@@ -129,21 +145,54 @@
    checkResult: [{ required: true, message: "请输入", trigger: "change" }],
  },
});
const userList = ref([]);
const { form, rules } = toRefs(data);
const supplierList = ref([]);
const productOptions = ref([]);
const tableColumn = ref([
    {
        label: "指标",
        prop: "parameterItem",
    },
    {
        label: "单位",
        prop: "unit",
    },
    {
        label: "标准值",
        prop: "standardValue",
    },
    {
        label: "内控值",
        prop: "controlValue",
    },
    {
        label: "检验值",
        prop: "testValue",
        dataType: 'slot',
        slot: 'slot',
    },
]);
const tableData = ref([]);
const tableLoading = ref(false);
const currentProductId = ref(0);
// æ‰“开弹框
const openDialog = (type, row) => {
  operationType.value = type;
  dialogFormVisible.value = true;
  getOptions().then((res) => {
    supplierList.value = res.data;
  });
  getProductOptions();
  if (operationType.value === 'edit') {
    form.value = {...row}
  }
const openDialog = async (type, row) => {
    operationType.value = type;
    dialogFormVisible.value = true;
    getOptions().then((res) => {
        supplierList.value = res.data;
    });
    let userLists = await userListNoPage();
    userList.value = userLists.data;
    form.value = {}
    getProductOptions();
    if (operationType.value === 'edit') {
        form.value = {...row}
        currentProductId.value = row.productId || 0
        getQualityInspectParamList(row.id)
    }
}
const getProductOptions = () => {
  productTreeList().then((res) => {
@@ -151,7 +200,11 @@
  });
};
const getModels = (value) => {
    currentProductId.value = value
  form.value.productName = findNodeById(productOptions.value, value);
    if (currentProductId) {
        getList();
    }
};
const findNodeById = (nodes, productId) => {
  for (let i = 0; i < nodes.length; i++) {
@@ -186,13 +239,19 @@
  proxy.$refs.formRef.validate(valid => {
    if (valid) {
      form.value.inspectType = 1
            if (operationType.value === "add") {
                tableData.value.forEach((item) => {
                    delete item.id
                })
            }
            const data = {...form.value, qualityInspectParams: tableData.value}
      if (operationType.value === "add") {
        qualityInspectAdd(form.value).then(res => {
        qualityInspectAdd(data).then(res => {
          proxy.$modal.msgSuccess("提交成功");
          closeDia();
        })
      } else {
        qualityInspectUpdate(form.value).then(res => {
        qualityInspectUpdate(data).then(res => {
          proxy.$modal.msgSuccess("提交成功");
          closeDia();
        })
@@ -200,6 +259,16 @@
    }
  })
}
const getList = () => {
    qualityInspectDetailByProductId(currentProductId.value).then(res => {
        tableData.value = res.data;
    })
}
const getQualityInspectParamList = (id) => {
    qualityInspectParamInfo(id).then(res => {
        tableData.value = res.data;
    })
}
// å…³é—­å¼¹æ¡†
const closeDia = () => {
  proxy.resetForm("formRef");
src/views/qualityManagement/processInspection/index.vue
@@ -40,6 +40,23 @@
    <InspectionFormDia ref="inspectionFormDia" @close="handleQuery"></InspectionFormDia>
    <FormDia ref="formDia" @close="handleQuery"></FormDia>
    <files-dia ref="filesDia" @close="handleQuery"></files-dia>
        <el-dialog v-model="dialogFormVisible" title="编辑检验员" width="30%"
                             @close="closeDia">
            <el-form :model="form" label-width="140px" label-position="top" :rules="rules" ref="formRef">
                <el-form-item label="检验员:" prop="checkName">
                    <el-select v-model="form.checkName" placeholder="请选择" clearable>
                        <el-option v-for="item in userList" :key="item.nickName" :label="item.nickName"
                                             :value="item.nickName"/>
                    </el-select>
                </el-form-item>
            </el-form>
            <template #footer>
                <div class="dialog-footer">
                    <el-button type="primary" @click="submitForm">确认</el-button>
                    <el-button @click="closeDia">取消</el-button>
                </div>
            </template>
        </el-dialog>
  </div>
</template>
@@ -49,9 +66,15 @@
import InspectionFormDia from "@/views/qualityManagement/processInspection/components/inspectionFormDia.vue";
import FormDia from "@/views/qualityManagement/processInspection/components/formDia.vue";
import {ElMessageBox} from "element-plus";
import {qualityInspectDel, qualityInspectListPage} from "@/api/qualityManagement/rawMaterialInspection.js";
import {
    downloadQualityInspect,
    qualityInspectDel,
    qualityInspectListPage, qualityInspectUpdate,
    submitQualityInspect
} from "@/api/qualityManagement/rawMaterialInspection.js";
import FilesDia from "@/views/qualityManagement/processInspection/components/filesDia.vue";
import dayjs from "dayjs";
import {userListNoPage} from "@/api/system/user.js";
const data = reactive({
  searchForm: {
@@ -63,6 +86,9 @@
    entryDateStart: dayjs().format("YYYY-MM-DD"),
    entryDateEnd: dayjs().add(1, "day").format("YYYY-MM-DD"),
  },
    rules: {
        checkName: [{required: true, message: "请选择", trigger: "change"}],
    },
});
const { searchForm } = toRefs(data);
const tableColumn = ref([
@@ -116,12 +142,23 @@
      }
    },
  },
    {
        label: "提交状态",
        prop: "inspectState",
        formatData: (params) => {
            if (params) {
                return "已提交";
            } else {
                return "未提交";
            }
        },
    },
  {
    dataType: "action",
    label: "操作",
    align: "center",
    fixed: "right",
    width: 190,
    width: 280,
    operation: [
      {
        name: "编辑",
@@ -129,13 +166,9 @@
        clickFun: (row) => {
          openForm("edit", row);
        },
      },
      {
        name: "新增检验记录",
        type: "text",
        clickFun: (row) => {
          openInspectionForm("edit", row);
        },
                disabled: (row) => {
                    return row.inspectState == 1;
                }
      },
      {
        name: "附件",
@@ -144,12 +177,49 @@
          openFilesFormDia(row);
        },
      },
            {
                name: "提交",
                type: "text",
                clickFun: (row) => {
                    submit(row.id);
                },
                disabled: (row) => {
                    return row.inspectState == 1;
                }
            },
            {
                name: "分配检验员",
                type: "text",
                clickFun: (row) => {
                    if (!row.checkName) {
                        open(row)
                    } else {
                        proxy.$modal.msgError("检验员已存在");
                    }
                },
                disabled: (row) => {
                    return row.inspectState == 1 || row.checkName;
                }
            },
            {
                name: "下载",
                type: "text",
                clickFun: (row) => {
                    downLoadFile(row);
                },
            },
    ],
  },
]);
const userList = ref([]);
const currentRow = ref(null)
const tableData = ref([]);
const selectedRows = ref([]);
const tableLoading = ref(false);
const dialogFormVisible = ref(false);
const form = ref({
    checkName: ""
});
const page = reactive({
  current: 1,
  size: 100,
@@ -214,6 +284,38 @@
    filesDia.value?.openDialog(type, row)
  })
};
// æä»·
const submit = async (id) => {
    const res = await submitQualityInspect({id: id})
    if (res.code === 200) {
        proxy.$modal.msgSuccess("提交成功");
        getList();
    }
}
const open = async (row) => {
    let userLists = await userListNoPage();
    userList.value = userLists.data;
    currentRow.value = row
    dialogFormVisible.value = true
}
// å…³é—­å¼¹æ¡†
const closeDia = () => {
    proxy.resetForm("formRef");
    dialogFormVisible.value = false;
};
const submitForm = () => {
    if (currentRow.value) {
        const data = {
            ...form.value,
            id: currentRow.value.id
        }
        qualityInspectUpdate(data).then(res => {
            proxy.$modal.msgSuccess("提交成功");
            closeDia();
            getList();
        })
    }
};
// åˆ é™¤
const handleDelete = () => {
@@ -239,6 +341,23 @@
        proxy.$modal.msg("已取消");
      });
};
const downLoadFile = (row) => {
    downloadQualityInspect({ id: row.id }).then((blobData) => {
        const blob = new Blob([blobData], {
            type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
        })
        const downloadUrl = window.URL.createObjectURL(blob)
        const link = document.createElement('a')
        link.href = downloadUrl
        link.download = '过程检验报告.docx'
        document.body.appendChild(link)
        link.click()
        document.body.removeChild(link)
        window.URL.revokeObjectURL(downloadUrl)
    })
};
// å¯¼å‡º
const handleOut = () => {
  ElMessageBox.confirm("选中的内容将被导出,是否确认导出?", "导出", {
src/views/qualityManagement/rawMaterialInspection/components/filesDia.vue
@@ -46,18 +46,13 @@
        </div>
      </template>
    </el-dialog>
        <filePreview ref="filePreviewRef" />
  </div>
</template>
<script setup>
import {ref} from "vue";
import {getStaffJoinInfo, staffJoinAdd, staffJoinUpdate} from "@/api/personnelManagement/onboarding.js";
import {Search} from "@element-plus/icons-vue";
import {
  qualityInspectParamDel,
  qualityInspectParamInfo,
  qualityInspectParamUpdate
} from "@/api/qualityManagement/qualityInspectParam.js";
import filePreview from '@/components/filePreview/index.vue'
import {ElMessageBox} from "element-plus";
import {getToken} from "@/utils/auth.js";
import {
@@ -88,7 +83,14 @@
        clickFun: (row) => {
          downLoadFile(row);
        },
      }
      },
            {
                name: "预览",
                type: "text",
                clickFun: (row) => {
                    lookFile(row);
                },
            }
    ],
  },
]);
@@ -100,6 +102,7 @@
const tableData = ref([]);
const fileList = ref([]);
const tableLoading = ref(false);
const filePreviewRef = ref()
const headers = ref({
  Authorization: "Bearer " + getToken(),
});
@@ -159,6 +162,10 @@
const downLoadFile = (row) => {
  proxy.$download.name(row.url);
}
// é¢„览附件
const lookFile = (row) => {
    filePreviewRef.value.open(row.url)
}
// åˆ é™¤
const handleDelete = () => {
  let ids = [];
src/views/qualityManagement/rawMaterialInspection/components/formDia.vue
@@ -96,16 +96,14 @@
          </el-col>
        </el-row>
      </el-form>
      <div style="margin-bottom: 10px;text-align: right">
        <el-button type="danger" plain @click="handleDelete">删除</el-button>
      </div>
<!--      <div style="margin-bottom: 10px;text-align: right">-->
<!--        <el-button type="danger" plain @click="handleDelete">删除</el-button>-->
<!--      </div>-->
      <PIMTable
          rowKey="id"
          :column="tableColumn"
          :tableData="tableData"
          :tableLoading="tableLoading"
          :isSelection="true"
          @selection-change="handleSelectionChange"
          height="400"
      >
        <template #slot="{ row }">
@@ -200,6 +198,7 @@
  getOptions().then((res) => {
    supplierList.value = res.data;
  });
    form.value = {}
  getProductOptions();
  if (operationType.value === 'edit') {
    form.value = {...row}
@@ -254,6 +253,11 @@
  proxy.$refs.formRef.validate(valid => {
    if (valid) {
      form.value.inspectType = 0
            if (operationType.value === "add") {
                tableData.value.forEach((item) => {
                    delete item.id
                })
            }
      const data = {...form.value, qualityInspectParams: tableData.value}
      if (operationType.value === "add") {
        qualityInspectAdd(data).then(res => {
@@ -269,34 +273,6 @@
    }
  })
}
const handleSelectionChange = (selection) => {
  selectedRows.value = selection;
};
const handleDelete = () => {
  let ids = [];
  if (selectedRows.value.length > 0) {
    ids = selectedRows.value.map((item) => item.id);
  } else {
    proxy.$modal.msgWarning("请选择数据");
    return;
  }
  ElMessageBox.confirm("选中的内容将被删除,是否确认删除?", "导出", {
    confirmButtonText: "确认",
    cancelButtonText: "取消",
    type: "warning",
  })
      .then(() => {
        qualityInspectParamDel(ids).then((res) => {
          proxy.$modal.msgSuccess("删除成功");
          getList();
        });
      })
      .catch(() => {
        proxy.$modal.msg("已取消");
      });
};
const getList = () => {
  qualityInspectDetailByProductId(currentProductId.value).then(res => {
src/views/qualityManagement/rawMaterialInspection/index.vue
@@ -41,7 +41,7 @@
    <InspectionFormDia ref="inspectionFormDia" @close="handleQuery"></InspectionFormDia>
    <FormDia ref="formDia" @close="handleQuery"></FormDia>
    <files-dia ref="filesDia" @close="handleQuery"></files-dia>
    <el-dialog v-model="dialogFormVisible" title="编辑检验员" width="70%"
    <el-dialog v-model="dialogFormVisible" title="编辑检验员" width="30%"
               @close="closeDia">
      <el-form :model="form" label-width="140px" label-position="top" :rules="rules" ref="formRef">
        <el-form-item label="检验员:" prop="checkName">
@@ -160,7 +160,7 @@
    label: "操作",
    align: "center",
    fixed: "right",
    width: 250,
    width: 280,
    operation: [
      {
        name: "编辑",
@@ -168,6 +168,9 @@
        clickFun: (row) => {
          openForm("edit", row);
        },
                disabled: (row) => {
                    return row.inspectState == 1;
                }
      },
      {
        name: "附件",
@@ -182,6 +185,9 @@
        clickFun: (row) => {
          submit(row.id);
        },
                disabled: (row) => {
                    return row.inspectState == 1;
                }
      },
      {
        name: "分配检验员",
@@ -193,6 +199,9 @@
            proxy.$modal.msgError("检验员已存在");
          }
        },
                disabled: (row) => {
                    return row.inspectState == 1 || row.checkName;
                }
      },
      {
        name: "下载",
@@ -263,12 +272,6 @@
const openForm = (type, row) => {
  nextTick(() => {
    formDia.value?.openDialog(type, row)
  })
};
// æ‰“开新增检验弹框
const openInspectionForm = (type, row) => {
  nextTick(() => {
    inspectionFormDia.value?.openDialog(type, row)
  })
};
// æ‰“开附件弹框
@@ -354,19 +357,18 @@
}
const downLoadFile = (row) => {
  downloadQualityInspect({id: row.id}).then(res => {
    // åˆ›å»º blob å¯¹è±¡
    const blob = new Blob([res.data], {type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'})
  downloadQualityInspect({ id: row.id }).then((blobData) => {
    const blob = new Blob([blobData], {
      type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
    })
    const downloadUrl = window.URL.createObjectURL(blob)
    // åˆ›å»ºä¸´æ—¶ <a> æ ‡ç­¾è¿›è¡Œä¸‹è½½
    const link = document.createElement('a')
    link.href = downloadUrl
    link.download = '检验报告.docx' // è¿™é‡Œå’ŒåŽç«¯ä¸€è‡´
    link.download = '原材料检验报告.docx'
    document.body.appendChild(link)
    link.click()
    // æ¸…理
    document.body.removeChild(link)
    window.URL.revokeObjectURL(downloadUrl)
  })
src/views/reportAnalysis/projectProfit/index.vue
@@ -20,6 +20,8 @@
          size: pagination.pageSize,
          total: pagination.total,
        }"
        :isShowSummary="true"
        :summaryMethod="summaryMethod"
        @pagination="changePage"
      ></PIMTable>
    </div>
@@ -30,6 +32,7 @@
import { usePaginationApi } from "@/hooks/usePaginationApi";
import { getPurchaseList } from "@/api/procurementManagement/projectProfit";
import { onMounted } from "vue";
import { summarizeTable } from "@/utils/summarizeTable";
defineOptions({
  name: "项目利润",
@@ -98,6 +101,14 @@
  onCurrentChange(page);
};
// åˆè®¡æ–¹æ³•
const summaryMethod = (param) => {
  return summarizeTable(
    param,
    ['contractAmount', 'purchaseAmount', 'balance', 'balanceAmount', 'balanceRatio'],
  );
};
onMounted(() => {
  getTableData();
});
src/views/salesManagement/invoiceLedger/index.vue
@@ -27,7 +27,7 @@
    </div>
    <div class="table_list">
      <el-table :data="tableData" border v-loading="tableLoading" @selection-change="handleSelectionChange"
        :row-key="(row) => row.id" show-summary :summary-method="summarizeMainTable" height="calc(100vh - 18.5em)">
        :row-key="(row) => row.id" show-summary :summary-method="summarizeMainTable" height="calc(100vh - 18.5em)" stripe>
        <el-table-column align="center" type="selection" width="55" />
        <el-table-column align="center" label="序号" type="index" width="60" />
        <el-table-column label="销售合同号" prop="salesContractNo" show-overflow-tooltip width="180" />
@@ -43,7 +43,7 @@
        <el-table-column label="录入人" prop="invoicePerson" show-overflow-tooltip />
        <el-table-column label="录入日期" prop="createTime" show-overflow-tooltip :formatter="formatDate" width="180" />
        <el-table-column label="开票日期" prop="invoiceDate" show-overflow-tooltip width="120" />
        <el-table-column label="发票" prop="invoiceFileName" width="120" align="center" show-overflow-tooltip>
        <el-table-column label="发票" prop="invoiceFileName" width="120" align="center" show-overflow-tooltip fixed="right">
          <template #default="scope">
            <el-button v-if="scope.row.invoiceFileName" text bg type="primary"
              @click="handleFile(scope.row.commonFiles)">
@@ -296,8 +296,8 @@
function handleBeforeUpload(file) {
  console.log("file", file);
  // æ ¡æ£€æ–‡ä»¶å¤§å°
  if (file.size > 1024 * 1024 * 10) {
    proxy.$modal.msgError("上传文件大小不能超过10MB!");
  if (file.size > 1024 * 1024 * 50) {
    proxy.$modal.msgError("上传文件大小不能超过50MB!");
    return false;
  }
  // åˆ¤æ–­æ–‡ä»¶æ ¼å¼æ˜¯å¦ç¬¦åˆ
src/views/salesManagement/invoiceRegistration/index.vue
@@ -33,6 +33,12 @@
            @change="handleQuery"
          />
        </el-form-item>
        <br/>
        <el-form-item label="合同录入日期">
          <el-date-picker style="width: 240px" v-model="searchForm.commonDate" value-format="YYYY-MM-DD"
                          format="YYYY-MM-DD" type="daterange" start-placeholder="开始时间" end-placeholder="结束时间" clearable
                          @change="changeDateRange" @clear="clearRange" />
        </el-form-item>
        <el-form-item>
          <el-button type="primary" @click="handleQuery"> æœç´¢ </el-button>
          <el-button @click="resetForm"> é‡ç½® </el-button>
@@ -57,6 +63,7 @@
        :summary-method="summarizeMainTable"
        @expand-change="expandChange"
        @selection-change="handleSelectionChange"
        stripe
      >
        <el-table-column align="center" type="selection" width="55" />
        <el-table-column type="expand">
@@ -66,6 +73,7 @@
              border
              show-summary
              :summary-method="summarizeChildrenTable"
              stripe
            >
              <el-table-column
                align="center"
@@ -121,6 +129,7 @@
          </template>
        </el-table-column>
        <el-table-column align="center" label="序号" type="index" width="60" />
        <el-table-column label="合同录入日期" prop="entryDate" width="120" />
        <el-table-column
          label="销售合同号"
          prop="salesContractNo"
@@ -461,6 +470,21 @@
const { form, rules } = toRefs(data);
const { form: searchForm, resetForm } = useFormData(data.searchForm);
const changeDateRange = (date) => {
  if (date) {
    searchForm.entryDateStart = date[0];
    searchForm.entryDateEnd = date[1];
    getList();
  }
};
const clearRange = () => {
  searchForm.commonDate = [];
  searchForm.entryDateStart = undefined;
  searchForm.entryDateEnd = undefined;
  getList();
};
const formattedNumber = (row, column, cellValue) => {
  if (cellValue == 0) {
    return parseFloat(cellValue).toFixed(2);
@@ -557,6 +581,7 @@
  getSalesLedgerWithProducts({ id: selectedRows.value[0].id }).then((res) => {
    form.value = { ...res };
    form.value.createTime = dayjs().format("YYYY-MM-DD");
    form.value.issueDate = dayjs().format("YYYY-MM-DD");
    form.value.createUer = userStore.nickName;
    productData.value = form.value.productData.map((item) => {
      return item;
src/views/salesManagement/receiptPayment/index.vue
@@ -38,6 +38,12 @@
                @change="handleQuery"
              />
            </el-form-item>
            <br/>
            <el-form-item label="开票日期">
              <el-date-picker style="width: 240px" v-model="searchForm.commonDate" value-format="YYYY-MM-DD"
                              format="YYYY-MM-DD" type="daterange" start-placeholder="开始时间" end-placeholder="结束时间" clearable
                              @change="changeDateRange" @clear="clearRange" />
            </el-form-item>
            <el-form-item>
              <el-button type="primary" @click="handleQuery"> æœç´¢ </el-button>
            </el-form-item>
@@ -65,7 +71,7 @@
        :summary-method="summarizeMainTable"
        :expand-row-keys="expandedRowKeys"
        @expand-change="expandChange"
        stripe
        height="calc(100vh - 21.5em)"
      >
        <el-table-column align="center" type="selection" width="55" />
@@ -76,6 +82,7 @@
              border
              show-summary
              :summary-method="summarizeChildrenTable"
              stripe
            >
              <el-table-column
                align="center"
@@ -148,6 +155,12 @@
          </template>
        </el-table-column>
        <el-table-column align="center" label="序号" type="index" width="60" />
        <el-table-column
            label="开票日期"
            prop="invoiceDate"
            show-overflow-tooltip
            width="240"
        />
        <el-table-column
          label="销售合同号"
          prop="salesContractNo"
@@ -426,6 +439,21 @@
const formattedNumber = (row, column, cellValue) => {
  return parseFloat(cellValue).toFixed(2);
};
const changeDateRange = (date) => {
  if (date) {
    searchForm.invoiceDateStart = date[0];
    searchForm.invoiceDateEnd = date[1];
    getList();
  }
};
const clearRange = () => {
  searchForm.commonDate = [];
  searchForm.invoiceDateStart = undefined;
  searchForm.invoiceDateEnd = undefined;
  getList();
};
// æŸ¥è¯¢åˆ—表
/** æœç´¢æŒ‰é’®æ“ä½œ */
const handleQuery = () => {
@@ -514,7 +542,7 @@
    return;
  }
  if (selectedRows.value[0].noReceiptAmount == 0) {
    proxy.$modal.warning("无需再回款");
    proxy.$modal.msgWarning("无需再回款");
    return;
  }
  invoiceInfo({ id: selectedRows.value[0].id }).then((res) => {
src/views/salesManagement/receiptPaymentLedger/index.vue
@@ -27,6 +27,7 @@
          :summary-method="summarizeMainTable"
          @row-click="rowClickMethod"
          height="calc(100vh - 18.5em)"
          stripe
        >
          <el-table-column
            align="center"
@@ -81,6 +82,7 @@
          :data="receiptRecord"
          border
          :row-key="(row) => row.id"
          stripe
          show-summary
          :summary-method="summarizeMainTable1"
          height="calc(100vh - 18.5em)"
src/views/salesManagement/salesLedger/fileList.vue
@@ -1,6 +1,6 @@
<template>
  <el-dialog v-model="dialogVisible" title="附件" width="40%" :before-close="handleClose">
    <el-table :data="tableData" border height="40vh">
    <el-table :data="tableData" border height="40vh" stripe>
      <el-table-column label="附件名称" prop="name" min-width="400" show-overflow-tooltip />
      <el-table-column fixed="right" label="操作" width="100" align="center">
        <template #default="scope">
src/views/salesManagement/salesLedger/index.vue
@@ -40,11 +40,11 @@
      </div>
      <el-table :data="tableData" border v-loading="tableLoading" @selection-change="handleSelectionChange"
        :expand-row-keys="expandedRowKeys" :row-key="(row) => row.id" show-summary style="width: 100%"
        :summary-method="summarizeMainTable" @expand-change="expandChange" height="calc(100vh - 18.5em)">
        :summary-method="summarizeMainTable" @expand-change="expandChange" height="calc(100vh - 18.5em)" stripe>
        <el-table-column align="center" type="selection" width="55" />
        <el-table-column type="expand">
          <template #default="props">
            <el-table :data="props.row.children" border show-summary :summary-method="summarizeChildrenTable">
            <el-table :data="props.row.children" border show-summary :summary-method="summarizeChildrenTable" stripe>
              <el-table-column align="center" label="序号" type="index" width="60" />
              <el-table-column label="产品大类" prop="productCategory" />
              <el-table-column label="规格型号" prop="specificationModel" />
@@ -66,6 +66,14 @@
        <el-table-column label="付款方式" prop="paymentMethod" show-overflow-tooltip />
        <el-table-column label="合同金额(元)" prop="contractAmount" width="220" show-overflow-tooltip
          :formatter="formattedNumber" />
        <el-table-column label="已开票金额(元)" prop="invoiceTotal" width="220" show-overflow-tooltip
                         :formatter="formattedNumber" />
        <el-table-column label="未开票金额(元)" prop="noInvoiceAmountTotal" width="220" show-overflow-tooltip
                         :formatter="formattedNumber" />
        <el-table-column label="回款金额(元)" prop="receiptPaymentAmountTotal" width="220" show-overflow-tooltip
                         :formatter="formattedNumber" />
        <el-table-column label="待回款金额(元)" prop="noReceiptAmount" width="220" show-overflow-tooltip
                         :formatter="formattedNumber" />
        <el-table-column label="录入人" prop="entryPersonName" width="100" show-overflow-tooltip />
        <el-table-column label="录入日期" prop="entryDate" width="120" show-overflow-tooltip />
        <el-table-column label="签订日期" prop="executionDate" width="120" show-overflow-tooltip />
@@ -157,7 +165,7 @@
            <el-button v-if="operationType !== 'view'" plain type="danger" @click="deleteProduct" >删除</el-button>
          </el-form-item>
        </el-row>
        <el-table :data="productData" border @selection-change="productSelected" show-summary
        <el-table :data="productData" border @selection-change="productSelected" show-summary stripe
          :summary-method="summarizeMainTable">
          <el-table-column align="center" type="selection" width="55" v-if="operationType !== 'view'" />
          <el-table-column align="center" label="序号" type="index" width="60" />
@@ -556,6 +564,10 @@
    "contractAmount",
    "taxInclusiveTotalPrice",
    "taxExclusiveTotalPrice",
        'invoiceTotal',
        'noInvoiceAmountTotal',
        'receiptPaymentAmountTotal',
        'noReceiptAmount',
  ]);
};
// å­è¡¨åˆè®¡æ–¹æ³•
src/views/system/config/index.vue
@@ -96,7 +96,7 @@
         <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
      </el-row>
      <el-table v-loading="loading" :data="configList" @selection-change="handleSelectionChange">
      <el-table v-loading="loading" :data="configList" @selection-change="handleSelectionChange" stripe>
         <el-table-column type="selection" width="55" align="center" />
         <el-table-column label="参数主键" align="center" prop="configId" />
         <el-table-column label="参数名称" align="center" prop="configName" :show-overflow-tooltip="true" />
src/views/system/dept/index.vue
@@ -1,17 +1,17 @@
<template>
   <div class="app-container">
      <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch">
         <el-form-item label="部门名称" prop="deptName">
         <el-form-item label="公司名称" prop="deptName">
            <el-input
               v-model="queryParams.deptName"
               placeholder="请输入部门名称"
               placeholder="请输入公司名称"
               clearable
               style="width: 200px"
               @keyup.enter="handleQuery"
            />
         </el-form-item>
         <el-form-item label="状态" prop="status">
            <el-select v-model="queryParams.status" placeholder="部门状态" clearable style="width: 200px">
            <el-select v-model="queryParams.status" placeholder="公司状态" clearable style="width: 200px">
               <el-option
                  v-for="dict in sys_normal_disable"
                  :key="dict.value"
@@ -54,8 +54,9 @@
         row-key="deptId"
         :default-expand-all="isExpandAll"
         :tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
         stripe
      >
         <el-table-column prop="deptName" label="部门名称" width="260"></el-table-column>
         <el-table-column prop="deptName" label="公司名称" width="260"></el-table-column>
         <el-table-column prop="orderNum" label="排序" width="200"></el-table-column>
         <el-table-column prop="status" label="状态" width="100">
            <template #default="scope">
@@ -76,25 +77,25 @@
         </el-table-column>
      </el-table>
      <!-- æ·»åŠ æˆ–ä¿®æ”¹éƒ¨é—¨å¯¹è¯æ¡† -->
      <!-- æ·»åŠ æˆ–ä¿®æ”¹å…¬å¸å¯¹è¯æ¡† -->
      <el-dialog :title="title" v-model="open" width="600px" append-to-body>
         <el-form ref="deptRef" :model="form" :rules="rules" label-width="80px">
            <el-row>
               <el-col :span="24" v-if="form.parentId !== 0">
                  <el-form-item label="上级部门" prop="parentId">
                  <el-form-item label="上级公司" prop="parentId">
                     <el-tree-select
                        v-model="form.parentId"
                        :data="deptOptions"
                        :props="{ value: 'deptId', label: 'deptName', children: 'children' }"
                        value-key="deptId"
                        placeholder="选择上级部门"
                        placeholder="选择上级公司"
                        check-strictly
                     />
                  </el-form-item>
               </el-col>
               <el-col :span="12">
                  <el-form-item label="部门名称" prop="deptName">
                     <el-input v-model="form.deptName" placeholder="请输入部门名称" />
                  <el-form-item label="公司名称" prop="deptName">
                     <el-input v-model="form.deptName" placeholder="请输入公司名称" />
                  </el-form-item>
               </el-col>
               <el-col :span="12">
@@ -118,7 +119,7 @@
                  </el-form-item>
               </el-col>
               <el-col :span="12">
                  <el-form-item label="部门状态">
                  <el-form-item label="公司状态">
                     <el-radio-group v-model="form.status">
                        <el-radio
                           v-for="dict in sys_normal_disable"
@@ -129,8 +130,8 @@
                  </el-form-item>
               </el-col>
              <el-col :span="12">
                <el-form-item label="部门编号" prop="deptNick">
                  <el-input v-model="form.deptNick" placeholder="请输入部门编号" maxlength="50" />
                <el-form-item label="公司编号" prop="deptNick">
                  <el-input v-model="form.deptNick" placeholder="请输入公司编号" maxlength="50" />
                </el-form-item>
              </el-col>
            </el-row>
@@ -167,18 +168,18 @@
    status: undefined
  },
  rules: {
    parentId: [{ required: true, message: "上级部门不能为空", trigger: "blur" }],
    deptName: [{ required: true, message: "部门名称不能为空", trigger: "blur" }],
    parentId: [{ required: true, message: "上级公司不能为空", trigger: "blur" }],
    deptName: [{ required: true, message: "公司名称不能为空", trigger: "blur" }],
    orderNum: [{ required: true, message: "显示排序不能为空", trigger: "blur" }],
    email: [{ type: "email", message: "请输入正确的邮箱地址", trigger: ["blur", "change"] }],
    phone: [{ pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/, message: "请输入正确的手机号码", trigger: "blur" }],
    deptNick: [{ required: true, message: "部门编号不能为空", trigger: "blur" }],
    deptNick: [{ required: true, message: "公司编号不能为空", trigger: "blur" }],
  },
})
const { queryParams, form, rules } = toRefs(data)
/** æŸ¥è¯¢éƒ¨é—¨åˆ—表 */
/** æŸ¥è¯¢å…¬å¸åˆ—表 */
function getList() {
  loading.value = true
  listDept(queryParams.value).then(response => {
@@ -230,7 +231,7 @@
    form.value.parentId = row.deptId
  }
  open.value = true
  title.value = "添加部门"
  title.value = "添加公司"
}
/** å±•å¼€/折叠操作 */
@@ -251,7 +252,7 @@
  getDept(row.deptId).then(response => {
    form.value = response.data
    open.value = true
    title.value = "修改部门"
    title.value = "修改公司"
  })
}
src/views/system/dict/data.vue
@@ -86,7 +86,7 @@
         <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
      </el-row>
      <el-table v-loading="loading" :data="dataList" @selection-change="handleSelectionChange">
      <el-table v-loading="loading" :data="dataList" @selection-change="handleSelectionChange" stripe>
         <el-table-column type="selection" width="55" align="center" />
         <el-table-column label="字典编码" align="center" prop="dictCode" />
         <el-table-column label="字典标签" align="center" prop="dictLabel">
src/views/system/dict/index.vue
@@ -101,7 +101,7 @@
         <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
      </el-row>
      <el-table v-loading="loading" :data="typeList" @selection-change="handleSelectionChange">
      <el-table v-loading="loading" :data="typeList" @selection-change="handleSelectionChange" stripe>
         <el-table-column type="selection" width="55" align="center" />
         <el-table-column label="字典编号" align="center" prop="dictId" />
         <el-table-column label="字典名称" align="center" prop="dictName" :show-overflow-tooltip="true"/>
src/views/system/menu/index.vue
@@ -51,6 +51,7 @@
         v-if="refreshTable"
         v-loading="loading"
         :data="menuList"
         stripe
         row-key="menuId"
         :default-expand-all="isExpandAll"
         :tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
src/views/system/notice/index.vue
@@ -68,7 +68,7 @@
         <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
      </el-row>
      <el-table v-loading="loading" :data="noticeList" @selection-change="handleSelectionChange">
      <el-table v-loading="loading" :data="noticeList" @selection-change="handleSelectionChange" stripe>
         <el-table-column type="selection" width="55" align="center" />
         <el-table-column label="序号" align="center" prop="noticeId" width="100" />
         <el-table-column
src/views/system/post/index.vue
@@ -77,7 +77,7 @@
         <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
      </el-row>
      <el-table v-loading="loading" :data="postList" @selection-change="handleSelectionChange">
      <el-table v-loading="loading" :data="postList" @selection-change="handleSelectionChange" stripe>
         <el-table-column type="selection" width="55" align="center" />
         <el-table-column label="岗位编号" align="center" prop="postId" />
         <el-table-column label="岗位编码" align="center" prop="postCode" />
src/views/system/role/authUser.vue
@@ -57,7 +57,7 @@
         <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
      </el-row>
      <el-table v-loading="loading" :data="userList" @selection-change="handleSelectionChange">
      <el-table v-loading="loading" :data="userList" @selection-change="handleSelectionChange" stripe>
         <el-table-column type="selection" width="55" align="center" />
         <el-table-column label="用户名称" prop="userName" :show-overflow-tooltip="true" />
         <el-table-column label="用户昵称" prop="nickName" :show-overflow-tooltip="true" />
src/views/system/role/index.vue
@@ -92,7 +92,7 @@
      </el-row>
      <!-- è¡¨æ ¼æ•°æ® -->
      <el-table v-loading="loading" :data="roleList" @selection-change="handleSelectionChange">
      <el-table v-loading="loading" :data="roleList" @selection-change="handleSelectionChange" stripe>
         <el-table-column type="selection" width="55" align="center" />
         <el-table-column label="角色编号" prop="roleId" width="120" />
         <el-table-column label="角色名称" prop="roleName" :show-overflow-tooltip="true" width="150" />
src/views/system/role/selectUser.vue
@@ -26,7 +26,7 @@
         </el-form-item>
      </el-form>
      <el-row>
         <el-table @row-click="clickRow" ref="refTable" :data="userList" @selection-change="handleSelectionChange" height="260px">
         <el-table @row-click="clickRow" ref="refTable" :data="userList" @selection-change="handleSelectionChange" height="260px" stripe>
            <el-table-column type="selection" width="55"></el-table-column>
            <el-table-column label="用户名称" prop="userName" :show-overflow-tooltip="true" />
            <el-table-column label="用户昵称" prop="nickName" :show-overflow-tooltip="true" />
src/views/system/user/authRole.vue
@@ -17,7 +17,7 @@
      </el-form>
      <h4 class="form-header h4">角色信息</h4>
      <el-table v-loading="loading" :row-key="getRowKey" @row-click="clickRow" ref="roleRef" @selection-change="handleSelectionChange" :data="roles.slice((pageNum - 1) * pageSize, pageNum * pageSize)">
      <el-table v-loading="loading" :row-key="getRowKey" @row-click="clickRow" ref="roleRef" @selection-change="handleSelectionChange" :data="roles.slice((pageNum - 1) * pageSize, pageNum * pageSize)" stripe>
         <el-table-column label="序号" width="55" type="index" align="center">
            <template #default="scope">
               <span>{{ (pageNum - 1) * pageSize + scope.$index + 1 }}</span>
src/views/system/user/index.vue
@@ -157,6 +157,7 @@
              v-loading="loading"
              :data="userList"
              @selection-change="handleSelectionChange"
              stripe
            >
              <el-table-column type="selection" width="50" align="center" />
              <el-table-column
src/views/tool/gen/editTable.vue
@@ -5,7 +5,7 @@
        <basic-info-form ref="basicInfo" :info="info" />
      </el-tab-pane>
      <el-tab-pane label="字段信息" name="columnInfo">
        <el-table ref="dragTable" :data="columns" row-key="columnId" :max-height="tableHeight">
        <el-table ref="dragTable" :data="columns" row-key="columnId" :max-height="tableHeight" stripe>
          <el-table-column label="序号" type="index" min-width="5%"/>
          <el-table-column
            label="字段列名"
src/views/tool/gen/importTable.vue
@@ -26,7 +26,7 @@
      </el-form-item>
    </el-form>
    <el-row>
      <el-table @row-click="clickRow" ref="table" :data="dbTableList" @selection-change="handleSelectionChange" height="260px">
      <el-table @row-click="clickRow" ref="table" :data="dbTableList" @selection-change="handleSelectionChange" height="260px" stripe>
        <el-table-column type="selection" width="55"></el-table-column>
        <el-table-column prop="tableName" label="表名称" :show-overflow-tooltip="true"></el-table-column>
        <el-table-column prop="tableComment" label="表描述" :show-overflow-tooltip="true"></el-table-column>
src/views/tool/gen/index.vue
@@ -106,6 +106,7 @@
      ref="genRef"
      v-loading="loading"
      :data="tableList"
      stripe
      @selection-change="handleSelectionChange"
      :default-sort="defaultSort"
      @sort-change="handleSortChange"