Procházet zdrojové kódy

新增任务报工界面

695593266@qq.com před 1 měsícem
rodič
revize
45b1047ffd

+ 3 - 1
.claude/settings.local.json

@@ -1,7 +1,9 @@
 {
   "permissions": {
     "allow": [
-      "mcp__ide__getDiagnostics"
+      "mcp__ide__getDiagnostics",
+      "Bash(grep -E '\\\\$theme-color|\\\\$page-bg' 'D:\\\\project\\\\vue_project\\\\zhongying\\\\app\\\\uni.scss')",
+      "Bash(git -C D:/project/vue_project/zhongying/app diff manifest.json)"
     ]
   }
 }

+ 121 - 0
api/pda/workReport.js

@@ -0,0 +1,121 @@
+import { postJ, post, putJ, get } from "@/utils/request";
+import Vue from "vue";
+
+// 我的任务
+export async function pageByCurrentUser(params) {
+  const data = await get(
+    Vue.prototype.apiUrl + `/aps/assign/pageByCurrentUser/v2`,
+    params,
+    true,
+  );
+  if (data.code == 0) return data.data;
+  return Promise.reject(data.message);
+}
+
+// 全部任务
+export async function pageByCurrentUserLeader(params) {
+  const data = await get(
+    Vue.prototype.apiUrl + `/aps/assign/pageByCurrentUserLeader/v2`,
+    params,
+    true,
+  );
+  if (data.code == 0) return data.data;
+  return Promise.reject(data.message);
+}
+
+// 班组任务
+export async function pageByCurrentCurrentUserTeam(params) {
+  const data = await get(
+    Vue.prototype.apiUrl + `/aps/assign/pageByCurrentUserTeam`,
+    params,
+    true,
+  );
+  if (data.code == 0) return data.data;
+  return Promise.reject(data.message);
+}
+
+// NC任务列表
+export async function getNCtaskListData(params) {
+  const data = await get(
+    Vue.prototype.apiUrl + `/aps/assign/eventPage`,
+    params,
+    true,
+  );
+  if (data.code == 0) return data.data;
+  return Promise.reject(data.message);
+}
+
+// NC任务报工
+export async function ncTaskReport(params) {
+  const data = await postJ(
+    Vue.prototype.apiUrl + `/aps/assign/eventReport`,
+    params,
+    true,
+  );
+  if (data.code == 0) return data.data;
+  return Promise.reject(data.message);
+}
+
+// 任务接收/拒绝
+export async function taskManagement(params) {
+  const data = await putJ(
+    Vue.prototype.apiUrl + `/aps/assign/disposal`,
+    params,
+    true,
+  );
+  if (data.code == 0) return data.data;
+  return Promise.reject(data.message);
+}
+
+// 转派任务
+export async function transferTasks(params) {
+  const data = await putJ(
+    Vue.prototype.apiUrl + `/aps/assign/redeploy`,
+    params,
+    true,
+  );
+  if (data.code == 0) return data.data;
+  return Promise.reject(data.message);
+}
+
+// 更新派单任务实际时间(报工)
+export async function batchUpdateRealTime(params) {
+  const data = await putJ(
+    Vue.prototype.apiUrl + `/aps/assign/batchUpdateRealTime`,
+    params,
+    true,
+  );
+  if (data.code == 0) return data.data;
+  return Promise.reject(data.message);
+}
+
+// 报工记录
+export async function listUpdateRealTimeRecord(assigneeId) {
+  const data = await get(
+    Vue.prototype.apiUrl +
+      `/aps/assign/listUpdateRealTimeRecord/${assigneeId}`,
+  );
+  if (data.code == 0) return data.data;
+  return Promise.reject(data.message);
+}
+
+// 工作中心/班组
+export async function listWorkCenter(firstProduceTaskId) {
+  const data = await get(
+    Vue.prototype.apiUrl +
+      `/aps/assign/listWorkCenterTeamsByProduceTaskId/${firstProduceTaskId}`,
+  );
+  if (data.code == 0) return data.data;
+  return Promise.reject(data.message);
+}
+
+// 班组人员列表(用于转派人员选择)
+export async function getteampage(params) {
+  const data = await get(
+    Vue.prototype.apiUrl + `/main/team/page`,
+    params,
+    true,
+  );
+  if (data.code == 0) return data.data;
+  return Promise.reject(data.message);
+}

+ 2 - 2
manifest.json

@@ -2,7 +2,7 @@
     "name" : "智慧工厂",
     "appid" : "__UNI__45B3907",
     "description" : "",
-    "versionName" : "V1.0.3.92",
+    "versionName" : "V1.0.3.94",
     "versionCode" : "100",
     "transformPx" : false,
     "h5" : {
@@ -11,8 +11,8 @@
                 "/api" : {
                     // "target" : "http://192.168.1.110:18086/",
                     // "target" : "http://123.249.79.125/api/",
-                    "target" : "http://192.168.1.251:18086/",
                     // "target" : "http://192.168.1.251:18086/",
+                    "target" : "http://192.168.1.125:18086/",
                     // "target" : "http://110.41.182.105/api/",
                     // "target": "http://116.63.185.248:80/api",
                     // "target" : "http://192.168.1.251:18186/",

+ 8 - 0
pages.json

@@ -1759,6 +1759,14 @@
 				"navigationBarTextStyle": "white"
 			}
 		},
