Просмотр исходного кода

Merge branch 'master' of http://110.41.163.243:9980/kd-aiot/aiot-app

liujt 1 неделя назад
Родитель
Сommit
813ac37031

+ 53 - 0
api/accidentReport/index.js

@@ -0,0 +1,53 @@
+import { get, postJ, putJ, deleteApi } from "@/utils/request";
+import Vue from "vue";
+
+// 保存或更新
+export async function save(data) {
+  const url = Vue.prototype.apiUrl + '/ehs/accidentIncidents/' + (data.id ? 'update' : 'save');
+  const fn = data.id ? putJ : postJ;
+  const res = await fn(url, data);
+  if (res.code == 0) return res.data;
+  return Promise.reject(new Error(res.message));
+}
+
+// 局部修改(处理)
+export async function updatePartial(data) {
+  const res = await postJ(Vue.prototype.apiUrl + '/ehs/accidentIncidents/updatePartial', data);
+  if (res.code == 0) return res.data;
+  return Promise.reject(new Error(res.message));
+}
+
+// 列表
+export async function getList(params) {
+  const res = await postJ(Vue.prototype.apiUrl + '/ehs/accidentIncidents/page', params);
+  if (res.code == 0) return res.data;
+  return Promise.reject(new Error(res.message));
+}
+
+// 审批通过分页(处理列表)
+export async function pageApproved(params) {
+  const res = await postJ(Vue.prototype.apiUrl + '/ehs/accidentIncidents/pageApproved', params);
+  if (res.code == 0) return res.data;
+  return Promise.reject(new Error(res.message));
+}
+
+// 详情
+export async function getById(id) {
+  const res = await get(Vue.prototype.apiUrl + `/ehs/accidentIncidents/getById/${id}`);
+  if (res.code == 0) return res.data;
+  return Promise.reject(new Error(res.message));
+}
+
+// 删除
+export async function remove(data) {
+  const res = await postJ(Vue.prototype.apiUrl + '/ehs/accidentIncidents/delete', data);
+  if (res.code == 0) return res.data;
+  return Promise.reject(new Error(res.message));
+}
+
+// 处理(废弃,使用updatePartial)
+export async function process(data) {
+  const res = await putJ(Vue.prototype.apiUrl + '/ehs/accidentIncidents/process', data);
+  if (res.code == 0) return res.data;
+  return Promise.reject(new Error(res.message));
+}

+ 10 - 0
api/centralizedMeterReading/index.js

@@ -74,3 +74,13 @@ export async function updatePublishStatus(id, publishStatus) {
 
   return Promise.reject(new Error(res.data.message));
 }
+// ===== 获取上期抄表数(根据设备ID) =====
+export async function queryBySubstanceId(substanceId, id) {
+  const res = await get(
+    Vue.prototype.apiUrl + `/ems/remote_meter_reading/queryBySubstanceId?substanceId=${substanceId}&id=${id || ''}`
+  );
+  if (res.code == 0) {
+    return res.data;
+  }
+  return Promise.reject(new Error(res.message));
+}

+ 96 - 0
api/emergencyDrill/workOrder.js

@@ -0,0 +1,96 @@
+import { get, postJ, put, putJ, deleteApi } from "@/utils/request";
+import Vue from "vue";
+
+// 保存(新增)
+export async function save(data) {
+  const res = await postJ(
+    Vue.prototype.apiUrl + "/ehs/EmergencyDrillWorkOrder/save",
+    data.workOrder,
+  );
+  if (res.code == 0) {
+    return res.data;
+  }
+  return Promise.reject(new Error(res.message));
+}
+
+// 更新(编辑)
+export async function update(data) {
+  const res = await putJ(
+    Vue.prototype.apiUrl + "/ehs/EmergencyDrillWorkOrder/update",
+    data.workOrder,
+  );
+  if (res.code == 0) {
+    return res.data;
+  }
+  return Promise.reject(new Error(res.message));
+}
+
+// 实施(从计划生成工单)
+export async function implement(data) {
+  const res = await putJ(
+    Vue.prototype.apiUrl + "/ehs/emergencydrillplan/implement",
+    data,
+  );
+  if (res.code == 0) {
+    return res.data;
+  }
+  return Promise.reject(new Error(res.message));
+}
+
+// 分页列表
+export async function getList(params) {
+  const res = await get(
+    Vue.prototype.apiUrl + "/ehs/EmergencyDrillWorkOrder/page",
+    params,
+  );
+  if (res.code == 0) {
+    return res.data;
+  }
+  return Promise.reject(new Error(res.message));
+}
+
+// 详情
+export async function getById(id) {
+  const res = await get(
+    Vue.prototype.apiUrl + `/ehs/EmergencyDrillWorkOrder/getById/${id}`,
+  );
+  if (res.code == 0) {
+    return res.data;
+  }
+  return Promise.reject(new Error(res.message));
+}
+
+// 过程记录(报工)
+export async function recordProcess(data) {
+  const res = await putJ(
+    Vue.prototype.apiUrl + "/ehs/EmergencyDrillWorkOrder/recordProcess",
+    data,
+  );
+  if (res.code == 0) {
+    return res.data;
+  }
+  return Promise.reject(new Error(res.message));
+}
+
+// 演练评估(验收)
+export async function evaluate(data) {
+  const res = await putJ(
+    Vue.prototype.apiUrl + "/ehs/EmergencyDrillWorkOrder/evaluate",
+    data,
+  );
+  if (res.code == 0) {
+    return res.data;
+  }
+  return Promise.reject(new Error(res.message));
+}
+
+// 详情
+export async function getEmergencyplanById(id) {
+  const res = await get(
+    Vue.prototype.apiUrl + `/ehs/emergencyplan/getById/${id}`,
+  );
+  if (res.code == 0) {
+    return res.data;
+  }
+  return Promise.reject(new Error(res.message));
+}

+ 103 - 103
enum/dict.js

@@ -12,22 +12,23 @@ export default {
   取样类型: "quality_method_code",
   计量单位: "measuring_uint",
   销售类型: "order_type",
-  记录规则类型: 'record_sheet',
-  表计类型: 'meter_type',
-  不拆物料层规格: 'material_layer',
-  结算方式: 'settlement_mode',
-  质保期单位: 'date_unit',
-  根因类型: 'root_cause_type',
-  应急预案类型: 'emergency_plan_type',
-  应急演练类型: 'drill_type',
-  应急演练级别: 'drill_level',
-  事故级别: 'acdnt_level',
-  事故事件类型: 'acdnt_type',
+  记录规则类型: "record_sheet",
+  表计类型: "meter_type",
+  不拆物料层规格: "material_layer",
+  结算方式: "settlement_mode",
+  质保期单位: "date_unit",
+  根因类型: "root_cause_type",
+  应急预案类型: "emergency_plan_type",
+  应急演练类型: "drill_type",
+  应急演练级别: "drill_level",
+  事故级别: "acdnt_level",
+  事故事件类型: "acdnt_type",
 };
 export const numberList = [
-
-  'quality_method_code',
-  
+  "emergency_plan_type",
+  "drill_type",
+  "drill_level",
+  "quality_method_code",
 ];
 //发货审核状态
 export const reviewStatusEnum = [
@@ -127,17 +128,17 @@ export const requirementSourceType = [
 
 // 质检状态 0未检 1已检
 export const qualityStatus = {
-  0: '未质检',
-  1: '待检',
-  2: '已质检'
+  0: "未质检",
+  1: "待检",
+  2: "已质检",
 };
 
 // 质检结果 0无 1合格 2不合格
 export const qualityResults = {
-  0: '无',
-  1: '合格',
-  2: '不合格',
-  3: '让步接收'
+  0: "无",
+  1: "合格",
+  2: "不合格",
+  3: "让步接收",
 };
 
 // 列表维度
@@ -234,27 +235,27 @@ export const warehousingType = [
 ];
 
 export const sceneState = [
-  { code: 1, label: '生产入库', documentsName: '生产工单' },
-  { code: 2, label: '采购入库', documentsName: '采购收货单' },
-  { code: 3, label: '归还入库', documentsName: '出库单' },
-  { code: 4, label: '领料退货入库', documentsName: '领料退货单' },
+  { code: 1, label: "生产入库", documentsName: "生产工单" },
+  { code: 2, label: "采购入库", documentsName: "采购收货单" },
+  { code: 3, label: "归还入库", documentsName: "出库单" },
+  { code: 4, label: "领料退货入库", documentsName: "领料退货单" },
   // { code: 5, label: '其他入库' },
-  { code: 6, label: '销售退货入库', documentsName: '销售订单' },
-  { code: 7, label: '销售受托入库', documentsName: '受托收货单' },
-  { code: 8, label: '半成品入库', documentsName: '委外工单' },
-  { code: 9, label: '外协入库', documentsName: '销售退货处理单' },
-  { code: 10, label: '委外入库', documentsName: '委外订单' },
-  { code: 11, label: '委外退货入库', documentsName: '委外发货单' },
-  { code: 12, label: '委外入库(非采购)', documentsName: '委外申请单' },
-  { code: 13, label: '受托入库', documentsName: '受托收货单' },
-  { code: 14, label: '项目入库', documentsName: '项目编码' },
-  { code: 15, label: '调拨入库' },
-  { code: 16, label: '异常处理入库' },
+  { code: 6, label: "销售退货入库", documentsName: "销售订单" },
+  { code: 7, label: "销售受托入库", documentsName: "受托收货单" },
+  { code: 8, label: "半成品入库", documentsName: "委外工单" },
+  { code: 9, label: "外协入库", documentsName: "销售退货处理单" },
+  { code: 10, label: "委外入库", documentsName: "委外订单" },
+  { code: 11, label: "委外退货入库", documentsName: "委外发货单" },
+  { code: 12, label: "委外入库(非采购)", documentsName: "委外申请单" },
+  { code: 13, label: "受托入库", documentsName: "受托收货单" },
+  { code: 14, label: "项目入库", documentsName: "项目编码" },
+  { code: 15, label: "调拨入库" },
+  { code: 16, label: "异常处理入库" },
   {
     code: 17,
-    label: '回收入库'
+    label: "回收入库",
   },
-  { code: 99, label: '其他入库' }
+  { code: 99, label: "其他入库" },
 ];
 
 export const outputSceneState = [
@@ -288,75 +289,74 @@ export const materialType = [
 
 //来源类型
 export const relationTypeOption = {
-  1: '发货确认单',
-  2: '收货确认单'
+  1: "发货确认单",
+  2: "收货确认单",
 };
 
-
 // 用能单位集合
 export const energyConsumingUnitList = [
   {
-    value: '1',
-    label: '水',
+    value: "1",
+    label: "水",
     unit: [
-      { value: '1', label: 'm³' },
-      { value: '2', label: 'L' },
-      { value: '3', label: 'T' }
+      { value: "1", label: "m³" },
+      { value: "2", label: "L" },
+      { value: "3", label: "T" },
     ],
     priceUnit: [
-      { value: '1', label: '元/m³' },
-      { value: '2', label: '元/L' },
-      { value: '3', label: '元/T' }
-    ]
+      { value: "1", label: "元/m³" },
+      { value: "2", label: "元/L" },
+      { value: "3", label: "元/T" },
+    ],
   },
   {
-    value: '2',
-    label: '电',
+    value: "2",
+    label: "电",
     unit: [
-      { value: '1', label: 'kWh' },
-      { value: '2', label: 'MW' },
-      { value: '3', label: 'VA' }
+      { value: "1", label: "kWh" },
+      { value: "2", label: "MW" },
+      { value: "3", label: "VA" },
     ],
     priceUnit: [
-      { value: '1', label: '元/kWh' },
-      { value: '2', label: '元/MWh' },
-      { value: '3', label: '元/VA' }
-    ]
+      { value: "1", label: "元/kWh" },
+      { value: "2", label: "元/MWh" },
+      { value: "3", label: "元/VA" },
+    ],
   },
   {
-    value: '3',
-    label: '气',
+    value: "3",
+    label: "气",
     unit: [
       // { value: '1', label: 'J' },
       // { value: '2', label: 'kJ' },
       // { value: '3', label: 'MJ' },
       // { value: '4', label: 'GJ' },
-      { value: '5', label: 'm³' }
+      { value: "5", label: "m³" },
     ],
     priceUnit: [
       // { value: '1', label: '元/J' },
       // { value: '2', label: '元/kJ' },
       // { value: '3', label: '元/MJ' },
       // { value: '4', label: '元/GJ' },
-      { value: '5', label: '元/m³' }
-    ]
+      { value: "5", label: "元/m³" },
+    ],
   },
   {
-    value: '4',
-    label: '热力',
+    value: "4",
+    label: "热力",
     unit: [
-      { value: '1', label: 'J' },
-      { value: '2', label: 'kJ' },
-      { value: '3', label: 'MJ' },
-      { value: '4', label: 'GJ' },
+      { value: "1", label: "J" },
+      { value: "2", label: "kJ" },
+      { value: "3", label: "MJ" },
+      { value: "4", label: "GJ" },
     ],
     priceUnit: [
-      { value: '1', label: '元/J' },
-      { value: '2', label: '元/kJ' },
-      { value: '3', label: '元/MJ' },
-      { value: '4', label: '元/GJ' },
-    ]
-  }
+      { value: "1", label: "元/J" },
+      { value: "2", label: "元/kJ" },
+      { value: "3", label: "元/MJ" },
+      { value: "4", label: "元/GJ" },
+    ],
+  },
 ];
 
 export const warehouseDefinition_areaType = [
@@ -384,43 +384,43 @@ export const orderTypeEnum = [
   { code: 4, label: "不定向订单" },
   { code: 5, label: "委外订单" },
   { code: 0, label: "库存式订单" },
-]
+];
 
 //销售订单整体进度
 export const saleOrderProgressStatusEnum = [
-  { value: 0, label: '未开始' },
-  { value: 100, label: '待排程' },
-  { value: 200, label: '已排程' },
-  { value: 300, label: '待派单' },
-  { value: 400, label: '已派单' },
-  { value: 500, label: '生产执行中' },
-  { value: 501, label: '部分入库' },
-  { value: 600, label: '已入库' },
-  { value: 700, label: '待发货' },
-  { value: 701, label: '部分发货' },
-  { value: 800, label: '全部发货' },
-  { value: 1000, label: '完成' }
+  { value: 0, label: "未开始" },
+  { value: 100, label: "待排程" },
+  { value: 200, label: "已排程" },
+  { value: 300, label: "待派单" },
+  { value: 400, label: "已派单" },
+  { value: 500, label: "生产执行中" },
+  { value: 501, label: "部分入库" },
+  { value: 600, label: "已入库" },
+  { value: 700, label: "待发货" },
+  { value: 701, label: "部分发货" },
+  { value: 800, label: "全部发货" },
+  { value: 1000, label: "完成" },
 ];
 
 //采购订单整体进度
 export const purchaseOrderProgressStatusEnum = [
-  { value: 0, label: '未开始' },
-  { value: 100, label: '在途' },
-  { value: 101, label: '部分入库' },
-  { value: 102, label: '待质检' },
-  { value: 103, label: '质检中' },
-  { value: 200, label: '已入库' },
-  { value: 1000, label: '完成' }
+  { value: 0, label: "未开始" },
+  { value: 100, label: "在途" },
+  { value: 101, label: "部分入库" },
+  { value: 102, label: "待质检" },
+  { value: 103, label: "质检中" },
+  { value: 200, label: "已入库" },
+  { value: 1000, label: "完成" },
 ];
 
 // 状态(业务状态)
 export const businessStatus = [
-  { code: 0, label: '空闲' },
-  { code: 1, label: '占用' },
-  { code: 2, label: '故障' },
-  { code: 3, label: '维修' },
-  { code: 4, label: '保养' },
-  { code: 5, label: '巡点检' }
+  { code: 0, label: "空闲" },
+  { code: 1, label: "占用" },
+  { code: 2, label: "故障" },
+  { code: 3, label: "维修" },
+  { code: 4, label: "保养" },
+  { code: 5, label: "巡点检" },
 ];
 
-export const orderSourceType = ['3', '4', '5', '6', '7'];
+export const orderSourceType = ["3", "4", "5", "6", "7"];

+ 2 - 2
manifest.json

@@ -2,7 +2,7 @@
     "name" : "AiMil工业互联网平台",
     "appid" : "__UNI__45B3907",
     "description" : "",
-    "versionName" : "V1.0.4.28",
+    "versionName" : "V1.0.4.29",
     "versionCode" : "100",
     "transformPx" : false,
     "h5" : {
@@ -14,7 +14,7 @@
                     // "target" : "http://114.116.248.196:86/api/",
                     // "target": "http://116.63.185.248:80/api",
                     // "target" : "http://192.168.1.251:18086/",
-					  "target" : "http://192.168.1.125:18086/",
+                    "target" : "http://192.168.1.102:18086/",
                     // "target" : "http://aiot.zoomwin.com.cn:51001/api",
                     "changeOrigin" : true,
                     "secure" : false,

+ 28 - 13
pages.json

@@ -2754,21 +2754,24 @@
 				"navigationStyle": "custom",
 				"navigationBarTextStyle": "black"
 			}
