zhangwencui
2026-03-04 ac1218810d89c8daba0e8a3b516c3164b7ef0b2a
生产报工定制
已添加4个文件
已修改2个文件
1449 ■■■■■ 文件已修改
src/api/productionManagement/productWorkOrderFile.js 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/productionManagement/workOrder.js 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages.json 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/index.vue 23 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/productionManagement/workOrder/fileList.vue 564 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/productionManagement/workOrder/index.vue 784 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/productionManagement/productWorkOrderFile.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,29 @@
import request from "@/utils/request";
// æŸ¥è¯¢å·¥å•附件列表
export function productWorkOrderFileListPage(query) {
  return request({
    url: "/productWorkOrderFile/listPage",
    method: "get",
    params: query,
  });
}
// æ–°å¢žå·¥å•附件
export function productWorkOrderFileAdd(data) {
  return request({
    url: "/productWorkOrderFile/add",
    method: "post",
    data,
  });
}
// åˆ é™¤å·¥å•附件
export function productWorkOrderFileDel(data) {
  return request({
    url: "/productWorkOrderFile/del",
    method: "delete",
    data,
  });
}
src/api/productionManagement/workOrder.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,35 @@
import request from "@/utils/request";
export function productWorkOrderPage(query) {
  return request({
    url: "/productWorkOrder/page",
    method: "get",
    params: query,
  });
}
export function updateProductWorkOrder(data) {
  return request({
    url: "/productWorkOrder/updateProductWorkOrder",
    method: "post",
    data: data,
  });
}
export function addProductMain(data) {
  return request({
    url: "/productionProductMain/addProductMain",
    method: "post",
    data: data,
  });
}
// ä¸‹è½½å·¥å•流转卡(返回文件流)
export function downProductWorkOrder(id) {
  return request({
    url: "/productWorkOrder/down",
    method: "post",
    data: { id },
    responseType: "blob",
  });
}
src/pages.json
@@ -900,6 +900,20 @@
      }
    },
    {
      "path": "pages/productionManagement/workOrder/index",
      "style": {
        "navigationBarTitleText": "生产工单",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/productionManagement/workOrder/fileList",
      "style": {
        "navigationBarTitleText": "生产工单附件",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/message",
      "style": {
        "navigationBarTitleText": "消息中心"
src/pages/index.vue
@@ -397,10 +397,14 @@
    //   label: "工序排产",
    //   bgColor: "#E91E63",
    // },
    // {
    //   icon: "/static/images/icon/shengchanbaogong@2x.png",
    //   label: "生产报工",
    //   bgColor: "#673AB7",
    // },
    {
      icon: "/static/images/icon/shengchanbaogong@2x.png",
      icon: "/static/images/icon/caigoutaizhang@2x.png",
      label: "生产报工",
      bgColor: "#673AB7",
    },
    // {
    //   icon: "/static/images/icon/shengchanhesuan@2x.png",
@@ -617,8 +621,13 @@
          url: "/pages/productionManagement/processScheduling/index",
        });
        break;
      // case "生产报工":
      //   getcode();
      //   break;
      case "生产报工":
        getcode();
        uni.navigateTo({
          url: "/pages/productionManagement/workOrder/index",
        });
        break;
      case "生产核算":
        uni.navigateTo({
@@ -1003,10 +1012,14 @@
    // è¿‡æ»¤ç”Ÿäº§ç®¡æŽ§èœå•
    const originalProduction = [
      // {
      //   icon: "/static/images/icon/shengchanbaogong@2x.png",
      //   label: "生产报工",
      //   bgColor: "#673AB7",
      // },
      {
        icon: "/static/images/icon/shengchanbaogong@2x.png",
        icon: "/static/images/icon/caigoutaizhang@2x.png",
        label: "生产报工",
        bgColor: "#673AB7",
      },
    ];
    const filteredProduction = originalProduction.filter(item => {
src/pages/productionManagement/workOrder/fileList.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,564 @@
<template>
  <view class="file-list-page">
    <!-- é¡µé¢å¤´éƒ¨ -->
    <PageHeader title="附件管理"
                @back="goBack" />
    <!-- é™„件列表 -->
    <view class="file-list-container">
      <view v-if="fileList.length > 0"
            class="file-list">
        <view v-for="(file, index) in fileList"
              :key="file.id || index"
              class="file-item">
          <!-- æ–‡ä»¶å›¾æ ‡ -->
          <!-- <view class="file-icon"
                :class="getFileIconClass(file.fileType)">
            <up-icon :name="getFileIcon(file.fileType)"
                     size="24"
                     color="#ffffff" />
          </view> -->
          <!-- æ–‡ä»¶ä¿¡æ¯ -->
          <view class="file-info">
            <text class="file-name">{{ file.name }}</text>
            <!-- <text class="file-meta">{{ formatFileSize(file.fileSize) }} Â· {{ file.uploadTime || file.createTime }}</text> -->
          </view>
          <!-- æ“ä½œæŒ‰é’® -->
          <view class="file-actions">
            <!-- <u-button size="small"
                      type="primary"
                      plain
                      @click="previewFile(file)">预览</u-button> -->
            <u-button size="small"
                      type="info"
                      plain
                      @click="downloadFile(file)">下载并预览</u-button>
            <u-button size="small"
                      type="error"
                      plain
                      @click="confirmDelete(file, index)">删除</u-button>
          </view>
        </view>
      </view>
      <!-- ç©ºçŠ¶æ€ -->
      <view v-else
            class="empty-state">
        <up-icon name="document"
                 size="64"
                 color="#c0c4cc" />
        <text class="empty-text">暂无附件</text>
      </view>
    </view>
    <!-- <a rel="nofollow"
       id="downloadLink"
       href="#"
       style="display:none;">下载文本文件</a> -->
    <!-- ä¸Šä¼ æŒ‰é’® -->
    <view class="upload-button"
          @click="chooseFile">
      <up-icon name="plus"
               size="24"
               color="#ffffff" />
      <text class="upload-text">上传附件</text>
    </view>
  </view>
</template>
<script setup>
  import { ref, onMounted } from "vue";
  import PageHeader from "@/components/PageHeader.vue";
  import config from "@/config";
  import { getToken } from "@/utils/auth";
  // import { saveAs } from "file-saver";
  import {
    listRuleFiles,
    delRuleFile,
  } from "@/api/managementMeetings/rulesRegulationsManagement";
  import {
    productWorkOrderFileAdd,
    productWorkOrderFileDel,
    productWorkOrderFileListPage,
  } from "@/api/productionManagement/productWorkOrderFile";
  import { blobValidate } from "@/utils/ruoyi";
  // é™„件列表
  const fileList = ref([]);
  // è¿”回上一页
  const goBack = () => {
    uni.navigateBack();
  };
  // const request = axios.create({
  //   baseURL: "URL.com",
  //   adapter: axiosAdapterUniapp,
  // });
  // èŽ·å–æ–‡ä»¶å›¾æ ‡
  const getFileIcon = fileType => {
    const iconMap = {
      doc: "document",
      docx: "document",
      xls: "grid",
      xlsx: "grid",
      pdf: "document",
      ppt: "copy",
      pptx: "copy",
      txt: "document",
      jpg: "image",
      jpeg: "image",
      png: "image",
      gif: "image",
      zip: "folder",
      rar: "folder",
    };
    return iconMap[fileType.toLowerCase()] || "document";
  };
  // èŽ·å–æ–‡ä»¶å›¾æ ‡æ ·å¼ç±»
  const getFileIconClass = fileType => {
    const colorMap = {
      doc: "blue",
      docx: "blue",
      xls: "green",
      xlsx: "green",
      pdf: "red",
      ppt: "orange",
      pptx: "orange",
      txt: "gray",
      jpg: "purple",
      jpeg: "purple",
      png: "purple",
      gif: "purple",
      zip: "yellow",
      rar: "yellow",
    };
    return colorMap[fileType.toLowerCase()] || "gray";
  };
  // æ ¼å¼åŒ–文件大小
  const formatFileSize = bytes => {
    if (bytes === 0) return "0 B";
    const k = 1024;
    const sizes = ["B", "KB", "MB", "GB"];
    const i = Math.floor(Math.log(bytes) / Math.log(k));
    return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
  };
  // é€‰æ‹©æ–‡ä»¶
  const chooseFile = () => {
    uni.chooseImage({
      count: 9,
      sizeType: ["original", "compressed"],
      sourceType: ["album", "camera"],
      success: res => {
        console.log(res, "选择图片成功");
        uploadFiles(res.tempFiles);
      },
      fail: err => {
        console.error("选择图片失败:", err);
        showToast("选择文件失败");
      },
    });
    // uni.chooseFile({
    //   count: 9,
    //   extension: [
    //     ".doc",
    //     ".docx",
    //     ".xls",
    //     ".xlsx",
    //     ".pdf",
    //     ".ppt",
    //     ".pptx",
    //     ".txt",
    //     ".jpg",
    //     ".jpeg",
    //     ".png",
    //     ".gif",
    //     ".zip",
    //     ".rar",
    //   ],
    //   success: res => {
    //     console.log(res, "选择文件成功");
    //     uploadFiles(res.tempFiles);
    //   },
    //   fail: err => {
    //     showToast("选择文件失败");
    //   },
    // });
  };
  // ä¸Šä¼ æ–‡ä»¶
  const uploadFiles = tempFiles => {
    console.log(tempFiles, "上传文件1");
    tempFiles.forEach((tempFile, index) => {
      // æ˜¾ç¤ºä¸Šä¼ ä¸­æç¤º
      uni.showLoading({
        title: "上传中...",
        mask: true,
      });
      console.log(tempFile, "上传文件2");
      // 1. ç›´æŽ¥ä½¿ç”¨ uni.uploadFile ä¸Šä¼ æ–‡ä»¶
      uni.uploadFile({
        url: config.baseUrl + "/file/upload",
        filePath: tempFile.path,
        name: "file",
        header: {
          Authorization: "Bearer " + getToken(),
        },
        success: uploadRes => {
          uni.hideLoading();
          console.log(uploadRes, "上传文件3");
          try {
            const res = JSON.parse(uploadRes.data);
            console.log(res, "上传文件4");
            if (res.code === 200) {
              // 2. æå–文件信息
              const fileName = tempFile.name
                ? tempFile.name
                : tempFile.path.split("/").pop();
              // const fileType = fileName.split(".").pop();
              // 3. æž„造保存文件信息的参数
              const saveData = {
                name: fileName,
                workOrderId: rulesRegulationsManagementId.value,
                url: res.data.tempPath || "",
              };
              console.log(saveData, "保存文件信息参数");
              // 4. è°ƒç”¨ addRuleFile æŽ¥å£ä¿å­˜æ–‡ä»¶ä¿¡æ¯
              productWorkOrderFileAdd(saveData)
                .then(addRes => {
                  if (addRes.code === 200) {
                    // 5. æ·»åŠ åˆ°æ–‡ä»¶åˆ—è¡¨
                    const newFile = {
                      ...addRes.data,
                      uploadTime: new Date().toLocaleString(),
                    };
                    // fileList.value.push(newFile);
                    getFileList();
                    showToast("上传成功");
                  } else {
                    showToast("保存文件信息失败");
                  }
                })
                .catch(err => {
                  console.error("保存文件信息失败:", err);
                  showToast("保存文件信息失败");
                });
            } else {
              showToast("文件上传失败");
            }
          } catch (e) {
            console.error("解析上传结果失败:", e);
            showToast("上传失败");
          }
        },
        fail: err => {
          uni.hideLoading();
          console.error("上传失败:", err);
          showToast("上传失败");
        },
      });
    });
  };
  // ä¸‹è½½æ–‡ä»¶
  const downloadFile = file => {
    var url =
      config.baseUrl +
      "/common/download?fileName=" +
      encodeURIComponent(file.url) +
      "&delete=true";
    console.log(url, "url");
    uni
      .downloadFile({
        url: url,
        responseType: "blob",
        header: { Authorization: "Bearer " + getToken() },
      })
      .then(res => {
        let osType = uni.getStorageSync("deviceInfo").osName;
        let filePath = res.tempFilePath;
        if (osType === "ios") {
          uni.openDocument({
            filePath: filePath,
            showMenu: true,
            success: res => {
              resolve(res);
            },
            fail: err => {
              console.log("uni.openDocument--fail");
              reject(err);
            },
          });
        } else {
          uni.saveFile({
            tempFilePath: filePath,
            success: fileRes => {
              uni.showToast({
                icon: "none",
                mask: true,
                title:
                  "文件已保存:Android/data/uni.UNI720216F/apps/__UNI__720216F/" +
                  fileRes.savedFilePath, //保存路径
                duration: 3000,
              });
              setTimeout(() => {
                //打开文档查看
                uni.openDocument({
                  filePath: fileRes.savedFilePath,
                  success: function (res) {
                    resolve(fileRes);
                  },
                });
              }, 3000);
            },
            fail: err => {
              console.log("uni.save--fail");
              reject(err);
            },
          });
        }
        // const isBlob = blobValidate(res.data);
        // if (isBlob) {
        //   const blob = new Blob([res.data], { type: "text/plain" });
        //   const url = URL.createObjectURL(blob);
        //   const downloadLink = document.getElementById("downloadLink");
        //   downloadLink.href = url;
        //   downloadLink.download = file.name;
        //   downloadLink.click();
        //   showToast("下载成功");
        // } else {
        //   showToast("下载失败");
        // }
      })
      .catch(err => {
        console.error("下载失败:", err);
        showToast("下载失败");
      });
  };
  // ç¡®è®¤åˆ é™¤
  const confirmDelete = (file, index) => {
    uni.showModal({
      title: "删除确认",
      content: `确定要删除附件 "${file.name}" å—?`,
      success: res => {
        if (res.confirm) {
          deleteFile(file.id, index);
        }
      },
    });
  };
  // åˆ é™¤æ–‡ä»¶
  const deleteFile = (fileId, index) => {
    uni.showLoading({
      title: "删除中...",
      mask: true,
    });
    productWorkOrderFileDel([fileId])
      .then(res => {
        uni.hideLoading();
        if (res.code === 200) {
          // fileList.value.splice(index, 1);
          getFileList();
          showToast("删除成功");
        } else {
          showToast("删除失败");
        }
      })
      .catch(err => {
        uni.hideLoading();
        showToast("删除失败");
      });
  };
  // æ˜¾ç¤ºæç¤º
  const showToast = message => {
    uni.showToast({
      title: message,
      icon: "none",
    });
  };
  const rulesRegulationsManagementId = ref("");
  // é¡µé¢åŠ è½½æ—¶
  onMounted(() => {
    rulesRegulationsManagementId.value = uni.getStorageSync("workOrderFileId");
    // ä»Ž API èŽ·å–é™„ä»¶åˆ—è¡¨
    getFileList();
    // ä»Žæœ¬åœ°å­˜å‚¨èŽ·å– rulesRegulationsManagementId
  });
  // èŽ·å–é™„ä»¶åˆ—è¡¨
  const getFileList = () => {
    uni.showLoading({
      title: "加载中...",
      mask: true,
    });
    productWorkOrderFileListPage({
      workOrderId: rulesRegulationsManagementId.value,
      current: -1,
      size: -1,
    })
      .then(res => {
        uni.hideLoading();
        if (res.code === 200) {
          fileList.value = res.data.records || [];
        } else {
          showToast("获取附件列表失败");
        }
      })
      .catch(err => {
        uni.hideLoading();
        showToast("获取附件列表失败");
      });
  };
</script>
<style scoped lang="scss">
  @import "../../../styles/sales-common.scss";
  .file-list-page {
    min-height: 100vh;
    background: #f8f9fa;
    padding-bottom: 100rpx;
  }
  .file-list-container {
    padding: 20rpx;
  }
  .file-list {
    background: #ffffff;
    border-radius: 8rpx;
    overflow: hidden;
    box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
  }
  .file-item {
    display: flex;
    align-items: center;
    padding: 20rpx;
    border-bottom: 1rpx solid #f0f0f0;
    &:last-child {
      border-bottom: none;
    }
  }
  .file-icon {
    width: 56rpx;
    height: 56rpx;
    border-radius: 8rpx;
    display: flex;
    justify-content: center;
    align-items: center;
    margin-right: 20rpx;
    &.blue {
      background: #409eff;
    }
    &.green {
      background: #67c23a;
    }
    &.red {
      background: #f56c6c;
    }
    &.orange {
      background: #e6a23c;
    }
    &.gray {
      background: #909399;
    }
    &.purple {
      background: #909399;
    }
    &.yellow {
      background: #e6a23c;
    }
  }
  .file-info {
    flex: 1;
    min-width: 0;
  }
  .file-name {
    display: block;
    font-size: 16px;
    color: #303133;
    margin-bottom: 8rpx;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .file-meta {
    display: block;
    font-size: 12px;
    color: #909399;
  }
  .file-actions {
    display: flex;
    gap: 12rpx;
  }
  .empty-state {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    padding: 100rpx 0;
    background: #ffffff;
    border-radius: 8rpx;
    box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
  }
  .empty-text {
    font-size: 14px;
    color: #909399;
    margin-top: 20rpx;
  }
  .upload-button {
    position: fixed;
    bottom: 40rpx;
    right: 40rpx;
    width: 130rpx;
    height: 130rpx;
    border-radius: 50%;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    box-shadow: 0 4rpx 16rpx rgba(102, 126, 234, 0.4);
    z-index: 1000;
  }
  .upload-text {
    font-size: 10px;
    color: #ffffff;
    margin-top: 4rpx;
  }
  .upload-progress {
    padding: 40rpx 0;
  }
  .upload-progress-text {
    display: block;
    text-align: center;
    margin-top: 20rpx;
    font-size: 14px;
    color: #606266;
  }
</style>
src/pages/productionManagement/workOrder/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,784 @@
<template>
  <view class="work-order-page">
    <!-- é¡µé¢å¤´éƒ¨ -->
    <PageHeader title="生产报工" />
    <!-- æœç´¢åŒºåŸŸ -->
    <view class="search-section">
      <view class="search-bar">
        <view class="search-input">
          <up-input class="search-text"
                    placeholder="请输入工单编号"
                    v-model="searchForm.workOrderNo"
                    @change="handleQuery"
                    clearable />
        </view>
        <view class="filter-button"
              @click="handleQuery">
          <u-icon name="search"
                  size="24"
                  color="#999"></u-icon>
        </view>
      </view>
    </view>
    <!-- å·¥å•列表 -->
    <view v-if="tableData.length > 0"
          class="work-order-list">
      <view v-for="(item, index) in tableData"
            :key="index"
            class="work-order-item">
        <view class="item-header">
          <view class="item-title">
            <text class="work-order-no">{{ item.workOrderNo || '无编号' }}</text>
            <up-tag :type="getWorkOrderTypeTag(item.workOrderType)"
                    size="small"
                    class="type-tag">
              {{ item.workOrderType || '未知' }}
            </up-tag>
          </view>
          <up-tag :type="getCompletionStatusTag(item.completionStatus)"
                  size="small">
            {{ getCompletionStatusText(item.completionStatus) }}
          </up-tag>
        </view>
        <view class="item-content">
          <view class="content-row">
            <text class="label">产品名称:</text>
            <text class="value">{{ item.productName || '-' }}</text>
          </view>
          <view class="content-row">
            <text class="label">规格:</text>
            <text class="value">{{ item.model || '-' }}</text>
          </view>
          <view class="content-row">
            <text class="label">单位:</text>
            <text class="value">{{ item.unit || '-' }}</text>
          </view>
          <view class="content-row">
            <text class="label">工序名称:</text>
            <text class="value">{{ item.processName || '-' }}</text>
          </view>
          <view class="content-row">
            <text class="label">需求数量:</text>
            <text class="value">{{ item.planQuantity || 0 }}</text>
          </view>
          <view class="content-row">
            <text class="label">完成数量:</text>
            <text class="value">{{ item.completeQuantity || 0 }}</text>
          </view>
          <view class="content-row">
            <text class="label">完成进度:</text>
            <view class="progress-container">
              <!-- <up-progress :percentage="toProgressPercentage(item.completionStatus)"
                           :color="progressColor(toProgressPercentage(item.completionStatus))"
                           :active-color="progressColor(toProgressPercentage(item.completionStatus))"
                           :height="6"
                           :show-percentage="false"
                           class="progress-bar" /> -->
              <text class="progress-text">{{ toProgressPercentage(item.completionStatus) }}%</text>
            </view>
          </view>
          <view class="content-row">
            <text class="label">计划时间:</text>
            <text class="value">{{ formatDate(item.planStartTime) }} è‡³ {{ formatDate(item.planEndTime) }}</text>
          </view>
          <view v-if="item.actualStartTime"
                class="content-row">
            <text class="label">实际时间:</text>
            <text class="value">{{ formatDate(item.actualStartTime) }} è‡³ {{ formatDate(item.actualEndTime) }}</text>
          </view>
        </view>
        <view class="item-footer">
          <u-button size="small"
                    @click="handleEdit(item)">
            ç¼–辑
          </u-button>
          <u-button size="small"
                    @click="viewFileList(item)">
            é™„ä»¶
          </u-button>
          <u-button type="primary"
                    size="small"
                    :disabled="item.planQuantity <= 0"
                    @click="showReportDialog(item)">
            æŠ¥å·¥
          </u-button>
        </view>
      </view>
    </view>
    <!-- ç©ºçŠ¶æ€ -->
    <view v-else
          class="no-data">
      <up-empty mode="data"
                text="暂无工单数据" />
    </view>
    <!-- åˆ†é¡µç»„ä»¶ -->
    <!-- ç¼–辑时间弹窗 -->
    <up-popup v-model:show="editDialogVisible"
              mode="center"
              round>
      <view class="dialog-content">
        <view class="dialog-header">
          <text class="dialog-title">编辑时间</text>
          <up-icon name="close"
                   size="20"
                   color="#999"
                   @click="editDialogVisible = false" />
        </view>
        <view class="dialog-body">
          <view class="form-item">
            <text class="form-label">计划开始时间</text>
            <view class="fake-input-wrapper"
                  @click="showDatePicker('planStartTime',editrow.planStartTime)">
              <text class="fake-input-text"
                    :class="{ 'placeholder': !editrow.planStartTime }">
                {{ editrow.planStartTime || '请选择计划开始时间' }}
              </text>
              <up-icon name="calendar"
                       size="20"
                       color="#999" />
            </view>
          </view>
          <view class="form-item">
            <text class="form-label">计划结束时间</text>
            <view class="fake-input-wrapper"
                  @click="showDatePicker('planEndTime',editrow.planEndTime)">
              <text class="fake-input-text"
                    :class="{ 'placeholder': !editrow.planEndTime }">
                {{ editrow.planEndTime || '请选择计划结束时间' }}
              </text>
              <up-icon name="calendar"
                       size="20"
                       color="#999" />
            </view>
          </view>
          <view class="form-item">
            <text class="form-label">实际开始时间</text>
            <view class="fake-input-wrapper"
                  @click="showDatePicker('actualStartTime',editrow.actualStartTime)">
              <text class="fake-input-text"
                    :class="{ 'placeholder': !editrow.actualStartTime }">
                {{ editrow.actualStartTime || '请选择实际开始时间' }}
              </text>
              <up-icon name="calendar"
                       size="20"
                       color="#999" />
            </view>
          </view>
          <view class="form-item">
            <text class="form-label">实际结束时间</text>
            <view class="fake-input-wrapper"
                  @click="showDatePicker('actualEndTime',editrow.actualEndTime)">
              <text class="fake-input-text"
                    :class="{ 'placeholder': !editrow.actualEndTime }">
                {{ editrow.actualEndTime || '请选择实际结束时间' }}
              </text>
              <up-icon name="calendar"
                       size="20"
                       color="#999" />
            </view>
          </view>
        </view>
        <view class="dialog-footer">
          <u-button type="default"
                    class="footer-btn"
                    @click="editDialogVisible = false">
            å–消
          </u-button>
          <u-button type="primary"
                    class="footer-btn"
                    @click="handleUpdate">
            ç¡®å®š
          </u-button>
        </view>
      </view>
    </up-popup>
    <!-- æŠ¥å·¥å¼¹çª— -->
    <up-popup v-model:show="reportDialogVisible"
              mode="center"
              round
              style="width: 500px">
      <view class="dialog-content">
        <view class="dialog-header">
          <text class="dialog-title">报工</text>
          <up-icon name="close"
                   size="20"
                   color="#999"
                   @click="reportDialogVisible = false" />
        </view>
        <view class="dialog-body">
          <view class="form-item">
            <text class="form-label">待生产数量</text>
            <up-input v-model="reportForm.planQuantity"
                      disabled
                      readonly />
          </view>
          <view class="form-item">
            <text class="form-label required">本次生产数量</text>
            <up-input v-model.number="reportForm.quantity"
                      type="number"
                      placeholder="请输入本次生产数量" />
          </view>
          <view class="form-item">
            <text class="form-label">报废数量</text>
            <up-input v-model.number="reportForm.scrapQty"
                      type="number"
                      placeholder="请输入报废数量" />
          </view>
          <view class="form-item">
            <text class="form-label">班组信息</text>
            <view class="fake-input-wrapper"
                  @click="showUserSheet = true">
              <text class="fake-input-text"
                    :class="{ 'placeholder': !reportForm.userName }">
                {{ reportForm.userName || '请选择班组信息' }}
              </text>
              <up-icon name="arrow-right"
                       size="20"
                       color="#999" />
            </view>
          </view>
        </view>
        <view class="dialog-footer">
          <u-button type="default"
                    class="footer-btn"
                    @click="reportDialogVisible = false">
            å–消
          </u-button>
          <u-button type="primary"
                    class="footer-btn"
                    @click="handleReport">
            ç¡®å®š
          </u-button>
        </view>
      </view>
    </up-popup>
    <!-- æ—¥æœŸé€‰æ‹©å™¨ -->
    <!-- <up-popup v-model:show="showDate"
              mode="date"
              :start-year="2020"
              :end-year="2030"
              @close="showDate = false"
              @confirm="confirmDate" /> -->
    <u-datetime-picker :show="showDate"
                       v-model="value1"
                       @close="showDate = false"
                       @confirm="confirmDate"
                       @cancel="showDate = false"
                       mode="date"
                       format="YYYY-MM-DD"></u-datetime-picker>
    <!-- ç­ç»„选择 -->
    <up-action-sheet :show="showUserSheet"
                     :actions="userSheetOptions"
                     @select="selectUser"
                     @close="showUserSheet = false"
                     title="选择班组信息" />
  </view>
</template>
<script setup>
  import { ref, reactive, onMounted, computed } from "vue";
  import { onShow } from "@dcloudio/uni-app";
  import PageHeader from "@/components/PageHeader.vue";
  import dayjs from "dayjs";
  import {
    productWorkOrderPage,
    updateProductWorkOrder,
    addProductMain,
  } from "@/api/productionManagement/workOrder.js";
  import { getUserProfile, userListNoPageByTenantId } from "@/api/system/user.js";
  // æ˜¾ç¤ºæç¤ºä¿¡æ¯
  const showToast = message => {
    uni.showToast({
      title: message,
      icon: "none",
    });
  };
  // æœç´¢è¡¨å•
  const searchForm = ref({
    workOrderNo: "",
  });
  // å·¥å•列表数据
  const tableData = ref([]);
  const tableLoading = ref(false);
  const value1 = ref(new Date());
  // åˆ†é¡µæ•°æ®
  const page = reactive({
    current: -1,
    size: -1,
    total: 0,
  });
  // ç¼–辑弹窗
  const editDialogVisible = ref(false);
  const editrow = ref(null);
  // æŠ¥å·¥å¼¹çª—
  const reportDialogVisible = ref(false);
  const reportForm = reactive({
    planQuantity: 0,
    quantity: 0,
    userName: "",
    workOrderId: "",
    reportWork: "",
    productProcessRouteItemId: "",
    userId: "",
    productMainId: null,
    scrapQty: 0,
  });
  const currentReportRowData = ref(null);
  // æ—¥æœŸé€‰æ‹©å™¨
  const showDate = ref(false);
  const currentDateField = ref("");
  // ç­ç»„选择
  const showUserSheet = ref(false);
  const userOptions = ref([]);
  const userSheetOptions = computed(() => {
    return userOptions.value.map(user => ({
      name: user.nickName,
      value: user.userId,
    }));
  });
  // æ ¼å¼åŒ–日期
  const formatDate = dateStr => {
    if (!dateStr) return "-";
    return dayjs(dateStr).format("YYYY-MM-DD");
  };
  // è¿›åº¦ç™¾åˆ†æ¯”转换
  const toProgressPercentage = val => {
    const n = Number(val);
    if (!Number.isFinite(n)) return 0;
    if (n <= 0) return 0;
    if (n >= 100) return 100;
    return Math.round(n);
  };
  // è¿›åº¦æ¡é¢œè‰²
  const progressColor = percentage => {
    const p = toProgressPercentage(percentage);
    if (p < 30) return "#f56c6c";
    if (p < 50) return "#e6a23c";
    if (p < 80) return "#409eff";
    return "#67c23a";
  };
  // èŽ·å–å·¥å•ç±»åž‹æ ‡ç­¾
  const getWorkOrderTypeTag = type => {
    switch (type) {
      case "生产工单":
        return "success";
      case "维修工单":
        return "warning";
      case "检验工单":
        return "info";
      default:
        return "info";
    }
  };
  // èŽ·å–å®ŒæˆçŠ¶æ€æ ‡ç­¾
  const getCompletionStatusTag = status => {
    const percentage = toProgressPercentage(status);
    if (percentage >= 100) return "success";
    if (percentage >= 50) return "warning";
    return "error";
  };
  // èŽ·å–å®ŒæˆçŠ¶æ€æ–‡æœ¬
  const getCompletionStatusText = status => {
    const percentage = toProgressPercentage(status);
    if (percentage >= 100) return "已完成";
    return `${percentage}%`;
  };
  // æŸ¥è¯¢åˆ—表
  const handleQuery = () => {
    page.current = -1;
    getList();
  };
  const getList = () => {
    tableLoading.value = true;
    const params = { ...searchForm.value, ...page };
    productWorkOrderPage(params)
      .then(res => {
        tableLoading.value = false;
        tableData.value = res.data.records || [];
        // tableData.value = [
        //   {
        //     id: "WO20260304001",
        //     workOrderNo: "WO20260304001",
        //     workOrderType: "生产工单",
        //     productName: "不锈钢板材",
        //     model: "304-2B",
        //     unit: "kg",
        //     processName: "切割工序",
        //     planQuantity: 1000,
        //     completeQuantity: 650,
        //     completionStatus: 65,
        //     planStartTime: "2026-03-01",
        //     planEndTime: "2026-03-10",
        //     actualStartTime: "2026-03-02",
        //     actualEndTime: null,
        //     remark: "紧急订单,请优先处理",
        //   },
        // ];
        page.total = res.data.total || 0;
      })
      .catch(() => {
        tableLoading.value = false;
        showToast("获取工单列表失败");
      });
  };
  // ç¼–辑工单
  const handleEdit = row => {
    editrow.value = JSON.parse(JSON.stringify(row));
    editDialogVisible.value = true;
  };
  // æ›´æ–°å·¥å•
  const handleUpdate = () => {
    updateProductWorkOrder(editrow.value)
      .then(res => {
        showToast("修改成功");
        editDialogVisible.value = false;
        getList();
      })
      .catch(() => {
        showToast("修改失败");
      });
  };
  // æ˜¾ç¤ºæ—¥æœŸé€‰æ‹©å™¨
  const showDatePicker = (field, defaultValue) => {
    currentDateField.value = field;
    // è®¾ç½®é»˜è®¤å€¼
    if (defaultValue) {
      value1.value = dayjs(defaultValue);
    } else {
      value1.value = dayjs();
    }
    showDate.value = true;
  };
  // ç¡®è®¤æ—¥æœŸé€‰æ‹©
  const confirmDate = e => {
    if (currentDateField.value && editrow.value) {
      // ç¡®ä¿æ—¥æœŸæ ¼å¼ä¸º YYYY-MM-DD
      const formattedDate = dayjs(e.value).format("YYYY-MM-DD");
      editrow.value[currentDateField.value] = formattedDate;
    }
    showDate.value = false;
  };
  // æ˜¾ç¤ºæŠ¥å·¥å¼¹çª—
  const showReportDialog = row => {
    currentReportRowData.value = row;
    reportForm.planQuantity = row.planQuantity || 0;
    reportForm.quantity = row.quantity || 0;
    reportForm.productProcessRouteItemId = row.productProcessRouteItemId;
    reportForm.workOrderId = row.id;
    reportForm.reportWork = row.reportWork;
    reportForm.productMainId = row.productMainId;
    reportForm.scrapQty = row.scrapQty || 0;
    // èŽ·å–å½“å‰ç™»å½•ç”¨æˆ·ä¿¡æ¯ï¼Œè®¾ç½®ä¸ºé»˜è®¤é€‰ä¸­
    getUserProfile()
      .then(res => {
        if (res.code === 200) {
          reportForm.userId = res.data.userId;
          reportForm.userName = res.data.nickName;
        }
      })
      .catch(err => {
        console.error("获取用户信息失败", err);
      });
    reportDialogVisible.value = true;
  };
  // å¤„理报工
  const handleReport = () => {
    if (reportForm.planQuantity <= 0) {
      showToast("待生产数量为0,无法报工");
      return;
    }
    if (!reportForm.quantity || reportForm.quantity <= 0) {
      showToast("请输入有效的本次生产数量");
      return;
    }
    if (reportForm.quantity > reportForm.planQuantity) {
      showToast("本次生产数量不能超过待生产数量");
      return;
    }
    addProductMain(reportForm)
      .then(res => {
        if (res.code === 200) {
          showToast("报工成功");
          reportDialogVisible.value = false;
          getList();
        } else {
          showToast(res.msg || "报工失败");
        }
      })
      .catch(() => {
        showToast("报工失败");
      });
  };
  const viewFileList = item => {
    uni.setStorageSync("workOrderFileId", item.id);
    uni.navigateTo({
      url: "/pages/productionManagement/workOrder/fileList",
    });
  };
  // èŽ·å–ç”¨æˆ·åˆ—è¡¨
  const getUserList = () => {
    userListNoPageByTenantId()
      .then(res => {
        if (res.code === 200) {
          userOptions.value = res.data || [];
        }
      })
      .catch(err => {
        console.error("获取用户列表失败", err);
      });
  };
  // é€‰æ‹©ç”¨æˆ·
  const selectUser = e => {
    reportForm.userId = e.value;
    const selectedUser = userOptions.value.find(user => user.userId === e.value);
    if (selectedUser) {
      reportForm.userName = selectedUser.nickName;
    }
    showUserSheet.value = false;
  };
  onMounted(() => {
    getList();
    getUserList();
  });
  onShow(() => {
    getList();
  });
</script>
<style scoped lang="scss">
  @import "../../../styles/sales-common.scss";
  .work-order-page {
    min-height: 100vh;
    background-color: #f5f5f5;
  }
  // æœç´¢åŒºåŸŸ
  .search-container {
    padding: 16px;
    background-color: #ffffff;
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
  }
  .search-box {
    display: flex;
    gap: 12px;
    align-items: center;
  }
  .search-box :deep(.up-input) {
    flex: 1;
  }
  // å·¥å•列表
  .work-order-list {
    padding: 16px;
  }
  .work-order-item {
    background: #ffffff;
    border-radius: 12px;
    margin-bottom: 16px;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
    overflow: hidden;
  }
  .item-header {
    padding: 16px;
    border-bottom: 1px solid #f0f0f0;
    display: flex;
    justify-content: space-between;
    align-items: center;
  }
  .item-title {
    display: flex;
    align-items: center;
    gap: 8px;
  }
  .work-order-no {
    font-size: 16px;
    font-weight: 600;
    color: #303133;
  }
  .type-tag {
    margin-left: 8px;
  }
  .item-content {
    padding: 16px;
  }
  .content-row {
    display: flex;
    margin-bottom: 12px;
    align-items: flex-start;
  }
  .content-row:last-child {
    margin-bottom: 0;
  }
  .label {
    width: 90px;
    font-size: 14px;
    color: #606266;
  }
  .value {
    flex: 1;
    font-size: 14px;
    color: #303133;
  }
  .progress-container {
    flex: 1;
    display: flex;
    align-items: center;
    gap: 8px;
  }
  .progress-bar {
    flex: 1;
  }
  .progress-text {
    font-size: 12px;
    color: #606266;
    min-width: 40px;
  }
  .item-footer {
    padding: 16px;
    border-top: 1px solid #f0f0f0;
    display: flex;
    gap: 12px;
    justify-content: flex-end;
  }
  // ç©ºçŠ¶æ€
  .no-data {
    padding: 60px 20px;
    text-align: center;
  }
  // åˆ†é¡µç»„ä»¶
  .pagination {
    padding: 20px;
    background: #fff;
    margin-top: 10px;
    display: flex;
    justify-content: center;
  }
  // å¼¹çª—样式
  .dialog-content {
    padding: 24px;
    background: #ffffff;
    border-radius: 12px;
    width: 90vw;
  }
  .dialog-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 24px;
    padding-bottom: 16px;
    border-bottom: 1px solid #f0f0f0;
  }
  .dialog-title {
    font-size: 18px;
    font-weight: 600;
    color: #303133;
  }
  .dialog-body {
    margin-bottom: 24px;
  }
  .form-item {
    margin-bottom: 20px;
  }
  .form-label {
    display: block;
    font-size: 14px;
    color: #606266;
    margin-bottom: 8px;
  }
  .form-label.required::before {
    content: "*";
    color: #f56c6c;
    margin-right: 4px;
  }
  .dialog-body :deep(.up-input) {
    width: 100%;
  }
  .fake-input-wrapper {
    display: flex;
    align-items: center;
    justify-content: space-between;
    height: 44px;
    padding: 0 12px;
    // background-color: #f5f7fa;
    border: 1px solid #dcdfe6;
    border-radius: 4px;
  }
  .fake-input-text {
    font-size: 14px;
    color: #303133;
  }
  .fake-input-text.placeholder {
    color: #c0c4cc;
  }
  .dialog-footer {
    display: flex;
    gap: 16px;
    padding-top: 16px;
    border-top: 1px solid #f0f0f0;
  }
  .footer-btn {
    flex: 1;
    height: 44px;
  }
</style>