+		{
+			"path": "pages/pda/workReport/index/index",
+			"style": {
+				"navigationBarTitleText": "任务报工",
+				"navigationStyle": "custom",
+				"navigationBarTextStyle": "white"
+			}
+		},
 		{
 			"path": "pages/pda/nonconforming/index/index",
 			"style": {

+ 162 - 31
pages/pda/beEntrust/index/index.vue

@@ -188,14 +188,46 @@
             labelAlign="left"
           >
             <u-form-item label="类型:" borderBottom>
-              <picker
-                mode="selector"
-                :range="typeList"
-                range-key="label"
-                @change="typeChange"
-              >
-                <view class="picker-value">{{ currentType || "全部" }}</view>
-              </picker>
+              <zxz-uni-data-select
+                :localdata="typeList"
+                v-model="searchForm.type"
+                dataValue="value"
+                dataKey="label"
+                format="{label}"
+              ></zxz-uni-data-select>
+            </u-form-item>
+
+            <u-form-item label="请托工厂:" borderBottom>
+              <zxz-uni-data-select
+                :localdata="factoryList"
+                v-model="searchForm.applyFactoriesId"
+                dataValue="value"
+                dataKey="label"
+                format="{label}"
+                filterable
+              ></zxz-uni-data-select>
+            </u-form-item>
+
+            <u-form-item label="请托时间:" borderBottom>
+              <view class="date-range-wrapper">
+                <view
+                  class="date-picker-box"
+                  @click="showStartPicker = true"
+                >
+                  <text :class="{ placeholder: !startTimeDisplay }">
+                    {{ startTimeDisplay || "开始日期" }}
+                  </text>
+                </view>
+                <text class="date-separator">至</text>
+                <view
+                  class="date-picker-box"
+                  @click="showEndPicker = true"
+                >
+                  <text :class="{ placeholder: !endTimeDisplay }">
+                    {{ endTimeDisplay || "结束日期" }}
+                  </text>
+                </view>
+              </view>
             </u-form-item>
           </u-form>
         </view>
@@ -219,12 +251,31 @@
         </view>
       </template>
     </SearchPopup>
+
+    <!-- 开始日期选择器 -->
+    <u-datetime-picker
+      v-model="startTime"
+      :show="showStartPicker"
+      mode="date"
+      @confirm="confirmStartTime"
+      @cancel="showStartPicker = false"
+    ></u-datetime-picker>
+
+    <!-- 结束日期选择器 -->
+    <u-datetime-picker
+      v-model="endTime"
+      :show="showEndPicker"
+      mode="date"
+      @confirm="confirmEndTime"
+      @cancel="showEndPicker = false"
+    ></u-datetime-picker>
   </view>
 </template>
 
 <script>
 import { getList } from "@/api/beEntrust/index.js";
 import SearchPopup from "../../components/searchPopup.vue";
+import { getFactoryarea } from "@/api/inspectionWork/index";
 
 let isEnd = false;
 
@@ -238,9 +289,11 @@ export default {
         type: "",
         name: "",
         applyFactoriesId: "",
+        startTime: "",
+        endTime: "",
       },
-      currentType: "",
       typeList: [],
+      factoryList: [],
       list: [],
       pageNum: 1,
       pageSize: 20,
@@ -248,12 +301,19 @@ export default {
       loading: false,
       hasMore: true,
       showFilter: false,
+      showStartPicker: false,
+      showEndPicker: false,
+      startTime: "",
+      endTime: "",
+      startTimeDisplay: "",
+      endTimeDisplay: "",
       tabValue: "1",
     };
   },
 
   onLoad() {
     this.getTypeList();
+    this.getFactoryList();
   },
 
   onShow() {
@@ -267,16 +327,59 @@ export default {
 
     async getTypeList() {
       this.typeList = [
-        { label: "全部", value: "" },
         { label: "加工", value: "1" },
         { label: "装配", value: "2" },
       ];
     },
 
-    typeChange(e) {
-      const index = e.detail.value;
-      this.searchForm.type = this.typeList[index].value;
-      this.currentType = this.typeList[index].label;
+    async getFactoryList() {
+      try {
+        const res = await getFactoryarea({
+          pageNum: 1,
+          size: 999,
+          type: 1,
+        });
+        if (res && res.list) {
+          this.factoryList = [
+            ...res.list.map((item) => ({
+              label: item.name,
+              value: item.id,
+            })),
+          ];
+        }
+      } catch (error) {
+        console.error("获取工厂列表失败", error);
+        this.factoryList = [];
+      }
+    },
+
+    confirmStartTime(e) {
+      const timestamp = e.value;
+      const date = new Date(timestamp);
+      this.startTime = timestamp;
+      this.startTimeDisplay = this.formatDate(date);
+      this.searchForm.startTime = this.formatDateTime(date, "00:00:00");
+      this.showStartPicker = false;
+    },
+
+    confirmEndTime(e) {
+      const timestamp = e.value;
+      const date = new Date(timestamp);
+      this.endTime = timestamp;
+      this.endTimeDisplay = this.formatDate(date);
+      this.searchForm.endTime = this.formatDateTime(date, "23:59:59");
+      this.showEndPicker = false;
+    },
+
+    formatDate(date) {
+      const year = date.getFullYear();
+      const month = String(date.getMonth() + 1).padStart(2, "0");
+      const day = String(date.getDate()).padStart(2, "0");
+      return `${year}-${month}-${day}`;
+    },
+
+    formatDateTime(date, time) {
+      return `${this.formatDate(date)} ${time}`;
     },
 
     async loadData(isRefresh = false) {
@@ -373,7 +476,14 @@ export default {
 
     filterCancel() {
       this.searchForm.type = "";
-      this.currentType = "";
+      this.searchForm.name = "";
+      this.searchForm.applyFactoriesId = "";
+      this.searchForm.startTime = "";
+      this.searchForm.endTime = "";
+      this.startTime = "";
+      this.endTime = "";
+      this.startTimeDisplay = "";
+      this.endTimeDisplay = "";
       this.showFilter = false;
       this.loadData(true);
     },
@@ -532,18 +642,17 @@ page {
 .top-wrapper {
   background-color: #fff;
   display: flex;
-  width: 750rpx;
-  padding: 16rpx 32rpx;
+  width: 100%;
+  padding: 20rpx 26rpx;
   align-items: center;
   gap: 16rpx;
   flex-shrink: 0;
-  position: sticky;
-  top: 88rpx;
-  z-index: 99;
+  box-sizing: border-box;
 
   /deep/.uni-section {
     margin-top: 0px;
     flex: 1;
+    min-width: 0;
   }
 
   /deep/.uni-section-header {
@@ -551,17 +660,20 @@ page {
   }
 
   .search_btn {
-    width: 120rpx;
-    height: 70rpx;
-    line-height: 70rpx;
-    padding: 0 24rpx;
+    flex-shrink: 0;
+    padding: 0 26rpx;
+    height: 64rpx;
+    line-height: 64rpx;
     background: $theme-color;
-    font-size: 32rpx;
+    font-size: 26rpx;
+    border-radius: 8rpx;
     color: #fff;
+    white-space: nowrap;
     margin: 0;
   }
 
   .menu_icon {
+    flex-shrink: 0;
     width: 44rpx;
     height: 44rpx;
   }
@@ -618,7 +730,7 @@ page {
 }
 
 .list_box {
-  flex: 1;
+  // flex: 1;
   overflow: hidden;
   padding: 16rpx 0;
 
@@ -755,11 +867,30 @@ page {
   min-height: 100rpx;
   padding: 24rpx 32rpx;
 
-  .picker-value {
-    padding: 12rpx 16rpx;
-    border: 1rpx solid #e0e0e0;
-    border-radius: 6rpx;
-    font-size: 28rpx;
+  .date-range-wrapper {
+    display: flex;
+    align-items: center;
+    gap: 12rpx;
+
+    .date-picker-box {
+      flex: 1;
+      padding: 12rpx 16rpx;
+      border: 1rpx solid #e0e0e0;
+      border-radius: 6rpx;
+      font-size: 28rpx;
+      color: #333;
+      min-height: 56rpx;
+      line-height: 32rpx;
+
+      .placeholder {
+        color: #c0c4cc;
+      }
+    }
+
+    .date-separator {
+      font-size: 28rpx;
+      color: #666;
+    }
   }
 }
 

+ 175 - 31
pages/pda/entrust/index/index.vue

@@ -179,14 +179,57 @@
             labelAlign="left"
           >
             <u-form-item label="类型:" borderBottom>
-              <picker
-                mode="selector"
-                :range="typeList"
-                range-key="label"
-                @change="typeChange"
-              >
-                <view class="picker-value">{{ currentType || "全部" }}</view>
-              </picker>
+              <zxz-uni-data-select
+                :localdata="typeList"
+                v-model="searchForm.type"
+                dataValue="value"
+                dataKey="label"
+                format="{label}"
+              ></zxz-uni-data-select>
+            </u-form-item>
+
+            <u-form-item label="请托工厂:" borderBottom>
+              <zxz-uni-data-select
+                :localdata="factoryList"
+                v-model="searchForm.factoriesId"
+                dataValue="id"
+                dataKey="name"
+                format="{name}"
+                filterable
+              ></zxz-uni-data-select>
+            </u-form-item>
+
+            <u-form-item label="受托工厂:" borderBottom>
+              <zxz-uni-data-select
+                :localdata="beEntrustedFactoryList"
+                v-model="searchForm.beEntrustedFactoriesId"
+                dataValue="id"
+                dataKey="name"
+                format="{name}"
+                filterable
+              ></zxz-uni-data-select>
+            </u-form-item>
+
+            <u-form-item label="完成时间:" borderBottom>
+              <view class="date-range-wrapper">
+                <view
+                  class="date-picker-box"
+                  @click="showStartPicker = true"
+                >
+                  <text :class="{ placeholder: !startTimeDisplay }">
+                    {{ startTimeDisplay || "开始日期" }}
+                  </text>
+                </view>
+                <text class="date-separator">至</text>
+                <view
+                  class="date-picker-box"
+                  @click="showEndPicker = true"
+                >
+                  <text :class="{ placeholder: !endTimeDisplay }">
+                    {{ endTimeDisplay || "结束日期" }}
+                  </text>
+                </view>
+              </view>
             </u-form-item>
           </u-form>
         </view>
@@ -210,12 +253,31 @@
         </view>
       </template>
     </SearchPopup>
+
+    <!-- 开始日期选择器 -->
+    <u-datetime-picker
+      v-model="startTime"
+      :show="showStartPicker"
+      mode="date"
+      @confirm="confirmStartTime"
+      @cancel="showStartPicker = false"
+    ></u-datetime-picker>
+
+    <!-- 结束日期选择器 -->
+    <u-datetime-picker
+      v-model="endTime"
+      :show="showEndPicker"
+      mode="date"
+      @confirm="confirmEndTime"
+      @cancel="showEndPicker = false"
+    ></u-datetime-picker>
   </view>
 </template>
 
 <script>
 import { getList, submit, remove } from "@/api/entrust/index.js";
 import SearchPopup from "../../components/searchPopup.vue";
+import { getFactoryarea } from "@/api/inspectionWork/index";
 
 let isEnd = false;
 
@@ -228,10 +290,14 @@ export default {
       searchForm: {
         type: "",
         name: "",
+        factoriesId: "",
         beEntrustedFactoriesId: "",
+        planDeliveryTimeStart: "",
+        planDeliveryTimeEnd: "",
       },
-      currentType: "",
       typeList: [],
+      factoryList: [],
+      beEntrustedFactoryList: [],
       list: [],
       pageNum: 1,
       pageSize: 20,
@@ -239,11 +305,18 @@ export default {
       loading: false,
       hasMore: true,
       showFilter: false,
+      showStartPicker: false,
+      showEndPicker: false,
+      startTime: "",
+      endTime: "",
+      startTimeDisplay: "",
+      endTimeDisplay: "",
     };
   },
 
   onLoad() {
     this.getTypeList();
+    this.getFactoryList();
   },
 
   onShow() {
@@ -258,16 +331,59 @@ export default {
     async getTypeList() {
       // 获取字典数据
       this.typeList = [
-        { label: "全部", value: "" },
         { label: "加工", value: "1" },
         { label: "装配", value: "2" },
       ];
     },
 
-    typeChange(e) {
-      const index = e.detail.value;
-      this.searchForm.type = this.typeList[index].value;
-      this.currentType = this.typeList[index].label;
+    async getFactoryList() {
+      try {
+        const res = await getFactoryarea({
+          pageNum: 1,
+          size: 999,
+          type: 1,
+        });
+        const list = res?.list || [];
+        this.factoryList = [...list];
+        this.beEntrustedFactoryList = [...list];
+      } catch (error) {
+        console.error("获取工厂列表失败", error);
+      }
+    },
+
+    confirmStartTime(e) {
+      const timestamp = e.value;
+      const date = new Date(timestamp);
+      this.startTime = timestamp;
+      this.startTimeDisplay = this.formatDate(date);
+      this.searchForm.planDeliveryTimeStart = this.formatDateTime(
+        date,
+        "00:00:00",
+      );
+      this.showStartPicker = false;
+    },
+
+    confirmEndTime(e) {
+      const timestamp = e.value;
+      const date = new Date(timestamp);
+      this.endTime = timestamp;
+      this.endTimeDisplay = this.formatDate(date);
+      this.searchForm.planDeliveryTimeEnd = this.formatDateTime(
+        date,
+        "23:59:59",
+      );
+      this.showEndPicker = false;
+    },
+
+    formatDate(date) {
+      const year = date.getFullYear();
+      const month = String(date.getMonth() + 1).padStart(2, "0");
+      const day = String(date.getDate()).padStart(2, "0");
+      return `${year}-${month}-${day}`;
+    },
+
+    formatDateTime(date, time) {
+      return `${this.formatDate(date)} ${time}`;
     },
 
     async loadData(isRefresh = false) {
@@ -331,7 +447,14 @@ export default {
 
     filterCancel() {
       this.searchForm.type = "";
-      this.currentType = "";
+      this.searchForm.factoriesId = "";
+      this.searchForm.beEntrustedFactoriesId = "";
+      this.searchForm.planDeliveryTimeStart = "";
+      this.searchForm.planDeliveryTimeEnd = "";
+      this.startTime = "";
+      this.endTime = "";
+      this.startTimeDisplay = "";
+      this.endTimeDisplay = "";
       this.showFilter = false;
       this.loadData(true);
     },
@@ -561,18 +684,17 @@ page {
 .top-wrapper {
   background-color: #fff;
   display: flex;
-  width: 750rpx;
-  padding: 16rpx 32rpx;
+  width: 100%;
+  padding: 20rpx 26rpx;
   align-items: center;
   gap: 16rpx;
   flex-shrink: 0;
-  position: sticky;
-  top: 88rpx;
-  z-index: 99;
+  box-sizing: border-box;
 
   /deep/.uni-section {
     margin-top: 0px;
     flex: 1;
+    min-width: 0;
   }
 
   /deep/.uni-section-header {
@@ -580,17 +702,20 @@ page {
   }
 
   .search_btn {
-    width: 120rpx;
-    height: 70rpx;
-    line-height: 70rpx;
-    padding: 0 24rpx;
+    flex-shrink: 0;
+    padding: 0 26rpx;
+    height: 64rpx;
+    line-height: 64rpx;
     background: $theme-color;
-    font-size: 32rpx;
+    font-size: 26rpx;
+    border-radius: 8rpx;
     color: #fff;
+    white-space: nowrap;
     margin: 0;
   }
 
   .menu_icon {
+    flex-shrink: 0;
     width: 44rpx;
     height: 44rpx;
   }
@@ -609,7 +734,7 @@ page {
 }
 
 .list_box {
-  flex: 1;
+  // flex: 1;
   overflow: hidden;
   padding: 16rpx 0;
 
@@ -751,11 +876,30 @@ page {
   min-height: 100rpx;
   padding: 24rpx 32rpx;
 
-  .picker-value {
-    padding: 12rpx 16rpx;
-    border: 1rpx solid #e0e0e0;
-    border-radius: 6rpx;
-    font-size: 28rpx;
+  .date-range-wrapper {
+    display: flex;
+    align-items: center;
+    gap: 12rpx;
+
+    .date-picker-box {
+      flex: 1;
+      padding: 12rpx 16rpx;
+      border: 1rpx solid #e0e0e0;
+      border-radius: 6rpx;
+      font-size: 28rpx;
+      color: #333;
+      min-height: 56rpx;
+      line-height: 32rpx;
+
+      .placeholder {
+        color: #c0c4cc;
+      }
+    }
+
+    .date-separator {
+      font-size: 28rpx;
+      color: #666;
+    }
   }
 }
 

+ 374 - 0
pages/pda/workReport/components/ncReportDialog.vue

@@ -0,0 +1,374 @@
+<template>
+  <u-popup
+    :show="show"
+    mode="bottom"
+    :round="10"
+    closeable
+    @close="cancel"
+    bgColor="#fff"
+    :customStyle="{ maxHeight: '85vh', overflowY: 'auto' }"
+  >
+    <view class="nc_wrap">
+      <view class="title">{{ title }}</view>
+
+      <view class="section_title">报工信息</view>
+      <view class="form_row">
+        <view class="label required">实际开始时间</view>
+        <view class="picker_box" @click="!isDetail && openTimePicker('realStartTime')">
+          <text v-if="form.realStartTime" class="val">{{ form.realStartTime }}</text>
+          <text v-else class="placeholder">请选择实际开始时间</text>
+        </view>
+      </view>
+      <view class="form_row">
+        <view class="label required">实际结束时间</view>
+        <view class="picker_box" @click="!isDetail && openTimePicker('realEndTime')">
+          <text v-if="form.realEndTime" class="val">{{ form.realEndTime }}</text>
+          <text v-else class="placeholder">请选择实际结束时间</text>
+        </view>
+      </view>
+
+      <view class="info_grid">
+        <view class="info_item" v-for="item in fields" :key="item.prop">
+          <view class="info_label">{{ item.label }}</view>
+          <view class="info_val">{{ form[item.prop] || '-' }}</view>
+        </view>
+      </view>
+
+      <view class="form_row" v-if="form.remark">
+        <view class="label">订单备注</view>
+        <view class="readonly_text">{{ form.remark }}</view>
+      </view>
+
+      <view class="form_row">
+        <view class="label">附件</view>
+        <view class="file_list">
+          <view
+            v-for="(f, idx) in form.fileParam"
+            :key="(f && f.id) || idx"
+            class="file_item"
+          >
+            <text class="file_name">{{ f.name || f.fileName || ("附件" + (idx + 1)) }}</text>
+            <text v-if="!isDetail" class="file_del" @click="removeFile(idx)">删除</text>
+          </view>
+          <button v-if="!isDetail" class="upload_btn" @click="chooseFile">+ 上传附件</button>
+        </view>
+      </view>
+
+      <view class="form_row">
+        <view class="label">{{ isDetail ? '报备注' : '备注' }}</view>
+        <textarea
+          class="ipt textarea"
+          v-model="form.reportRemark"
+          :disabled="isDetail"
+          placeholder="请输入"
+          maxlength="-1"
+        />
+      </view>
+
+      <view class="btns">
+        <button class="btn cancel" @click="cancel">关闭</button>
+        <button v-if="!isDetail" class="btn confirm" @click="submit">报工</button>
+      </view>
+    </view>
+
+    <u-datetime-picker
+      :show="timeShow"
+      v-model="timeValue"
+      mode="datetime"
+      @confirm="onTimeConfirm"
+      @cancel="timeShow = false"
+      @close="timeShow = false"
+      :closeOnClickOverlay="true"
+    ></u-datetime-picker>
+  </u-popup>
+</template>
+
+<script>
+import { ncTaskReport } from "@/api/pda/workReport.js";
+
+export default {
+  data() {
+    return {
+      show: false,
+      title: "NC任务报工",
+      type: "",
+      form: {
+        realStartTime: "",
+        realEndTime: "",
+        brandNum: "",
+        categoryName: "",
+        categoryCode: "",
+        code: "",
+        productionPlanCode: "",
+        produceRoutingName: "",
+        taskName: "",
+        modelType: "",
+        specification: "",
+        fileParam: [],
+        remark: "",
+        reportRemark: "",
+      },
+      fields: [
+        { label: "牌号", prop: "brandNum" },
+        { label: "名称", prop: "categoryName" },
+        { label: "编码", prop: "categoryCode" },
+        { label: "任务编号", prop: "code" },
+        { label: "计划编号", prop: "productionPlanCode" },
+        { label: "工艺路线", prop: "produceRoutingName" },
+        { label: "工序名称", prop: "taskName" },
+        { label: "型号", prop: "modelType" },
+        { label: "规格", prop: "specification" },
+      ],
+      timeShow: false,
+      timeField: "",
+      timeValue: Date.now(),
+    };
+  },
+  computed: {
+    isDetail() {
+      return this.type === "detail";
+    },
+  },
+  methods: {
+    open(item, type) {
+      this.type = type || "";
+      this.title = this.isDetail ? "NC任务详情" : "NC任务报工";
+      this.form = {
+        realStartTime: "",
+        realEndTime: "",
+        fileParam: [],
+        remark: "",
+        reportRemark: "",
+        ...JSON.parse(JSON.stringify(item || {})),
+      };
+      this.show = true;
+    },
+    cancel() {
+      this.show = false;
+    },
+    openTimePicker(field) {
+      this.timeField = field;
+      const cur = this.form[field];
+      this.timeValue = cur ? new Date(cur.replace(/-/g, "/")).getTime() : Date.now();
+      this.timeShow = true;
+    },
+    onTimeConfirm(e) {
+      const d = new Date(e.value);
+      const pad = (n) => String(n).padStart(2, "0");
+      this.form[this.timeField] =
+        d.getFullYear() +
+        "-" +
+        pad(d.getMonth() + 1) +
+        "-" +
+        pad(d.getDate()) +
+        " " +
+        pad(d.getHours()) +
+        ":" +
+        pad(d.getMinutes()) +
+        ":" +
+        pad(d.getSeconds());
+      this.timeShow = false;
+    },
+    chooseFile() {
+      const max = 9;
+      const cur = this.form.fileParam || [];
+      const remain = max - cur.length;
+      if (remain <= 0) {
+        return uni.showToast({ title: "最多上传9个附件", icon: "none" });
+      }
+      uni.chooseImage({
+        count: remain,
+        sizeType: ["compressed"],
+        sourceType: ["camera", "album"],
+        success: (res) => this.uploadFiles(res.tempFilePaths),
+      });
+    },
+    uploadFiles(paths) {
+      uni.showLoading({ title: "上传中..." });
+      const tasks = paths.map(
+        (p) =>
+          new Promise((resolve, reject) => {
+            uni.uploadFile({
+              url: this.apiUrl + "/main/file/upload",
+              filePath: p,
+              name: "multiPartFile",
+              header: { authorization: uni.getStorageSync("token") },
+              success: (r) => {
+                try {
+                  const data = JSON.parse(r.data);
+                  if (data.code == 0) resolve(data.data);
+                  else reject(data.message || "上传失败");
+                } catch (e) {
+                  reject("上传失败");
+                }
+              },
+              fail: () => reject("上传失败"),
+            });
+          }),
+      );
+      Promise.all(tasks)
+        .then((arr) => {
+          if (!this.form.fileParam) this.form.fileParam = [];
+          this.form.fileParam.push(...arr);
+          uni.hideLoading();
+          uni.showToast({ title: "上传成功", icon: "success" });
+        })
+        .catch((err) => {
+          uni.hideLoading();
+          uni.showToast({ title: err || "上传失败", icon: "none" });
+        });
+    },
+    removeFile(idx) {
+      this.form.fileParam.splice(idx, 1);
+    },
+    submit() {
+      if (!this.form.realStartTime)
+        return uni.showToast({ title: "请选择实际开始时间", icon: "none" });
+      if (!this.form.realEndTime)
+        return uni.showToast({ title: "请选择实际结束时间", icon: "none" });
+      if (!this.form.reportRemark)
+        return uni.showToast({ title: "请输入备注", icon: "none" });
+
+      const payload = {
+        ...this.form,
+        fileParam: (this.form.fileParam || []).map((f) =>
+          f && typeof f === "object" ? { id: f.id } : { id: f },
+        ),
+      };
+
+      ncTaskReport(payload)
+        .then(() => {
+          uni.showToast({ title: "报工成功", icon: "success" });
+          this.$emit("refreshList");
+          this.cancel();
+        })
+        .catch((err) => {
+          uni.showToast({ title: err.message || "报工失败", icon: "none" });
+        });
+    },
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+.nc_wrap {
+  padding: 30rpx;
+  .title {
+    font-size: 32rpx;
+    font-weight: 600;
+    text-align: center;
+    margin-bottom: 20rpx;
+  }
+  .section_title {
+    font-size: 28rpx;
+    font-weight: 600;
+    color: #333;
+    margin: 20rpx 0 16rpx;
+    padding-left: 16rpx;
+    border-left: 6rpx solid $theme-color;
+  }
+  .form_row {
+    margin-bottom: 20rpx;
+    .label {
+      font-size: 26rpx;
+      color: #333;
+      margin-bottom: 8rpx;
+      &.required::before {
+        content: "*";
+        color: red;
+        margin-right: 4rpx;
+      }
+    }
+    .picker_box {
+      border: 1rpx solid #e0e0e0;
+      border-radius: 6rpx;
+      padding: 14rpx 16rpx;
+      font-size: 26rpx;
+      min-height: 60rpx;
+      .placeholder { color: #aaa; }
+    }
+    .ipt {
+      border: 1rpx solid #e0e0e0;
+      border-radius: 6rpx;
+      padding: 14rpx 16rpx;
+      font-size: 26rpx;
+      width: 100%;
+      box-sizing: border-box;
+    }
+    .textarea { min-height: 120rpx; }
+    .readonly_text {
+      padding: 14rpx 16rpx;
+      background: #f7f9fa;
+      border-radius: 6rpx;
+      font-size: 26rpx;
+      color: #666;
+      line-height: 40rpx;
+      word-break: break-all;
+    }
+    .file_list {
+      display: flex;
+      flex-wrap: wrap;
+      gap: 12rpx;
+      .file_item {
+        display: flex;
+        align-items: center;
+        background: #f7f9fa;
+        padding: 8rpx 14rpx;
+        border-radius: 6rpx;
+        font-size: 24rpx;
+        max-width: 100%;
+        .file_name {
+          color: #333;
+          word-break: break-all;
+          margin-right: 10rpx;
+        }
+        .file_del {
+          color: #f5222d;
+          font-size: 22rpx;
+        }
+      }
+      .upload_btn {
+        height: 56rpx;
+        line-height: 56rpx;
+        padding: 0 22rpx;
+        background: #fff;
+        color: $theme-color;
+        border: 1rpx dashed $theme-color;
+        border-radius: 6rpx;
+        font-size: 24rpx;
+        margin: 0;
+      }
+    }
+  }
+  .info_grid {
+    display: flex;
+    flex-wrap: wrap;
+    background: #f7f9fa;
+    padding: 16rpx;
+    border-radius: 8rpx;
+    margin-bottom: 20rpx;
+    .info_item {
+      width: 50%;
+      padding: 8rpx 6rpx;
+      font-size: 24rpx;
+      box-sizing: border-box;
+      .info_label { color: #999; margin-bottom: 4rpx; }
+      .info_val { color: #333; word-break: break-all; }
+    }
+  }
+  .btns {
+    display: flex;
+    gap: 20rpx;
+    margin-top: 30rpx;
+    .btn {
+      flex: 1;
+      height: 80rpx;
+      line-height: 80rpx;
+      border-radius: 8rpx;
+      font-size: 28rpx;
+    }
+    .cancel { background: #f5f5f5; color: #333; }
+    .confirm { background: $theme-color; color: #fff; }
+  }
+}
+</style>

+ 161 - 0
pages/pda/workReport/components/reassignDialog.vue

@@ -0,0 +1,161 @@
+<template>
+  <u-popup
+    :show="show"
+    mode="center"
+    :round="10"
+    closeable
+    @close="cancel"
+    bgColor="#fff"
+    :customStyle="{ width: '85vw' }"
+  >
+    <view class="reassign_wrap">
+      <view class="title">选择转派人员</view>
+
+      <view class="search_row">
+        <input class="search_ipt" v-model="kw" placeholder="搜索人员姓名" />
+      </view>
+
+      <scroll-view scroll-y class="user_list">
+        <view
+          v-for="item in filteredList"
+          :key="item.id"
+          class="user_item"
+          :class="{ active: selectedId === item.id }"
+          @click="selectedId = item.id"
+        >
+          <view class="name">{{ item.name }}</view>
+          <view v-if="selectedId === item.id" class="check">✓</view>
+        </view>
+        <view v-if="!filteredList.length" class="empty">暂无人员</view>
+      </scroll-view>
+
+      <view class="btns">
+        <button class="btn cancel" @click="cancel">取消</button>
+        <button class="btn confirm" @click="confirm">确定</button>
+      </view>
+    </view>
+  </u-popup>
+</template>
+
+<script>
+import { getteampage, transferTasks } from "@/api/pda/workReport.js";
+
+export default {
+  data() {
+    return {
+      show: false,
+      kw: "",
+      selectedId: "",
+      taskId: "",
+      teamList: [],
+    };
+  },
+  computed: {
+    filteredList() {
+      const k = (this.kw || "").trim();
+      if (!k) return this.teamList;
+      return this.teamList.filter((u) => (u.name || "").indexOf(k) > -1);
+    },
+  },
+  methods: {
+    async open(row) {
+      this.taskId = row.id;
+      this.selectedId = "";
+      this.kw = "";
+      this.teamList = [];
+      this.show = true;
+      try {
+        const res = await getteampage({ name: row.assignTeamName });
+        if (res && res.list && res.list[0]) {
+          this.teamList = res.list[0].userVOList || [];
+        }
+      } catch (err) {
+        uni.showToast({ title: err.message || "人员加载失败", icon: "none" });
+      }
+    },
+    cancel() {
+      this.show = false;
+    },
+    async confirm() {
+      if (!this.selectedId) {
+        return uni.showToast({ title: "请选择转派人员", icon: "none" });
+      }
+      const user = this.teamList.find((u) => u.id == this.selectedId);
+      try {
+        await transferTasks({
+          assigneeId: user.id,
+          assigneeName: user.name,
+          id: this.taskId,
+        });
+        uni.showToast({ title: "转派成功", icon: "success" });
+        this.$emit("success");
+        this.cancel();
+      } catch (err) {
+        uni.showToast({ title: err.message || "转派失败", icon: "none" });
+      }
+    },
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+.reassign_wrap {
+  padding: 30rpx;
+  .title {
+    font-size: 32rpx;
+    font-weight: 600;
+    text-align: center;
+    margin-bottom: 20rpx;
+  }
+  .search_row {
+    margin-bottom: 16rpx;
+    .search_ipt {
+      width: 100%;
+      box-sizing: border-box;
+      border: 1rpx solid #e0e0e0;
+      border-radius: 6rpx;
+      padding: 14rpx 16rpx;
+      font-size: 26rpx;
+    }
+  }
+  .user_list {
+    max-height: 50vh;
+    .user_item {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      padding: 20rpx 16rpx;
+      border-bottom: 1rpx solid #f0f0f0;
+      font-size: 28rpx;
+      &.active {
+        color: $theme-color;
+        background: rgba(21, 122, 44, 0.06);
+      }
+      .check {
+        color: $theme-color;
+        font-weight: bold;
+      }
+    }
+    .empty {
+      text-align: center;
+      color: #999;
+      padding: 40rpx 0;
+      font-size: 26rpx;
+    }
+  }
+  .btns {
+    display: flex;
+    gap: 20rpx;
+    margin-top: 30rpx;
+    .btn {
+      flex: 1;
+      height: 80rpx;
+      line-height: 80rpx;
+      border-radius: 8rpx;
+      font-size: 28rpx;
+    }
+    .cancel { background: #f5f5f5; color: #333; }
+    .confirm { background: $theme-color; color: #fff; }
+  }
+}
+</style>

+ 89 - 0
pages/pda/workReport/components/recordDialog.vue

@@ -0,0 +1,89 @@
+<template>
+  <u-popup
+    :show="show"
+    mode="center"
+    :round="10"
+    closeable
+    @close="cancel"
+    bgColor="#fff"
+    :customStyle="{ width: '88vw', maxHeight: '80vh', overflow: 'auto' }"
+  >
+    <view class="record_wrap">
+      <view class="title">报工记录</view>
+      <view v-if="!list.length" class="empty">暂无修改历史记录数据</view>
+      <view v-for="(rec, idx) in list" :key="idx" class="record_item">
+        <view class="rec_index">第 {{ idx + 1 }} 次</view>
+        <view class="rec_row"><text class="rec_label">实际开始:</text>{{ rec.realStartTime || '-' }}</view>
+        <view class="rec_row"><text class="rec_label">实际结束:</text>{{ rec.realEndTime || '-' }}</view>
+        <view class="rec_row"><text class="rec_label">工时:</text>{{ rec.durationText || '-' }}</view>
+        <view class="rec_row"><text class="rec_label">报工数:</text>{{ rec.reportQuantity || 0 }}</view>
+        <view class="rec_row"><text class="rec_label">损耗数:</text>{{ rec.lossQuantity || 0 }}</view>
+        <view class="rec_row"><text class="rec_label">备注:</text>{{ rec.remark || '-' }}</view>
+      </view>
+      <button class="btn" @click="cancel">返回</button>
+    </view>
+  </u-popup>
+</template>
+
+<script>
+export default {
+  data() {
+    return { show: false, list: [] };
+  },
+  methods: {
+    open(list = []) {
+      this.list = list;
+      this.show = true;
+    },
+    cancel() {
+      this.show = false;
+      this.list = [];
+    },
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+.record_wrap {
+  padding: 30rpx;
+  .title {
+    font-size: 32rpx;
+    font-weight: 600;
+    text-align: center;
+    margin-bottom: 20rpx;
+  }
+  .empty {
+    text-align: center;
+    color: #999;
+    padding: 40rpx 0;
+    font-size: 26rpx;
+  }
+  .record_item {
+    background: #f7f9fa;
+    padding: 16rpx;
+    border-radius: 8rpx;
+    margin-bottom: 14rpx;
+    .rec_index {
+      font-size: 24rpx;
+      color: $theme-color;
+      font-weight: 600;
+      margin-bottom: 6rpx;
+    }
+    .rec_row {
+      font-size: 24rpx;
+      color: #333;
+      line-height: 40rpx;
+      .rec_label { color: #999; }
+    }
+  }
+  .btn {
+    margin-top: 20rpx;
+    height: 76rpx;
+    line-height: 76rpx;
+    background: $theme-color;
+    color: #fff;
+    border-radius: 8rpx;
+    font-size: 28rpx;
+  }
+}
+</style>

+ 505 - 0
pages/pda/workReport/components/reportDialog.vue

@@ -0,0 +1,505 @@
+<template>
+  <u-popup
+    :show="show"
+    mode="bottom"
+    :round="10"
+    :closeable="true"
+    @close="cancel"
+    bgColor="#fff"
+    :customStyle="{ maxHeight: '85vh', overflowY: 'auto' }"
+  >
+    <view class="report_wrap">
+      <view class="title">{{ title }}</view>
+
+      <view class="section_title">报工信息</view>
+      <view class="form_row">
+        <view class="label required">实际开始时间</view>
+        <picker
+          mode="multiSelector"
+          v-if="false"
+        ></picker>
+        <view class="picker_box" @click="!inputDis && openTimePicker('realStartTime')">
+          <text v-if="form.realStartTime" class="val">{{ form.realStartTime }}</text>
+          <text v-else class="placeholder">请选择实际开始时间</text>
+        </view>
+      </view>
+
+      <view class="form_row">
+        <view class="label required">实际结束时间</view>
+        <view class="picker_box" @click="!inputDis && openTimePicker('realEndTime')">
+          <text v-if="form.realEndTime" class="val">{{ form.realEndTime }}</text>
+          <text v-else class="placeholder">请选择实际结束时间</text>
+        </view>
+      </view>
+
+      <view class="form_row">
+        <view class="label required">报工数</view>
+        <input
+          class="ipt"
+          type="digit"
+          v-model="form.reportQuantity"
+          :disabled="inputDis"
+          placeholder="请输入"
+          @input="(e) => handleQuantityInput(e.detail.value, 'reportQuantity')"
+        />
+      </view>
+
+      <view class="form_row">
+        <view class="label required">损耗数</view>
+        <input
+          class="ipt"
+          type="digit"
+          v-model="form.lossQuantity"
+          :disabled="inputDis"
+          placeholder="请输入"
+          @input="(e) => handleQuantityInput(e.detail.value, 'lossQuantity')"
+        />
+      </view>
+
+      <view class="form_row">
+        <view class="label">备注信息</view>
+        <textarea
+          class="ipt textarea"
+          v-model="form.remark"
+          :disabled="inputDis"
+          placeholder="请输入"
+          maxlength="-1"
+        />
+      </view>
+
+      <view class="tip" v-if="!inputDis">
+        任务剩余可报数量:{{ remaining }}
+      </view>
+
+      <view class="section_title">工单信息</view>
+      <view class="info_grid">
+        <view class="info_item" v-for="item in fieldMap.orderList" :key="item.prop">
+          <view class="info_label">{{ item.label }}</view>
+          <view class="info_val">{{ current[item.prop] || '-' }}</view>
+        </view>
+      </view>
+
+      <view class="section_title">任务信息</view>
+      <view class="info_grid">
+        <view class="info_item" v-for="item in fieldMap.taskList" :key="item.prop">
+          <view class="info_label">{{ item.label }}</view>
+          <view class="info_val">{{ current[item.prop] || '-' }}</view>
+        </view>
+      </view>
+
+      <view v-if="title === '详情'" class="section_title">报工记录</view>
+      <view v-if="title === '详情'" class="record_list">
+        <view v-if="list.length === 0" class="empty">暂无记录</view>
+        <view v-for="(rec, idx) in list" :key="idx" class="record_item">
+          <view class="rec_row"><text class="rec_label">实际开始:</text>{{ rec.realStartTime || '-' }}</view>
+          <view class="rec_row"><text class="rec_label">实际结束:</text>{{ rec.realEndTime || '-' }}</view>
+          <view class="rec_row"><text class="rec_label">工时:</text>{{ rec.durationText || '-' }}</view>
+          <view class="rec_row"><text class="rec_label">报工数:</text>{{ rec.reportQuantity || 0 }}</view>
+          <view class="rec_row"><text class="rec_label">损耗数:</text>{{ rec.lossQuantity || 0 }}</view>
+          <view class="rec_row"><text class="rec_label">备注:</text>{{ rec.remark || '-' }}</view>
+        </view>
+      </view>
+
+      <view class="btns">
+        <button class="btn cancel" @click="cancel">关闭</button>
+        <button v-if="!inputDis" class="btn confirm" :disabled="loadingBtn" @click="submitAdd">报工</button>
+      </view>
+    </view>
+
+    <u-datetime-picker
+      :show="timeShow"
+      v-model="timeValue"
+      mode="datetime"
+      @confirm="onTimeConfirm"
+      @cancel="timeShow = false"
+      @close="timeShow = false"
+      :closeOnClickOverlay="true"
+    ></u-datetime-picker>
+  </u-popup>
+</template>
+
+<script>
+import {
+  listWorkCenter,
+  batchUpdateRealTime,
+  listUpdateRealTimeRecord,
+} from "@/api/pda/workReport.js";
+
+export default {
+  data() {
+    return {
+      show: false,
+      title: "详情",
+      timeShow: false,
+      timeField: "",
+      timeValue: Date.now(),
+      form: {
+        realEndTime: "",
+        realStartTime: "",
+        remark: "",
+        reportQuantity: "",
+        lossQuantity: "",
+        workOrderCode: "",
+        workOrderId: "",
+        taskId: "",
+      },
+      current: {},
+      list: [],
+      reportNum: 0,
+      lossNum: 0,
+      loadingBtn: false,
+      fieldMap: {
+        orderList: [
+          { label: "计划批次号", prop: "batchNo" },
+          { label: "工单编码", prop: "mesWorkOrderCode" },
+          { label: "产品编码", prop: "productCode" },
+          { label: "产品名称", prop: "productName" },
+          { label: "规格", prop: "specification" },
+          { label: "生产订单编码", prop: "workOrderCode" },
+          { label: "计划编号", prop: "productionPlanCode" },
+          { label: "工艺路线", prop: "produceRoutingName" },
+          { label: "要求生产数量", prop: "formingNum" },
+          { label: "要求生产重量", prop: "formingWeight" },
+          { label: "所属工厂", prop: "factoryName" },
+          { label: "所属班组", prop: "assignTeamName" },
+          { label: "计划开始时间", prop: "planStartTime" },
+          { label: "计划结束时间", prop: "planCompleteTime" },
+        ],
+        taskList: [
+          { label: "任务编码", prop: "assignCode" },
+          { label: "工序名称", prop: "taskName" },
+          { label: "执行类型", prop: "assigneeType" },
+          { label: "执行对象", prop: "assigneeName" },
+          { label: "工时", prop: "durationText" },
+          { label: "任务开始时间", prop: "startTime" },
+          { label: "任务结束时间", prop: "endTime" },
+          { label: "任务数量", prop: "quantity" },
+          { label: "任务重量", prop: "weight" },
+        ],
+      },
+    };
+  },
+  computed: {
+    inputDis() {
+      return this.title === "详情";
+    },
+    remaining() {
+      const req = Number(this.current.quantity) || 0;
+      return Math.max(this.sub(req, this.add(this.reportNum, this.lossNum)), 0);
+    },
+  },
+  methods: {
+    open(type, row, form) {
+      this.title = type === "report" ? "报工" : "详情";
+      this.current = { ...row };
+      this.list = [];
+      if (form && form.realEndTime) {
+        this.form = { ...form };
+      } else {
+        this.form = {
+          realEndTime: form ? form.realEndTime || "" : "",
+          realStartTime: form ? form.realStartTime || "" : "",
+          remark: form ? form.remark || "" : "",
+          reportQuantity: form ? form.reportQuantity : "",
+          lossQuantity: form ? form.lossQuantity : 0,
+          workOrderCode: form ? form.workOrderCode : "",
+          workOrderId: form ? form.workOrderId : "",
+          taskId: form ? form.taskId : "",
+        };
+      }
+      this.reportNum = Number(this.current.reportQuantityReported) || 0;
+      this.lossNum = Number(this.current.lossQuantityReported) || 0;
+
+      if (type !== "report") {
+        listUpdateRealTimeRecord(this.current.apsAssigneeId)
+          .then((res) => {
+            if (res) this.list = res;
+          })
+          .catch((err) => {
+            uni.showToast({ title: err.message || "记录加载失败", icon: "none" });
+          });
+      }
+
+      this.getFactory();
+      this.show = true;
+    },
+    async getFactory() {
+      if (!this.current.firstTaskId) return;
+      try {
+        const res = await listWorkCenter(this.current.firstTaskId);
+        if (res && res.length) {
+          this.$set(this.current, "factoryName", res[0].factoryName);
+        }
+      } catch (err) {
+        // 忽略:仅展示字段
+      }
+    },
+    cancel() {
+      this.show = false;
+      this.form = {
+        realEndTime: "",
+        realStartTime: "",
+        remark: "",
+        reportQuantity: "",
+        lossQuantity: "",
+        workOrderCode: "",
+        workOrderId: "",
+        taskId: "",
+      };
+      this.current = {};
+      this.list = [];
+    },
+    openTimePicker(field) {
+      this.timeField = field;
+      const cur = this.form[field];
+      this.timeValue = cur ? new Date(cur.replace(/-/g, "/")).getTime() : Date.now();
+      this.timeShow = true;
+    },
+    onTimeConfirm(e) {
+      const ts = e.value;
+      const d = new Date(ts);
+      const pad = (n) => String(n).padStart(2, "0");
+      const val =
+        d.getFullYear() +
+        "-" +
+        pad(d.getMonth() + 1) +
+        "-" +
+        pad(d.getDate()) +
+        " " +
+        pad(d.getHours()) +
+        ":" +
+        pad(d.getMinutes()) +
+        ":" +
+        pad(d.getSeconds());
+      this.form[this.timeField] = val;
+      this.timeShow = false;
+      this.handleTimeChange();
+    },
+    handleTimeChange() {
+      if (this.form.realStartTime && this.current.startTime) {
+        const a = new Date(this.form.realStartTime.replace(/-/g, "/")).getTime();
+        const b = new Date(String(this.current.startTime).replace(/-/g, "/")).getTime();
+        if (a < b) {
+          this.form.realStartTime = "";
+          uni.showToast({ title: "实际开始时间不能早于任务开始时间", icon: "none" });
+          return;
+        }
+      }
+      if (this.form.realStartTime && this.form.realEndTime) {
+        const a = new Date(this.form.realStartTime.replace(/-/g, "/")).getTime();
+        const b = new Date(this.form.realEndTime.replace(/-/g, "/")).getTime();
+        if (a > b) {
+          this.form.realEndTime = "";
+          uni.showToast({ title: "结束时间不能早于开始时间", icon: "none" });
+        }
+      }
+    },
+    handleQuantityInput(val, type) {
+      let newVal = String(val || "").replace(/[^\d.]/g, "");
+      if (newVal.startsWith(".")) newVal = "";
+      const firstDot = newVal.indexOf(".");
+      if (firstDot !== -1) {
+        newVal =
+          newVal.slice(0, firstDot + 1) +
+          newVal.slice(firstDot + 1).replace(/\./g, "");
+      }
+      const match = newVal.match(/^(\d+)(\.\d{0,4})?/);
+      this.form[type] = match ? match[0] : "";
+      this.calculateQuantity(type);
+    },
+    calculateQuantity(type) {
+      const curRep = Number(this.form.reportQuantity) || 0;
+      const curLoss = Number(this.form.lossQuantity) || 0;
+      if (this.add(curRep, curLoss) > this.remaining) {
+        this.form[type] = "";
+        uni.showToast({
+          title: `本次报工数与损耗数之和不能超过任务剩余可报数量(剩余 ${this.remaining})`,
+          icon: "none",
+        });
+      }
+    },
+    getDecimalLength(num) {
+      return (num.toString().split(".")[1] || "").length;
+    },
+    toInteger(num) {
+      const len = this.getDecimalLength(num);
+      return { int: Math.round(num * Math.pow(10, len)), factor: Math.pow(10, len) };
+    },
+    add(a, b) {
+      const { int: aInt, factor: aFactor } = this.toInteger(a);
+      const { int: bInt, factor: bFactor } = this.toInteger(b);
+      const maxFactor = Math.max(aFactor, bFactor);
+      return (aInt * (maxFactor / aFactor) + bInt * (maxFactor / bFactor)) / maxFactor;
+    },
+    sub(a, b) {
+      const { int: aInt, factor: aFactor } = this.toInteger(a);
+      const { int: bInt, factor: bFactor } = this.toInteger(b);
+      const maxFactor = Math.max(aFactor, bFactor);
+      return (aInt * (maxFactor / aFactor) - bInt * (maxFactor / bFactor)) / maxFactor;
+    },
+    submitAdd() {
+      if (!this.form.realStartTime) {
+        return uni.showToast({ title: "请选择实际开始时间", icon: "none" });
+      }
+      if (!this.form.realEndTime) {
+        return uni.showToast({ title: "请选择实际结束时间", icon: "none" });
+      }
+      if (this.form.reportQuantity === "" || this.form.reportQuantity === null) {
+        return uni.showToast({ title: "请输入报工数量", icon: "none" });
+      }
+      if (this.form.lossQuantity === "" || this.form.lossQuantity === null) {
+        return uni.showToast({ title: "请输入损耗数量", icon: "none" });
+      }
+      const curRep = Number(this.form.reportQuantity) || 0;
+      const curLoss = Number(this.form.lossQuantity) || 0;
+      if (this.add(curRep, curLoss) > this.remaining) {
+        return uni.showToast({
+          title: `本次报工数与损耗数之和不能超过任务剩余可报数量(剩余 ${this.remaining})`,
+          icon: "none",
+        });
+      }
+      const data = { ...this.form, apsAssigneeId: this.current.apsAssigneeId };
+      this.loadingBtn = true;
+      batchUpdateRealTime([data])
+        .then(() => {
+          this.loadingBtn = false;
+          uni.showToast({ title: "操作成功", icon: "success" });
+          this.$emit("success");
+          this.cancel();
+        })
+        .catch((err) => {
+          this.loadingBtn = false;
+          uni.showToast({ title: err.message || "操作失败", icon: "none" });
+        });
+    },
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+.report_wrap {
+  padding: 30rpx;
+  .title {
+    font-size: 32rpx;
+    font-weight: 600;
+    text-align: center;
+    margin-bottom: 20rpx;
+  }
+  .section_title {
+    font-size: 28rpx;
+    font-weight: 600;
+    color: #333;
+    margin: 20rpx 0 16rpx;
+    padding-left: 16rpx;
+    border-left: 6rpx solid $theme-color;
+  }
+  .form_row {
+    margin-bottom: 20rpx;
+    .label {
+      font-size: 26rpx;
+      color: #333;
+      margin-bottom: 8rpx;
+      &.required::before {
+        content: "*";
+        color: red;
+        margin-right: 4rpx;
+      }
+    }
+    .picker_box {
+      border: 1rpx solid #e0e0e0;
+      border-radius: 6rpx;
+      padding: 14rpx 16rpx;
+      font-size: 26rpx;
+      min-height: 60rpx;
+      .placeholder {
+        color: #aaa;
+      }
+    }
+    .ipt {
+      border: 1rpx solid #e0e0e0;
+      border-radius: 6rpx;
+      padding: 14rpx 16rpx;
+      font-size: 26rpx;
+      width: 100%;
+      box-sizing: border-box;
+      min-height: 72rpx;
+      line-height: 44rpx;
+    }
+    .textarea {
+      min-height: 120rpx;
+    }
+  }
+  .tip {
+    color: #ff9c00;
+    font-size: 24rpx;
+    margin: -8rpx 0 16rpx;
+  }
+  .info_grid {
+    display: flex;
+    flex-wrap: wrap;
+    background: #f7f9fa;
+    padding: 16rpx;
+    border-radius: 8rpx;
+    .info_item {
+      width: 50%;
+      padding: 8rpx 6rpx;
+      font-size: 24rpx;
+      box-sizing: border-box;
+      .info_label {
+        color: #999;
+        margin-bottom: 4rpx;
+      }
+      .info_val {
+        color: #333;
+        word-break: break-all;
+      }
+    }
+  }
+  .record_list {
+    .empty {
+      text-align: center;
+      color: #999;
+      font-size: 26rpx;
+      padding: 30rpx 0;
+    }
+    .record_item {
+      background: #f7f9fa;
+      padding: 16rpx;
+      border-radius: 8rpx;
+      margin-bottom: 12rpx;
+      .rec_row {
+        font-size: 24rpx;
+        color: #333;
+        line-height: 40rpx;
+        .rec_label {
+          color: #999;
+        }
+      }
+    }
+  }
+  .btns {
+    display: flex;
+    gap: 20rpx;
+    margin-top: 30rpx;
+    position: sticky;
+    bottom: 0;
+    background: #fff;
+    padding-top: 16rpx;
+    .btn {
+      flex: 1;
+      height: 80rpx;
+      line-height: 80rpx;
+      border-radius: 8rpx;
+      font-size: 28rpx;
+    }
+    .cancel {
+      background: #f5f5f5;
+      color: #333;
+    }
+    .confirm {
+      background: $theme-color;
+      color: #fff;
+    }
+  }
+}
+</style>

+ 173 - 0
pages/pda/workReport/components/searchPopup.vue

@@ -0,0 +1,173 @@
+<template>
+  <u-popup
+    :show="show"
+    mode="top"
+    :round="10"
+    closeable
+    @close="close"
+    bgColor="#fff"
+  >
+    <view class="search_wrap">
+      <view class="title">筛选</view>
+      <view class="form_item">
+        <view class="label">接收状态</view>
+        <u-radio-group
+          class="radio-group"
+          v-model="form.disposalStatus"
+          placement="row"
+          iconSize="32"
+          labelSize="28"
+        >
+          <u-radio
+            v-for="(it, idx) in disposalOpts"
+            :key="idx"
+            :customStyle="{ marginRight: '20rpx' }"
+            :label="it.label"
+            :name="it.value"
+          ></u-radio>
+        </u-radio-group>
+      </view>
+      <view class="form_item">
+        <view class="label">执行状态</view>
+        <u-radio-group
+          class="radio-group"
+          v-model="form.status"
+          placement="row"
+          iconSize="32"
+          labelSize="28"
+        >
+          <u-radio
+            v-for="(it, idx) in statusOpts"
+            :key="idx"
+            :customStyle="{ marginRight: '20rpx' }"
+            :label="it.label"
+            :name="it.value"
+          ></u-radio>
+        </u-radio-group>
+      </view>
+      <view class="form_item">
+        <view class="label">班组名称</view>
+        <u-input v-model="form.teamName" placeholder="请输入"></u-input>
+      </view>
+      <view class="form_item">
+        <view class="label">产品名称</view>
+        <u-input v-model="form.productName" placeholder="请输入"></u-input>
+      </view>
+      <view class="form_item">
+        <view class="label">工序名称</view>
+        <u-input v-model="form.workTaskName" placeholder="请输入"></u-input>
+      </view>
+      <view class="form_item">
+        <view class="label">订单编码</view>
+        <u-input v-model="form.workOrderCode" placeholder="请输入"></u-input>
+      </view>
+      <view class="btns">
+        <button class="btn reset" @click="reset">重置</button>
+        <button class="btn confirm" @click="confirm">确定</button>
+      </view>
+    </view>
+  </u-popup>
+</template>
+
+<script>
+export default {
+  props: {
+    show: { type: Boolean, default: false },
+  },
+  data() {
+    return {
+      form: {
+        disposalStatus: "",
+        status: "",
+        teamName: "",
+        productName: "",
+        workTaskName: "",
+        workOrderCode: "",
+      },
+      disposalOpts: [
+        { label: "待接收", value: 0 },
+        { label: "已接收", value: 1 },
+        { label: "已拒绝", value: 2 },
+      ],
+      statusOpts: [
+        { label: "进行中", value: 1 },
+        { label: "已转派", value: 2 },
+        { label: "已取消", value: 3 },
+        { label: "已关闭", value: 4 },
+        { label: "已完成", value: 5 },
+      ],
+    };
+  },
+  methods: {
+    close() {
+      this.$emit("close");
+    },
+    reset() {
+      this.form = {
+        disposalStatus: "",
+        status: "",
+        teamName: "",
+        productName: "",
+        workTaskName: "",
+        workOrderCode: "",
+      };
+      this.$emit("search", { ...this.form });
+      this.close();
+    },
+    confirm() {
+      this.$emit("search", { ...this.form });
+      this.close();
+    },
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+.search_wrap {
+  padding: 30rpx 30rpx 40rpx;
+  .title {
+    font-size: 32rpx;
+    font-weight: 600;
+    text-align: center;
+    margin-bottom: 20rpx;
+  }
+  .form_item {
+    margin-bottom: 24rpx;
+    .label {
+      font-size: 26rpx;
+      color: #333;
+      margin-bottom: 10rpx;
+    }
+  }
+  /deep/ .radio-group {
+    flex-wrap: wrap;
+    .u-radio__text {
+      font-size: 28rpx !important;
+    }
+    .u-radio__icon-wrap {
+      width: 32rpx !important;
+      height: 32rpx !important;
+    }
+  }
+  .btns {
+    display: flex;
+    gap: 20rpx;
+    margin-top: 30rpx;
+    .btn {
+      flex: 1;
+      height: 72rpx;
+      line-height: 72rpx;
+      border-radius: 8rpx;
+      font-size: 28rpx;
+    }
+    .reset {
+      background: #f5f5f5;
+      color: #333;
+    }
+    .confirm {
+      background: $theme-color;
+      color: #fff;
+    }
+  }
+}
+</style>

+ 753 - 0
pages/pda/workReport/index/index.vue

@@ -0,0 +1,753 @@
+<template>
+  <view class="content-box">
+    <uni-nav-bar
+      fixed="true"
+      statusBar="true"
+      left-icon="back"
+      title="任务报工"
+      background-color="#157A2C"
+      color="#fff"
+      @clickLeft="back"
+    ></uni-nav-bar>
+
+    <view class="tabs_wrap">
+      <view
+        v-for="tab in tabs"
+        :key="tab.value"
+        class="tab_item"
+        :class="{ active: tabValue === tab.value }"
+        @click="handleTabClick(tab.value)"
+      >
+        {{ tab.label }}
+      </view>
+    </view>
+
+    <view class="top-wrapper">
+      <uni-easyinput
+        prefixIcon="search"
+        style="flex: 1"
+        v-model="searchFrom.workOrderCode"
+        placeholder="订单编码"
+      ></uni-easyinput>
+      <button class="search_btn" @click="doSearch">搜索</button>
+      <button class="search_btn" @click="filterShow = true">筛选</button>
+    </view>
+
+    <view class="list_box">
+      <u-list @scrolltolower="scrolltolower">
+        <u-list-item v-for="(item, index) in dataList" :key="item.id || index">
+          <view class="card_box">
+            <view class="item_box rx-bc">
+              <view class="round">{{ index + 1 }}</view>
+              <view class="item_one perce100 rx-sc">
+                <view class="lable">任务编号:</view>
+                <view class="val">{{
+                  tabValue === "4" ? item.code : item.assigneeCode
+                }}</view>
+              </view>
+            </view>
+
+            <view v-if="tabValue !== '4'" class="item_box rx-bc">
+              <view class="item_one perce100 rx-sc">
+                <view class="lable">订单编号:</view>
+                <view class="val">{{ item.workOrderCode }}</view>
+              </view>
+            </view>
+
+            <view class="item_box rx-bc">
+              <view class="item_one perce100 rx-sc">
+                <view class="lable">计划编号:</view>
+                <view class="val">{{ item.productionPlanCode }}</view>
+              </view>
+            </view>
+
+            <view class="item_box rx-bc">
+              <view class="item_one perce100 rx-sc">
+                <view class="lable">产品名称:</view>
+                <view class="val">{{
+                  tabValue === "4" ? item.categoryName : item.productName
+                }}</view>
+              </view>
+            </view>
+
+            <view class="item_box rx-bc">
+              <view class="item_one perce100 rx-sc">
+                <view class="lable">工序名称:</view>
+                <view class="val">{{ item.taskName }}</view>
+              </view>
+            </view>
+
+            <view v-if="tabValue !== '4'" class="item_box rx-bc">
+              <view class="item_one perce50 rx-sc">
+                <view class="lable">任务数量:</view>
+                <view class="val">{{ item.quantity }}</view>
+              </view>
+              <view class="item_one perce50 rx-sc">
+                <view class="lable">班组:</view>
+                <view class="val">{{ item.assignTeamName }}</view>
+              </view>
+            </view>
+
+            <template v-if="tabValue === '4'">
+              <view class="item_box rx-bc">
+                <view class="item_one perce50 rx-sc">
+                  <view class="lable">执行部门:</view>
+                  <view class="val">{{ item.executeGroupName || "-" }}</view>
+                </view>
+                <view class="item_one perce50 rx-sc">
+                  <view class="lable">执行人:</view>
+                  <view class="val">{{ item.executorName || "-" }}</view>
+                </view>
+              </view>
+              <view class="item_box rx-bc">
+                <view class="item_one perce50 rx-sc">
+                  <view class="lable">产品编码:</view>
+                  <view class="val">{{ item.categoryCode || "-" }}</view>
+                </view>
+                <view class="item_one perce50 rx-sc">
+                  <view class="lable">牌号:</view>
+                  <view class="val">{{ item.brandNum || "-" }}</view>
+                </view>
+              </view>
+              <view class="item_box rx-bc">
+                <view class="item_one perce50 rx-sc">
+                  <view class="lable">型号:</view>
+                  <view class="val">{{ item.modelType || "-" }}</view>
+                </view>
+                <view class="item_one perce50 rx-sc">
+                  <view class="lable">规格:</view>
+                  <view class="val">{{ item.specification || "-" }}</view>
+                </view>
+              </view>
+              <view
+                class="item_box rx-bc"
+                v-if="item.realStartTime || item.realEndTime"
+              >
+                <view class="item_one perce100 rx-sc">
+                  <view class="lable">实际时间:</view>
+                  <view class="val"
+                    >{{ item.realStartTime || "-" }} ~
+                    {{ item.realEndTime || "-" }}</view
+                  >
+                </view>
+              </view>
+              <view class="item_box rx-bc" v-if="item.remark">
+                <view class="item_one perce100 rx-sc">
+                  <view class="lable">订单备注:</view>
+                  <view class="val">{{ item.remark }}</view>
+                </view>
+              </view>
+              <view class="item_box rx-bc" v-if="item.reportRemark">
+                <view class="item_one perce100 rx-sc">
+                  <view class="lable">报备注:</view>
+                  <view class="val">{{ item.reportRemark }}</view>
+                </view>
+              </view>
+            </template>
+
+            <view v-if="tabValue !== '4'" class="item_box rx-bc">
+              <view class="item_one perce100 rx-sc">
+                <view class="lable">状态:</view>
+                <view class="val">
+                  <text class="tag" :class="disposalCls(item.disposalStatus)">{{
+                    disposalText(item.disposalStatus)
+                  }}</text>
+                  <text class="tag tag-default" v-if="item.statusText">{{
+                    item.statusText
+                  }}</text>
+                </view>
+              </view>
+            </view>
+
+            <view class="action_row">
+              <!-- 我的任务 / 班组任务:未处置时可接收 / 拒绝 -->
+              <button
+                v-if="canAcceptReject(item)"
+                class="op_btn primary"
+                @click="receiveTask(item.id, '1')"
+              >
+                接收
+              </button>
+              <button
+                v-if="canAcceptReject(item)"
+                class="op_btn warn"
+                @click="receiveTask(item.id, '2')"
+              >
+                拒绝
+              </button>
+
+              <!-- 全部任务: 已拒绝可转派 -->
+              <button
+                v-if="tabValue === '2' && item.disposalStatus == '2'"
+                class="op_btn primary"
+                @click="reassignTask(item)"
+              >
+                转派
+              </button>
+
+              <!-- 详情 -->
+              <button class="op_btn ghost" @click="details('detail', item)">
+                详情
+              </button>
+
+              <!-- 报工 -->
+              <button
+                v-if="canReport(item)"
+                class="op_btn primary"
+                @click="details('report', item)"
+              >
+                报工
+              </button>
+
+              <!-- 报工记录 -->
+              <button
+                v-if="item.disposalStatus == '1' && tabValue !== '4'"
+                class="op_btn ghost"
+                @click="viewRecords(item.id)"
+              >
+                报工记录
+              </button>
+            </view>
+          </view>
+        </u-list-item>
+
+        <u-list-item v-if="dataList.length === 0">
+          <view style="margin-top: 20vh">
+            <u-empty iconSize="150" textSize="32" text="暂无任务"></u-empty>
+          </view>
+        </u-list-item>
+      </u-list>
+    </view>
+
+    <searchPopup
+      :show="filterShow"
+      ref="filterRef"
+      @close="filterShow = false"
+      @search="onFilterSearch"
+    ></searchPopup>
+
+    <reportDialog ref="reportRef" @success="reload"></reportDialog>
+    <ncReportDialog ref="ncReportRef" @refreshList="reload"></ncReportDialog>
+    <recordDialog ref="recordRef"></recordDialog>
+    <reassignDialog ref="reassignRef" @success="reload"></reassignDialog>
+  </view>
+</template>
+
+<script>
+import {
+  pageByCurrentUser,
+  pageByCurrentUserLeader,
+  pageByCurrentCurrentUserTeam,
+  getNCtaskListData,
+  taskManagement,
+  listUpdateRealTimeRecord,
+} from "@/api/pda/workReport.js";
+
+import searchPopup from "../components/searchPopup.vue";
+import reportDialog from "../components/reportDialog.vue";
+import ncReportDialog from "../components/ncReportDialog.vue";
+import recordDialog from "../components/recordDialog.vue";
+import reassignDialog from "../components/reassignDialog.vue";
+
+let isEnd = false;
+
+export default {
+  components: {
+    searchPopup,
+    reportDialog,
+    ncReportDialog,
+    recordDialog,
+    reassignDialog,
+  },
+  data() {
+    return {
+      tabValue: "1",
+      page: 1,
+      size: 10,
+      dataList: [],
+      filterShow: false,
+      searchFrom: {
+        workOrderCode: "",
+        teamName: "",
+        productName: "",
+        workTaskName: "",
+        disposalStatus: "",
+        status: "",
+      },
+    };
+  },
+  onShow() {
+    if (!this.tabs.find((t) => t.value === this.tabValue)) {
+      this.tabValue = this.tabs.length ? this.tabs[0].value : "1";
+    }
+    this.page = 1;
+    this.getList();
+  },
+  computed: {
+    tabs() {
+      const list = [{ label: "我的任务", value: "1" }];
+      if (this.$isAuthorities("mes:taskreport:allorder")) {
+        list.push({ label: "全部任务", value: "2" });
+      }
+      if (this.$isAuthorities("mes:taskreport:teamorder")) {
+        list.push({ label: "班组任务", value: "3" });
+      }
+      if (this.$isAuthorities("mes:taskreport:programmingorder")) {
+        list.push({ label: "NC编程任务", value: "4" });
+      }
+      return list;
+    },
+  },
+  methods: {
+    back() {
+      uni.navigateBack();
+    },
+    handleTabClick(val) {
+      if (val === this.tabValue) return;
+      this.tabValue = val;
+      this.page = 1;
+      this.dataList = [];
+      this.getList();
+    },
+    doSearch() {
+      this.page = 1;
+      this.getList();
+    },
+    onFilterSearch(e) {
+      this.searchFrom = { ...this.searchFrom, ...e };
+      this.doSearch();
+    },
+    reload() {
+      this.page = 1;
+      this.getList();
+    },
+    scrolltolower() {
+      if (isEnd) return;
+      this.page++;
+      this.getList();
+    },
+    async getList() {
+      const params = {
+        ...this.searchFrom,
+        pageNum: this.page,
+        size: this.size,
+      };
+      if (
+        params.status !== undefined &&
+        params.status !== null &&
+        params.status !== "" &&
+        this.tabValue !== "4"
+      ) {
+        params.status = params.status;
+      }
+      if (this.tabValue === "3") {
+        const userInfo = uni.getStorageSync("userInfo") || {};
+        params.teamIdsStr = userInfo.teamId || "";
+      }
+      Object.keys(params).forEach((k) => {
+        if (params[k] === "" || params[k] === null || params[k] === undefined) {
+          delete params[k];
+        }
+      });
+
+      const api =
+        this.tabValue === "1"
+          ? pageByCurrentUser
+          : this.tabValue === "2"
+            ? pageByCurrentUserLeader
+            : this.tabValue === "3"
+              ? pageByCurrentCurrentUserTeam
+              : getNCtaskListData;
+
+      isEnd = false;
+      try {
+        const res = await api(params);
+        if (!res) return;
+        if (this.page === 1) this.dataList = [];
+        const list = res.list || [];
+        this.dataList.push(...list);
+
+        // 判断是否到底:优先用 total/count,否则看本次返回数量是否小于 pageSize
+        if (res.total !== undefined || res.count !== undefined) {
+          const total = res.total || res.count;
+          isEnd = this.dataList.length >= total;
+        } else {
+          isEnd = list.length < this.size;
+        }
+      } catch (err) {
+        uni.showToast({ title: err.message || "加载失败", icon: "none" });
+      }
+    },
+
+    /* 卡片操作的可见性条件(与 PC 端保持一致) */
+    canAcceptReject(row) {
+      const result =
+        row.disposalStatus != "1" &&
+        row.disposalStatus != "2" &&
+        this.tabValue !== "2" &&
+        this.tabValue !== "4";
+      return result;
+    },
+    canReport(row) {
+      if (this.tabValue === "4") {
+        return !row.status || row.status.code != 5;
+      }
+      const noInspect =
+        row.disposalStatus == "1" && row.hasFirstArticleDualInspection == "0";
+      const inspectDone =
+        row.disposalStatus == "1" &&
+        row.totalFirstArticleDualInspectionStatus == 2 &&
+        row.hasFirstArticleDualInspection == "1";
+      return noInspect || inspectDone;
+    },
+
+    disposalText(s) {
+      if (s == 0) return "待接收";
+      if (s == 1) return "已接收";
+      if (s == 2) return "已拒绝";
+      return "";
+    },
+    disposalCls(s) {
+      if (s == 0) return "tag-warn";
+      if (s == 1) return "tag-success";
+      if (s == 2) return "tag-danger";
+      return "";
+    },
+
+    async receiveTask(id, type) {
+      try {
+        const res = await taskManagement({ id, disposalStatus: Number(type) });
+        console.log("[receiveTask] api success", res);
+        uni.showToast({
+          title: type === "1" ? "已接收" : "已拒绝",
+          icon: "success",
+        });
+        this.reload();
+      } catch (err) {
+        console.error("[receiveTask] api error", err);
+        uni.showToast({
+          title: err.message || err || "操作失败",
+          icon: "none",
+        });
+      }
+    },
+
+    reassignTask(row) {
+      this.$refs.reassignRef.open(row);
+    },
+
+    viewRecords(apsAssigneeId) {
+      listUpdateRealTimeRecord(apsAssigneeId)
+        .then((res) => {
+          if (res && res.length > 0) {
+            this.$refs.recordRef.open(res);
+          } else {
+            uni.showToast({ title: "暂无修改历史记录数据", icon: "none" });
+          }
+        })
+        .catch((err) => {
+          uni.showToast({ title: err.message || "加载失败", icon: "none" });
+        });
+    },
+
+    /* 跳转报工/详情/NC */
+    details(type, row) {
+      if (this.tabValue === "4") {
+        if (type === "detail") {
+          this.$refs.ncReportRef.open(
+            JSON.parse(JSON.stringify(row)),
+            "detail",
+          );
+          return;
+        }
+        const data = {
+          realStartTime: "",
+          realEndTime: "",
+          brandNum: row.brandNum,
+          categoryName: row.categoryName,
+          categoryCode: row.categoryCode,
+          code: row.code,
+          productionPlanCode: row.productionPlanCode,
+          produceRoutingName: row.produceRoutingName,
+          taskName: row.taskName,
+          modelType: row.modelType,
+          specification: row.specification,
+          fileParam: [],
+          reportRemark: row.reportRemark,
+          id: row.id,
+          sourceTaskId: row.sourceTaskId,
+        };
+        this.$refs.ncReportRef.open(data);
+        return;
+      }
+
+      const currentRow = {
+        assignCode: row.assignCode,
+        mesWorkOrderCode: row.mesWorkOrderCode,
+        workOrderCode: row.workOrderCode,
+        workOrderId: row.workOrderId,
+        productionPlanCode: row.productionPlanCode,
+        produceRoutingName: row.produceRoutingName,
+        formingNum: row.formingNum,
+        assignTeamName: row.assignTeamName,
+        formingWeight: row.formingWeight,
+        planStartTime: row.planStartTime,
+        planCompleteTime: row.planCompleteTime,
+        startTime: row.startTime,
+        firstTaskId: row.firstTaskId,
+        endTime: row.endTime,
+        taskName: row.taskName,
+        firstTaskName: row.firstTaskName,
+        assigneeType: row.assigneeType ? row.assigneeType.desc : "",
+        assigneeName: row.assigneeName,
+        weight: row.weight,
+        quantity: row.quantity,
+        durationText: row.durationText,
+        apsAssigneeId: row.id,
+        batchNo: row.batchNo,
+        productCode: row.productCode,
+        productName: row.productName,
+        specification: row.specification,
+        newWeightUnit: row.newWeightUnit,
+        measuringUnit: row.measuringUnit,
+        reportQuantityReported: row.reportQuantity || 0,
+        lossQuantityReported: row.lossQuantity || 0,
+      };
+
+      const taskQuantity = Number(row.quantity) || 0;
+      const actualQuantity = this.add(
+        row.reportQuantity ? row.reportQuantity : 0,
+        row.lossQuantity ? row.lossQuantity : 0,
+      );
+      const remaining = Math.max(this.sub(taskQuantity, actualQuantity), 0);
+      const form = {
+        realEndTime: row.realEndTime,
+        realStartTime: row.realStartTime,
+        remark: row.assigneeRemark,
+        reportQuantity: row.reportQuantity ? remaining : taskQuantity,
+        lossQuantity: 0,
+        workOrderCode: row.workOrderCode,
+        workOrderId: row.workOrderId,
+        taskId: row.taskId,
+      };
+
+      this.$refs.reportRef.open(type, currentRow, form);
+    },
+
+    getDecimalLength(num) {
+      return (num.toString().split(".")[1] || "").length;
+    },
+    toInteger(num) {
+      const len = this.getDecimalLength(num);
+      return {
+        int: Math.round(num * Math.pow(10, len)),
+        factor: Math.pow(10, len),
+      };
+    },
+    add(a, b) {
+      const { int: aInt, factor: aFactor } = this.toInteger(a);
+      const { int: bInt, factor: bFactor } = this.toInteger(b);
+      const maxFactor = Math.max(aFactor, bFactor);
+      return (
+        (aInt * (maxFactor / aFactor) + bInt * (maxFactor / bFactor)) /
+        maxFactor
+      );
+    },
+    sub(a, b) {
+      const { int: aInt, factor: aFactor } = this.toInteger(a);
+      const { int: bInt, factor: bFactor } = this.toInteger(b);
+      const maxFactor = Math.max(aFactor, bFactor);
+      return (
+        (aInt * (maxFactor / aFactor) - bInt * (maxFactor / bFactor)) /
+        maxFactor
+      );
+    },
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+.content-box {
+  height: 100vh;
+  display: flex;
+  flex-direction: column;
+  background-color: $page-bg;
+}
+
+.tabs_wrap {
+  display: flex;
+  background: #fff;
+  border-bottom: 1rpx solid #eee;
+  .tab_item {
+    flex: 1;
+    text-align: center;
+    padding: 22rpx 0;
+    font-size: 28rpx;
+    color: #666;
+    position: relative;
+    &.active {
+      color: $theme-color;
+      font-weight: 600;
+      &::after {
+        content: "";
+        position: absolute;
+        left: 50%;
+        bottom: 0;
+        transform: translateX(-50%);
+        width: 60rpx;
+        height: 4rpx;
+        background: $theme-color;
+        border-radius: 2rpx;
+      }
+    }
+  }
+}
+
+.top-wrapper {
+  background: #fff;
+  display: flex;
+  align-items: center;
+  padding: 20rpx 26rpx;
+  gap: 20rpx;
+  box-sizing: border-box;
+  .search_btn {
+    padding: 0 26rpx;
+    height: 64rpx;
+    line-height: 64rpx;
+    background: $theme-color;
+    color: #fff;
+    border-radius: 8rpx;
+    font-size: 26rpx;
+    white-space: nowrap;
+  }
+}
+
+.list_box {
+  // flex: 1;
+  min-height: 0;
+  overflow: hidden;
+  padding: 16rpx 0;
+  display: flex;
+  flex-direction: column;
+
+  /deep/ .u-list {
+    flex: 1;
+    height: 0 !important;
+    min-height: 0;
+    padding: 20rpx;
+  }
+
+  .card_box {
+    width: 100%;
+    padding: 24rpx 28rpx;
+    box-sizing: border-box;
+    border-bottom: 2rpx solid #f0f0f0;
+    background: #fff;
+    border-radius: 12rpx;
+    margin-bottom: 16rpx;
+
+    .item_box {
+      display: flex;
+      flex-wrap: wrap;
+      align-items: center;
+      margin-top: 14rpx;
+
+      .round {
+        width: 46rpx;
+        height: 46rpx;
+        line-height: 46rpx;
+        border-radius: 50%;
+        background: $theme-color;
+        color: #fff;
+        font-size: 24rpx;
+        text-align: center;
+        margin-right: 16rpx;
+        flex-shrink: 0;
+      }
+
+      .item_one {
+        width: 100%;
+        display: flex;
+        font-size: 26rpx;
+        line-height: 44rpx;
+        color: #333;
+        word-wrap: break-word;
+        .lable {
+          color: #666;
+          margin-right: 8rpx;
+          flex-shrink: 0;
+        }
+        .val {
+          flex: 1;
+          word-break: break-all;
+        }
+      }
+      .perce50 {
+        width: 48%;
+      }
+      .perce100 {
+        width: 100%;
+      }
+    }
+
+    .tag {
+      display: inline-block;
+      padding: 0 12rpx;
+      height: 36rpx;
+      line-height: 36rpx;
+      font-size: 22rpx;
+      border-radius: 4rpx;
+      margin-right: 8rpx;
+      background: #f0f0f0;
+      color: #555;
+      &.tag-warn {
+        background: #fff7e6;
+        color: #fa8c16;
+      }
+      &.tag-success {
+        background: #e6f6ec;
+        color: $theme-color;
+      }
+      &.tag-danger {
+        background: #ffece8;
+        color: #f5222d;
+      }
+      &.tag-default {
+        background: #f0f0f0;
+        color: #555;
+      }
+    }
+
+    .action_row {
+      display: flex;
+      flex-wrap: wrap;
+      gap: 14rpx;
+      margin-top: 20rpx;
+      padding-top: 14rpx;
+      border-top: 1rpx dashed #eee;
+      .op_btn {
+        height: 56rpx;
+        line-height: 56rpx;
+        padding: 0 22rpx;
+        border-radius: 6rpx;
+        font-size: 24rpx;
+        margin: 0;
+      }
+      .primary {
+        background: $theme-color;
+        color: #fff;
+      }
+      .warn {
+        background: #fff;
+        color: #fa8c16;
+        border: 1rpx solid #fa8c16;
+      }
+      .ghost {
+        background: #fff;
+        color: $theme-color;
+        border: 1rpx solid $theme-color;
+      }
+    }
+  }
+}
+</style>