-		}, {
+		},
+		{
 			"path": "pages/pcs/QualityInspection",
 			"style": {
 				"navigationBarTitleText": "来煤质检记录",
 				"navigationStyle": "custom",
 				"navigationBarTextStyle": "black"
 			}
-		}, {
+		},
+		{
 			"path": "pages/pcs/HaulSlag",
 			"style": {
 				"navigationBarTitleText": "拉渣记录表",
 				"navigationStyle": "custom",
 				"navigationBarTextStyle": "black"
 			}
-		}, {
+		},
+		{
 			"path": "pages/pcs/TransportAsh",
 			"style": {
 				"navigationBarTitleText": "拉灰记录表 ",
@@ -2792,10 +2795,6 @@
 				"navigationBarTextStyle": "black"
 			}
 		},
-
-
-
-
 		//安全生产
 		{
 			"path": "pages/ehs/snapshot/mySnapshot",
@@ -2804,24 +2803,39 @@
 				"navigationStyle": "custom",
 				"navigationBarTextStyle": "black"
 			}
-		}, {
+		},
+		{
 			"path": "pages/ehs/snapshot/snapshotManage",
 			"style": {
 				"navigationBarTitleText": "随手拍管理",
 				"navigationStyle": "custom",
 				"navigationBarTextStyle": "black"
 			}
-		}, {
+		},
+		{
 			"path": "pages/ehs/hazardManagement/hazardList",
 			"style": {
 				"navigationBarTitleText": "隐患台账",
 				"navigationStyle": "custom",
 				"navigationBarTextStyle": "black"
 			}
+		},
+		{
+			"path": "pages/ehs/emergencyDrill/workOrder",
+			"style": {
+				"navigationBarTitleText": "应急演练",
+				"navigationStyle": "custom",
+				"navigationBarTextStyle": "black"
+			}
+		},
+		{
+			"path": "pages/ehs/accidentReport/index",
+			"style": {
+				"navigationBarTitleText": "事故事件",
+				"navigationStyle": "custom",
+				"navigationBarTextStyle": "black"
+			}
 		}
-
-
-
 	],
 	"tabBar": {
 		"color": "#908f8f",
@@ -2829,7 +2843,8 @@
 		"borderStyle": "black",
 		"backgroundColor": "#ffffff",
 		"fontSize": "14px",
-		"list": [{
+		"list": [
+			{
 				"pagePath": "pages/home/home",
 				"iconPath": "static/tab/home.png",
 				"selectedIconPath": "static/tab/home_.png",

+ 303 - 0
pages/ehs/accidentReport/accident.vue

@@ -0,0 +1,303 @@
+<template>
+  <view class="accident-list">
+    <!-- 搜索 -->
+    <view class="search-row">
+      <input
+        class="search-input"
+        v-model="searchName"
+        placeholder="事故事件名称"
+      />
+
+      <button class="search-btn" @click="handleSearch">搜索</button>
+    </view>
+
+    <u-list @scrolltolower="loadMore" class="list-content">
+      <view v-for="(item, idx) in listData" :key="item.id" class="card-wrapper">
+        <myCard
+          :item="item"
+          :index="idx + 1"
+          :btnList="getBtnList(item)"
+          :columns="cardColumns"
+          :title="item.acdntName"
+          :status="getStatusLabel(item.approvalStatus)"
+          @goDetail="goDetail"
+          @edit="handleEdit"
+          @handleSubmit="handleSubmit"
+          @handleDelete="handleDelete"
+        />
+      </view>
+      <view style="height: 20rpx"></view>
+      <view v-if="loading" class="load-more">加载中...</view>
+      <view v-if="isEnd && listData.length > 0" class="load-more"
+        >没有更多了</view
+      >
+      <u-empty v-if="!loading && listData.length === 0" text="暂无数据" />
+    </u-list>
+
+    <!-- 新增按钮 -->
+    <view class="add-btn" @click="handleAdd">
+      <u-icon name="plus" color="#fff" size="40"></u-icon>
+    </view>
+
+    <!-- 弹窗 -->
+    <accidentDialog ref="dialogRef" @reload="reloadList" />
+    <!-- <processSubmitDialog ref="processSubmitRef" @reload="reloadList" /> -->
+
+    <u-toast ref="uToast" />
+  </view>
+</template>
+
+<script>
+import myCard from "@/components/myCard.vue";
+import accidentDialog from "./accidentDialog.vue";
+
+import { getList, remove } from "@/api/accidentReport/index.js";
+import { reviewStatus } from "@/enum/dict";
+
+export default {
+  components: { myCard, accidentDialog },
+  data() {
+    return {
+      searchName: "",
+      deptId: null,
+      listData: [],
+      page: 1,
+      size: 10,
+      isEnd: false,
+      loading: false,
+      cardColumns: [
+        [{ prop: "acdntCode", label: "编码", className: "perce100" }],
+        [{ prop: "acdntPlace", label: "发生地点", className: "perce100" }],
+        [
+          { prop: "dutyUnitName", label: "责任单位", className: "perce50" },
+          { prop: "acdntLevelName", label: "事故级别", className: "perce50" },
+        ],
+        [{ prop: "occurrenceTime", label: "发生时间", className: "perce100" }],
+        [
+          {
+            label: "操作",
+            prop: "action",
+            type: "action",
+            className: "perce100",
+          },
+        ],
+      ],
+    };
+  },
+  mounted() {
+    this.getList();
+  },
+  methods: {
+    onDeptConfirm(data) {
+      const id = data[0];
+      this.deptId = id;
+      const findDept = (list) => {
+        for (const item of list) {
+          if (item.id === id) return item.name;
+          if (item.children) {
+            const found = findDept(item.children);
+            if (found) return found;
+          }
+        }
+        return null;
+      };
+      this.deptName = findDept(this.deptList) || "责任单位";
+      this.page = 1;
+      this.isEnd = false;
+      this.getList();
+    },
+
+    getStatusLabel(status) {
+      return;
+      return reviewStatus[status] || status;
+    },
+
+    getBtnList(item) {
+      const btns = [];
+      const canEdit = [0, 3].includes(item.approvalStatus);
+      if (canEdit) {
+        btns.push({
+          name: "编辑",
+          apiName: "edit",
+          btnType: "primary",
+          judge: [{ fn: () => true }],
+        });
+        btns.push({
+          name: "提交",
+          apiName: "handleSubmit",
+          btnType: "success",
+          judge: [{ fn: () => true }],
+        });
+        btns.push({
+          name: "删除",
+          apiName: "handleDelete",
+          btnType: "danger",
+          judge: [{ fn: () => true }],
+        });
+      }
+      // 查看始终有
+      btns.push({
+        name: "详情",
+        apiName: "goDetail",
+        btnType: "info",
+        judge: [{ fn: () => true }],
+      });
+      return btns;
+    },
+
+    goDetail(item) {
+      this.$refs.dialogRef.open(item, "view");
+    },
+    handleAdd() {
+      this.$refs.dialogRef.open(null, "add");
+    },
+    handleEdit(item) {
+      this.$refs.dialogRef.open(item, "edit");
+    },
+
+    handleSubmit(item) {
+      this.$refs.processSubmitRef.open({
+        businessId: item.id,
+        businessKey: "ehs_accident_incidents",
+        formCreateUserId: item.createUserId,
+        variables: {
+          businessCode: item.acdntCode,
+          businessName: item.acdntName,
+          businessType: item.acdntTypeName,
+        },
+      });
+    },
+
+    async handleDelete(item) {
+      const res = await uni.showModal({
+        title: "提示",
+        content: "确认删除该事故记录吗?",
+      });
+      if (res.confirm) {
+        try {
+          await remove([item.id]);
+          uni.showToast({ title: "删除成功", icon: "success" });
+          this.reloadList();
+        } catch (e) {
+          this.$refs.uToast.show({ type: "error", message: e.message });
+        }
+      }
+    },
+
+    handleSearch() {
+      this.page = 1;
+      this.isEnd = false;
+      this.getList();
+    },
+
+    reloadList() {
+      this.page = 1;
+      this.isEnd = false;
+      this.getList();
+    },
+
+    async getList() {
+      if (this.loading || this.isEnd) return;
+      this.loading = true;
+      try {
+        const params = {
+          pageNum: this.page,
+          size: this.size,
+          acdntName: this.searchName || undefined,
+          dutyUnitId: this.deptId || undefined,
+        };
+        const res = await getList(params);
+        const list = res.list || [];
+        if (this.page === 1) this.listData = list;
+        else this.listData = this.listData.concat(list);
+        this.isEnd =
+          list.length < this.size || this.listData.length >= res.count;
+        this.page += 1;
+      } catch (e) {
+        this.$refs.uToast.show({
+          type: "error",
+          message: e.message || "加载失败",
+        });
+      } finally {
+        this.loading = false;
+      }
+    },
+
+    loadMore() {
+      if (!this.isEnd && !this.loading) this.getList();
+    },
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+.accident-list {
+  .search-row {
+    display: flex;
+    align-items: center;
+    gap: 12rpx;
+    padding: 16rpx 0;
+    background: #fff;
+    border-radius: 24rpx;
+    padding: 16rpx 20rpx;
+    margin-bottom: 16rpx;
+    .search-input {
+      flex: 1;
+      height: 60rpx;
+      background: #f5f7fb;
+      border-radius: 48rpx;
+      padding: 0 20rpx;
+      font-size: 26rpx;
+    }
+    .dept-btn {
+      display: flex;
+      align-items: center;
+      height: 60rpx;
+      padding: 0 20rpx;
+      background: #e8edf4;
+      border-radius: 48rpx;
+      color: #2979ff;
+      font-size: 24rpx;
+      gap: 4rpx;
+      .arrow {
+        font-size: 18rpx;
+      }
+    }
+    .search-btn {
+      height: 60rpx;
+      padding: 0 28rpx;
+      background: $theme-color;
+      border-radius: 48rpx;
+      color: #fff;
+      font-size: 26rpx;
+      line-height: 60rpx;
+    }
+  }
+  .list-content {
+    height: calc(100vh - 300rpx);
+  }
+  .card-wrapper {
+    margin-top: 16rpx;
+  }
+  .add-btn {
+    position: fixed;
+    bottom: 120rpx;
+    right: 40rpx;
+    width: 100rpx;
+    height: 100rpx;
+    border-radius: 50%;
+    background: $theme-color;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.15);
+    z-index: 999;
+  }
+  .load-more {
+    text-align: center;
+    font-size: 26rpx;
+    color: #aaa;
+    padding: 20rpx 0;
+  }
+}
+</style>

+ 717 - 0
pages/ehs/accidentReport/accidentDialog.vue

@@ -0,0 +1,717 @@
+<template>
+  <u-popup
+    :show="visible"
+    mode="bottom"
+    :round="10"
+    :closeOnClickOverlay="false"
+    @close="cancel"
+    class="accident-popup"
+  >
+    <view class="popup-content">
+      <view class="popup-header"
+        ><text class="popup-title">{{ dialogTitle }}</text
+        ><view class="close-btn" @click="cancel">×</view></view
+      >
+      <scroll-view class="popup-body" scroll-y>
+        <view class="card-a">
+          <!-- 事故事件信息 -->
+          <view class="card-section">
+            <view class="section-title">📋 事故事件信息</view>
+            <u--form
+              labelPosition="left"
+              labelWidth="180rpx"
+              :model="form"
+              :rules="rules"
+              ref="formRef"
+            >
+              <u-form-item
+                label="事故事件名称"
+                prop="acdntName"
+                borderBottom
+                required
+              >
+                <u--input
+                  v-model="form.acdntName"
+                  placeholder="请输入"
+                  :disabled="isView"
+                  border="none"
+                  inputAlign="right"
+                />
+              </u-form-item>
+              <u-form-item
+                label="发生时间"
+                prop="occurrenceTime"
+                borderBottom
+                required
+              >
+                <view
+                  class="picker-value"
+                  @click="!isView && showDateTimePicker('occurrenceTime')"
+                  >{{ form.occurrenceTime || "请选择" }}</view
+                >
+                <u-icon slot="right" name="arrow-right" />
+              </u-form-item>
+              <u-form-item
+                label="事故发生地点"
+                prop="acdntPlace"
+                borderBottom
+                required
+              >
+                <u--input
+                  v-model="form.acdntPlace"
+                  placeholder="请输入"
+                  :disabled="isView"
+                  border="none"
+                  inputAlign="right"
+                />
+              </u-form-item>
+              <u-form-item label="来源类型" borderBottom>
+                <view
+                  class="picker-value"
+                  @click="!isView && showSourceTypePicker()"
+                  >{{ getSourceTypeLabel(form.sourceType) || "请选择" }}</view
+                >
+                <u-icon slot="right" name="arrow-right" />
+              </u-form-item>
+              <u-form-item label="来源单据名称" borderBottom>
+                <u--input
+                  v-model="form.sourceName"
+                  placeholder="请选择"
+                  readonly
+                  :disabled="isView"
+                  border="none"
+                  inputAlign="right"
+                  @click="addSource"
+                />
+              </u-form-item>
+              <u-form-item
+                label="事故事件经过"
+                prop="acdntPass"
+                borderBottom
+                required
+              >
+                <u--textarea
+                  v-model="form.acdntPass"
+                  placeholder="请输入"
+                  :disabled="isView"
+                  border="none"
+                  height="120rpx"
+                />
+              </u-form-item>
+              <u-form-item label="上报人" prop="reportPersonName" borderBottom>
+                <u--input
+                  v-model="form.reportPersonName"
+                  placeholder="请选择"
+                  readonly
+                  :disabled="isView"
+                  border="none"
+                  inputAlign="report"
+                  @click="userOpen('duty')"
+                />
+              </u-form-item>
+              <u-form-item label="上报部门" borderBottom>
+                <u--input
+                  v-model="form.reportDeptName"
+                  disabled
+                  border="none"
+                  inputAlign="right"
+                />
+              </u-form-item>
+              <u-form-item
+                label="上报时间"
+                prop="reportTime"
+                borderBottom
+                required
+              >
+                <view
+                  class="picker-value"
+                  @click="!isView && showDateTimePicker('reportTime')"
+                  >{{ form.reportTime || "请选择" }}</view
+                >
+                <u-icon slot="right" name="arrow-right" />
+              </u-form-item>
+            </u--form>
+          </view>
+
+          <!-- 调查处理信息 -->
+          <view class="card-section" v-if="!isView || form.acdntLevelId">
+            <view class="section-title">🔍 调查处理信息</view>
+            <u--form
+              labelPosition="left"
+              labelWidth="180rpx"
+              :model="form"
+              ref="form2Ref"
+            >
+              <u-form-item
+                label="事故级别"
+                prop="acdntLevelId"
+                borderBottom
+                required
+              >
+                <DictSelection
+                  dictName="事故级别"
+                  clearable
+                  :disabled="isView"
+                  v-model="formData.acdntLevelId"
+                />
+              </u-form-item>
+              <u-form-item
+                label="事故事件类型"
+                prop="acdntTypeId"
+                borderBottom
+                required
+              >
+                <DictSelection
+                  dictName="事故事件类型"
+                  clearable
+                  :disabled="isView"
+                  v-model="formData.acdntTypeId"
+                />
+              </u-form-item>
+              <u-form-item label="事故原因" prop="acdntCauselText" borderBottom>
+                <u--textarea
+                  v-model="form.acdntCauselText"
+                  placeholder="请输入"
+                  :disabled="isView"
+                  border="none"
+                  height="120rpx"
+                />
+              </u-form-item>
+              <u-form-item
+                label="事故责任人"
+                prop="dutyPersonName"
+                borderBottom
+              >
+                <u--input
+                  v-model="form.dutyPersonName"
+                  placeholder="请选择"
+                  readonly
+                  :disabled="isView"
+                  border="none"
+                  inputAlign="right"
+                  @click="userOpen('duty')"
+                />
+              </u-form-item>
+              <u-form-item label="责任单位" borderBottom>
+                <u--input
+                  v-model="form.dutyUnitName"
+                  disabled
+                  border="none"
+                  inputAlign="right"
+                />
+              </u-form-item>
+              <u-form-item label="间接经济损失(万元)" borderBottom>
+                <u--input
+                  v-model="form.indirectEconomicLoss"
+                  type="number"
+                  placeholder="请输入"
+                  :disabled="isView"
+                  border="none"
+                  inputAlign="right"
+                />
+              </u-form-item>
+              <u-form-item label="直接经济损失(万元)" borderBottom>
+                <u--input
+                  v-model="form.directEconomicLoss"
+                  type="number"
+                  placeholder="请输入"
+                  :disabled="isView"
+                  border="none"
+                  inputAlign="right"
+                />
+              </u-form-item>
+            </u--form>
+          </view>
+
+          <!-- 伤亡人员列表 -->
+          <view class="card-section">
+            <view class="section-title"
+              >👥 伤亡人员列表
+              <text class="add-btn" v-if="!isView" @click="userOpen('casualty')"
+                >+ 添加</text
+              ></view
+            >
+            <view
+              v-for="(p, idx) in form.ehsAccidentCasualtiesVOList"
+              :key="idx"
+              class="person-item"
+            >
+              <view class="person-info"
+                ><text class="name">{{ p.userName }}</text
+                ><text class="dept">{{ p.userDeptName }}</text></view
+              >
+              <view class="person-extra">
+                <picker
+                  mode="selector"
+                  :range="injuryDegreeOptions"
+                  range-key="label"
+                  :value="injuryDegreeIndex(p.injuryDegree)"
+                  @change="(e) => onInjuryChange(e, idx)"
+                  :disabled="isView"
+                >
+                  <view class="tag">{{
+                    getInjuryLabel(p.injuryDegree) || "受伤程度"
+                  }}</view>
+                </picker>
+                <input
+                  class="days-input"
+                  v-model="p.lostWorkDays"
+                  type="number"
+                  placeholder="损失工作日"
+                  :disabled="isView"
+                />
+                <text class="delete" v-if="!isView" @click="removeCasualty(idx)"
+                  >✕</text
+                >
+              </view>
+            </view>
+            <view
+              v-if="form.ehsAccidentCasualtiesVOList.length === 0"
+              class="empty-tip"
+              >暂无伤亡人员</view
+            >
+          </view>
+        </view>
+      </scroll-view>
+
+      <view class="popup-footer">
+        <u-button type="default" @click="cancel">取消</u-button>
+        <u-button v-if="!isView" type="primary" @click="save" :loading="loading"
+          >保存</u-button
+        >
+      </view>
+    </view>
+
+    <!-- 选择器:字典、日期、人员等 -->
+    <u-action-sheet
+      :show="showDictPopup"
+      :actions="dictActions"
+      @close="showDictPopup = false"
+      @select="onDictSelect"
+    />
+    <u-datetime-picker
+      :show="showDateTimePopup"
+      v-model="datetimeValue"
+      mode="datetime"
+      @confirm="onDateTimeConfirm"
+      @cancel="showDateTimePopup = false"
+    />
+    <selectUserDialog ref="userSelectRef" @confirm="changePersonel" />
+    <drillOrderSelect
+      ref="drillOrderSelectRef"
+      @confirm="onDrillOrderConfirm"
+    />
+    <u-toast ref="uToast" />
+  </u-popup>
+</template>
+
+<script>
+import drillOrderSelect from "./drillOrderSelect.vue";
+import selectUserDialog from "@/components/selectUserDialog.vue";
+import { save, getById } from "@/api/accidentReport/index.js";
+import dictMixins from "@/mixins/dictMixins";
+
+const defForm = {
+  acdntName: "",
+  occurrenceTime: "",
+  acdntPlace: "",
+  acdntPass: "",
+  sourceType: "",
+  sourceName: "",
+  sourceId: "",
+  reportPersonName: "",
+  reportPersonUserId: "",
+  reportDeptName: "",
+  reportDeptId: "",
+  reportTime: "",
+  acdntLevelId: "",
+  acdntTypeId: "",
+  acdntCauselText: "",
+  dutyUnitId: "",
+  dutyUnitName: "",
+  dutyPersonName: "",
+  dutyPersonId: "",
+  indirectEconomicLoss: undefined,
+  directEconomicLoss: undefined,
+  ehsAccidentCasualtiesVOList: [],
+};
+
+export default {
+  components: { selectUserDialog, drillOrderSelect },
+  mixins: [dictMixins],
+  data() {
+    return {
+      visible: false,
+      loading: false,
+      dialogType: "add",
+      form: JSON.parse(JSON.stringify(defForm)),
+      injuryDegreeOptions: [
+        { value: "1", label: "轻微伤" },
+        { value: "2", label: "轻伤二级" },
+        { value: "3", label: "轻微一级" },
+        { value: "4", label: "重伤二级" },
+        { value: "5", label: "重伤一级" },
+      ],
+      showDictPopup: false,
+      dictActions: [],
+      dictField: "",
+      showDateTimePopup: false,
+      datetimeValue: 0,
+      dateField: "",
+      selectMode: "",
+      rules: {
+        acdntName: { required: true, message: "请输入事故事件名称" },
+        occurrenceTime: { required: true, message: "请选择发生时间" },
+        acdntPlace: { required: true, message: "请输入事故发生地点" },
+        acdntPass: { required: true, message: "请输入事故事件经过" },
+        reportTime: { required: true, message: "请选择上报时间" },
+        acdntLevelId: { required: true, message: "请选择事故级别" },
+        acdntTypeId: { required: true, message: "请选择事故事件类型" },
+      },
+    };
+  },
+  computed: {
+    dialogTitle() {
+      return (
+        { add: "新增事故上报", edit: "编辑事故上报", view: "查看事故上报" }[
+          this.dialogType
+        ] || "事故上报"
+      );
+    },
+    isView() {
+      return this.dialogType === "view";
+    },
+  },
+  methods: {
+    async open(row, type = "add") {
+      this.visible = true;
+      this.dialogType = type;
+      if (row && row.id) {
+        const data = await getById(row.id);
+        this.form = { ...JSON.parse(JSON.stringify(defForm)), ...data };
+        if (!this.form.ehsAccidentCasualtiesVOList)
+          this.form.ehsAccidentCasualtiesVOList = [];
+      } else {
+        this.form = JSON.parse(JSON.stringify(defForm));
+        const userInfo = uni.getStorageSync("userInfo") || {};
+        this.form.reportPersonName = userInfo.name || "";
+        this.form.reportPersonUserId = userInfo.userId || "";
+        this.form.reportDeptName = userInfo.groupName || "";
+        this.form.reportDeptId = userInfo.groupId || "";
+      }
+      this.$nextTick(() => {
+        if (this.$refs.formRef) this.$refs.formRef.setRules(this.rules);
+      });
+    },
+    cancel() {
+      this.visible = false;
+      this.form = JSON.parse(JSON.stringify(defForm));
+    },
+
+    // 日期选择
+    showDateTimePicker(field) {
+      if (this.isView) return;
+      this.dateField = field;
+      this.datetimeValue = this.form[field]
+        ? new Date(this.form[field]).getTime()
+        : Date.now();
+      this.showDateTimePopup = true;
+    },
+    onDateTimeConfirm(e) {
+      this.showDateTimePopup = false;
+      this.form[this.dateField] = this.$u.timeFormat(
+        e.value,
+        "yyyy-mm-dd hh:MM:ss",
+      );
+    },
+
+    onDictSelect(e) {
+      this.showDictPopup = false;
+      this.form[this.dictField] = e.value;
+      if (this.$refs.formRef) this.$refs.formRef.validateField(this.dictField);
+    },
+
+    // 来源类型
+    getSourceTypeLabel(val) {
+      const map = { 1: "应急演练工单" };
+      return map[val] || "";
+    },
+    showSourceTypePicker() {
+      if (this.isView) return;
+      this.dictField = "sourceType";
+      this.dictActions = [{ name: "应急演练工单", value: "1" }];
+      this.showDictPopup = true;
+    },
+
+    // 来源单据
+    addSource() {
+      if (this.isView) return;
+      if (!this.form.sourceType) {
+        this.$refs.uToast.show({
+          type: "warning",
+          message: "请先选择来源类型",
+        });
+        return;
+      }
+      if (this.form.sourceType == "1") this.$refs.drillOrderSelectRef.open();
+    },
+    onDrillOrderConfirm(data) {
+      this.form.sourceName = data.name || "";
+      this.form.sourceId = data.id || "";
+      this.form.acdntPlace = data.planLocation || "";
+    },
+
+    // 打开人员选择
+    userOpen(key) {
+      if (this.isView || this.loading) return;
+      this.userKey = key;
+      // 上报人、责任人:单选
+      if (key === "report" || key === "duty") {
+        this.$refs.userSelectRef.open(2);
+      }
+      // 伤亡人员:多选,传入已选列表禁用
+      if (key === "casualty") {
+        const disabledList = this.form.ehsAccidentCasualtiesVOList.map(
+          (item) => ({
+            id: item.userId,
+          }),
+        );
+        this.$refs.userSelectRef.open(1);
+      }
+    },
+
+    // 统一回调
+    changePersonel(data) {
+      if (!data || data.length === 0) return;
+
+      if (this.userKey === "report") {
+        const person = data[0];
+        this.form.reportPersonName = person.name || "";
+        this.form.reportPersonUserId = person.id || "";
+        this.form.reportDeptName = person.groupName || "";
+        this.form.reportDeptId = person.groupId || "";
+      } else if (this.userKey === "duty") {
+        const person = data[0];
+        this.form.dutyPersonName = person.name || "";
+        this.form.dutyPersonId = person.id || "";
+        this.form.dutyUnitName = person.groupName || "";
+        this.form.dutyUnitId = person.groupId || "";
+      } else if (this.userKey === "casualty") {
+        data.forEach((person) => {
+          this.form.ehsAccidentCasualtiesVOList.push({
+            userId: person.id || "",
+            userName: person.name || "",
+            userDeptId: person.groupId || "",
+            userDeptName: person.groupName || "",
+            injuryDegree: "",
+            lostWorkDays: undefined,
+            accidentId: this.form.id || "",
+          });
+        });
+      }
+    },
+
+    // 伤亡人员受伤程度
+    injuryDegreeIndex(val) {
+      return this.injuryDegreeOptions.findIndex((i) => i.value == val);
+    },
+    getInjuryLabel(val) {
+      const item = this.injuryDegreeOptions.find((i) => i.value == val);
+      return item ? item.label : "";
+    },
+    onInjuryChange(e, idx) {
+      const val = this.injuryDegreeOptions[e.detail.value]?.value;
+      this.$set(
+        this.form.ehsAccidentCasualtiesVOList[idx],
+        "injuryDegree",
+        val,
+      );
+    },
+    removeCasualty(idx) {
+      this.form.ehsAccidentCasualtiesVOList.splice(idx, 1);
+    },
+
+    // 保存
+    async save() {
+      try {
+        await this.$refs.formRef.validate();
+        // 填充分类名称
+        this.form.acdntLevelName = this.getDictValue(
+          "事故级别",
+          this.form.acdntLevelId,
+        );
+        this.form.acdntTypeName = this.getDictValue(
+          "事故事件类型",
+          this.form.acdntTypeId,
+        );
+        this.loading = true;
+        await save(this.form);
+        this.$refs.uToast.show({ type: "success", message: "保存成功" });
+        this.cancel();
+        this.$emit("reload");
+      } catch (e) {
+        if (e && e.message)
+          this.$refs.uToast.show({ type: "error", message: e.message });
+      } finally {
+        this.loading = false;
+      }
+    },
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+.accident-popup {
+  /deep/ .u-popup__content {
+    border-radius: 32rpx 32rpx 0 0;
+    overflow: hidden;
+  }
+}
+.popup-content {
+  height: 80vh;
+  display: flex;
+  flex-direction: column;
+  background: #eff2f7;
+}
+.popup-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 30rpx 32rpx;
+  background: #fff;
+  border-bottom: 2rpx solid #eef2f6;
+  flex-shrink: 0;
+  .popup-title {
+    font-size: 36rpx;
+    font-weight: bold;
+    color: #1f2b3c;
+  }
+  .close-btn {
+    width: 60rpx;
+    height: 60rpx;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    font-size: 52rpx;
+    color: #8e9aae;
+  }
+}
+.popup-body {
+  flex: 1;
+  overflow-y: auto;
+  padding: 24rpx;
+}
+.card-a {
+  background: #fff;
+  border-radius: 48rpx;
+  box-shadow: 0 12rpx 40rpx rgba(0, 0, 0, 0.05);
+  overflow: hidden;
+}
+.card-section {
+  padding: 30rpx 32rpx;
+  &:not(:last-child) {
+    border-bottom: 2rpx solid #f0f2f5;
+  }
+}
+.section-title {
+  font-size: 30rpx;
+  font-weight: 700;
+  color: #1f2a44;
+  margin-bottom: 24rpx;
+  padding-left: 16rpx;
+  border-left: 6rpx solid #4caf50;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  .add-btn {
+    font-size: 26rpx;
+    color: #2979ff;
+    font-weight: normal;
+    padding: 4rpx 16rpx;
+    background: #e8f0fe;
+    border-radius: 30rpx;
+  }
+}
+/deep/ .u-form-item {
+  padding: 12rpx 0;
+  .u-form-item__body__left__label {
+    font-size: 26rpx !important;
+    color: #6c7a91 !important;
+  }
+  .u-input__content__field-wrapper__field {
+    font-size: 26rpx !important;
+    text-align: right;
+  }
+}
+.picker-value {
+  font-size: 26rpx;
+  color: #1e2a3a;
+  text-align: right;
+  flex: 1;
+  min-height: 44rpx;
+  padding: 4rpx 0;
+}
+.person-item {
+  background: #f5f7fb;
+  border-radius: 16rpx;
+  padding: 16rpx;
+  margin-bottom: 12rpx;
+  .person-info {
+    display: flex;
+    gap: 20rpx;
+    .name {
+      font-weight: 600;
+      font-size: 28rpx;
+    }
+    .dept {
+      color: #8e9aae;
+      font-size: 24rpx;
+    }
+  }
+  .person-extra {
+    display: flex;
+    align-items: center;
+    gap: 12rpx;
+    margin-top: 8rpx;
+    .tag {
+      background: #e8edf4;
+      padding: 4rpx 16rpx;
+      border-radius: 30rpx;
+      font-size: 24rpx;
+      color: #2979ff;
+    }
+    .days-input {
+      flex: 1;
+      background: #fff;
+      border-radius: 30rpx;
+      padding: 0 16rpx;
+      height: 50rpx;
+      font-size: 24rpx;
+    }
+    .delete {
+      color: #f56c6c;
+      font-size: 28rpx;
+      padding: 0 8rpx;
+    }
+  }
+}
+.empty-tip {
+  text-align: center;
+  font-size: 26rpx;
+  color: #aaa;
+  padding: 30rpx 0;
+}
+.popup-footer {
+  display: flex;
+  padding: 16rpx 24rpx;
+  background: #fff;
+  border-top: 2rpx solid #eef2f6;
+  gap: 16rpx;
+  flex-shrink: 0;
+  /deep/ .u-button {
+    flex: 1;
+    border-radius: 48rpx;
+    height: 72rpx;
+  }
+}
+</style>

+ 236 - 0
pages/ehs/accidentReport/drillOrderSelect.vue

@@ -0,0 +1,236 @@
+<template>
+  <u-popup
+    :show="visible"
+    mode="bottom"
+    :round="10"
+    :closeOnClickOverlay="false"
+    @close="cancel"
+    class="drill-select-popup"
+  >
+    <view class="popup-content">
+      <view class="popup-header"
+        ><text class="popup-title">选择应急演练工单</text
+        ><view class="close-btn" @click="cancel">×</view></view
+      >
+      <view class="search-row">
+        <input
+          class="search-input"
+          v-model="searchKey"
+          placeholder="请输入演练名称"
+        />
+        <button class="search-btn" @click="reload">搜索</button>
+      </view>
+      <scroll-view class="popup-body" scroll-y @scrolltolower="loadMore">
+        <view
+          v-for="(item, idx) in listData"
+          :key="item.id"
+          class="card-wrapper"
+          @click="selectItem(item)"
+        >
+          <myCard
+            :item="item"
+            :index="idx + 1"
+            :columns="cardColumns"
+            :title="item.name"
+            :showRadio="true"
+            :radioValue="selectedId"
+            @radioChange="selectItem"
+          />
+        </view>
+        <view style="height: 20rpx"></view>
+        <view v-if="loading" class="load-more">加载中...</view>
+        <view v-if="isEnd && listData.length > 0" class="load-more"
+          >没有更多了</view
+        >
+        <u-empty v-if="!loading && listData.length === 0" text="暂无工单" />
+      </scroll-view>
+      <view class="popup-footer">
+        <u-button type="default" @click="cancel">取消</u-button>
+        <u-button type="primary" :disabled="!selectedRow" @click="confirm"
+          >确定</u-button
+        >
+      </view>
+    </view>
+    <u-toast ref="uToast" />
+  </u-popup>
+</template>
+
+<script>
+import myCard from "@/components/myCard.vue";
+import { getList } from "@/api/emergencyDrill/workOrder.js";
+
+export default {
+  components: { myCard },
+  data() {
+    return {
+      visible: false,
+      searchKey: "",
+      listData: [],
+      page: 1,
+      size: 10,
+      isEnd: false,
+      loading: false,
+      selectedRow: null,
+      selectedId: null,
+      cardColumns: [
+        [{ prop: "planName", label: "演练计划名称", className: "perce100" }],
+        [
+          { prop: "planLocation", label: "演练地点", className: "perce50" },
+          {
+            prop: "superviseDeptName",
+            label: "主管部门",
+            className: "perce50",
+          },
+        ],
+      ],
+    };
+  },
+  methods: {
+    open() {
+      this.visible = true;
+      this.searchKey = "";
+      this.page = 1;
+      this.isEnd = false;
+      this.listData = [];
+      this.selectedRow = null;
+      this.selectedId = null;
+      this.getList();
+    },
+    cancel() {
+      this.visible = false;
+    },
+    selectItem(item) {
+      this.selectedRow = item;
+      this.selectedId = item.id;
+    },
+    confirm() {
+      if (!this.selectedRow) {
+        this.$refs.uToast.show({ type: "warning", message: "请选择一条工单" });
+        return;
+      }
+      this.$emit("confirm", this.selectedRow);
+      this.cancel();
+    },
+    reload() {
+      this.page = 1;
+      this.isEnd = false;
+      this.listData = [];
+      this.getList();
+    },
+    async getList() {
+      if (this.loading || this.isEnd) return;
+      this.loading = true;
+      try {
+        const params = {
+          pageNum: this.page,
+          size: this.size,
+          name: this.searchKey || undefined,
+        };
+        const res = await getList(params);
+        const list = res.list || [];
+        if (this.page === 1) this.listData = list;
+        else this.listData = this.listData.concat(list);
+        this.isEnd =
+          list.length < this.size || this.listData.length >= res.count;
+        this.page += 1;
+      } catch (e) {
+        this.$refs.uToast.show({ type: "error", message: e.message });
+      } finally {
+        this.loading = false;
+      }
+    },
+    loadMore() {
+      if (!this.isEnd && !this.loading) this.getList();
+    },
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+.drill-select-popup {
+  /deep/ .u-popup__content {
+    border-radius: 32rpx 32rpx 0 0;
+    overflow: hidden;
+  }
+}
+.popup-content {
+  height: 75vh;
+  display: flex;
+  flex-direction: column;
+  background: #eff2f7;
+}
+.popup-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 30rpx 32rpx;
+  background: #fff;
+  border-bottom: 2rpx solid #eef2f6;
+  flex-shrink: 0;
+  .popup-title {
+    font-size: 36rpx;
+    font-weight: bold;
+  }
+  .close-btn {
+    width: 60rpx;
+    height: 60rpx;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    font-size: 52rpx;
+    color: #8e9aae;
+  }
+}
+.search-row {
+  display: flex;
+  align-items: center;
+  padding: 16rpx 24rpx;
+  background: #fff;
+  gap: 12rpx;
+  flex-shrink: 0;
+  .search-input {
+    flex: 1;
+    height: 64rpx;
+    background: #f5f7fb;
+    border-radius: 48rpx;
+    padding: 0 24rpx;
+    font-size: 26rpx;
+  }
+  .search-btn {
+    height: 64rpx;
+    padding: 0 32rpx;
+    background: $theme-color;
+    border-radius: 48rpx;
+    color: #fff;
+    font-size: 26rpx;
+    line-height: 64rpx;
+  }
+}
+.popup-body {
+  flex: 1;
+  overflow-y: auto;
+  padding: 16rpx 24rpx;
+}
+.card-wrapper {
+  margin-top: 16rpx;
+}
+.popup-footer {
+  display: flex;
+  padding: 16rpx 24rpx;
+  background: #fff;
+  border-top: 2rpx solid #eef2f6;
+  gap: 16rpx;
+  flex-shrink: 0;
+  /deep/ .u-button {
+    flex: 1;
+    border-radius: 48rpx;
+    height: 72rpx;
+  }
+}
+.load-more {
+  text-align: center;
+  font-size: 26rpx;
+  color: #aaa;
+  padding: 20rpx 0;
+}
+</style>

+ 97 - 0
pages/ehs/accidentReport/index.vue

@@ -0,0 +1,97 @@
+<template>
+  <view class="mainBox">
+    <uni-nav-bar
+      fixed
+      statusBar
+      left-icon="back"
+      :title="navTitle"
+      @clickLeft="back"
+    />
+
+    <!-- Tab 切换 -->
+    <view class="tab-bar">
+      <view
+        class="tab-item"
+        :class="{ active: activeTab === 'accident' }"
+        @click="switchTab('accident')"
+        >事故事件</view
+      >
+      <view
+        class="tab-item"
+        :class="{ active: activeTab === 'process' }"
+        @click="switchTab('process')"
+        >处理</view
+      >
+    </view>
+
+    <!-- 内容 -->
+    <view class="tab-content">
+      <accident v-if="activeTab === 'accident'" ref="accidentRef" />
+      <process v-if="activeTab === 'process'" ref="processRef" />
+    </view>
+  </view>
+</template>
+
+<script>
+import accident from "./accident.vue";
+import process from "./process.vue";
+
+export default {
+  components: { accident, process },
+  data() {
+    return {
+      activeTab: "accident",
+    };
+  },
+  computed: {
+    navTitle() {
+      return "事故上报";
+    },
+  },
+  methods: {
+    back() {
+      uni.navigateBack();
+    },
+    switchTab(tab) {
+      if (this.activeTab === tab) return;
+      this.activeTab = tab;
+      // 刷新对应列表
+      this.$nextTick(() => {
+        if (tab === "accident" && this.$refs.accidentRef) {
+          this.$refs.accidentRef.reloadList();
+        } else if (tab === "process" && this.$refs.processRef) {
+          this.$refs.processRef.reloadList();
+        }
+      });
+    },
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+.mainBox {
+  background: #f5f7fb;
+  min-height: 100vh;
+}
+.tab-bar {
+  display: flex;
+  background: #fff;
+  border-bottom: 2rpx solid #eef2f6;
+  .tab-item {
+    flex: 1;
+    text-align: center;
+    padding: 24rpx 0;
+    font-size: 30rpx;
+    color: #666;
+    &.active {
+      color: $theme-color;
+      font-weight: bold;
+      border-bottom: 4rpx solid $theme-color;
+    }
+  }
+}
+.tab-content {
+  flex: 1;
+  padding: 16rpx 24rpx;
+}
+</style>

+ 270 - 0
pages/ehs/accidentReport/process.vue

@@ -0,0 +1,270 @@
+<template>
+  <view class="process-list">
+    <view class="search-row">
+      <input
+        class="search-input"
+        v-model="searchName"
+        placeholder="事故事件名称"
+      />
+      <view class="dept-btn" @click="showDeptPicker"
+        ><text>{{ deptName || "责任单位" }}</text
+        ><text class="arrow">▾</text></view
+      >
+      <button class="search-btn" @click="handleSearch">搜索</button>
+    </view>
+
+    <u-list @scrolltolower="loadMore" class="list-content">
+      <view v-for="(item, idx) in listData" :key="item.id" class="card-wrapper">
+        <myCard
+          :item="item"
+          :index="idx + 1"
+          :btnList="getBtnList(item)"
+          :columns="processColumns"
+          :title="item.acdntName"
+          :status="getStatusLabel(item.approvalStatus)"
+          @goDetail="goDetail"
+          @handleProcess="handleProcess"
+        />
+      </view>
+      <view style="height: 20rpx"></view>
+      <view v-if="loading" class="load-more">加载中...</view>
+      <view v-if="isEnd && listData.length > 0" class="load-more"
+        >没有更多了</view
+      >
+      <u-empty v-if="!loading && listData.length === 0" text="暂无待处理事故" />
+    </u-list>
+
+    <!-- 弹窗 -->
+    <processDialog ref="processDialogRef" @reload="reloadList" />
+    <accidentDialog ref="dialogRef" @reload="reloadList" />
+    <ba-tree-picker
+      ref="treePicker"
+      key="dept"
+      :multiple="false"
+      @select-change="onDeptConfirm"
+      title="选择部门"
+      :localdata="deptList"
+      valueKey="id"
+      textKey="name"
+    />
+    <u-toast ref="uToast" />
+  </view>
+</template>
+
+<script>
+import myCard from "@/components/myCard.vue";
+import processDialog from "./processDialog.vue";
+import accidentDialog from "./accidentDialog.vue";
+import baTreePicker from "@/components/ba-tree-picker/ba-tree-picker.vue";
+import { pageApproved } from "@/api/accidentReport/index.js";
+import { listOrganizations } from "@/api/common.js";
+
+export default {
+  components: { myCard, processDialog, accidentDialog, baTreePicker },
+  data() {
+    return {
+      searchName: "",
+      deptId: null,
+      deptName: "责任单位",
+      deptList: [],
+      listData: [],
+      page: 1,
+      size: 10,
+      isEnd: false,
+      loading: false,
+      processColumns: [
+        [{ prop: "acdntCode", label: "编码", className: "perce100" }],
+        [
+          { prop: "dutyUnitName", label: "责任单位", className: "perce50" },
+          { prop: "acdntPlace", label: "地点", className: "perce50" },
+        ],
+        [{ prop: "occurrenceTime", label: "发生时间", className: "perce100" }],
+        [
+          {
+            prop: "ascertainedStatus",
+            label: "事故原因",
+            className: "perce33",
+            formatter: (row) =>
+              row.ascertainedStatus == 1 ? "已查清" : "未查清",
+          },
+          {
+            prop: "responsibilityStatus",
+            label: "责任人员",
+            className: "perce33",
+            formatter: (row) =>
+              row.responsibilityStatus == 1 ? "已处理" : "未处理",
+          },
+          {
+            prop: "correctiveMeasuresStatus",
+            label: "整改措施",
+            className: "perce33",
+            formatter: (row) =>
+              row.correctiveMeasuresStatus == 1 ? "已落实" : "未落实",
+          },
+        ],
+        [
+          {
+            label: "操作",
+            prop: "action",
+            type: "action",
+            className: "perce100",
+          },
+        ],
+      ],
+    };
+  },
+  mounted() {
+    this.loadDeptList();
+    this.getList();
+  },
+  methods: {
+    async loadDeptList() {
+      try {
+        const res = await listOrganizations({});
+        this.deptList = res || [];
+      } catch (e) {}
+    },
+    showDeptPicker() {
+      this.$refs.treePicker._show();
+    },
+    onDeptConfirm(data) {
+      const id = data[0];
+      this.deptId = id;
+      const find = (list) => {
+        for (const item of list) {
+          if (item.id === id) return item.name;
+          if (item.children) {
+            const f = find(item.children);
+            if (f) return f;
+          }
+        }
+        return null;
+      };
+      this.deptName = find(this.deptList) || "责任单位";
+      this.page = 1;
+      this.isEnd = false;
+      this.getList();
+    },
+    getStatusLabel(status) {
+      return (
+        { 0: "待提交", 1: "审批中", 2: "已通过", 3: "已驳回" }[status] || status
+      );
+    },
+    getBtnList(item) {
+      return [
+        {
+          name: "查看",
+          apiName: "goDetail",
+          btnType: "info",
+          judge: [{ fn: () => true }],
+        },
+        {
+          name: "处理",
+          apiName: "handleProcess",
+          btnType: "primary",
+          judge: [{ fn: () => true }],
+        },
+      ];
+    },
+    goDetail(item) {
+      this.$refs.dialogRef.open(item, "view");
+    },
+    handleProcess(item) {
+      this.$refs.processDialogRef.open(item);
+    },
+    handleSearch() {
+      this.page = 1;
+      this.isEnd = false;
+      this.getList();
+    },
+    reloadList() {
+      this.page = 1;
+      this.isEnd = false;
+      this.getList();
+    },
+    async getList() {
+      if (this.loading || this.isEnd) return;
+      this.loading = true;
+      try {
+        const params = {
+          pageNum: this.page,
+          size: this.size,
+          acdntName: this.searchName || undefined,
+          dutyUnitId: this.deptId || undefined,
+        };
+        const res = await pageApproved(params);
+        const list = res.list || [];
+        if (this.page === 1) this.listData = list;
+        else this.listData = this.listData.concat(list);
+        this.isEnd =
+          list.length < this.size || this.listData.length >= res.count;
+        this.page += 1;
+      } catch (e) {
+        this.$refs.uToast.show({ type: "error", message: e.message });
+      } finally {
+        this.loading = false;
+      }
+    },
+    loadMore() {
+      if (!this.isEnd && !this.loading) this.getList();
+    },
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+.process-list {
+  .search-row {
+    display: flex;
+    align-items: center;
+    gap: 12rpx;
+    padding: 16rpx 20rpx;
+    background: #fff;
+    border-radius: 24rpx;
+    margin-bottom: 16rpx;
+    .search-input {
+      flex: 1;
+      height: 60rpx;
+      background: #f5f7fb;
+      border-radius: 48rpx;
+      padding: 0 20rpx;
+      font-size: 26rpx;
+    }
+    .dept-btn {
+      display: flex;
+      align-items: center;
+      height: 60rpx;
+      padding: 0 20rpx;
+      background: #e8edf4;
+      border-radius: 48rpx;
+      color: #2979ff;
+      font-size: 24rpx;
+      gap: 4rpx;
+      .arrow {
+        font-size: 18rpx;
+      }
+    }
+    .search-btn {
+      height: 60rpx;
+      padding: 0 28rpx;
+      background: $theme-color;
+      border-radius: 48rpx;
+      color: #fff;
+      font-size: 26rpx;
+      line-height: 60rpx;
+    }
+  }
+  .list-content {
+    height: calc(100vh - 300rpx);
+  }
+  .card-wrapper {
+    margin-top: 16rpx;
+  }
+  .load-more {
+    text-align: center;
+    font-size: 26rpx;
+    color: #aaa;
+    padding: 20rpx 0;
+  }
+}
+</style>

+ 269 - 0
pages/ehs/accidentReport/processDialog.vue

@@ -0,0 +1,269 @@
+<template>
+  <u-popup
+    :show="visible"
+    mode="bottom"
+    :round="10"
+    :closeOnClickOverlay="false"
+    @close="cancel"
+    class="process-popup"
+  >
+    <view class="popup-content">
+      <view class="popup-header"
+        ><text class="popup-title">事故事件处理</text
+        ><view class="close-btn" @click="cancel">×</view></view
+      >
+      <scroll-view class="popup-body" scroll-y>
+        <view class="card-a">
+          <view class="card-section">
+            <u--form
+              labelPosition="left"
+              labelWidth="200rpx"
+              :model="form"
+              ref="formRef"
+            >
+              <u-form-item label="事故事件名称" borderBottom
+                ><u--input
+                  v-model="form.acdntName"
+                  disabled
+                  border="none"
+                  inputAlign="right"
+              /></u-form-item>
+              <u-form-item label="责任单位" borderBottom
+                ><u--input
+                  v-model="form.dutyUnitName"
+                  disabled
+                  border="none"
+                  inputAlign="right"
+              /></u-form-item>
+            </u--form>
+          </view>
+
+          <!-- 四个模块 -->
+          <view
+            class="card-section"
+            v-for="section in sections"
+            :key="section.key"
+          >
+            <view class="section-title">{{ section.title }}</view>
+            <u--form
+              labelPosition="left"
+              labelWidth="200rpx"
+              :model="form"
+              ref="formRef"
+            >
+              <u-form-item :label="section.statusLabel" borderBottom>
+                <u-radio-group
+                  v-model="form[section.statusKey]"
+                  placement="row"
+                >
+                  <u-radio :name="1" :labelSize="24"
+                    >已{{ section.statusLabel.replace("是否", "") }}</u-radio
+                  >
+                  <u-radio :name="0" :labelSize="24"
+                    >未{{ section.statusLabel.replace("是否", "") }}</u-radio
+                  >
+                </u-radio-group>
+              </u-form-item>
+              <u-form-item :label="section.noteLabel" borderBottom>
+                <u--textarea
+                  v-model="form[section.noteKey]"
+                  placeholder="请输入"
+                  border="none"
+                  height="100rpx"
+                />
+              </u-form-item>
+              <u-form-item :label="section.materialLabel" borderBottom>
+                <fileMain v-model="form[section.materialKey]" type="add" />
+              </u-form-item>
+            </u--form>
+          </view>
+        </view>
+      </scroll-view>
+      <view class="popup-footer">
+        <u-button type="default" @click="cancel">取消</u-button>
+        <u-button type="primary" @click="save" :loading="loading"
+          >保存</u-button
+        >
+      </view>
+    </view>
+    <u-toast ref="uToast" />
+  </u-popup>
+</template>
+
+<script>
+import fileMain from '@/pages/doc/index.vue';
+
+import { getById, updatePartial } from "@/api/accidentReport/index.js";
+
+const defForm = {
+  acdntName: "",
+  dutyUnitName: "",
+  ascertainedStatus: 0,
+  ascertainedNote: "",
+  ascertainedMaterials: [],
+  responsibilityStatus: 0,
+  responsibilityNote: "",
+  responsibilityMaterials: [],
+  correctiveMeasuresStatus: 0,
+  correctiveMeasuresNote: "",
+  correctiveMeasuresMaterials: [],
+  personnelEducationStatus: 0,
+  personnelEducationNote: "",
+  personnelEducationMaterials: [],
+};
+
+export default {
+  components: { fileMain },
+  data() {
+    return {
+      visible: false,
+      loading: false,
+      form: JSON.parse(JSON.stringify(defForm)),
+      sections: [
+        {
+          key: "ascertained",
+          title: "事故查明情况",
+          statusLabel: "是否查清",
+          statusKey: "ascertainedStatus",
+          noteKey: "ascertainedNote",
+          materialKey: "ascertainedMaterials",
+        },
+        {
+          key: "responsibility",
+          title: "责任人员处理",
+          statusLabel: "是否处理",
+          statusKey: "responsibilityStatus",
+          noteKey: "responsibilityNote",
+          materialKey: "responsibilityMaterials",
+        },
+        {
+          key: "corrective",
+          title: "整改措施落实",
+          statusLabel: "是否落实",
+          statusKey: "correctiveMeasuresStatus",
+          noteKey: "correctiveMeasuresNote",
+          materialKey: "correctiveMeasuresMaterials",
+        },
+        {
+          key: "education",
+          title: "相关人员教育",
+          statusLabel: "是否已教育",
+          statusKey: "personnelEducationStatus",
+          noteKey: "personnelEducationNote",
+          materialKey: "personnelEducationMaterials",
+        },
+      ],
+    };
+  },
+  methods: {
+    async open(row) {
+      this.visible = true;
+      const data = await getById(row.id);
+      this.form = { ...JSON.parse(JSON.stringify(defForm)), ...data };
+    },
+    cancel() {
+      this.visible = false;
+      this.form = JSON.parse(JSON.stringify(defForm));
+    },
+    async save() {
+      this.loading = true;
+      try {
+        await updatePartial(this.form);
+        this.$refs.uToast.show({ type: "success", message: "处理成功" });
+        this.cancel();
+        this.$emit("reload");
+      } catch (e) {
+        this.$refs.uToast.show({ type: "error", message: e.message });
+      } finally {
+        this.loading = false;
+      }
+    },
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+.process-popup {
+  /deep/ .u-popup__content {
+    border-radius: 32rpx 32rpx 0 0;
+    overflow: hidden;
+  }
+}
+.popup-content {
+  height: 80vh;
+  display: flex;
+  flex-direction: column;
+  background: #eff2f7;
+}
+.popup-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 30rpx 32rpx;
+  background: #fff;
+  border-bottom: 2rpx solid #eef2f6;
+  flex-shrink: 0;
+  .popup-title {
+    font-size: 36rpx;
+    font-weight: bold;
+    color: #1f2b3c;
+  }
+  .close-btn {
+    width: 60rpx;
+    height: 60rpx;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    font-size: 52rpx;
+    color: #8e9aae;
+  }
+}
+.popup-body {
+  flex: 1;
+  overflow-y: auto;
+  padding: 24rpx;
+}
+.card-a {
+  background: #fff;
+  border-radius: 48rpx;
+  box-shadow: 0 12rpx 40rpx rgba(0, 0, 0, 0.05);
+  overflow: hidden;
+}
+.card-section {
+  padding: 30rpx 32rpx;
+  &:not(:last-child) {
+    border-bottom: 2rpx solid #f0f2f5;
+  }
+}
+.section-title {
+  font-size: 30rpx;
+  font-weight: 700;
+  color: #1f2a44;
+  margin-bottom: 24rpx;
+  padding-left: 16rpx;
+  border-left: 6rpx solid #4caf50;
+}
+/deep/ .u-form-item {
+  padding: 12rpx 0;
+  .u-form-item__body__left__label {
+    font-size: 26rpx !important;
+    color: #6c7a91 !important;
+  }
+  .u-radio-group .u-radio {
+    margin-right: 24rpx;
+  }
+}
+.popup-footer {
+  display: flex;
+  padding: 16rpx 24rpx;
+  background: #fff;
+  border-top: 2rpx solid #eef2f6;
+  gap: 16rpx;
+  flex-shrink: 0;
+  /deep/ .u-button {
+    flex: 1;
+    border-radius: 48rpx;
+    height: 72rpx;
+  }
+}
+</style>

+ 385 - 0
pages/ehs/emergencyDrill/emergencyPlanDialog.vue

@@ -0,0 +1,385 @@
+<template>
+  <u-popup
+    :show="visible"
+    mode="bottom"
+    :round="10"
+    :closeOnClickOverlay="false"
+    @close="cancel"
+    class="emergency-plan-popup"
+  >
+    <view class="popup-content">
+      <!-- 头部 -->
+      <view class="popup-header">
+        <text class="popup-title">{{ dialogTitle }}</text>
+        <view class="close-btn" @click="cancel">×</view>
+      </view>
+
+      <scroll-view class="popup-body" scroll-y>
+        <view class="page">
+          <view class="card-a">
+            <view class="card-section">
+              <!-- ===== 表单 ===== -->
+              <u--form
+                labelPosition="left"
+                labelWidth="160rpx"
+                :model="form"
+                :rules="rules"
+                ref="formRef"
+              >
+                <!-- 预案文件 -->
+                <u-form-item
+                  label="预案文件"
+                  prop="fileUrl"
+                  borderBottom
+                  required
+                >
+                  <fileMain
+                    v-model="form.fileUrl"
+                    :type="isView ? 'view' : ''"
+                  />
+                </u-form-item>
+
+                <!-- 预案名称 -->
+                <u-form-item label="预案名称" prop="name" borderBottom required>
+                  <u--input
+                    v-model="form.name"
+                    placeholder="请输入预案名称"
+                    :disabled="isView"
+                    border="none"
+                    inputAlign="right"
+                  />
+                </u-form-item>
+
+                <!-- 所属部门 -->
+                <u-form-item
+                  label="所属部门"
+                  prop="departmentId"
+                  borderBottom
+                  required
+                >
+                  <view class="picker-value">
+                    {{ form.departmentName || "请选择部门" }}
+                  </view>
+                  <u-icon slot="right" name="arrow-right" />
+                </u-form-item>
+
+                <!-- 预案类型 -->
+                <u-form-item label="预案类型" prop="type" borderBottom required>
+                  <view class="picker-value">
+                    {{ getDictValue("应急预案类型", form.type) || "请选择" }}
+                  </view>
+                  <u-icon slot="right" name="arrow-right" />
+                </u-form-item>
+
+                <!-- 发布时间 -->
+                <u-form-item label="发布时间" prop="publishTime" borderBottom>
+                  <view class="picker-value">
+                    {{ form.publishTime || "请选择" }}
+                  </view>
+                  <u-icon slot="right" name="arrow-right" />
+                </u-form-item>
+
+                <!-- 标签 -->
+                <u-form-item label="标签" prop="tags" borderBottom>
+                  <u--input
+                    v-model="form.tags"
+                    placeholder="请输入标签"
+                    :disabled="isView"
+                    border="none"
+                    inputAlign="right"
+                  />
+                </u-form-item>
+              </u--form>
+            </view>
+
+            <!-- ===== 编写组成员 ===== -->
+            <view class="card-section">
+              <view class="section-title"> 👥 组成员 </view>
+
+              <view
+                v-for="(member, idx) in form.planMemberList"
+                :key="idx"
+                class="member-item"
+              >
+                <view class="member-info">
+                  <text class="member-name">{{ member.memberUserName }}</text>
+                  <text class="member-dept">{{
+                    member.memberUserDeptName
+                  }}</text>
+                  <text class="member-phone">{{ member.phone }}</text>
+                </view>
+              </view>
+
+              <view v-if="form.planMemberList.length === 0" class="empty-tip">
+                暂无组成员
+              </view>
+            </view>
+          </view>
+        </view>
+      </scroll-view>
+
+      <!-- 底部按钮 -->
+      <view class="popup-footer">
+        <u-button type="default" @click="cancel">取消</u-button>
+      </view>
+    </view>
+
+    <u-toast ref="uToast" />
+  </u-popup>
+</template>
+
+<script>
+import fileMain from "@/pages/doc/index.vue";
+import dictMixins from "@/mixins/dictMixins";
+import { getEmergencyplanById } from "@/api/emergencyDrill/workOrder.js";
+
+const defForm = {
+  id: "",
+  name: "",
+  departmentId: "",
+  departmentName: "",
+  type: "",
+  publishTime: "", 
+  tags: "",
+  fileUrl: [],
+  planMemberList: [],
+};
+
+export default {
+  components: { fileMain,  },
+  mixins: [dictMixins],
+  data() {
+    return {
+      visible: false,
+      loading: false,
+      dialogType: "add", // add, edit, view
+      form: JSON.parse(JSON.stringify(defForm)),
+
+      rules: {},
+    };
+  },
+  computed: {
+    dialogTitle() {
+      const map = {
+        add: "新增应急预案",
+        edit: "编辑应急预案",
+        view: "查看应急预案",
+      };
+      return map[this.dialogType] || "应急预案";
+    },
+    isView() {
+      return this.dialogType === "view";
+    },
+  },
+  mounted() {},
+  methods: {
+    // 打开弹窗
+    async open(row, type = "add") {
+      this.visible = true;
+      this.dialogType = type;
+      this.loading = false;
+
+      if (row && row.id) {
+        try {
+          const data = await getEmergencyplanById(row.id);
+          this.form = { ...data };
+        } catch (e) {
+          console.error(e);
+        }
+      } else {
+        this.form = JSON.parse(JSON.stringify(defForm));
+      }
+
+      this.$nextTick(() => {
+        if (this.$refs.formRef) {
+          this.$refs.formRef.setRules(this.rules);
+        }
+      });
+    },
+
+    cancel() {
+      this.visible = false;
+      this.form = JSON.parse(JSON.stringify(defForm));
+    },
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+.emergency-plan-popup {
+  /deep/ .u-popup__content {
+    border-radius: 32rpx 32rpx 0 0;
+    overflow: hidden;
+  }
+
+  .popup-content {
+    height: 80vh;
+    display: flex;
+    flex-direction: column;
+    background: #eff2f7;
+  }
+
+  .popup-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    padding: 30rpx 32rpx;
+    background: #fff;
+    border-bottom: 2rpx solid #eef2f6;
+    flex-shrink: 0;
+
+    .popup-title {
+      font-size: 36rpx;
+      font-weight: bold;
+      color: #1f2b3c;
+    }
+    .close-btn {
+      width: 60rpx;
+      height: 60rpx;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      font-size: 52rpx;
+      color: #8e9aae;
+      line-height: 1;
+    }
+  }
+
+  .popup-body {
+    flex: 1;
+    overflow-y: auto;
+    padding: 24rpx;
+  }
+
+  .page {
+    font-family:
+      system-ui,
+      -apple-system,
+      "Segoe UI",
+      Roboto,
+      sans-serif;
+  }
+
+  .card-a {
+    background: #ffffff;
+    border-radius: 48rpx;
+    box-shadow: 0 12rpx 40rpx rgba(0, 0, 0, 0.05);
+    overflow: hidden;
+  }
+
+  .card-section {
+    padding: 30rpx 32rpx;
+    &:not(:last-child) {
+      border-bottom: 2rpx solid #f0f2f5;
+    }
+  }
+
+  .section-title {
+    font-size: 30rpx;
+    font-weight: 700;
+    color: #1f2a44;
+    margin-bottom: 24rpx;
+    padding-left: 16rpx;
+    border-left: 6rpx solid #4caf50;
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+
+    .add-btn {
+      font-size: 26rpx;
+      color: #2979ff;
+      font-weight: normal;
+      padding: 4rpx 16rpx;
+      background: #e8f0fe;
+      border-radius: 30rpx;
+    }
+  }
+
+  /deep/ .u-form-item {
+    padding: 12rpx 0;
+    .u-form-item__body__left__label {
+      font-size: 26rpx !important;
+      color: #6c7a91 !important;
+    }
+    .u-input__content__field-wrapper__field {
+      font-size: 26rpx !important;
+      text-align: right;
+    }
+  }
+
+  .picker-value {
+    font-size: 26rpx;
+    color: #1e2a3a;
+    text-align: right;
+    flex: 1;
+    min-height: 44rpx;
+    padding: 4rpx 0;
+  }
+
+  // ===== 成员列表 =====
+  .member-item {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    padding: 16rpx 20rpx;
+    background: #f5f7fb;
+    border-radius: 16rpx;
+    margin-bottom: 12rpx;
+
+    .member-info {
+      display: flex;
+      align-items: center;
+      gap: 20rpx;
+      flex: 1;
+      flex-wrap: wrap;
+
+      .member-name {
+        font-size: 28rpx;
+        font-weight: 600;
+        color: #1f2b3c;
+      }
+      .member-dept {
+        font-size: 24rpx;
+        color: #8e9aae;
+      }
+      .member-phone {
+        font-size: 24rpx;
+        color: #8e9aae;
+      }
+    }
+
+    .member-delete {
+      width: 48rpx;
+      height: 48rpx;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      color: #f56c6c;
+      font-size: 28rpx;
+    }
+  }
+
+  .empty-tip {
+    text-align: center;
+    font-size: 26rpx;
+    color: #aaa;
+    padding: 30rpx 0;
+  }
+
+  // ===== 底部按钮 =====
+  .popup-footer {
+    display: flex;
+    padding: 16rpx 24rpx;
+    background: #fff;
+    border-top: 2rpx solid #eef2f6;
+    gap: 16rpx;
+    flex-shrink: 0;
+
+    /deep/ .u-button {
+      flex: 1;
+      border-radius: 48rpx;
+      height: 72rpx;
+    }
+  }
+}
+</style>

+ 360 - 0
pages/ehs/emergencyDrill/workOrder.vue

@@ -0,0 +1,360 @@
+<template>
+  <view class="mainBox">
+    <uni-nav-bar
+      fixed
+      statusBar
+      left-icon="back"
+      :title="navTitle"
+      @clickLeft="back"
+    />
+
+    <!-- 搜索区域 -->
+    <view class="top-wrapper">
+      <input class="search-input" v-model="searchName" placeholder="演练名称" />
+      <!--      <view class="dept-btn" @click="showDeptPicker">
+        <text>{{ deptName || '主管部门' }}</text>
+        <text class="arrow">▾</text>
+      </view> -->
+      <button class="search_btn" @click="handleSearch">搜索</button>
+    </view>
+
+    <!-- 列表 -->
+    <view class="wrapper">
+      <u-list @scrolltolower="scrolltolower" class="listContent">
+        <view
+          v-for="(item, idx) in listData"
+          :key="item.id"
+          style="position: relative"
+        >
+          <myCard
+            :item="item"
+            :index="idx + 1"
+            :btnList="getBtnList(item)"
+            :columns="cardColumns"
+            :title="item.name"
+            :status="getStatusLabel(item.status)"
+            @goDetail="goDetail"
+            @handleReport="handleReport(item)"
+            @handleAcceptance="handleAcceptance(item)"
+          />
+        </view>
+        <view style="width: 100%; height: 40rpx"></view>
+        <view style="margin-top: 20vh" v-if="!loading && listData.length == 0">
+          <u-empty iconSize="150" textSize="32" text="暂无数据" />
+        </view>
+        <view v-if="loading" class="load-more">加载中...</view>
+        <view v-if="isEnd && listData.length > 0" class="load-more"
+          >没有更多了</view
+        >
+      </u-list>
+    </view>
+
+    <!-- 新增按钮 -->
+    <!-- <view class="add" @click="handleAdd">
+      <u-icon name="plus" color="#fff"></u-icon>
+    </view> -->
+
+    <!-- 弹窗 -->
+    <workOrderDialog ref="dialogRef" @reload="reloadList" />
+    <workOrderReportDialog ref="reportRef" @reload="reloadList" />
+    <workOrderReportAcceptanceDialog ref="acceptanceRef" @reload="reloadList" />
+
+    <!-- 部门选择器 -->
+    <ba-tree-picker
+      ref="treePicker"
+      key="dept"
+      :multiple="false"
+      @select-change="onDeptConfirm"
+      title="选择部门"
+      :localdata="deptList"
+      valueKey="id"
+      textKey="name"
+    />
+
+    <u-toast ref="uToast" />
+  </view>
+</template>
+
+<script>
+import myCard from "@/components/myCard.vue";
+import workOrderDialog from "./workOrderDialog.vue";
+import workOrderReportDialog from "./workOrderReportDialog.vue";
+import workOrderReportAcceptanceDialog from "./workOrderReportAcceptanceDialog.vue";
+import { getList } from "@/api/emergencyDrill/workOrder.js";
+import dictMixns from "@/mixins/dictMixins";
+
+export default {
+  mixins: [dictMixns],
+
+  components: {
+    myCard,
+    workOrderDialog,
+    workOrderReportDialog,
+    workOrderReportAcceptanceDialog,
+  },
+  data() {
+    return {
+      searchName: "",
+      deptId: null,
+      deptName: "主管部门",
+      deptList: [],
+      listData: [],
+      page: 1,
+      size: 10,
+      isEnd: false,
+      loading: false,
+      cardColumns: [
+        [{ prop: "code", label: "编码", className: "perce100" }],
+        [
+          {
+            prop: "drillType",
+            label: "演练类型",
+            className: "perce50",
+            formatter: (row) => {
+              return (
+                this.getDictValue("应急演练类型", row.drillType)
+              );
+            },
+          },
+        ],
+        [
+          {
+            prop: "superviseDeptName",
+            label: "主管部门",
+            className: "perce100",
+          },
+        ],
+        [{ prop: "planLocation", label: "演练地点", className: "perce100" }],
+        [
+          {
+            prop: "emergencyPlanName",
+            label: "应急预案",
+            className: "perce100",
+          },
+        ],
+        [
+          {
+            label: "操作",
+            prop: "action",
+            type: "action",
+            className: "perce100",
+          },
+        ],
+      ],
+    };
+  },
+  computed: {
+    navTitle() {
+      return "应急演练工单";
+    },
+  },
+  onLoad() {
+    this.getList();
+    this.requestDict('应急演练类型')
+  },
+  methods: {
+    // 部门选择
+    showDeptPicker() {
+      this.$refs.treePicker._show();
+    },
+    onDeptConfirm(data) {
+      const id = data[0];
+      this.deptId = id;
+      // 查找部门名称
+      const findDept = (list) => {
+        for (const item of list) {
+          if (item.id === id) return item.name;
+          if (item.children && item.children.length > 0) {
+            const found = findDept(item.children);
+            if (found) return found;
+          }
+        }
+        return null;
+      };
+      this.deptName = findDept(this.deptList) || "主管部门";
+      this.page = 1;
+      this.isEnd = false;
+      this.getList();
+    },
+
+    // 状态标签
+    getStatusLabel(status) {
+      const map = {
+        0: "待演练",
+        1: "待验收",
+        2: "已完成",
+      };
+      return map[status] || status;
+    },
+
+    // 按钮列表
+    getBtnList(item) {
+      const btns = [];
+      // 报工(待演练状态)
+      if (item.status == 0) {
+        btns.push({
+          name: "报工",
+          apiName: "handleReport",
+          btnType: "primary",
+          judge: [{ fn: () => true }],
+        });
+      }
+      // 验收(待验收状态)
+      if (item.status == 1) {
+        btns.push({
+          name: "验收",
+          apiName: "handleAcceptance",
+          btnType: "success",
+          judge: [{ fn: () => true }],
+        });
+      }
+
+      return btns;
+    },
+
+    // 列表操作
+    goDetail(item) {
+      this.$refs.dialogRef.open(item, "view");
+    },
+    handleAdd() {
+      // 需要先选择演练计划,简化处理:打开弹窗后选择
+      this.$refs.dialogRef.open(null, "add");
+    },
+    handleReport(item) {
+      this.$refs.reportRef.open(item);
+    },
+    handleAcceptance(item) {
+      this.$refs.acceptanceRef.open(item);
+    },
+
+    // 搜索
+    handleSearch() {
+      this.page = 1;
+      this.isEnd = false;
+      this.getList();
+    },
+
+    // 刷新列表
+    reloadList() {
+      this.page = 1;
+      this.isEnd = false;
+      this.getList();
+    },
+
+    // 加载数据
+    async getList() {
+      if (this.loading || this.isEnd) return;
+      this.loading = true;
+      try {
+        const params = {
+          pageNum: this.page,
+          size: this.size,
+          name: this.searchName || undefined,
+        };
+        const res = await getList(params);
+        const list = res.list || [];
+        if (this.page === 1) {
+          this.listData = list;
+        } else {
+          this.listData = this.listData.concat(list);
+        }
+        this.isEnd =
+          list.length < this.size || this.listData.length >= res.count;
+        this.page += 1;
+      } catch (e) {
+        console.error("加载失败", e);
+        this.$refs.uToast.show({ type: "error", message: "加载失败,请重试" });
+      } finally {
+        this.loading = false;
+      }
+    },
+
+    scrolltolower() {
+      if (!this.isEnd && !this.loading) {
+        this.getList();
+      }
+    },
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+.mainBox {
+  background-color: #f3f8fb;
+  min-height: 100vh;
+}
+
+.top-wrapper {
+  background: #fff;
+  padding: 16rpx 24rpx;
+  display: flex;
+  align-items: center;
+  border-bottom: 2rpx solid #eee;
+  gap: 12rpx;
+
+  .search-input {
+    flex: 1;
+    height: 64rpx;
+    border: 2rpx solid #e9edf2;
+    border-radius: 48rpx;
+    padding: 0 24rpx;
+    font-size: 26rpx;
+    background: #fafafa;
+    min-width: 0;
+  }
+
+  .dept-btn {
+    display: flex;
+    align-items: center;
+    height: 64rpx;
+    padding: 0 20rpx;
+    background: #e8edf4;
+    border-radius: 48rpx;
+    color: #2979ff;
+    font-size: 24rpx;
+    flex-shrink: 0;
+    gap: 4rpx;
+    .arrow {
+      font-size: 18rpx;
+    }
+  }
+
+  .search_btn {
+    height: 64rpx;
+    padding: 0 28rpx;
+    background: $theme-color;
+    font-size: 26rpx;
+    color: #fff;
+    border-radius: 48rpx;
+    text-align: center;
+    line-height: 64rpx;
+    flex-shrink: 0;
+  }
+}
+
+.wrapper {
+  padding: 16rpx 24rpx;
+}
+
+.add {
+  position: fixed;
+  bottom: 100rpx;
+  right: 40rpx;
+  width: 100rpx;
+  height: 100rpx;
+  border-radius: 50%;
+  background: $theme-color;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.15);
+  z-index: 999;
+}
+
+.load-more {
+  text-align: center;
+  font-size: 26rpx;
+  color: #aaa;
+  padding: 20rpx 0 40rpx;
+}
+</style>

+ 539 - 0
pages/ehs/emergencyDrill/workOrderDialog.vue

@@ -0,0 +1,539 @@
+<template>
+  <u-popup
+    :show="visible"
+    mode="bottom"
+    :round="10"
+    :closeOnClickOverlay="false"
+    @close="cancel"
+    class="workorder-popup"
+  >
+    <view class="popup-content">
+      <!-- 头部 -->
+      <view class="popup-header">
+        <text class="popup-title">{{ dialogTitle }}</text>
+        <view class="close-btn" @click="cancel">×</view>
+      </view>
+
+      <scroll-view class="popup-body" scroll-y>
+        <view class="page">
+          <view class="card-a">
+            <!-- ===== 基本信息 ===== -->
+            <view class="card-section">
+              <view class="section-title">📋 基本信息</view>
+              <u--form
+                labelPosition="left"
+                labelWidth="160rpx"
+                :model="form"
+                :rules="rules"
+                ref="formRef"
+              >
+                <u-form-item label="应急演练计划" prop="planName" borderBottom>
+                  <u--input v-model="form.planName" disabled border="none" inputAlign="right" />
+                </u-form-item>
+
+                <u-form-item label="应急预案" prop="emergencyPlanName" borderBottom>
+                  <view class="link-text" @click="handleViewPlan">
+                    {{ form.emergencyPlanName || '请选择' }}
+                  </view>
+                  <u-icon slot="right" name="arrow-right" />
+                </u-form-item>
+
+                <u-form-item label="演练名称" prop="name" borderBottom required>
+                  <u--input
+                    v-model="form.name"
+                    placeholder="请输入"
+                    :disabled="isView"
+                    border="none"
+                    inputAlign="right"
+                  />
+                </u-form-item>
+
+                <u-form-item label="演练类型" prop="drillType" borderBottom required>
+                  <view class="picker-value" @click="!isView && showTypePicker()">
+                    {{ getDictValue('应急演练类型', form.drillType) || '请选择' }}
+                  </view>
+                  <u-icon slot="right" name="arrow-right" />
+                </u-form-item>
+
+                <u-form-item label="主管单位" prop="superviseDeptName" borderBottom>
+                  <u--input v-model="form.superviseDeptName" disabled border="none" inputAlign="right" />
+                </u-form-item>
+
+                <u-form-item label="演练地点" prop="planLocation" borderBottom required>
+                  <u--input
+                    v-model="form.planLocation"
+                    placeholder="请输入"
+                    :disabled="isView"
+                    border="none"
+                    inputAlign="right"
+                  />
+                </u-form-item>
+
+                <u-form-item label="附件" prop="attachments" borderBottom>
+                  <fileMain
+                    v-model="form.attachments"
+                    :type="isView ? 'view' : ''"
+                  />
+                </u-form-item>
+              </u--form>
+            </view>
+
+            <!-- ===== 参演人员 ===== -->
+            <view class="card-section">
+              <view class="section-title">
+                👥 参演人员
+                <text class="add-user-btn" v-if="!isView" @click="addUser">+ 添加</text>
+              </view>
+              <view
+                v-for="(person, idx) in form.participantPersonInfo"
+                :key="idx"
+                class="person-item"
+              >
+                <view class="person-info">
+                  <text class="person-name">{{ person.name }}</text>
+                  <text class="person-dept">{{ person.groupName }}</text>
+                  <text class="person-phone">{{ person.phone }}</text>
+                </view>
+                <view class="person-delete" v-if="!isView" @click="removePerson(idx)">
+                  <text>✕</text>
+                </view>
+              </view>
+              <view v-if="form.participantPersonInfo.length === 0" class="empty-tip">
+                暂无参演人员
+              </view>
+            </view>
+
+            <!-- ===== 演练记录(仅查看) ===== -->
+            <view class="card-section" v-if="isView">
+              <view class="section-title">📝 演练记录</view>
+              <view class="info-item">
+                <text class="info-label">演练实施记录</text>
+                <text class="info-value">{{ form.processRecord || '-' }}</text>
+              </view>
+              <view class="info-item">
+                <text class="info-label">演练时间</text>
+                <text class="info-value">{{ form.drillTime || '-' }}</text>
+              </view>
+              <view class="info-item">
+                <text class="info-label">附件</text>
+                <fileMain v-model="form.processRecordAttachment" type="view" />
+              </view>
+            </view>
+
+            <!-- ===== 验收信息(仅查看) ===== -->
+            <view class="card-section" v-if="isView">
+              <view class="section-title">✅ 验收信息</view>
+              <view class="info-item">
+                <text class="info-label">演练效果和总结评价</text>
+                <text class="info-value">{{ form.drillEvaluation || '-' }}</text>
+              </view>
+              <view class="info-item">
+                <text class="info-label">存在的问题及整改措施</text>
+                <text class="info-value">{{ form.drillEvaluationMeasure || '-' }}</text>
+              </view>
+              <view class="info-item">
+                <text class="info-label">整改情况</text>
+                <text class="info-value">{{ form.inspectionRecordRectification || '-' }}</text>
+              </view>
+              <view class="info-item">
+                <text class="info-label">验收结果</text>
+                <text class="info-value">{{ form.inspectionResult || '-' }}</text>
+              </view>
+              <view class="info-item">
+                <text class="info-label">验收人</text>
+                <text class="info-value">{{ form.inspectorName || '-' }}</text>
+              </view>
+              <view class="info-item">
+                <text class="info-label">附件</text>
+                <fileMain v-model="form.inspectionRecordAttachment" type="view" />
+              </view>
+            </view>
+          </view>
+        </view>
+      </scroll-view>
+
+      <!-- 底部按钮 -->
+      <view class="popup-footer">
+        <u-button type="default" @click="cancel">取消</u-button>
+        <u-button
+          v-if="!isView"
+          type="primary"
+          @click="save"
+          :loading="loading"
+        >保存</u-button>
+      </view>
+    </view>
+
+    <!-- 演练类型选择器 -->
+    <u-action-sheet
+      :show="showTypePickerPopup"
+      :actions="typeActions"
+      title="请选择演练类型"
+      @close="showTypePickerPopup = false"
+      @select="onTypeSelect"
+    />
+
+    <!-- 应急预案查看弹窗 -->
+    <emergencyPlanDialog ref="emergencyPlanDialogRef" />
+
+
+
+    <u-toast ref="uToast" />
+  </u-popup>
+</template>
+
+<script>
+import emergencyPlanDialog from './emergencyPlanDialog.vue';
+
+import fileMain from '@/pages/doc/index.vue';
+import { save, getById, implement } from '@/api/emergencyDrill/workOrder.js';
+import dictMixins from '@/mixins/dictMixins';
+
+export default {
+  components: {  fileMain,emergencyPlanDialog },
+  mixins: [dictMixins],
+  data() {
+    return {
+      visible: false,
+      loading: false,
+      dialogType: 'add', // add, edit, view
+      form: this.getDefaultForm(),
+      showTypePickerPopup: false,
+      typeActions: [],
+      rules: {
+        name: { type: 'string', required: true, message: '请输入演练名称', trigger: ['blur', 'change'] },
+        drillType: { type: 'string', required: true, message: '请选择演练类型', trigger: ['change'] },
+        planLocation: { type: 'string', required: true, message: '请输入演练地点', trigger: ['blur', 'change'] }
+      }
+    };
+  },
+  computed: {
+    dialogTitle() {
+      const map = { add: '新增工单', edit: '编辑工单', view: '工单详情' };
+      return map[this.dialogType] || '工单信息';
+    },
+    isView() {
+      return this.dialogType === 'view';
+    }
+  },
+  methods: {
+    getDefaultForm() {
+      return {
+        id: '',
+        planId: '',
+        planName: '',
+        emergencyPlanId: '',
+        emergencyPlanName: '',
+        name: '',
+        drillType: '',
+        superviseDeptId: '',
+        superviseDeptName: '',
+        planLocation: '',
+        attachments: [],
+        participantPersonInfo: [],
+        processRecord: '',
+        drillTime: '',
+        processRecordAttachment: [],
+        drillEvaluation: '',
+        drillEvaluationMeasure: '',
+        inspectionRecordRectification: '',
+        inspectionResult: '',
+        inspectorName: '',
+        inspectionRecordAttachment: [],
+        status: 0
+      };
+    },
+
+    async open(row, type = 'add') {
+      this.visible = true;
+      this.dialogType = type;
+      this.loading = false;
+      this.form = this.getDefaultForm();
+
+      if (row && row.id && type != 'add') {
+        try {
+          const data = await getById(row.id);
+          this.form = { ...data };
+        } catch (e) {
+          console.error(e);
+        }
+      } else if (row && row.id) {
+        // 从计划创建工单
+        this.form.planId = row.id;
+        this.form.planName = row.name;
+        this.form.emergencyPlanName = row.emergencyPlanName;
+        this.form.emergencyPlanId = row.emergencyPlanId;
+        this.form.drillType = row.drillType;
+        this.form.superviseDeptId = row.responsibleDeptId;
+        this.form.superviseDeptName = row.responsibleDeptName;
+        this.form.planLocation = row.planLocation;
+      }
+
+      // 加载演练类型选项
+      this.typeActions = this.getDictOptions('应急演练类型').map(item => ({
+        name: item.label,
+        value: item.value
+      }));
+
+      this.$nextTick(() => {
+        if (this.$refs.formRef) {
+          this.$refs.formRef.setRules(this.rules);
+        }
+      });
+    },
+
+    cancel() {
+      this.visible = false;
+      this.form = this.getDefaultForm();
+    },
+
+    // 演练类型选择
+    showTypePicker() {
+      if (this.isView) return;
+      this.showTypePickerPopup = true;
+    },
+    onTypeSelect(e) {
+      this.showTypePickerPopup = false;
+      this.form.drillType = e.value;
+      if (this.$refs.formRef) {
+        this.$refs.formRef.validateField('drillType');
+      }
+    },
+
+    // 查看应急预案
+    handleViewPlan() {
+      if (this.form.emergencyPlanId) {
+        this.$refs.emergencyPlanDialogRef.open({ id: this.form.emergencyPlanId }, 'view');
+      }
+    },
+
+    // 添加人员
+    addUser() {
+      this.$refs.userSelectRef.open(null, 0, true);
+    },
+    changePersonel(list) {
+      this.form.participantPersonInfo.push(...list);
+    },
+    removePerson(index) {
+      this.form.participantPersonInfo.splice(index, 1);
+    },
+
+    // 保存
+    async save() {
+      try {
+        if (this.$refs.formRef) {
+          await this.$refs.formRef.validate();
+        }
+        if (this.form.participantPersonInfo.length === 0) {
+          this.$refs.uToast.show({ type: 'warning', message: '请选择参演人员' });
+          return;
+        }
+        this.loading = true;
+        const fn = this.form.id ? save : implement;
+        const res = await fn({ workOrder: this.form, id: this.form.planId });
+        this.$refs.uToast.show({ type: 'success', message: '保存成功' });
+        this.cancel();
+        this.$emit('reload');
+      } catch (e) {
+        if (e && e.message) {
+          this.$refs.uToast.show({ type: 'error', message: e.message });
+        }
+      } finally {
+        this.loading = false;
+      }
+    }
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+.workorder-popup {
+  /deep/ .u-popup__content {
+    border-radius: 32rpx 32rpx 0 0;
+    overflow: hidden;
+  }
+
+  .popup-content {
+    height: calc(100vh - 100px);
+    display: flex;
+    flex-direction: column;
+    background: #eff2f7;
+  }
+
+  .popup-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    padding: 30rpx 32rpx;
+    background: #fff;
+    border-bottom: 2rpx solid #eef2f6;
+    flex-shrink: 0;
+    .popup-title {
+      font-size: 36rpx;
+      font-weight: bold;
+      color: #1f2b3c;
+    }
+    .close-btn {
+      width: 60rpx;
+      height: 60rpx;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      font-size: 52rpx;
+      color: #8e9aae;
+      line-height: 1;
+    }
+  }
+
+  .popup-body {
+    flex: 1;
+    overflow-y: auto;
+    padding: 24rpx;
+  }
+
+  .page {
+    font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
+  }
+
+  .card-a {
+    background: #ffffff;
+    border-radius: 48rpx;
+    box-shadow: 0 12rpx 40rpx rgba(0, 0, 0, 0.05);
+    overflow: hidden;
+  }
+
+  .card-section {
+    padding: 30rpx 32rpx;
+    &:not(:last-child) {
+      border-bottom: 2rpx solid #f0f2f5;
+    }
+  }
+
+  .section-title {
+    font-size: 30rpx;
+    font-weight: 700;
+    color: #1f2a44;
+    margin-bottom: 24rpx;
+    padding-left: 16rpx;
+    border-left: 6rpx solid #4caf50;
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+
+    .add-user-btn {
+      font-size: 26rpx;
+      color: #2979ff;
+      font-weight: normal;
+      padding: 4rpx 16rpx;
+      background: #e8f0fe;
+      border-radius: 30rpx;
+    }
+  }
+
+  /deep/ .u-form-item {
+    padding: 12rpx 0;
+    .u-form-item__body__left__label {
+      font-size: 26rpx !important;
+      color: #6c7a91 !important;
+    }
+    .u-input__content__field-wrapper__field {
+      font-size: 26rpx !important;
+      text-align: right;
+    }
+  }
+
+  .picker-value, .link-text {
+    font-size: 26rpx;
+    color: #1e2a3a;
+    text-align: right;
+    flex: 1;
+    min-height: 44rpx;
+    padding: 4rpx 0;
+  }
+  .link-text {
+    color: #2979ff;
+  }
+
+  // 参演人员
+  .person-item {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    padding: 16rpx 20rpx;
+    background: #f5f7fb;
+    border-radius: 16rpx;
+    margin-bottom: 12rpx;
+
+    .person-info {
+      display: flex;
+      align-items: center;
+      gap: 20rpx;
+      flex: 1;
+      flex-wrap: wrap;
+      .person-name {
+        font-size: 28rpx;
+        font-weight: 600;
+        color: #1f2b3c;
+      }
+      .person-dept {
+        font-size: 24rpx;
+        color: #8e9aae;
+      }
+      .person-phone {
+        font-size: 24rpx;
+        color: #8e9aae;
+      }
+    }
+    .person-delete {
+      width: 48rpx;
+      height: 48rpx;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      color: #f56c6c;
+      font-size: 28rpx;
+    }
+  }
+
+  .empty-tip {
+    text-align: center;
+    font-size: 26rpx;
+    color: #aaa;
+    padding: 30rpx 0;
+  }
+
+  // 信息展示(查看模式)
+  .info-item {
+    display: flex;
+    flex-direction: column;
+    gap: 8rpx;
+    margin-bottom: 20rpx;
+    &:last-child {
+      margin-bottom: 0;
+    }
+    .info-label {
+      font-size: 24rpx;
+      color: #8e9aae;
+    }
+    .info-value {
+      font-size: 26rpx;
+      color: #1f2b3c;
+      line-height: 1.6;
+    }
+  }
+
+  .popup-footer {
+    display: flex;
+    padding: 16rpx 24rpx;
+    background: #fff;
+    border-top: 2rpx solid #eef2f6;
+    gap: 16rpx;
+    flex-shrink: 0;
+    /deep/ .u-button {
+      flex: 1;
+      border-radius: 48rpx;
+      height: 72rpx;
+    }
+  }
+}
+</style>

+ 254 - 0
pages/ehs/emergencyDrill/workOrderReportAcceptanceDialog.vue

@@ -0,0 +1,254 @@
+<template>
+  <u-popup
+    :show="visible"
+    mode="bottom"
+    :round="10"
+    :closeOnClickOverlay="false"
+    @close="cancel"
+    class="acceptance-popup"
+  >
+    <view class="popup-content">
+      <view class="popup-header">
+        <text class="popup-title">验收</text>
+        <view class="close-btn" @click="cancel">×</view>
+      </view>
+
+      <scroll-view class="popup-body" scroll-y>
+        <view class="card-a">
+          <view class="card-section">
+            <u--form
+              labelPosition="left"
+              labelWidth="200rpx"
+              :model="form"
+              :rules="rules"
+              ref="formRef"
+            >
+              <u-form-item label="演练效果和总结评价" prop="drillEvaluation" borderBottom required>
+                <u--textarea
+                  v-model="form.drillEvaluation"
+                  placeholder="请输入"
+                  border="none"
+                  height="120rpx"
+                />
+              </u-form-item>
+
+              <u-form-item label="存在的问题及整改措施" prop="drillEvaluationMeasure" borderBottom>
+                <u--textarea
+                  v-model="form.drillEvaluationMeasure"
+                  placeholder="请输入"
+                  border="none"
+                  height="120rpx"
+                />
+              </u-form-item>
+
+              <u-form-item label="整改情况" prop="inspectionRecordRectification" borderBottom>
+                <u--textarea
+                  v-model="form.inspectionRecordRectification"
+                  placeholder="请输入"
+                  border="none"
+                  height="120rpx"
+                />
+              </u-form-item>
+
+              <u-form-item label="验收结果" prop="inspectionResult" borderBottom required>
+                <u--input
+                  v-model="form.inspectionResult"
+                  placeholder="请输入"
+                  border="none"
+                  inputAlign="right"
+                />
+              </u-form-item>
+
+              <u-form-item label="验收人" prop="inspectorName" borderBottom>
+                <u--input v-model="form.inspectorName" disabled border="none" inputAlign="right" />
+              </u-form-item>
+
+              <u-form-item label="附件" prop="inspectionRecordAttachment" borderBottom>
+                <fileMain v-model="form.inspectionRecordAttachment" />
+              </u-form-item>
+            </u--form>
+          </view>
+        </view>
+      </scroll-view>
+
+      <view class="popup-footer">
+        <u-button type="default" @click="cancel">取消</u-button>
+        <u-button type="primary" @click="save" :loading="loading">提交</u-button>
+      </view>
+    </view>
+
+    <u-toast ref="uToast" />
+  </u-popup>
+</template>
+
+<script>
+import fileMain from '@/pages/doc/index.vue';
+import { evaluate } from '@/api/emergencyDrill/workOrder.js';
+
+export default {
+  components: { fileMain },
+  data() {
+    return {
+      visible: false,
+      loading: false,
+      form: {
+        id: '',
+        drillEvaluation: '',
+        drillEvaluationMeasure: '',
+        inspectionRecordRectification: '',
+        inspectionResult: '',
+        inspectorName: '',
+        inspector: '',
+        inspectionRecordAttachment: []
+      },
+      rules: {
+        drillEvaluation: { type: 'string', required: true, message: '请输入演练效果和总结评价', trigger: ['blur', 'change'] },
+        inspectionResult: { type: 'string', required: true, message: '请输入验收结果', trigger: ['blur', 'change'] }
+      }
+    };
+  },
+  methods: {
+    open(row) {
+      this.visible = true;
+      const userInfo = uni.getStorageSync('userInfo') || {};
+      this.form = {
+        id: row.id,
+        drillEvaluation: '',
+        drillEvaluationMeasure: '',
+        inspectionRecordRectification: '',
+        inspectionResult: '',
+        inspectorName: userInfo.name || '',
+        inspector: userInfo.userId || '',
+        inspectionRecordAttachment: []
+      };
+      this.$nextTick(() => {
+        if (this.$refs.formRef) {
+          this.$refs.formRef.setRules(this.rules);
+        }
+      });
+    },
+
+    cancel() {
+      this.visible = false;
+      this.form = {
+        id: '',
+        drillEvaluation: '',
+        drillEvaluationMeasure: '',
+        inspectionRecordRectification: '',
+        inspectionResult: '',
+        inspectorName: '',
+        inspector: '',
+        inspectionRecordAttachment: []
+      };
+    },
+
+    async save() {
+      try {
+        if (this.$refs.formRef) {
+          await this.$refs.formRef.validate();
+        }
+        this.loading = true;
+        await evaluate(this.form);
+        this.$refs.uToast.show({ type: 'success', message: '验收成功' });
+        this.cancel();
+        this.$emit('reload');
+      } catch (e) {
+        if (e && e.message) {
+          this.$refs.uToast.show({ type: 'error', message: e.message });
+        }
+      } finally {
+        this.loading = false;
+      }
+    }
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+.acceptance-popup {
+  /deep/ .u-popup__content {
+    border-radius: 32rpx 32rpx 0 0;
+    overflow: hidden;
+  }
+
+  .popup-content {
+     height: calc(100vh - 100px);
+
+    display: flex;
+    flex-direction: column;
+    background: #eff2f7;
+  }
+
+  .popup-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    padding: 30rpx 32rpx;
+    background: #fff;
+    border-bottom: 2rpx solid #eef2f6;
+    flex-shrink: 0;
+    .popup-title {
+      font-size: 36rpx;
+      font-weight: bold;
+      color: #1f2b3c;
+    }
+    .close-btn {
+      width: 60rpx;
+      height: 60rpx;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      font-size: 52rpx;
+      color: #8e9aae;
+      line-height: 1;
+    }
+  }
+
+  .popup-body {
+    flex: 1;
+    overflow-y: auto;
+    padding: 24rpx;
+  }
+
+  .card-a {
+    background: #ffffff;
+    border-radius: 48rpx;
+    box-shadow: 0 12rpx 40rpx rgba(0, 0, 0, 0.05);
+    overflow: hidden;
+  }
+
+  .card-section {
+    padding: 30rpx 32rpx;
+  }
+
+  /deep/ .u-form-item {
+    padding: 12rpx 0;
+    .u-form-item__body__left__label {
+      font-size: 26rpx !important;
+      color: #6c7a91 !important;
+    }
+    .u-textarea__field {
+      font-size: 26rpx !important;
+      min-height: 100rpx;
+    }
+    .u-input__content__field-wrapper__field {
+      font-size: 26rpx !important;
+      text-align: right;
+    }
+  }
+
+  .popup-footer {
+    display: flex;
+    padding: 16rpx 24rpx;
+    background: #fff;
+    border-top: 2rpx solid #eef2f6;
+    gap: 16rpx;
+    flex-shrink: 0;
+    /deep/ .u-button {
+      flex: 1;
+      border-radius: 48rpx;
+      height: 72rpx;
+    }
+  }
+}
+</style>

+ 244 - 0
pages/ehs/emergencyDrill/workOrderReportDialog.vue

@@ -0,0 +1,244 @@
+<template>
+  <u-popup
+    :show="visible"
+    mode="bottom"
+    :round="10"
+    :closeOnClickOverlay="false"
+    @close="cancel"
+    class="report-popup"
+  >
+    <view class="popup-content">
+      <view class="popup-header">
+        <text class="popup-title">报工</text>
+        <view class="close-btn" @click="cancel">×</view>
+      </view>
+
+      <scroll-view class="popup-body" scroll-y>
+        <view class="card-a">
+          <view class="card-section">
+            <u--form
+              labelPosition="left"
+              labelWidth="190rpx"
+              :model="form"
+              :rules="rules"
+              ref="formRef"
+            >
+              <u-form-item label="演练实施记录" prop="processRecord" borderBottom required>
+                <u--textarea
+                  v-model="form.processRecord"
+                  placeholder="请输入"
+                  height="120rpx"
+                />
+              </u-form-item>
+
+              <u-form-item label="演练时间" prop="drillTime" borderBottom required>
+                <view class="picker-value" @click="showDateTimePicker">
+                  {{ form.drillTime || '请选择' }}
+                </view>
+                <u-icon slot="right" name="arrow-right" />
+              </u-form-item>
+
+              <u-form-item label="附件" prop="processRecordAttachment" borderBottom required>
+                <fileMain v-model="form.processRecordAttachment" />
+              </u-form-item>
+            </u--form>
+          </view>
+        </view>
+      </scroll-view>
+
+      <view class="popup-footer">
+        <u-button type="default" @click="cancel">取消</u-button>
+        <u-button type="primary" @click="save" :loading="loading">提交</u-button>
+      </view>
+    </view>
+
+    <u-datetime-picker
+      :show="showDateTimePopup"
+      v-model="datetimeValue"
+      mode="datetime"
+      @confirm="onDateTimeConfirm"
+      @cancel="showDateTimePopup = false"
+    />
+
+    <u-toast ref="uToast" />
+  </u-popup>
+</template>
+
+<script>
+import fileMain from '@/pages/doc/index.vue';
+import { recordProcess } from '@/api/emergencyDrill/workOrder.js';
+
+export default {
+  components: { fileMain },
+  data() {
+    return {
+      visible: false,
+      loading: false,
+      form: {
+        id: '',
+        processRecord: '',
+        processRecordAttachment: [],
+        drillTime: ''
+      },
+      showDateTimePopup: false,
+      datetimeValue: 0,
+      rules: {
+        processRecord: { type: 'string', required: true, message: '请输入演练实施记录', trigger: ['blur', 'change'] },
+        processRecordAttachment: { type: 'array', required: true, message: '请选择附件', trigger: ['change'] },
+        drillTime: { type: 'string', required: true, message: '请选择演练时间', trigger: ['change'] }
+      }
+    };
+  },
+  methods: {
+    open(row) {
+      this.visible = true;
+      this.form = {
+        id: row.id,
+        processRecord: '',
+        processRecordAttachment: [],
+        drillTime: this.$u.timeFormat(Date.now(), 'yyyy-mm-dd hh:MM:ss')
+      };
+      this.$nextTick(() => {
+        if (this.$refs.formRef) {
+          this.$refs.formRef.setRules(this.rules);
+        }
+      });
+    },
+
+    cancel() {
+      this.visible = false;
+      this.form = { id: '', processRecord: '', processRecordAttachment: [], drillTime: '' };
+    },
+
+    showDateTimePicker() {
+      this.datetimeValue = this.form.drillTime ? new Date(this.form.drillTime).getTime() : Date.now();
+      this.showDateTimePopup = true;
+    },
+
+    onDateTimeConfirm(e) {
+      this.showDateTimePopup = false;
+      this.form.drillTime = this.$u.timeFormat(e.value, 'yyyy-mm-dd hh:MM:ss');
+      if (this.$refs.formRef) {
+        this.$refs.formRef.validateField('drillTime');
+      }
+    },
+
+    async save() {
+      try {
+        if (this.$refs.formRef) {
+          await this.$refs.formRef.validate();
+        }
+        if (!this.form.processRecordAttachment || this.form.processRecordAttachment.length === 0) {
+          this.$refs.uToast.show({ type: 'warning', message: '请选择附件' });
+          return;
+        }
+        this.loading = true;
+        await recordProcess(this.form);
+        this.$refs.uToast.show({ type: 'success', message: '报工成功' });
+        this.cancel();
+        this.$emit('reload');
+      } catch (e) {
+        if (e && e.message) {
+          this.$refs.uToast.show({ type: 'error', message: e.message });
+        }
+      } finally {
+        this.loading = false;
+      }
+    }
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+.report-popup {
+  /deep/ .u-popup__content {
+    border-radius: 32rpx 32rpx 0 0;
+    overflow: hidden;
+  }
+
+  .popup-content {
+        height: calc(100vh - 100px);
+
+    display: flex;
+    flex-direction: column;
+    background: #eff2f7;
+  }
+
+  .popup-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    padding: 30rpx 32rpx;
+    background: #fff;
+    border-bottom: 2rpx solid #eef2f6;
+    flex-shrink: 0;
+    .popup-title {
+      font-size: 36rpx;
+      font-weight: bold;
+      color: #1f2b3c;
+    }
+    .close-btn {
+      width: 60rpx;
+      height: 60rpx;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      font-size: 52rpx;
+      color: #8e9aae;
+      line-height: 1;
+    }
+  }
+
+  .popup-body {
+    flex: 1;
+    overflow-y: auto;
+    padding: 24rpx;
+  }
+
+  .card-a {
+    background: #ffffff;
+    border-radius: 48rpx;
+    box-shadow: 0 12rpx 40rpx rgba(0, 0, 0, 0.05);
+    overflow: hidden;
+  }
+
+  .card-section {
+    padding: 30rpx 32rpx;
+  }
+
+  /deep/ .u-form-item {
+    padding: 12rpx 0;
+    .u-form-item__body__left__label {
+      font-size: 26rpx !important;
+      color: #6c7a91 !important;
+    }
+    .u-textarea__field {
+      font-size: 26rpx !important;
+      min-height: 100rpx;
+    }
+  }
+
+  .picker-value {
+    font-size: 26rpx;
+    color: #1e2a3a;
+    text-align: right;
+    flex: 1;
+    min-height: 44rpx;
+    padding: 4rpx 0;
+  }
+
+  .popup-footer {
+    display: flex;
+    padding: 16rpx 24rpx;
+    background: #fff;
+    border-top: 2rpx solid #eef2f6;
+    gap: 16rpx;
+    flex-shrink: 0;
+    /deep/ .u-button {
+      flex: 1;
+      border-radius: 48rpx;
+      height: 72rpx;
+    }
+  }
+}
+</style>

+ 233 - 106
pages/ems/centralizedMeterReading/components/workOrderReport.vue

@@ -13,9 +13,6 @@
 						<!-- 头部 -->
 						<view class="a-header">
 							<text class="a-main-title">{{ '抄表记录' }}</text>
-							<!-- <view class="a-sub">
-                <text>📋 {{ title }}</text>
-              </view> -->
 						</view>
 
 						<!-- 设备信息 -->
@@ -59,31 +56,44 @@
 						<!-- 抄表信息 -->
 						<view class="card-section">
 							<view class="section-title">📝 抄表信息</view>
-							<view class="info-grid">
-								<view class="info-item">
-									<text class="info-label">抄表人</text>
-									<input class="info-input" v-model="form.meterReadingName" placeholder="请输入抄表人"
-										:disabled="true" />
-								</view>
-								<view class="info-item">
-									<text class="info-label">本期抄表时间</text>
-									<uni-datetime-picker type="datetime" v-model="form.currentMeterReadingTime"
-										returnType="timestamp" :disabled="isView">
-										<view class="info-value">
-											{{
-                        form.currentMeterReadingTime
-                          ? dayjs(form.currentMeterReadingTime).format('YYYY-MM-DD HH:mm:ss')
-                          : '请选择'
-                      }}
-										</view>
-									</uni-datetime-picker>
-								</view>
-								<view class="info-item">
-									<text class="info-label">本期抄表数</text>
-									<input class="info-input" v-model="form.currentMeterReadingNum" type="number"
-										placeholder="请输入本期抄表数" :disabled="isView" />
-								</view>
-							</view>
+							<u--form labelPosition="left" labelWidth="190rpx" :model="form" :rules="rules"
+								ref="formRef">
+								<!-- 抄表人 -->
+								<u-form-item label="抄表人" prop="meterReadingName" borderBottom>
+									<u--input v-model="form.meterReadingName" placeholder="自动带出" disabled
+										disabledColor="#f5f5f5" border="none" inputAlign="right" />
+								</u-form-item>
+
+								<!-- 本期抄表时间 -->
+								<u-form-item label="本期抄表时间" prop="currentMeterReadingTime" borderBottom required>
+									<view class="picker-value" @click="!isView && showDateTimePicker()">
+										{{
+                      form.currentMeterReadingTime
+                        ? dayjs(form.currentMeterReadingTime).format('YYYY-MM-DD HH:mm:ss')
+                        : '请选择'
+                    }}
+									</view>
+									<u-icon slot="right" name="arrow-right" />
+								</u-form-item>
+								<!-- 上次抄表数 -->
+								<u-form-item label="上次抄表数" prop="lastMeterReadingNum" borderBottom>
+									<u--input v-model="form.lastMeterReadingNum" type="number" placeholder="自动获取"
+										disabled disabledColor="#f5f5f5" border="none" inputAlign="right" />
+								</u-form-item>
+								<!-- 本期抄表数 -->
+								<u-form-item label="本期抄表数" prop="currentMeterReadingNum" borderBottom required>
+									<u--input v-model="form.currentMeterReadingNum" type="number" placeholder="请输入本期抄表数"
+										:disabled="isView" border="none" inputAlign="right" @blur="setQuantity" />
+								</u-form-item>
+
+
+
+								<!-- 本期用量 -->
+								<u-form-item label="本期用量" prop="quantity" borderBottom required>
+									<u--input v-model="form.quantity" type="number" placeholder="请输入本期用量"
+										:disabled="isView" border="none" inputAlign="right" />
+								</u-form-item>
+							</u--form>
 						</view>
 					</view>
 				</view>
@@ -96,7 +106,13 @@
 				<u-button type="primary" @click="handleSubmit" v-if="!isView" :loading="loading">确定</u-button>
 			</view>
 		</view>
+
 		<u-toast ref="uToast"></u-toast>
+
+		<!-- 日期时间选择器 -->
+		<u-datetime-picker :show="showDateTimePopup" v-model="datetimeValue" mode="datetime"
+			@confirm="onDateTimeConfirm" @cancel="showDateTimePopup = false" />
+
 		<materialDialog ref="materialAddRef" @affirm="handleChooseEquipment"></materialDialog>
 	</u-popup>
 </template>
@@ -105,7 +121,8 @@
 	import {
 		save,
 		update,
-		getById
+		getById,
+		queryBySubstanceId,
 	} from '@/api/centralizedMeterReading/index.js';
 	import dayjs from 'dayjs';
 	import {
@@ -126,11 +143,16 @@
 		meterReadingId: '',
 		currentMeterReadingTime: '',
 		currentMeterReadingNum: '',
+		lastMeterReadingNum: '',
+		quantity: '',
 		meterReadingMethod: 1,
 	};
 
 	export default {
 		name: 'MeterReadingDialog',
+		components: {
+			materialDialog,
+		},
 		data() {
 			return {
 				visible: false,
@@ -141,6 +163,29 @@
 					...defForm
 				},
 				dayjs,
+				// 日期时间选择器
+				showDateTimePopup: false,
+				datetimeValue: 0,
+				// 标记是否手动输入了用量(防止自动计算覆盖用户输入)
+				isManualQuantity: false,
+				// uView Form 校验规则
+				rules: {
+					currentMeterReadingTime: {
+						required: true,
+						message: '请选择本期抄表时间',
+						trigger: ['change'],
+					},
+					currentMeterReadingNum: {
+						required: true,
+						message: '请输入本期抄表数',
+						trigger: ['blur', 'change'],
+					},
+					quantity: {
+						required: true,
+						message: '请输入本期用量',
+						trigger: ['blur', 'change'],
+					},
+				},
 			};
 		},
 		computed: {
@@ -148,20 +193,20 @@
 				return this.type === 'view';
 			},
 		},
-		components: {
-			materialDialog
-		},
 		methods: {
-			// 打开弹窗
+			// ===== 打开弹窗 =====
 			async open(row, type = 'add') {
 				this.type = type;
+				this.isManualQuantity = false;
 				if (type === 'add') {
 					this.title = '新增';
 					this.resetForm();
-					const currentUser = uni.getStorageSync("userInfo");
+					const currentUser = uni.getStorageSync('userInfo');
 					this.form.meterReadingName = currentUser?.name || '';
 					this.form.meterReadingId = currentUser?.userId || '';
-					this.form.currentMeterReadingTime = dayjs().format('YYYY-MM-DD HH:mm:ss');
+					this.form.currentMeterReadingTime = dayjs().format(
+						'YYYY-MM-DD HH:mm:ss',
+					);
 				} else {
 					this.title = type === 'edit' ? '编辑' : '详情';
 					try {
@@ -170,32 +215,43 @@
 							...defForm,
 							...res
 						};
+			
 					} catch (e) {
 						console.error('获取详情失败', e);
 					}
 				}
 				this.visible = true;
+				this.$nextTick(() => {
+					if (this.$refs.formRef) {
+						this.$refs.formRef.setRules(this.rules);
+					}
+				});
 			},
-			// 关闭弹窗
+
+			// ===== 关闭弹窗 =====
 			handleCancel() {
 				this.visible = false;
 				this.resetForm();
+				this.showDateTimePopup = false;
+				this.isManualQuantity = false;
 			},
-			// 重置表单
+
+			// ===== 重置表单 =====
 			resetForm() {
 				this.form = {
 					...defForm
 				};
 			},
-			// 打开设备选择(uni-app 中可替换为 navigateTo 或 picker)
+
+			// ===== 设备选择 =====
 			openMaterialAdd() {
 				if (this.isView) return;
-				// TODO: 接入设备选择页面
-				this.$refs.materialAddRef.open()
+				this.$refs.materialAddRef.open();
 			},
-			// 选择设备回调
-			handleChooseEquipment(data) {
-				let row = data[0] || {}
+
+			// ===== 选择设备回调 =====
+			async handleChooseEquipment(data) {
+				const row = data[0] || {};
 				this.form.substanceId = row.id;
 				this.form.substanceName = row?.category?.name;
 				this.form.substanceCode = row.code;
@@ -209,56 +265,116 @@
 				this.form.unit = energyConsumingUnitList
 					.find((item) => item.value == row.energyClassificationCode)
 					?.unit.find((item) => item.value == row.energyUnit)?.label;
-			},
-			// 提交
-			async handleSubmit() {
-				// 必填校验
-				if (!this.form.substanceId) {
-					this.$refs.uToast.show({
-						type: 'error',
-						icon: false,
-						message: '请选择表计设备'
-					});
-					return;
-				}
-				if (!this.form.meterReadingName) {
-					this.$refs.uToast.show({
-						type: 'error',
-						icon: false,
-						message: '请输入抄表人'
-					});
-					return;
+
+				// 设备变更后获取上次抄表数(排除当前记录)
+				if (row.id) {
+					await this.fetchLastMeterReading();
 				}
-				if (!this.form.currentMeterReadingTime) {
-					this.$refs.uToast.show({
-						type: 'error',
-						icon: false,
-						message: '请选择本期抄表时间'
-					});
-					return;
+			},
+
+			// ===== 获取上次抄表数 =====
+			async fetchLastMeterReading() {
+				if (!this.form.substanceId) return;
+				try {
+					const data = await queryBySubstanceId(
+						this.form.substanceId,
+						this.form.id || undefined,
+					);
+					if (data && data.currentMeterReadingNum !== undefined) {
+						this.form.lastMeterReadingNum = data.currentMeterReadingNum;
+					} else {
+						this.form.lastMeterReadingNum = '';
+					}
+					// 获取上次抄表数后重新计算本期用量
+					this.setQuantity();
+				} catch (e) {
+					console.error('获取上次抄表数失败', e);
+					this.form.lastMeterReadingNum = '';
 				}
-				if (this.form.currentMeterReadingNum === '' || this.form.currentMeterReadingNum == null) {
-					this.$refs.uToast.show({
-						type: 'error',
-						icon: false,
-						message: '请输入本期抄表数'
-					});
-					return;
+			},
+
+			// ===== 计算本期用量 =====
+			setQuantity() {
+				// // 如果用户手动输入了用量,不自动覆盖
+				// if (this.isManualQuantity) return;
+
+				const currentNum = parseFloat(this.form.currentMeterReadingNum);
+				const lastNum = parseFloat(this.form.lastMeterReadingNum);
+
+				if (!isNaN(currentNum) && !isNaN(lastNum)) {
+					const diff = currentNum - lastNum;
+					this.form.quantity = diff >= 0 ? diff : 0;
+				} else if (!isNaN(currentNum) && isNaN(lastNum)) {
+					// 只有本期数,没有上次数,用量 = 本期数
+					this.form.quantity = currentNum;
+				} else {
+					this.form.quantity = '';
 				}
+			},
 
-				// 时间格式补齐
-				const formatTime = (timeStr) => {
-					if (!timeStr) return timeStr;
-					if (/^\d{4}-\d{2}-\d{2}$/.test(timeStr)) {
-						return timeStr + ' 00:00:00';
-					}
-					return timeStr;
-				};
-				this.form.currentMeterReadingTime = formatTime(this.form.currentMeterReadingTime);
 
-				const api = this.type === 'edit' ? update : save;
-				this.loading = true;
+			// ===== 日期时间选择器 =====
+			showDateTimePicker() {
+				if (this.isView || this.loading) return;
+				const val = this.form.currentMeterReadingTime;
+				this.datetimeValue = val ? new Date(val).getTime() : Date.now();
+				this.showDateTimePopup = true;
+			},
+
+			onDateTimeConfirm(e) {
+				this.showDateTimePopup = false;
+				const value = this.$u.timeFormat(e.value, 'yyyy-mm-dd hh:MM:ss');
+				this.form.currentMeterReadingTime = value;
+				if (this.$refs.formRef) {
+					this.$refs.formRef.validateField('currentMeterReadingTime');
+				}
+			},
+
+			// ===== 提交 =====
+			async handleSubmit() {
 				try {
+					// if (this.$refs.formRef) {
+					// 	await this.$refs.formRef.validate();
+					// }
+
+					if (!this.form.substanceId) {
+						this.$refs.uToast.show({
+							type: 'error',
+							icon: false,
+							message: '请选择表计设备',
+						});
+						return;
+					}
+					if (!this.form.currentMeterReadingNum) {
+						this.$refs.uToast.show({
+							type: 'error',
+							icon: false,
+							message: '请输入本期抄表数',
+						});
+						return;
+					}
+					if (!this.form.quantity) {
+						this.$refs.uToast.show({
+							type: 'error',
+							icon: false,
+							message: '请输入本期用量',
+						});
+						return;
+					}
+					// 时间格式补齐
+					const formatTime = (timeStr) => {
+						if (!timeStr) return timeStr;
+						if (/^\d{4}-\d{2}-\d{2}$/.test(timeStr)) {
+							return timeStr + ' 00:00:00';
+						}
+						return timeStr;
+					};
+					this.form.currentMeterReadingTime = formatTime(
+						this.form.currentMeterReadingTime,
+					);
+
+					const api = this.type === 'edit' ? update : save;
+					this.loading = true;
 					await api(this.form);
 					this.$refs.uToast.show({
 						type: 'success',
@@ -267,8 +383,14 @@
 					this.visible = false;
 					this.$emit('refresh');
 					this.resetForm();
-				} catch (error) {
-					console.error(error);
+					this.isManualQuantity = false;
+				} catch (e) {
+					if (e && e.message) {
+						this.$refs.uToast.show({
+							type: 'error',
+							message: e.message
+						});
+					}
 				} finally {
 					this.loading = false;
 				}
@@ -343,12 +465,6 @@
 		color: #2e7d32;
 	}
 
-	.a-sub {
-		font-size: 24rpx;
-		color: #558b2f;
-		margin-top: 12rpx;
-	}
-
 	.card-section {
 		padding: 30rpx 32rpx;
 		border-bottom: 2rpx solid #f0f2f5;
@@ -400,19 +516,30 @@
 		}
 	}
 
-	.info-input {
-		font-size: 28rpx;
-		color: #1e2a3a;
-		padding: 16rpx 20rpx;
-		border-radius: 24rpx;
-		border: 2rpx solid #e9edf2;
-		background: #fff;
-		height: 76rpx;
+	// ===== u--form 样式 =====
+	/deep/ .u-form-item {
+		padding: 12rpx 0;
 
-		&:disabled {
-			color: #999;
-			background: #f5f5f5;
+		.u-form-item__body__left__label {
+			font-size: 26rpx !important;
+			color: #6c7a91 !important;
 		}
+
+		.u-form-item__body__right__content {
+			.u-input__content__field-wrapper__field {
+				font-size: 26rpx !important;
+				text-align: right;
+			}
+		}
+	}
+
+	.picker-value {
+		font-size: 26rpx;
+		color: #1e2a3a;
+		padding: 8rpx 0;
+		text-align: right;
+		flex: 1;
+		min-height: 44rpx;
 	}
 
 	.popup-footer {

+ 1 - 34
pages/maintenance/service/order.vue

@@ -150,40 +150,7 @@
 				})
 
 			},
-			getCount() {
-				statistics().then(data => {
-					console.log('onsole.log(data)-----------')
-					console.log(data)
-					this.tabList = this.tabList.map(item => {
-						// switch (item.value) {
-						// 	case '0':
-						// 		return {
-						// 			...item, badge: {
-						// 				value: data.maintenanceNum
-						// 			}
-						// 		}
-						// 	case '2':
-						// 		return {
-						// 			...item, badge: {
-						// 				value: data.patrolInspection
-						// 			}
-						// 		}
-						// 	case '3':
-						// 		return {
-						// 			...item, badge: {
-						// 				value: data.quantityNum
-						// 			}
-						// 		}
-						// 	case '4':
-						// 		return {
-						// 			...item, badge: {
-						// 				value: data.repairsNum
-						// 			}
-						// 		}
-						// }
-					})
-				})
-			},
+
 			getFirstList: function() {
 				this.page = 1
 				this.isEnd = false

+ 1 - 0
store/getters.js

@@ -9,6 +9,7 @@ const getters = {
     ) || {},
   // 根据字典enumName  和 dictCode 获取字典 值(名称
   getDictValue: state => (enumName, dictCode) => {
+    console.log(state.dict)
     const obj = (state.dict[dictEnum[enumName]] || []).find(
       item => item.dictCode === dictCode
     )