gaoluyang
2026-05-20 de4ac959d99138074276563d6d4ca44d76b17705
Merge branch 'dev_NEW_pro' into dev_天津_宝东

# Conflicts:
# src/config.js
# src/manifest.json
# src/pages/sales/salesQuotation/detail.vue
# src/pages/sales/salesQuotation/edit.vue
# src/pages/sales/salesQuotation/index.vue
# src/pages/works.vue
已添加54个文件
已修改42个文件
已删除2个文件
21439 ■■■■ 文件已修改
src/api/basicData/parameterMaintenance.js 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/basicData/storageAttachment.js 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/collaborativeApproval/approvalProcess.js 89 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/fileManagement/bookshelf.js 129 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/fileManagement/borrow.js 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/fileManagement/document.js 189 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/fileManagement/return.js 61 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/fileManagement/statistics.js 75 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/inventoryManagement/stockInventory.js 101 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/procurementManagement/procurementLedger.js 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/productionManagement/bom.js 82 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/productionManagement/processManagement.js 87 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/productionManagement/processRoute.js 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/productionManagement/productProcessRoute.js 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/productionManagement/productionCosting.js 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/productionManagement/productionOrder.js 78 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/productionManagement/productionPlan.js 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/productionManagement/productionProductMain.js 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/productionManagement/productionReporting.js 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/productionManagement/workOrder.js 23 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/CommonUpload.vue 164 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages.json 177 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/cooperativeOffice/collaborativeApproval/approve.vue 812 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/cooperativeOffice/collaborativeApproval/detail.vue 1113 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/cooperativeOffice/collaborativeApproval/index.vue 28 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/equipmentManagement/repair/add.vue 57 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/equipmentManagement/repair/index.vue 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/equipmentManagement/upkeep/add.vue 767 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/equipmentManagement/upkeep/fileList.vue 133 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/equipmentManagement/upkeep/index.vue 17 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/equipmentManagement/upkeep/maintain.vue 97 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/fileManagement/borrow/edit.vue 333 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/fileManagement/borrow/index.vue 262 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/fileManagement/return/edit.vue 313 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/fileManagement/return/index.vue 262 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/index.vue 448 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/inspectionUpload/attachment.vue 460 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/inspectionUpload/components/formDia.vue 228 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/inspectionUpload/index.vue 1478 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/inspectionUpload/upload.vue 982 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/inventoryManagement/stockManagement/Qualified.vue 151 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/inventoryManagement/stockManagement/Record.vue 292 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/inventoryManagement/stockManagement/Unqualified.vue 134 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/inventoryManagement/stockManagement/index.vue 133 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/procurementManagement/procurementLedger/detail.vue 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/productionDesign/basicParameters/edit.vue 290 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/productionDesign/basicParameters/index.vue 245 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/productionDesign/bom/BomStructureItem.vue 256 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/productionDesign/bom/index.vue 179 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/productionDesign/bom/structure.vue 100 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/productionDesign/processManagement/edit.vue 236 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/productionDesign/processManagement/index.vue 261 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/productionDesign/processManagement/params.vue 413 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/productionManagement/mainProductionPlan/detail.vue 252 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/productionManagement/mainProductionPlan/index.vue 300 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/productionManagement/processRoute/index.vue 287 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/productionManagement/processRoute/items.vue 554 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/productionManagement/processStatistics/index.vue 370 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/productionManagement/productionAccounting/index.vue 506 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/productionManagement/productionDispatching/components/DispatchModal.vue 708 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/productionManagement/productionDispatching/components/formDia.vue 265 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/productionManagement/productionDispatching/index.vue 421 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/productionManagement/productionOrder/index.vue 697 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/productionManagement/productionOrder/pickingDetail.vue 350 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/productionManagement/productionOrder/source.vue 166 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/productionManagement/productionReport/index.vue 375 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/productionManagement/productionReporting/ledger.vue 424 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/productionManagement/productionScheduling/index.vue 241 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/productionManagement/productionTraceability/index.vue 1032 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/qualityManagement/finalInspection/add.vue 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/qualityManagement/finalInspection/detail.vue 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/qualityManagement/finalInspection/index.vue 19 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/qualityManagement/materialInspection/add.vue 30 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/qualityManagement/materialInspection/detail.vue 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/qualityManagement/materialInspection/index.vue 19 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/qualityManagement/processInspection/add.vue 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/qualityManagement/processInspection/detail.vue 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/qualityManagement/processInspection/index.vue 19 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/sales/salesAccount/goOut.vue 976 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/sales/salesQuotation/detail.vue 186 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/sales/salesQuotation/edit.vue 636 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/sales/salesQuotation/index.vue 236 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/works.vue 243 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/static/images/icon/baogongtaizhang.svg 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/static/images/icon/bom.svg 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/static/images/icon/gongxuguanli.svg 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/static/images/icon/gongyiluxian.svg 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/static/images/icon/guihuandengji.svg 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/static/images/icon/jichucanshu.svg 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/static/images/icon/jieyuedengji.svg 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/static/images/icon/kucunguanli.svg 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/static/images/icon/shengchandingdan.svg 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/static/images/icon/shengchanhesuan.svg 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/static/images/icon/shengchanjihua.svg 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/static/images/icon/shengchanpaichan.svg 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/static/images/icon/shengchanshikuang.svg 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/static/images/icon/shengchanzhuisu.svg 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/versionUpgrade.js 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/basicData/parameterMaintenance.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,36 @@
import request from "@/utils/request";
// æŸ¥è¯¢åŸºç¡€å‚数列表
export function getBaseParamList(query) {
  return request({
    url: "/technologyParam/list",
    method: "get",
    params: query,
  });
}
// æ–°å¢žåŸºç¡€å‚æ•°
export function addBaseParam(data) {
  return request({
    url: "/technologyParam/add",
    method: "post",
    data: data,
  });
}
// ç¼–辑基础参数
export function editBaseParam(data) {
  return request({
    url: "/technologyParam/edit",
    method: "put",
    data: data,
  });
}
// åˆ é™¤åŸºç¡€å‚æ•°
export function removeBaseParam(id) {
  return request({
    url: "/technologyParam/remove/" + id,
    method: "delete",
  });
}
src/api/basicData/storageAttachment.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,29 @@
// é™„件页面接口
import request from '@/utils/request'
// é™„件查询
export function attachmentList(query) {
    return request({
        url: '/storageAttachment/list',
        method: 'get',
        params: query
    })
}
// é™„件新增
export function createAttachment(data) {
    return request({
        url: '/storageAttachment/add',
        method: 'post',
        data
    })
}
// é™„件删除
export function deleteAttachment(data) {
    return request({
        url: '/storageAttachment/delete',
        method: 'delete',
        data
    })
}
src/api/collaborativeApproval/approvalProcess.js
@@ -2,63 +2,72 @@
import request from "@/utils/request";
export function approveProcessListPage(query) {
    return request({
        url: '/approveProcess/list',
        method: 'get',
        params: query,
    })
  return request({
    url: "/approveProcess/list",
    method: "get",
    params: query,
  });
}
export function getDept(query) {
    return request({
        url: '/approveProcess/getDept',
        method: 'get',
        params: query,
    })
  return request({
    url: "/approveProcess/getDept",
    method: "get",
    params: query,
  });
}
export function approveProcessGetInfo(query) {
    return request({
        url: '/approveProcess/get',
        method: 'get',
        params: query,
    })
  return request({
    url: "/approveProcess/get",
    method: "get",
    params: query,
  });
}
// æ–°å¢žå®¡æ‰¹æµç¨‹
export function approveProcessAdd(query) {
    return request({
        url: '/approveProcess/add',
        method: 'post',
        data: query,
    })
  return request({
    url: "/approveProcess/add",
    method: "post",
    data: query,
  });
}
// ä¿®æ”¹å®¡æ‰¹æµç¨‹
export function approveProcessUpdate(query) {
    return request({
        url: '/approveProcess/update',
        method: 'post',
        data: query,
    })
  return request({
    url: "/approveProcess/update",
    method: "post",
    data: query,
  });
}
// æäº¤å®¡æ‰¹
export function updateApproveNode(query) {
    return request({
        url: '/approveNode/updateApproveNode',
        method: 'post',
        data: query,
    })
  return request({
    url: "/approveNode/updateApproveNode",
    method: "post",
    data: query,
  });
}
// åˆ é™¤å®¡æ‰¹æµç¨‹
export function approveProcessDelete(query) {
    return request({
        url: '/approveProcess/deleteIds',
        method: 'delete',
        data: query,
    })
  return request({
    url: "/approveProcess/deleteIds",
    method: "delete",
    data: query,
  });
}
// æŸ¥è¯¢å®¡æ‰¹æµç¨‹
export function approveProcessDetails(query) {
    return request({
        url: '/approveNode/details/' + query,
        method: 'get',
    })
}
  return request({
    url: "/approveNode/details/" + query,
    method: "get",
  });
}
// å®¡æ‰¹è¯¦æƒ…
export function getDeliveryDetailByShippingNo(query) {
  return request({
    url: "/shippingInfo/getDateilByShippingNo",
    method: "get",
    params: query,
  });
}
src/api/fileManagement/bookshelf.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,129 @@
import request from "@/utils/request";
/**
 * ä¹¦æž¶ç®¡ç†ç›¸å…³API接口
 * åŒ…含仓库管理、货架管理、图书管理等功能的接口
 */
/**
 * èŽ·å–ä»“åº“åˆ—è¡¨
 * @description èŽ·å–æ‰€æœ‰ä»“åº“çš„åŸºæœ¬ä¿¡æ¯åˆ—è¡¨
 * @returns {Promise} è¿”回仓库列表数据
 */
export function getWarehouseList() {
  return request({
    url: "/warehouse/tree",
    method: "get",
  });
}
/**
 * æ–°å¢žä»“库
 * @description åˆ›å»ºæ–°çš„仓库记录
 * @param {Object} data ä»“库信息对象,包含仓库名称等字段
 * @returns {Promise} è¿”回新增结果
 */
export function addWarehouse(data) {
  return request({
    url: "/warehouse/add",
    method: "post",
    data,
  });
}
/**
 * æ›´æ–°ä»“库信息
 * @description ä¿®æ”¹çŽ°æœ‰ä»“åº“çš„åŸºæœ¬ä¿¡æ¯
 * @param {Object} data ä»“库信息对象,必须包含仓库ID
 * @returns {Promise} è¿”回更新结果
 */
export function updateWarehouse(data) {
  return request({
    url: "/warehouse/update",
    method: "put",
    data,
  });
}
/**
 * åˆ é™¤ä»“库
 * @description æ ¹æ®ä»“库ID删除指定的仓库记录
 * @param {string|number} id ä»“库ID
 * @returns {Promise} è¿”回删除结果
 */
export function deleteWarehouse(data) {
  return request({
    url: `/warehouse/delete/`,
    method: "delete",
    data,
  });
}
/**
 * èŽ·å–è´§æž¶åˆ—è¡¨
 * @description æ ¹æ®ä»“库ID获取该仓库下的所有货架信息
 * @param {string|number} warehouseId ä»“库ID
 * @returns {Promise} è¿”回货架列表数据
 */
export function getShelfList(warehouseId) {
  return request({
    url: `/shelf/list/${warehouseId}`,
    method: "get",
  });
}
/**
 * æ–°å¢žè´§æž¶
 * @description åœ¨æŒ‡å®šä»“库下创建新的货架记录
 * @param {Object} data è´§æž¶ä¿¡æ¯å¯¹è±¡ï¼ŒåŒ…含货架名称、层数、列数等字段
 * @returns {Promise} è¿”回新增结果
 */
export function addShelf(data) {
  return request({
    url: "/warehouse/goodsShelves/add",
    method: "post",
    data,
  });
}
/**
 * æ›´æ–°è´§æž¶ä¿¡æ¯
 * @description ä¿®æ”¹çŽ°æœ‰è´§æž¶çš„åŸºæœ¬ä¿¡æ¯
 * @param {Object} data è´§æž¶ä¿¡æ¯å¯¹è±¡ï¼Œå¿…须包含货架ID
 * @returns {Promise} è¿”回更新结果
 */
export function updateShelf(data) {
  return request({
    url: "/warehouse/goodsShelves/update",
    method: "put",
    data,
  });
}
/**
 * åˆ é™¤è´§æž¶
 * @description æ ¹æ®è´§æž¶ID删除指定的货架记录,后端要求传入 ID æ•°ç»„(支持批量)
 * @param {Array<string|number>} data è´§æž¶ID数组
 * @returns {Promise} è¿”回删除结果
 */
export function deleteShelf(data) {
  return request({
    url: `/warehouse/goodsShelves/delete/`,
    method: "delete",
    data,
  });
}
/**
 * èŽ·å–ä»“åº“ç»“æž„
 * @description èŽ·å–æŒ‡å®šä»“åº“çš„å®Œæ•´ç»“æž„ä¿¡æ¯ï¼ŒåŒ…æ‹¬è´§æž¶ã€å±‚æ•°ã€åˆ—æ•°ç­‰
 * @param {string|number} warehouseId ä»“库ID
 * @returns {Promise} è¿”回仓库的完整结构数据
 */
export function getWarehouseStructure(data) {
  return request({
    url: `/warehouse/goodsShelvesRowcol/list`,
    method: "get",
    params: data,
  });
}
src/api/fileManagement/borrow.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,47 @@
import request from "@/utils/request";
// æ–‡æ¡£å€Ÿé˜…管理相关接口
// èŽ·å–æ–‡æ¡£åˆ—è¡¨ï¼ˆç”¨äºŽå€Ÿé˜…ä¹¦ç±é€‰æ‹©ï¼‰
export function getDocumentList() {
  return request({
    url: "/documentation/list",
    method: "get",
  });
}
// å€Ÿé˜…分页查询
export function getBorrowList(params) {
  return request({
    url: "/documentationBorrowManagement/listPage",
    method: "get",
    params: params,
  });
}
// æ–°å¢žå€Ÿé˜…
export function addBorrow(data) {
  return request({
    url: "/documentationBorrowManagement/add",
    method: "post",
    data: data,
  });
}
// æ›´æ–°å€Ÿé˜…
export function updateBorrow(data) {
  return request({
    url: "/documentationBorrowManagement/update",
    method: "put",
    data: data,
  });
}
// åˆ é™¤å€Ÿé˜…
export function deleteBorrow(ids) {
  return request({
    url: "/documentationBorrowManagement/delete",
    method: "delete",
    data: ids,
  });
}
src/api/fileManagement/document.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,189 @@
import request from "@/utils/request";
// èŽ·å–åˆ†ç±»æ ‘
export function getCategoryTree() {
  return request({
    url: "/warehouse/documentClassification/getList",
    method: "get",
  });
}
// æ–°å¢žåˆ†ç±»
export function addCategory(data) {
  return request({
    url: "/warehouse/documentClassification/add",
    method: "post",
    data: {
      category: data.category,
      parentId: data.parentId,
    },
  });
}
// ä¿®æ”¹åˆ†ç±»
export function updateCategory(data) {
  return request({
    url: "/warehouse/documentClassification/update",
    method: "put",
    data: {
      id: data.id,
      category: data.category,
    },
  });
}
// åˆ é™¤åˆ†ç±»
export function deleteCategory(ids) {
  return request({
    url: "/warehouse/documentClassification/delete",
    method: "delete",
    data: ids,
  });
}
// èŽ·å–æ–‡æ¡£åˆ—è¡¨ï¼ˆåˆ†é¡µï¼‰
export function getDocumentList(query) {
  return request({
    url: "/documentation/listPage",
    method: "get",
    params: query,
  });
}
// æ–°å¢žæ–‡æ¡£
export function addDocument(data) {
  return request({
    url: "/documentation/add",
    method: "post",
    data: data,
  });
}
// ä¿®æ”¹æ–‡æ¡£
export function updateDocument(data) {
  return request({
    url: "/documentation/update",
    method: "put",
    data: data,
  });
}
// åˆ é™¤æ–‡æ¡£
export function deleteDocument(ids) {
  return request({
    url: "/documentation/delete",
    method: "delete",
    data: ids,
  });
}
// èŽ·å–æ–‡æ¡£è¯¦æƒ…
export function getDocumentDetail(id) {
  return request({
    url: "/document/" + id,
    method: "get",
  });
}
// æœç´¢æ–‡æ¡£
export function searchDocument(query) {
  return request({
    url: "/document/search",
    method: "get",
    params: query,
  });
}
// èŽ·å–ä»“åº“ç»“æž„
export function getWarehouseStructure() {
  return request({
    url: "/document/warehouse/structure",
    method: "get",
  });
}
// é™„件管理相关接口
// æ·»åР附件
export function addDocumentationFile(data) {
  return request({
    url: "/documentation/documentationFile/add",
    method: "post",
    data: data,
  });
}
// èŽ·å–é™„ä»¶åˆ—è¡¨
export function getDocumentationFileList(params) {
  return request({
    url: "/documentation/documentationFile/listPage",
    method: "get",
    params: params,
  });
}
// åˆ é™¤é™„ä»¶
export function deleteDocumentationFile(ids) {
  return request({
    url: "/documentation/documentationFile/del",
    method: "delete",
    data: ids,
  });
}
// æ–‡æ¡£å€Ÿé˜…管理相关接口
export function getBorrowList(params) {
  return request({
    url: "/documentationBorrowManagement/listPage",
    method: "get",
    params: params,
  });
}
export function addBorrow(data) {
  return request({
    url: "/documentationBorrowManagement/add",
    method: "post",
    data: data,
  });
}
export function updateBorrow(data) {
  return request({
    url: "/documentationBorrowManagement/update",
    method: "put",
    data: data,
  });
}
export function deleteBorrow(ids) {
  return request({
    url: "/documentationBorrowManagement/delete",
    method: "delete",
    data: ids,
  });
}
// ç»Ÿè®¡ç›¸å…³æŽ¥å£
// èŽ·å–æ€»ä½“ç»Ÿè®¡æ•°æ®
export function getDocumentationOverview() {
  return request({
    url: "/documentation/overview",
    method: "get",
  });
}
// èŽ·å–åˆ†ç±»ç»Ÿè®¡æ•°æ®
export function getDocumentationCategoryStats() {
  return request({
    url: "/documentation/category",
    method: "get",
  });
}
// èŽ·å–çŠ¶æ€ç»Ÿè®¡æ•°æ®
export function getDocumentationStatusStats() {
  return request({
    url: "/documentation/status",
    method: "get",
  });
}
src/api/fileManagement/return.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,61 @@
import request from "@/utils/request";
// åˆ†é¡µæŸ¥è¯¢å½’还记录
export function getReturnListPage(query) {
  return request({
    url: "/documentationBorrowManagement/listPageReturn",
    method: "get",
    params: query,
  });
}
// å½’还操作
export function returnDocument(data) {
  return request({
    url: "/documentationBorrowManagement/revent",
    method: "put",
    data: data,
  });
}
// åˆ é™¤å½’还记录
export function deleteReturn(ids) {
  return request({
    url: "/documentationBorrowManagement/reventDelete",
    method: "delete",
    data: ids,
  });
}
//根据书籍id查询借阅记录
export function getBorrowListByDocumentationId(id) {
  return request({
    url: "/documentationBorrowManagement/getByDocumentationId/"+id,
    method: "get"
  });
}
// æ›´æ–°å€Ÿé˜…记录
export function updateBorrow(data) {
  return request({
    url: "/documentationBorrowManagement/update",
    method: "put",
    data: data,
  });
}
// å½’还更新
export function reventUpdate(data) {
  return request({
    url: "/documentationBorrowManagement/reventUpdate",
    method: "put",
    data: data,
  });
}
// èŽ·å–æ–‡æ¡£åˆ—è¡¨
export function getDocumentList() {
  return request({
    url: "/documentationBorrowManagement/list",
    method: "get",
  });
}
src/api/fileManagement/statistics.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,75 @@
import request from "@/utils/request";
// èŽ·å–æ¡£æ¡ˆæ€»ä½“ç»Ÿè®¡
export function getDocumentStatistics() {
  return request({
    url: "/fileManagement/statistics/overview",
    method: "get",
  });
}
// èŽ·å–æ¡£æ¡ˆåˆ†ç±»ç»Ÿè®¡
export function getCategoryStatistics() {
  return request({
    url: "/fileManagement/statistics/category",
    method: "get",
  });
}
// èŽ·å–æ¡£æ¡ˆçŠ¶æ€ç»Ÿè®¡
export function getStatusStatistics() {
  return request({
    url: "/fileManagement/statistics/status",
    method: "get",
  });
}
// èŽ·å–æ¡£æ¡ˆå€Ÿé˜…ç»Ÿè®¡
export function getBorrowStatistics() {
  return request({
    url: "/fileManagement/statistics/borrow",
    method: "get",
  });
}
// èŽ·å–æ¡£æ¡ˆå¹´åº¦ç»Ÿè®¡
export function getYearStatistics() {
  return request({
    url: "/fileManagement/statistics/year",
    method: "get",
  });
}
// èŽ·å–æ¡£æ¡ˆä½ç½®ç»Ÿè®¡
export function getLocationStatistics() {
  return request({
    url: "/fileManagement/statistics/location",
    method: "get",
  });
}
// èŽ·å–æ¡£æ¡ˆè¶‹åŠ¿ç»Ÿè®¡
export function getTrendStatistics(params) {
  return request({
    url: "/fileManagement/statistics/trend",
    method: "get",
    params: params,
  });
}
// èŽ·å–æ¡£æ¡ˆå€Ÿé˜…æŽ’è¡Œ
export function getBorrowRanking() {
  return request({
    url: "/fileManagement/statistics/borrowRanking",
    method: "get",
  });
}
// èŽ·å–æ¡£æ¡ˆåˆ†ç±»è¯¦æƒ…ç»Ÿè®¡
export function getCategoryDetailStatistics(categoryId) {
  return request({
    url: `/fileManagement/statistics/categoryDetail/${categoryId}`,
    method: "get",
  });
}
src/api/inventoryManagement/stockInventory.js
@@ -1,61 +1,78 @@
import request from "@/utils/request";
// åˆ†é¡µæŸ¥è¯¢åº“存记录列表
export const getStockInventoryListPage = (params) => {
    return request({
        url: "/stockInventory/pagestockInventory",
        method: "get",
        params,
    });
export const getStockInventoryListPage = params => {
  return request({
    url: "/stockInventory/pagestockInventory",
    method: "get",
    params,
  });
};
// åˆ†é¡µæŸ¥è¯¢è”合库存记录列表(包含商品信息)
export const getStockInventoryListPageCombined = params => {
  return request({
    url: "/stockInventory/pageListCombinedStockInventory",
    method: "get",
    params,
  });
};
// åˆ›å»ºåº“存记录
export const createStockInventory = (params) => {
    return request({
        url: "/stockInventory/addstockInventory",
        method: "post",
        data: params,
    });
export const createStockInventory = params => {
  return request({
    url: "/stockInventory/addstockInventory",
    method: "post",
    data: params,
  });
};
// å‡å°‘库存记录
export const subtractStockInventory = (params) => {
    return request({
        url: "/stockInventory/subtractStockInventory",
        method: "post",
        data: params,
    });
export const subtractStockInventory = params => {
  return request({
    url: "/stockInventory/subtractStockInventory",
    method: "post",
    data: params,
  });
};
export const getStockInventoryReportList = (params) => {
    return request({
        url: "/stockInventory/stockInventoryPage",
        method: "get",
        params,
    });
export const getStockInventoryReportList = params => {
  return request({
    url: "/stockInventory/stockInventoryPage",
    method: "get",
    params,
  });
};
export const getStockInventoryInAndOutReportList = (params) => {
    return request({
        url: "/stockInventory/stockInAndOutRecord",
        method: "get",
        params,
    });
export const getStockInventoryInAndOutReportList = params => {
  return request({
    url: "/stockInventory/stockInAndOutRecord",
    method: "get",
    params,
  });
};
// å†»ç»“库存记录
export const frozenStockInventory = (params) => {
    return request({
        url: "/stockInventory/frozenStock",
        method: "post",
        data: params,
    });
export const frozenStockInventory = params => {
  return request({
    url: "/stockInventory/frozenStock",
    method: "post",
    data: params,
  });
};
// è§£å†»åº“存记录
export const thawStockInventory = (params) => {
    return request({
        url: "/stockInventory/thawStock",
        method: "post",
        data: params,
    });
export const thawStockInventory = params => {
  return request({
    url: "/stockInventory/thawStock",
    method: "post",
    data: params,
  });
};
export const getStockInventoryByModelId = productModelId => {
  return request({
    url: "/stockInventory/getByModelId",
    method: "get",
    params: { productModelId },
  });
};
src/api/procurementManagement/procurementLedger.js
@@ -72,6 +72,16 @@
    method: "get",
  });
}
// æŸ¥è¯¢é‡‡è´­è¯¦æƒ…
export function getPurchaseByCode(query) {
  return request({
    url: "/purchase/ledger/getPurchaseByCode",
    method: "get",
    params: query,
  });
}
export function approveProcessGetInfo(query) {
    return request({
        url: '/approveProcess/get',
src/api/productionManagement/bom.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,82 @@
import request from "@/utils/request";
// BOM åˆ—表分页查询
export function listPage(query) {
  return request({
    url: "/technologyBom/listPage",
    method: "get",
    params: query,
  });
}
// æ–°å¢ž BOM
export function add(data) {
  return request({
    url: "/technologyBom/add",
    method: "post",
    data: data,
  });
}
// ä¿®æ”¹ BOM
export function update(data) {
  return request({
    url: "/technologyBom/update",
    method: "put",
    data: data,
  });
}
// åˆ é™¤ BOM
export function batchDelete(ids) {
  return request({
    url: "/technologyBom/batchDelete",
    method: "delete",
    data: ids,
  });
}
// å¤åˆ¶ BOM
export function copy(data) {
  return request({
    url: "/technologyBom/copy",
    method: "post",
    data: data,
  });
}
// èŽ·å–äº§å“åˆ—è¡¨ (用于新增BOM时选择产品)
export function getProductList(query) {
  return request({
    url: "/product/ledger/listPage",
    method: "get",
    params: query,
  });
}
// --- BOM ç»“构相关 ---
// æ ¹æ® BOM ID èŽ·å–ç»“æž„åˆ—è¡¨
export function queryStructureList(bomId) {
  return request({
    url: "/technologyBomStructure/listByBomId/" + bomId,
    method: "get",
  });
}
// ä¿å­˜ BOM ç»“æž„
export function addStructure(data) {
  return request({
    url: "/technologyBomStructure/batchSave",
    method: "post",
    data: data,
  });
}
// åˆ é™¤ BOM ç»“构项
export function deleteStructure(id) {
  return request({
    url: "/technologyBomStructure/batchDelete/" + id,
    method: "delete",
  });
}
src/api/productionManagement/processManagement.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,87 @@
import request from "@/utils/request";
export function getProcessList(query) {
  return request({
    url: "/technologyOperation/listPage",
    method: "get",
    params: query,
  });
}
export function list() {
  return request({
    url: "/technologyOperation/list",
    method: "get",
  });
}
export function add(data) {
  return request({
    url: "/technologyOperation/add",
    method: "post",
    data: data,
  });
}
export function update(data) {
  return request({
    url: "/technologyOperation/update",
    method: "put",
    data: data,
  });
}
export function del(ids) {
  return request({
    url: "/technologyOperation/batchDelete",
    method: "delete",
    data: ids,
  });
}
export function getProcessParamList(params) {
  return request({
    url: "/technologyOperationParam/list",
    method: "get",
    params,
  });
}
export function addProcessParam(data) {
  return request({
    url: "/technologyOperationParam/",
    method: "post",
    data: data,
  });
}
export function editProcessParam(data) {
  return request({
    url: "/technologyOperationParam/",
    method: "post",
    data: data,
  });
}
export function deleteProcessParam(id) {
  return request({
    url: `/technologyOperationParam/batchDelete/${id}`,
    method: "delete",
  });
}
export function getDeviceLedger(query) {
  return request({
    url: "/device/ledger/getDeviceLedger",
    method: "get",
    params: query,
  });
}
export function getBaseParamList(query) {
  return request({
    url: "/technologyParam/list",
    method: "get",
    params: query,
  });
}
src/api/productionManagement/processRoute.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,37 @@
// å·¥è‰ºè·¯çº¿ç›¸å…³æŽ¥å£
import request from "@/utils/request";
// åˆ†é¡µæŸ¥è¯¢å·¥è‰ºè·¯çº¿åˆ—表
export function listPage(query) {
  return request({
    url: "/technologyRouting/page",
    method: "get",
    params: query,
  });
}
// æŸ¥è¯¢å·¥è‰ºè·¯çº¿é¡¹ç›®åˆ—表
export function findProcessRouteItemList(query) {
  return request({
    url: "/technologyRoutingOperation/list",
    method: "get",
    params: query,
  });
}
// èŽ·å–å·¥åºå‚æ•°åˆ—è¡¨
export function getProcessParamList(query) {
  return request({
    url: "/technologyRoutingOperationParam/list",
    method: "get",
    params: query,
  });
}
// æŸ¥è¯¢BOM结构 (工艺路线)
export function queryBomList(bomId) {
  return request({
    url: "/technologyBomStructure/listByBomId/" + bomId,
    method: "get",
  });
}
src/api/productionManagement/productProcessRoute.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,11 @@
// ç”Ÿäº§æŠ¥å·¥é¡µé¢æŽ¥å£
import request from "@/utils/request";
// èŽ·å–å·¥åºå‚æ•°åˆ—è¡¨-生产订单
export function findProcessParamListOrder(query) {
  return request({
    url: `/productionOrderRoutingOperationParam/list`,
    method: "get",
    params: query,
  });
}
src/api/productionManagement/productionCosting.js
@@ -8,4 +8,22 @@
    method: "get",
    params: query,
  });
}
// å·¦è¾¹è¡¨æ ¼çš„æŽ¥å£ (汇总)
export function salesLedgerProductionAccountingList(query) {
  return request({
    url: "/productionAccount/listPage",
    method: "get",
    params: query,
  });
}
// å³è¾¹è¡¨æ ¼çš„æŽ¥å£ (明细)
export function salesLedgerProductionAccountingListProductionDetails(query) {
  return request({
    url: "/productionAccount/listProductionDetails",
    method: "get",
    params: query,
  });
}
src/api/productionManagement/productionOrder.js
@@ -1,19 +1,79 @@
// ç”Ÿäº§è®¢å•页面接口
import request from "@/utils/request";
// åˆ†é¡µæŸ¥è¯¢
export function schedulingListPage(query) {
// åˆ†é¡µæŸ¥è¯¢ç”Ÿäº§è®¢å•
export function productOrderListPage(query) {
  return request({
    url: "/salesLedger/scheduling/listPage",
    url: "/productionOrder/page",
    method: "get",
    params: query,
  });
}
// ç”Ÿäº§æ´¾å·¥
export function productionDispatch(query) {
// ç”Ÿäº§è®¢å•溯源详情
export function getOrderDetail(npsNo) {
  return request({
    url: "/salesLedger/scheduling/productionDispatch",
    method: "post",
    data: query,
    url: "/productionOrder/ordeDetail",
    method: "get",
    params: { npsNo },
  });
}
}
// èŽ·å–ç”Ÿäº§è®¢å•æ¥æºæ•°æ®
export function getProductOrderSource(id) {
  return request({
    url: `/productionOrder/source/${id}`,
    method: "get",
  });
}
// é¢†æ–™è¯¦æƒ…列表
export function listMaterialPickingDetail(productionOrderId) {
  return request({
    url: "/productionOrderPick/detail/" + productionOrderId,
    method: "get",
  });
}
// è¡¥æ–™è®°å½•列表
export function listMaterialSupplementRecord(query) {
  return request({
    url: "/productionOrderPickRecord/feeding",
    method: "get",
    params: query,
  });
}
// èŽ·å–é¢†æ–™BOM信息 (可选,备用)
export function listMaterialPickingBom(productionOrderId) {
  return request({
    url: "/productionOrder/pick/" + productionOrderId,
    method: "get",
  });
}
// èŽ·å–ç”Ÿäº§è®¢å•å…³è”çš„å·¥è‰ºè·¯çº¿ä¸»ä¿¡æ¯
export function getOrderProcessRouteMain(orderId) {
  return request({
    url: "/productionOrderRouting/listMain",
    method: "get",
    params: { orderId },
  });
}
// æŸ¥è¯¢BOM结构 (生产订单)
export function queryOrderBomList(bomId) {
  return request({
    url: "/productionBomStructure/listByBomId/" + bomId,
    method: "get",
  });
}
// èŽ·å–å·¥åºå‚æ•°åˆ—è¡¨ (生产订单)
export function findProcessParamListOrder(query) {
  return request({
    url: "/productionOrderRoutingOperationParam/list",
    method: "get",
    params: query,
  });
}
src/api/productionManagement/productionPlan.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,29 @@
// ä¸»ç”Ÿäº§è®¡åˆ’接口
import request from "@/utils/request";
// åˆ†é¡µåˆ—表
export function productionPlanListPage(query) {
  return request({
    url: "/productionPlan/listPage",
    method: "get",
    params: query,
  });
}
// æ‹‰å–数据
export function loadProdData(query) {
  return request({
    url: "/productionPlan/loadProdData",
    method: "get",
    params: query,
  });
}
// æ±‡æ€»ç»Ÿè®¡
export function summaryByProductType(query) {
  return request({
    url: "/productionPlan/summaryByProductType",
    method: "get",
    params: query,
  });
}
src/api/productionManagement/productionProductMain.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,29 @@
// ç”Ÿäº§æŠ¥å·¥é¡µé¢æŽ¥å£
import request from "@/utils/request";
// åˆ†é¡µæŸ¥è¯¢ç”Ÿäº§æŠ¥å·¥ä¸»è¡¨
export function productionProductMainListPage(query) {
    return request({
        url: "/productionProductMain/listPage",
        method: "get",
        params: query,
    });
}
// åˆ é™¤æŠ¥å·¥
export function productionReportDelete(query) {
    return request({
        url: "/productionProductMain/delete",
        method: "get",
        params: query,
    });
}
// æŸ¥è¯¢æŠ•入列表
export function productionProductInputListPage(query) {
    return request({
        url: "/productionProductInput/listPage",
        method: "get",
        params: query,
    });
}
src/api/productionManagement/productionReporting.js
@@ -20,9 +20,8 @@
// æ ¹æ®ID获取工单详情
export function getProductWorkOrderById(query) {
  return request({
    url: "/productWorkOrder/getProductWorkOrderById",
    url: "/productionOperationTask/" + query.id,
    method: "get",
    params: query,
  });
}
// ç”Ÿäº§æŠ¥å·¥
src/api/productionManagement/workOrder.js
@@ -2,7 +2,7 @@
export function productWorkOrderPage(query) {
  return request({
    url: "/productWorkOrder/page",
    url: "/productionOperationTask/page",
    method: "get",
    params: query,
  });
@@ -10,7 +10,7 @@
export function updateProductWorkOrder(data) {
  return request({
    url: "/productWorkOrder/updateProductWorkOrder",
    url: "/productionOperationTask/updateProductWorkOrder",
    method: "post",
    data: data,
  });
@@ -24,12 +24,29 @@
  });
}
export function assignProductWorkOrder(data) {
  return request({
    url: "/productionOperationTask/assign",
    method: "post",
    data: data,
  });
}
// ä¸‹è½½å·¥å•流转卡(返回文件流)
export function downProductWorkOrder(id) {
  return request({
    url: "/productWorkOrder/down",
    url: "/productionOperationTask/down",
    method: "post",
    data: { id },
    responseType: "blob",
  });
}
// èŽ·å–å·¥åºç»Ÿè®¡æ•°æ®
export function getOperationStatistics(query) {
  return request({
    url: "/productionOperationTask/getOperation",
    method: "get",
    params: query,
  });
}
src/components/CommonUpload.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,164 @@
<template>
  <view class="common-upload">
    <u-upload
      :fileList="internalFileList"
      @afterRead="afterRead"
      @delete="deleteFile"
      :name="name"
      :multiple="multiple"
      :maxCount="maxCount"
      :accept="accept"
      :disabled="disabled"
    ></u-upload>
  </view>
</template>
<script setup>
import { ref, watch, onMounted } from 'vue';
import { getToken } from "@/utils/auth";
import config from "@/config";
const props = defineProps({
  // çˆ¶ç»„件传入的文件列表(对应后端存储的对象列表)
  modelValue: {
    type: Array,
    default: () => []
  },
  // æœ€å¤§ä¸Šä¼ æ•°é‡
  maxCount: {
    type: Number,
    default: 9
  },
  // æ˜¯å¦æ”¯æŒå¤šé€‰
  multiple: {
    type: Boolean,
    default: true
  },
  // æŽ¥å—的文件类型
  accept: {
    type: String,
    default: 'image'
  },
  // ä¸Šä¼ æŽ¥å£å¯¹åº”的参数名
  name: {
    type: String,
    default: 'file'
  },
  // æ˜¯å¦ç¦ç”¨
  disabled: {
    type: Boolean,
    default: false
  }
});
const emit = defineEmits(['update:modelValue']);
// ç”¨äºŽ u-upload æ˜¾ç¤ºçš„内部列表
const internalFileList = ref([]);
// ç›‘听外部 modelValue å˜åŒ–,同步到内部显示列表
watch(() => props.modelValue, (newVal) => {
  if (newVal) {
    internalFileList.value = newVal.map(item => ({
      ...item,
      url: item.url || item.previewURL,
      status: 'success',
      message: ''
    }));
  }
}, { immediate: true, deep: true });
const showToast = (message) => {
  uni.showToast({
    title: message,
    icon: "none",
  });
};
// ä¸Šä¼ é€»è¾‘
const uploadFilePromise = (url) => {
  return new Promise((resolve, reject) => {
    uni.uploadFile({
      url: config.baseUrl + "/common/upload",
      filePath: url,
      name: "files", // æ³¨æ„ï¼šè¿™é‡Œæ ¹æ®åŽŸä»£ç æ˜¯ "files"
      header: {
        Authorization: "Bearer " + getToken(),
      },
      success: (res) => {
        try {
          const data = JSON.parse(res.data);
          if (data.code === 200) {
            // å¦‚果返回的是数组,取第一个元素
            const resultData = Array.isArray(data.data) ? data.data[0] : data.data;
            // å¤„理 url èµ‹å€¼
            if (!resultData.url && resultData.previewURL) {
              resultData.url = resultData.previewURL;
            }
            // å…¼å®¹åŽŸä»£ç ä¸­çš„ name èµ‹å€¼
            if (!resultData.name && resultData.originalFilename) {
              resultData.name = resultData.originalFilename;
            }
            resolve(resultData);
          } else {
            reject(data.msg || "上传失败");
          }
        } catch (e) {
          reject("解析响应失败");
        }
      },
      fail: (err) => {
        reject(err);
      },
    });
  });
};
// ä¸Šä¼ åŽçš„处理
const afterRead = async (event) => {
  let lists = [].concat(event.file);
  let currentModelValue = [...props.modelValue];
  // å…ˆåœ¨å†…部列表中添加占位(上传中状态)
  lists.forEach(item => {
    internalFileList.value.push({
      ...item,
      status: 'uploading',
      message: '上传中'
    });
  });
  for (let i = 0; i < lists.length; i++) {
    try {
      const result = await uploadFilePromise(lists[i].url);
      // æ›´æ–° modelValue
      currentModelValue.push(result);
      emit('update:modelValue', currentModelValue);
    } catch (e) {
      // å¦‚果上传失败,从内部列表中移除刚才添加的项
      const errorIndex = internalFileList.value.findIndex(item => item.status === 'uploading');
      if (errorIndex > -1) {
        internalFileList.value.splice(errorIndex, 1);
      }
      showToast(typeof e === "string" ? e : "上传失败");
    }
  }
};
// åˆ é™¤å¤„理
const deleteFile = (event) => {
  const newList = [...props.modelValue];
  newList.splice(event.index, 1);
  emit('update:modelValue', newList);
};
</script>
<style scoped lang="scss">
.common-upload {
  width: 100%;
}
</style>
src/pages.json
@@ -367,6 +367,55 @@
      }
    },
    {
      "path": "pages/productionDesign/basicParameters/index",
      "style": {
        "navigationBarTitleText": "基础参数",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/productionDesign/basicParameters/edit",
      "style": {
        "navigationBarTitleText": "参数详情",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/productionDesign/processManagement/index",
      "style": {
        "navigationBarTitleText": "工序管理",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/productionDesign/processManagement/edit",
      "style": {
        "navigationBarTitleText": "工序详情",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/productionDesign/processManagement/params",
      "style": {
        "navigationBarTitleText": "工序参数配置",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/productionDesign/bom/index",
      "style": {
        "navigationBarTitleText": "BOM管理",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/productionDesign/bom/structure",
      "style": {
        "navigationBarTitleText": "BOM结构",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/cooperativeOffice/collaborativeApproval/index1",
      "style": {
        "navigationBarTitleText": "公出管理",
@@ -712,6 +761,20 @@
      }
    },
    {
      "path": "pages/inspectionUpload/upload",
      "style": {
        "navigationBarTitleText": "上传巡检记录",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/inspectionUpload/attachment",
      "style": {
        "navigationBarTitleText": "查看附件",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/equipmentManagement/faultAnalysis/index",
      "style": {
        "navigationBarTitleText": "故障分析追溯",
@@ -722,6 +785,34 @@
      "path": "pages/productionManagement/productionOrder/index",
      "style": {
        "navigationBarTitleText": "生产订单",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/productionManagement/productionOrder/source",
      "style": {
        "navigationBarTitleText": "来源数据",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/productionManagement/productionOrder/pickingDetail",
      "style": {
        "navigationBarTitleText": "领料详情",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/productionManagement/processRoute/index",
      "style": {
        "navigationBarTitleText": "工艺路线",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/productionManagement/processRoute/items",
      "style": {
        "navigationBarTitleText": "路线项目",
        "navigationStyle": "custom"
      }
    },
@@ -747,19 +838,61 @@
      }
    },
    {
      "path": "pages/productionManagement/productionReporting/ledger",
      "style": {
        "navigationBarTitleText": "报工台账",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/productionManagement/workOrder/index",
      "style": {
        "navigationBarTitleText": "生产工单",
        "navigationStyle": "custom"
      }
    },
    // {
    //   "path": "pages/productionManagement/productionCosting/index",
    //   "style": {
    //     "navigationBarTitleText": "生产核算",
    //     "navigationStyle": "custom"
    //   }
    // },
    {
      "path": "pages/productionManagement/mainProductionPlan/index",
      "style": {
        "navigationBarTitleText": "主生产计划",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/productionManagement/mainProductionPlan/detail",
      "style": {
        "navigationBarTitleText": "生产计划详情",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/productionManagement/productionScheduling/index",
      "style": {
        "navigationBarTitleText": "生产排产",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/productionManagement/productionAccounting/index",
      "style": {
        "navigationBarTitleText": "生产核算",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/productionManagement/productionTraceability/index",
      "style": {
        "navigationBarTitleText": "生产追溯",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/productionManagement/processStatistics/index",
      "style": {
        "navigationBarTitleText": "工序生产实况",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/inventoryManagement/receiptManagement/index",
      "style": {
@@ -1143,6 +1276,34 @@
      "style": {
        "navigationBarTitleText": "消息中心"
      }
    },
    {
      "path": "pages/fileManagement/borrow/index",
      "style": {
        "navigationBarTitleText": "借阅管理",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/fileManagement/borrow/edit",
      "style": {
        "navigationBarTitleText": "借阅登记",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/fileManagement/return/index",
      "style": {
        "navigationBarTitleText": "归还管理",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/fileManagement/return/edit",
      "style": {
        "navigationBarTitleText": "归还登记",
        "navigationStyle": "custom"
      }
    }
  ],
  "subPackages": [
@@ -1379,4 +1540,4 @@
    "navigationBarTitleText": "RuoYi",
    "navigationBarBackgroundColor": "#FFFFFF"
  }
}
}
src/pages/cooperativeOffice/collaborativeApproval/approve.vue
@@ -1,8 +1,7 @@
<template>
  <view class="approve-page">
    <PageHeader title="审核" @back="goBack" />
    <PageHeader title="审核"
                @back="goBack" />
    <!-- ç”³è¯·ä¿¡æ¯ -->
    <view class="application-info">
      <view class="info-header">
@@ -25,7 +24,6 @@
          <text class="info-label">申请日期</text>
          <text class="info-value">{{ approvalData.approveTime }}</text>
        </view>
        <!-- approveType=2 è¯·å‡ç›¸å…³å­—段 -->
        <template v-if="approvalData.approveType === 2">
          <view class="info-row">
@@ -37,462 +35,472 @@
            <text class="info-value">{{ approvalData.endDate || '-' }}</text>
          </view>
        </template>
        <!-- approveType=3 å‡ºå·®ç›¸å…³å­—段 -->
        <view v-if="approvalData.approveType === 3" class="info-row">
        <view v-if="approvalData.approveType === 3"
              class="info-row">
          <text class="info-label">出差地点</text>
          <text class="info-value">{{ approvalData.location || '-' }}</text>
        </view>
        <!-- approveType=4 æŠ¥é”€ç›¸å…³å­—段 -->
        <view v-if="approvalData.approveType === 4" class="info-row">
        <view v-if="approvalData.approveType === 4"
              class="info-row">
          <text class="info-label">报销金额</text>
          <text class="info-value">{{ approvalData.price ? `Â¥${approvalData.price}` : '-' }}</text>
        </view>
      </view>
    </view>
    <!-- å®¡æ‰¹æµç¨‹ -->
    <view class="approval-process">
      <view class="process-header">
        <text class="process-title">审批流程</text>
      </view>
      <view class="process-steps">
        <view
          v-for="(step, index) in approvalSteps"
          :key="index"
          class="process-step"
          :class="{
        <view v-for="(step, index) in approvalSteps"
              :key="index"
              class="process-step"
              :class="{
            'completed': step.status === 'completed',
            'current': step.status === 'current',
            'pending': step.status === 'pending',
            'rejected': step.status === 'rejected'
          }"
        >
          }">
          <view class="step-indicator">
            <view class="step-dot">
              <text v-if="step.status === 'completed'" class="step-icon">✓</text>
              <text v-else-if="step.status === 'rejected'" class="step-icon">✗</text>
              <text v-else class="step-number">{{ index + 1 }}</text>
              <text v-if="step.status === 'completed'"
                    class="step-icon">✓</text>
              <text v-else-if="step.status === 'rejected'"
                    class="step-icon">✗</text>
              <text v-else
                    class="step-number">{{ index + 1 }}</text>
            </view>
            <view v-if="index < approvalSteps.length - 1" class="step-line"></view>
            <view v-if="index < approvalSteps.length - 1"
                  class="step-line"></view>
          </view>
          <view class="step-content">
            <view class="step-info">
              <text class="step-title">{{ step.title }}</text>
              <text class="step-approver">{{ step.approverName }}</text>
              <text v-if="step.approveTime" class="step-time">{{ step.approveTime }}</text>
              <text v-if="step.approveTime"
                    class="step-time">{{ step.approveTime }}</text>
            </view>
            <view v-if="step.opinion" class="step-opinion">
            <view v-if="step.opinion"
                  class="step-opinion">
              <text class="opinion-label">审批意见:</text>
              <text class="opinion-content">{{ step.opinion }}</text>
            </view>
            <!-- ç­¾åå±•示 -->
            <view v-if="step.urlTem" class="step-opinion" style="margin-top:8px;">
            <view v-if="step.urlTem"
                  class="step-opinion"
                  style="margin-top:8px;">
              <text class="opinion-label">签名:</text>
              <image :src="step.urlTem" mode="widthFix" style="width:180px;border-radius:6px;border:1px solid #eee;" />
              <image :src="step.urlTem"
                     mode="widthFix"
                     style="width:180px;border-radius:6px;border:1px solid #eee;" />
            </view>
          </view>
        </view>
      </view>
    </view>
    <!-- å®¡æ ¸æ„è§è¾“å…¥ -->
    <view v-if="canApprove" class="approval-input">
    <view v-if="canApprove"
          class="approval-input">
      <view class="input-header">
        <text class="input-title">审核意见</text>
      </view>
      <view class="input-content">
        <u-textarea
          v-model="approvalOpinion"
          rows="4"
          placeholder="请输入审核意见"
          maxlength="200"
          count
        />
        <u-textarea v-model="approvalOpinion"
                    rows="4"
                    placeholder="请输入审核意见"
                    maxlength="200"
                    count />
      </view>
    </view>
    <!-- åº•部操作按钮 -->
    <view v-if="canApprove" class="footer-actions">
      <u-button class="reject-btn" @click="handleReject">驳回</u-button>
      <u-button class="approve-btn" @click="handleApprove">通过</u-button>
    <view v-if="canApprove"
          class="footer-actions">
      <u-button class="reject-btn"
                @click="handleReject">驳回</u-button>
      <u-button class="approve-btn"
                @click="handleApprove">通过</u-button>
    </view>
  </view>
</template>
<script setup>
import { ref, onMounted, computed } from 'vue'
import { approveProcessGetInfo, approveProcessDetails, updateApproveNode } from '@/api/collaborativeApproval/approvalProcess'
import useUserStore from '@/store/modules/user'
const showToast = (message) => {
    uni.showToast({
        title: message,
        icon: 'none'
    })
}
import PageHeader from "@/components/PageHeader.vue";
  import { ref, onMounted, computed } from "vue";
  import {
    approveProcessGetInfo,
    approveProcessDetails,
    updateApproveNode,
  } from "@/api/collaborativeApproval/approvalProcess";
  import useUserStore from "@/store/modules/user";
  const showToast = message => {
    uni.showToast({
      title: message,
      icon: "none",
    });
  };
  import PageHeader from "@/components/PageHeader.vue";
const userStore = useUserStore()
const approvalData = ref({})
const approvalSteps = ref([])
const approvalOpinion = ref('')
const approveId = ref('')
  const userStore = useUserStore();
  const approvalData = ref({});
  const approvalSteps = ref([]);
  const approvalOpinion = ref("");
  const approveId = ref("");
// ä»Žè¯¦æƒ…接口字段对齐 canApprove:仅当有 isShen çš„节点时可审批
const canApprove = computed(() => {
  return approvalSteps.value.some(step => step.isShen === true)
})
  // ä»Žè¯¦æƒ…接口字段对齐 canApprove:仅当有 isShen çš„节点时可审批
  const canApprove = computed(() => {
    return approvalSteps.value.some(step => step.isShen === true);
  });
onMounted(() => {
  approveId.value = uni.getStorageSync('approveId')
  if (approveId.value) {
    loadApprovalData()
  }
})
const loadApprovalData = () => {
  // åŸºæœ¬ç”³è¯·ä¿¡æ¯
  approveProcessGetInfo({ id: approveId.value }).then(res => {
    approvalData.value = res.data || {}
  })
  // å®¡æ‰¹èŠ‚ç‚¹è¯¦æƒ…
  approveProcessDetails(approveId.value).then(res => {
    const list = Array.isArray(res.data) ? res.data : []
    // ä¿å­˜åŽŸå§‹èŠ‚ç‚¹æ•°æ®ä¾›æäº¤ä½¿ç”¨
    activities.value = list
    approvalSteps.value = list.map((it, idx) => {
      // èŠ‚ç‚¹çŠ¶æ€æ˜ å°„ï¼š1=通过,2=不通过,否则看是否当前(isShen),再默认为待处理
      let status = 'pending'
      if (it.approveNodeStatus === 1) status = 'completed'
      else if (it.approveNodeStatus === 2) status = 'rejected'
      else if (it.isShen) status = 'current'
      return {
        title: `第${idx + 1}步审批`,
        approverName: it.approveNodeUser || '未知用户',
        status,
        approveTime: it.approveTime || null,
        opinion: it.approveNodeReason || '',
        urlTem: it.urlTem || '',
        isShen: !!it.isShen
      }
    })
  })
}
const goBack = () => {
  uni.removeStorageSync('approveId');
  uni.navigateBack()
}
const submitForm = (status) => {
  // å¯é€‰ï¼šæ ¡éªŒå®¡æ ¸æ„è§
  if (!approvalOpinion.value?.trim()) {
    showToast('请输入审核意见')
    return
  }
  // æ‰¾åˆ°å½“前可审批节点
  const filteredActivities = activities.value.filter(activity => activity.isShen)
  if (!filteredActivities.length) {
    showToast('当前无可审批节点')
    return
  }
  // å†™å…¥çŠ¶æ€å’Œæ„è§
  filteredActivities[0].approveNodeStatus = status
  filteredActivities[0].approveNodeReason = approvalOpinion.value || ''
  // è®¡ç®—是否为最后一步
  const isLast = activities.value.findIndex(a => a.isShen) === activities.value.length - 1
  // è°ƒç”¨åŽç«¯
  updateApproveNode({ ...filteredActivities[0], isLast }).then(() => {
    const msg = status === 1 ? '审批通过' : '审批已驳回'
    showToast(msg)
    // æç¤ºåŽè¿”回上一个页面
    setTimeout(() => {
      goBack() // å†…部是 uni.navigateBack()
    }, 800)
  })
}
const handleApprove = () => {
  uni.showModal({
    title: '确认操作',
    content: '确定要通过此审批吗?',
    success: (res) => {
      if (res.confirm) submitForm(1)
  onMounted(() => {
    approveId.value = uni.getStorageSync("approveId");
    if (approveId.value) {
      loadApprovalData();
    }
  })
}
  });
const handleReject = () => {
  uni.showModal({
    title: '确认操作',
    content: '确定要驳回此审批吗?',
    success: (res) => {
      if (res.confirm) submitForm(2)
  const loadApprovalData = () => {
    // åŸºæœ¬ç”³è¯·ä¿¡æ¯
    approveProcessGetInfo({ id: approveId.value }).then(res => {
      approvalData.value = res.data || {};
    });
    // å®¡æ‰¹èŠ‚ç‚¹è¯¦æƒ…
    approveProcessDetails(approveId.value).then(res => {
      const list = Array.isArray(res.data) ? res.data : [];
      // ä¿å­˜åŽŸå§‹èŠ‚ç‚¹æ•°æ®ä¾›æäº¤ä½¿ç”¨
      activities.value = list;
      approvalSteps.value = list.map((it, idx) => {
        // èŠ‚ç‚¹çŠ¶æ€æ˜ å°„ï¼š1=通过,2=不通过,否则看是否当前(isShen),再默认为待处理
        let status = "pending";
        if (it.approveNodeStatus === 1) status = "completed";
        else if (it.approveNodeStatus === 2) status = "rejected";
        else if (it.isShen) status = "current";
        return {
          title: `第${idx + 1}步审批`,
          approverName: it.approveNodeUser || "未知用户",
          status,
          approveTime: it.approveTime || null,
          opinion: it.approveNodeReason || "",
          urlTem: it.urlTem || "",
          isShen: !!it.isShen,
        };
      });
    });
  };
  const goBack = () => {
    uni.removeStorageSync("approveId");
    uni.navigateBack();
  };
  const submitForm = status => {
    // å¯é€‰ï¼šæ ¡éªŒå®¡æ ¸æ„è§
    if (!approvalOpinion.value?.trim()) {
      showToast("请输入审核意见");
      return;
    }
  })
}
// åŽŸå§‹èŠ‚ç‚¹æ•°æ®ï¼ˆç”¨äºŽæäº¤é€»è¾‘ï¼‰
const activities = ref([])
    // æ‰¾åˆ°å½“前可审批节点
    const filteredActivities = activities.value.filter(
      activity => activity.isShen
    );
    if (!filteredActivities.length) {
      showToast("当前无可审批节点");
      return;
    }
    // å†™å…¥çŠ¶æ€å’Œæ„è§
    filteredActivities[0].approveNodeStatus = status;
    filteredActivities[0].approveNodeReason = approvalOpinion.value || "";
    // è®¡ç®—是否为最后一步
    const isLast =
      activities.value.findIndex(a => a.isShen) === activities.value.length - 1;
    // è°ƒç”¨åŽç«¯
    updateApproveNode({ ...filteredActivities[0], isLast }).then(() => {
      const msg = status === 1 ? "审批通过" : "审批已驳回";
      showToast(msg);
      // æç¤ºåŽè¿”回上一个页面
      setTimeout(() => {
        goBack(); // å†…部是 uni.navigateBack()
      }, 800);
    });
  };
  const handleApprove = () => {
    uni.showModal({
      title: "确认操作",
      content: "确定要通过此审批吗?",
      success: res => {
        if (res.confirm) submitForm(1);
      },
    });
  };
  const handleReject = () => {
    uni.showModal({
      title: "确认操作",
      content: "确定要驳回此审批吗?",
      success: res => {
        if (res.confirm) submitForm(2);
      },
    });
  };
  // åŽŸå§‹èŠ‚ç‚¹æ•°æ®ï¼ˆç”¨äºŽæäº¤é€»è¾‘ï¼‰
  const activities = ref([]);
</script>
<style scoped lang="scss">
.approve-page {
  min-height: 100vh;
  background: #f8f9fa;
  padding-bottom: 80px;
}
.header {
  display: flex;
  align-items: center;
  background: #fff;
  padding: 16px 20px;
  border-bottom: 1px solid #f0f0f0;
  position: sticky;
  top: 0;
  z-index: 100;
}
.title {
  flex: 1;
  text-align: center;
  font-size: 18px;
  font-weight: 600;
  color: #333;
}
.application-info {
  background: #fff;
  margin: 16px;
  border-radius: 12px;
  overflow: hidden;
}
.info-header {
  padding: 16px;
  border-bottom: 1px solid #f0f0f0;
  background: #f8f9fa;
}
.info-title {
  font-size: 16px;
  font-weight: 600;
  color: #333;
}
.info-content {
  padding: 16px;
}
.info-row {
  display: flex;
  align-items: center;
  margin-bottom: 12px;
  &:last-child {
    margin-bottom: 0;
  .approve-page {
    min-height: 100vh;
    background: #f8f9fa;
    padding-bottom: 80px;
  }
}
.info-label {
  font-size: 14px;
  color: #666;
  width: 80px;
  flex-shrink: 0;
}
  .header {
    display: flex;
    align-items: center;
    background: #fff;
    padding: 16px 20px;
    border-bottom: 1px solid #f0f0f0;
    position: sticky;
    top: 0;
    z-index: 100;
  }
.info-value {
  font-size: 14px;
  color: #333;
  flex: 1;
}
  .title {
    flex: 1;
    text-align: center;
    font-size: 18px;
    font-weight: 600;
    color: #333;
  }
.approval-process {
  background: #fff;
  margin: 16px;
  border-radius: 12px;
  overflow: hidden;
}
  .application-info {
    background: #fff;
    margin: 16px;
    border-radius: 12px;
    overflow: hidden;
  }
.process-header {
  padding: 16px;
  border-bottom: 1px solid #f0f0f0;
  background: #f8f9fa;
}
  .info-header {
    padding: 16px;
    border-bottom: 1px solid #f0f0f0;
    background: #f8f9fa;
  }
.process-title {
  font-size: 16px;
  font-weight: 600;
  color: #333;
}
  .info-title {
    font-size: 16px;
    font-weight: 600;
    color: #333;
  }
.process-steps {
  padding: 20px;
}
  .info-content {
    padding: 16px;
  }
.process-step {
  display: flex;
  position: relative;
  margin-bottom: 24px;
  &:last-child {
    margin-bottom: 0;
    .step-line {
      display: none;
  .info-row {
    display: flex;
    align-items: center;
    margin-bottom: 12px;
    &:last-child {
      margin-bottom: 0;
    }
  }
}
.step-indicator {
  display: flex;
  flex-direction: column;
  align-items: center;
  margin-right: 16px;
}
  .info-label {
    font-size: 14px;
    color: #666;
    width: 80px;
    flex-shrink: 0;
  }
.step-dot {
  width: 32px;
  height: 32px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 14px;
  font-weight: 600;
  position: relative;
  z-index: 2;
}
  .info-value {
    font-size: 14px;
    color: #333;
    flex: 1;
  }
.process-step.completed .step-dot {
  background: #52c41a;
  color: #fff;
}
  .approval-process {
    background: #fff;
    margin: 16px;
    border-radius: 12px;
    overflow: hidden;
  }
.process-step.current .step-dot {
  background: #1890ff;
  color: #fff;
  animation: pulse 2s infinite;
}
  .process-header {
    padding: 16px;
    border-bottom: 1px solid #f0f0f0;
    background: #f8f9fa;
  }
.process-step.pending .step-dot {
  background: #d9d9d9;
  color: #999;
}
  .process-title {
    font-size: 16px;
    font-weight: 600;
    color: #333;
  }
.step-line {
  width: 2px;
  height: 40px;
  background: #d9d9d9;
  margin-top: 8px;
}
  .process-steps {
    padding: 20px;
  }
.process-step.completed .step-line {
  background: #52c41a;
}
  .process-step {
    display: flex;
    position: relative;
    margin-bottom: 24px;
.process-step.rejected .step-dot {
  background: #ff4d4f;
  color: #fff;
}
.process-step.rejected .step-line {
  background: #ff4d4f;
}
    &:last-child {
      margin-bottom: 0;
.step-content {
  flex: 1;
  padding-top: 4px;
}
      .step-line {
        display: none;
      }
    }
  }
.step-info {
  margin-bottom: 8px;
}
  .step-indicator {
    display: flex;
    flex-direction: column;
    align-items: center;
    margin-right: 16px;
  }
.step-title {
  font-size: 16px;
  font-weight: 600;
  color: #333;
  display: block;
  margin-bottom: 4px;
}
  .step-dot {
    width: 32px;
    height: 32px;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 14px;
    font-weight: 600;
    position: relative;
    z-index: 2;
  }
.step-approver {
  font-size: 14px;
  color: #666;
  display: block;
  margin-bottom: 4px;
}
  .process-step.completed .step-dot {
    background: #52c41a;
    color: #fff;
  }
.step-time {
  font-size: 12px;
  color: #999;
  display: block;
}
  .process-step.current .step-dot {
    background: #1890ff;
    color: #fff;
    animation: pulse 2s infinite;
  }
.step-opinion {
  background: #f8f9fa;
  padding: 12px;
  border-radius: 8px;
  border-left: 4px solid #52c41a;
}
  .process-step.pending .step-dot {
    background: #d9d9d9;
    color: #999;
  }
.opinion-label {
  font-size: 12px;
  color: #666;
  display: block;
  margin-bottom: 4px;
}
  .step-line {
    width: 2px;
    height: 40px;
    background: #d9d9d9;
    margin-top: 8px;
  }
.opinion-content {
  font-size: 14px;
  color: #333;
  line-height: 1.5;
}
  .process-step.completed .step-line {
    background: #52c41a;
  }
.approval-input {
  background: #fff;
  margin: 16px;
  border-radius: 12px;
  overflow: hidden;
}
  .process-step.rejected .step-dot {
    background: #ff4d4f;
    color: #fff;
  }
  .process-step.rejected .step-line {
    background: #ff4d4f;
  }
.input-header {
  padding: 16px;
  border-bottom: 1px solid #f0f0f0;
  background: #f8f9fa;
}
  .step-content {
    flex: 1;
    padding-top: 4px;
  }
.input-title {
  font-size: 16px;
  font-weight: 600;
  color: #333;
}
  .step-info {
    margin-bottom: 8px;
  }
.input-content {
  padding: 16px;
}
  .step-title {
    font-size: 16px;
    font-weight: 600;
    color: #333;
    display: block;
    margin-bottom: 4px;
  }
.footer-actions {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  background: #fff;
  display: flex;
  justify-content: space-around;
  align-items: center;
  padding: 16px;
  box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.1);
  z-index: 1000;
}
  .step-approver {
    font-size: 14px;
    color: #666;
    display: block;
    margin-bottom: 4px;
  }
.reject-btn {
  .step-time {
    font-size: 12px;
    color: #999;
    display: block;
  }
  .step-opinion {
    background: #f8f9fa;
    padding: 12px;
    border-radius: 8px;
    border-left: 4px solid #52c41a;
  }
  .opinion-label {
    font-size: 12px;
    color: #666;
    display: block;
    margin-bottom: 4px;
  }
  .opinion-content {
    font-size: 14px;
    color: #333;
    line-height: 1.5;
  }
  .approval-input {
    background: #fff;
    margin: 16px;
    border-radius: 12px;
    overflow: hidden;
  }
  .input-header {
    padding: 16px;
    border-bottom: 1px solid #f0f0f0;
    background: #f8f9fa;
  }
  .input-title {
    font-size: 16px;
    font-weight: 600;
    color: #333;
  }
  .input-content {
    padding: 16px;
  }
  .footer-actions {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    background: #fff;
    display: flex;
    justify-content: space-around;
    align-items: center;
    padding: 16px;
    box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.1);
    z-index: 1000;
  }
  .reject-btn {
    width: 120px;
    background: #ff4d4f;
    color: #fff;
@@ -503,47 +511,47 @@
    background: #52c41a;
    color: #fff;
  }
  /* é€‚配u-button样式 */
  :deep(.u-button) {
    border-radius: 6px;
  }
@keyframes pulse {
  0% {
    box-shadow: 0 0 0 0 rgba(24, 144, 255, 0.7);
  @keyframes pulse {
    0% {
      box-shadow: 0 0 0 0 rgba(24, 144, 255, 0.7);
    }
    70% {
      box-shadow: 0 0 0 10px rgba(24, 144, 255, 0);
    }
    100% {
      box-shadow: 0 0 0 0 rgba(24, 144, 255, 0);
    }
  }
  70% {
    box-shadow: 0 0 0 10px rgba(24, 144, 255, 0);
  .signature-section {
    background: #fff;
    padding: 12px 16px 16px;
    border-top: 1px solid #f0f0f0;
  }
  100% {
    box-shadow: 0 0 0 0 rgba(24, 144, 255, 0);
  .signature-header {
    margin-bottom: 8px;
  }
}
.signature-section {
  background: #fff;
  padding: 12px 16px 16px;
  border-top: 1px solid #f0f0f0;
}
.signature-header {
  margin-bottom: 8px;
}
.signature-title {
  font-size: 14px;
  font-weight: 600;
  color: #333;
}
.signature-box {
  width: 100%;
  height: 180px;
  background: #fff;
  border: 1px dashed #d9d9d9;
  border-radius: 8px;
  overflow: hidden;
}
.signature-actions {
  margin-top: 8px;
  display: flex;
  justify-content: flex-end;
}
  .signature-title {
    font-size: 14px;
    font-weight: 600;
    color: #333;
  }
  .signature-box {
    width: 100%;
    height: 180px;
    background: #fff;
    border: 1px dashed #d9d9d9;
    border-radius: 8px;
    overflow: hidden;
  }
  .signature-actions {
    margin-top: 8px;
    display: flex;
    justify-content: flex-end;
  }
</style>
src/pages/cooperativeOffice/collaborativeApproval/detail.vue
@@ -1,6 +1,6 @@
<template>
  <view class="account-detail">
    <PageHeader title="审批流程"
    <PageHeader :title="operationType === 'detail' ? '详情' : '审批流程'"
                @back="goBack" />
    <!-- è¡¨å•区域 -->
    <u-form ref="formRef"
@@ -8,38 +8,39 @@
            :rules="rules"
            :model="form"
            label-width="140rpx">
      <u-form-item prop="approveReason"
                   label="流程编号">
        <u-input v-model="form.approveId"
                 disabled
                 placeholder="自动编号" />
      </u-form-item>
      <u-form-item prop="approveReason"
                   :label="approveType === 5 ? '采购事由' : '申请事由'"
                   required>
        <u-input v-model="form.approveReason"
                 type="textarea"
                 rows="2"
                 auto-height
                 maxlength="200"
                 :placeholder="approveType === 5 ? '请输入采购事由' : '请输入申请事由'"
                 show-word-limit />
      </u-form-item>
      <u-form-item prop="approveDeptName"
                   label="申请部门"
                   required>
        <!-- <u-input v-model="form.approveDeptName"
      <template v-if="operationType !== 'detail'">
        <u-form-item prop="approveReason"
                     label="流程编号">
          <u-input v-model="form.approveId"
                   disabled
                   placeholder="自动编号" />
        </u-form-item>
        <u-form-item prop="approveReason"
                     :label="approveType === 5 ? '采购事由' : '申请事由'"
                     required>
          <u-input v-model="form.approveReason"
                   type="textarea"
                   rows="2"
                   auto-height
                   maxlength="200"
                   :placeholder="approveType === 5 ? '请输入采购事由' : '请输入申请事由'"
                   show-word-limit />
        </u-form-item>
        <u-form-item prop="approveDeptName"
                     label="申请部门"
                     required>
          <!-- <u-input v-model="form.approveDeptName"
                 placeholder="请选择申请部门" /> -->
        <u-input v-model="form.approveDeptName"
                 readonly
                 placeholder="请选择申请部门"
                 @click="showPicker = true" />
        <template #right>
          <up-icon name="arrow-right"
                   @click="showPicker = true"></up-icon>
        </template>
      </u-form-item>
      <u-form-item prop="approveUser"
          <u-input v-model="form.approveDeptName"
                   readonly
                   placeholder="请选择申请部门"
                   @click="showPicker = true" />
          <template #right>
            <up-icon name="arrow-right"
                     @click="showPicker = true"></up-icon>
          </template>
        </u-form-item>
        <!-- <u-form-item prop="approveUser"
                   label="申请人"
                   required>
        <u-input v-model="form.approveUserName"
@@ -57,141 +58,277 @@
          <up-icon name="arrow-right"
                   @click="showDatePicker"></up-icon>
        </template>
      </u-form-item>
      <!-- approveType=2 è¯·å‡ç›¸å…³å­—段 -->
      <template v-if="approveType === 2">
        <u-form-item prop="startDate"
                     label="开始时间"
      </u-form-item> -->
        <!-- approveType=2 è¯·å‡ç›¸å…³å­—段 -->
        <template v-if="approveType === 2">
          <u-form-item prop="startDate"
                       label="开始时间"
                       required>
            <u-input v-model="form.startDate"
                     readonly
                     placeholder="请假开始时间"
                     @click="showStartDatePicker" />
            <template #right>
              <up-icon name="arrow-right"
                       @click="showStartDatePicker"></up-icon>
            </template>
          </u-form-item>
          <u-form-item prop="endDate"
                       label="结束时间"
                       required>
            <u-input v-model="form.endDate"
                     readonly
                     placeholder="请假结束时间"
                     @click="showEndDatePicker" />
            <template #right>
              <up-icon name="arrow-right"
                       @click="showEndDatePicker"></up-icon>
            </template>
          </u-form-item>
        </template>
        <!-- approveType=3 å‡ºå·®ç›¸å…³å­—段 -->
        <u-form-item v-if="approveType === 3"
                     prop="location"
                     label="出差地点"
                     required>
          <u-input v-model="form.startDate"
                   readonly
                   placeholder="请假开始时间"
                   @click="showStartDatePicker" />
          <template #right>
            <up-icon name="arrow-right"
                     @click="showStartDatePicker"></up-icon>
          </template>
          <u-input v-model="form.location"
                   placeholder="请输入出差地点"
                   clearable />
        </u-form-item>
        <u-form-item prop="endDate"
                     label="结束时间"
        <!-- approveType=4 æŠ¥é”€ç›¸å…³å­—段 -->
        <u-form-item v-if="approveType === 4"
                     prop="price"
                     label="报销金额"
                     required>
          <u-input v-model="form.endDate"
                   readonly
                   placeholder="请假结束时间"
                   @click="showEndDatePicker" />
          <template #right>
            <up-icon name="arrow-right"
                     @click="showEndDatePicker"></up-icon>
          </template>
          <u-input v-model="form.price"
                   type="number"
                   placeholder="请输入报销金额"
                   clearable />
        </u-form-item>
      </template>
      <!-- approveType=3 å‡ºå·®ç›¸å…³å­—段 -->
      <u-form-item v-if="approveType === 3"
                   prop="location"
                   label="出差地点"
                   required>
        <u-input v-model="form.location"
                 placeholder="请输入出差地点"
                 clearable />
      </u-form-item>
      <!-- approveType=4 æŠ¥é”€ç›¸å…³å­—段 -->
      <u-form-item v-if="approveType === 4"
                   prop="price"
                   label="报销金额"
                   required>
        <u-input v-model="form.price"
                 type="number"
                 placeholder="请输入报销金额"
                 clearable />
      <!-- æŠ¥ä»·å®¡æ‰¹è¯¦æƒ… -->
      <view v-if="isQuotationApproval"
            style="margin: 20rpx 0;">
        <u-divider text="报价详情"
                   text-size="28rpx"
                   color="#2979ff"></u-divider>
        <u-skeleton :loading="quotationLoading"
                    rows="3"
                    animated>
          <view v-if="!currentQuotation || !currentQuotation.quotationNo"
                style="padding: 40rpx; text-align: center; color: #999;">
            æœªæŸ¥è¯¢åˆ°å¯¹åº”报价详情
          </view>
          <view v-else>
            <u-cell-group :border="false">
              <u-cell title="报价单号"
                      :value="currentQuotation.quotationNo"></u-cell>
              <u-cell title="客户名称"
                      :value="currentQuotation.customer"></u-cell>
              <u-cell title="业务员"
                      :value="currentQuotation.salesperson"></u-cell>
              <u-cell title="报价日期"
                      :value="currentQuotation.quotationDate"></u-cell>
              <u-cell title="有效期至"
                      :value="currentQuotation.validDate"></u-cell>
              <u-cell title="付款方式"
                      :value="currentQuotation.paymentMethod"></u-cell>
              <u-cell title="报价总额">
                <template #value>
                  <text style="font-size: 32rpx; color: #e6a23c; font-weight: bold;">
                    Â¥{{ Number(currentQuotation.totalAmount ?? 0).toFixed(2) }}
                  </text>
                </template>
              </u-cell>
            </u-cell-group>
            <view style="margin-top: 20rpx; padding: 0 30rpx;">
              <view style="font-size: 28rpx; font-weight: bold; margin-bottom: 10rpx;">产品明细</view>
              <view v-for="(item, index) in (currentQuotation.products || [])"
                    :key="index"
                    style="background: #f8f8f8; border-radius: 8rpx; padding: 20rpx; margin-bottom: 10rpx;">
                <view style="display: flex; justify-content: space-between;">
                  <text style="font-weight: bold;">{{ item.product }}</text>
                  <text style="color: #e6a23c;">Â¥{{ Number(item.unitPrice ?? 0).toFixed(2) }}</text>
                </view>
                <view style="font-size: 24rpx; color: #666; margin-top: 10rpx;">
                  è§„æ ¼: {{ item.specification }} | å•位: {{ item.unit }}
                </view>
              </view>
            </view>
            <view v-if="currentQuotation.remark"
                  style="margin-top: 20rpx; padding: 0 30rpx;">
              <view style="font-size: 28rpx; font-weight: bold;">备注</view>
              <view style="font-size: 26rpx; color: #666; margin-top: 10rpx;">{{ currentQuotation.remark }}</view>
            </view>
          </view>
        </u-skeleton>
      </view>
      <!-- é‡‡è´­å®¡æ‰¹è¯¦æƒ… -->
      <view v-if="isPurchaseApproval"
            style="margin: 20rpx 0;">
        <u-divider text="采购详情"
                   text-size="28rpx"
                   color="#2979ff"></u-divider>
        <u-skeleton :loading="purchaseLoading"
                    rows="3"
                    animated>
          <view v-if="!currentPurchase || !currentPurchase.purchaseContractNumber"
                style="padding: 40rpx; text-align: center; color: #999;">
            æœªæŸ¥è¯¢åˆ°å¯¹åº”采购详情
          </view>
          <view v-else>
            <u-cell-group :border="false">
              <u-cell title="采购合同号"
                      :value="currentPurchase.purchaseContractNumber"></u-cell>
              <u-cell title="供应商名称"
                      :value="currentPurchase.supplierName"></u-cell>
              <u-cell title="项目名称"
                      :value="currentPurchase.projectName"></u-cell>
              <u-cell title="销售合同号"
                      :value="currentPurchase.salesContractNo"></u-cell>
              <u-cell title="签订日期"
                      :value="currentPurchase.executionDate"></u-cell>
              <u-cell title="录入日期"
                      :value="currentPurchase.entryDate"></u-cell>
              <u-cell title="付款方式"
                      :value="currentPurchase.paymentMethod"></u-cell>
              <u-cell title="合同金额">
                <template #value>
                  <text style="font-size: 32rpx; color: #e6a23c; font-weight: bold;">
                    Â¥{{ Number(currentPurchase.contractAmount ?? 0).toFixed(2) }}
                  </text>
                </template>
              </u-cell>
            </u-cell-group>
            <view style="margin-top: 20rpx; padding: 0 30rpx;">
              <view style="font-size: 28rpx; font-weight: bold; margin-bottom: 10rpx;">产品明细</view>
              <view v-for="(item, index) in (currentPurchase.productData || [])"
                    :key="index"
                    style="background: #f8f8f8; border-radius: 8rpx; padding: 20rpx; margin-bottom: 10rpx;">
                <view style="display: flex; justify-content: space-between;">
                  <text style="font-weight: bold;">{{ item.productCategory }}</text>
                  <text style="color: #e6a23c;">Â¥{{ Number(item.taxInclusiveTotalPrice ?? 0).toFixed(2) }}</text>
                </view>
                <view style="font-size: 24rpx; color: #666; margin-top: 10rpx;">
                  è§„æ ¼: {{ item.specificationModel }} | æ•°é‡: {{ item.quantity }} {{ item.unit }}
                </view>
                <view style="font-size: 24rpx; color: #999; margin-top: 4rpx;">
                  å«ç¨Žå•ä»·: Â¥{{ Number(item.taxInclusiveUnitPrice ?? 0).toFixed(2) }}
                </view>
              </view>
            </view>
          </view>
        </u-skeleton>
      </view>
      <!-- å‘货审批详情 -->
      <view v-if="isDeliveryApproval"
            style="margin: 20rpx 0;">
        <u-divider text="发货详情"
                   text-size="28rpx"
                   color="#2979ff"></u-divider>
        <u-skeleton :loading="deliveryLoading"
                    rows="3"
                    animated>
          <view v-if="!currentDelivery || !currentDelivery.shippingInfo"
                style="padding: 40rpx; text-align: center; color: #999;">
            æœªæŸ¥è¯¢åˆ°å¯¹åº”发货详情
          </view>
          <view v-else>
            <u-cell-group :border="false">
              <u-cell title="销售订单"
                      :value="currentDelivery.shippingInfo.salesContractNo || '--'"></u-cell>
              <u-cell title="发货订单号"
                      :value="currentDelivery.shippingInfo.shippingNo || '--'"></u-cell>
              <u-cell title="客户名称"
                      :value="currentDelivery.shippingInfo.customerName || '--'"></u-cell>
              <u-cell title="发货类型"
                      :value="currentDelivery.shippingInfo.type || '--'"></u-cell>
              <u-cell title="发货日期"
                      :value="currentDelivery.shippingInfo.shippingDate || '--'"></u-cell>
              <u-cell title="审核状态"
                      :value="currentDelivery.shippingInfo.status || '--'"></u-cell>
              <u-cell title="发货车牌号"
                      :value="currentDelivery.shippingInfo.shippingCarNumber || '--'"></u-cell>
              <u-cell title="快递公司"
                      :value="currentDelivery.shippingInfo.expressCompany || '--'"></u-cell>
              <u-cell title="快递单号"
                      :value="currentDelivery.shippingInfo.expressNumber || '--'"></u-cell>
            </u-cell-group>
            <view style="margin-top: 20rpx; padding: 0 30rpx;">
              <view style="font-size: 28rpx; font-weight: bold; margin-bottom: 10rpx;">产品明细</view>
              <view v-for="(item, index) in deliveryProductList"
                    :key="index"
                    style="background: #f8f8f8; border-radius: 8rpx; padding: 20rpx; margin-bottom: 10rpx;">
                <view style="display: flex; justify-content: space-between;">
                  <text style="font-weight: bold;">{{ item.productName }}</text>
                  <text style="color: #2979ff;">数量: {{ item.deliveryQuantity }}</text>
                </view>
                <view style="font-size: 24rpx; color: #666; margin-top: 10rpx;">
                  è§„æ ¼: {{ item.specificationModel }}
                </view>
                <view v-if="item.batchNo"
                      style="font-size: 24rpx; color: #999; margin-top: 4rpx;">
                  æ‰¹å·: {{ item.batchNo }}
                </view>
              </view>
            </view>
            <view v-if="currentDelivery.shippingInfo.storageBlobVOs && currentDelivery.shippingInfo.storageBlobVOs.length"
                  style="margin-top: 20rpx; padding: 0 30rpx;">
              <view style="font-size: 28rpx; font-weight: bold; margin-bottom: 10rpx;">发货图片</view>
              <CommonUpload :model-value="currentDelivery.shippingInfo.storageBlobVOs"
                            disabled />
            </view>
          </view>
        </u-skeleton>
      </view>
      <u-form-item v-if="operationType !== 'detail'"
                   label="图片附件"
                   prop="storageBlobDTOS"
                   border-bottom>
        <CommonUpload v-model="form.storageBlobDTOS" />
      </u-form-item>
    </u-form>
    <!-- é€‰æ‹©å™¨å¼¹çª— -->
    <up-action-sheet :show="showPicker"
                     :actions="productOptions"
                     title="选择部门"
                     @select="onConfirm"
                     @close="showPicker = false" />
    <!-- æ—¥æœŸé€‰æ‹©å™¨ -->
    <up-popup :show="showDate"
              mode="bottom"
              @close="showDate = false">
      <up-datetime-picker :show="true"
                          v-model="currentDate"
                          @confirm="onDateConfirm"
                          @cancel="showDate = false"
                          mode="date" />
    </up-popup>
    <!-- è¯·å‡å¼€å§‹æ—¶é—´é€‰æ‹©å™¨ -->
    <up-popup :show="showStartDate"
              mode="bottom"
              @close="showStartDate = false">
      <up-datetime-picker :show="true"
                          v-model="startDateValue"
                          @confirm="onStartDateConfirm"
                          @cancel="showStartDate = false"
                          mode="date" />
    </up-popup>
    <!-- è¯·å‡ç»“束时间选择器 -->
    <up-popup :show="showEndDate"
              mode="bottom"
              @close="showEndDate = false">
      <up-datetime-picker :show="true"
                          v-model="endDateValue"
                          @confirm="onEndDateConfirm"
                          @cancel="showEndDate = false"
                          mode="date" />
    </up-popup>
    <!-- å®¡æ ¸æµç¨‹åŒºåŸŸ -->
    <view class="approval-process">
      <view class="approval-header">
        <text class="approval-title">审核流程</text>
        <text class="approval-desc">每个步骤只能选择一个审批人</text>
      </view>
      <view class="approval-steps">
        <view v-for="(step, stepIndex) in approverNodes"
              :key="stepIndex"
              class="approval-step">
          <view class="step-dot"></view>
          <view class="step-title">
            <text>审批人</text>
          </view>
          <view class="approver-container">
            <view v-if="step.nickName"
                  class="approver-item">
              <view class="approver-avatar">
                <text class="avatar-text">{{ step.nickName.charAt(0) }}</text>
                <view class="status-dot"></view>
              </view>
              <view class="approver-info">
                <text class="approver-name">{{ step.nickName }}</text>
              </view>
              <view class="delete-approver-btn"
                    @click="removeApprover(stepIndex)">×</view>
            </view>
            <view v-else
                  class="add-approver-btn"
                  @click="addApprover(stepIndex)">
              <view class="add-circle">+</view>
              <text class="add-label">选择审批人</text>
            </view>
          </view>
          <view class="step-line"
                v-if="stepIndex < approverNodes.length - 1"></view>
          <view class="delete-step-btn"
                v-if="approverNodes.length > 1"
                @click="removeApprovalStep(stepIndex)">删除节点</view>
        </view>
      </view>
      <view class="add-step-btn">
        <u-button icon="plus"
                  plain
                  type="primary"
                  style="width: 100%"
                  @click="addApprovalStep">新增节点</u-button>
      </view>
    </view>
    <template v-if="operationType !== 'detail'">
      <up-action-sheet :show="showPicker"
                       :actions="productOptions"
                       title="选择部门"
                       @select="onConfirm"
                       @close="showPicker = false" />
      <!-- æ—¥æœŸé€‰æ‹©å™¨ -->
      <up-popup :show="showDate"
                mode="bottom"
                @close="showDate = false">
        <up-datetime-picker :show="true"
                            v-model="currentDate"
                            @confirm="onDateConfirm"
                            @cancel="showDate = false"
                            mode="date" />
      </up-popup>
      <!-- è¯·å‡å¼€å§‹æ—¶é—´é€‰æ‹©å™¨ -->
      <up-popup :show="showStartDate"
                mode="bottom"
                @close="showStartDate = false">
        <up-datetime-picker :show="true"
                            v-model="startDateValue"
                            @confirm="onStartDateConfirm"
                            @cancel="showStartDate = false"
                            mode="date" />
      </up-popup>
      <!-- è¯·å‡ç»“束时间选择器 -->
      <up-popup :show="showEndDate"
                mode="bottom"
                @close="showEndDate = false">
        <up-datetime-picker :show="true"
                            v-model="endDateValue"
                            @confirm="onEndDateConfirm"
                            @cancel="showEndDate = false"
                            mode="date" />
      </up-popup>
    </template>
    <!-- åº•部按钮 -->
    <view class="footer-btns">
    <view class="footer-btns"
          v-if="operationType !== 'detail'">
      <u-button class="cancel-btn"
                @click="goBack">取消</u-button>
      <u-button class="save-btn"
@@ -201,8 +338,17 @@
</template>
<script setup>
  import { ref, onMounted, onUnmounted, reactive, toRefs } from "vue";
  import {
    ref,
    onMounted,
    onUnmounted,
    reactive,
    toRefs,
    computed,
    watch,
  } from "vue";
  import PageHeader from "@/components/PageHeader.vue";
  import CommonUpload from "@/components/CommonUpload.vue";
  import useUserStore from "@/store/modules/user";
  import { formatDateToYMD } from "@/utils/ruoyi";
  import {
@@ -210,14 +356,16 @@
    approveProcessGetInfo,
    approveProcessAdd,
    approveProcessUpdate,
    getDeliveryDetailByShippingNo,
  } from "@/api/collaborativeApproval/approvalProcess";
  import { getQuotationList } from "@/api/salesManagement/salesQuotation";
  import { getPurchaseByCode } from "@/api/procurementManagement/procurementLedger";
  const showToast = message => {
    uni.showToast({
      title: message,
      icon: "none",
    });
  };
  import { userListNoPageByTenantId } from "@/api/system/user";
  const data = reactive({
    form: {
@@ -229,8 +377,7 @@
      approveDeptId: "",
      approveReason: "",
      checkResult: "",
      tempFileIds: [],
      approverList: [], // æ–°å¢žå­—段,存储所有节点的审批人id
      storageBlobDTOS: [],
      startDate: "",
      endDate: "",
      location: "",
@@ -258,8 +405,6 @@
  const productOptions = ref([]);
  const operationType = ref("");
  const currentApproveStatus = ref("");
  const approverNodes = ref([]);
  const userList = ref([]);
  const formRef = ref(null);
  const message = ref("");
  const showDate = ref(false);
@@ -270,6 +415,19 @@
  const endDateValue = ref(Date.now());
  const userStore = useUserStore();
  const approveType = ref(0);
  const isInitialLoading = ref(false);
  const quotationLoading = ref(false);
  const currentQuotation = ref({});
  const purchaseLoading = ref(false);
  const currentPurchase = ref({});
  const deliveryLoading = ref(false);
  const currentDelivery = ref({});
  const deliveryProductList = ref([]);
  const isQuotationApproval = computed(() => Number(approveType.value) === 6);
  const isPurchaseApproval = computed(() => Number(approveType.value) === 5);
  const isDeliveryApproval = computed(() => Number(approveType.value) === 7);
  const getProductOptions = () => {
    getDept().then(res => {
@@ -279,20 +437,133 @@
      }));
    });
  };
  const fileList = ref([]);
  let nextApproverId = 2;
  const getCurrentinfo = () => {
    userStore.getInfo().then(res => {
      form.value.approveDeptId = res.user.tenantId;
      console.log(res.user.tenantId, "res.user.tenantId");
    });
  };
  // æ˜¾ç¤ºæ—¥æœŸé€‰æ‹©å™¨
  const showDatePicker = () => {
    showDate.value = true;
  };
  // ç¡®è®¤æ—¥æœŸé€‰æ‹©
  const onDateConfirm = e => {
    form.value.approveTime = formatDateToYMD(e.value);
    currentDate.value = formatDateToYMD(e.value);
    showDate.value = false;
  };
  // æ˜¾ç¤ºè¯·å‡å¼€å§‹æ—¶é—´é€‰æ‹©å™¨
  const showStartDatePicker = () => {
    showStartDate.value = true;
  };
  // ç¡®è®¤è¯·å‡å¼€å§‹æ—¶é—´é€‰æ‹©
  const onStartDateConfirm = e => {
    form.value.startDate = formatDateToYMD(e.value);
    showStartDate.value = false;
  };
  const showEndDatePicker = () => {
    showEndDate.value = true;
  };
  // ç¡®è®¤è¯·å‡ç»“束时间选择
  const onEndDateConfirm = e => {
    form.value.endDate = formatDateToYMD(e.value);
    showEndDate.value = false;
  };
  const fetchDetailData = async row => {
    // æŠ¥ä»·å®¡æ‰¹
    if (isQuotationApproval.value) {
      const quotationNo = row?.approveReason;
      if (quotationNo) {
        quotationLoading.value = true;
        getQuotationList({ quotationNo })
          .then(res => {
            const records = res?.data?.records || [];
            currentQuotation.value = records[0] || {};
          })
          .finally(() => {
            quotationLoading.value = false;
          });
      }
    }
    // é‡‡è´­å®¡æ‰¹
    if (isPurchaseApproval.value) {
      const purchaseContractNumber = row?.approveReason;
      if (purchaseContractNumber) {
        purchaseLoading.value = true;
        getPurchaseByCode({ purchaseContractNumber })
          .then(res => {
            currentPurchase.value = res;
          })
          .catch(err => {
            console.error("查询采购详情失败:", err);
          })
          .finally(() => {
            purchaseLoading.value = false;
          });
      }
    }
    // å‘货审批
    if (isDeliveryApproval.value) {
      const deliveryNo = row?.approveReason;
      if (deliveryNo) {
        deliveryLoading.value = true;
        currentDelivery.value = {};
        deliveryProductList.value = [];
        getDeliveryDetailByShippingNo({ shippingNo: deliveryNo })
          .then(res => {
            const detailData = res?.data || res || {};
            currentDelivery.value = detailData;
            deliveryProductList.value =
              detailData.shippingProductDetailDtoList || [];
          })
          .catch(err => {
            console.error("查询发货详情失败:", err);
          })
          .finally(() => {
            deliveryLoading.value = false;
          });
      }
    }
  };
  // ç›‘听审批事由变化,如果是特定审批类型则尝试获取详情
  watch(
    () => form.value.approveReason,
    newVal => {
      if (isInitialLoading.value) return;
      if (
        newVal &&
        (isQuotationApproval.value ||
          isPurchaseApproval.value ||
          isDeliveryApproval.value)
      ) {
        // å»¶è¿Ÿä¸€ä¼šå†è¯·æ±‚,避免输入过程中频繁触发
        debounceFetchDetail();
      }
    }
  );
  let timer = null;
  const debounceFetchDetail = () => {
    if (timer) clearTimeout(timer);
    timer = setTimeout(() => {
      fetchDetailData(form.value);
    }, 800);
  };
  onMounted(async () => {
    try {
      getProductOptions();
      userListNoPageByTenantId().then(res => {
        userList.value = res.data;
      });
      form.value.approveUser = userStore.id;
      form.value.approveUserName = userStore.nickName;
      form.value.approveTime = getCurrentDate();
@@ -302,57 +573,39 @@
      approveType.value = uni.getStorageSync("approveType") || 0;
      // å¦‚果是编辑模式,从本地存储获取数据
      if (operationType.value === "edit") {
      if (operationType.value === "edit" || operationType.value === "detail") {
        const storedData = uni.getStorageSync("invoiceLedgerEditRow");
        if (storedData) {
          const row = JSON.parse(storedData);
          fileList.value = row.commonFileList || [];
          form.value.tempFileIds = fileList.value.map(file => file.id);
          currentApproveStatus.value = row.approveStatus;
          approveProcessGetInfo({ id: row.approveId, approveReason: "1" }).then(
            res => {
          isInitialLoading.value = true;
          approveProcessGetInfo({ id: row.approveId, approveReason: "1" })
            .then(res => {
              form.value = { ...res.data };
              // åæ˜¾å®¡æ‰¹äºº
              if (res.data && res.data.approveUserIds) {
                const userIds = res.data.approveUserIds.split(",");
                approverNodes.value = userIds.map((userId, idx) => {
                  const userIdNum = parseInt(userId.trim());
                  // ä»ŽuserList中找到对应的用户信息
                  const userInfo = userList.value.find(
                    user => user.userId === userIdNum
                  );
                  return {
                    id: idx + 1,
                    userId: userIdNum,
                    nickName: userInfo ? userInfo.nickName : null,
                  };
                });
                nextApproverId = userIds.length + 1;
              } else {
                // æ–°å¢žæ¨¡å¼ï¼Œåˆå§‹åŒ–一个空的审批节点
                approverNodes.value = [{ id: 1, userId: null, nickName: null }];
                nextApproverId = 2;
              // è®¾ç½®å›¾ç‰‡åˆ—表显示
              const fileData =
                res.data.storageBlobVOS || res.data.commonFileList || [];
              if (fileData.length > 0) {
                form.value.storageBlobDTOS = fileData;
              }
            }
          );
              // èŽ·å–é¢å¤–è¯¦æƒ…
              fetchDetailData(res.data);
            })
            .finally(() => {
              // å»¶è¿Ÿä¸€ä¼šé‡ç½®ï¼Œç¡®ä¿ watch ä¸ä¼šè¢«è§¦å‘
              setTimeout(() => {
                isInitialLoading.value = false;
              }, 100);
            });
        }
      } else {
        // æ–°å¢žæ¨¡å¼ï¼Œåˆå§‹åŒ–一个空的审批节点
        approverNodes.value = [{ id: 1, userId: null }];
      }
      // ç›‘听联系人选择事件
      uni.$on("selectContact", handleSelectContact);
    } catch (error) {
      console.error("获取部门数据失败:", error);
      console.error("获取数据失败:", error);
    }
  });
  onUnmounted(() => {
    // ç§»é™¤äº‹ä»¶ç›‘听
    uni.$off("selectContact", handleSelectContact);
  });
  onUnmounted(() => {});
  const onConfirm = item => {
    // è®¾ç½®é€‰ä¸­çš„部门
@@ -375,13 +628,6 @@
  };
  const submitForm = () => {
    // æ£€æŸ¥æ¯ä¸ªå®¡æ‰¹æ­¥éª¤æ˜¯å¦éƒ½æœ‰å®¡æ‰¹äºº
    const hasEmptyStep = approverNodes.value.some(step => !step.nickName);
    if (hasEmptyStep) {
      showToast("请为每个审批步骤选择审批人");
      return;
    }
    // æ‰‹åŠ¨æ£€æŸ¥å¿…å¡«å­—æ®µï¼Œé˜²æ­¢å› æ•°æ®ç±»åž‹é—®é¢˜å¯¼è‡´çš„æ ¡éªŒå¤±è´¥
    if (!form.value.approveReason || !form.value.approveReason.trim()) {
      showToast("请输入申请事由");
@@ -406,26 +652,8 @@
      .then(valid => {
        if (valid) {
          // è¡¨å•校验通过,可以提交数据
          // æ”¶é›†æ‰€æœ‰èŠ‚ç‚¹çš„å®¡æ‰¹äººid
          console.log("approverNodes---", approverNodes.value);
          form.value.approveUserIds = approverNodes.value
            .map(node => node.userId)
            .join(",");
          form.value.approveType = approveType.value;
          form.value.approveDeptId = Number(form.value.approveDeptId);
          // const submitForm = {
          //   approveDeptId: form.value.approveDeptId,
          //   approveDeptName: form.value.approveDeptName,
          //   approveReason: form.value.approveReason,
          //   approveTime: form.value.approveTime,
          //   approveType: form.value.approveType,
          //   approveUser: form.value.approveUser,
          //   approveUserIds: form.value.approveUserIds,
          //   endDate: form.value.endDate,
          //   startDate: form.value.startDate,
          // };
          // console.log("form.value---", form.value);
          // console.log("submitForm", submitForm);
          if (operationType.value === "add" || currentApproveStatus.value == 3) {
            approveProcessAdd(form.value).then(res => {
@@ -461,77 +689,6 @@
      });
  };
  // å¤„理联系人选择结果
  const handleSelectContact = data => {
    const { stepIndex, contact } = data;
    // å°†é€‰ä¸­çš„联系人设置为对应审批步骤的审批人
    approverNodes.value[stepIndex].userId = contact.userId;
    approverNodes.value[stepIndex].nickName = contact.nickName;
  };
  const addApprover = stepIndex => {
    // è·³è½¬åˆ°è”系人选择页面
    uni.setStorageSync("stepIndex", stepIndex);
    uni.navigateTo({
      url: "/pages/cooperativeOffice/collaborativeApproval/contactSelect",
    });
  };
  const addApprovalStep = () => {
    // æ·»åŠ æ–°çš„å®¡æ‰¹æ­¥éª¤
    approverNodes.value.push({ userId: null, nickName: null });
  };
  const removeApprover = stepIndex => {
    // ç§»é™¤å®¡æ‰¹äºº
    approverNodes.value[stepIndex].userId = null;
    approverNodes.value[stepIndex].nickName = null;
  };
  const removeApprovalStep = stepIndex => {
    // ç¡®ä¿è‡³å°‘保留一个审批步骤
    if (approverNodes.value.length > 1) {
      approverNodes.value.splice(stepIndex, 1);
    } else {
      uni.showToast({
        title: "至少需要一个审批步骤",
        icon: "none",
      });
    }
  };
  // æ˜¾ç¤ºæ—¥æœŸé€‰æ‹©å™¨
  const showDatePicker = () => {
    showDate.value = true;
  };
  // ç¡®è®¤æ—¥æœŸé€‰æ‹©
  const onDateConfirm = e => {
    form.value.approveTime = formatDateToYMD(e.value);
    currentDate.value = formatDateToYMD(e.value);
    showDate.value = false;
  };
  // æ˜¾ç¤ºè¯·å‡å¼€å§‹æ—¶é—´é€‰æ‹©å™¨
  const showStartDatePicker = () => {
    showStartDate.value = true;
  };
  // ç¡®è®¤è¯·å‡å¼€å§‹æ—¶é—´é€‰æ‹©
  const onStartDateConfirm = e => {
    form.value.startDate = formatDateToYMD(e.value);
    showStartDate.value = false;
  };
  const showEndDatePicker = () => {
    showEndDate.value = true;
  };
  // ç¡®è®¤è¯·å‡ç»“束时间选择
  const onEndDateConfirm = e => {
    form.value.endDate = formatDateToYMD(e.value);
    showEndDate.value = false;
  };
  // èŽ·å–å½“å‰æ—¥æœŸå¹¶æ ¼å¼åŒ–ä¸º YYYY-MM-DD
  function getCurrentDate() {
    const today = new Date();
@@ -544,238 +701,8 @@
<style scoped lang="scss">
  @import "@/static/scss/form-common.scss";
  .approval-process {
    background: #fff;
    margin: 16px;
    border-radius: 16px;
    padding: 16px;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
  }
  .approval-header {
    margin-bottom: 16px;
  }
  .approval-title {
    font-size: 16px;
    font-weight: 600;
    color: #333;
    display: block;
    margin-bottom: 4px;
  }
  .approval-desc {
    font-size: 12px;
    color: #999;
  }
  /* æ ·å¼å¢žå¼ºä¸ºâ€œç®€æ´å°åœ†åœˆé£Žæ ¼â€ */
  .approval-steps {
    padding-left: 22px;
    position: relative;
    &::before {
      content: "";
      position: absolute;
      left: 11px;
      top: 40px;
      bottom: 40px;
      width: 2px;
      background: linear-gradient(
        to bottom,
        #e6f7ff 0%,
        #bae7ff 50%,
        #91d5ff 100%
      );
      border-radius: 1px;
    }
  }
  .approval-step {
    position: relative;
    margin-bottom: 24px;
    &::before {
      content: "";
      position: absolute;
      left: -18px;
      top: 14px; // ä»Ž 8px è°ƒæ•´ä¸º 14px,与文字中心对齐
      width: 12px;
      height: 12px;
      background: #fff;
      border: 3px solid #006cfb;
      border-radius: 50%;
      z-index: 2;
      box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
    }
  }
  .step-title {
    top: 12px;
    margin-bottom: 12px;
    position: relative;
    margin-left: 6px;
  }
  .step-title text {
    font-size: 14px;
    color: #666;
    background: #f0f0f0;
    padding: 4px 12px;
    border-radius: 12px;
    position: relative;
    line-height: 1.4; // ç¡®ä¿æ–‡å­—行高一致
  }
  .approver-item {
    display: flex;
    align-items: center;
    background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);
    border-radius: 16px;
    padding: 16px;
    gap: 12px;
    position: relative;
    border: 1px solid #e6f7ff;
    box-shadow: 0 4px 12px rgba(0, 108, 251, 0.08);
    transition: all 0.3s ease;
  }
  .approver-avatar {
    width: 48px;
    height: 48px;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    position: relative;
    box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
  }
  .avatar-text {
    color: #fff;
    font-size: 18px;
    font-weight: 600;
    text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
  }
  .approver-info {
    flex: 1;
    position: relative;
  }
  .approver-name {
    display: block;
    font-size: 16px;
    color: #333;
    font-weight: 500;
    position: relative;
  }
  .approver-dept {
    font-size: 12px;
    color: #999;
    background: rgba(0, 108, 251, 0.05);
    padding: 2px 8px;
    border-radius: 8px;
    display: inline-block;
    position: relative;
    &::before {
      content: "";
      position: absolute;
      left: 4px;
      top: 50%;
      transform: translateY(-50%);
      width: 2px;
      height: 2px;
      background: #006cfb;
      border-radius: 50%;
    }
  }
  .delete-approver-btn {
    font-size: 16px;
    color: #ff4d4f;
    background: linear-gradient(
      135deg,
      rgba(255, 77, 79, 0.1) 0%,
      rgba(255, 77, 79, 0.05) 100%
    );
    width: 28px;
    height: 28px;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    transition: all 0.3s ease;
    position: relative;
  }
  .add-approver-btn {
    display: flex;
    align-items: center;
    justify-content: center;
    background: linear-gradient(135deg, #f0f8ff 0%, #e6f7ff 100%);
    border: 2px dashed #006cfb;
    border-radius: 16px;
    padding: 20px;
    color: #006cfb;
    font-size: 14px;
    position: relative;
    transition: all 0.3s ease;
    &::before {
      content: "";
      position: absolute;
      left: 50%;
      top: 50%;
      transform: translate(-50%, -50%);
      width: 32px;
      height: 32px;
      border: 2px solid #006cfb;
      border-radius: 50%;
      opacity: 0;
      transition: all 0.3s ease;
    }
  }
  .delete-step-btn {
    color: #ff4d4f;
    font-size: 12px;
    background: linear-gradient(
      135deg,
      rgba(255, 77, 79, 0.1) 0%,
      rgba(255, 77, 79, 0.05) 100%
    );
    padding: 6px 12px;
    border-radius: 12px;
    display: inline-block;
    position: relative;
    transition: all 0.3s ease;
    &::before {
      content: "";
      position: absolute;
      left: 6px;
      top: 50%;
      transform: translateY(-50%);
      width: 4px;
      height: 4px;
      background: #ff4d4f;
      border-radius: 50%;
    }
  }
  .step-line {
    display: none; // éšè—åŽŸæ¥çš„çº¿æ¡ï¼Œä½¿ç”¨ä¼ªå…ƒç´ ä»£æ›¿
  }
  .add-step-btn {
    display: flex;
    align-items: center;
    justify-content: center;
  .account-detail {
    background-color: #fff;
  }
  .footer-btns {
    position: fixed;
@@ -809,121 +736,5 @@
    background: linear-gradient(140deg, #00baff 0%, #006cfb 100%);
    box-shadow: 0 0.25rem 0.625rem 0 rgba(3, 88, 185, 0.2);
    border-radius: 2.5rem 2.5rem 2.5rem 2.5rem;
  }
  // åŠ¨ç”»å®šä¹‰
  @keyframes pulse {
    0% {
      transform: scale(1);
      opacity: 1;
    }
    50% {
      transform: scale(1.2);
      opacity: 0.7;
    }
    100% {
      transform: scale(1);
      opacity: 1;
    }
  }
  @keyframes rotate {
    0% {
      transform: rotate(0deg);
    }
    100% {
      transform: rotate(360deg);
    }
  }
  @keyframes ripple {
    0% {
      transform: translate(-50%, -50%) scale(0.8);
      opacity: 1;
    }
    100% {
      transform: translate(-50%, -50%) scale(1.6);
      opacity: 0;
    }
  }
  /* å¦‚果已有 .step-line,这里更精准定位到左侧与小圆点对齐 */
  .step-line {
    position: absolute;
    left: 4px;
    top: 48px;
    width: 2px;
    height: calc(100% - 48px);
    background: #e5e7eb;
  }
  .approver-container {
    display: flex;
    align-items: center;
    background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);
    border-radius: 16px;
    gap: 12px;
    padding: 10px 0;
    background: transparent;
    border: none;
    box-shadow: none;
  }
  .approver-item {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 8px 10px;
    background: transparent;
    border: none;
    box-shadow: none;
    border-radius: 0;
  }
  .approver-avatar {
    position: relative;
    width: 40px;
    height: 40px;
    border-radius: 50%;
    background: #f3f4f6;
    border: 2px solid #e5e7eb;
    display: flex;
    align-items: center;
    justify-content: center;
    animation: none; /* ç¦ç”¨æ—‹è½¬ç­‰åŠ¨ç”»ï¼Œå›žå½’ç®€æ´ */
  }
  .avatar-text {
    font-size: 14px;
    color: #374151;
    font-weight: 600;
  }
  .add-approver-btn {
    display: flex;
    align-items: center;
    gap: 8px;
    background: transparent;
    border: none;
    box-shadow: none;
    padding: 0;
  }
  .add-approver-btn .add-circle {
    width: 40px;
    height: 40px;
    border: 2px dashed #a0aec0;
    border-radius: 50%;
    color: #6b7280;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 22px;
    line-height: 1;
  }
  .add-approver-btn .add-label {
    color: #3b82f6;
    font-size: 14px;
  }
</style>
src/pages/cooperativeOffice/collaborativeApproval/index.vue
@@ -97,13 +97,20 @@
              </view>
              <view class="detail-row">
                <view class="actions">
                  <!-- <u-button type="primary"
                  <u-button type="primary"
                            size="small"
                            class="action-btn edit"
                            :disabled="item.approveStatus == 2 || item.approveStatus == 1 || item.approveStatus == 4 || item.approveStatus == 8"
                            v-if="!(item.approveStatus == 2 || item.approveStatus == 1 || item.approveStatus == 4 || item.approveStatus == 8 || item.approveType == 5 || item.approveType == 6 || item.approveType == 7)"
                            @click="handleItemClick(item)">
                    ç¼–辑
                  </u-button> -->
                  </u-button>
                  <u-button type="info"
                            v-if="item.approveType == 5 || item.approveType == 6 || item.approveType == 7"
                            size="small"
                            class="action-btn detail"
                            @click="handleDetailClick(item)">
                    è¯¦æƒ…
                  </u-button>
                  <u-button type="success"
                            size="small"
                            class="action-btn approve"
@@ -123,13 +130,13 @@
      <text>暂无审批数据</text>
    </view>
    <!-- æµ®åŠ¨æ“ä½œæŒ‰é’® -->
    <!-- <view class="fab-button"
    <view class="fab-button"
          v-if="props.approveType != 5 && props.approveType != 6 && props.approveType != 7"
          @click="handleAdd">
      <up-icon name="plus"
               size="24"
               color="#ffffff"></up-icon>
    </view> -->
    </view>
  </view>
</template>
@@ -262,6 +269,17 @@
    });
  };
  // æŸ¥çœ‹è¯¦æƒ…
  const handleDetailClick = item => {
    uni.setStorageSync("invoiceLedgerEditRow", JSON.stringify(item));
    uni.setStorageSync("operationType", "detail");
    uni.setStorageSync("approveId", item.approveId);
    uni.setStorageSync("approveType", props.approveType);
    uni.navigateTo({
      url: "/pages/cooperativeOffice/collaborativeApproval/detail",
    });
  };
  // æ·»åŠ æ–°è®°å½•
  const handleAdd = () => {
    uni.setStorageSync("operationType", "add");
src/pages/equipmentManagement/repair/add.vue
@@ -69,6 +69,20 @@
                   placeholder="请输入报修人"
                   clearable />
        </u-form-item>
        <u-form-item label="维修人"
                     prop="maintenanceName"
                     border-bottom>
          <u-input v-model="form.maintenanceName"
                   placeholder="请输入维修人"
                   clearable />
        </u-form-item>
        <u-form-item label="维修项目"
                     prop="machineryCategory"
                     border-bottom>
          <u-input v-model="form.machineryCategory"
                   placeholder="请输入维修项目"
                   clearable />
        </u-form-item>
        <u-form-item label="故障现象"
                     prop="remark"
                     required
@@ -79,6 +93,11 @@
                      clearable
                      count
                      maxlength="200" />
        </u-form-item>
        <u-form-item label="图片附件"
                     prop="storageBlobDTOs"
                     border-bottom>
          <CommonUpload v-model="form.storageBlobDTOs" />
        </u-form-item>
      </u-cell-group>
      <!-- æäº¤æŒ‰é’® -->
@@ -108,8 +127,9 @@
<script setup>
  import { ref, computed, onMounted, onUnmounted } from "vue";
  import { onShow } from "@dcloudio/uni-app";
  import { onShow, onLoad } from "@dcloudio/uni-app";
  import PageHeader from "@/components/PageHeader.vue";
  import CommonUpload from "@/components/CommonUpload.vue";
  import { getDeviceLedger } from "@/api/equipmentManagement/ledger";
  import {
    addRepair,
@@ -132,10 +152,18 @@
  // è¡¨å•引用
  const formRef = ref(null);
  const operationType = ref("add");
  const repairId = ref("");
  const loading = ref(false);
  const showDevice = ref(false);
  const showDate = ref(false);
  const pickerDateValue = ref(Date.now());
  onLoad(options => {
    if (options.id) {
      repairId.value = options.id;
    }
    getPageParams();
  });
  // è®¾å¤‡é€‰é¡¹
  const deviceOptions = ref([]);
@@ -169,7 +197,10 @@
    deviceModel: undefined, // è§„格型号
    repairTime: dayjs().format("YYYY-MM-DD"), // æŠ¥ä¿®æ—¥æœŸ
    repairName: undefined, // æŠ¥ä¿®äºº
    maintenanceName: undefined, // ç»´ä¿®äºº
    machineryCategory: undefined, // ç»´ä¿®é¡¹ç›®
    remark: undefined, // æ•…障现象
    storageBlobDTOs: [], // å›¾ç‰‡é™„ä»¶
  });
  // æŠ¥ä¿®çŠ¶æ€é€‰é¡¹
@@ -221,7 +252,10 @@
          form.value.deviceModel = data.deviceModel;
          form.value.repairTime = dayjs(data.repairTime).format("YYYY-MM-DD");
          form.value.repairName = data.repairName;
          form.value.maintenanceName = data.maintenanceName;
          form.value.machineryCategory = data.machineryCategory;
          form.value.remark = data.remark;
          form.value.storageBlobDTOs = data.storageBlobVOs || [];
          repairStatusText.value =
            repairStatusOptions.value.find(item => item.value == data.status)
              ?.name || "";
@@ -328,14 +362,12 @@
  };
  onShow(() => {
    // é¡µé¢æ˜¾ç¤ºæ—¶èŽ·å–å‚æ•°
    getPageParams();
    // é¡µé¢æ˜¾ç¤ºæ—¶é€»è¾‘
  });
  onMounted(() => {
    // é¡µé¢åŠ è½½æ—¶èŽ·å–è®¾å¤‡åˆ—è¡¨å’Œå‚æ•°
    // é¡µé¢åŠ è½½æ—¶èŽ·å–è®¾å¤‡åˆ—è¡¨
    loadDeviceName();
    getPageParams();
  });
  // ç»„件卸载时清理定时器
@@ -375,7 +407,6 @@
      // å‡†å¤‡æäº¤æ•°æ®
      const submitData = { ...form.value };
      const { code } = id
        ? await editRepair({ id: id, ...submitData })
        : await addRepair(submitData);
@@ -396,21 +427,15 @@
  // è¿”回上一页
  const goBack = () => {
    uni.removeStorageSync("repairId");
    uni.navigateBack();
  };
  // èŽ·å–é¡µé¢å‚æ•°
  const getPageParams = () => {
    // ä½¿ç”¨uni.getStorageSync获取id
    const id = uni.getStorageSync("repairId");
    // æ ¹æ®æ˜¯å¦æœ‰id参数来判断是新增还是编辑
    if (id) {
    if (repairId.value) {
      // ç¼–辑模式,获取详情
      loadForm(id);
      // å¯é€‰ï¼šèŽ·å–åŽæ¸…é™¤å­˜å‚¨çš„id,避免影响后续操作
      uni.removeStorageSync("repairId");
      loadForm(repairId.value);
    } else {
      // æ–°å¢žæ¨¡å¼
      loadForm();
@@ -419,9 +444,7 @@
  // èŽ·å–é¡µé¢ID
  const getPageId = () => {
    // ä½¿ç”¨uni.getStorageSync获取id
    const id = uni.getStorageSync("repairId");
    return id;
    return repairId.value;
  };
</script>
src/pages/equipmentManagement/repair/index.vue
@@ -58,12 +58,16 @@
              <text class="detail-value">{{ item.repairName || '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">故障现象</text>
              <text class="detail-value">{{ item.remark || '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">维修人</text>
              <text class="detail-value">{{ item.maintenanceName || '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">维修项目</text>
              <text class="detail-value">{{ item.machineryCategory || '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">故障现象</text>
              <text class="detail-value">{{ item.remark || '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">维修结果</text>
@@ -208,9 +212,9 @@
  const edit = id => {
    if (!id) return;
    // ä½¿ç”¨uni.setStorageSync存储id
    uni.setStorageSync("repairId", id);
    // uni.setStorageSync("repairId", id);
    uni.navigateTo({
      url: "/pages/equipmentManagement/repair/add",
      url: "/pages/equipmentManagement/repair/add?id=" + id,
    });
  };
src/pages/equipmentManagement/upkeep/add.vue
@@ -1,393 +1,444 @@
<template>
    <view class="upkeep-add">
        <!-- ä½¿ç”¨é€šç”¨é¡µé¢å¤´éƒ¨ç»„ä»¶ -->
        <PageHeader :title="operationType === 'edit' ? '编辑保养计划' : '新增保养计划'" @back="goBack" />
        <!-- è¡¨å•内容 -->
        <u-form ref="formRef" :model="form" :rules="formRules" label-width="110px">
            <!-- åŸºæœ¬ä¿¡æ¯ -->
            <u-form-item label="设备名称" prop="deviceNameText" required border-bottom>
                <u-input
                    v-model="form.deviceNameText"
                    placeholder="请选择设备名称"
                    readonly
                    @click="showDevicePicker"
                    clearable
                />
                <template #right>
                    <u-icon name="scan" @click="startScan" class="scan-icon" />
                </template>
            </u-form-item>
            <u-form-item label="规格型号" prop="deviceModel" border-bottom>
                <u-input
                    v-model="form.deviceModel"
                    placeholder="请输入规格型号"
                    readonly
                    clearable
                />
            </u-form-item>
            <u-form-item label="计划保养日期" prop="maintenancePlanTime" required border-bottom>
                <u-input
                    v-model="form.maintenancePlanTime"
                    placeholder="请选择计划保养日期"
                    readonly
                    @click="showDatePicker"
                    clearable
                />
                <template #right>
                    <u-icon name="arrow-right" @click="showDatePicker" />
                </template>
            </u-form-item>
            <!-- æäº¤æŒ‰é’® -->
            <view class="footer-btns">
                <u-button class="cancel-btn" @click="goBack">取消</u-button>
                <u-button class="save-btn" @click="sendForm" :loading="loading">保存</u-button>
            </view>
        </u-form>
        <!-- è®¾å¤‡é€‰æ‹©å™¨ -->
        <up-action-sheet
            :show="showDevice"
            :actions="deviceActions"
            title="选择设备"
            @select="onDeviceConfirm"
            @close="showDevice = false"
        />
<up-datetime-picker
            :show="showDate"
            v-model="pickerDateValue"
            @confirm="onDateConfirm"
            @cancel="showDate = false"
            mode="date"
        />
    </view>
  <view class="upkeep-add">
    <!-- ä½¿ç”¨é€šç”¨é¡µé¢å¤´éƒ¨ç»„ä»¶ -->
    <PageHeader :title="operationType === 'edit' ? '编辑保养计划' : '新增保养计划'"
                @back="goBack" />
    <!-- è¡¨å•内容 -->
    <u-form ref="formRef"
            :model="form"
            :rules="formRules"
            label-width="110px">
      <!-- åŸºæœ¬ä¿¡æ¯ -->
      <u-form-item label="设备名称"
                   prop="deviceNameText"
                   required
                   border-bottom>
        <u-input v-model="form.deviceNameText"
                 placeholder="请选择设备名称"
                 readonly
                 @click="showDevicePicker"
                 clearable />
        <template #right>
          <u-icon name="scan"
                  @click="startScan"
                  class="scan-icon" />
        </template>
      </u-form-item>
      <u-form-item label="规格型号"
                   prop="deviceModel"
                   border-bottom>
        <u-input v-model="form.deviceModel"
                 placeholder="请输入规格型号"
                 readonly
                 clearable />
      </u-form-item>
      <u-form-item label="计划保养日期"
                   prop="maintenancePlanTime"
                   required
                   border-bottom>
        <u-input v-model="form.maintenancePlanTime"
                 placeholder="请选择计划保养日期"
                 readonly
                 @click="showDatePicker"
                 clearable />
        <template #right>
          <u-icon name="arrow-right"
                  @click="showDatePicker" />
        </template>
      </u-form-item>
      <u-form-item label="保养人"
                   prop="maintenancePerson"
                   border-bottom>
        <u-input v-model="form.maintenancePerson"
                 placeholder="请输入保养人"
                 clearable />
      </u-form-item>
      <u-form-item label="保养项目"
                   prop="machineryCategory"
                   border-bottom>
        <u-input v-model="form.machineryCategory"
                 placeholder="请输入保养项目"
                 clearable />
      </u-form-item>
      <u-form-item label="附件图片"
                   prop="storageBlobDTOs"
                   border-bottom>
        <CommonUpload v-model="form.storageBlobDTOs" />
      </u-form-item>
      <!-- æäº¤æŒ‰é’® -->
      <view class="footer-btns">
        <u-button class="cancel-btn"
                  @click="goBack">取消</u-button>
        <u-button class="save-btn"
                  @click="sendForm"
                  :loading="loading">保存</u-button>
      </view>
    </u-form>
    <!-- è®¾å¤‡é€‰æ‹©å™¨ -->
    <up-action-sheet :show="showDevice"
                     :actions="deviceActions"
                     title="选择设备"
                     @select="onDeviceConfirm"
                     @close="showDevice = false" />
    <up-datetime-picker :show="showDate"
                        v-model="pickerDateValue"
                        @confirm="onDateConfirm"
                        @cancel="showDate = false"
                        mode="date" />
  </view>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue';
import { onShow } from '@dcloudio/uni-app';
import PageHeader from '@/components/PageHeader.vue';
import { getDeviceLedger } from '@/api/equipmentManagement/ledger';
import { addUpkeep, editUpkeep, getUpkeepById } from '@/api/equipmentManagement/upkeep';
import dayjs from "dayjs";
import { formatDateToYMD } from '@/utils/ruoyi';
  import { ref, computed, onMounted, onUnmounted } from "vue";
  import { onShow } from "@dcloudio/uni-app";
  import PageHeader from "@/components/PageHeader.vue";
  import CommonUpload from "@/components/CommonUpload.vue";
  import { getDeviceLedger } from "@/api/equipmentManagement/ledger";
  import {
    addUpkeep,
    editUpkeep,
    getUpkeepById,
  } from "@/api/equipmentManagement/upkeep";
  import dayjs from "dayjs";
  import { formatDateToYMD } from "@/utils/ruoyi";
defineOptions({
    name: "设备保养计划表单",
});
const showToast = (message) => {
  uni.showToast({
    title: message,
    icon: 'none'
  })
}
  defineOptions({
    name: "设备保养计划表单",
  });
  const showToast = message => {
    uni.showToast({
      title: message,
      icon: "none",
    });
  };
// è¡¨å•引用
const formRef = ref(null);
const operationType = ref('add');
const loading = ref(false);
const showDevice = ref(false);
const showDate = ref(false);
const pickerDateValue = ref(Date.now());
const currentDate = ref([new Date().getFullYear(), new Date().getMonth() + 1, new Date().getDate()]);
  // è¡¨å•引用
  const formRef = ref(null);
  const operationType = ref("add");
  const loading = ref(false);
  const showDevice = ref(false);
  const showDate = ref(false);
  const pickerDateValue = ref(Date.now());
  const currentDate = ref([
    new Date().getFullYear(),
    new Date().getMonth() + 1,
    new Date().getDate(),
  ]);
// è®¾å¤‡é€‰é¡¹
const deviceOptions = ref([]);
const deviceNameText = ref('');
// è½¬æ¢ä¸º action-sheet éœ€è¦çš„æ ¼å¼
const deviceActions = computed(() => {
    return deviceOptions.value.map(item => ({
        text: item.deviceName,
        value: item.id,
        data: item
    }));
});
  // è®¾å¤‡é€‰é¡¹
  const deviceOptions = ref([]);
  const deviceNameText = ref("");
  // è½¬æ¢ä¸º action-sheet éœ€è¦çš„æ ¼å¼
  const deviceActions = computed(() => {
    return deviceOptions.value.map(item => ({
      text: item.deviceName,
      value: item.id,
      data: item,
    }));
  });
// æ‰«ç ç›¸å…³çŠ¶æ€
const isScanning = ref(false);
const scanTimer = ref(null);
  // æ‰«ç ç›¸å…³çŠ¶æ€
  const isScanning = ref(false);
  const scanTimer = ref(null);
// è¡¨å•验证规则
const formRules = {
    deviceLedgerId: [{ required: true, trigger: "change", message: "请选择设备名称" }],
    maintenancePlanTime: [{ required: true, trigger: "change", message: "请选择计划保养日期" }],
};
  // è¡¨å•验证规则
  const formRules = {
    deviceLedgerId: [
      { required: true, trigger: "change", message: "请选择设备名称" },
    ],
    maintenancePlanTime: [
      { required: true, trigger: "change", message: "请选择计划保养日期" },
    ],
  };
// ä½¿ç”¨ ref å£°æ˜Žè¡¨å•数据
const form = ref({
    deviceLedgerId: undefined, // è®¾å¤‡ID
    deviceModel: undefined, // è§„格型号
    maintenancePlanTime: dayjs().format("YYYY-MM-DD"), // è®¡åˆ’保养日期
});
  // ä½¿ç”¨ ref å£°æ˜Žè¡¨å•数据
  const form = ref({
    deviceLedgerId: undefined, // è®¾å¤‡ID
    deviceModel: undefined, // è§„格型号
    maintenancePlanTime: dayjs().format("YYYY-MM-DD"), // è®¡åˆ’保养日期
    maintenancePerson: undefined, // ä¿å…»äºº
    machineryCategory: undefined, // ä¿å…»é¡¹ç›®
    storageBlobDTOs: [], // é™„件图片
  });
// åŠ è½½è®¾å¤‡åˆ—è¡¨
const loadDeviceName = async () => {
    try {
        const { data } = await getDeviceLedger();
        deviceOptions.value = data || [];
    } catch (e) {
        showToast('获取设备列表失败');
    }
};
  // åŠ è½½è®¾å¤‡åˆ—è¡¨
  const loadDeviceName = async () => {
    try {
      const { data } = await getDeviceLedger();
      deviceOptions.value = data || [];
    } catch (e) {
      showToast("获取设备列表失败");
    }
  };
// åŠ è½½è¡¨å•æ•°æ®ï¼ˆç¼–è¾‘æ¨¡å¼ï¼‰
const loadForm = async (id) => {
    if (id) {
        operationType.value = 'edit';
        try {
            const { code, data } = await getUpkeepById(id);
            if (code == 200) {
                form.value.deviceLedgerId = data.deviceLedgerId;
                form.value.deviceModel = data.deviceModel;
                form.value.maintenancePlanTime = dayjs(data.maintenancePlanTime).format("YYYY-MM-DD");
                // è®¾ç½®è®¾å¤‡åç§°æ˜¾ç¤º
                const device = deviceOptions.value.find(item => item.id === data.deviceLedgerId);
                if (device) {
                    form.value.deviceNameText = device.deviceName;
                }
            }
        } catch (e) {
            showToast('获取详情失败');
        }
    } else {
        // æ–°å¢žæ¨¡å¼
        operationType.value = 'add';
    }
};
  // åŠ è½½è¡¨å•æ•°æ®ï¼ˆç¼–è¾‘æ¨¡å¼ï¼‰
  const loadForm = async id => {
    if (id) {
      operationType.value = "edit";
      try {
        const { code, data } = await getUpkeepById(id);
        if (code == 200) {
          form.value.deviceLedgerId = data.deviceLedgerId;
          form.value.deviceModel = data.deviceModel;
          form.value.maintenancePlanTime = dayjs(data.maintenancePlanTime).format(
            "YYYY-MM-DD"
          );
          form.value.maintenancePerson = data.maintenancePerson;
          form.value.machineryCategory = data.machineryCategory;
          form.value.storageBlobDTOs = data.storageBlobVOs || [];
          // è®¾ç½®è®¾å¤‡åç§°æ˜¾ç¤º
          const device = deviceOptions.value.find(
            item => item.id === data.deviceLedgerId
          );
          if (device) {
            form.value.deviceNameText = device.deviceName;
          }
        }
      } catch (e) {
        showToast("获取详情失败");
      }
    } else {
      // æ–°å¢žæ¨¡å¼
      operationType.value = "add";
    }
  };
// æ‰«æäºŒç»´ç åŠŸèƒ½
const startScan = () => {
    if (isScanning.value) {
        showToast('正在扫描中,请稍候...');
        return;
    }
    // è°ƒç”¨uni-app的扫码API
    uni.scanCode({
        scanType: ['qrCode', 'barCode'],
        success: (res) => {
            handleScanResult(res.result);
        },
        fail: (err) => {
            console.error('扫码失败:', err);
            showToast('扫码失败,请重试');
        }
    });
};
  // æ‰«æäºŒç»´ç åŠŸèƒ½
  const startScan = () => {
    if (isScanning.value) {
      showToast("正在扫描中,请稍候...");
      return;
    }
// å¤„理扫码结果
const handleScanResult = (scanResult) => {
    if (!scanResult) {
        showToast('扫码结果为空');
        return;
    }
    isScanning.value = true;
    showToast('扫码成功');
    // 3秒后处理扫码结果
    scanTimer.value = setTimeout(() => {
        processScanResult(scanResult);
        isScanning.value = false;
    }, 1000);
};
function getDeviceIdByRegExp(url) {
    // åŒ¹é…deviceId=后面的数字
    const reg = /deviceId=(\d+)/;
    const match = url.match(reg);
    // å¦‚果匹配到结果,返回数字类型,否则返回null
    return match ? Number(match[1]) : null;
}
// å¤„理扫码结果并匹配设备
const processScanResult = (scanResult) => {
    const deviceId = getDeviceIdByRegExp(scanResult);
    const matchedDevice = deviceOptions.value.find(item => item.id == deviceId);
    if (matchedDevice) {
        // æ‰¾åˆ°åŒ¹é…çš„设备,自动填充
        form.value.deviceLedgerId = matchedDevice.id;
        form.value.deviceNameText = matchedDevice.deviceName;
        form.value.deviceModel = matchedDevice.deviceModel;
        showToast('设备信息已自动填充');
    } else {
        // æœªæ‰¾åˆ°åŒ¹é…çš„设备
        showToast('未找到匹配的设备,请手动选择');
    }
};
    // è°ƒç”¨uni-app的扫码API
    uni.scanCode({
      scanType: ["qrCode", "barCode"],
      success: res => {
        handleScanResult(res.result);
      },
      fail: err => {
        console.error("扫码失败:", err);
        showToast("扫码失败,请重试");
      },
    });
  };
// æ˜¾ç¤ºè®¾å¤‡é€‰æ‹©å™¨
const showDevicePicker = () => {
    showDevice.value = true;
};
  // å¤„理扫码结果
  const handleScanResult = scanResult => {
    if (!scanResult) {
      showToast("扫码结果为空");
      return;
    }
// ç¡®è®¤è®¾å¤‡é€‰æ‹©
const onDeviceConfirm = (selected) => {
    // selected è¿”回的是选中项
    form.value.deviceLedgerId = selected.value;
        form.value.deviceNameText = selected.name;
    const selectedDevice = deviceOptions.value.find(item => item.id === selected.value);
    if (selectedDevice) {
        form.value.deviceModel = selectedDevice.deviceModel;
    }
    showDevice.value = false;
};
    isScanning.value = true;
    showToast("扫码成功");
// æ˜¾ç¤ºæ—¥æœŸé€‰æ‹©å™¨
const showDatePicker = () => {
    showDate.value = true;
};
    // 3秒后处理扫码结果
    scanTimer.value = setTimeout(() => {
      processScanResult(scanResult);
      isScanning.value = false;
    }, 1000);
  };
  function getDeviceIdByRegExp(url) {
    // åŒ¹é…deviceId=后面的数字
    const reg = /deviceId=(\d+)/;
    const match = url.match(reg);
    // å¦‚果匹配到结果,返回数字类型,否则返回null
    return match ? Number(match[1]) : null;
  }
  // å¤„理扫码结果并匹配设备
  const processScanResult = scanResult => {
    const deviceId = getDeviceIdByRegExp(scanResult);
    const matchedDevice = deviceOptions.value.find(item => item.id == deviceId);
// ç¡®è®¤æ—¥æœŸé€‰æ‹©
const onDateConfirm = (e) => {
    form.value.maintenancePlanTime = formatDateToYMD(e.value);
    showDate.value = false;
};
    if (matchedDevice) {
      // æ‰¾åˆ°åŒ¹é…çš„设备,自动填充
      form.value.deviceLedgerId = matchedDevice.id;
      form.value.deviceNameText = matchedDevice.deviceName;
      form.value.deviceModel = matchedDevice.deviceModel;
      showToast("设备信息已自动填充");
    } else {
      // æœªæ‰¾åˆ°åŒ¹é…çš„设备
      showToast("未找到匹配的设备,请手动选择");
    }
  };
onShow(() => {
    // é¡µé¢æ˜¾ç¤ºæ—¶èŽ·å–å‚æ•°
    getPageParams();
});
  // æ˜¾ç¤ºè®¾å¤‡é€‰æ‹©å™¨
  const showDevicePicker = () => {
    showDevice.value = true;
  };
onMounted(() => {
    // é¡µé¢åŠ è½½æ—¶èŽ·å–è®¾å¤‡åˆ—è¡¨å’Œå‚æ•°
    loadDeviceName();
    getPageParams();
});
  // ç¡®è®¤è®¾å¤‡é€‰æ‹©
  const onDeviceConfirm = selected => {
    // selected è¿”回的是选中项
    form.value.deviceLedgerId = selected.value;
    form.value.deviceNameText = selected.name;
    const selectedDevice = deviceOptions.value.find(
      item => item.id === selected.value
    );
    if (selectedDevice) {
      form.value.deviceModel = selectedDevice.deviceModel;
    }
    showDevice.value = false;
  };
// ç»„件卸载时清理定时器
onUnmounted(() => {
    if (scanTimer.value) {
        clearTimeout(scanTimer.value);
    }
});
  // æ˜¾ç¤ºæ—¥æœŸé€‰æ‹©å™¨
  const showDatePicker = () => {
    showDate.value = true;
  };
// æäº¤è¡¨å•
const sendForm = async () => {
    try {
        // æ‰‹åŠ¨éªŒè¯è¡¨å•
        const valid = await formRef.value.validate();
        if (!valid) return;
        loading.value = true;
        const id = getPageId();
        // å‡†å¤‡æäº¤æ•°æ®
        const submitData = { ...form.value };
        // ç¡®ä¿æ—¥æœŸæ ¼å¼æ­£ç¡®
        if (submitData.maintenancePlanTime && !submitData.maintenancePlanTime.includes(':')) {
            submitData.maintenancePlanTime = submitData.maintenancePlanTime + ' 00:00:00';
        }
        const { code } = id
            ? await editUpkeep({ id: id, ...submitData })
            : await addUpkeep(submitData);
        if (code == 200) {
            showToast(`${id ? "编辑" : "新增"}计划成功`);
            setTimeout(() => {
                uni.navigateBack();
            }, 1500);
        } else {
            loading.value = false;
        }
    } catch (e) {
        loading.value = false;
        showToast('表单验证失败');
    }
};
  // ç¡®è®¤æ—¥æœŸé€‰æ‹©
  const onDateConfirm = e => {
    form.value.maintenancePlanTime = formatDateToYMD(e.value);
    showDate.value = false;
  };
// è¿”回上一页
const goBack = () => {
    // æ¸…除存储的id
    uni.removeStorageSync('repairId');
    uni.navigateBack();
};
  onShow(() => {
    // é¡µé¢æ˜¾ç¤ºæ—¶èŽ·å–å‚æ•°
    getPageParams();
  });
// èŽ·å–é¡µé¢å‚æ•°
const getPageParams = () => {
    // ä»Žæœ¬åœ°å­˜å‚¨èŽ·å–id
    const id = uni.getStorageSync('repairId');
    // æ ¹æ®æ˜¯å¦æœ‰id参数来判断是新增还是编辑
    if (id) {
        // ç¼–辑模式,获取详情
        loadForm(id);
    } else {
        // æ–°å¢žæ¨¡å¼
        loadForm();
    }
};
  onMounted(() => {
    // é¡µé¢åŠ è½½æ—¶èŽ·å–è®¾å¤‡åˆ—è¡¨å’Œå‚æ•°
    loadDeviceName();
    getPageParams();
  });
// èŽ·å–é¡µé¢ID
const getPageId = () => {
    // ä»Žæœ¬åœ°å­˜å‚¨èŽ·å–id
    return uni.getStorageSync('repairId');
};
  // ç»„件卸载时清理定时器
  onUnmounted(() => {
    if (scanTimer.value) {
      clearTimeout(scanTimer.value);
    }
  });
  // æäº¤è¡¨å•
  const sendForm = async () => {
    try {
      // æ‰‹åŠ¨éªŒè¯è¡¨å•
      const valid = await formRef.value.validate();
      if (!valid) return;
      loading.value = true;
      const id = getPageId();
      // å‡†å¤‡æäº¤æ•°æ®
      const submitData = { ...form.value, status: 0 };
      // ç¡®ä¿æ—¥æœŸæ ¼å¼æ­£ç¡®
      if (
        submitData.maintenancePlanTime &&
        !submitData.maintenancePlanTime.includes(":")
      ) {
        submitData.maintenancePlanTime =
          submitData.maintenancePlanTime + " 00:00:00";
      }
      const { code } = id
        ? await editUpkeep({ id: id, ...submitData })
        : await addUpkeep(submitData);
      if (code == 200) {
        showToast(`${id ? "编辑" : "新增"}计划成功`);
        setTimeout(() => {
          uni.navigateBack();
        }, 1500);
      } else {
        loading.value = false;
      }
    } catch (e) {
      loading.value = false;
      showToast("表单验证失败");
    }
  };
  // è¿”回上一页
  const goBack = () => {
    // æ¸…除存储的id
    uni.removeStorageSync("repairId");
    uni.navigateBack();
  };
  // èŽ·å–é¡µé¢å‚æ•°
  const getPageParams = () => {
    // ä»Žæœ¬åœ°å­˜å‚¨èŽ·å–id
    const id = uni.getStorageSync("repairId");
    // æ ¹æ®æ˜¯å¦æœ‰id参数来判断是新增还是编辑
    if (id) {
      // ç¼–辑模式,获取详情
      loadForm(id);
    } else {
      // æ–°å¢žæ¨¡å¼
      loadForm();
    }
  };
  // èŽ·å–é¡µé¢ID
  const getPageId = () => {
    // ä»Žæœ¬åœ°å­˜å‚¨èŽ·å–id
    return uni.getStorageSync("repairId");
  };
</script>
<style scoped lang="scss">
@import '@/static/scss/form-common.scss';
.upkeep-add {
    min-height: 100vh;
    background: #f8f9fa;
    padding-bottom: 5rem;
}
  @import "@/static/scss/form-common.scss";
  .upkeep-add {
    min-height: 100vh;
    background: #f8f9fa;
    padding-bottom: 5rem;
  }
.footer-btns {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    background: #fff;
    display: flex;
    justify-content: space-around;
    align-items: center;
    padding: 0.75rem 0;
    box-shadow: 0 -0.125rem 0.5rem rgba(0,0,0,0.05);
    z-index: 1000;
}
  .footer-btns {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    background: #fff;
    display: flex;
    justify-content: space-around;
    align-items: center;
    padding: 0.75rem 0;
    box-shadow: 0 -0.125rem 0.5rem rgba(0, 0, 0, 0.05);
    z-index: 1000;
  }
.cancel-btn {
    font-weight: 400;
    font-size: 1rem;
    color: #FFFFFF;
    width: 6.375rem;
    background: #C7C9CC;
    box-shadow: 0 0.25rem 0.625rem 0 rgba(3,88,185,0.2);
    border-radius: 2.5rem 2.5rem 2.5rem 2.5rem;
}
  .cancel-btn {
    font-weight: 400;
    font-size: 1rem;
    color: #ffffff;
    width: 6.375rem;
    background: #c7c9cc;
    box-shadow: 0 0.25rem 0.625rem 0 rgba(3, 88, 185, 0.2);
    border-radius: 2.5rem 2.5rem 2.5rem 2.5rem;
  }
.save-btn {
    font-weight: 400;
    font-size: 1rem;
    color: #FFFFFF;
    width: 14rem;
    background: linear-gradient( 140deg, #00BAFF 0%, #006CFB 100%);
    box-shadow: 0 0.25rem 0.625rem 0 rgba(3,88,185,0.2);
    border-radius: 2.5rem 2.5rem 2.5rem 2.5rem;
}
  .save-btn {
    font-weight: 400;
    font-size: 1rem;
    color: #ffffff;
    width: 14rem;
    background: linear-gradient(140deg, #00baff 0%, #006cfb 100%);
    box-shadow: 0 0.25rem 0.625rem 0 rgba(3, 88, 185, 0.2);
    border-radius: 2.5rem 2.5rem 2.5rem 2.5rem;
  }
// å“åº”式调整
@media (max-width: 768px) {
    .submit-section {
        padding: 12px;
    }
}
  // å“åº”式调整
  @media (max-width: 768px) {
    .submit-section {
      padding: 12px;
    }
  }
.tip-text {
    padding: 4px 16px 0 16px;
    font-size: 12px;
    color: #888;
}
  .tip-text {
    padding: 4px 16px 0 16px;
    font-size: 12px;
    color: #888;
  }
.scan-icon {
    color: #1989fa;
    font-size: 18px;
    margin-left: 8px;
    cursor: pointer;
}
  .scan-icon {
    color: #1989fa;
    font-size: 18px;
    margin-left: 8px;
    cursor: pointer;
  }
</style>
src/pages/equipmentManagement/upkeep/fileList.vue
@@ -8,7 +8,7 @@
      <view v-if="fileList.length > 0"
            class="file-list">
        <view v-for="(file, index) in fileList"
              :key="file.id || index"
              :key="file.storageAttachmentId || file.id || index"
              class="file-item">
          <!-- æ–‡ä»¶å›¾æ ‡ -->
          <!-- <view class="file-icon"
@@ -19,7 +19,7 @@
          </view> -->
          <!-- æ–‡ä»¶ä¿¡æ¯ -->
          <view class="file-info">
            <text class="file-name">{{ file.name }}</text>
            <text class="file-name">{{ file.originalFilename || file.name }}</text>
            <!-- <text class="file-meta">{{ formatFileSize(file.fileSize) }} Â· {{ file.uploadTime || file.createTime }}</text> -->
          </view>
          <!-- æ“ä½œæŒ‰é’® -->
@@ -65,15 +65,16 @@
<script setup>
  import { ref, onMounted } from "vue";
  import { onLoad } from "@dcloudio/uni-app";
  import PageHeader from "@/components/PageHeader.vue";
  import config from "@/config";
  import { getToken } from "@/utils/auth";
  // import { saveAs } from "file-saver";
  import {
    listMaintenanceTaskFiles,
    addMaintenanceTaskFile,
    delMaintenanceTaskFile,
  } from "@/api/equipmentManagement/upkeep";
    attachmentList,
    createAttachment,
    deleteAttachment,
  } from "@/api/basicData/storageAttachment";
  import { blobValidate } from "@/utils/ruoyi";
  // é™„件列表
@@ -214,21 +215,27 @@
              // const fileType = fileName.split(".").pop();
              // 3. æž„造保存文件信息的参数
              const saveData = {
                name: fileName,
                deviceMaintenanceId: upkeepId.value,
                url: res.data.tempPath || "",
                application: "file",
                recordType: recordType.value,
                recordId: upkeepId.value,
                storageBlobDTOs: [
                  {
                    name: fileName,
                    url:
                      res.data.url ||
                      res.data.previewURL ||
                      res.data.tempPath ||
                      "",
                    ...res.data,
                  },
                ],
              };
              console.log(saveData, "保存文件信息参数");
              // 4. è°ƒç”¨ addRuleFile æŽ¥å£ä¿å­˜æ–‡ä»¶ä¿¡æ¯
              addMaintenanceTaskFile(saveData)
              // 4. è°ƒç”¨ createAttachment æŽ¥å£ä¿å­˜æ–‡ä»¶ä¿¡æ¯
              createAttachment(saveData)
                .then(addRes => {
                  if (addRes.code === 200) {
                    // 5. æ·»åŠ åˆ°æ–‡ä»¶åˆ—è¡¨
                    const newFile = {
                      ...addRes.data,
                      uploadTime: new Date().toLocaleString(),
                    };
                    // fileList.value.push(newFile);
                    // 5. åˆ·æ–°åˆ—表
                    getFileList();
                    showToast("上传成功");
                  } else {
@@ -257,20 +264,32 @@
  };
  // ä¸‹è½½æ–‡ä»¶
  const downloadFile = file => {
    var url =
      config.baseUrl +
      "/common/download?fileName=" +
      encodeURIComponent(file.url) +
      "&delete=true";
    console.log(url, "url");
    let url = file.downloadURL || file.previewURL || file.url;
    if (!url) {
      showToast("文件地址无效");
      return;
    }
    // å¦‚果不是完整的URL,则拼接
    if (!url.startsWith("http")) {
      url =
        config.baseUrl +
        "/common/download?fileName=" +
        encodeURIComponent(url) +
        "&delete=true";
    }
    console.log(url, "下载地址");
    uni.showLoading({ title: "正在下载...", mask: true });
    uni
      .downloadFile({
        url: url,
        responseType: "blob",
        header: { Authorization: "Bearer " + getToken() },
      })
      .then(res => {
        uni.hideLoading();
        let osType = uni.getStorageSync("deviceInfo").osName;
        let filePath = res.tempFilePath;
        if (osType === "ios") {
@@ -280,7 +299,6 @@
            success: res => {},
            fail: err => {
              console.log("uni.openDocument--fail");
              reject(err);
            },
          });
        } else {
@@ -290,10 +308,8 @@
              uni.showToast({
                icon: "none",
                mask: true,
                title:
                  "文件已保存:Android/data/uni.UNI720216F/apps/__UNI__720216F/" +
                  fileRes.savedFilePath, //保存路径
                duration: 3000,
                title: "文件已下载并尝试打开",
                duration: 2000,
              });
              setTimeout(() => {
                //打开文档查看
@@ -305,24 +321,12 @@
            },
            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 => {
        uni.hideLoading();
        console.error("下载失败:", err);
        showToast("下载失败");
      });
@@ -335,7 +339,7 @@
      content: `确定要删除附件 "${file.name}" å—?`,
      success: res => {
        if (res.confirm) {
          deleteFile(file.id, index);
          deleteFile(file.storageAttachmentId || file.id, index);
        }
      },
    });
@@ -348,7 +352,7 @@
      mask: true,
    });
    delMaintenanceTaskFile([fileId])
    deleteAttachment([fileId])
      .then(res => {
        uni.hideLoading();
        if (res.code === 200) {
@@ -372,37 +376,48 @@
      icon: "none",
    });
  };
  const rulesRegulationsManagementId = ref("");
  const upkeepId = ref("");
  const recordType = ref("");
  // é¡µé¢åŠ è½½æ—¶èŽ·å–å‚æ•°
  onLoad(options => {
    if (options.recordId) {
      upkeepId.value = options.recordId;
    } else {
      upkeepId.value = uni.getStorageSync("upkeepId");
    }
    if (options.recordType) {
      recordType.value = options.recordType;
    } else {
      recordType.value = "device_maintenance"; // é»˜è®¤å…¼å®¹
    }
    getFileList();
  });
  // é¡µé¢åŠ è½½æ—¶
  onMounted(() => {
    // ä»Ž API èŽ·å–é™„ä»¶åˆ—è¡¨
    // ä»Žæœ¬åœ°å­˜å‚¨èŽ·å– rulesRegulationsManagementId
    rulesRegulationsManagementId.value = uni.getStorageSync(
      "rulesRegulationsManagement"
    );
    upkeepId.value = uni.getStorageSync("upkeepId");
    getFileList();
    // getFileList(); // onLoad ä¸­å·²ç»è°ƒç”¨äº†
  });
  // èŽ·å–é™„ä»¶åˆ—è¡¨
  const getFileList = () => {
    if (!upkeepId.value) return;
    uni.showLoading({
      title: "加载中...",
      mask: true,
    });
    listMaintenanceTaskFiles({
      current: 1,
      size: 100,
      deviceMaintenanceId: upkeepId.value,
      rulesRegulationsManagementId: upkeepId.value,
    attachmentList({
      recordType: recordType.value,
      recordId: upkeepId.value,
    })
      .then(res => {
        uni.hideLoading();
        if (res.code === 200) {
          fileList.value = res.data.records || [];
          fileList.value = res.data || [];
        } else {
          showToast("获取附件列表失败");
        }
src/pages/equipmentManagement/upkeep/index.vue
@@ -63,6 +63,14 @@
              <text class="detail-value">{{ formatDateTime(item.createTime) || '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">保养人</text>
              <text class="detail-value">{{ item.maintenancePerson || '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">保养项目</text>
              <text class="detail-value">{{ item.machineryCategory || '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">实际保养人</text>
              <text class="detail-value">{{ item.maintenanceActuallyName || '-' }}</text>
            </view>
@@ -72,7 +80,8 @@
            </view>
            <view class="detail-row">
              <text class="detail-label">保养结果</text>
              <view class="detail-value">
              <text class="detail-value">{{ item.maintenanceResult || '-' }}</text>
              <!-- <view class="detail-value">
                <u-tag v-if="item.maintenanceResult === 1"
                       type="success"
                       size="mini">
@@ -84,7 +93,7 @@
                  ç»´ä¿®
                </u-tag>
                <text v-if="item.maintenanceResult === undefined || item.maintenanceResult === null">-</text>
              </view>
              </view> -->
            </view>
          </view>
          <!-- æŒ‰é’®åŒºåŸŸ -->
@@ -198,10 +207,8 @@
  };
  // æ–°å¢žé™„ä»¶ - è·³è½¬åˆ°é™„件页面
  const addFile = id => {
    // ä½¿ç”¨æœ¬åœ°å­˜å‚¨ä¼ é€’id
    uni.setStorageSync("upkeepId", id);
    uni.navigateTo({
      url: "/pages/equipmentManagement/upkeep/fileList",
      url: `/pages/equipmentManagement/upkeep/fileList?recordId=${id}&recordType=device_maintenance`,
    });
  };
src/pages/equipmentManagement/upkeep/maintain.vue
@@ -100,81 +100,9 @@
      <!-- ä¸Šä¼ é™„ä»¶ -->
      <u-form-item v-if="form.status == '1'"
                   label="保养附件"
                   prop="storageBlobDTOs"
                   border-bottom>
        <view class="simple-upload-area">
          <view class="upload-buttons">
            <u-button type="primary"
                      @click="chooseMedia('image')"
                      :loading="uploading"
                      :disabled="uploadFiles.length >= uploadConfig.limit"
                      :customStyle="{ marginRight: '10px', flex: 1 }">
              <u-icon name="camera"
                      size="18"
                      color="#fff"
                      style="margin-right: 5px;"></u-icon>
              {{ uploading ? '上传中...' : '拍照' }}
            </u-button>
            <!-- <u-button type="success"
                      @click="chooseMedia('video')"
                      :loading="uploading"
                      :disabled="uploadFiles.length >= uploadConfig.limit"
                      :customStyle="{ flex: 1 }">
              <uni-icons type="videocam"
                         name="videocam"
                         size="18"
                         color="#fff"
                         style="margin-right: 5px;"></uni-icons>
              {{ uploading ? '上传中...' : '拍视频' }}
            </u-button> -->
          </view>
          <!-- ä¸Šä¼ è¿›åº¦ -->
          <view v-if="uploading"
                class="upload-progress">
            <u-line-progress :percentage="uploadProgress"
                             :showText="true"
                             activeColor="#409eff"></u-line-progress>
          </view>
          <!-- ä¸Šä¼ çš„æ–‡ä»¶åˆ—表 -->
          <view v-if="uploadFiles.length > 0"
                class="file-list">
            <view v-for="(file, index) in uploadFiles"
                  :key="index"
                  class="file-item">
              <view class="file-preview-container">
                <!-- {{formatFileUrl(file.url)}} -->
                <image v-if="file.type === 'image' || isImageFile(file)"
                       :src="formatFileUrl(file.url || file.tempFilePath || file.path || file.downloadUrl)"
                       class="file-preview"
                       mode="aspectFill" />
                <view v-else-if="file.type === 'video'"
                      class="video-preview">
                  <uni-icons type="videocam"
                             name="videocam"
                             size="18"
                             color="#fff"
                             style="margin-right: 5px;"></uni-icons>
                  <text class="video-text">视频</text>
                </view>
                <!-- åˆ é™¤æŒ‰é’® -->
                <view class="delete-btn"
                      @click="removeFile(index)">
                  <u-icon name="close"
                          size="12"
                          color="#fff"></u-icon>
                </view>
              </view>
              <view class="file-info">
                <text class="file-name">{{ file.bucketFilename || file.name || (file.type === 'image' ? '图片' : '视频')
                  }}</text>
                <text class="file-size">{{ formatFileSize(file.size) }}</text>
              </view>
            </view>
          </view>
          <view v-if="uploadFiles.length === 0"
                class="empty-state">
            <text>请选择要上传的保养图片</text>
          </view>
        </view>
        <CommonUpload v-model="form.storageBlobDTOs" />
      </u-form-item>
      <!-- æäº¤æŒ‰é’® -->
      <view class="footer-btns">
@@ -235,6 +163,7 @@
  import { ref, onMounted, reactive } from "vue";
  import { onShow } from "@dcloudio/uni-app";
  import PageHeader from "@/components/PageHeader.vue";
  import CommonUpload from "@/components/CommonUpload.vue";
  import { addMaintenance } from "@/api/equipmentManagement/upkeep";
  import { getSparePartsList } from "@/api/equipmentManagement/repair";
  import useUserStore from "@/store/modules/user";
@@ -275,7 +204,6 @@
  const sparePartsQtyRaw = ref("");
  // æ–‡ä»¶ä¸Šä¼ ç›¸å…³
  const uploadFiles = ref([]);
  const uploading = ref(false);
  const uploadProgress = ref(0);
  const number = ref(0);
@@ -316,6 +244,7 @@
    maintenanceResult: undefined, // ä¿å…»ç»“æžœ
    maintenanceActuallyTime: dayjs().format("YYYY-MM-DD HH:mm:ss"), // å®žé™…保养日期(只显示日期)
    sparePartsIds: undefined, // è®¾å¤‡å¤‡ä»¶ID
    storageBlobDTOs: [], // ä¿å…»é™„ä»¶
  });
  // æ¸…除表单校验状态
@@ -330,6 +259,7 @@
      maintenanceResult: undefined,
      maintenanceActuallyTime: dayjs().format("YYYY-MM-DD HH:mm:ss"),
      sparePartsIds: [],
      storageBlobDTOs: [],
    };
    maintenancestatusText.value = "";
    selectedSpareParts.value = [];
@@ -374,7 +304,11 @@
      } else if (form.value.maintenanceResult === undefined) {
        isValid = false;
        errorMessage = "请选择保养结果";
      } else if (uploadFiles.value.length === 0 && form.value.status == "1") {
      } else if (
        (!form.value.storageBlobDTOs ||
          form.value.storageBlobDTOs.length === 0) &&
        form.value.status == "1"
      ) {
        isValid = false;
        errorMessage = "请上传保养照片";
      }
@@ -436,7 +370,6 @@
      const submitData = {
        ...form.value,
        imagesFile: form.value.status == "1" ? uploadFiles.value : [],
        sparePartsIds: spareIds.length ? spareIds.join(",") : "",
        sparePartsQty: spareIds.length
          ? spareIds.map(pid => sparePartQtyMap?.[pid] ?? 1).join(",")
@@ -605,7 +538,7 @@
    // é‡ç½®é€‰æ‹©çš„备件
    selectedSpareParts.value = [];
    // é‡ç½®ä¸Šä¼ çš„æ–‡ä»¶
    uploadFiles.value = [];
    form.value.storageBlobDTOs = [];
    uploading.value = false;
    uploadProgress.value = 0;
    maintenancestatusText.value = "";
@@ -655,8 +588,10 @@
      sparePartsIds.value = itemData.sparePartsIds;
      // å¡«å……附件数据
      if (itemData.files && itemData.files.length > 0) {
        uploadFiles.value = itemData.files.map(file => ({
      if (itemData.storageBlobVOs && itemData.storageBlobVOs.length > 0) {
        form.value.storageBlobDTOs = itemData.storageBlobVOs;
      } else if (itemData.files && itemData.files.length > 0) {
        form.value.storageBlobDTOs = itemData.files.map(file => ({
          id: file.id,
          name: file.name || file.bucketFilename || file.originalFilename,
          url: file.url || file.downloadUrl,
@@ -668,7 +603,7 @@
          size: file.size || file.byteSize,
        }));
      } else if (itemData.uploadFiles && itemData.uploadFiles.length > 0) {
        uploadFiles.value = itemData.uploadFiles.map(file => ({
        form.value.storageBlobDTOs = itemData.uploadFiles.map(file => ({
          id: file.id,
          name: file.name || file.bucketFilename || file.originalFilename,
          url: file.url || file.downloadUrl || file.tempFilePath || file.path,
src/pages/fileManagement/borrow/edit.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,333 @@
<template>
  <view class="borrow-edit">
    <PageHeader :title="pageTitle" @back="goBack" />
    <up-form
      ref="formRef"
      :model="form"
      :rules="rules"
      label-width="110"
    >
      <up-form-item label="借阅人" prop="borrower" required>
        <up-input
          v-model="form.borrower"
          placeholder="请输入借阅人"
          clearable
          :disabled="isReturned"
        />
      </up-form-item>
      <up-form-item label="借阅书籍" prop="documentationId" required>
        <up-input
          v-model="displayDocName"
          placeholder="请选择借阅书籍"
          readonly
          :disabled="isEdit"
          @click="!isEdit && (showDocPicker = true)"
        />
        <template #right>
          <up-icon v-if="!isEdit" name="arrow-right" @click="showDocPicker = true"></up-icon>
        </template>
      </up-form-item>
      <up-form-item label="借阅日期" prop="borrowDate" required>
        <up-input
          v-model="form.borrowDate"
          placeholder="请选择借阅日期"
          readonly
          @click="!isReturned && (showBorrowDatePicker = true)"
          :disabled="isReturned"
        />
        <template #right>
          <up-icon name="arrow-right" @click="!isReturned && (showBorrowDatePicker = true)"></up-icon>
        </template>
      </up-form-item>
      <up-form-item label="应归还日期" prop="dueReturnDate" required>
        <up-input
          v-model="form.dueReturnDate"
          placeholder="请选择应归还日期"
          readonly
          @click="!isReturned && (showDueDatePicker = true)"
          :disabled="isReturned"
        />
        <template #right>
          <up-icon name="arrow-right" @click="!isReturned && (showDueDatePicker = true)"></up-icon>
        </template>
      </up-form-item>
      <up-form-item label="借阅目的" prop="borrowPurpose" required>
        <up-input
          v-model="form.borrowPurpose"
          placeholder="请输入借阅目的"
          clearable
          :disabled="isReturned"
        />
      </up-form-item>
      <up-form-item label="备注">
        <up-textarea
          v-model="form.remark"
          placeholder="请输入备注信息"
          height="80"
          border="none"
          :disabled="isReturned"
        />
      </up-form-item>
    </up-form>
    <FooterButtons
      v-if="!isReturned"
      :loading="loading"
      :confirmText="isEdit ? '保存' : '新增'"
      @cancel="goBack"
      @confirm="handleSubmit"
    />
    <!-- å€Ÿé˜…日期选择器 -->
    <up-popup :show="showBorrowDatePicker" mode="bottom" @close="showBorrowDatePicker = false">
      <up-datetime-picker
        :show="true"
        v-model="borrowDateValue"
        @confirm="onBorrowDateConfirm"
        @cancel="showBorrowDatePicker = false"
        mode="date"
      />
    </up-popup>
    <!-- åº”归还日期选择器 -->
    <up-popup :show="showDueDatePicker" mode="bottom" @close="showDueDatePicker = false">
      <up-datetime-picker
        :show="true"
        v-model="dueReturnDateValue"
        @confirm="onDueDateConfirm"
        @cancel="showDueDatePicker = false"
        mode="date"
      />
    </up-popup>
    <!-- æ–‡æ¡£é€‰æ‹©å™¨ -->
    <up-action-sheet
      :show="showDocPicker"
      :actions="documentOptions"
      title="选择借阅书籍"
      @select="onDocSelect"
      @close="showDocPicker = false"
    />
  </view>
</template>
<script setup>
import { ref, computed, onMounted } from "vue";
import { onLoad } from "@dcloudio/uni-app";
import PageHeader from "@/components/PageHeader.vue";
import FooterButtons from "@/components/FooterButtons.vue";
import { addBorrow, updateBorrow, getDocumentList } from "@/api/fileManagement/borrow";
const formRef = ref();
const loading = ref(false);
const borrowId = ref("");
const isEdit = ref(false);
// å¼¹çª—显示状态
const showDocPicker = ref(false);
const showBorrowDatePicker = ref(false);
const showDueDatePicker = ref(false);
// æ•°æ®
const documentList = ref([]);
const borrowDateValue = ref(Date.now());
const dueReturnDateValue = ref(Date.now());
const displayDocName = ref(""); // ç”¨äºŽæ˜¾ç¤ºçš„æ–‡æ¡£åç§°
const form = ref({
  id: "",
  documentationId: "",
  borrower: "",
  borrowPurpose: "",
  borrowDate: "",
  dueReturnDate: "",
  remark: "",
  borrowStatus: "",
});
const rules = {
  borrower: [{ required: true, message: "请输入借阅人", trigger: "blur" }],
  documentationId: [{ required: true, message: "请选择借阅书籍", trigger: "change" }],
  borrowPurpose: [{ required: true, message: "请输入借阅目的", trigger: "blur" }],
  borrowDate: [{ required: true, message: "请选择借阅日期", trigger: "change" }],
  dueReturnDate: [{ required: true, message: "请选择应归还日期", trigger: "change" }],
};
// é¡µé¢æ ‡é¢˜
const pageTitle = computed(() => {
  if (isEdit.value) {
    return form.value.borrowStatus === "归还" ? "借阅详情" : "编辑借阅";
  }
  return "新增借阅";
});
// æ˜¯å¦å·²å½’还
const isReturned = computed(() => {
  return form.value.borrowStatus === "归还";
});
// æ–‡æ¡£é€‰é¡¹ï¼ˆä»…新增模式使用)
const documentOptions = computed(() => {
  return documentList.value.map((item) => ({
    name: item.docName || item.name,
    id: item.id,
  }));
});
// è¿”回上一页
const goBack = () => {
  uni.removeStorageSync("borrowEditData");
  uni.navigateBack();
};
// åŠ è½½æ–‡æ¡£åˆ—è¡¨ï¼ˆä»…æ–°å¢žæ¨¡å¼éœ€è¦ï¼‰
const loadDocumentList = async () => {
  try {
    const res = await getDocumentList();
    if (res.code === 200) {
      documentList.value = res.data || [];
    }
  } catch (error) {
    console.error("获取文档列表失败", error);
  }
};
// æ–‡æ¡£é€‰æ‹©ç¡®è®¤
const onDocSelect = (e) => {
  form.value.documentationId = e.id;
  displayDocName.value = e.name;
  showDocPicker.value = false;
};
// å€Ÿé˜…日期确认
const onBorrowDateConfirm = (e) => {
  const date = new Date(e.value);
  form.value.borrowDate = formatDate(date);
  showBorrowDatePicker.value = false;
};
// åº”归还日期确认
const onDueDateConfirm = (e) => {
  const date = new Date(e.value);
  form.value.dueReturnDate = formatDate(date);
  showDueDatePicker.value = false;
};
// æ ¼å¼åŒ–日期
const 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}`;
};
// æäº¤è¡¨å•
const handleSubmit = () => {
  // å¦‚果已归还,禁止提交
  if (isReturned.value) {
    uni.showToast({ title: "已归还的借阅记录不能编辑", icon: "none" });
    return;
  }
  formRef.value
    .validate()
    .then(async () => {
      try {
        loading.value = true;
        if (isEdit.value) {
          // ç¼–辑模式
          const res = await updateBorrow({
            id: form.value.id,
            borrower: form.value.borrower,
            borrowPurpose: form.value.borrowPurpose,
            borrowDate: form.value.borrowDate,
            dueReturnDate: form.value.dueReturnDate,
            remark: form.value.remark,
          });
          if (res.code === 200) {
            uni.showToast({ title: "编辑成功", icon: "success" });
            setTimeout(() => {
              goBack();
            }, 1500);
          } else {
            uni.showToast({ title: res.msg || "编辑失败", icon: "none" });
          }
        } else {
          // æ–°å¢žæ¨¡å¼
          const res = await addBorrow({
            documentationId: form.value.documentationId,
            borrower: form.value.borrower,
            borrowPurpose: form.value.borrowPurpose,
            borrowDate: form.value.borrowDate,
            dueReturnDate: form.value.dueReturnDate,
            borrowStatus: "借阅",
            remark: form.value.remark,
          });
          if (res.code === 200) {
            uni.showToast({ title: "新增成功", icon: "success" });
            setTimeout(() => {
              goBack();
            }, 1500);
          } else {
            uni.showToast({ title: res.msg || "新增失败", icon: "none" });
          }
        }
      } catch (error) {
        uni.showToast({ title: "操作失败", icon: "none" });
      } finally {
        loading.value = false;
      }
    })
    .catch(() => {
      // éªŒè¯å¤±è´¥
    });
};
// é¡µé¢åŠ è½½
onLoad((options) => {
  if (options.id) {
    // ç¼–辑模式
    isEdit.value = true;
    borrowId.value = options.id;
    // ä»Ž storage èŽ·å–ç¼–è¾‘æ•°æ®
    const editDataStr = uni.getStorageSync("borrowEditData");
    if (editDataStr) {
      try {
        const data = JSON.parse(editDataStr);
        Object.assign(form.value, data);
        borrowDateValue.value = new Date(data.borrowDate).getTime();
        dueReturnDateValue.value = new Date(data.dueReturnDate).getTime();
        // ç›´æŽ¥ä½¿ç”¨ä¼ é€’的文档名称显示,尝试多个可能的字段名
        displayDocName.value = data.docName || data.documentationName || data.fileName || data.name || "";
      } catch (e) {
        console.error("解析编辑数据失败", e);
      }
    }
  } else {
    // æ–°å¢žæ¨¡å¼ï¼ŒåŠ è½½æ–‡æ¡£åˆ—è¡¨å¹¶è®¾ç½®é»˜è®¤æ—¥æœŸ
    loadDocumentList();
    const today = new Date();
    form.value.borrowDate = formatDate(today);
    borrowDateValue.value = today.getTime();
  }
});
</script>
<style lang="scss">
@import "@/static/scss/form-common.scss";
.borrow-edit {
  min-height: 100vh;
  background: #f5f5f5;
}
</style>
src/pages/fileManagement/borrow/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,262 @@
<template>
  <view class="sales-account">
    <!-- ä½¿ç”¨é€šç”¨é¡µé¢å¤´éƒ¨ç»„ä»¶ -->
    <PageHeader title="借阅管理" @back="goBack" />
    <!-- æœç´¢å’Œç­›é€‰åŒºåŸŸ -->
    <view class="search-section">
      <view class="search-bar">
        <view class="search-input">
          <up-input
            class="search-text"
            placeholder="请输入借阅人搜索"
            v-model="searchForm.borrower"
            @change="getList"
            clearable
          />
        </view>
        <view class="filter-button" @click="getList">
          <up-icon name="search" size="24" color="#999"></up-icon>
        </view>
      </view>
    </view>
    <!-- å€Ÿé˜…列表 -->
    <view class="ledger-list" v-if="borrowList.length > 0">
      <view v-for="(item, index) in borrowList" :key="index">
        <view class="ledger-item">
          <view class="item-header">
            <view class="item-left">
              <view class="document-icon">
                <up-icon name="file-text" size="16" color="#ffffff"></up-icon>
              </view>
              <text class="item-id">{{ item.docName || '-' }}</text>
            </view>
            <view class="item-tag" :class="getStatusClass(item.borrowStatus)">
              <text class="tag-text">{{ item.borrowStatus }}</text>
            </view>
          </view>
          <up-divider></up-divider>
          <view class="item-details">
            <view class="detail-row">
              <text class="detail-label">借阅人</text>
              <text class="detail-value">{{ item.borrower || '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">借阅目的</text>
              <text class="detail-value">{{ item.borrowPurpose || '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">借阅日期</text>
              <text class="detail-value">{{ item.borrowDate || '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">应归还日期</text>
              <text class="detail-value">{{ item.dueReturnDate || '-' }}</text>
            </view>
            <view class="detail-row" v-if="item.remark">
              <text class="detail-label">备注</text>
              <text class="detail-value">{{ item.remark }}</text>
            </view>
          </view>
          <up-divider></up-divider>
          <view class="detail-buttons">
            <u-button
              v-if="item.borrowStatus !== '归还'"
              class="detail-button"
              size="small"
              type="primary"
              @click.stop="goEdit(item)"
            >
              ç¼–辑
            </u-button>
            <u-button
              v-if="item.borrowStatus !== '归还'"
              class="detail-button"
              size="small"
              type="error"
              plain
              @click.stop="handleDelete(item)"
            >
              åˆ é™¤
            </u-button>
            <u-button
              v-if="item.borrowStatus === '归还'"
              class="detail-button"
              size="small"
              type="primary"
              plain
              @click.stop="goView(item)"
            >
              æŸ¥çœ‹
            </u-button>
          </view>
        </view>
      </view>
    </view>
    <view v-else class="no-data">
      <text>暂无借阅记录</text>
    </view>
    <!-- æµ®åŠ¨æ“ä½œæŒ‰é’® -->
    <view class="fab-button" @click="goAdd">
      <up-icon name="plus" size="24" color="#ffffff"></up-icon>
    </view>
  </view>
</template>
<script setup>
import { ref, reactive } from "vue";
import { onShow } from "@dcloudio/uni-app";
import PageHeader from "@/components/PageHeader.vue";
import { getBorrowList, deleteBorrow } from "@/api/fileManagement/borrow";
// æŸ¥è¯¢è¡¨å•
const searchForm = reactive({
  borrower: "",
});
// å€Ÿé˜…列表数据
const borrowList = ref([]);
// åˆ†é¡µç›¸å…³
const pagination = reactive({
  currentPage: 1,
  pageSize: 10,
  total: 0,
});
// è¿”回上一页
const goBack = () => {
  uni.navigateBack();
};
// èŽ·å–çŠ¶æ€æ ·å¼ç±»
const getStatusClass = (status) => {
  if (status === "归还") return "tag-success";
  if (status === "借阅") return "tag-warning";
  return "tag-default";
};
// åŠ è½½å€Ÿé˜…åˆ—è¡¨
const getList = async () => {
  uni.showLoading({ title: "加载中...", mask: true });
  const query = {
    page: -1,
    size: -1,
    borrower: searchForm.borrower || undefined,
  };
  try {
    const res = await getBorrowList(query);
    if (res.code === 200) {
      borrowList.value = res.data.records || [];
      pagination.total = res.data.total || 0;
    } else {
      uni.showToast({ title: res.msg || "获取借阅列表失败", icon: "none" });
      borrowList.value = [];
    }
  } catch (error) {
    uni.showToast({ title: "获取借阅列表失败", icon: "none" });
    borrowList.value = [];
  } finally {
    uni.hideLoading();
  }
};
// è·³è½¬åˆ°æ–°å¢žé¡µé¢
const goAdd = () => {
  uni.navigateTo({
    url: "/pages/fileManagement/borrow/edit",
  });
};
// è·³è½¬åˆ°ç¼–辑页面
const goEdit = (item) => {
  uni.setStorageSync("borrowEditData", JSON.stringify(item));
  uni.navigateTo({
    url: `/pages/fileManagement/borrow/edit?id=${item.id}`,
  });
};
// è·³è½¬åˆ°æŸ¥çœ‹é¡µé¢ï¼ˆå·²å½’还记录)
const goView = (item) => {
  uni.setStorageSync("borrowEditData", JSON.stringify(item));
  uni.navigateTo({
    url: `/pages/fileManagement/borrow/edit?id=${item.id}`,
  });
};
// åˆ é™¤
const handleDelete = (row) => {
  uni.showModal({
    title: "删除确认",
    content: "选中的内容将被删除,是否确认删除?",
    confirmText: "确认",
    cancelText: "取消",
    success: async (res) => {
      if (res.confirm) {
        try {
          uni.showLoading({ title: "删除中...", mask: true });
          const result = await deleteBorrow([row.id]);
          if (result.code === 200) {
            uni.showToast({ title: "删除成功", icon: "success" });
            getList();
          } else {
            uni.showToast({ title: result.msg || "删除失败", icon: "none" });
          }
        } catch (error) {
          uni.showToast({ title: "删除失败", icon: "none" });
        } finally {
          uni.hideLoading();
        }
      }
    },
  });
};
onShow(() => {
  getList();
});
</script>
<style scoped lang="scss">
@import "@/styles/sales-common.scss";
// æ ‡ç­¾æ ·å¼
.item-tag {
  border-radius: 4px;
  padding: 2px 8px;
  &.tag-success {
    background: #4caf50;
  }
  &.tag-warning {
    background: #ff9800;
  }
  &.tag-default {
    background: #9e9e9e;
  }
}
.tag-text {
  font-size: 11px;
  color: #ffffff;
  font-weight: 500;
}
// æŒ‰é’®æ ·å¼
.detail-buttons {
  padding: 12px 0;
  display: flex;
  gap: 12px;
}
.detail-button {
  flex: 1;
}
</style>
src/pages/fileManagement/return/edit.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,313 @@
<template>
  <view class="return-edit">
    <PageHeader :title="pageTitle" @back="goBack" />
    <up-form
      ref="formRef"
      :model="form"
      :rules="rules"
      label-width="110"
    >
      <up-form-item label="文档" prop="borrowId" required>
        <up-input
          v-model="displayDocName"
          placeholder="请选择文档"
          readonly
          :disabled="isEdit"
          @click="!isEdit && (showDocPicker = true)"
        />
        <template #right>
          <up-icon v-if="!isEdit" name="arrow-right" @click="showDocPicker = true"></up-icon>
        </template>
      </up-form-item>
      <up-form-item label="借阅人" prop="borrower">
        <up-input
          v-model="form.borrower"
          placeholder="选择文档后自动带出"
          disabled
        />
      </up-form-item>
      <up-form-item label="归还人" prop="returner" required>
        <up-input
          v-model="form.returner"
          placeholder="请输入归还人"
          clearable
          :disabled="isReturned"
        />
      </up-form-item>
      <up-form-item label="归还日期" prop="returnDate" required>
        <up-input
          v-model="form.returnDate"
          placeholder="请选择归还日期"
          readonly
          @click="!isReturned && (showReturnDatePicker = true)"
          :disabled="isReturned"
        />
        <template #right>
          <up-icon name="arrow-right" @click="!isReturned && (showReturnDatePicker = true)"></up-icon>
        </template>
      </up-form-item>
      <up-form-item label="应归还日期" prop="dueReturnDate">
        <up-input
          v-model="form.dueReturnDate"
          placeholder="选择文档后自动带出"
          disabled
        />
      </up-form-item>
      <up-form-item label="备注说明" prop="remark">
        <up-textarea
          v-model="form.remark"
          placeholder="请输入备注说明"
          height="80"
          border="none"
          :disabled="isReturned"
        />
      </up-form-item>
    </up-form>
    <FooterButtons
      v-if="!isReturned"
      :loading="loading"
      :confirmText="isEdit ? '保存' : '新增'"
      @cancel="goBack"
      @confirm="handleSubmit"
    />
    <!-- æ–‡æ¡£é€‰æ‹©å™¨ -->
    <up-action-sheet
      :show="showDocPicker"
      :actions="documentOptions"
      title="选择文档"
      @select="onDocSelect"
      @close="showDocPicker = false"
    />
    <!-- å½’还日期选择器 -->
    <up-popup :show="showReturnDatePicker" mode="bottom" @close="showReturnDatePicker = false">
      <up-datetime-picker
        :show="true"
        v-model="returnDateValue"
        @confirm="onReturnDateConfirm"
        @cancel="showReturnDatePicker = false"
        mode="date"
      />
    </up-popup>
  </view>
</template>
<script setup>
import { ref, computed, onMounted } from "vue";
import { onLoad } from "@dcloudio/uni-app";
import PageHeader from "@/components/PageHeader.vue";
import FooterButtons from "@/components/FooterButtons.vue";
import { returnDocument, reventUpdate, getDocumentList } from "@/api/fileManagement/return";
const formRef = ref();
const loading = ref(false);
const returnId = ref("");
const isEdit = ref(false);
// å¼¹çª—显示状态
const showDocPicker = ref(false);
const showReturnDatePicker = ref(false);
// æ•°æ®
const documentList = ref([]);
const returnDateValue = ref(Date.now());
const displayDocName = ref(""); // ç”¨äºŽæ˜¾ç¤ºçš„æ–‡æ¡£åç§°
const form = ref({
  id: "",
  borrowId: "",
  documentationId: "",
  borrower: "",
  returner: "",
  borrowStatus: "",
  returnDate: "",
  dueReturnDate: "",
  remark: "",
});
const rules = {
  borrowId: [{ required: true, message: "请选择文档", trigger: "change" }],
  returner: [{ required: true, message: "请输入归还人", trigger: "blur" }],
  returnDate: [{ required: true, message: "请选择归还日期", trigger: "change" }],
};
// é¡µé¢æ ‡é¢˜
const pageTitle = computed(() => {
  if (isEdit.value) {
    return form.value.borrowStatus === "归还" ? "归还详情" : "编辑归还";
  }
  return "新增归还";
});
// æ˜¯å¦å·²å½’还
const isReturned = computed(() => {
  return form.value.borrowStatus === "归还";
});
// æ–‡æ¡£é€‰é¡¹ï¼ˆä»…新增模式使用)
const documentOptions = computed(() => {
  return documentList.value.map((item) => ({
    name: item.docName || item.name,
    id: item.id,
    borrower: item.borrower,
    dueReturnDate: item.dueReturnDate,
  }));
});
// è¿”回上一页
const goBack = () => {
  uni.removeStorageSync("returnEditData");
  uni.navigateBack();
};
// åŠ è½½æ–‡æ¡£åˆ—è¡¨ï¼ˆä»…æ–°å¢žæ¨¡å¼éœ€è¦ï¼‰
const loadDocumentList = async () => {
  try {
    const res = await getDocumentList();
    if (res.code === 200) {
      documentList.value = res.data || [];
    }
  } catch (error) {
    console.error("获取文档列表失败", error);
  }
};
// æ–‡æ¡£é€‰æ‹©ç¡®è®¤
const onDocSelect = (e) => {
  form.value.borrowId = e.id;
  form.value.documentationId = e.id;
  displayDocName.value = e.name;
  // è‡ªåŠ¨å¸¦å‡ºå€Ÿé˜…äººå’Œåº”å½’è¿˜æ—¥æœŸ
  form.value.borrower = e.borrower || "";
  form.value.dueReturnDate = e.dueReturnDate || "";
  showDocPicker.value = false;
};
// å½’还日期确认
const onReturnDateConfirm = (e) => {
  const date = new Date(e.value);
  form.value.returnDate = formatDate(date);
  showReturnDatePicker.value = false;
};
// æ ¼å¼åŒ–日期
const 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}`;
};
// æäº¤è¡¨å•
const handleSubmit = () => {
  // å¦‚果已归还,禁止提交
  if (isReturned.value) {
    uni.showToast({ title: "已归还的记录不能编辑", icon: "none" });
    return;
  }
  formRef.value
    .validate()
    .then(async () => {
      try {
        loading.value = true;
        if (isEdit.value) {
          // ç¼–辑模式
          const res = await reventUpdate({
            id: form.value.id,
            documentationId: form.value.documentationId,
            borrower: form.value.borrower,
            returner: form.value.returner,
            borrowStatus: form.value.borrowStatus,
            returnDate: form.value.returnDate,
            dueReturnDate: form.value.dueReturnDate,
            remark: form.value.remark,
          });
          if (res.code === 200) {
            uni.showToast({ title: "编辑成功", icon: "success" });
            setTimeout(() => {
              goBack();
            }, 1500);
          } else {
            uni.showToast({ title: res.msg || "编辑失败", icon: "none" });
          }
        } else {
          // æ–°å¢žæ¨¡å¼
          const res = await returnDocument({
            borrowId: form.value.borrowId,
            borrower: form.value.borrower,
            returner: form.value.returner,
            borrowStatus: "归还",
            returnDate: form.value.returnDate,
            dueReturnDate: form.value.dueReturnDate,
            remark: form.value.remark,
          });
          if (res.code === 200) {
            uni.showToast({ title: "新增成功", icon: "success" });
            setTimeout(() => {
              goBack();
            }, 1500);
          } else {
            uni.showToast({ title: res.msg || "新增失败", icon: "none" });
          }
        }
      } catch (error) {
        uni.showToast({ title: "操作失败", icon: "none" });
      } finally {
        loading.value = false;
      }
    })
    .catch(() => {
      // éªŒè¯å¤±è´¥
    });
};
// é¡µé¢åŠ è½½
onLoad((options) => {
  if (options.id) {
    // ç¼–辑模式
    isEdit.value = true;
    returnId.value = options.id;
    // ä»Ž storage èŽ·å–ç¼–è¾‘æ•°æ®
    const editDataStr = uni.getStorageSync("returnEditData");
    if (editDataStr) {
      try {
        const data = JSON.parse(editDataStr);
        Object.assign(form.value, data);
        returnDateValue.value = new Date(data.returnDate).getTime();
        // ç›´æŽ¥ä½¿ç”¨ä¼ é€’çš„ docName æ˜¾ç¤º
        displayDocName.value = data.docName || "";
      } catch (e) {
        console.error("解析编辑数据失败", e);
      }
    }
  } else {
    // æ–°å¢žæ¨¡å¼ï¼ŒåŠ è½½æ–‡æ¡£åˆ—è¡¨å¹¶è®¾ç½®é»˜è®¤æ—¥æœŸ
    loadDocumentList();
    const today = new Date();
    form.value.returnDate = formatDate(today);
    returnDateValue.value = today.getTime();
  }
});
</script>
<style lang="scss">
@import "@/static/scss/form-common.scss";
.return-edit {
  min-height: 100vh;
  background: #f5f5f5;
}
</style>
src/pages/fileManagement/return/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,262 @@
<template>
  <view class="sales-account">
    <!-- ä½¿ç”¨é€šç”¨é¡µé¢å¤´éƒ¨ç»„ä»¶ -->
    <PageHeader title="归还管理" @back="goBack" />
    <!-- æœç´¢å’Œç­›é€‰åŒºåŸŸ -->
    <view class="search-section">
      <view class="search-bar">
        <view class="search-input">
          <up-input
            class="search-text"
            placeholder="请输入借阅人搜索"
            v-model="searchForm.borrower"
            @change="getList"
            clearable
          />
        </view>
        <view class="filter-button" @click="getList">
          <up-icon name="search" size="24" color="#999"></up-icon>
        </view>
      </view>
    </view>
    <!-- å½’还列表 -->
    <view class="ledger-list" v-if="returnList.length > 0">
      <view v-for="(item, index) in returnList" :key="index">
        <view class="ledger-item">
          <view class="item-header">
            <view class="item-left">
              <view class="document-icon">
                <up-icon name="file-text" size="16" color="#ffffff"></up-icon>
              </view>
              <text class="item-id">{{ item.docName || '-' }}</text>
            </view>
            <view class="item-tag" :class="getStatusClass(item.borrowStatus)">
              <text class="tag-text">{{ item.borrowStatus }}</text>
            </view>
          </view>
          <up-divider></up-divider>
          <view class="item-details">
            <view class="detail-row">
              <text class="detail-label">借阅人</text>
              <text class="detail-value">{{ item.borrower || '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">归还人</text>
              <text class="detail-value">{{ item.returner || '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">归还日期</text>
              <text class="detail-value">{{ item.returnDate || '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">应归还日期</text>
              <text class="detail-value">{{ item.dueReturnDate || '-' }}</text>
            </view>
            <view class="detail-row" v-if="item.remark">
              <text class="detail-label">备注</text>
              <text class="detail-value">{{ item.remark }}</text>
            </view>
          </view>
          <up-divider></up-divider>
          <view class="detail-buttons">
            <u-button
              v-if="item.borrowStatus !== '归还'"
              class="detail-button"
              size="small"
              type="primary"
              @click.stop="goEdit(item)"
            >
              ç¼–辑
            </u-button>
            <u-button
              v-if="item.borrowStatus !== '归还'"
              class="detail-button"
              size="small"
              type="error"
              plain
              @click.stop="handleDelete(item)"
            >
              åˆ é™¤
            </u-button>
            <u-button
              v-if="item.borrowStatus === '归还'"
              class="detail-button"
              size="small"
              type="primary"
              plain
              @click.stop="goView(item)"
            >
              æŸ¥çœ‹
            </u-button>
          </view>
        </view>
      </view>
    </view>
    <view v-else class="no-data">
      <text>暂无归还记录</text>
    </view>
    <!-- æµ®åŠ¨æ“ä½œæŒ‰é’® -->
    <view class="fab-button" @click="goAdd">
      <up-icon name="plus" size="24" color="#ffffff"></up-icon>
    </view>
  </view>
</template>
<script setup>
import { ref, reactive } from "vue";
import { onShow } from "@dcloudio/uni-app";
import PageHeader from "@/components/PageHeader.vue";
import { getReturnListPage, deleteReturn } from "@/api/fileManagement/return";
// æŸ¥è¯¢è¡¨å•
const searchForm = reactive({
  borrower: "",
});
// å½’还列表数据
const returnList = ref([]);
// åˆ†é¡µç›¸å…³
const pagination = reactive({
  currentPage: 1,
  pageSize: 10,
  total: 0,
});
// è¿”回上一页
const goBack = () => {
  uni.navigateBack();
};
// èŽ·å–çŠ¶æ€æ ·å¼ç±»
const getStatusClass = (status) => {
  if (status === "归还") return "tag-success";
  if (status === "借阅") return "tag-warning";
  return "tag-default";
};
// åŠ è½½å½’è¿˜åˆ—è¡¨
const getList = async () => {
  uni.showLoading({ title: "加载中...", mask: true });
  const query = {
    page: -1,
    size: -1,
    borrower: searchForm.borrower || undefined,
  };
  try {
    const res = await getReturnListPage(query);
    if (res.code === 200) {
      returnList.value = res.data.records || [];
      pagination.total = res.data.total || 0;
    } else {
      uni.showToast({ title: res.msg || "获取归还列表失败", icon: "none" });
      returnList.value = [];
    }
  } catch (error) {
    uni.showToast({ title: "获取归还列表失败", icon: "none" });
    returnList.value = [];
  } finally {
    uni.hideLoading();
  }
};
// è·³è½¬åˆ°æ–°å¢žé¡µé¢
const goAdd = () => {
  uni.navigateTo({
    url: "/pages/fileManagement/return/edit",
  });
};
// è·³è½¬åˆ°ç¼–辑页面
const goEdit = (item) => {
  uni.setStorageSync("returnEditData", JSON.stringify(item));
  uni.navigateTo({
    url: `/pages/fileManagement/return/edit?id=${item.id}`,
  });
};
// è·³è½¬åˆ°æŸ¥çœ‹é¡µé¢ï¼ˆå·²å½’还记录)
const goView = (item) => {
  uni.setStorageSync("returnEditData", JSON.stringify(item));
  uni.navigateTo({
    url: `/pages/fileManagement/return/edit?id=${item.id}`,
  });
};
// åˆ é™¤
const handleDelete = (row) => {
  uni.showModal({
    title: "删除确认",
    content: "选中的内容将被删除,是否确认删除?",
    confirmText: "确认",
    cancelText: "取消",
    success: async (res) => {
      if (res.confirm) {
        try {
          uni.showLoading({ title: "删除中...", mask: true });
          const result = await deleteReturn([row.id]);
          if (result.code === 200) {
            uni.showToast({ title: "删除成功", icon: "success" });
            getList();
          } else {
            uni.showToast({ title: result.msg || "删除失败", icon: "none" });
          }
        } catch (error) {
          uni.showToast({ title: "删除失败", icon: "none" });
        } finally {
          uni.hideLoading();
        }
      }
    },
  });
};
onShow(() => {
  getList();
});
</script>
<style scoped lang="scss">
@import "@/styles/sales-common.scss";
// æ ‡ç­¾æ ·å¼
.item-tag {
  border-radius: 4px;
  padding: 2px 8px;
  &.tag-success {
    background: #4caf50;
  }
  &.tag-warning {
    background: #ff9800;
  }
  &.tag-default {
    background: #9e9e9e;
  }
}
.tag-text {
  font-size: 11px;
  color: #ffffff;
  font-weight: 500;
}
// æŒ‰é’®æ ·å¼
.detail-buttons {
  padding: 12px 0;
  display: flex;
  gap: 12px;
}
.detail-button {
  flex: 1;
}
</style>
src/pages/index.vue
@@ -3,23 +3,42 @@
    <scroll-view class="scroll" scroll-y>
      <!-- é¡¶éƒ¨ Banner:放入滚动区域,随页面一起滚动,不固定在顶部 -->
      <view class="hero-section">
        <view class="bg-img">
          <view class="hero-content">
            <view class="hero-ornaments">
              <view class="hero-glow glow-left" />
              <view class="hero-glow glow-right" />
              <view class="hero-mist mist-top" />
              <view class="hero-mist mist-bottom" />
              <view class="hero-curve curve-main" />
              <view class="hero-curve curve-sub" />
        <view class="hero-banner">
          <view class="hero-top">
            <view class="hero-copy">
              <view class="hero-badge">经营看板</view>
              <text class="hero-title">{{ heroTitle }}</text>
              <text class="hero-subtitle">{{ heroSubtitle }}</text>
              <view class="hero-meta">
                <text
                  v-for="item in heroMetaItems"
                  :key="item"
                  class="hero-meta-item"
                >
                  {{ item }}
                </text>
              </view>
            </view>
            <view class="hero-avatar">
              <text class="hero-avatar-text">{{ heroInitial }}</text>
            </view>
          </view>
          <view class="hero-wave"></view>
          <view class="hero-panels">
            <view
              v-for="item in heroMetrics"
              :key="item.label"
              class="hero-panel"
            >
              <text class="hero-panel-label">{{ item.label }}</text>
              <text class="hero-panel-value">{{ item.value }}</text>
              <text class="hero-panel-hint">{{ item.hint }}</text>
            </view>
          </view>
        </view>
      </view>
      <!-- å¿«æ·å…¥å£ -->
      <view class="quick-section">
      <view v-if="quickTools.length" class="quick-section">
        <up-grid :border="false" col="4">
          <up-grid-item
            v-for="item in quickTools"
@@ -35,7 +54,7 @@
      </view>
      <!-- æ•°æ®æ€»è§ˆ -->
      <view class="section">
      <view v-if="hasOverviewSection" class="section">
        <view class="section-header">
          <view class="section-title">
            <view class="title-bar" />
@@ -48,7 +67,7 @@
        </view>
        <view v-show="overviewExpanded" class="overview">
          <view class="overview-card sales">
          <view v-if="canShowSalesOverview" class="overview-card sales">
            <view class="card-left">
              <text class="card-title">销售数据</text>
              <view class="card-metrics">
@@ -64,7 +83,7 @@
            </view>
          </view>
          <view class="overview-card purchase">
          <view v-if="canShowPurchaseOverview" class="overview-card purchase">
            <view class="card-left">
              <text class="card-title">采购数据</text>
              <view class="card-metrics">
@@ -80,7 +99,7 @@
            </view>
          </view>
          <view class="overview-card stock">
          <view v-if="canShowStockOverview" class="overview-card stock">
            <view class="card-left">
              <text class="card-title">库存数据</text>
              <view class="card-metrics">
@@ -99,7 +118,7 @@
      </view>
      <!-- å®¢æˆ·åˆåŒé‡‘额分析 -->
      <view class="section">
      <view v-if="canShowContractAnalysis" class="section">
        <view class="section-header">
          <view class="section-title">
            <view class="title-bar" />
@@ -197,12 +216,15 @@
import { analysisCustomerContractAmounts, getBusiness } from "@/api/viewIndex";
import { createVersionUpgradeChecker } from "@/utils/versionUpgrade";
import DownloadProgressMask from "@/components/DownloadProgressMask.vue";
import useUserStore from "@/store/modules/user";
const imgNum1 = "/static/images/index/num1.png";
const imgNum2 = "/static/images/index/num2.png";
const imgNum3 = "/static/images/index/num3.png";
const quickTools = [
const userStore = useUserStore();
const quickToolSource = [
  {
    label: "生产报工",
    icon: "/static/images/icon/shengchanbaogong.svg",
@@ -224,6 +246,8 @@
    route: "/pages/equipmentManagement/repair/index",
  },
];
const quickTools = ref([...quickToolSource]);
const allowedMenuTitles = ref(new Set());
const isCanvas2d = ref(false);
@@ -272,6 +296,125 @@
  uni.showToast({ title: "更多功能待接入", icon: "none" });
}
function filterQuickToolsByRoutes() {
  const routers = userStore.routers || [];
  if (!routers || routers.length === 0) {
    allowedMenuTitles.value = new Set();
    quickTools.value = [...quickToolSource];
    return;
  }
  const titles = new Set();
  const collectMenuTitles = (routes) => {
    if (!Array.isArray(routes)) return;
    routes.forEach((route) => {
      if (route.meta && route.meta.title) {
        titles.add(route.meta.title);
      }
      if (route.children && route.children.length > 0) {
        collectMenuTitles(route.children);
      }
    });
  };
  collectMenuTitles(routers);
  allowedMenuTitles.value = titles;
  quickTools.value = quickToolSource.filter((item) =>
    titles.has(item.label)
  );
}
function hasAnyPermission(titles) {
  const titleSet = allowedMenuTitles.value;
  if (!titleSet || titleSet.size === 0) return true;
  return titles.some((title) => titleSet.has(title));
}
const canShowSalesOverview = computed(() => hasAnyPermission(["销售台账"]));
const canShowPurchaseOverview = computed(() => hasAnyPermission(["采购台账"]));
const canShowStockOverview = computed(() => hasAnyPermission(["库存管理"]));
const hasOverviewSection = computed(
  () =>
    canShowSalesOverview.value ||
    canShowPurchaseOverview.value ||
    canShowStockOverview.value
);
const canShowContractAnalysis = computed(() =>
  hasAnyPermission(["销售台账", "客户档案", "客户往来"])
);
const userDisplayName = computed(
  () => userStore.nickName
);
const heroInitial = computed(() => userDisplayName.value.slice(0, 1).toUpperCase());
const heroTitle = computed(() => `你好,${userDisplayName.value}`);
const heroSubtitle = computed(
  () => userStore.currentFactoryName || "当前账号已进入业务首页"
);
const heroMetaItems = computed(() => {
  const items = [];
  if (userStore.roleName) items.push(userStore.roleName);
  if (userStore.currentLoginTime) items.push(`登录于 ${userStore.currentLoginTime}`);
  if (!items.length) items.push("当前业务概览");
  return items;
});
const visibleSectionCount = computed(() => {
  let count = 0;
  if (quickTools.value.length > 0) count += 1;
  if (hasOverviewSection.value) count += 1;
  if (canShowContractAnalysis.value) count += 1;
  return count;
});
const heroMetrics = computed(() => {
  const items = [];
  if (canShowSalesOverview.value) {
    items.push({
      label: "销售",
      value: overviewCards.value.sales.today,
      hint: "本月营业额",
    });
  }
  if (canShowPurchaseOverview.value) {
    items.push({
      label: "采购",
      value: overviewCards.value.purchase.today,
      hint: "本月采购额",
    });
  }
  if (canShowStockOverview.value) {
    items.push({
      label: "库存",
      value: overviewCards.value.stock.today,
      hint: "当前库存量",
    });
  }
  if (canShowContractAnalysis.value && items.length < 3) {
    items.push({
      label: "合同",
      value: contractSummaryView.value.sumText,
      hint: "客户合同额",
    });
  }
  if (items.length < 3) {
    items.push({
      label: "快捷",
      value: String(quickTools.value.length),
      hint: "可用入口",
    });
  }
  if (items.length < 3) {
    items.push({
      label: "模块",
      value: String(visibleSectionCount.value),
      hint: "可见板块",
    });
  }
  return items.slice(0, 3);
});
function getByPath(obj, path) {
  if (!obj || !path) return undefined;
@@ -423,7 +566,13 @@
async function loadHome() {
  chartReady.value = false;
  try {
    const [bRes, cRes] = await Promise.all([getBusiness(), analysisCustomerContractAmounts()]);
    const businessPromise = hasOverviewSection.value
      ? getBusiness()
      : Promise.resolve({ data: {} });
    const contractPromise = canShowContractAnalysis.value
      ? analysisCustomerContractAmounts()
      : Promise.resolve({ data: { item: [], sum: "0", chain: "0", yny: "0" } });
    const [bRes, cRes] = await Promise.all([businessPromise, contractPromise]);
    businessRaw.value = bRes?.data || {};
    const cData = cRes?.data || {};
    contractSummary.value = {
@@ -448,11 +597,21 @@
    isCanvas2d.value = false;
  }
  triggerVersionCheck("onMounted");
  loadHome();
  userStore
    .getRouters()
    .then(() => {
      filterQuickToolsByRoutes();
      loadHome();
    })
    .catch(() => {
      filterQuickToolsByRoutes();
      loadHome();
    });
});
onShow(() => {
  triggerVersionCheck("onShow");
  filterQuickToolsByRoutes();
});
</script>
@@ -508,146 +667,169 @@
    }
}
.hero-section {
    margin: 0 12px;
    margin-bottom: 12px;
    animation: fadeInUp 0.6s ease-out 0.1s both;
  margin: 0 14px 12px;
  animation: fadeInUp 0.6s ease-out 0.1s both;
}
.bg-img {
    width: 100%;
    height: 10.25rem;
    background:
        linear-gradient(135deg, rgba(234, 245, 255, 0.98) 0%, rgba(220, 239, 255, 0.94) 42%, rgba(244, 250, 255, 0.96) 100%),
        url("/static/images/banner/backview.png") center/cover no-repeat;
    border-radius: 18px;
    position: relative;
    overflow: hidden;
    box-shadow: 0 12px 30px rgba(118, 154, 186, 0.16);
    &::before {
        content: "";
        position: absolute;
        inset: 0;
        background:
            radial-gradient(circle at 14% 22%, rgba(255, 255, 255, 0.95) 0, rgba(255, 255, 255, 0) 28%),
            radial-gradient(circle at 84% 18%, rgba(191, 226, 255, 0.7) 0, rgba(191, 226, 255, 0) 26%),
            linear-gradient(180deg, rgba(255, 255, 255, 0.46) 0%, rgba(255, 255, 255, 0.16) 42%, rgba(206, 229, 247, 0.22) 100%);
        pointer-events: none;
    }
    &::after {
        content: "";
        position: absolute;
        left: 18%;
        bottom: -44px;
        width: 64%;
        height: 88px;
        background: radial-gradient(ellipse at center, rgba(255, 255, 255, 0.72) 0%, rgba(255, 255, 255, 0) 72%);
        border-radius: 50%;
        filter: blur(10px);
        pointer-events: none;
    }
.hero-banner {
  position: relative;
  overflow: hidden;
  border-radius: 18px;
  padding: 18px 16px 16px;
  min-height: 182px;
  background:
    linear-gradient(135deg, rgba(22, 74, 170, 0.92) 0%, rgba(33, 115, 185, 0.88) 48%, rgba(18, 156, 144, 0.82) 100%),
    url("/static/images/banner/backview.png") center right / cover no-repeat;
  box-shadow: 0 14px 34px rgba(29, 78, 137, 0.2);
  border: 1px solid rgba(255, 255, 255, 0.18);
  &::before {
    content: "";
    position: absolute;
    inset: 0;
    background:
      linear-gradient(120deg, rgba(255, 255, 255, 0.16) 0%, rgba(255, 255, 255, 0.02) 32%, rgba(255, 255, 255, 0) 60%),
      radial-gradient(circle at top right, rgba(255, 255, 255, 0.24) 0%, rgba(255, 255, 255, 0) 34%);
    pointer-events: none;
  }
  &::after {
    content: "";
    position: absolute;
    right: -28px;
    bottom: -34px;
    width: 156px;
    height: 156px;
    border-radius: 50%;
    background: radial-gradient(circle, rgba(255, 255, 255, 0.18) 0%, rgba(255, 255, 255, 0) 72%);
    pointer-events: none;
  }
}
.hero-content {
    position: relative;
    z-index: 1;
    padding: 16px 16px 14px;
    height: 100%;
    backdrop-filter: blur(2px);
.hero-top {
  position: relative;
  z-index: 1;
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 14px;
}
.hero-ornaments {
    position: relative;
    width: 100%;
    height: 100%;
.hero-copy {
  min-width: 0;
  flex: 1;
}
.hero-glow {
    position: absolute;
    border-radius: 50%;
    filter: blur(4px);
    background: radial-gradient(circle, rgba(255, 255, 255, 0.96) 0%, rgba(255, 255, 255, 0) 72%);
    opacity: 0.9;
.hero-badge {
  display: inline-flex;
  align-items: center;
  height: 26px;
  padding: 0 10px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.16);
  border: 1px solid rgba(255, 255, 255, 0.18);
  color: rgba(255, 255, 255, 0.92);
  font-size: 11px;
  font-weight: 600;
}
.hero-glow.glow-left {
    left: -10px;
    top: 8px;
    width: 120px;
    height: 120px;
.hero-title {
  display: block;
  margin-top: 12px;
  color: #ffffff;
  font-size: 24px;
  font-weight: 700;
  line-height: 1.2;
}
.hero-glow.glow-right {
    right: -20px;
    top: 4px;
    width: 144px;
    height: 144px;
    background: radial-gradient(circle, rgba(207, 234, 255, 0.92) 0%, rgba(207, 234, 255, 0) 74%);
.hero-subtitle {
  display: block;
  margin-top: 8px;
  color: rgba(255, 255, 255, 0.84);
  font-size: 13px;
  line-height: 1.45;
}
.hero-mist {
    position: absolute;
    border-radius: 999px;
    background: linear-gradient(90deg, rgba(255, 255, 255, 0.52), rgba(255, 255, 255, 0.08));
    border: 1px solid rgba(255, 255, 255, 0.34);
    backdrop-filter: blur(10px);
    box-shadow: 0 10px 24px rgba(154, 190, 219, 0.14);
.hero-meta {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-top: 12px;
}
.hero-mist.mist-top {
    left: 18px;
    top: 20px;
    width: 112px;
    height: 18px;
.hero-meta-item {
  padding: 3px 10px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.12);
  color: rgba(255, 255, 255, 0.86);
  font-size: 11px;
  line-height: 18px;
}
.hero-mist.mist-bottom {
    left: 18px;
    top: 48px;
    width: 72px;
    height: 10px;
    opacity: 0.82;
.hero-avatar {
  position: relative;
  z-index: 1;
  width: 52px;
  height: 52px;
  flex: 0 0 52px;
  border-radius: 16px;
  background: rgba(255, 255, 255, 0.18);
  border: 1px solid rgba(255, 255, 255, 0.22);
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.3);
}
.hero-curve {
    position: absolute;
    border-radius: 999px;
    border: 2px solid rgba(255, 255, 255, 0.72);
    background: linear-gradient(180deg, rgba(255, 255, 255, 0.34), rgba(255, 255, 255, 0.12));
    box-shadow:
        0 10px 26px rgba(154, 190, 219, 0.16),
        inset 0 1px 0 rgba(255, 255, 255, 0.8);
    backdrop-filter: blur(10px);
.hero-avatar-text {
  color: #ffffff;
  font-size: 20px;
  font-weight: 700;
}
.hero-curve.curve-main {
    right: 18px;
    bottom: 22px;
    width: 176px;
    height: 84px;
    transform: rotate(-9deg);
    border-top-left-radius: 90px;
    border-bottom-right-radius: 90px;
    opacity: 1;
.hero-panels {
  position: relative;
  z-index: 1;
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 10px;
  margin-top: 18px;
}
.hero-curve.curve-sub {
    right: 96px;
    bottom: 60px;
    width: 104px;
    height: 40px;
    transform: rotate(-9deg);
    border-top-left-radius: 60px;
    border-bottom-right-radius: 60px;
    opacity: 0.9;
.hero-panel {
  min-width: 0;
  padding: 12px 10px;
  border-radius: 14px;
  background: rgba(11, 25, 48, 0.18);
  border: 1px solid rgba(255, 255, 255, 0.14);
  backdrop-filter: blur(10px);
}
.hero-wave {
    height: 1.1rem;
    background: linear-gradient(180deg, rgba(255, 255, 255, 0) 0%, rgba(244, 249, 253, 0.96) 100%);
    margin-top: -1px;
    position: relative;
.hero-panel-label {
  display: block;
  color: rgba(255, 255, 255, 0.7);
  font-size: 11px;
  line-height: 1.2;
}
.hero-panel-value {
  display: block;
  margin-top: 8px;
  color: #ffffff;
  font-size: 18px;
  font-weight: 700;
  line-height: 1.2;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.hero-panel-hint {
  display: block;
  margin-top: 6px;
  color: rgba(255, 255, 255, 0.72);
  font-size: 11px;
  line-height: 1.2;
}
.safe-top {
src/pages/inspectionUpload/attachment.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,460 @@
<template>
  <view class="attachment-page">
    <!-- é¡µé¢å¤´éƒ¨ -->
    <PageHeader :title="`查看附件 - ${taskInfo?.taskName || ''}`" @back="goBack" />
    <!-- é¡µé¢å†…容 -->
    <view class="attachment-content">
      <!-- åˆ†ç±»æ ‡ç­¾é¡µ -->
      <view class="attachment-tabs">
        <view
          class="tab-item"
          :class="{ active: currentViewType === 'before' }"
          @click="switchViewType('before')"
        >
          ç”Ÿäº§å‰ ({{ getAttachmentsByType(0).length }})
        </view>
        <view
          class="tab-item"
          :class="{ active: currentViewType === 'after' }"
          @click="switchViewType('after')"
        >
          ç”Ÿäº§ä¸­ ({{ getAttachmentsByType(1).length }})
        </view>
        <view
          class="tab-item"
          :class="{ active: currentViewType === 'issue' }"
          @click="switchViewType('issue')"
        >
          ç”Ÿäº§åŽ ({{ getAttachmentsByType(2).length }})
        </view>
      </view>
      <!-- å½“前分类的附件列表 -->
      <view class="attachment-list-container">
        <view v-if="getCurrentViewAttachments().length > 0" class="attachment-list">
          <view
            v-for="(file, index) in getCurrentViewAttachments()"
            :key="index"
            class="attachment-item"
            @click="previewAttachment(file)"
          >
            <view class="attachment-preview-container">
              <image
                v-if="isImageFile(file)"
                :src="file.url || file.downloadUrl"
                class="attachment-preview"
                mode="aspectFill"
              />
              <view v-else class="attachment-video-preview">
                <u-icon name="video" size="40" color="#409eff"></u-icon>
                <text class="video-text">视频</text>
              </view>
            </view>
            <view class="attachment-info">
              <text class="attachment-name">{{ file.originalFilename || file.bucketFilename || file.name || '附件' }}</text>
              <text class="attachment-size">{{ formatFileSize(file.byteSize || file.size) }}</text>
            </view>
          </view>
        </view>
        <view v-else class="attachment-empty">
          <u-icon name="folder-open" size="60" color="#ccc"></u-icon>
          <text class="empty-text">该分类暂无附件</text>
        </view>
      </view>
    </view>
    <!-- è§†é¢‘预览弹窗 -->
    <view v-if="showVideoDialog" class="video-modal-overlay" @click="closeVideoPreview">
      <view class="video-modal-container" @click.stop>
        <view class="video-modal-header">
          <text class="video-modal-title">{{ currentVideoFile?.originalFilename || '视频预览' }}</text>
          <view class="close-btn-video" @click="closeVideoPreview">
            <u-icon name="close" size="20" color="#fff"></u-icon>
          </view>
        </view>
        <view class="video-modal-body">
          <video
            v-if="currentVideoFile"
            :src="currentVideoFile.url || currentVideoFile.downloadUrl"
            class="video-player"
            controls
            autoplay
            @error="handleVideoError"
          ></video>
        </view>
      </view>
    </view>
  </view>
</template>
<script setup>
import { ref } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
import PageHeader from '@/components/PageHeader.vue';
import config from '@/config';
// ä»»åŠ¡ä¿¡æ¯
const taskInfo = ref(null);
// é™„件列表
const attachmentList = ref([]);
// å½“前查看类型
const currentViewType = ref('before'); // 'before', 'after', 'issue'
// è§†é¢‘预览相关状态
const showVideoDialog = ref(false);
const currentVideoFile = ref(null);
// æ–‡ä»¶è®¿é—®åŸºç¡€åŸŸ
const filePreviewBase = config.fileUrl;
// é¡µé¢åŠ è½½
onLoad((options) => {
  if (options.taskInfo) {
    try {
      taskInfo.value = JSON.parse(decodeURIComponent(options.taskInfo));
      loadAttachments();
    } catch (e) {
      console.error('解析任务信息失败:', e);
      uni.showToast({
        title: '加载失败',
        icon: 'error'
      });
    }
  }
});
// åŠ è½½é™„ä»¶æ•°æ®
const loadAttachments = () => {
  const task = taskInfo.value;
  if (!task) return;
  attachmentList.value = [];
  // åŽç«¯åæ˜¾å­—段
  const allList = Array.isArray(task?.commonFileList) ? task.commonFileList : [];
  const beforeList = Array.isArray(task?.commonFileListBefore)
    ? task.commonFileListBefore
    : allList.filter((f) => f?.type === 10);
  const afterList = Array.isArray(task?.commonFileListAfter)
    ? task.commonFileListAfter
    : allList.filter((f) => f?.type === 11);
  const issueList = Array.isArray(task?.commonFileListIssue)
    ? task.commonFileListIssue
    : allList.filter((f) => f?.type === 12);
  const mapToViewFile = (file, viewType) => {
    const u = normalizeFileUrl(file?.url || file?.downloadUrl || '');
    return {
      ...file,
      type: viewType,
      name: file?.name || file?.originalFilename || file?.bucketFilename,
      bucketFilename: file?.bucketFilename || file?.name,
      originalFilename: file?.originalFilename || file?.name,
      url: u,
      downloadUrl: u,
      size: file?.size || file?.byteSize,
    };
  };
  attachmentList.value.push(...beforeList.map((f) => mapToViewFile(f, 0)));
  attachmentList.value.push(...afterList.map((f) => mapToViewFile(f, 1)));
  attachmentList.value.push(...issueList.map((f) => mapToViewFile(f, 2)));
};
// å°†åŽç«¯è¿”回的文件地址规范成可访问URL
const normalizeFileUrl = (rawUrl) => {
  try {
    if (!rawUrl || typeof rawUrl !== 'string') return '';
    const url = rawUrl.trim();
    if (!url) return '';
    if (/^https?:\/\//i.test(url)) return url;
    if (url.startsWith('/')) return `${filePreviewBase}${url}`;
    // Windows path -> web path
    if (/^[a-zA-Z]:\\/.test(url)) {
      const normalized = url.replace(/\\/g, '/');
      const idx = normalized.indexOf('/prod/');
      if (idx >= 0) {
        const relative = normalized.slice(idx + '/prod/'.length);
        return `${filePreviewBase}/${relative}`;
      }
      return normalized;
    }
    return `${filePreviewBase}/${url.replace(/^\//, '')}`;
  } catch (e) {
    return rawUrl || '';
  }
};
// è¿”回上一页
const goBack = () => {
  uni.navigateBack();
};
// åˆ‡æ¢æŸ¥çœ‹ç±»åž‹
const switchViewType = (type) => {
  currentViewType.value = type;
};
// æ ¹æ®type获取对应分类的附件
const getAttachmentsByType = (typeValue) => {
  return attachmentList.value.filter((file) => file.type === typeValue) || [];
};
// èŽ·å–å½“å‰æŸ¥çœ‹ç±»åž‹çš„é™„ä»¶
const getCurrentViewAttachments = () => {
  switch (currentViewType.value) {
    case 'before':
      return getAttachmentsByType(0);
    case 'after':
      return getAttachmentsByType(1);
    case 'issue':
      return getAttachmentsByType(2);
    default:
      return [];
  }
};
// åˆ¤æ–­æ˜¯å¦ä¸ºå›¾ç‰‡æ–‡ä»¶
const isImageFile = (file) => {
  if (file.contentType && file.contentType.startsWith('image/')) {
    return true;
  }
  if (file.type === 'image') return true;
  const name = file.bucketFilename || file.originalFilename || file.name || '';
  const ext = name.split('.').pop()?.toLowerCase();
  return ['jpg', 'jpeg', 'png', 'gif', 'webp'].includes(ext);
};
// é¢„览附件
const previewAttachment = (file) => {
  if (isImageFile(file)) {
    const imageUrls = getCurrentViewAttachments()
      .filter((f) => isImageFile(f))
      .map((f) => f.url || f.downloadUrl);
    uni.previewImage({
      urls: imageUrls,
      current: file.url || file.downloadUrl,
    });
  } else {
    showVideoPreview(file);
  }
};
// æ˜¾ç¤ºè§†é¢‘预览
const showVideoPreview = (file) => {
  currentVideoFile.value = file;
  showVideoDialog.value = true;
};
// å…³é—­è§†é¢‘预览
const closeVideoPreview = () => {
  showVideoDialog.value = false;
  currentVideoFile.value = null;
};
// è§†é¢‘播放错误处理
const handleVideoError = () => {
  uni.showToast({
    title: '视频播放失败',
    icon: 'error',
  });
};
// æ ¼å¼åŒ–文件大小
const formatFileSize = (size) => {
  if (!size) return '';
  if (size < 1024) return size + 'B';
  if (size < 1024 * 1024) return (size / 1024).toFixed(1) + 'KB';
  return (size / (1024 * 1024)).toFixed(1) + 'MB';
};
</script>
<style scoped>
.attachment-page {
  min-height: 100vh;
  background-color: #f5f5f5;
}
.attachment-content {
  padding: 15px;
}
/* æ ‡ç­¾é¡µæ ·å¼ */
.attachment-tabs {
  display: flex;
  background: #fff;
  border-radius: 12px;
  margin-bottom: 15px;
  padding: 4px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
.tab-item {
  flex: 1;
  text-align: center;
  padding: 12px 8px;
  font-size: 14px;
  color: #666;
  border-radius: 8px;
  transition: all 0.3s ease;
}
.tab-item.active {
  background: #409eff;
  color: #fff;
  font-weight: 500;
}
/* é™„件列表样式 */
.attachment-list-container {
  background: #fff;
  border-radius: 12px;
  padding: 15px;
  min-height: 400px;
}
.attachment-list {
  display: flex;
  flex-wrap: wrap;
  gap: 15px;
}
.attachment-item {
  width: calc(33.33% - 10px);
  background: #f8f9fa;
  border-radius: 12px;
  overflow: hidden;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
  transition: all 0.3s ease;
}
.attachment-item:active {
  transform: scale(0.98);
}
.attachment-preview-container {
  width: 100%;
  height: 120px;
  background: #e9ecef;
  display: flex;
  align-items: center;
  justify-content: center;
}
.attachment-preview {
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.attachment-video-preview {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 8px;
}
.video-text {
  font-size: 12px;
  color: #666;
}
.attachment-info {
  padding: 10px;
}
.attachment-name {
  font-size: 12px;
  color: #333;
  display: block;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  margin-bottom: 4px;
}
.attachment-size {
  font-size: 10px;
  color: #999;
}
/* ç©ºçŠ¶æ€æ ·å¼ */
.attachment-empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 80px 20px;
  color: #999;
}
.empty-text {
  margin-top: 15px;
  font-size: 14px;
}
/* è§†é¢‘弹窗样式 */
.video-modal-overlay {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: rgba(0, 0, 0, 0.9);
  z-index: 10000;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 20px;
}
.video-modal-container {
  width: 100%;
  max-width: 800px;
  background: #000;
  border-radius: 12px;
  overflow: hidden;
}
.video-modal-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 15px 20px;
  background: #1a1a1a;
}
.video-modal-title {
  font-size: 16px;
  color: #fff;
  font-weight: 500;
}
.close-btn-video {
  width: 32px;
  height: 32px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(255, 255, 255, 0.1);
  border-radius: 50%;
}
.video-modal-body {
  padding: 20px;
}
.video-player {
  width: 100%;
  height: 400px;
  border-radius: 8px;
}
</style>
src/pages/inspectionUpload/components/formDia.vue
@@ -8,53 +8,99 @@
  >
    <view class="popup-content">
      <view class="popup-header">
        <text class="popup-title">上传</text>
        <text class="popup-title">巡检记录上传</text>
      </view>
      
      <view class="upload-container">
        <!-- å¼‚常状态选择 -->
        <view class="form-container">
          <view class="title">生产前</view>
          <u-upload
            :fileList="beforeModelValue"
            @afterRead="afterRead"
            @delete="deleteFile"
            name="before"
            multiple
            :maxCount="10"
            :maxSize="5 * 1024 * 1024"
            accept="image/*"
            :previewFullImage="true"
          ></u-upload>
          <view class="title">巡检状态</view>
          <view class="exception-section">
            <view class="exception-options">
              <view
                class="exception-option"
                :class="{ active: hasException === false }"
                @click="setExceptionStatus(false)"
              >
                <u-icon name="checkmark-circle" size="20" color="#52c41a"></u-icon>
                <text class="option-text">正常</text>
              </view>
              <view
                class="exception-option"
                :class="{ active: hasException === true }"
                @click="setExceptionStatus(true)"
              >
                <u-icon name="close-circle" size="20" color="#ff4d4f"></u-icon>
                <text class="option-text">存在异常</text>
              </view>
            </view>
          </view>
        </view>
        <!-- å¼‚常描述(仅在异常时显示) -->
        <view class="form-container" v-if="hasException === true">
          <view class="title">异常描述</view>
          <u-input
            v-model="exceptionDescription"
            type="textarea"
            :maxlength="500"
            placeholder="请描述异常情况..."
            :customStyle="{ padding: '10px', backgroundColor: '#f5f5f5' }"
          />
        </view>
        
        <view class="form-container">
          <view class="title">生产后</view>
          <u-upload
            :fileList="afterModelValue"
            @afterRead="afterRead"
            @delete="deleteFile"
            name="after"
            multiple
            :maxCount="10"
            :maxSize="5 * 1024 * 1024"
            accept="image/*"
            :previewFullImage="true"
          ></u-upload>
        </view>
        <view class="form-container">
          <view class="title">生产问题</view>
          <u-upload
            :fileList="issueModelValue"
            @afterRead="afterRead"
            @delete="deleteFile"
            name="issue"
            multiple
            :maxCount="10"
            :maxSize="5 * 1024 * 1024"
            accept="image/*"
            :previewFullImage="true"
          ></u-upload>
        <!-- ä¸Šä¼ åŒºåŸŸï¼ˆä»…在异常时显示) -->
        <template v-if="hasException === true">
          <view class="form-container">
            <view class="title">生产前</view>
            <u-upload
              :fileList="beforeModelValue"
              @afterRead="afterRead"
              @delete="deleteFile"
              name="before"
              multiple
              :maxCount="10"
              :maxSize="5 * 1024 * 1024"
              accept="image/*"
              :previewFullImage="true"
            ></u-upload>
          </view>
          <view class="form-container">
            <view class="title">生产后</view>
            <u-upload
              :fileList="afterModelValue"
              @afterRead="afterRead"
              @delete="deleteFile"
              name="after"
              multiple
              :maxCount="10"
              :maxSize="5 * 1024 * 1024"
              accept="image/*"
              :previewFullImage="true"
            ></u-upload>
          </view>
          <view class="form-container">
            <view class="title">生产问题</view>
            <u-upload
              :fileList="issueModelValue"
              @afterRead="afterRead"
              @delete="deleteFile"
              name="issue"
              multiple
              :maxCount="10"
              :maxSize="5 * 1024 * 1024"
              accept="image/*"
              :previewFullImage="true"
            ></u-upload>
          </view>
        </template>
        <!-- æ­£å¸¸çŠ¶æ€æç¤º -->
        <view class="form-container normal-tip" v-if="hasException === false">
          <u-icon name="info-circle" size="40" color="#52c41a"></u-icon>
          <text class="tip-text">设备运行正常,无需上传照片</text>
        </view>
      </view>
      
@@ -79,6 +125,11 @@
const afterModelValue = ref([])
const issueModelValue = ref([])
const infoData = ref(null)
// å¼‚常状态:null=未选择, false=正常, true=异常
const hasException = ref(null)
// å¼‚常描述
const exceptionDescription = ref('')
// è®¡ç®—上传URL
const uploadFileUrl = computed(() => {
@@ -196,9 +247,43 @@
  }
}
// è®¾ç½®å¼‚常状态
const setExceptionStatus = (status) => {
  hasException.value = status
}
// æäº¤è¡¨å•
const submitForm = async () => {
  try {
    // æ£€æŸ¥æ˜¯å¦é€‰æ‹©äº†å·¡æ£€çŠ¶æ€
    if (hasException.value === null) {
      uni.showToast({
        title: '请选择巡检状态',
        icon: 'none'
      })
      return
    }
    // å¦‚果是异常状态,检查是否有上传文件
    if (hasException.value === true) {
      const totalFiles = beforeModelValue.value.length + afterModelValue.value.length + issueModelValue.value.length
      if (totalFiles === 0) {
        uni.showToast({
          title: '请上传异常照片',
          icon: 'none'
        })
        return
      }
      // æ£€æŸ¥æ˜¯å¦å¡«å†™äº†å¼‚常描述
      if (!exceptionDescription.value.trim()) {
        uni.showToast({
          title: '请填写异常描述',
          icon: 'none'
        })
        return
      }
    }
    let arr = []
    if (beforeModelValue.value.length > 0) {
      arr.push(...beforeModelValue.value.map(item => ({ ...item, statusType: 0 })))
@@ -212,6 +297,8 @@
    
    // æäº¤æ•°æ®
    infoData.value.storageBlobDTO = arr
    infoData.value.hasException = hasException.value
    infoData.value.exceptionDescription = exceptionDescription.value
    await submitInspectionRecord({ ...infoData.value })
    
    uni.showToast({
@@ -238,6 +325,8 @@
  beforeModelValue.value = []
  afterModelValue.value = []
  issueModelValue.value = []
  hasException.value = null
  exceptionDescription.value = ''
}
// å…³é—­å¼¹æ¡†
@@ -311,4 +400,61 @@
  border-top: 1px solid #f0f0f0;
  background-color: #fafafa;
}
// å¼‚常状态选择样式
.exception-section {
  padding: 10px 0;
}
.exception-options {
  display: flex;
  gap: 15px;
}
.exception-option {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 15px 20px;
  border: 2px solid #e0e0e0;
  border-radius: 8px;
  cursor: pointer;
  transition: all 0.3s;
  background-color: #fff;
  &.active {
    border-color: #1890ff;
    background-color: #e6f7ff;
  }
  &:active {
    opacity: 0.8;
  }
}
.option-text {
  font-size: 14px;
  color: #333;
  font-weight: 500;
}
// æ­£å¸¸çŠ¶æ€æç¤ºæ ·å¼
.normal-tip {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 40px 20px;
  background-color: #f6ffed;
  border: 1px dashed #b7eb8f;
  border-radius: 8px;
  .tip-text {
    margin-top: 15px;
    font-size: 14px;
    color: #52c41a;
  }
}
</style>
src/pages/inspectionUpload/index.vue
@@ -56,6 +56,10 @@
              <text class="detail-value">{{ item.taskId || item.id }}</text>
            </view>
            <view class="detail-item">
              <text class="detail-label">巡检项目</text>
              <text class="detail-value">{{ item.inspectionProject || '无' }}</text>
            </view>
            <view class="detail-item">
              <text class="detail-label">备注</text>
              <text class="detail-value">{{ item.remarks || '无' }}</text>
            </view>
@@ -80,7 +84,7 @@
                         size="small"
                         type="primary"
                         inverted></uni-tag>
                <uni-tag v-else=""
                <uni-tag v-else
                         text="未巡检"
                         size="small"
                         type="warning"
@@ -95,227 +99,6 @@
      <view v-if="taskTableData?.length === 0"
            class="no-data">
        <text>暂无数据</text>
      </view>
    </view>
    <!-- å›¾ç‰‡ä¸Šä¼ å¼¹çª— - åŽŸç”Ÿå®žçŽ° -->
    <view v-if="showUploadDialog"
          class="custom-modal-overlay"
          @click="closeUploadDialog">
      <view class="custom-modal-container"
            @click.stop>
        <view class="upload-popup-content">
          <view class="upload-popup-header">
            <text class="upload-popup-title">上传巡检记录</text>
          </view>
          <view class="upload-popup-body">
            <!-- åˆ†ç±»æ ‡ç­¾é¡µ -->
            <view class="upload-tabs">
              <view class="tab-item"
                    :class="{ active: currentUploadType === 'before' }"
                    @click="switchUploadType('before')">
                ç”Ÿäº§å‰
              </view>
              <view class="tab-item"
                    :class="{ active: currentUploadType === 'after' }"
                    @click="switchUploadType('after')">
                ç”Ÿäº§ä¸­
              </view>
              <view class="tab-item"
                    :class="{ active: currentUploadType === 'issue' }"
                    @click="switchUploadType('issue')">
                ç”Ÿäº§åŽ
              </view>
            </view>
            <!-- å¼‚常状态选择 -->
            <view class="exception-section">
              <text class="section-title">是否存在异常?</text>
              <view class="exception-options">
                <view class="exception-option"
                      :class="{ active: hasException === false }"
                      @click="setExceptionStatus(false)">
                  <u-icon name="checkmark-circle"
                          size="20"
                          color="#52c41a"></u-icon>
                  <text>正常</text>
                </view>
                <view class="exception-option"
                      :class="{ active: hasException === true }"
                      @click="setExceptionStatus(true)">
                  <u-icon name="close-circle"
                          size="20"
                          color="#ff4d4f"></u-icon>
                  <text>存在异常</text>
                </view>
              </view>
            </view>
            <!-- å½“前分类的上传区域 -->
            <view class="simple-upload-area">
              <view class="upload-buttons">
                <u-button type="primary"
                          @click="chooseMedia('image')"
                          :loading="uploading"
                          :disabled="getCurrentFiles().length >= uploadConfig.limit"
                          :customStyle="{ marginRight: '10px', flex: 1 }">
                  <u-icon name="camera"
                          size="18"
                          color="#fff"
                          style="margin-right: 5px;"></u-icon>
                  {{ uploading ? '上传中...' : '拍照' }}
                </u-button>
                <u-button type="success"
                          @click="chooseMedia('video')"
                          :loading="uploading"
                          :disabled="getCurrentFiles().length >= uploadConfig.limit"
                          :customStyle="{ flex: 1 }">
                  <uni-icons type="videocam"
                             name="videocam"
                             size="18"
                             color="#fff"
                             style="margin-right: 5px;"></uni-icons>
                  {{ uploading ? '上传中...' : '拍视频' }}
                </u-button>
              </view>
              <!-- ä¸Šä¼ è¿›åº¦ -->
              <view v-if="uploading"
                    class="upload-progress">
                <u-line-progress :percentage="uploadProgress"
                                 :showText="true"
                                 activeColor="#409eff"></u-line-progress>
              </view>
              <!-- å½“前分类的文件列表 -->
              <view v-if="getCurrentFiles().length > 0"
                    class="file-list">
                <view v-for="(file, index) in getCurrentFiles()"
                      :key="index"
                      class="file-item">
                  <view class="file-preview-container">
                    <image v-if="file.type === 'image' || (file.type !== 'video' && !file.type)"
                           :src="file.url || file.tempFilePath || file.path || file.downloadUrl"
                           class="file-preview"
                           mode="aspectFill" />
                    <view v-else-if="file.type === 'video'"
                          class="video-preview">
                      <uni-icons type="videocam"
                                 name="videocam"
                                 size="18"
                                 color="#fff"
                                 style="margin-right: 5px;"></uni-icons>
                      <text class="video-text">视频</text>
                    </view>
                    <!-- åˆ é™¤æŒ‰é’® -->
                    <view class="delete-btn"
                          @click="removeFile(index)">
                      <u-icon name="close"
                              size="12"
                              color="#fff"></u-icon>
                    </view>
                  </view>
                  <view class="file-info">
                    <text class="file-name">{{ file.bucketFilename || file.name || (file.type === 'image' ? '图片' : '视频')
                      }}</text>
                    <text class="file-size">{{ formatFileSize(file.size) }}</text>
                  </view>
                </view>
              </view>
              <view v-if="getCurrentFiles().length === 0"
                    class="empty-state">
                <text>请选择要上传的{{ getUploadTypeText() }}图片或视频</text>
              </view>
              <!-- ç»Ÿè®¡ä¿¡æ¯ -->
              <view class="upload-summary">
                <text class="summary-text">
                  ç”Ÿäº§å‰: {{ beforeModelValue.length }}个文件 |
                  ç”Ÿäº§ä¸­: {{ afterModelValue.length }}个文件 |
                  ç”Ÿäº§åŽ: {{ issueModelValue.length }}个文件
                </text>
              </view>
            </view>
          </view>
          <view class="upload-popup-footer">
            <u-button @click="closeUploadDialog"
                      :customStyle="{ marginRight: '10px' }">取消</u-button>
            <u-button v-if="hasException === true"
                      type="warning"
                      @click="goToRepair"
                      :customStyle="{ marginRight: '10px' }">
              æ–°å¢žæŠ¥ä¿®
            </u-button>
            <u-button type="primary"
                      @click="submitUpload">提交</u-button>
          </view>
        </view>
      </view>
    </view>
    <!-- æŸ¥çœ‹é™„件弹窗 -->
    <view v-if="showAttachmentDialog"
          class="custom-modal-overlay"
          @click="closeAttachmentDialog">
      <view class="custom-modal-container"
            @click.stop>
        <view class="attachment-popup-content">
          <view class="attachment-popup-header">
            <text class="attachment-popup-title">查看附件 - {{ currentViewTask?.taskName }}</text>
            <view class="close-btn-attachment"
                  @click="closeAttachmentDialog">
              <u-icon name="close"
                      size="16"
                      color="#666"></u-icon>
            </view>
          </view>
          <view class="attachment-popup-body">
            <!-- åˆ†ç±»æ ‡ç­¾é¡µ -->
            <view class="attachment-tabs">
              <view class="tab-item"
                    :class="{ active: currentViewType === 'before' }"
                    @click="switchViewType('before')">
                ç”Ÿäº§å‰ ({{ getAttachmentsByType(0).length }})
              </view>
              <view class="tab-item"
                    :class="{ active: currentViewType === 'after' }"
                    @click="switchViewType('after')">
                ç”Ÿäº§ä¸­ ({{ getAttachmentsByType(1).length }})
              </view>
              <view class="tab-item"
                    :class="{ active: currentViewType === 'issue' }"
                    @click="switchViewType('issue')">
                ç”Ÿäº§åŽ ({{ getAttachmentsByType(2).length }})
              </view>
            </view>
            <!-- å½“前分类的附件列表 -->
            <view class="attachment-content">
              <view v-if="getCurrentViewAttachments().length > 0"
                    class="attachment-list">
                <view v-for="(file, index) in getCurrentViewAttachments()"
                      :key="index"
                      class="attachment-item"
                      @click="previewAttachment(file)">
                  <view class="attachment-preview-container">
                    <image v-if="file.type === 'image' || isImageFile(file)"
                           :src="file.url || file.downloadUrl"
                           class="attachment-preview"
                           mode="aspectFill" />
                    <view v-else
                          class="attachment-video-preview">
                      <u-icon name="video"
                              size="24"
                              color="#409eff"></u-icon>
                      <text class="video-text">视频</text>
                    </view>
                  </view>
                  <view class="attachment-info">
                    <text class="attachment-name">{{ file.originalFilename || file.bucketFilename || file.name || '附件'
                      }}</text>
                    <text class="attachment-size">{{ formatFileSize(file.byteSize || file.size) }}</text>
                  </view>
                </view>
              </view>
              <view v-else
                    class="attachment-empty">
                <text>该分类暂无附件</text>
              </view>
            </view>
          </view>
        </view>
      </view>
    </view>
    <!-- è§†é¢‘预览弹窗 -->
@@ -378,57 +161,9 @@
  const currentScanningTask = ref(null);
  const infoData = ref(null);
  // ä¸Šä¼ ç›¸å…³çŠ¶æ€
  const showUploadDialog = ref(false);
  const uploadFiles = ref([]); // ä¿ç•™ç”¨äºŽå…¼å®¹æ€§
  const uploadStatusType = ref(0);
  const uploading = ref(false);
  const uploadProgress = ref(0);
  const number = ref(0);
  const uploadList = ref([]);
  // ä¸‰ä¸ªåˆ†ç±»çš„上传状态
  const beforeModelValue = ref([]); // ç”Ÿäº§å‰
  const afterModelValue = ref([]); // ç”Ÿäº§ä¸­
  const issueModelValue = ref([]); // ç”Ÿäº§åŽ
  // å½“前激活的上传类型
  const currentUploadType = ref("before"); // 'before', 'after', 'issue'
  // æŸ¥çœ‹é™„件相关状态
  const showAttachmentDialog = ref(false);
  const currentViewTask = ref(null);
  const currentViewType = ref("before"); // 'before', 'after', 'issue'
  const attachmentList = ref([]); // å½“前查看任务的附件列表
  // è§†é¢‘预览相关状态
  const showVideoDialog = ref(false);
  const currentVideoFile = ref(null);
  // å¼‚常状态
  const hasException = ref(null); // null: æœªé€‰æ‹©, true: å­˜åœ¨å¼‚常, false: æ­£å¸¸
  // ä¸Šä¼ é…ç½®
  const uploadConfig = {
    action: "/file/upload",
    limit: 10,
    fileSize: 50, // MB
    fileType: ["jpg", "jpeg", "png", "mp4", "mov"],
    maxVideoDuration: 60, // ç§’
  };
  // è®¡ç®—上传URL
  const uploadFileUrl = computed(() => {
    const baseUrl = config.baseUrl;
    return baseUrl + uploadConfig.action;
  });
  // è®¡ç®—请求头
  const headers = computed(() => {
    const token = getToken();
    return token ? { Authorization: "Bearer " + token } : {};
  });
  // è¯·æ±‚取消标志,用于取消正在进行的请求
  let isRequestCancelled = false;
@@ -489,11 +224,6 @@
  onUnmounted(() => {
    // è®¾ç½®å–消标志,阻止后续的异步操作
    isRequestCancelled = true;
    // å…³é—­ä¸Šä¼ å¼¹çª—
    if (showUploadDialog.value) {
      showUploadDialog.value = false;
    }
  });
  // è¿”回上一页
@@ -653,322 +383,27 @@
    }
  };
  // æ‰“开上传弹窗
  // æ‰“开上传页面
  const openUploadDialog = task => {
    // è®¾ç½®ä»»åŠ¡ä¿¡æ¯åˆ°infoData
    if (task) {
      infoData.value = {
        ...task,
        taskId: task.taskId || task.id,
        storageBlobDTO: [], // åˆå§‹åŒ–文件列表
      };
    }
    // è®¾ç½®ä¸Šä¼ çŠ¶æ€ç±»åž‹ï¼ˆå¯ä»¥æ ¹æ®ä»»åŠ¡ç±»åž‹è®¾ç½®ä¸åŒçš„çŠ¶æ€ï¼‰
    uploadStatusType.value = 0; // é»˜è®¤çŠ¶æ€
    // æ¸…空之前的文件
    uploadFiles.value = [];
    // æ˜¾ç¤ºä¸Šä¼ å¼¹çª—
    showUploadDialog.value = true;
  };
  // å…³é—­ä¸Šä¼ å¼¹çª—
  const closeUploadDialog = () => {
    showUploadDialog.value = false;
    uploadFiles.value = [];
    // æ¸…理三个分类的数据
    beforeModelValue.value = [];
    afterModelValue.value = [];
    issueModelValue.value = [];
    currentUploadType.value = "before";
    hasException.value = null; // é‡ç½®å¼‚常状态
    infoData.value = null; // æ¸…理任务数据
  };
  // åˆ‡æ¢ä¸Šä¼ ç±»åž‹
  const switchUploadType = type => {
    currentUploadType.value = type;
  };
  // èŽ·å–å½“å‰åˆ†ç±»çš„æ–‡ä»¶åˆ—è¡¨
  const getCurrentFiles = () => {
    switch (currentUploadType.value) {
      case "before":
        return beforeModelValue.value || [];
      case "after":
        return afterModelValue.value || [];
      case "issue":
        return issueModelValue.value || [];
      default:
        return [];
    }
  };
  // èŽ·å–ä¸Šä¼ ç±»åž‹æ–‡æœ¬
  const getUploadTypeText = () => {
    switch (currentUploadType.value) {
      case "before":
        return "生产前";
      case "after":
        return "生产中";
      case "issue":
        return "生产后";
      default:
        return "";
    }
  };
  // å¤„理上传文件更新
  const handleUploadUpdate = files => {
    uploadFiles.value = files;
  };
  // è®¾ç½®å¼‚常状态
  const setExceptionStatus = status => {
    hasException.value = status;
  };
  // è·³è½¬åˆ°æ–°å¢žæŠ¥ä¿®é¡µé¢
  const goToRepair = () => {
    try {
      // å­˜å‚¨å½“前任务信息到本地存储,供报修页面使用
      const taskInfo = {
        taskId: infoData.value?.taskId || infoData.value?.id,
        taskName: infoData.value?.taskName,
        inspectionLocation: infoData.value?.inspectionLocation,
        inspector: infoData.value?.inspector,
        // ä¼ é€’当前上传的文件信息
        uploadedFiles: {
          before: beforeModelValue.value,
          after: afterModelValue.value,
          issue: issueModelValue.value,
        },
      };
      uni.setStorageSync("repairTaskInfo", JSON.stringify(taskInfo));
      // è·³è½¬åˆ°æ–°å¢žæŠ¥ä¿®é¡µé¢
      uni.navigateTo({
        url: "/pages/equipmentManagement/repair/add",
      });
      // å…³é—­ä¸Šä¼ å¼¹çª—
      closeUploadDialog();
    } catch (error) {
      console.error("跳转报修页面失败:", error);
      uni.showToast({
        title: "跳转失败,请重试",
        icon: "error",
      });
    }
  };
  // æäº¤ä¸Šä¼ 
  const submitUpload = async () => {
    try {
      // æ£€æŸ¥æ˜¯å¦é€‰æ‹©äº†å¼‚常状态
      if (hasException.value === null) {
        uni.showToast({
          title: "请选择是否存在异常",
          icon: "none",
        });
        return;
      }
      // æ£€æŸ¥æ˜¯å¦æœ‰ä»»ä½•文件上传
      const totalFiles =
        beforeModelValue.value.length +
        afterModelValue.value.length +
        issueModelValue.value.length;
      if (totalFiles === 0) {
        uni.showToast({
          title: "请先上传文件",
          icon: "none",
        });
        return;
      }
      // æ˜¾ç¤ºæäº¤ä¸­çš„加载提示
      showLoadingToast("提交中...");
      // æŒ‰ç…§æ‚¨çš„逻辑合并所有分类的文件
      let arr = [];
      if (beforeModelValue.value.length > 0) {
        arr.push(...beforeModelValue.value);
      }
      if (afterModelValue.value.length > 0) {
        arr.push(...afterModelValue.value);
      }
      if (issueModelValue.value.length > 0) {
        arr.push(...issueModelValue.value);
      }
      // ä¼ ç»™åŽç«¯çš„临时文件ID列表(tempFileIds)
      // å…¼å®¹ï¼šæœ‰äº›æŽ¥å£å¯èƒ½è¿”回 tempId / tempFileId / id
      let tempFileIds = [];
      if (arr !== null && arr.length > 0) {
        tempFileIds = arr
          .map(item => item?.tempId ?? item?.tempFileId ?? item?.id)
          .filter(v => v !== undefined && v !== null && v !== "");
      }
      // æäº¤æ•°æ®
      infoData.value.storageBlobDTO = arr;
      // æ·»åŠ å¼‚å¸¸çŠ¶æ€ä¿¡æ¯
      infoData.value.hasException = hasException.value;
      infoData.value.tempFileIds = tempFileIds;
      const result = await uploadInspectionTask({ ...infoData.value });
      // æ£€æŸ¥æäº¤ç»“æžœ
      if (result && (result.code === 200 || result.success)) {
        // æäº¤æˆåŠŸ
        closeToast(); // å…³é—­åŠ è½½æç¤º
        uni.showToast({
          title: "提交成功",
          icon: "success",
        });
        // å…³é—­å¼¹çª—
        closeUploadDialog();
        // åˆ·æ–°åˆ—表
        setTimeout(() => {
          reloadPage();
        }, 500);
      } else {
        // æäº¤å¤±è´¥
        closeToast();
        uni.showToast({
          title: result?.msg || result?.message || "提交失败",
          icon: "error",
        });
      }
    } catch (error) {
      console.error("提交上传失败:", error);
      closeToast(); // å…³é—­åŠ è½½æç¤º
      let errorMessage = "提交失败";
      if (error.message) {
        errorMessage = error.message;
      } else if (error.msg) {
        errorMessage = error.msg;
      } else if (typeof error === "string") {
        errorMessage = error;
      }
      uni.showToast({
        title: errorMessage,
        icon: "error",
      });
    }
    // å°†ä»»åŠ¡ä¿¡æ¯ä¼ é€’åˆ°ä¸Šä¼ é¡µé¢
    const taskData = encodeURIComponent(JSON.stringify(task));
    uni.navigateTo({
      url: `/pages/inspectionUpload/upload?taskInfo=${taskData}`,
    });
  };
  // å›¾ç‰‡ä¸Šä¼ (可选择图片上传或者是相机拍照)
  const startUploadForTask = async (task, type) => {
    // ç›´æŽ¥æ‰“开上传弹窗
    // æ‰“开上传页面
    openUploadDialog(task);
  };
  // æŸ¥çœ‹é™„ä»¶
  // æŸ¥çœ‹é™„ä»¶ - è·³è½¬åˆ°é™„件页面
  const viewAttachments = async task => {
    try {
      currentViewTask.value = task;
      currentViewType.value = "before";
      // è§£æžæ–°çš„æ•°æ®ç»“æž„
      attachmentList.value = [];
      // åŽç«¯åæ˜¾å­—段(你提供的数据结构):
      // - commonFileListBefore:生产前(通常 type=10)
      // - commonFileListAfter:生产中(通常 type=11)
      // - commonFileList:可能是全部/兜底(若包含生产后,一般 type=12)
      const allList = Array.isArray(task?.commonFileList)
        ? task.commonFileList
        : [];
      const beforeList = Array.isArray(task?.commonFileListBefore)
        ? task.commonFileListBefore
        : allList.filter(f => f?.type === 10);
      const afterList = Array.isArray(task?.commonFileListAfter)
        ? task.commonFileListAfter
        : allList.filter(f => f?.type === 11);
      // å¦‚果后端后续补了 commonFileListIssue,则优先用;否则从 commonFileList é‡ŒæŒ‰ type=12 å…œåº•
      const issueList = Array.isArray(task?.commonFileListIssue)
        ? task.commonFileListIssue
        : allList.filter(f => f?.type === 12);
      const mapToViewFile = (file, viewType) => {
        const u = normalizeFileUrl(file?.url || file?.downloadUrl || "");
        return {
          ...file,
          // ç”¨äºŽä¸‰æ ‡ç­¾é¡µåˆ†ç»„:0=生产前 1=生产中 2=生产后
          type: viewType,
          name: file?.name || file?.originalFilename || file?.bucketFilename,
          bucketFilename: file?.bucketFilename || file?.name,
          originalFilename: file?.originalFilename || file?.name,
          url: u,
          downloadUrl: u,
          size: file?.size || file?.byteSize,
        };
      };
      attachmentList.value.push(...beforeList.map(f => mapToViewFile(f, 0)));
      attachmentList.value.push(...afterList.map(f => mapToViewFile(f, 1)));
      attachmentList.value.push(...issueList.map(f => mapToViewFile(f, 2)));
      showAttachmentDialog.value = true;
    } catch (error) {
      uni.showToast({
        title: "获取附件失败",
        icon: "error",
      });
    }
  };
  // å…³é—­é™„件查看弹窗
  const closeAttachmentDialog = () => {
    showAttachmentDialog.value = false;
    currentViewTask.value = null;
    attachmentList.value = [];
    currentViewType.value = "before";
  };
  // åˆ‡æ¢æŸ¥çœ‹ç±»åž‹
  const switchViewType = type => {
    currentViewType.value = type;
  };
  // æ ¹æ®type获取对应分类的附件
  const getAttachmentsByType = typeValue => {
    return attachmentList.value.filter(file => file.type === typeValue) || [];
  };
  // èŽ·å–type值
  const getTabType = () => {
    switch (currentUploadType.value) {
      case "before":
        return 10;
      case "after":
        return 11;
      case "issue":
        return 12;
      default:
        return 10;
    }
  };
  // èŽ·å–å½“å‰æŸ¥çœ‹ç±»åž‹çš„é™„ä»¶
  const getCurrentViewAttachments = () => {
    switch (currentViewType.value) {
      case "before":
        return getAttachmentsByType(0);
      case "after":
        return getAttachmentsByType(1);
      case "issue":
        return getAttachmentsByType(2);
      default:
        return [];
    }
    const taskData = encodeURIComponent(JSON.stringify(task));
    uni.navigateTo({
      url: `/pages/inspectionUpload/attachment?taskInfo=${taskData}`,
    });
  };
  // åˆ¤æ–­æ˜¯å¦ä¸ºå›¾ç‰‡æ–‡ä»¶
@@ -1058,475 +493,6 @@
      title: "视频播放失败",
      icon: "error",
    });
  };
  // æ‹ç…§/拍视频(真机优先用 chooseMedia;不支持则降级)
  const chooseMedia = type => {
    if (getCurrentFiles().length >= uploadConfig.limit) {
      uni.showToast({
        title: `最多只能选择${uploadConfig.limit}个文件`,
        icon: "none",
      });
      return;
    }
    const remaining = uploadConfig.limit - getCurrentFiles().length;
    // ä¼˜å…ˆï¼šchooseMedia(支持 image/video)
    if (typeof uni.chooseMedia === "function") {
      uni.chooseMedia({
        count: Math.min(remaining, 1),
        mediaType: [type || "image"],
        sizeType: ["compressed", "original"],
        sourceType: ["camera"],
        success: res => {
          try {
            const files = res?.tempFiles || [];
            if (!files.length) throw new Error("未获取到文件");
            files.forEach((tf, idx) => {
              const filePath = tf.tempFilePath || tf.path || "";
              const fileType = tf.fileType || type || "image";
              const ext = fileType === "video" ? "mp4" : "jpg";
              const file = {
                tempFilePath: filePath,
                path: filePath,
                type: fileType,
                name: `${fileType}_${Date.now()}_${idx}.${ext}`,
                size: tf.size || 0,
                duration: tf.duration || 0,
                createTime: Date.now(),
                uid: Date.now() + Math.random() + idx,
              };
              handleBeforeUpload(file);
            });
          } catch (e) {
            console.error("处理拍摄结果失败:", e);
            uni.showToast({ title: "处理文件失败", icon: "error" });
          }
        },
        fail: err => {
          console.error("拍摄失败:", err);
          uni.showToast({ title: "拍摄失败", icon: "error" });
        },
      });
      return;
    }
    // é™çº§ï¼šchooseImage / chooseVideo
    if (type === "video") {
      chooseVideo();
    } else {
      uni.chooseImage({
        count: 1,
        sizeType: ["compressed", "original"],
        sourceType: ["camera"],
        success: res => {
          const tempFilePath = res?.tempFilePaths?.[0];
          const tempFile = res?.tempFiles?.[0] || {};
          if (!tempFilePath) return;
          handleBeforeUpload({
            tempFilePath,
            path: tempFilePath,
            type: "image",
            name: `photo_${Date.now()}.jpg`,
            size: tempFile.size || 0,
            createTime: Date.now(),
            uid: Date.now() + Math.random(),
          });
        },
      });
    }
  };
  // æ‹ç…§
  const chooseImage = () => {
    if (uploadFiles.value.length >= uploadConfig.limit) {
      uni.showToast({
        title: `最多只能拍摄${uploadConfig.limit}个文件`,
        icon: "none",
      });
      return;
    }
    uni.chooseMedia({
      count: 1,
      mediaType: ["image", "video"],
      sizeType: ["compressed", "original"],
      sourceType: ["camera"],
      success: res => {
        try {
          if (!res.tempFiles || res.tempFiles.length === 0) {
            throw new Error("未获取到图片文件");
          }
          const tempFilePath = res.tempFiles[0];
          const tempFile =
            res.tempFiles && res.tempFiles[0] ? res.tempFiles[0] : {};
          const file = {
            tempFilePath: tempFilePath,
            path: tempFilePath, // ä¿æŒå…¼å®¹æ€§
            type: "image",
            name: `photo_${Date.now()}.jpg`,
            size: tempFile.size || 0,
            createTime: new Date().getTime(),
            uid: Date.now() + Math.random(),
          };
          handleBeforeUpload(file);
        } catch (error) {
          console.error("处理拍照结果失败:", error);
          uni.showToast({
            title: "处理图片失败",
            icon: "error",
          });
        }
      },
      fail: err => {
        console.error("拍照失败:", err);
        uni.showToast({
          title: "拍照失败: " + (err.errMsg || "未知错误"),
          icon: "error",
        });
      },
    });
  };
  // æ‹è§†é¢‘
  const chooseVideo = () => {
    if (uploadFiles.value.length >= uploadConfig.limit) {
      uni.showToast({
        title: `最多只能拍摄${uploadConfig.limit}个文件`,
        icon: "none",
      });
      return;
    }
    uni.chooseVideo({
      sourceType: ["camera"],
      maxDuration: uploadConfig.maxVideoDuration,
      camera: "back",
      success: res => {
        try {
          if (!res.tempFilePath) {
            throw new Error("未获取到视频文件");
          }
          const file = {
            tempFilePath: res.tempFilePath,
            path: res.tempFilePath, // ä¿æŒå…¼å®¹æ€§
            type: "video",
            name: `video_${Date.now()}.mp4`,
            size: res.size || 0,
            duration: res.duration || 0,
            createTime: new Date().getTime(),
            uid: Date.now() + Math.random(),
          };
          handleBeforeUpload(file);
        } catch (error) {
          console.error("处理拍视频结果失败:", error);
          uni.showToast({
            title: "处理视频失败",
            icon: "error",
          });
        }
      },
      fail: err => {
        console.error("拍视频失败:", err);
        uni.showToast({
          title: "拍视频失败: " + (err.errMsg || "未知错误"),
          icon: "error",
        });
      },
    });
  };
  // åˆ é™¤æ–‡ä»¶
  const removeFile = index => {
    uni.showModal({
      title: "确认删除",
      content: "确定要删除这个文件吗?",
      success: res => {
        if (res.confirm) {
          // æ ¹æ®å½“前上传类型删除对应分类的文件
          switch (currentUploadType.value) {
            case "before":
              beforeModelValue.value.splice(index, 1);
              break;
            case "after":
              afterModelValue.value.splice(index, 1);
              break;
            case "issue":
              issueModelValue.value.splice(index, 1);
              break;
          }
          uni.showToast({
            title: "删除成功",
            icon: "success",
          });
        }
      },
    });
  };
  // æ£€æŸ¥ç½‘络连接
  const checkNetworkConnection = () => {
    return new Promise(resolve => {
      uni.getNetworkType({
        success: res => {
          if (res.networkType === "none") {
            resolve(false);
          } else {
            resolve(true);
          }
        },
        fail: () => {
          resolve(false);
        },
      });
    });
  };
  // ä¸Šä¼ å‰æ ¡éªŒ
  const handleBeforeUpload = async file => {
    // æ ¡éªŒæ–‡ä»¶ç±»åž‹
    if (
      uploadConfig.fileType &&
      Array.isArray(uploadConfig.fileType) &&
      uploadConfig.fileType.length > 0
    ) {
      const fileName = file.name || "";
      const fileExtension = fileName
        ? fileName.split(".").pop().toLowerCase()
        : "";
      // æ ¹æ®æ–‡ä»¶ç±»åž‹ç¡®å®šæœŸæœ›çš„æ‰©å±•名
      let expectedTypes = [];
      if (file.type === "image") {
        expectedTypes = ["jpg", "jpeg", "png", "gif", "webp"];
      } else if (file.type === "video") {
        expectedTypes = ["mp4", "mov", "avi", "wmv"];
      }
      // æ£€æŸ¥æ–‡ä»¶æ‰©å±•名是否在允许的类型中
      if (fileExtension && expectedTypes.length > 0) {
        const isAllowed = expectedTypes.some(
          type => uploadConfig.fileType.includes(type) && type === fileExtension
        );
        if (!isAllowed) {
          uni.showToast({
            title: `文件格式不支持,请拍摄 ${expectedTypes.join("/")} æ ¼å¼çš„æ–‡ä»¶`,
            icon: "none",
          });
          return false;
        }
      }
    }
    // æ ¡éªŒé€šè¿‡ï¼Œå¼€å§‹ä¸Šä¼ 
    uploadFile(file);
    return true;
  };
  // æ–‡ä»¶ä¸Šä¼ å¤„理(真机走 uni.uploadFile)
  const uploadFile = async file => {
    uploading.value = true;
    uploadProgress.value = 0;
    number.value++; // å¢žåŠ ä¸Šä¼ è®¡æ•°
    // ç¡®ä¿token存在
    const token = getToken();
    if (!token) {
      handleUploadError("用户未登录");
      return;
    }
    const typeValue = getTabType(); // ç”Ÿäº§å‰:10, ç”Ÿäº§ä¸­:11, ç”Ÿäº§åŽ:12
    uploadWithUniUploadFile(
      file,
      file.tempFilePath || file.path || "",
      typeValue,
      token
    );
  };
  // ä½¿ç”¨uni.uploadFile上传(非H5环境或H5回退方案)
  const uploadWithUniUploadFile = (file, filePath, typeValue, token) => {
    if (!filePath) {
      handleUploadError("文件路径不存在");
      return;
    }
    const uploadTask = uni.uploadFile({
      url: uploadFileUrl.value,
      filePath: filePath,
      name: "file",
      formData: {
        type: typeValue,
      },
      header: {
        Authorization: `Bearer ${token}`,
      },
      success: res => {
        try {
          if (res.statusCode === 200) {
            const response = JSON.parse(res.data);
            if (response.code === 200) {
              handleUploadSuccess(response, file);
              uni.showToast({
                title: "上传成功",
                icon: "success",
              });
            } else {
              handleUploadError(response.msg || "服务器返回错误");
            }
          } else {
            handleUploadError(`服务器错误,状态码: ${res.statusCode}`);
          }
        } catch (e) {
          console.error("解析响应失败:", e);
          console.error("原始响应数据:", res.data);
          handleUploadError("响应数据解析失败: " + e.message);
        }
      },
      fail: err => {
        console.error("上传失败:", err.errMsg || err);
        number.value--; // ä¸Šä¼ å¤±è´¥æ—¶å‡å°‘计数
        let errorMessage = "上传失败";
        if (err.errMsg) {
          if (err.errMsg.includes("statusCode: null")) {
            errorMessage = "网络连接失败,请检查网络设置";
          } else if (err.errMsg.includes("timeout")) {
            errorMessage = "上传超时,请重试";
          } else if (err.errMsg.includes("fail")) {
            errorMessage = "上传失败,请检查网络连接";
          } else {
            errorMessage = err.errMsg;
          }
        }
        handleUploadError(errorMessage);
      },
      complete: () => {
        uploading.value = false;
        uploadProgress.value = 0;
      },
    });
    // ç›‘听上传进度
    if (uploadTask && uploadTask.onProgressUpdate) {
      uploadTask.onProgressUpdate(res => {
        uploadProgress.value = res.progress;
      });
    }
  };
  // ä¸Šä¼ å¤±è´¥å¤„理
  const handleUploadError = (message = "上传文件失败", showRetry = false) => {
    uploading.value = false;
    uploadProgress.value = 0;
    if (showRetry) {
      uni.showModal({
        title: "上传失败",
        content: message + ",是否重试?",
        success: res => {
          if (res.confirm) {
            // ç”¨æˆ·é€‰æ‹©é‡è¯•,这里可以重新触发上传
          }
        },
      });
    } else {
      uni.showToast({
        title: message,
        icon: "error",
      });
    }
  };
  // ä¸Šä¼ æˆåŠŸå›žè°ƒ
  const handleUploadSuccess = (res, file) => {
    console.log("上传成功响应:", res);
    // å¤„理不同的数据结构:可能是数组,也可能是单个对象
    let uploadedFile = null;
    uploadedFile = res.data;
    if (!uploadedFile) {
      console.error("无法解析上传响应数据:", res);
      number.value--; // ä¸Šä¼ å¤±è´¥æ—¶å‡å°‘计数
      handleUploadError("上传响应数据格式错误", false);
      return;
    }
    // æ ¹æ®å½“前上传类型设置type字段
    let typeValue = 0; // é»˜è®¤ä¸ºç”Ÿäº§å‰
    switch (currentUploadType.value) {
      case "before":
        typeValue = 0;
        break;
      case "after":
        typeValue = 1;
        break;
      case "issue":
        typeValue = 2;
        break;
    }
    // ç¡®ä¿ä¸Šä¼ çš„æ–‡ä»¶æ•°æ®å®Œæ•´ï¼ŒåŒ…含id和type
    const fileData = {
      ...file,
      id: uploadedFile.id, // æ·»åŠ æœåŠ¡å™¨è¿”å›žçš„id
      tempId: uploadedFile.tempId ?? uploadedFile.tempFileId ?? uploadedFile.id,
      url:
        uploadedFile.url ||
        uploadedFile.downloadUrl ||
        file.tempFilePath ||
        file.path,
      bucketFilename:
        uploadedFile.bucketFilename || uploadedFile.originalFilename || file.name,
      downloadUrl: uploadedFile.downloadUrl || uploadedFile.url,
      size: uploadedFile.size || uploadedFile.byteSize || file.size,
      createTime: uploadedFile.createTime || new Date().getTime(),
      type: typeValue, // æ·»åŠ ç±»åž‹å­—æ®µï¼š0=生产前, 1=生产中, 2=生产后
    };
    uploadList.value.push(fileData);
    // ç«‹å³æ·»åŠ åˆ°å¯¹åº”çš„åˆ†ç±»ï¼Œä¸ç­‰å¾…æ‰€æœ‰æ–‡ä»¶ä¸Šä¼ å®Œæˆ
    switch (currentUploadType.value) {
      case "before":
        beforeModelValue.value.push(fileData);
        break;
      case "after":
        afterModelValue.value.push(fileData);
        break;
      case "issue":
        issueModelValue.value.push(fileData);
        break;
    }
    // é‡ç½®ä¸Šä¼ åˆ—表(因为已经添加到对应分类了)
    uploadList.value = [];
    number.value = 0;
  };
  // ä¸Šä¼ ç»“束处理(已废弃,现在在handleUploadSuccess中直接处理)
  const uploadedSuccessfully = () => {
    // æ­¤å‡½æ•°å·²ä¸å†ä½¿ç”¨ï¼Œæ–‡ä»¶ä¸Šä¼ æˆåŠŸåŽç«‹å³æ·»åŠ åˆ°å¯¹åº”åˆ†ç±»
  };
  // æ ¼å¼åŒ–文件大小
  const formatFileSize = size => {
    if (!size) return "";
    if (size < 1024) return size + "B";
    if (size < 1024 * 1024) return (size / 1024).toFixed(1) + "KB";
    return (size / (1024 * 1024)).toFixed(1) + "MB";
  };
</script>
@@ -1725,416 +691,6 @@
    display: flex;
    align-items: center;
    justify-content: center;
  }
  /* ä¸Šä¼ å¼¹çª—样式 */
  .upload-popup-content {
    background: #fff;
    border-radius: 12px;
    width: 100%;
    min-height: 300px;
    max-height: 70vh;
    overflow: hidden;
    display: flex;
    flex-direction: column;
    box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
  }
  .upload-popup-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 15px 20px;
    border-bottom: 1px solid #eee;
  }
  .upload-popup-title {
    font-size: 16px;
    font-weight: 600;
    color: #333;
  }
  .upload-popup-body {
    flex: 1;
    padding: 20px;
    overflow-y: auto;
  }
  .upload-popup-footer {
    display: flex;
    justify-content: flex-end;
    padding: 15px 20px;
    border-top: 1px solid #eee;
    gap: 10px;
  }
  /* ç®€åŒ–上传组件样式 */
  .simple-upload-area {
    padding: 15px;
  }
  .upload-buttons {
    display: flex;
    gap: 10px;
    margin-bottom: 15px;
  }
  .file-list {
    margin-top: 15px;
    display: flex;
    flex-wrap: wrap;
    gap: 12px;
  }
  .file-item {
    display: flex;
    flex-direction: column;
    align-items: center;
    background: #fff;
    border-radius: 12px;
    padding: 8px;
    border: 1px solid #e9ecef;
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
    transition: all 0.3s ease;
    width: calc(50% - 6px);
    min-width: 120px;
  }
  .file-item:hover {
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
    transform: translateY(-2px);
  }
  .file-preview-container {
    position: relative;
    margin-bottom: 8px;
  }
  .file-preview {
    width: 80px;
    height: 80px;
    border-radius: 8px;
    object-fit: cover;
    border: 2px solid #f0f0f0;
  }
  .video-preview {
    width: 80px;
    height: 80px;
    background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
    border-radius: 8px;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    border: 2px solid #f0f0f0;
  }
  .video-text {
    font-size: 12px;
    color: #666;
    margin-top: 4px;
  }
  .delete-btn {
    position: absolute;
    top: -6px;
    right: -6px;
    width: 20px;
    height: 20px;
    background: #ff4757;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    box-shadow: 0 2px 4px rgba(255, 71, 87, 0.3);
    transition: all 0.3s ease;
  }
  .delete-btn:hover {
    background: #ff3742;
    transform: scale(1.1);
  }
  .file-info {
    text-align: center;
    width: 100%;
  }
  .file-name {
    font-size: 12px;
    color: #333;
    font-weight: 500;
    display: block;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    max-width: 100px;
  }
  .file-size {
    font-size: 10px;
    color: #999;
    margin-top: 2px;
    display: block;
  }
  .empty-state {
    text-align: center;
    padding: 40px 20px;
    color: #999;
    font-size: 14px;
    background: #f8f9fa;
    border-radius: 8px;
    border: 2px dashed #ddd;
  }
  .upload-progress {
    margin: 15px 0;
    padding: 0 10px;
  }
  /* ä¸Šä¼ æ ‡ç­¾é¡µæ ·å¼ */
  .upload-tabs {
    display: flex;
    background: #f8f9fa;
    border-radius: 8px;
    margin-bottom: 15px;
    padding: 4px;
  }
  .tab-item {
    flex: 1;
    text-align: center;
    padding: 8px 12px;
    font-size: 14px;
    color: #666;
    border-radius: 6px;
    transition: all 0.3s ease;
    cursor: pointer;
  }
  .tab-item.active {
    background: #409eff;
    color: #fff;
    font-weight: 500;
  }
  .tab-item:hover:not(.active) {
    background: #e9ecef;
    color: #333;
  }
  /* å¼‚常状态选择样式 */
  .exception-section {
    margin-bottom: 20px;
    padding: 15px;
    background: #f8f9fa;
    border-radius: 8px;
    border: 1px solid #e9ecef;
  }
  .section-title {
    display: block;
    font-size: 14px;
    font-weight: 600;
    color: #333;
    margin-bottom: 12px;
  }
  .exception-options {
    display: flex;
    gap: 12px;
  }
  .exception-option {
    flex: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    padding: 12px 16px;
    background: #fff;
    border: 2px solid #e9ecef;
    border-radius: 8px;
    cursor: pointer;
    transition: all 0.3s ease;
    font-size: 14px;
    color: #666;
  }
  .exception-option.active {
    border-color: #409eff;
    background: #f0f8ff;
    color: #409eff;
    font-weight: 500;
  }
  .exception-option:hover:not(.active) {
    border-color: #d9d9d9;
    background: #fafafa;
  }
  /* ç»Ÿè®¡ä¿¡æ¯æ ·å¼ */
  .upload-summary {
    margin-top: 15px;
    padding: 10px;
    background: #f8f9fa;
    border-radius: 6px;
    border-left: 3px solid #409eff;
  }
  .summary-text {
    font-size: 12px;
    color: #666;
    line-height: 1.4;
  }
  /* æŸ¥çœ‹é™„件弹窗样式 */
  .attachment-popup-content {
    background: #fff;
    border-radius: 12px;
    width: 100%;
    min-height: 400px;
    max-height: 70vh;
    overflow: hidden;
    display: flex;
    flex-direction: column;
    box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
  }
  .attachment-popup-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 15px 20px;
    border-bottom: 1px solid #eee;
    background: #f8f9fa;
  }
  .attachment-popup-title {
    font-size: 16px;
    font-weight: 600;
    color: #333;
  }
  .close-btn-attachment {
    width: 28px;
    height: 28px;
    border-radius: 50%;
    background: #f5f5f5;
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    transition: all 0.3s ease;
  }
  .close-btn-attachment:hover {
    background: #e9ecef;
    transform: scale(1.1);
  }
  .attachment-popup-body {
    flex: 1;
    padding: 15px 20px;
    overflow-y: auto;
  }
  .attachment-tabs {
    display: flex;
    background: #f8f9fa;
    border-radius: 8px;
    margin-bottom: 15px;
    padding: 4px;
  }
  .attachment-content {
    min-height: 200px;
  }
  .attachment-list {
    display: flex;
    flex-wrap: wrap;
    gap: 12px;
  }
  .attachment-item {
    display: flex;
    flex-direction: column;
    align-items: center;
    background: #fff;
    border-radius: 12px;
    padding: 8px;
    border: 1px solid #e9ecef;
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
    transition: all 0.3s ease;
    width: calc(33.33% - 8px);
    min-width: 100px;
    cursor: pointer;
  }
  .attachment-item:hover {
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
    transform: translateY(-2px);
  }
  .attachment-preview-container {
    margin-bottom: 8px;
  }
  .attachment-preview {
    width: 80px;
    height: 80px;
    border-radius: 8px;
    object-fit: cover;
    border: 2px solid #f0f0f0;
  }
  .attachment-video-preview {
    width: 80px;
    height: 80px;
    background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
    border-radius: 8px;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    border: 2px solid #f0f0f0;
  }
  .attachment-info {
    text-align: center;
    width: 100%;
  }
  .attachment-name {
    font-size: 12px;
    color: #333;
    font-weight: 500;
    display: block;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    max-width: 80px;
  }
  .attachment-size {
    font-size: 10px;
    color: #999;
    margin-top: 2px;
    display: block;
  }
  .attachment-empty {
    text-align: center;
    padding: 60px 20px;
    color: #999;
    font-size: 14px;
    background: #f8f9fa;
    border-radius: 8px;
    border: 2px dashed #ddd;
  }
  /* è§†é¢‘预览弹窗样式 */
src/pages/inspectionUpload/upload.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,982 @@
<template>
  <view class="inspection-upload-page">
    <!-- é¡µé¢å¤´éƒ¨ -->
    <PageHeader title="上传巡检记录"
                @back="goBack" />
    <!-- é¡µé¢å†…容 -->
    <view class="upload-content">
      <!-- ä»»åŠ¡ä¿¡æ¯å¡ç‰‡ -->
      <view class="task-info-card"
            v-if="taskInfo">
        <view class="task-info-header">
          <text class="task-name">{{ taskInfo.taskName }}</text>
        </view>
        <view class="task-info-body">
          <view class="info-item">
            <text class="info-label">任务ID</text>
            <text class="info-value">{{ taskInfo.taskId || taskInfo.id }}</text>
          </view>
          <view class="info-item">
            <text class="info-label">巡检位置</text>
            <text class="info-value">{{ taskInfo.inspectionLocation || '-' }}</text>
          </view>
          <view class="info-item">
            <text class="info-label">执行人</text>
            <text class="info-value">{{ taskInfo.inspector || '-' }}</text>
          </view>
        </view>
      </view>
      <!-- å¼‚常状态选择 -->
      <view class="section-card">
        <view class="section-title">巡检状态</view>
        <view class="exception-options">
          <view class="exception-option"
                :class="{ active: hasException === false }"
                @click="setExceptionStatus(false)">
            <u-icon name="checkmark-circle"
                    size="20"
                    color="#52c41a"></u-icon>
            <text class="option-text">正常</text>
          </view>
          <view class="exception-option"
                :class="{ active: hasException === true }"
                @click="setExceptionStatus(true)">
            <u-icon name="close-circle"
                    size="20"
                    color="#ff4d4f"></u-icon>
            <text class="option-text">存在异常</text>
          </view>
        </view>
      </view>
      <!-- å¼‚常描述(仅在异常时显示) -->
      <view class="section-card"
            v-if="hasException === true">
        <view class="section-title">异常描述</view>
        <textarea v-model="abnormalDescription"
                  class="exception-textarea"
                  maxlength="500"
                  placeholder="请描述异常情况..." />
      </view>
      <!-- åˆ†ç±»æ ‡ç­¾é¡µï¼ˆä»…在异常时显示) -->
      <view class="section-card"
            v-if="hasException === true">
        <view class="upload-tabs">
          <view class="tab-item"
                :class="{ active: currentUploadType === 'before' }"
                @click="switchUploadType('before')">
            ç”Ÿäº§å‰
          </view>
          <view class="tab-item"
                :class="{ active: currentUploadType === 'after' }"
                @click="switchUploadType('after')">
            ç”Ÿäº§ä¸­
          </view>
          <view class="tab-item"
                :class="{ active: currentUploadType === 'issue' }"
                @click="switchUploadType('issue')">
            ç”Ÿäº§åŽ
          </view>
        </view>
        <!-- å½“前分类的上传区域 -->
        <view class="upload-area">
          <view class="upload-buttons">
            <u-button type="primary"
                      @click="chooseMedia('image')"
                      :loading="uploading"
                      :disabled="getCurrentFiles().length >= uploadConfig.limit"
                      :customStyle="{ marginRight: '10px', flex: 1 }">
              <u-icon name="camera"
                      size="18"
                      color="#fff"
                      style="margin-right: 5px"></u-icon>
              {{ uploading ? '上传中...' : '拍照' }}
            </u-button>
            <u-button type="success"
                      @click="chooseMedia('video')"
                      :loading="uploading"
                      :disabled="getCurrentFiles().length >= uploadConfig.limit"
                      :customStyle="{ flex: 1 }">
              <uni-icons type="videocam"
                         size="18"
                         color="#fff"
                         style="margin-right: 5px"></uni-icons>
              {{ uploading ? '上传中...' : '拍视频' }}
            </u-button>
          </view>
          <!-- ä¸Šä¼ è¿›åº¦ -->
          <view v-if="uploading"
                class="upload-progress">
            <u-line-progress :percentage="uploadProgress"
                             :showText="true"
                             activeColor="#409eff"></u-line-progress>
          </view>
          <!-- å½“前分类的文件列表 -->
          <view v-if="getCurrentFiles().length > 0"
                class="file-list">
            <view v-for="(file, index) in getCurrentFiles()"
                  :key="index"
                  class="file-item">
              <view class="file-preview-container">
                <image v-if="file.type === 'image' || (file.type !== 'video' && !file.type)"
                       :src="file.url || file.tempFilePath || file.path || file.downloadUrl"
                       class="file-preview"
                       mode="aspectFill" />
                <view v-else-if="file.type === 'video'"
                      class="video-preview">
                  <uni-icons type="videocam"
                             size="18"
                             color="#fff"
                             style="margin-right: 5px"></uni-icons>
                  <text class="video-text">视频</text>
                </view>
                <!-- åˆ é™¤æŒ‰é’® -->
                <view class="delete-btn"
                      @click="removeFile(index)">
                  <u-icon name="close"
                          size="12"
                          color="#fff"></u-icon>
                </view>
              </view>
              <view class="file-info">
                <text class="file-name">{{ file.bucketFilename || file.name || (file.type === 'image' ? '图片' : '视频') }}</text>
                <text class="file-size">{{ formatFileSize(file.size) }}</text>
              </view>
            </view>
          </view>
          <view v-if="getCurrentFiles().length === 0"
                class="empty-state">
            <text>请选择要上传的{{ getUploadTypeText() }}图片或视频</text>
          </view>
        </view>
        <!-- ç»Ÿè®¡ä¿¡æ¯ -->
        <view class="upload-summary">
          <text class="summary-text">
            ç”Ÿäº§å‰: {{ beforeModelValue.length }}个文件 |
            ç”Ÿäº§ä¸­: {{ afterModelValue.length }}个文件 |
            ç”Ÿäº§åŽ: {{ issueModelValue.length }}个文件
          </text>
        </view>
      </view>
      <!-- æ­£å¸¸çŠ¶æ€æç¤º -->
      <view class="normal-tip-card"
            v-if="hasException === false">
        <u-icon name="info-circle"
                size="60"
                color="#52c41a"></u-icon>
        <text class="tip-text">设备运行正常,无需上传照片</text>
      </view>
    </view>
    <!-- åº•部按钮 -->
    <view class="footer-buttons">
      <u-button @click="goBack"
                :customStyle="{ marginRight: '10px' }">取消</u-button>
      <u-button v-if="hasException === true"
                type="warning"
                @click="goToRepair"
                :customStyle="{ marginRight: '10px' }">
        æ–°å¢žæŠ¥ä¿®
      </u-button>
      <u-button type="primary"
                @click="submitUpload">提交</u-button>
    </view>
  </view>
</template>
<script setup>
  import { ref, computed, onMounted } from "vue";
  import { onLoad } from "@dcloudio/uni-app";
  import PageHeader from "@/components/PageHeader.vue";
  import { uploadInspectionTask } from "@/api/inspectionManagement";
  import { getToken } from "@/utils/auth";
  import config from "@/config";
  // ä»»åŠ¡ä¿¡æ¯
  const taskInfo = ref(null);
  // ä¸Šä¼ ç›¸å…³çŠ¶æ€
  const uploading = ref(false);
  const uploadProgress = ref(0);
  // ä¸‰ä¸ªåˆ†ç±»çš„上传状态
  const beforeModelValue = ref([]); // ç”Ÿäº§å‰
  const afterModelValue = ref([]); // ç”Ÿäº§ä¸­
  const issueModelValue = ref([]); // ç”Ÿäº§åŽ
  // å½“前激活的上传类型
  const currentUploadType = ref("before"); // 'before', 'after', 'issue'
  // å¼‚常状态
  const hasException = ref(null); // null: æœªé€‰æ‹©, true: å­˜åœ¨å¼‚常, false: æ­£å¸¸
  // å¼‚常描述
  const abnormalDescription = ref("");
  // ä¸Šä¼ é…ç½®
  const uploadConfig = {
    action: "/common/upload",
    limit: 10,
    fileSize: 50, // MB
    fileType: ["jpg", "jpeg", "png", "mp4", "mov"],
    maxVideoDuration: 60, // ç§’
  };
  // è®¡ç®—上传URL
  const uploadFileUrl = computed(() => {
    const baseUrl = config.baseUrl;
    return baseUrl + uploadConfig.action;
  });
  // é¡µé¢åŠ è½½
  onLoad(options => {
    if (options.taskInfo) {
      try {
        const info = JSON.parse(decodeURIComponent(options.taskInfo));
        taskInfo.value = info;
        // å›žæ˜¾é€»è¾‘:从 taskInfo ä¸­æ¢å¤å·²ä¸Šä¼ çš„æ–‡ä»¶
        const mapFiles = list => {
          if (!list || !Array.isArray(list)) return [];
          return list.map(item => {
            // å¤„理 URL,去除可能的空格
            const finalUrl = (item.url || item.previewURL || "").trim();
            // è‡ªåŠ¨æŽ¨æ–­æ–‡ä»¶ç±»åž‹
            let fileType = item.type;
            if (!fileType && item.contentType) {
              fileType = item.contentType.startsWith("video") ? "video" : "image";
            } else if (!fileType) {
              fileType = "image"; // é»˜è®¤å›¾ç‰‡
            }
            return {
              ...item,
              url: finalUrl,
              name: item.name || item.originalFilename,
              tempId: item.tempId || item.id || item.tempFileId,
              size: item.size || item.byteSize || 0, // æ˜ å°„大小字段
              type: fileType,
              status: "success",
            };
          });
        };
        // æ ¹æ®ç”¨æˆ·è¦æ±‚映射:AfterDTO(生产前), DTO(生产中), BeforeDTO(生产后)
        if (
          info.commonFileListAfterVO &&
          Array.isArray(info.commonFileListAfterVO)
        ) {
          beforeModelValue.value = mapFiles(info.commonFileListAfterVO);
        }
        console.log(beforeModelValue.value, "beforeModelValue");
        if (info.commonFileListVO && Array.isArray(info.commonFileListVO)) {
          afterModelValue.value = mapFiles(info.commonFileListVO);
        }
        if (
          info.commonFileListBeforeVO &&
          Array.isArray(info.commonFileListBeforeVO)
        ) {
          issueModelValue.value = mapFiles(info.commonFileListBeforeVO);
        }
        // å¦‚果有异常描述,也恢复
        if (info.abnormalDescription) {
          abnormalDescription.value = info.abnormalDescription;
        }
        // å¦‚果有异常状态,也恢复
        if (info.hasException !== undefined && info.hasException !== null) {
          hasException.value = info.hasException;
        } else if (
          info.inspectionResult !== undefined &&
          info.inspectionResult !== null
        ) {
          // 0-异常,1-正常
          hasException.value = String(info.inspectionResult) === "0";
        }
        // è‡ªåŠ¨å…œåº•ï¼šå¦‚æžœå­˜åœ¨å·²ä¸Šä¼ æ–‡ä»¶ï¼Œåˆ™å¿…ç„¶æ˜¯å¼‚å¸¸çŠ¶æ€ï¼Œç¡®ä¿ UI æ­£å¸¸æ˜¾ç¤º
        if (
          !hasException.value &&
          (beforeModelValue.value.length > 0 ||
            afterModelValue.value.length > 0 ||
            issueModelValue.value.length > 0)
        ) {
          hasException.value = true;
        }
      } catch (e) {
        console.error("解析任务信息失败:", e);
      }
    }
  });
  // è¿”回上一页
  const goBack = () => {
    uni.navigateBack();
  };
  // åˆ‡æ¢ä¸Šä¼ ç±»åž‹
  const switchUploadType = type => {
    currentUploadType.value = type;
  };
  // èŽ·å–å½“å‰åˆ†ç±»çš„æ–‡ä»¶åˆ—è¡¨
  const getCurrentFiles = () => {
    switch (currentUploadType.value) {
      case "before":
        return beforeModelValue.value || [];
      case "after":
        return afterModelValue.value || [];
      case "issue":
        return issueModelValue.value || [];
      default:
        return [];
    }
  };
  // èŽ·å–ä¸Šä¼ ç±»åž‹æ–‡æœ¬
  const getUploadTypeText = () => {
    switch (currentUploadType.value) {
      case "before":
        return "生产前";
      case "after":
        return "生产中";
      case "issue":
        return "生产后";
      default:
        return "";
    }
  };
  // è®¾ç½®å¼‚常状态
  const setExceptionStatus = status => {
    hasException.value = status;
  };
  // è·³è½¬åˆ°æ–°å¢žæŠ¥ä¿®é¡µé¢
  const goToRepair = () => {
    try {
      const taskData = {
        taskId: taskInfo.value?.taskId || taskInfo.value?.id,
        taskName: taskInfo.value?.taskName,
        inspectionLocation: taskInfo.value?.inspectionLocation,
        inspector: taskInfo.value?.inspector,
        hasException: hasException.value,
        inspectionResult: hasException.value ? 0 : 1, // 0-异常,1-正常
        commonFileListAfterDTO: beforeModelValue.value,
        commonFileListDTO: afterModelValue.value,
        commonFileListBeforeDTO: issueModelValue.value,
        uploadedFiles: {
          before: beforeModelValue.value,
          after: afterModelValue.value,
          issue: issueModelValue.value,
        },
      };
      uni.setStorageSync("repairTaskInfo", JSON.stringify(taskData));
      uni.navigateTo({
        url: "/pages/equipmentManagement/repair/add",
      });
    } catch (error) {
      console.error("跳转报修页面失败:", error);
      uni.showToast({
        title: "跳转失败,请重试",
        icon: "error",
      });
    }
  };
  // æäº¤ä¸Šä¼ 
  const submitUpload = async () => {
    try {
      // æ£€æŸ¥æ˜¯å¦é€‰æ‹©äº†å¼‚常状态
      if (hasException.value === null) {
        uni.showToast({
          title: "请选择巡检状态",
          icon: "none",
        });
        return;
      }
      // å¦‚果是异常状态,检查是否有上传文件和描述
      if (hasException.value === true) {
        const totalFiles =
          beforeModelValue.value.length +
          afterModelValue.value.length +
          issueModelValue.value.length;
        if (totalFiles === 0) {
          uni.showToast({
            title: "请上传异常照片",
            icon: "none",
          });
          return;
        }
        // æ£€æŸ¥æ˜¯å¦å¡«å†™äº†å¼‚常描述
        if (!abnormalDescription.value.trim()) {
          uni.showToast({
            title: "请填写异常描述",
            icon: "none",
          });
          return;
        }
      }
      // æ˜¾ç¤ºæäº¤ä¸­çš„加载提示
      uni.showLoading({
        title: "提交中...",
        mask: true,
      });
      // æŒ‰ç…§é€»è¾‘合并所有分类的文件用于提取ID
      const allFiles = [
        ...beforeModelValue.value,
        ...afterModelValue.value,
        ...issueModelValue.value,
      ];
      // ä¼ ç»™åŽç«¯çš„临时文件ID列表
      let tempFileIds = [];
      if (allFiles.length > 0) {
        tempFileIds = allFiles
          .map(item => item?.tempId ?? item?.tempFileId ?? item?.id)
          .filter(v => v !== undefined && v !== null && v !== "");
      }
      // æäº¤æ•°æ®
      const submitData = {
        ...taskInfo.value,
        commonFileListAfterDTO: beforeModelValue.value, // ç”Ÿäº§å‰
        commonFileListDTO: afterModelValue.value, // ç”Ÿäº§ä¸­
        commonFileListBeforeDTO: issueModelValue.value, // ç”Ÿäº§åŽ
        hasException: hasException.value,
        inspectionResult: hasException.value ? 0 : 1, // 0-异常,1-正常
        abnormalDescription: abnormalDescription.value,
        tempFileIds: tempFileIds,
      };
      const result = await uploadInspectionTask(submitData);
      // æ£€æŸ¥æäº¤ç»“æžœ
      if (result && (result.code === 200 || result.success)) {
        uni.hideLoading();
        uni.showToast({
          title: "提交成功",
          icon: "success",
        });
        // è¿”回列表页并刷新
        setTimeout(() => {
          uni.navigateBack();
        }, 500);
      } else {
        uni.hideLoading();
        uni.showToast({
          title: result?.msg || result?.message || "提交失败",
          icon: "error",
        });
      }
    } catch (error) {
      console.error("提交上传失败:", error);
      uni.hideLoading();
      uni.showToast({
        title: error?.message || "提交失败",
        icon: "error",
      });
    }
  };
  // æ ¼å¼åŒ–文件大小
  const formatFileSize = size => {
    if (!size) return "0 B";
    const units = ["B", "KB", "MB", "GB"];
    let index = 0;
    let fileSize = size;
    while (fileSize >= 1024 && index < units.length - 1) {
      fileSize /= 1024;
      index++;
    }
    return `${fileSize.toFixed(2)} ${units[index]}`;
  };
  // æ‹ç…§/拍视频
  const chooseMedia = type => {
    if (getCurrentFiles().length >= uploadConfig.limit) {
      uni.showToast({
        title: `最多只能选择${uploadConfig.limit}个文件`,
        icon: "none",
      });
      return;
    }
    const remaining = uploadConfig.limit - getCurrentFiles().length;
    // ä¼˜å…ˆä½¿ç”¨ chooseMedia
    if (typeof uni.chooseMedia === "function") {
      uni.chooseMedia({
        count: Math.min(remaining, 1),
        mediaType: [type || "image"],
        sizeType: ["compressed", "original"],
        sourceType: ["camera"],
        success: res => {
          try {
            const files = res?.tempFiles || [];
            if (!files.length) throw new Error("未获取到文件");
            files.forEach((tf, idx) => {
              const filePath = tf.tempFilePath || tf.path || "";
              const fileType = tf.fileType || type || "image";
              const ext = fileType === "video" ? "mp4" : "jpg";
              const file = {
                tempFilePath: filePath,
                path: filePath,
                type: fileType,
                name: `${fileType}_${Date.now()}_${idx}.${ext}`,
                size: tf.size || 0,
                duration: tf.duration || 0,
                createTime: Date.now(),
              };
              uploadFile(file);
            });
          } catch (err) {
            uni.showToast({ title: err.message || "处理文件失败", icon: "none" });
          }
        },
        fail: err => {
          console.error("选择媒体失败:", err);
          uni.showToast({ title: "选择失败", icon: "none" });
        },
      });
    } else {
      // é™çº§æ–¹æ¡ˆ
      if (type === "video") {
        uni.chooseVideo({
          sourceType: ["camera"],
          success: res => {
            const file = {
              tempFilePath: res.tempFilePath,
              path: res.tempFilePath,
              type: "video",
              name: `video_${Date.now()}.mp4`,
              size: res.size || 0,
              duration: res.duration || 0,
              createTime: Date.now(),
            };
            uploadFile(file);
          },
          fail: () => {
            uni.showToast({ title: "选择视频失败", icon: "none" });
          },
        });
      } else {
        uni.chooseImage({
          count: Math.min(remaining, 9),
          sizeType: ["compressed"],
          sourceType: ["camera"],
          success: res => {
            const list = res.tempFilePaths || res.tempFiles || [];
            list.forEach((src, idx) => {
              const path = typeof src === "string" ? src : src.path;
              const file = {
                tempFilePath: path,
                path: path,
                type: "image",
                name: `image_${Date.now()}_${idx}.jpg`,
                size: 0,
                createTime: Date.now(),
              };
              uploadFile(file);
            });
          },
          fail: () => {
            uni.showToast({ title: "选择图片失败", icon: "none" });
          },
        });
      }
    }
  };
  // ä¸Šä¼ å•个文件
  const uploadFile = file => {
    const token = getToken();
    if (!token) {
      uni.showToast({ title: "用户未登录", icon: "none" });
      return;
    }
    uploading.value = true;
    uploadProgress.value = 0;
    const uploadTask = uni.uploadFile({
      url: uploadFileUrl.value,
      filePath: file.tempFilePath,
      name: "files",
      header: {
        Authorization: `Bearer ${token}`,
      },
      formData: {
        type: getTabType(),
      },
      success: res => {
        try {
          const data = JSON.parse(res.data);
          if (data.code === 200) {
            // å…¼å®¹ CommonUpload.vue çš„处理逻辑
            const resultData = Array.isArray(data.data)
              ? data.data[0]
              : data.data;
            // å¤„理 url å’Œ name èµ‹å€¼
            const finalUrl = resultData.url || resultData.previewURL;
            const finalName = resultData.name || resultData.originalFilename;
            const finalId =
              resultData.tempId || resultData.id || resultData.tempFileId;
            const uploadedFile = {
              ...file,
              ...resultData, // åŒ…含后端返回的所有字段
              url: finalUrl,
              name: finalName,
              tempId: finalId,
              status: "success",
            };
            // æ ¹æ®å½“前类型添加到对应数组
            if (currentUploadType.value === "before") {
              beforeModelValue.value.push(uploadedFile);
            } else if (currentUploadType.value === "after") {
              afterModelValue.value.push(uploadedFile);
            } else if (currentUploadType.value === "issue") {
              issueModelValue.value.push(uploadedFile);
            }
            uni.showToast({ title: "上传成功", icon: "success" });
          } else {
            uni.showToast({ title: data.msg || "上传失败", icon: "none" });
          }
        } catch (e) {
          uni.showToast({ title: "解析响应失败", icon: "none" });
        }
      },
      fail: err => {
        console.error("上传失败:", err);
        uni.showToast({ title: "上传失败", icon: "none" });
      },
      complete: () => {
        uploading.value = false;
      },
    });
    // ç›‘听上传进度
    uploadTask.onProgressUpdate(res => {
      uploadProgress.value = res.progress;
    });
  };
  // èŽ·å–type值
  const getTabType = () => {
    switch (currentUploadType.value) {
      case "before":
        return 10;
      case "after":
        return 11;
      case "issue":
        return 12;
      default:
        return 10;
    }
  };
  // åˆ é™¤æ–‡ä»¶
  const removeFile = index => {
    const files = getCurrentFiles();
    files.splice(index, 1);
  };
</script>
<style scoped>
  .inspection-upload-page {
    min-height: 100vh;
    background-color: #f5f5f5;
    padding-bottom: 80px;
  }
  .upload-content {
    padding: 15px;
  }
  /* ä»»åŠ¡ä¿¡æ¯å¡ç‰‡ */
  .task-info-card {
    background: #fff;
    border-radius: 12px;
    padding: 15px;
    margin-bottom: 15px;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
  }
  .task-info-header {
    margin-bottom: 12px;
    padding-bottom: 12px;
    border-bottom: 1px solid #f0f0f0;
  }
  .task-name {
    font-size: 16px;
    font-weight: 600;
    color: #333;
  }
  .task-info-body {
    display: flex;
    flex-direction: column;
    gap: 8px;
  }
  .info-item {
    display: flex;
    justify-content: space-between;
    align-items: center;
  }
  .info-label {
    font-size: 13px;
    color: #999;
  }
  .info-value {
    font-size: 13px;
    color: #666;
  }
  /* é€šç”¨å¡ç‰‡æ ·å¼ */
  .section-card {
    background: #fff;
    border-radius: 12px;
    padding: 15px;
    margin-bottom: 15px;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
  }
  .section-title {
    font-size: 14px;
    font-weight: 600;
    color: #333;
    margin-bottom: 12px;
  }
  /* å¼‚常状态选择 */
  .exception-options {
    display: flex;
    gap: 12px;
  }
  .exception-option {
    flex: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    padding: 14px 16px;
    background: #f8f9fa;
    border: 2px solid #e9ecef;
    border-radius: 8px;
    cursor: pointer;
    transition: all 0.3s ease;
  }
  .exception-option.active {
    border-color: #409eff;
    background: #f0f8ff;
  }
  .option-text {
    font-size: 14px;
    color: #333;
    font-weight: 500;
  }
  /* å¼‚常描述 */
  .exception-textarea {
    width: 100%;
    min-height: 100px;
    padding: 12px;
    background: #f8f9fa;
    border: 1px solid #e9ecef;
    border-radius: 8px;
    font-size: 14px;
    color: #333;
    resize: none;
    box-sizing: border-box;
  }
  .exception-textarea:focus {
    outline: none;
    border-color: #409eff;
    background: #fff;
  }
  /* åˆ†ç±»æ ‡ç­¾é¡µ */
  .upload-tabs {
    display: flex;
    gap: 10px;
    margin-bottom: 15px;
  }
  .tab-item {
    flex: 1;
    padding: 10px;
    text-align: center;
    background: #f5f5f5;
    border-radius: 6px;
    font-size: 13px;
    color: #666;
    cursor: pointer;
    transition: all 0.3s;
  }
  .tab-item.active {
    background: #409eff;
    color: #fff;
  }
  /* ä¸Šä¼ åŒºåŸŸ */
  .upload-area {
    padding: 10px 0;
  }
  .upload-buttons {
    display: flex;
    gap: 10px;
    margin-bottom: 15px;
  }
  .upload-progress {
    margin-bottom: 15px;
  }
  /* æ–‡ä»¶åˆ—表 */
  .file-list {
    display: flex;
    flex-wrap: wrap;
    gap: 10px;
  }
  .file-item {
    width: calc(33.33% - 7px);
  }
  .file-preview-container {
    position: relative;
    width: 100%;
    aspect-ratio: 1;
    border-radius: 8px;
    overflow: hidden;
    background: #f5f5f5;
  }
  .file-preview {
    width: 100%;
    height: 100%;
    object-fit: cover;
  }
  .video-preview {
    width: 100%;
    height: 100%;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    background: #333;
  }
  .video-text {
    font-size: 12px;
    color: #fff;
    margin-top: 5px;
  }
  .delete-btn {
    position: absolute;
    top: 5px;
    right: 5px;
    width: 22px;
    height: 22px;
    background: rgba(0, 0, 0, 0.5);
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
  }
  .file-info {
    margin-top: 5px;
  }
  .file-name {
    display: block;
    font-size: 11px;
    color: #666;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .file-size {
    display: block;
    font-size: 10px;
    color: #999;
    margin-top: 2px;
  }
  .empty-state {
    text-align: center;
    padding: 30px;
    color: #999;
    font-size: 13px;
  }
  /* ç»Ÿè®¡ä¿¡æ¯ */
  .upload-summary {
    margin-top: 15px;
    padding: 10px;
    background: #f8f9fa;
    border-radius: 6px;
    border-left: 3px solid #409eff;
  }
  .summary-text {
    font-size: 12px;
    color: #666;
  }
  /* æ­£å¸¸çŠ¶æ€æç¤º */
  .normal-tip-card {
    background: #f6ffed;
    border: 2px dashed #b7eb8f;
    border-radius: 12px;
    padding: 50px 20px;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    margin-bottom: 15px;
  }
  .normal-tip-card .tip-text {
    margin-top: 15px;
    font-size: 16px;
    color: #52c41a;
    font-weight: 500;
  }
  /* åº•部按钮 */
  .footer-buttons {
    position: fixed;
    bottom: 0;
    left: 0;
    right: 0;
    display: flex;
    padding: 15px;
    background: #fff;
    border-top: 1px solid #f0f0f0;
    box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.05);
  }
</style>
src/pages/inventoryManagement/stockManagement/Qualified.vue
ÎļþÒÑɾ³ý
src/pages/inventoryManagement/stockManagement/Record.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,292 @@
<template>
  <view class="record-container">
    <view class="search-section">
      <view class="search-bar">
        <view class="search-input">
          <up-input
            class="search-text"
            placeholder="请输入产品大类"
            v-model="searchForm.productName"
            @confirm="handleQuery"
            clearable
          />
        </view>
        <view class="filter-button" @click="handleQuery">
          <up-icon name="search" size="24" color="#999"></up-icon>
        </view>
      </view>
    </view>
    <scroll-view scroll-y class="ledger-list" v-if="tableData.length > 0" @scrolltolower="loadMore">
      <view v-for="item in tableData" :key="item.id" class="ledger-item">
        <view class="item-header">
          <view class="item-left">
            <view class="document-icon">
              <up-icon name="file-text" size="16" color="#ffffff"></up-icon>
            </view>
            <text class="item-id">{{ item.productName }}</text>
          </view>
        </view>
        <up-divider></up-divider>
        <view class="item-details">
          <view class="detail-row">
            <text class="detail-label">规格型号</text>
            <text class="detail-value">{{ item.model }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">单位</text>
            <text class="detail-value">{{ item.unit }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">批号</text>
            <text class="detail-value">{{ item.batchNo }}</text>
          </view>
          <view class="quantity-section">
            <view class="quantity-box qualified">
              <text class="q-label">合格库存</text>
              <text class="q-value">{{ item.qualifiedQuantity }}</text>
            </view>
            <view class="quantity-box unqualified">
              <text class="q-label">不合格库存</text>
              <text class="q-value">{{ item.unQualifiedQuantity }}</text>
            </view>
          </view>
          <view class="quantity-section">
            <view class="quantity-box locked">
              <text class="q-label">合格冻结</text>
              <text class="q-value">{{ item.qualifiedLockedQuantity }}</text>
            </view>
            <view class="quantity-box locked">
              <text class="q-label">不合格冻结</text>
              <text class="q-value">{{ item.unQualifiedLockedQuantity }}</text>
            </view>
          </view>
          <view class="detail-row">
            <text class="detail-label">库存预警</text>
            <text class="detail-value">{{ item.warnNum }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">备注</text>
            <text class="detail-value">{{ item.remark || '-' }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">更新时间</text>
            <text class="detail-value">{{ item.updateTime }}</text>
          </view>
        </view>
      </view>
      <up-loadmore :status="loadStatus" />
    </scroll-view>
    <view v-else-if="!loading" class="no-data">
      <up-empty mode="data" text="暂无库存数据"></up-empty>
    </view>
  </view>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue';
import { getStockInventoryListPageCombined } from "@/api/inventoryManagement/stockInventory.js";
const props = defineProps({
  productId: {
    type: Number,
    required: true
  }
});
const tableData = ref([]);
const loading = ref(false);
const loadStatus = ref('loadmore');
const page = reactive({ current: 1, size: 10 });
const total = ref(0);
const searchForm = reactive({
  productName: '',
  topParentProductId: props.productId
});
const handleQuery = () => {
  page.current = 1;
  tableData.value = [];
  getList();
};
const getList = () => {
  if (loading.value) return;
  loading.value = true;
  loadStatus.value = 'loading';
  getStockInventoryListPageCombined({
    ...searchForm,
    current: page.current,
    size: page.size
  }).then(res => {
    loading.value = false;
    const records = res.data.records || [];
    tableData.value = page.current === 1 ? records : [...tableData.value, ...records];
    total.value = res.data.total;
    loadStatus.value = tableData.value.length >= total.value ? 'nomore' : 'loadmore';
  }).catch(() => {
    loading.value = false;
    loadStatus.value = 'loadmore';
  });
};
const loadMore = () => {
  if (loadStatus.value === 'loadmore') {
    page.current++;
    getList();
  }
};
onMounted(() => {
  getList();
});
</script>
<style scoped lang="scss">
.record-container {
  height: 100%;
  display: flex;
  flex-direction: column;
  background-color: #f5f7fa;
}
.search-section {
  padding: 20rpx;
  background-color: #ffffff;
  position: sticky;
  top: 0;
  z-index: 10;
}
.search-bar {
  display: flex;
  align-items: center;
  background-color: #f2f2f2;
  border-radius: 40rpx;
  padding: 0 30rpx;
  height: 80rpx;
}
.search-input {
  flex: 1;
}
.search-text {
  font-size: 28rpx;
}
.filter-button {
  padding-left: 20rpx;
}
.ledger-list {
  flex: 1;
  padding: 20rpx;
  box-sizing: border-box;
}
.ledger-item {
  background-color: #ffffff;
  border-radius: 16rpx;
  padding: 30rpx;
  margin-bottom: 20rpx;
  box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
}
.item-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 20rpx;
}
.item-left {
  display: flex;
  align-items: center;
}
.document-icon {
  width: 40rpx;
  height: 40rpx;
  background: linear-gradient(135deg, #2979ff, #1565c0);
  border-radius: 8rpx;
  display: flex;
  align-items: center;
  justify-content: center;
  margin-right: 16rpx;
}
.item-id {
  font-size: 30rpx;
  font-weight: bold;
  color: #303133;
}
.item-details {
  .detail-row {
    display: flex;
    justify-content: space-between;
    margin-bottom: 16rpx;
    font-size: 26rpx;
    .detail-label {
      color: #909399;
    }
    .detail-value {
      color: #303133;
      font-weight: 500;
    }
  }
}
.quantity-section {
  display: flex;
  gap: 20rpx;
  margin: 20rpx 0;
  .quantity-box {
    flex: 1;
    padding: 16rpx;
    border-radius: 8rpx;
    display: flex;
    flex-direction: column;
    align-items: center;
    .q-label {
      font-size: 22rpx;
      margin-bottom: 8rpx;
    }
    .q-value {
      font-size: 32rpx;
      font-weight: bold;
    }
    &.qualified {
      background-color: #ecf5ff;
      color: #409eff;
    }
    &.unqualified {
      background-color: #fef0f0;
      color: #f56c6c;
    }
    &.locked {
      background-color: #f4f4f5;
      color: #909399;
    }
  }
}
.no-data {
  padding-top: 200rpx;
}
</style>
src/pages/inventoryManagement/stockManagement/Unqualified.vue
ÎļþÒÑɾ³ý
src/pages/inventoryManagement/stockManagement/index.vue
@@ -1,57 +1,104 @@
<template>
  <view class="app-container">
    <PageHeader title="库存管理" @back="goBack" />
    <up-tabs :list="tabs" @click="handleTabClick" :current="activeTab"/>
    <swiper class="swiper-box" :current="activeTab" @change="handleSwiperChange">
      <swiper-item class="swiper-item">
        <qualified-record />
      </swiper-item>
      <swiper-item class="swiper-item">
        <unqualified-record />
      </swiper-item>
    </swiper>
    <PageHeader title="库存管理"
                @back="goBack" />
    <view v-if="loading"
          class="loading-state">
      <up-loading-icon text="加载中..."></up-loading-icon>
    </view>
    <template v-else>
      <up-tabs :list="tabs"
               @click="handleTabClick"
               :current="activeTab" />
      <swiper class="swiper-box"
              :current="activeTab"
              @change="handleSwiperChange">
        <swiper-item class="swiper-item"
                     v-for="tab in products"
                     :key="tab.id">
          <record :product-id="tab.id"
                  v-if="activeTab === products.indexOf(tab) || initializedTabs.includes(tab.id)" />
        </swiper-item>
      </swiper>
    </template>
  </view>
</template>
<script setup>
import { ref } from 'vue';
import PageHeader from "@/components/PageHeader.vue";
import QualifiedRecord from "./Qualified.vue";
import UnqualifiedRecord from "./Unqualified.vue";
  import { ref, onMounted } from "vue";
  import PageHeader from "@/components/PageHeader.vue";
  import Record from "./Record.vue";
  import { productTreeList } from "@/api/basicData/product.js";
const activeTab = ref(0);
const tabs = ref([
  { name: '合格库存' },
  { name: '不合格库存' }
]);
  const activeTab = ref(0);
  const tabs = ref([]);
  const products = ref([]);
  const loading = ref(false);
  const initializedTabs = ref([]);
const handleTabClick = (item) => {
  activeTab.value = item.index;
};
  const handleTabClick = item => {
    activeTab.value = item.index;
    if (!initializedTabs.value.includes(products.value[item.index].id)) {
      initializedTabs.value.push(products.value[item.index].id);
    }
  };
const handleSwiperChange = (e) => {
  activeTab.value = e.detail.current;
};
  const handleSwiperChange = e => {
    const index = e.detail.current;
    activeTab.value = index;
    if (!initializedTabs.value.includes(products.value[index].id)) {
      initializedTabs.value.push(products.value[index].id);
    }
  };
const goBack = () => {
  uni.navigateBack();
};
  const fetchProducts = async () => {
    loading.value = true;
    try {
      const res = await productTreeList();
      // è¿‡æ»¤æ ¹èŠ‚ç‚¹äº§å“
      products.value = res
        .filter(item => item.parentId === null)
        .map(({ id, productName }) => ({ id, productName }));
      tabs.value = products.value.map(p => ({ name: p.productName }));
      if (products.value.length > 0) {
        activeTab.value = 0;
        initializedTabs.value = [products.value[0].id];
      }
    } finally {
      loading.value = false;
    }
  };
  const goBack = () => {
    uni.navigateBack();
  };
  onMounted(() => {
    fetchProducts();
  });
</script>
<style scoped lang="scss">
.app-container {
  display: flex;
  flex-direction: column;
  height: 100vh;
  background-color: #f8f9fa;
}
.swiper-box {
  flex: 1;
}
.swiper-item {
  height: 100%;
}
:deep(.up-tabs) {
  background-color: #fff;
}
  .app-container {
    display: flex;
    flex-direction: column;
    height: 100vh;
    background-color: #f8f9fa;
  }
  .loading-state {
    flex: 1;
    display: flex;
    align-items: center;
    justify-content: center;
  }
  .swiper-box {
    flex: 1;
  }
  .swiper-item {
    height: 100%;
  }
  :deep(.up-tabs) {
    background-color: #fff;
  }
</style>
src/pages/procurementManagement/procurementLedger/detail.vue
@@ -31,8 +31,7 @@
      </up-form-item>
      <up-form-item label="供应商名称"
                    prop="supplierName"
                    required
                    >
                    required>
        <up-input v-model="form.supplierName"
                  readonly
                  :disabled="isReadOnly"
@@ -82,55 +81,6 @@
                  placeholder="请输入"
                  disabled />
      </up-form-item>
      <view class="approval-process">
        <view class="approval-header">
          <text class="approval-title">审核流程</text>
          <text class="approval-desc">每个步骤只能选择一个审批人</text>
        </view>
        <view class="approval-steps">
          <view v-for="(step, stepIndex) in approverNodes"
                :key="stepIndex"
                class="approval-step">
            <view class="step-dot"></view>
            <view class="step-title">
              <text>审批人</text>
            </view>
            <view class="approver-container">
              <view v-if="step.nickName"
                    class="approver-item">
                <view class="approver-avatar">
                  <text class="avatar-text">{{ step.nickName.charAt(0) }}</text>
                  <view class="status-dot"></view>
                </view>
                <view class="approver-info">
                  <text class="approver-name">{{ step.nickName }}</text>
                </view>
                <view class="delete-approver-btn"
                      v-if="!isReadOnly"
                      @click="removeApprover(stepIndex)">×</view>
              </view>
              <view v-else-if="!isReadOnly"
                    class="add-approver-btn"
                    @click="addApprover(stepIndex)">
                <view class="add-circle">+</view>
                <text class="add-label">选择审批人</text>
              </view>
            </view>
            <view class="step-line"
                  v-if="stepIndex < approverNodes.length - 1"></view>
            <view class="delete-step-btn"
                  v-if="approverNodes.length > 1 && !isReadOnly"
                  @click="removeApprovalStep(stepIndex)">删除节点</view>
          </view>
        </view>
        <view class="add-step-btn" v-if="!isReadOnly">
          <u-button icon="plus"
                    plain
                    type="primary"
                    style="width: 100%"
                    @click="addApprovalStep">新增节点</u-button>
        </view>
      </view>
      <up-popup :show="showTimePicker"
                mode="bottom"
                @close="showTimePicker = false">
src/pages/productionDesign/basicParameters/edit.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,290 @@
<template>
  <view class="basic-parameters-edit">
    <PageHeader :title="pageTitle"
                @back="goBack" />
    <up-form ref="formRef"
             :model="form"
             :rules="rules"
             :errorType="['none']"
             label-width="110">
      <up-form-item label="参数编码"
                    prop="paramCode">
        <up-input v-model="form.paramCode"
                  disabled
                  placeholder="自动生成" />
      </up-form-item>
      <up-form-item label="参数名称"
                    prop="paramName"
                    required>
        <up-input v-model="form.paramName"
                  placeholder="请输入参数名称"
                  clearable />
      </up-form-item>
      <up-form-item label="参数类型"
                    prop="paramType"
                    required>
        <up-input v-model="paramTypeText"
                  placeholder="请选择参数类型"
                  readonly
                  @click="showParamTypeSheet = true" />
        <template #right>
          <up-icon name="arrow-right"
                   @click="showParamTypeSheet = true"></up-icon>
        </template>
      </up-form-item>
      <up-form-item label="单位"
                    prop="unit"
                    :required="form.paramType === 1">
        <up-input v-model="form.unit"
                  placeholder="请输入单位"
                  clearable />
      </up-form-item>
      <up-form-item label="取值格式"
                    v-if="form.paramType === 1 || form.paramType === 2"
                    prop="paramFormat">
        <up-input v-model="form.paramFormat"
                  placeholder="请输入取值格式"
                  clearable />
      </up-form-item>
      <up-form-item label="下拉字典"
                    v-else-if="form.paramType === 3"
                    prop="paramFormat">
        <up-input v-model="dictTypeText"
                  placeholder="请选择下拉字典"
                  readonly
                  @click="showDictTypeSheet = true" />
        <template #right>
          <up-icon name="arrow-right"
                   @click="showDictTypeSheet = true"></up-icon>
        </template>
      </up-form-item>
      <up-form-item label="时间格式"
                    v-else-if="form.paramType === 4"
                    prop="paramFormat">
        <up-input v-model="form.paramFormat"
                  placeholder="请选择时间格式"
                  readonly
                  @click="showTimeFormatSheet = true" />
        <template #right>
          <up-icon name="arrow-right"
                   @click="showTimeFormatSheet = true"></up-icon>
        </template>
      </up-form-item>
      <up-form-item label="是否必填"
                    prop="isRequired">
        <view style="display: flex; justify-content: flex-end; width: 100%;">
          <up-switch v-model="form.isRequired"
                     :activeValue="1"
                     :inactiveValue="0" />
        </view>
      </up-form-item>
      <up-form-item label="备注"
                    prop="remark">
        <up-textarea v-model="form.remark"
                     placeholder="请输入备注"
                     autoHeight />
      </up-form-item>
    </up-form>
    <FooterButtons :loading="loading"
                   :confirmText="paramId ? '保存' : '新增'"
                   @cancel="goBack"
                   @confirm="handleSubmit" />
    <!-- å‚数类型选择 -->
    <up-action-sheet :show="showParamTypeSheet"
                     title="选择参数类型"
                     :actions="paramTypeActions"
                     @select="onSelectParamType"
                     @close="showParamTypeSheet = false" />
    <!-- ä¸‹æ‹‰å­—典选择 -->
    <up-action-sheet :show="showDictTypeSheet"
                     title="选择下拉字典"
                     :actions="dictTypeActions"
                     @select="onSelectDictType"
                     @close="showDictTypeSheet = false" />
    <!-- æ—¶é—´æ ¼å¼é€‰æ‹© -->
    <up-action-sheet :show="showTimeFormatSheet"
                     title="选择时间格式"
                     :actions="timeFormatActions"
                     @select="onSelectTimeFormat"
                     @close="showTimeFormatSheet = false" />
  </view>
</template>
<script setup>
  import { computed, nextTick, onMounted, ref } from "vue";
  import { onLoad, onReady } from "@dcloudio/uni-app";
  import FooterButtons from "@/components/FooterButtons.vue";
  import PageHeader from "@/components/PageHeader.vue";
  import {
    addBaseParam,
    editBaseParam,
  } from "@/api/basicData/parameterMaintenance";
  import { listType } from "@/api/system/dict/type";
  const formRef = ref();
  const loading = ref(false);
  const paramId = ref("");
  const showParamTypeSheet = ref(false);
  const showDictTypeSheet = ref(false);
  const showTimeFormatSheet = ref(false);
  const dictTypes = ref([]);
  const form = ref({
    id: null,
    paramCode: "",
    paramName: "",
    paramType: "",
    unit: "",
    remark: "",
    isRequired: 0,
    paramFormat: "",
  });
  const rules = {
    paramName: [{ required: true, message: "请输入参数名称" }],
    paramType: [{ required: true, message: "请选择参数类型" }],
    unit: [
      {
        validator: (rule, value, callback) => {
          if (form.value.paramType === 1 && !value) {
            callback(new Error("数值类型必须填写单位"));
          } else {
            callback();
          }
        },
      },
    ],
  };
  const paramTypeActions = [
    { name: "数值格式", value: 1 },
    { name: "文本格式", value: 2 },
    { name: "下拉选项", value: 3 },
    { name: "时间格式", value: 4 },
  ];
  const timeFormatActions = [
    { name: "YYYY-MM-DD", value: "YYYY-MM-DD" },
    { name: "YYYY-MM-DD HH:mm:ss", value: "YYYY-MM-DD HH:mm:ss" },
  ];
  const dictTypeActions = computed(() => {
    return dictTypes.value.map(item => ({
      name: item.dictName,
      value: item.dictType,
    }));
  });
  const pageTitle = computed(() => (paramId.value ? "编辑参数" : "新增参数"));
  const paramTypeText = computed(() => {
    const action = paramTypeActions.find(
      item => item.value === form.value.paramType
    );
    return action ? action.name : "";
  });
  const dictTypeText = computed(() => {
    const action = dictTypes.value.find(
      item => item.dictType === form.value.paramFormat
    );
    return action ? action.dictName : form.value.paramFormat || "";
  });
  const goBack = () => {
    uni.navigateBack();
  };
  const getDictTypes = () => {
    listType({ pageNum: 1, pageSize: 1000 }).then(res => {
      dictTypes.value = res.rows || [];
    });
  };
  const onSelectParamType = action => {
    form.value.paramType = action.value;
    if (action.value === 1) {
      form.value.paramFormat = "#.00000";
    } else if (action.value === 4) {
      form.value.paramFormat = "YYYY-MM-DD HH:mm:ss";
    } else {
      form.value.paramFormat = "";
    }
    showParamTypeSheet.value = false;
  };
  const onSelectDictType = action => {
    form.value.paramFormat = action.value;
    showDictTypeSheet.value = false;
  };
  const onSelectTimeFormat = action => {
    form.value.paramFormat = action.value;
    showTimeFormatSheet.value = false;
  };
  const handleSubmit = () => {
    formRef.value
      .validate()
      .then(() => {
        if (form.value.paramType === 3 && !form.value.paramFormat) {
          uni.showToast({ title: "请选择下拉字典", icon: "none" });
          return;
        }
        loading.value = true;
        const action = paramId.value ? editBaseParam : addBaseParam;
        action({ ...form.value, id: paramId.value || undefined })
          .then(() => {
            uni.showToast({ title: "保存成功", icon: "success" });
            setTimeout(() => {
              goBack();
            }, 1500);
          })
          .catch(() => {
            uni.showToast({ title: "保存失败", icon: "none" });
          })
          .finally(() => {
            loading.value = false;
          });
      })
      .catch(errors => {
        if (errors && errors.length > 0) {
          uni.showToast({
            title: errors[0].message,
            icon: "none",
          });
        }
      });
  };
  onReady(() => {
    if (formRef.value) {
      formRef.value.setRules(rules);
    }
  });
  onMounted(() => {
    getDictTypes();
  });
  onLoad(options => {
    if (options?.item) {
      const item = JSON.parse(decodeURIComponent(options.item));
      paramId.value = item.id;
      if (item.paramType) {
        item.paramType = Number(item.paramType);
      }
      Object.assign(form.value, item);
    }
  });
</script>
<style scoped lang="scss">
  @import "@/static/scss/form-common.scss";
  .basic-parameters-edit {
    min-height: 100vh;
    background: #f5f5f5;
  }
</style>
src/pages/productionDesign/basicParameters/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,245 @@
<template>
  <view class="sales-account">
    <PageHeader title="基础参数"
                @back="goBack" />
    <view class="search-section">
      <view class="search-bar">
        <view class="search-input">
          <up-input class="search-text"
                    v-model="paramName"
                    placeholder="请输入参数名称"
                    clearable
                    @change="handleSearch" />
        </view>
        <view class="filter-button"
              @click="handleSearch">
          <up-icon name="search"
                   size="24"
                   color="#999999"></up-icon>
        </view>
      </view>
    </view>
    <view v-if="list.length > 0"
          class="ledger-list">
      <view v-for="item in list"
            :key="item.id"
            class="ledger-item">
        <view class="item-header">
          <view class="item-left">
            <view class="document-icon">
              <up-icon name="setting-fill"
                       size="16"
                       color="#ffffff"></up-icon>
            </view>
            <text class="item-id">{{ item.paramName || "-" }}</text>
          </view>
          <text class="item-index">{{ item.paramCode || "-" }}</text>
        </view>
        <up-divider></up-divider>
        <view class="item-details">
          <view class="detail-row">
            <text class="detail-label">参数类型</text>
            <up-tag :text="getParamTypeLabel(item.paramType)"
                    :type="getParamTypeTag(item.paramType)"
                    size="mini" />
          </view>
          <view class="detail-row">
            <text class="detail-label">单位</text>
            <text class="detail-value">{{ item.unit || "-" }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">是否必填</text>
            <up-tag :text="item.isRequired === 1 ? '是' : '否'"
                    :type="item.isRequired === 1 ? 'success' : 'info'"
                    size="mini" />
          </view>
          <view class="detail-row">
            <text class="detail-label">取值格式</text>
            <text class="detail-value">{{ item.paramFormat || "-" }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">备注</text>
            <text class="detail-value">{{ item.remark || "-" }}</text>
          </view>
        </view>
        <view class="action-buttons">
          <up-button class="action-btn"
                     size="small"
                     type="primary"
                     @click="goEdit(item)">编辑</up-button>
          <up-button class="action-btn"
                     size="small"
                     type="error"
                     @click="handleDelete(item)">删除</up-button>
        </view>
      </view>
      <up-loadmore :status="page.status" />
    </view>
    <view v-else
          class="no-data">
      <text>暂无基础参数数据</text>
    </view>
    <view class="fab-button"
          @click="goAdd">
      <up-icon name="plus"
               size="28"
               color="#ffffff"></up-icon>
    </view>
  </view>
</template>
<script setup>
  import { reactive, ref } from "vue";
  import { onReachBottom, onShow } from "@dcloudio/uni-app";
  import {
    getBaseParamList,
    removeBaseParam,
  } from "@/api/basicData/parameterMaintenance";
  const paramName = ref("");
  const list = ref([]);
  const page = reactive({
    current: 1,
    size: 100,
    total: 0,
    status: "loadmore", // loadmore, loading, nomore
  });
  const goBack = () => {
    uni.navigateBack();
  };
  const getParamTypeLabel = type => {
    const map = {
      1: "数值格式",
      2: "文本格式",
      3: "下拉选项",
      4: "时间格式",
    };
    return map[type] || type;
  };
  const getParamTypeTag = type => {
    const map = {
      1: "primary",
      2: "info",
      3: "warning",
      4: "success",
    };
    return map[type] || "info";
  };
  const goAdd = () => {
    uni.navigateTo({ url: "/pages/productionDesign/basicParameters/edit" });
  };
  const goEdit = item => {
    uni.navigateTo({
      url: `/pages/productionDesign/basicParameters/edit?item=${encodeURIComponent(
        JSON.stringify(item)
      )}`,
    });
  };
  const handleDelete = item => {
    uni.showModal({
      title: "提示",
      content: "确定要删除该参数吗?",
      success: res => {
        if (res.confirm) {
          removeBaseParam(item.id).then(() => {
            uni.showToast({ title: "删除成功" });
            handleSearch();
          });
        }
      },
    });
  };
  const handleSearch = () => {
    page.current = 1;
    page.status = "loadmore";
    list.value = [];
    getList();
  };
  const getList = () => {
    if (page.status === "loading" || page.status === "nomore") return;
    page.status = "loading";
    getBaseParamList({
      current: page.current,
      size: page.size,
      paramName: paramName.value,
    })
      .then(res => {
        const records = res?.data?.records || res?.records || [];
        const total = res?.data?.total || res?.total || 0;
        if (page.current === 1) {
          list.value = records;
        } else {
          list.value = [...list.value, ...records];
        }
        page.total = total;
        if (list.value.length >= total) {
          page.status = "nomore";
        } else {
          page.status = "loadmore";
          page.current++;
        }
      })
      .catch(() => {
        uni.showToast({ title: "查询失败", icon: "error" });
        page.status = "loadmore";
      });
  };
  onReachBottom(() => {
    getList();
  });
  onShow(() => {
    handleSearch();
  });
</script>
<style scoped lang="scss">
  @import "@/styles/procurement-common.scss";
  .no-data {
    padding-top: 100rpx;
    text-align: center;
    color: #999;
    font-size: 28rpx;
  }
  .action-buttons {
    display: flex;
    justify-content: flex-end;
    gap: 20rpx;
    padding-bottom: 30rpx;
  }
  .action-btn {
    width: 140rpx;
    margin: 0 !important;
  }
  .fab-button {
    position: fixed;
    right: 40rpx;
    bottom: 60rpx;
    width: 100rpx;
    height: 100rpx;
    background: #2979ff;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    box-shadow: 0 4rpx 12rpx rgba(41, 121, 255, 0.4);
    z-index: 100;
  }
</style>
src/pages/productionDesign/bom/BomStructureItem.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,256 @@
<template>
  <view class="structure-item-wrapper"
        :class="{ 'is-root': level === 0, 'is-last': isLast }">
    <!-- æ ‘形连接线 (非根节点显示) -->
    <template v-if="level > 0">
      <view class="line-v"></view>
      <view class="line-h"></view>
    </template>
    <view class="structure-item-card"
          :class="{ 'has-children': hasChildren }">
      <view class="card-main">
        <view class="item-header"
              @click="toggleExpand">
          <view class="header-left">
            <view v-if="hasChildren"
                  class="expand-icon"
                  :class="{ 'is-expanded': isExpanded }">
              <up-icon name="arrow-right"
                       size="14"
                       color="#999"></up-icon>
            </view>
            <view v-else
                  class="dot-icon"></view>
            <text class="item-title">{{ item.productName || '未选择产品' }}</text>
          </view>
          <up-tag v-if="hasChildren"
                  text="组合"
                  type="primary"
                  size="mini"
                  plain
                  shape="circle" />
        </view>
        <view class="item-body">
          <view class="info-grid">
            <view class="info-item">
              <text class="label">规格型号:</text>
              <text class="value">{{ item.model || '-' }}</text>
            </view>
            <view class="info-item">
              <text class="label">消耗工序:</text>
              <text class="value">{{ getProcessName(item.processId) }}</text>
            </view>
            <view class="info-item">
              <text class="label">单位数量:</text>
              <text class="value highlight">{{ item.unitQuantity || 0 }}</text>
            </view>
            <view class="info-item">
              <text class="label">需求总量:</text>
              <text class="value highlight">{{ item.demandedQuantity || 0 }}</text>
            </view>
            <view class="info-item">
              <text class="label">单位:</text>
              <text class="value">{{ item.unit || '-' }}</text>
            </view>
            <view class="info-item">
              <text class="label">盘数:</text>
              <text class="value">{{ item.diskQuantity || 0 }}</text>
            </view>
          </view>
        </view>
      </view>
    </view>
    <!-- é€’归展示子节点 -->
    <view v-if="hasChildren && isExpanded"
          class="children-container">
      <BomStructureItem v-for="(child, index) in item.children"
                        :key="index"
                        :item="child"
                        :level="level + 1"
                        :isLast="index === item.children.length - 1"
                        :processOptions="processOptions" />
    </view>
  </view>
</template>
<script setup>
  import { ref, computed, defineProps } from "vue";
  const props = defineProps({
    item: {
      type: Object,
      required: true,
    },
    level: {
      type: Number,
      default: 0,
    },
    isLast: {
      type: Boolean,
      default: false,
    },
    processOptions: {
      type: Array,
      default: () => [],
    },
  });
  const isExpanded = ref(true);
  const hasChildren = computed(
    () => props.item.children && props.item.children.length > 0
  );
  const toggleExpand = () => {
    if (hasChildren.value) {
      isExpanded.value = !isExpanded.value;
    }
  };
  const getProcessName = id => {
    const process = props.processOptions.find(p => p.id === id);
    return process ? process.name : "-";
  };
</script>
<script>
  export default {
    name: "BomStructureItem",
  };
</script>
<style scoped lang="scss">
  .structure-item-wrapper {
    position: relative;
    padding-left: 44rpx;
    &.is-root {
      padding-left: 0;
    }
  }
  // åž‚直连接线段
  .line-v {
    position: absolute;
    left: 18rpx; // å±…中于 44rpx çš„缩进内
    top: -20rpx; // å‘上延伸覆盖上一个节点的 margin-bottom
    bottom: 0;
    width: 2rpx;
    background-color: #ddd;
    z-index: 1;
  }
  // æœ€åŽä¸€ä¸ªèŠ‚ç‚¹çš„åž‚ç›´çº¿åªå»¶ä¼¸åˆ°æ°´å¹³çº¿ä½ç½®
  .is-last > .line-v {
    bottom: auto;
    height: 60rpx; // 20rpx (top offset) + 40rpx (to horizontal line)
  }
  // æ°´å¹³è¿žæŽ¥çº¿
  .line-h {
    position: absolute;
    left: 18rpx;
    top: 40rpx; // å¯¹é½åˆ°å¡ç‰‡å†…部图标中心 (padding 24 + icon 32/2)
    width: 26rpx;
    height: 2rpx;
    background-color: #ddd;
    z-index: 1;
  }
  .structure-item-card {
    position: relative;
    background: #fff;
    border-radius: 16rpx;
    margin-bottom: 20rpx;
    padding: 24rpx;
    box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05);
    border: 1rpx solid #f0f0f0;
    transition: all 0.3s;
    z-index: 2;
    &:active {
      background-color: #f9f9f9;
    }
    &.has-children {
      border-left: 6rpx solid #3c9cff;
    }
  }
  .card-main {
    .item-header {
      display: flex;
      justify-content: space-between;
      align-items: center;
      margin-bottom: 20rpx;
      .header-left {
        display: flex;
        align-items: center;
        flex: 1;
        .expand-icon {
          margin-right: 12rpx;
          transition: transform 0.3s;
          display: flex;
          align-items: center;
          justify-content: center;
          width: 32rpx;
          height: 32rpx;
          &.is-expanded {
            transform: rotate(90deg);
          }
        }
        .dot-icon {
          width: 12rpx;
          height: 12rpx;
          border-radius: 50%;
          background-color: #ccc;
          margin-right: 20rpx;
          margin-left: 10rpx;
        }
        .item-title {
          font-size: 30rpx;
          font-weight: bold;
          color: #333;
          line-height: 1.4;
        }
      }
    }
    .item-body {
      .info-grid {
        display: grid;
        grid-template-columns: 1fr 1fr;
        gap: 12rpx 20rpx;
        .info-item {
          display: flex;
          font-size: 24rpx;
          line-height: 1.5;
          .label {
            color: #999;
            white-space: nowrap;
          }
          .value {
            color: #666;
            word-break: break-all;
            &.highlight {
              color: #3c9cff;
              font-weight: 500;
            }
          }
        }
      }
    }
  }
  .children-container {
    position: relative;
  }
</style>
src/pages/productionDesign/bom/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,179 @@
<template>
  <view class="bom-list">
    <PageHeader title="BOM管理"
                @back="goBack" />
    <view class="search-section">
      <view class="search-bar">
        <view class="search-input">
          <up-input class="search-text"
                    v-model="queryParams.productName"
                    placeholder="请输入产品名称"
                    clearable
                    @change="handleSearch" />
        </view>
        <view class="filter-button"
              @click="handleSearch">
          <up-icon name="search"
                   size="24"
                   color="#999999"></up-icon>
        </view>
      </view>
    </view>
    <view v-if="list.length > 0"
          class="ledger-list">
      <view v-for="item in list"
            :key="item.id"
            class="ledger-item">
        <view class="item-header">
          <view class="item-left">
            <view class="document-icon">
              <up-icon name="list-dot"
                       size="16"
                       color="#ffffff"></up-icon>
            </view>
            <text class="item-id">{{ item.bomNo || "-" }}</text>
          </view>
          <up-tag :text="'V' + (item.version || '1.0')"
                  type="primary"
                  size="mini" />
        </view>
        <up-divider></up-divider>
        <view class="item-details">
          <view class="detail-row">
            <text class="detail-label">产品名称</text>
            <text class="detail-value">{{ item.productName || "-" }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">规格型号</text>
            <text class="detail-value">{{ item.productModelName || "-" }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">备注</text>
            <text class="detail-value">{{ item.remark || "-" }}</text>
          </view>
        </view>
        <view class="action-buttons">
          <up-button class="action-btn"
                     size="small"
                     type="primary"
                     @click="goStructure(item)">查看详情</up-button>
        </view>
      </view>
      <up-loadmore :status="pageStatus" />
    </view>
    <view v-else
          class="no-data">
      <up-empty text="暂无BOM数据"
                mode="list"></up-empty>
    </view>
  </view>
</template>
<script setup>
  import { reactive, ref } from "vue";
  import { onReachBottom, onShow } from "@dcloudio/uni-app";
  import { listPage } from "@/api/productionManagement/bom";
  const queryParams = reactive({
    productName: "",
  });
  const list = ref([]);
  const pageStatus = ref("loadmore");
  const page = reactive({
    current: 1,
    size: 3,
    total: 0,
  });
  const goBack = () => {
    uni.navigateBack();
  };
  const handleSearch = () => {
    page.current = 1;
    pageStatus.value = "loadmore";
    list.value = [];
    getList();
  };
  const getList = () => {
    if (pageStatus.value === "loading" || pageStatus.value === "nomore") return;
    pageStatus.value = "loading";
    listPage({
      current: page.current,
      size: page.size,
      productName: queryParams.productName,
    })
      .then(res => {
        const records = res?.data?.records || res?.records || [];
        const total = res?.data?.total || res?.total || 0;
        if (page.current === 1) {
          list.value = records;
        } else {
          list.value = [...list.value, ...records];
        }
        page.total = total;
        if (list.value.length >= total) {
          pageStatus.value = "nomore";
        } else {
          pageStatus.value = "loadmore";
          page.current++;
        }
      })
      .catch(() => {
        uni.showToast({ title: "查询失败", icon: "error" });
        pageStatus.value = "loadmore";
      });
  };
  const goStructure = item => {
    uni.navigateTo({
      url: `/pages/productionDesign/bom/structure?id=${
        item.id
      }&bomNo=${encodeURIComponent(item.bomNo)}&productName=${encodeURIComponent(
        item.productName || ""
      )}&productModelName=${encodeURIComponent(
        item.productModelName || ""
      )}&remark=${encodeURIComponent(
        item.remark || ""
      )}&version=${encodeURIComponent(item.version || 1)}`,
    });
  };
  onReachBottom(() => {
    getList();
  });
  onShow(() => {
    handleSearch();
  });
</script>
<style scoped lang="scss">
  @import "@/styles/procurement-common.scss";
  .no-data {
    padding-top: 100rpx;
    text-align: center;
    color: #999;
    font-size: 28rpx;
  }
  .action-buttons {
    display: flex;
    justify-content: flex-end;
    gap: 15rpx;
    padding: 0 30rpx 30rpx;
    flex-wrap: wrap;
  }
  .action-btn {
    width: calc(50% - 15rpx);
    margin: 0 !important;
    margin-bottom: 15rpx !important;
  }
</style>
src/pages/productionDesign/bom/structure.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,100 @@
<template>
  <view class="structure-page">
    <PageHeader :title="'BOM结构 - ' + bomNo"
                @back="goBack" />
    <view class="info-card">
      <view class="info-row">
        <text class="info-label">产品名称:</text>
        <text class="info-value">{{ productName }}-{{ productModelName }}</text>
      </view>
    </view>
    <view class="structure-list"
          v-if="dataList.length > 0">
      <BomStructureItem v-for="(item, index) in dataList"
                        :key="index"
                        :item="item"
                        :level="0"
                        :isLast="index === dataList.length - 1"
                        :processOptions="processOptions" />
    </view>
    <view v-else
          class="no-data">
      <up-empty text="暂无结构数据"
                mode="list"></up-empty>
    </view>
  </view>
</template>
<script setup>
  import { ref, reactive, computed } from "vue";
  import { onLoad } from "@dcloudio/uni-app";
  import { queryStructureList } from "@/api/productionManagement/bom";
  import { list as getProcessList } from "@/api/productionManagement/processManagement";
  import BomStructureItem from "./BomStructureItem.vue";
  const bomId = ref(null);
  const bomNo = ref("");
  const productName = ref("");
  const dataList = ref([]);
  const processOptions = ref([]);
  const goBack = () => {
    uni.navigateBack();
  };
  const fetchData = () => {
    queryStructureList(bomId.value).then(res => {
      dataList.value = res.data || [];
    });
  };
  const fetchProcess = () => {
    getProcessList().then(res => {
      processOptions.value = res.data || [];
    });
  };
  const productModelName = ref("");
  onLoad(options => {
    bomId.value = options.id;
    bomNo.value = decodeURIComponent(options.bomNo);
    productName.value = decodeURIComponent(options.productName);
    productModelName.value = decodeURIComponent(options.productModelName);
    fetchData();
    fetchProcess();
  });
</script>
<style scoped lang="scss">
  .structure-page {
    background-color: #f5f5f5;
    min-height: 100vh;
    padding-bottom: 120rpx;
  }
  .info-card {
    background: #fff;
    padding: 30rpx;
    margin-bottom: 20rpx;
    .info-row {
      display: flex;
      font-size: 28rpx;
      .info-label {
        color: #666;
      }
      .info-value {
        color: #333;
        font-weight: bold;
      }
    }
  }
  .structure-list {
    padding: 20rpx;
  }
  .no-data {
    padding-top: 100rpx;
  }
</style>
src/pages/productionDesign/processManagement/edit.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,236 @@
<template>
  <view class="process-edit">
    <PageHeader :title="pageTitle"
                @back="goBack" />
    <up-form ref="formRef"
             :model="form"
             :rules="rules"
             :errorType="['none']"
             label-width="110">
      <up-form-item label="工序编码"
                    prop="no">
        <up-input v-model="form.no"
                  placeholder="请输入工序编码"
                  clearable />
      </up-form-item>
      <up-form-item label="工序名称"
                    prop="name"
                    required>
        <up-input v-model="form.name"
                  placeholder="请输入工序名称"
                  clearable />
      </up-form-item>
      <up-form-item label="工资定额"
                    prop="salaryQuota">
        <up-input v-model="form.salaryQuota"
                  type="number"
                  placeholder="请输入工资定额"
                  clearable />
      </up-form-item>
      <up-form-item label="计费类型"
                    prop="type">
        <up-input v-model="typeText"
                  placeholder="请选择计费类型"
                  readonly
                  @click="showTypeSheet = true" />
        <template #right>
          <up-icon name="arrow-right"
                   @click="showTypeSheet = true"></up-icon>
        </template>
      </up-form-item>
      <up-form-item label="是否质检"
                    prop="isQuality">
        <view style="display: flex; justify-content: flex-end; width: 100%;">
          <up-switch v-model="form.isQuality" />
        </view>
      </up-form-item>
      <up-form-item label="是否生产"
                    prop="isProduction">
        <view style="display: flex; justify-content: flex-end; width: 100%;">
          <up-switch v-model="form.isProduction" />
        </view>
      </up-form-item>
      <up-form-item label="关联设备"
                    prop="deviceLedgerId">
        <up-input v-model="deviceText"
                  placeholder="请选择关联设备"
                  readonly
                  @click="showDeviceSheet = true" />
        <template #right>
          <up-icon name="arrow-right"
                   @click="showDeviceSheet = true"></up-icon>
        </template>
      </up-form-item>
      <up-form-item label="工序描述"
                    prop="remark">
        <up-textarea v-model="form.remark"
                     placeholder="请输入工序描述"
                     autoHeight />
      </up-form-item>
    </up-form>
    <FooterButtons :loading="loading"
                   :confirmText="processId ? '保存' : '新增'"
                   @cancel="goBack"
                   @confirm="handleSubmit" />
    <!-- è®¡è´¹ç±»åž‹é€‰æ‹© -->
    <up-action-sheet :show="showTypeSheet"
                     title="选择计费类型"
                     :actions="typeActions"
                     @select="onSelectType"
                     @close="showTypeSheet = false" />
    <!-- è®¾å¤‡é€‰æ‹© -->
    <up-action-sheet :show="showDeviceSheet"
                     title="选择关联设备"
                     :actions="deviceActions"
                     @select="onSelectDevice"
                     @close="showDeviceSheet = false" />
  </view>
</template>
<script setup>
  import { reactive, ref, computed, onMounted } from "vue";
  import { onLoad, onReady } from "@dcloudio/uni-app";
  import FooterButtons from "@/components/FooterButtons.vue";
  import {
    add,
    update,
    getDeviceLedger,
  } from "@/api/productionManagement/processManagement";
  const formRef = ref(null);
  const loading = ref(false);
  const processId = ref(null);
  const pageTitle = computed(() => (processId.value ? "编辑工序" : "新增工序"));
  const form = ref({
    no: "",
    name: "",
    salaryQuota: "",
    isQuality: false,
    isProduction: false,
    remark: "",
    deviceLedgerId: null,
    type: 0,
  });
  const rules = {
    name: [{ required: true, message: "请输入工序名称" }],
    salaryQuota: [
      {
        validator: (rule, value, callback) => {
          if (value !== "" && value !== null && (isNaN(value) || value < 0)) {
            callback(new Error("工资定额必须是非负数字"));
          } else {
            callback();
          }
        },
      },
    ],
  };
  const showTypeSheet = ref(false);
  const typeActions = [
    { name: "计时", value: 0 },
    { name: "计件", value: 1 },
  ];
  const typeText = computed(() => {
    const action = typeActions.find(a => a.value === form.value.type);
    return action ? action.name : "";
  });
  const showDeviceSheet = ref(false);
  const deviceActions = ref([]);
  const deviceText = ref("");
  const onSelectType = e => {
    form.value.type = e.value;
    showTypeSheet.value = false;
  };
  const onSelectDevice = e => {
    form.value.deviceLedgerId = e.id;
    deviceText.value = e.name;
    showDeviceSheet.value = false;
  };
  const loadDevices = async () => {
    try {
      const { data } = await getDeviceLedger();
      deviceActions.value = (data || []).map(item => ({
        name: item.deviceName,
        id: item.id,
      }));
      if (form.value.deviceLedgerId) {
        const device = deviceActions.value.find(
          d => d.id === Number(form.value.deviceLedgerId)
        );
        if (device) deviceText.value = device.name;
      }
    } catch (error) {
      console.error("加载设备失败", error);
    }
  };
  const goBack = () => {
    uni.navigateBack();
  };
  const handleSubmit = () => {
    formRef.value
      .validate()
      .then(() => {
        loading.value = true;
        const promise = processId.value ? update(form.value) : add(form.value);
        promise
          .then(() => {
            uni.showToast({ title: processId.value ? "保存成功" : "新增成功" });
            setTimeout(() => {
              goBack();
            }, 1500);
          })
          .catch(err => {
            uni.showToast({ title: err.msg || "提交失败", icon: "error" });
          })
          .finally(() => {
            loading.value = false;
          });
      })
      .catch(errors => {
        if (errors && errors.length > 0) {
          uni.showToast({
            title: errors[0].message,
            icon: "none",
          });
        }
      });
  };
  onLoad(option => {
    if (option.item) {
      const item = JSON.parse(decodeURIComponent(option.item));
      processId.value = item.id;
      Object.assign(form.value, item);
      // å¤„理类型转换,确保是数字
      form.value.type = Number(form.value.type);
      form.value.isQuality = !!form.value.isQuality;
      form.value.isProduction = !!form.value.isProduction;
    }
  });
  onReady(() => {
    formRef.value.setRules(rules);
  });
  onMounted(() => {
    loadDevices();
  });
</script>
<style scoped lang="scss">
  @import "@/static/scss/form-common.scss";
  .process-edit {
    min-height: 100vh;
    background: #f5f5f5;
  }
</style>
src/pages/productionDesign/processManagement/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,261 @@
<template>
  <view class="sales-account">
    <PageHeader title="工序管理"
                @back="goBack" />
    <view class="search-section">
      <view class="search-bar">
        <view class="search-input">
          <up-input class="search-text"
                    v-model="queryParams.name"
                    placeholder="请输入工序名称"
                    clearable
                    @change="handleSearch" />
        </view>
        <view class="filter-button"
              @click="handleSearch">
          <up-icon name="search"
                   size="24"
                   color="#999999"></up-icon>
        </view>
      </view>
    </view>
    <view v-if="list.length > 0"
          class="ledger-list">
      <view v-for="item in list"
            :key="item.id"
            class="ledger-item">
        <view class="item-header">
          <view class="item-left">
            <view class="document-icon">
              <up-icon name="list-dot"
                       size="16"
                       color="#ffffff"></up-icon>
            </view>
            <text class="item-id">{{ item.name || "-" }}</text>
          </view>
          <text class="item-index">{{ item.no || "-" }}</text>
        </view>
        <up-divider></up-divider>
        <view class="item-details">
          <view class="detail-row">
            <text class="detail-label">关联设备</text>
            <text class="detail-value">{{ getDeviceName(item.deviceLedgerId) }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">工资定额</text>
            <text class="detail-value highlight">Â¥{{ item.salaryQuota || 0 }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">工序状态</text>
            <view class="detail-value">
              <up-tag :text="item.isQuality ? '质检' : '非质检'"
                      :type="item.isQuality ? 'warning' : 'info'"
                      size="mini"
                      style="margin-left: 8rpx" />
              <up-tag :text="item.isProduction ? '生产' : '不生产'"
                      :type="item.isProduction ? 'warning' : 'info'"
                      size="mini"
                      style="margin-left: 8rpx" />
              <up-tag v-if="item.type !== null && item.type !== undefined"
                      :text="item.type == 0 ? '计时' : '计件'"
                      :type="item.type == 1 ? 'primary' : 'success'"
                      size="mini"
                      style="margin-left: 8rpx" />
            </view>
          </view>
          <view class="detail-row">
            <text class="detail-label">备注</text>
            <text class="detail-value">{{ item.remark || "-" }}</text>
          </view>
        </view>
        <view class="action-buttons">
          <up-button class="action-btn"
                     size="small"
                     type="primary"
                     @click="goEdit(item)">编辑</up-button>
          <up-button class="action-btn"
                     size="small"
                     type="warning"
                     @click="goParams(item)">参数配置</up-button>
          <up-button class="action-btn"
                     size="small"
                     type="error"
                     @click="handleDelete(item)">删除</up-button>
        </view>
      </view>
      <up-loadmore :status="pageStatus" />
    </view>
    <view v-else
          class="no-data">
      <text>暂无工序数据</text>
    </view>
    <view class="fab-button"
          @click="goAdd">
      <up-icon name="plus"
               size="28"
               color="#ffffff"></up-icon>
    </view>
  </view>
</template>
<script setup>
  import { reactive, ref } from "vue";
  import { onReachBottom, onShow } from "@dcloudio/uni-app";
  import {
    getProcessList,
    del,
    getDeviceLedger,
  } from "@/api/productionManagement/processManagement";
  const queryParams = reactive({
    name: "",
  });
  const list = ref([]);
  const deviceOptions = ref([]);
  const pageStatus = ref("loadmore");
  const page = reactive({
    current: 1,
    size: 10,
    total: 0,
  });
  const goBack = () => {
    uni.navigateBack();
  };
  const getDeviceName = deviceId => {
    if (!deviceId) return "未关联";
    const device = deviceOptions.value.find(item => item.id === Number(deviceId));
    return device?.deviceName || "未关联";
  };
  const loadDevices = async () => {
    try {
      const { data } = await getDeviceLedger();
      deviceOptions.value = data || [];
    } catch (error) {
      console.error("加载设备列表失败", error);
    }
  };
  const handleSearch = () => {
    page.current = 1;
    pageStatus.value = "loadmore";
    list.value = [];
    getList();
  };
  const getList = () => {
    if (pageStatus.value === "loading" || pageStatus.value === "nomore") return;
    pageStatus.value = "loading";
    getProcessList({
      current: page.current,
      size: page.size,
      name: queryParams.name,
    })
      .then(res => {
        const records = res?.data?.records || res?.records || [];
        const total = res?.data?.total || res?.total || 0;
        if (page.current === 1) {
          list.value = records;
        } else {
          list.value = [...list.value, ...records];
        }
        page.total = total;
        if (list.value.length >= total) {
          pageStatus.value = "nomore";
        } else {
          pageStatus.value = "loadmore";
          page.current++;
        }
      })
      .catch(() => {
        uni.showToast({ title: "查询失败", icon: "error" });
        pageStatus.value = "loadmore";
      });
  };
  const goAdd = () => {
    uni.navigateTo({ url: "/pages/productionDesign/processManagement/edit" });
  };
  const goEdit = item => {
    uni.navigateTo({
      url: `/pages/productionDesign/processManagement/edit?item=${encodeURIComponent(
        JSON.stringify(item)
      )}`,
    });
  };
  const goParams = item => {
    uni.navigateTo({
      url: `/pages/productionDesign/processManagement/params?id=${item.id}&name=${encodeURIComponent(item.name)}`,
    });
  };
  const handleDelete = item => {
    uni.showModal({
      title: "提示",
      content: "确定要删除该工序吗?",
      success: res => {
        if (res.confirm) {
          del([item.id]).then(() => {
            uni.showToast({ title: "删除成功" });
            handleSearch();
          });
        }
      },
    });
  };
  onReachBottom(() => {
    getList();
  });
  onShow(async () => {
    await loadDevices();
    handleSearch();
  });
</script>
<style scoped lang="scss">
  @import "@/styles/procurement-common.scss";
  .no-data {
    padding-top: 100rpx;
    text-align: center;
    color: #999;
    font-size: 28rpx;
  }
  .action-buttons {
    display: flex;
    justify-content: flex-end;
    gap: 20rpx;
    padding-bottom: 30rpx;
  }
  .action-btn {
    flex: 1;
    margin: 0 !important;
  }
  .fab-button {
    position: fixed;
    right: 40rpx;
    bottom: 60rpx;
    width: 100rpx;
    height: 100rpx;
    background: #2979ff;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    box-shadow: 0 4rpx 12rpx rgba(41, 121, 255, 0.4);
    z-index: 100;
  }
</style>
src/pages/productionDesign/processManagement/params.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,413 @@
<template>
  <view class="process-params">
    <PageHeader :title="processName + ' - å‚数配置'"
                @back="goBack" />
    <view class="ledger-list">
      <view v-if="paramList.length > 0">
        <view v-for="item in paramList"
              :key="item.id"
              class="ledger-item">
          <view class="item-header">
            <view class="item-left">
              <view class="document-icon">
                <up-icon name="setting-fill"
                         size="16"
                         color="#ffffff"></up-icon>
              </view>
              <text class="item-id">{{ item.paramName || "-" }}</text>
            </view>
          </view>
          <up-divider></up-divider>
          <view class="item-details">
            <view class="detail-row">
              <text class="detail-label">标准值</text>
              <text class="detail-value highlight">{{ item.standardValue || "-" }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">单位</text>
              <text class="detail-value">{{ item.unit || "-" }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">参数类型</text>
              <up-tag :text="getParamTypeText(item.paramType)"
                      :type="getParamTypeTag(item.paramType)"
                      size="mini" />
            </view>
            <view class="detail-row">
              <text class="detail-label">取值格式</text>
              <text class="detail-value">{{ item.paramFormat || "-" }}</text>
            </view>
          </view>
          <view class="action-buttons">
            <up-button class="action-btn"
                       size="small"
                       type="primary"
                       @click="handleEditParam(item)">编辑</up-button>
            <up-button class="action-btn"
                       size="small"
                       type="error"
                       @click="handleDeleteParam(item)">删除</up-button>
          </view>
        </view>
      </view>
      <view v-else
            class="no-data">
        <up-empty text="暂无参数配置"
                  icon="account"
                  iconSize="60"></up-empty>
      </view>
    </view>
    <!-- æµ®åŠ¨æ–°å¢žæŒ‰é’® -->
    <view class="fab-button"
          @click="openSelectModal">
      <up-icon name="plus"
               size="28"
               color="#ffffff"></up-icon>
    </view>
    <!-- é€‰æ‹©å‚数弹窗 -->
    <up-modal :show="selectModalVisible"
              title="选择参数"
              width="650rpx"
              @confirm="handleSelectSubmit"
              @cancel="selectModalVisible = false"
              :closeOnClickOverlay="false"
              showCancelButton>
      <view class="modal-content">
        <view class="search-box">
          <up-input v-model="searchKeyword"
                    placeholder="搜索基础参数名称"
                    clearable
                    @confirm="handleSearch"
                    @change="handleSearch" />
        </view>
        <scroll-view scroll-y
                     class="param-scroll-list"
                     @scrolltolower="loadMoreParams">
          <view v-for="param in availableParams"
                :key="param.id"
                class="param-select-item"
                :class="{ active: selectedBaseParam?.id === param.id }"
                @click="selectParam(param)">
            <view class="param-main">
              <text class="param-name">{{ param.paramName }}</text>
              <up-tag :text="getParamTypeText(param.paramType)"
                      :type="getParamTypeTag(param.paramType)"
                      size="mini" />
            </view>
            <text class="param-code">{{ param.paramCode }}</text>
          </view>
          <up-loadmore :status="availablePageStatus" />
        </scroll-view>
        <view v-if="selectedBaseParam"
              class="standard-input-box">
          <text class="label">标准值:</text>
          <up-input v-model="selectedStandardValue"
                    placeholder="请输入该工序的标准值" />
        </view>
      </view>
    </up-modal>
    <!-- ç¼–辑参数标准值弹窗 -->
    <up-modal :show="editModalVisible"
              title="编辑标准值"
              width="500rpx"
              @confirm="handleEditSubmit"
              @cancel="editModalVisible = false"
              :closeOnClickOverlay="false"
              showCancelButton>
      <view class="modal-content">
        <view class="edit-info">
          <text class="edit-label">参数:{{ currentEditParam?.paramName }}</text>
          <up-input v-model="currentEditValue"
                    placeholder="请输入新的标准值" />
        </view>
      </view>
    </up-modal>
  </view>
</template>
<script setup>
  import { reactive, ref, onMounted } from "vue";
  import { onLoad } from "@dcloudio/uni-app";
  import {
    getProcessParamList,
    addProcessParam,
    editProcessParam,
    deleteProcessParam,
    getBaseParamList,
  } from "@/api/productionManagement/processManagement";
  const processId = ref(null);
  const processName = ref("");
  const paramList = ref([]);
  const loading = ref(false);
  // é€‰æ‹©å‚数相关
  const selectModalVisible = ref(false);
  const availableParams = ref([]);
  const searchKeyword = ref("");
  const selectedBaseParam = ref(null);
  const selectedStandardValue = ref("");
  const availablePage = reactive({ current: 1, size: 20, total: 0 });
  const availablePageStatus = ref("loadmore");
  // ç¼–辑参数相关
  const editModalVisible = ref(false);
  const currentEditParam = ref(null);
  const currentEditValue = ref("");
  const goBack = () => {
    uni.navigateBack();
  };
  const getParamList = () => {
    loading.value = true;
    getProcessParamList({ technologyOperationId: processId.value })
      .then(res => {
        paramList.value = res?.data || [];
      })
      .catch(() => {
        uni.showToast({ title: "获取列表失败", icon: "none" });
      })
      .finally(() => {
        loading.value = false;
      });
  };
  const openSelectModal = () => {
    searchKeyword.value = "";
    selectedBaseParam.value = null;
    selectedStandardValue.value = "";
    availableParams.value = [];
    availablePage.current = 1;
    availablePageStatus.value = "loadmore";
    selectModalVisible.value = true;
    loadAvailableParams(true);
  };
  const handleSearch = () => {
    availablePage.current = 1;
    availableParams.value = [];
    availablePageStatus.value = "loadmore";
    loadAvailableParams(true);
  };
  const loadMoreParams = () => {
    if (
      availablePageStatus.value === "nomore" ||
      availablePageStatus.value === "loading"
    )
      return;
    loadAvailableParams(false);
  };
  const loadAvailableParams = (isReset = false) => {
    if (availablePageStatus.value === "loading") return;
    if (isReset) {
      availablePage.current = 1;
      availableParams.value = [];
      availablePageStatus.value = "loading";
    } else if (availablePageStatus.value === "nomore") {
      return;
    } else {
      availablePageStatus.value = "loading";
    }
    getBaseParamList({
      paramName: searchKeyword.value,
      current: availablePage.current,
      size: availablePage.size,
    })
      .then(res => {
        const records = res?.data?.records || res?.records || [];
        const total = res?.data?.total || res?.total || 0;
        if (isReset || availablePage.current === 1) {
          availableParams.value = records;
        } else {
          availableParams.value = [...availableParams.value, ...records];
        }
        availablePage.total = total;
        if (availableParams.value.length >= total) {
          availablePageStatus.value = "nomore";
        } else {
          availablePageStatus.value = "loadmore";
          availablePage.current++;
        }
      })
      .catch(() => {
        availablePageStatus.value = "loadmore";
      });
  };
  const selectParam = param => {
    selectedBaseParam.value = param;
    selectedStandardValue.value = param.standardValue || "";
  };
  const handleSelectSubmit = () => {
    if (!selectedBaseParam.value) {
      uni.showToast({ title: "请选择一个基础参数", icon: "none" });
      return;
    }
    if (!selectedStandardValue.value) {
      uni.showToast({ title: "请输入标准值", icon: "none" });
      return;
    }
    addProcessParam({
      technologyOperationId: processId.value,
      technologyParamId: selectedBaseParam.value.id,
      standardValue: selectedStandardValue.value,
    })
      .then(() => {
        uni.showToast({ title: "添加成功" });
        selectModalVisible.value = false;
        getParamList();
      })
      .catch(err => {
        uni.showToast({ title: err.msg || "添加失败", icon: "error" });
      });
  };
  const handleEditParam = item => {
    currentEditParam.value = item;
    currentEditValue.value = item.standardValue;
    editModalVisible.value = true;
  };
  const handleEditSubmit = () => {
    if (!currentEditValue.value) {
      uni.showToast({ title: "请输入标准值", icon: "none" });
      return;
    }
    editProcessParam({
      id: currentEditParam.value.id,
      technologyOperationId: processId.value,
      technologyParamId: currentEditParam.value.technologyParamId,
      standardValue: currentEditValue.value,
    })
      .then(() => {
        uni.showToast({ title: "修改成功" });
        editModalVisible.value = false;
        getParamList();
      })
      .catch(err => {
        uni.showToast({ title: err.msg || "修改失败", icon: "error" });
      });
  };
  const handleDeleteParam = item => {
    uni.showModal({
      title: "提示",
      content: "确定要删除该参数配置吗?",
      success: res => {
        if (res.confirm) {
          deleteProcessParam(item.id).then(() => {
            uni.showToast({ title: "删除成功" });
            getParamList();
          });
        }
      },
    });
  };
  const getParamTypeText = type => {
    const typeMap = { 1: "数值", 2: "文本", 3: "下拉", 4: "时间" };
    return typeMap[type] || "未知";
  };
  const getParamTypeTag = type => {
    const typeMap = { 1: "primary", 2: "info", 3: "warning", 4: "success" };
    return typeMap[type] || "default";
  };
  onLoad(option => {
    if (option.id) {
      processId.value = option.id;
      processName.value = decodeURIComponent(option.name || "");
      getParamList();
    }
  });
</script>
<style scoped lang="scss">
  @import "@/styles/procurement-common.scss";
  .process-params {
    min-height: 100vh;
    background: #f5f5f5;
  }
  .modal-content {
    padding: 20rpx 0;
    width: 100%;
  }
  .param-scroll-list {
    height: 500rpx;
    margin-top: 20rpx;
    border: 1px solid #eee;
    border-radius: 8rpx;
  }
  .param-select-item {
    padding: 20rpx;
    border-bottom: 1px solid #f5f5f5;
    width: 100%;
    &.active {
      background-color: #e3f2fd;
    }
  }
  .param-main {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 8rpx;
  }
  .param-name {
    font-size: 28rpx;
    font-weight: bold;
    color: #333;
  }
  .param-code {
    font-size: 24rpx;
    color: #999;
  }
  .standard-input-box {
    margin-top: 30rpx;
    display: flex;
    align-items: center;
    .label {
      width: 120rpx;
      font-size: 28rpx;
      color: #333;
    }
  }
  .edit-info {
    .edit-label {
      display: block;
      margin-bottom: 20rpx;
      font-size: 28rpx;
      color: #666;
    }
  }
  .fab-button {
    position: fixed;
    right: 40rpx;
    bottom: 60rpx;
    width: 100rpx;
    height: 100rpx;
    background: #2979ff;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    box-shadow: 0 4rpx 12rpx rgba(41, 121, 255, 0.4);
    z-index: 100;
  }
</style>
src/pages/productionManagement/mainProductionPlan/detail.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,252 @@
<template>
  <view class="production-plan-detail">
    <PageHeader title="计划详情"
                @back="goBack" />
    <view class="detail-container"
          v-if="detailData">
      <!-- åŸºæœ¬ä¿¡æ¯å¡ç‰‡ -->
      <view class="detail-card">
        <view class="card-title">
          <up-icon name="info-circle"
                   size="18"
                   color="#3c9cff"></up-icon>
          <text class="title-text">基本信息</text>
        </view>
        <view class="card-content">
          <view class="info-item">
            <text class="label">主生产计划号</text>
            <text class="value">{{ detailData.mpsNo || '-' }}</text>
          </view>
          <view class="info-item">
            <text class="label">来源</text>
            <up-tag :text="detailData.source === '销售' ? '销售' : '内部'"
                    :type="detailData.source === '销售' ? 'primary' : 'info'"
                    size="mini" />
          </view>
          <view class="info-item">
            <text class="label">下发状态</text>
            <up-tag :text="getStatusText(detailData.status)"
                    :type="getStatusType(detailData.status)"
                    size="mini" />
          </view>
        </view>
      </view>
      <!-- äº§å“ä¿¡æ¯å¡ç‰‡ -->
      <view class="detail-card">
        <view class="card-title">
          <up-icon name="order"
                   size="18"
                   color="#3c9cff"></up-icon>
          <text class="title-text">产品信息</text>
        </view>
        <view class="card-content">
          <view class="info-item">
            <text class="label">产品名称</text>
            <text class="value font-bold">{{ detailData.productName || '-' }}</text>
          </view>
          <view class="info-item">
            <text class="label">规格型号</text>
            <text class="value">{{ detailData.model || '-' }}</text>
          </view>
          <view class="info-item">
            <text class="label">所需数量</text>
            <text class="value highlight">{{ detailData.qtyRequired || 0 }} {{ detailData.unit || '方' }}</text>
          </view>
          <view class="info-item">
            <text class="label">已下发数量</text>
            <text class="value">{{ detailData.quantityIssued || 0 }} {{ detailData.unit || '方' }}</text>
          </view>
        </view>
      </view>
      <!-- æ—¥æœŸä¸Žå…³è”卡片 -->
      <view class="detail-card">
        <view class="card-title">
          <up-icon name="calendar"
                   size="18"
                   color="#3c9cff"></up-icon>
          <text class="title-text">日期与关联</text>
        </view>
        <view class="card-content">
          <view class="info-item">
            <text class="label">需求日期</text>
            <text class="value">{{ formatDate(detailData.requiredDate) }}</text>
          </view>
          <view class="info-item">
            <text class="label">承诺日期</text>
            <text class="value">{{ formatDate(detailData.promisedDeliveryDate) }}</text>
          </view>
          <view class="info-item">
            <text class="label">销售合同号</text>
            <text class="value">{{ detailData.salesContractNo || '-' }}</text>
          </view>
          <view class="info-item">
            <text class="label">客户名称</text>
            <text class="value">{{ detailData.customerName || '-' }}</text>
          </view>
          <view class="info-item">
            <text class="label">项目名称</text>
            <text class="value">{{ detailData.projectName || '-' }}</text>
          </view>
        </view>
      </view>
      <!-- å¤‡æ³¨ä¿¡æ¯ -->
      <view class="detail-card">
        <view class="card-title">
          <up-icon name="edit-pen"
                   size="18"
                   color="#3c9cff"></up-icon>
          <text class="title-text">备注</text>
        </view>
        <view class="card-content">
          <view class="remark-box">
            <text class="remark-text">{{ detailData.remark || '无备注' }}</text>
          </view>
        </view>
      </view>
    </view>
    <view v-else
          class="no-data">
      <up-empty mode="data"
                text="暂无详情数据"></up-empty>
    </view>
  </view>
</template>
<script setup>
  import { ref, reactive, onMounted } from "vue";
  import { onLoad } from "@dcloudio/uni-app";
  import dayjs from "dayjs";
  import PageHeader from "@/components/PageHeader.vue";
  const detailData = ref(null);
  // è¿”回上一页
  const goBack = () => {
    uni.navigateBack();
  };
  // æ ¼å¼åŒ–日期
  const formatDate = date => {
    return date ? dayjs(date).format("YYYY-MM-DD") : "-";
  };
  // èŽ·å–çŠ¶æ€æ–‡æœ¬
  const getStatusText = status => {
    const statusMap = {
      0: "待下发",
      1: "部分下发",
      2: "已下发",
    };
    return statusMap[status] || "未知";
  };
  // èŽ·å–çŠ¶æ€ç±»åž‹
  const getStatusType = status => {
    const typeMap = {
      0: "warning",
      1: "primary",
      2: "info",
    };
    return typeMap[status] || "info";
  };
  onLoad(options => {
    if (options.data) {
      try {
        detailData.value = JSON.parse(decodeURIComponent(options.data));
      } catch (e) {
        console.error("解析数据失败", e);
        uni.showToast({
          title: "数据加载失败",
          icon: "error",
        });
      }
    }
  });
</script>
<style scoped lang="scss">
  .production-plan-detail {
    min-height: 100vh;
    background: #f8f9fa;
    padding-bottom: 40rpx;
  }
  .detail-container {
    padding: 20rpx;
  }
  .detail-card {
    background: #fff;
    border-radius: 16rpx;
    margin-bottom: 24rpx;
    overflow: hidden;
    box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.03);
    .card-title {
      display: flex;
      align-items: center;
      padding: 24rpx;
      border-bottom: 1rpx solid #f0f0f0;
      background: #fafafa;
      .title-text {
        font-size: 28rpx;
        font-weight: bold;
        color: #333;
        margin-left: 12rpx;
      }
    }
    .card-content {
      padding: 10rpx 24rpx;
      .info-item {
        display: flex;
        justify-content: space-between;
        align-items: center;
        padding: 20rpx 0;
        border-bottom: 1rpx solid #f9f9f9;
        &:last-child {
          border-bottom: none;
        }
        .label {
          font-size: 26rpx;
          color: #999;
        }
        .value {
          font-size: 26rpx;
          color: #333;
          text-align: right;
          max-width: 70%;
          &.font-bold {
            font-weight: bold;
          }
          &.highlight {
            color: #f56c6c;
            font-weight: bold;
          }
        }
      }
      .remark-box {
        padding: 20rpx 0;
        .remark-text {
          font-size: 26rpx;
          color: #666;
          line-height: 1.5;
        }
      }
    }
  }
  .no-data {
    padding-top: 200rpx;
  }
</style>
src/pages/productionManagement/mainProductionPlan/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,300 @@
<template>
    <view class="main-production-plan">
        <!-- ä½¿ç”¨é€šç”¨é¡µé¢å¤´éƒ¨ç»„ä»¶ -->
        <PageHeader title="主生产计划" @back="goBack" />
        <!-- æœç´¢åŒºåŸŸ -->
        <view class="search-section">
            <view class="search-bar">
                <view class="search-input">
                    <up-input
                        class="search-text"
                        placeholder="请输入计划号或产品名称"
                        v-model="searchForm.keyword"
                        @change="handleQuery"
                        clearable
                    />
                </view>
                <view class="filter-button" @click="handleQuery">
                    <up-icon name="search" size="24" color="#999"></up-icon>
                </view>
            </view>
        </view>
        <!-- åˆ—表区域 -->
        <scroll-view scroll-y class="list-container" v-if="tableData.length > 0" @scrolltolower="loadMore">
            <view v-for="(item, index) in tableData" :key="item.id || index" @click="goDetail(item)">
                <view class="ledger-item">
                    <view class="item-header">
                        <view class="item-left">
                            <view class="document-icon">
                                <up-icon name="file-text" size="16" color="#ffffff"></up-icon>
                            </view>
                            <text class="item-id">{{ item.mpsNo }}</text>
                        </view>
                        <view class="item-right">
                            <up-tag :text="getStatusText(item.status)" :type="getStatusType(item.status)" size="mini" />
                        </view>
                    </view>
                    <up-divider></up-divider>
                    <view class="item-details">
                        <view class="detail-row">
                            <text class="detail-label">产品名称</text>
                            <text class="detail-value">{{ item.productName || '-' }}</text>
                        </view>
                        <view class="detail-row">
                            <text class="detail-label">规格型号</text>
                            <text class="detail-value">{{ item.model || '-' }}</text>
                        </view>
                        <view class="detail-row">
                            <text class="detail-label">所需数量</text>
                            <text class="detail-value highlight">{{ item.qtyRequired || 0 }} {{ item.unit || '方' }}</text>
                        </view>
                        <view class="detail-row">
                            <text class="detail-label">需求日期</text>
                            <text class="detail-value">{{ formatDate(item.requiredDate) }}</text>
                        </view>
                        <view class="detail-row">
                            <text class="detail-label">来源</text>
                            <text class="detail-value">{{ item.source === '销售' ? '销售' : '内部' }}</text>
                        </view>
                    </view>
                    <view class="item-footer">
                        <text class="more-detail">查看详情</text>
                        <up-icon name="arrow-right" size="14" color="#999"></up-icon>
                    </view>
                </view>
            </view>
            <up-loadmore :status="loadStatus" v-if="tableData.length >= page.size" />
        </scroll-view>
        <view v-else class="no-data">
            <up-empty mode="data" text="暂无主生产计划数据"></up-empty>
        </view>
    </view>
</template>
<script setup>
import { ref, reactive, toRefs, getCurrentInstance } from "vue";
import { onShow } from '@dcloudio/uni-app';
import dayjs from "dayjs";
import { productionPlanListPage } from "@/api/productionManagement/productionPlan.js";
import PageHeader from "@/components/PageHeader.vue";
const { proxy } = getCurrentInstance();
// åŠ è½½çŠ¶æ€
const loading = ref(false);
const loadStatus = ref('loadmore');
// åˆ—表数据
const tableData = ref([]);
// åˆ†é¡µé…ç½®
const page = reactive({
    current: 1,
    size: 10,
    total: 0,
});
// æœç´¢è¡¨å•数据
const data = reactive({
    searchForm: {
        keyword: "",
        mpsNo: "",
        productName: ""
    },
});
const { searchForm } = toRefs(data);
// è¿”回上一页
const goBack = () => {
    uni.navigateBack();
};
// æ ¼å¼åŒ–日期
const formatDate = (date) => {
    return date ? dayjs(date).format('YYYY-MM-DD') : '-';
};
// èŽ·å–çŠ¶æ€æ–‡æœ¬
const getStatusText = (status) => {
    const statusMap = {
        0: "待下发",
        1: "部分下发",
        2: "已下发",
    };
    return statusMap[status] || "未知";
};
// èŽ·å–çŠ¶æ€ç±»åž‹ (uView tag type)
const getStatusType = (status) => {
    const typeMap = {
        0: "warning",
        1: "primary",
        2: "info",
    };
    return typeMap[status] || "info";
};
// æŸ¥è¯¢åˆ—表
const handleQuery = () => {
    page.current = 1;
    tableData.value = [];
    getList();
};
// åŠ è½½æ›´å¤š
const loadMore = () => {
    if (loadStatus.value === 'nomore' || loading.value) return;
    page.current++;
    getList();
};
// èŽ·å–åˆ—è¡¨æ•°æ®
const getList = () => {
    loading.value = true;
    loadStatus.value = 'loading';
    // æž„造请求参数
    // PC端接口支持 mpsNo, productName ç­‰ï¼Œè¿™é‡Œç®€å•处理,如果 keyword å­˜åœ¨ï¼Œåˆ™å°è¯•匹配
    const params = {
        current: page.current,
        size: page.size,
        mpsNo: searchForm.value.keyword, // ç®€å•处理:搜索号
        productName: searchForm.value.keyword // ç®€å•处理:搜索名称
    };
    productionPlanListPage(params).then((res) => {
        loading.value = false;
        const records = res.data.records || [];
        if (page.current === 1) {
            tableData.value = records;
        } else {
            tableData.value = [...tableData.value, ...records];
        }
        if (records.length < page.size) {
            loadStatus.value = 'nomore';
        } else {
            loadStatus.value = 'loadmore';
        }
        page.total = res.data.total || 0;
    }).catch(() => {
        loading.value = false;
        loadStatus.value = 'loadmore';
        uni.showToast({
            title: '加载失败',
            icon: 'error'
        });
    });
};
// è·³è½¬è¯¦æƒ…
const goDetail = (item) => {
    uni.navigateTo({
        url: `/pages/productionManagement/mainProductionPlan/detail?data=${encodeURIComponent(JSON.stringify(item))}`
    });
};
// é¡µé¢æ˜¾ç¤ºæ—¶åŠ è½½æ•°æ®
onShow(() => {
    handleQuery();
});
</script>
<style scoped lang="scss">
@import '@/styles/sales-common.scss';
.main-production-plan {
    min-height: 100vh;
    background: #f8f9fa;
    display: flex;
    flex-direction: column;
}
.list-container {
    flex: 1;
    height: 0;
}
.ledger-item {
    background: #fff;
    margin: 20rpx;
    padding: 20rpx;
    border-radius: 12rpx;
    box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
    .item-header {
        display: flex;
        justify-content: space-between;
        align-items: center;
        padding-bottom: 10rpx;
        .item-left {
            display: flex;
            align-items: center;
            .document-icon {
                width: 40rpx;
                height: 40rpx;
                background: #3c9cff;
                border-radius: 8rpx;
                display: flex;
                justify-content: center;
                align-items: center;
                margin-right: 16rpx;
            }
            .item-id {
                font-size: 28rpx;
                font-weight: bold;
                color: #333;
            }
        }
    }
    .item-details {
        padding: 10rpx 0;
        .detail-row {
            display: flex;
            justify-content: space-between;
            margin-bottom: 12rpx;
            .detail-label {
                font-size: 26rpx;
                color: #999;
            }
            .detail-value {
                font-size: 26rpx;
                color: #333;
                &.highlight {
                    color: #f56c6c;
                    font-weight: bold;
                }
            }
        }
    }
    .item-footer {
        display: flex;
        justify-content: flex-end;
        align-items: center;
        padding-top: 16rpx;
        border-top: 1rpx solid #f0f0f0;
        .more-detail {
            font-size: 24rpx;
            color: #999;
            margin-right: 8rpx;
        }
    }
}
.no-data {
    padding-top: 200rpx;
}
</style>
src/pages/productionManagement/processRoute/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,287 @@
<template>
  <view class="process-route">
    <!-- ä½¿ç”¨é€šç”¨é¡µé¢å¤´éƒ¨ç»„ä»¶ -->
    <PageHeader title="工艺路线"
                @back="goBack" />
    <!-- æœç´¢åŒºåŸŸ -->
    <view class="search-section">
      <view class="search-bar">
        <view class="search-input">
          <up-input class="search-text"
                    placeholder="请输入规格名称搜索"
                    v-model="searchForm.model"
                    @change="handleQuery"
                    clearable />
        </view>
        <view class="filter-button"
              @click="handleQuery">
          <up-icon name="search"
                   size="24"
                   color="#999"></up-icon>
        </view>
      </view>
    </view>
    <!-- åˆ—表区域 -->
    <scroll-view scroll-y
                 class="list-container"
                 v-if="tableData.length > 0"
                 @scrolltolower="loadMore">
      <view v-for="(item, index) in tableData"
            :key="item.id || index"
            @click="goDetail(item)">
        <view class="ledger-item">
          <view class="item-header">
            <view class="item-left">
              <view class="document-icon">
                <up-icon name="share-square"
                         size="16"
                         color="#ffffff"></up-icon>
              </view>
              <text class="item-id">{{ item.processRouteCode }}</text>
            </view>
          </view>
          <up-divider></up-divider>
          <view class="item-details">
            <view class="detail-row">
              <text class="detail-label">产品名称</text>
              <text class="detail-value font-bold">{{ item.productName || '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">规格名称</text>
              <text class="detail-value">{{ item.model || '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">BOM编号</text>
              <text class="detail-value">{{ item.bomNo || '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">描述</text>
              <text class="detail-value">{{ item.description || '-' }}</text>
            </view>
          </view>
          <view class="item-footer">
            <text class="more-detail">路线项目</text>
            <up-icon name="arrow-right"
                     size="14"
                     color="#999"></up-icon>
          </view>
        </view>
      </view>
      <up-loadmore :status="loadStatus"
                   v-if="tableData.length >= page.size" />
    </scroll-view>
    <view v-else
          class="no-data">
      <up-empty mode="data"
                text="暂无工艺路线数据"></up-empty>
    </view>
  </view>
</template>
<script setup>
  import { ref, reactive, toRefs, getCurrentInstance } from "vue";
  import { onShow } from "@dcloudio/uni-app";
  import { listPage } from "@/api/productionManagement/processRoute.js";
  import PageHeader from "@/components/PageHeader.vue";
  const { proxy } = getCurrentInstance();
  // åŠ è½½çŠ¶æ€
  const loading = ref(false);
  const loadStatus = ref("loadmore");
  // åˆ—表数据
  const tableData = ref([]);
  // åˆ†é¡µé…ç½®
  const page = reactive({
    current: 1,
    size: 10,
    total: 0,
  });
  // æœç´¢è¡¨å•数据
  const data = reactive({
    searchForm: {
      model: "",
    },
  });
  const { searchForm } = toRefs(data);
  // è¿”回上一页
  const goBack = () => {
    uni.navigateBack();
  };
  // æŸ¥è¯¢åˆ—表
  const handleQuery = () => {
    page.current = 1;
    tableData.value = [];
    getList();
  };
  // åŠ è½½æ›´å¤š
  const loadMore = () => {
    if (loadStatus.value === "nomore" || loading.value) return;
    page.current++;
    getList();
  };
  // èŽ·å–åˆ—è¡¨æ•°æ®
  const getList = () => {
    loading.value = true;
    loadStatus.value = "loading";
    const params = {
      current: page.current,
      size: page.size,
      model: searchForm.value.model,
    };
    listPage(params)
      .then(res => {
        loading.value = false;
        const records = res.data.records || [];
        if (page.current === 1) {
          tableData.value = records;
        } else {
          tableData.value = [...tableData.value, ...records];
        }
        if (records.length < page.size) {
          loadStatus.value = "nomore";
        } else {
          loadStatus.value = "loadmore";
        }
        page.total = res.data.total || 0;
      })
      .catch(() => {
        loading.value = false;
        loadStatus.value = "loadmore";
        uni.showToast({
          title: "加载失败",
          icon: "error",
        });
      });
  };
  // è·³è½¬è·¯çº¿é¡¹ç›®
  const goDetail = item => {
    uni.navigateTo({
      url: `/pages/productionManagement/processRoute/items?id=${
        item.id
      }&processRouteCode=${
        item.processRouteCode
      }&productName=${encodeURIComponent(
        item.productName || ""
      )}&model=${encodeURIComponent(item.model || "")}&bomNo=${
        item.bomNo || ""
      }&bomId=${item.bomId || ""}&description=${encodeURIComponent(
        item.description || ""
      )}`,
    });
  };
  // é¡µé¢æ˜¾ç¤ºæ—¶åŠ è½½æ•°æ®
  onShow(() => {
    handleQuery();
  });
</script>
<style scoped lang="scss">
  @import "@/styles/sales-common.scss";
  .process-route {
    min-height: 100vh;
    background: #f8f9fa;
    display: flex;
    flex-direction: column;
  }
  .list-container {
    flex: 1;
    height: 0;
  }
  .ledger-item {
    background: #fff;
    margin: 20rpx;
    padding: 24rpx;
    border-radius: 16rpx;
    box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
    .item-header {
      display: flex;
      justify-content: space-between;
      align-items: center;
      padding-bottom: 12rpx;
      .item-left {
        display: flex;
        align-items: center;
        .document-icon {
          width: 44rpx;
          height: 44rpx;
          background: #3c9cff;
          border-radius: 10rpx;
          display: flex;
          justify-content: center;
          align-items: center;
          margin-right: 20rpx;
        }
        .item-id {
          font-size: 30rpx;
          font-weight: bold;
          color: #333;
        }
      }
    }
    .item-details {
      padding: 16rpx 0;
      .detail-row {
        display: flex;
        justify-content: space-between;
        align-items: flex-start;
        margin-bottom: 16rpx;
        .detail-label {
          font-size: 26rpx;
          color: #999;
          min-width: 140rpx;
        }
        .detail-value {
          font-size: 26rpx;
          color: #333;
          text-align: right;
          flex: 1;
          &.font-bold {
            font-weight: bold;
          }
        }
      }
    }
    .item-footer {
      display: flex;
      justify-content: flex-end;
      align-items: center;
      padding-top: 16rpx;
      border-top: 1rpx solid #f0f0f0;
      .more-detail {
        font-size: 24rpx;
        color: #3c9cff;
        margin-right: 8rpx;
      }
    }
  }
  .no-data {
    padding-top: 200rpx;
  }
</style>
src/pages/productionManagement/processRoute/items.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,554 @@
<template>
  <view class="process-route-items">
    <PageHeader title="路线项目"
                @back="goBack" />
    <!-- è·¯çº¿åŸºç¡€ä¿¡æ¯å¡ç‰‡ -->
    <view class="route-info-card">
      <view class="info-row">
        <text class="label">工艺路线编号</text>
        <text class="value">{{ routeInfo.processRouteCode || '-' }}</text>
      </view>
      <view class="info-row">
        <text class="label">产品名称</text>
        <text class="value">{{ routeInfo.productName || '-' }}</text>
      </view>
      <view class="info-row">
        <text class="label">规格名称</text>
        <text class="value">{{ routeInfo.model || '-' }}</text>
      </view>
      <view class="info-row">
        <text class="label">BOM编号</text>
        <text class="value">{{ routeInfo.bomNo || '-' }}</text>
      </view>
    </view>
    <!-- é€‰é¡¹å¡åˆ‡æ¢ -->
    <view class="tabs-box">
      <up-tabs :list="tabsList"
               @click="handleTabClick"
               :current="currentTab"></up-tabs>
    </view>
    <!-- å·¥åºé¡¹ç›®åˆ—表 -->
    <scroll-view scroll-y
                 class="content-scroll"
                 v-if="currentTab === 0">
      <view v-if="itemsList.length > 0">
        <view v-for="(item, index) in itemsList"
              :key="index"
              class="process-card">
          <view class="card-header">
            <view class="index-badge">{{ index + 1 }}</view>
            <text class="process-name">{{ item.technologyOperationName || item.operationName || '-' }}</text>
          </view>
          <view class="card-content">
            <view class="detail-row">
              <text class="detail-label">关联产品</text>
              <text class="detail-value">{{ item.productName || '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">规格型号</text>
              <text class="detail-value">{{ item.model || '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">单位</text>
              <text class="detail-value">{{ item.unit || '-' }}</text>
            </view>
            <view class="tag-row">
              <up-tag v-if="item.isQuality"
                      text="质检"
                      type="primary"
                      size="mini"
                      plain />
              <up-tag v-if="item.isProduction"
                      text="生产"
                      type="success"
                      size="mini"
                      plain />
              <up-tag v-if="item.type==0"
                      text="计时"
                      type="info"
                      size="mini"
                      plain />
              <up-tag v-else
                      text="计件"
                      type="warning"
                      size="mini"
                      plain />
            </view>
          </view>
          <view class="card-footer"
                @click="showParams(item)">
            <text class="action-text">查看参数列表</text>
            <up-icon name="arrow-right"
                     size="14"
                     color="#3c9cff"></up-icon>
          </view>
        </view>
      </view>
      <view v-else
            class="no-data">
        <up-empty mode="data"
                  text="暂无路线项目"></up-empty>
      </view>
    </scroll-view>
    <!-- BOM ç»“构展示 -->
    <scroll-view scroll-y
                 class="content-scroll"
                 v-if="currentTab === 1">
      <view v-if="bomList.length > 0"
            class="bom-tree">
        <view v-for="(node, nIndex) in flatBomList"
              :key="nIndex"
              class="bom-node"
              :style="{ paddingLeft: (node.level * 40) + 'rpx' }">
          <view class="bom-node-inner">
            <view class="bom-line"
                  v-if="node.level > 0"></view>
            <view class="bom-content">
              <view class="bom-header">
                <text class="bom-product">{{ node.productName }}</text>
                <text class="bom-model"
                      v-if="node.model">({{ node.model }})</text>
              </view>
              <view class="bom-details">
                <text class="bom-info">工序: {{ node.operationName || '-' }}</text>
                <text class="bom-info">所需: {{ node.unitQuantity || 0 }} {{ node.unit || '' }}</text>
              </view>
            </view>
          </view>
        </view>
      </view>
      <view v-else
            class="no-data">
        <up-empty mode="data"
                  text="暂无 BOM ç»“æž„"></up-empty>
      </view>
    </scroll-view>
    <!-- å‚数列表弹窗 -->
    <up-popup :show="showPopup"
              mode="bottom"
              @close="showPopup = false"
              round="10">
      <view class="popup-content">
        <view class="popup-header">
          <text class="title">参数列表 - {{ currentItem.technologyOperationName || currentItem.operationName }}</text>
          <up-icon name="close"
                   size="20"
                   @click="showPopup = false"></up-icon>
        </view>
        <scroll-view scroll-y
                     class="param-list">
          <view v-if="paramList.length > 0">
            <view v-for="(param, pIndex) in paramList"
                  :key="pIndex"
                  class="param-item">
              <view class="param-row">
                <text class="param-label">参数名称:</text>
                <text class="param-value">{{ param.paramName || '-' }}</text>
              </view>
              <view class="param-row">
                <text class="param-label">标准值:</text>
                <text class="param-value">{{ param.standardValue || '-' }}</text>
              </view>
              <view class="param-row">
                <text class="param-label">单位:</text>
                <text class="param-value">{{ param.unit || '-' }}</text>
              </view>
            </view>
          </view>
          <view v-else
                class="no-record">
            <text>暂无参数记录</text>
          </view>
        </scroll-view>
      </view>
    </up-popup>
  </view>
</template>
<script setup>
  import { ref, reactive, computed } from "vue";
  import { onLoad } from "@dcloudio/uni-app";
  import {
    findProcessRouteItemList,
    getProcessParamList,
    queryBomList,
  } from "@/api/productionManagement/processRoute.js";
  import {
    queryOrderBomList,
    findProcessParamListOrder,
  } from "@/api/productionManagement/productionOrder.js";
  import PageHeader from "@/components/PageHeader.vue";
  const routeInfo = ref({});
  const itemsList = ref([]);
  const bomList = ref([]);
  const loading = ref(false);
  const pageType = ref("route"); // route | order
  // é€‰é¡¹å¡
  const tabsList = reactive([{ name: "路线项目" }, { name: "BOM结构" }]);
  const currentTab = ref(0);
  // å¼¹çª—相关
  const showPopup = ref(false);
  const currentItem = ref({});
  const paramList = ref([]);
  const paramLoading = ref(false);
  const goBack = () => {
    uni.navigateBack();
  };
  const handleTabClick = item => {
    currentTab.value = item.index;
    if (item.index === 1 && bomList.value.length === 0) {
      fetchBom();
    }
  };
  // æ‰å¹³åŒ– BOM æ ‘用于展示
  const flatBomList = computed(() => {
    const result = [];
    const flatten = (nodes, level = 0) => {
      nodes.forEach(node => {
        result.push({ ...node, level });
        if (node.children && node.children.length > 0) {
          flatten(node.children, level + 1);
        }
      });
    };
    flatten(bomList.value);
    return result;
  });
  onLoad(options => {
    if (options.id) {
      pageType.value = options.type || "route";
      routeInfo.value = {
        id: options.id,
        processRouteCode: options.processRouteCode || "",
        productName: decodeURIComponent(options.productName || ""),
        model: decodeURIComponent(options.model || ""),
        bomNo: options.bomNo || "",
        bomId: options.bomId || "",
        description: decodeURIComponent(options.description || ""),
        orderId: options.orderId || "",
      };
      fetchItems(options.id);
    }
  });
  const fetchItems = id => {
    loading.value = true;
    findProcessRouteItemList({ routeId: id, orderId: routeInfo.value.orderId })
      .then(res => {
        itemsList.value = res.data || [];
        loading.value = false;
      })
      .catch(() => {
        loading.value = false;
        uni.showToast({
          title: "获取项目失败",
          icon: "error",
        });
      });
  };
  const fetchBom = () => {
    console.log(routeInfo.value.bomId, "routeInfo.value.bomId");
    if (!routeInfo.value.bomId) return;
    loading.value = true;
    const api = pageType.value === "order" ? queryOrderBomList : queryBomList;
    api(routeInfo.value.bomId)
      .then(res => {
        bomList.value = res.data || [];
        loading.value = false;
      })
      .catch(() => {
        loading.value = false;
        uni.showToast({
          title: "获取 BOM å¤±è´¥",
          icon: "error",
        });
      });
  };
  const showParams = item => {
    currentItem.value = item;
    showPopup.value = true;
    paramLoading.value = true;
    paramList.value = [];
    const api =
      pageType.value === "order"
        ? findProcessParamListOrder
        : getProcessParamList;
    const params =
      pageType.value === "order"
        ? {
            productionOrderRoutingOperationId: item.id,
            productionOrderId: routeInfo.value.orderId,
          }
        : { technologyRoutingOperationId: item.id };
    api(params)
      .then(res => {
        paramList.value = res.data || [];
        paramLoading.value = false;
      })
      .catch(() => {
        paramLoading.value = false;
        uni.showToast({
          title: "获取参数失败",
          icon: "error",
        });
      });
  };
</script>
<style scoped lang="scss">
  .process-route-items {
    min-height: 100vh;
    background: #f8f9fa;
    display: flex;
    flex-direction: column;
  }
  .route-info-card {
    background: #fff;
    margin: 20rpx;
    padding: 24rpx;
    border-radius: 16rpx;
    box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
    .info-row {
      display: flex;
      justify-content: space-between;
      margin-bottom: 12rpx;
      &:last-child {
        margin-bottom: 0;
      }
      .label {
        font-size: 26rpx;
        color: #999;
      }
      .value {
        font-size: 26rpx;
        color: #333;
        font-weight: bold;
      }
    }
  }
  .tabs-box {
    background: #fff;
    margin-bottom: 10rpx;
  }
  .content-scroll {
    flex: 1;
    height: 0;
    padding: 0 20rpx;
  }
  .process-card {
    background: #fff;
    margin-bottom: 24rpx;
    border-radius: 16rpx;
    overflow: hidden;
    box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.05);
    .card-header {
      display: flex;
      align-items: center;
      padding: 20rpx 24rpx;
      background: #fcfcfc;
      border-bottom: 1rpx solid #f5f5f5;
      .index-badge {
        width: 40rpx;
        height: 40rpx;
        background: #3c9cff;
        color: #fff;
        border-radius: 20rpx;
        display: flex;
        justify-content: center;
        align-items: center;
        font-size: 24rpx;
        margin-right: 20rpx;
      }
      .process-name {
        font-size: 28rpx;
        font-weight: bold;
        color: #333;
      }
    }
    .card-content {
      padding: 24rpx;
      .detail-row {
        display: flex;
        justify-content: space-between;
        margin-bottom: 12rpx;
        .detail-label {
          font-size: 24rpx;
          color: #999;
        }
        .detail-value {
          font-size: 24rpx;
          color: #666;
        }
      }
      .tag-row {
        display: flex;
        gap: 16rpx;
        margin-top: 10rpx;
      }
    }
    .card-footer {
      padding: 16rpx 24rpx;
      border-top: 1rpx dashed #eee;
      display: flex;
      justify-content: space-between;
      align-items: center;
      .action-text {
        font-size: 24rpx;
        color: #3c9cff;
      }
    }
  }
  /* BOM æ ‘样式 */
  .bom-tree {
    padding: 20rpx 0;
  }
  .bom-node {
    position: relative;
    margin-bottom: 20rpx;
  }
  .bom-node-inner {
    background: #fff;
    padding: 20rpx;
    border-radius: 12rpx;
    box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.03);
    display: flex;
    align-items: center;
  }
  .bom-line {
    position: absolute;
    left: -20rpx;
    top: 50%;
    width: 20rpx;
    height: 2rpx;
    background: #ddd;
  }
  .bom-content {
    flex: 1;
  }
  .bom-header {
    display: flex;
    align-items: center;
    margin-bottom: 8rpx;
    .bom-product {
      font-size: 28rpx;
      font-weight: bold;
      color: #333;
    }
    .bom-model {
      font-size: 24rpx;
      color: #999;
      margin-left: 10rpx;
    }
  }
  .bom-details {
    display: flex;
    justify-content: space-between;
    .bom-info {
      font-size: 24rpx;
      color: #666;
    }
  }
  .no-data {
    padding-top: 100rpx;
  }
  /* å¼¹çª—样式 */
  .popup-content {
    background: #fff;
    padding: 30rpx;
    max-height: 70vh;
    display: flex;
    flex-direction: column;
  }
  .popup-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding-bottom: 30rpx;
    border-bottom: 1rpx solid #eee;
    .title {
      font-size: 30rpx;
      font-weight: bold;
      color: #333;
    }
  }
  .param-list {
    flex: 1;
    height: 0;
    padding-top: 20rpx;
  }
  .param-item {
    padding: 20rpx;
    background: #f9f9f9;
    border-radius: 12rpx;
    margin-bottom: 16rpx;
    .param-row {
      display: flex;
      margin-bottom: 8rpx;
      &:last-child {
        margin-bottom: 0;
      }
      .param-label {
        font-size: 24rpx;
        color: #999;
        width: 140rpx;
      }
      .param-value {
        font-size: 24rpx;
        color: #333;
        flex: 1;
      }
    }
  }
  .no-record {
    padding: 100rpx 0;
    text-align: center;
    color: #999;
    font-size: 26rpx;
  }
</style>
src/pages/productionManagement/processStatistics/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,370 @@
<template>
  <view class="process-statistics">
    <PageHeader title="工序生产实况"
                @back="goBack" />
    <!-- æœç´¢åŒºåŸŸ -->
    <view class="search-section">
      <view class="date-picker-container"
            @click="showCalendar = true">
        <view class="date-input">
          <up-icon name="calendar"
                   size="20"
                   color="#999"></up-icon>
          <text class="date-text"
                :class="{ 'placeholder': !searchForm.startDate }">{{ dateRangeText }}</text>
          <view v-if="searchForm.startDate"
                class="clear-icon-wrapper"
                @click.stop="handleClearDate">
            <up-icon name="close-circle-fill"
                     size="18"
                     color="#c0c4cc"></up-icon>
          </view>
        </view>
        <view class="search-btn-wrapper">
          <up-button type="primary"
                     size="small"
                     text="搜索"
                     @click.stop="handleQuery"></up-button>
        </view>
      </view>
    </view>
    <!-- ç»Ÿè®¡å¡ç‰‡åˆ—表 -->
    <scroll-view scroll-y
                 class="stats-list">
      <view v-if="loading"
            class="loading-box">
        <up-loading-icon text="加载中..."></up-loading-icon>
      </view>
      <view v-else-if="statsData.length > 0"
            class="card-grid">
        <view v-for="(item, index) in statsData"
              :key="index"
              class="stats-card">
          <view class="card-header">
            <text class="process-tag">{{ item.name }}</text>
            <view class="header-details">
              <view class="detail-row">
                <text class="label">计划数</text>
                <text class="value">{{ item.planned }}</text>
              </view>
              <view class="detail-row">
                <text class="label">良品数</text>
                <text class="value good">{{ item.good }}</text>
              </view>
              <view class="detail-row">
                <text class="label">不良品</text>
                <text class="value bad">{{ item.bad }}</text>
              </view>
            </view>
          </view>
          <view class="card-body">
            <view class="main-stat">
              <text class="big-number">{{ item.total }}</text>
              <text class="sub-label">生产任务数</text>
            </view>
          </view>
          <view class="card-footer">
            <view class="progress-section">
              <view class="progress-header">
                <text class="progress-label">生产进度</text>
                <text class="percentage-text">{{ item.percentage }}%</text>
              </view>
              <up-line-progress :percentage="Math.min(item.percentage, 100)"
                                :activeColor="getProgressColor(item.percentage)"
                                :show-text="false"
                                height="8"></up-line-progress>
            </view>
          </view>
        </view>
      </view>
      <view v-else
            class="no-data">
        <up-empty mode="data"
                  text="暂无工序统计数据"></up-empty>
      </view>
    </scroll-view>
    <!-- æ—¥åŽ†é€‰æ‹©å™¨ -->
    <up-calendar :show="showCalendar"
                 mode="range"
                 :maxDate="maxDate"
                 minDate="2026-01-01"
                 :monthNum="monthNum"
                 @confirm="onDateConfirm"
                 @close="showCalendar = false"></up-calendar>
  </view>
</template>
<script setup>
  import { ref, reactive, onMounted, computed } from "vue";
  import { getOperationStatistics } from "@/api/productionManagement/workOrder.js";
  import PageHeader from "@/components/PageHeader.vue";
  import dayjs from "dayjs";
  const loading = ref(false);
  const showCalendar = ref(false);
  const dateRange = ref([]);
  const maxDate = dayjs().format("YYYY-MM-DD");
  const monthNum = computed(() => {
    const min = dayjs("2022-02-01");
    const max = dayjs(maxDate);
    return max.diff(min, "month") + 1;
  });
  // const minDate = dayjs().subtract(7, "day").format("YYYY-MM-DD");
  const searchForm = reactive({
    startDate: "",
    endDate: "",
  });
  const statsData = ref([]);
  const dateRangeText = computed(() => {
    if (searchForm.startDate && searchForm.endDate) {
      return `${searchForm.startDate} è‡³ ${searchForm.endDate}`;
    }
    return "请选择日期区间";
  });
  const goBack = () => {
    uni.navigateBack();
  };
  const getProgressColor = percentage => {
    if (percentage >= 100) return "#67c23a";
    if (percentage >= 50) return "#3c9cff";
    if (percentage >= 25) return "#e6a23c";
    return "#f56c6c";
  };
  const onDateConfirm = e => {
    searchForm.startDate = e[0];
    searchForm.endDate = e[e.length - 1];
    showCalendar.value = false;
    handleQuery();
  };
  const getList = () => {
    loading.value = true;
    const params = {
      startDate: searchForm.startDate,
      endDate: searchForm.endDate,
    };
    getOperationStatistics(params)
      .then(res => {
        statsData.value = (res.data || []).map(item => ({
          name: item.operationName || "-",
          total: item.productionTaskCount || 0,
          planned: item.planQuantity || 0,
          good: item.goodQuantity || 0,
          bad: item.scrapQty || 0,
          percentage: Number(item.completionStatus || 0),
        }));
      })
      .finally(() => {
        loading.value = false;
      });
  };
  const handleQuery = () => {
    getList();
  };
  const handleClearDate = () => {
    searchForm.startDate = "";
    searchForm.endDate = "";
    handleQuery();
  };
  onMounted(() => {
    // é»˜è®¤æ—¶é—´ç½®ç©º
    searchForm.startDate = "";
    searchForm.endDate = "";
    getList();
  });
</script>
<style scoped lang="scss">
  @import "@/styles/procurement-common.scss";
  .process-statistics {
    min-height: 100vh;
    background-color: #f5f7fa;
    display: flex;
    flex-direction: column;
  }
  .search-section {
    background-color: #fff;
    padding: 24rpx 30rpx;
    margin-bottom: 20rpx;
    box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.02);
  }
  .date-picker-container {
    display: flex;
    align-items: center;
    width: 100%;
    .date-input {
      flex: 1;
      height: 80rpx;
      background-color: #f5f7fa;
      border: 1rpx solid #e4e7ed;
      border-radius: 12rpx;
      display: flex;
      align-items: center;
      padding: 0 24rpx;
      margin-right: 20rpx;
      transition: all 0.3s;
      &:active {
        background-color: #ebedf0;
      }
      .date-text {
        font-size: 28rpx;
        color: #303133;
        margin-left: 16rpx;
        flex: 1;
        &.placeholder {
          color: #c0c4cc;
        }
      }
      .clear-icon {
        padding: 10rpx;
        margin-right: -10rpx;
      }
    }
    .search-btn-wrapper {
      width: 140rpx;
    }
  }
  .stats-list {
    flex: 1;
    height: 0;
    padding: 0 24rpx 40rpx;
  }
  .loading-box {
    display: flex;
    justify-content: center;
    padding-top: 100rpx;
  }
  .card-grid {
    display: flex;
    flex-direction: column;
    gap: 24rpx;
  }
  .stats-card {
    background: #fff;
    border-radius: 16rpx;
    padding: 24rpx;
    box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.05);
    .card-header {
      display: flex;
      justify-content: space-between;
      align-items: flex-start;
      margin-bottom: 30rpx;
      .process-tag {
        background-color: #e6f7ff;
        color: #1890ff;
        padding: 6rpx 16rpx;
        border-radius: 8rpx;
        font-size: 26rpx;
        font-weight: bold;
      }
      .header-details {
        display: flex;
        flex-direction: column;
        gap: 4rpx;
        .detail-row {
          display: flex;
          justify-content: flex-end;
          align-items: center;
          gap: 12rpx;
          .label {
            font-size: 22rpx;
            color: #999;
          }
          .value {
            font-size: 24rpx;
            color: #333;
            font-weight: bold;
            min-width: 60rpx;
            text-align: right;
            &.good {
              color: #52c41a;
            }
            &.bad {
              color: #f56c6c;
            }
          }
        }
      }
    }
    .card-body {
      padding-bottom: 30rpx;
      border-bottom: 1rpx solid #f0f0f0;
      .main-stat {
        display: flex;
        flex-direction: column;
        align-items: center;
        .big-number {
          font-size: 56rpx;
          font-weight: bold;
          color: #333;
          line-height: 1;
        }
        .sub-label {
          font-size: 26rpx;
          color: #666;
          margin-top: 12rpx;
        }
      }
    }
    .card-footer {
      padding-top: 24rpx;
      .progress-section {
        .progress-header {
          display: flex;
          justify-content: space-between;
          margin-bottom: 12rpx;
          .progress-label {
            font-size: 24rpx;
            color: #999;
          }
          .percentage-text {
            font-size: 24rpx;
            font-weight: bold;
            color: #333;
          }
        }
      }
    }
  }
  .no-data {
    padding-top: 100rpx;
  }
</style>
src/pages/productionManagement/productionAccounting/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,506 @@
<template>
  <view class="production-accounting">
    <PageHeader title="生产核算"
                @back="goBack" />
    <!-- ç­›é€‰åŒºåŸŸ -->
    <view class="filter-section">
      <view class="date-type-selector">
        <up-tabs :list="dateTypeList"
                 :current="currentDateTypeIndex"
                 @change="handleDateTypeChange"
                 :activeStyle="{ color: '#2979ff', fontWeight: 'bold' }"
                 lineWidth="30"
                 lineHeight="3" />
      </view>
      <view class="date-picker-bar"
            @click="showDatePicker = true">
        <view class="date-display">
          <up-icon name="calendar"
                   size="20"
                   color="#2979ff"></up-icon>
          <text class="date-text">{{ dateDisplayText }}</text>
        </view>
        <up-icon name="arrow-right"
                 size="16"
                 color="#999"></up-icon>
      </view>
    </view>
    <!-- æ±‡æ€»åˆ—表 -->
    <view class="summary-section"
          v-if="!showDetail">
      <view class="section-header">
        <text class="section-title">生产人员汇总</text>
      </view>
      <view class="ledger-list"
            v-if="summaryList.length > 0">
        <view v-for="(item, index) in summaryList"
              :key="index"
              class="ledger-item"
              @click="handleRowClick(item)">
          <view class="item-header">
            <view class="item-left">
              <view class="user-icon">
                <up-icon name="account"
                         size="16"
                         color="#ffffff"></up-icon>
              </view>
              <text class="item-id">{{ item.schedulingUserName || '未知' }}</text>
            </view>
            <view class="item-right">
              <up-icon name="arrow-right"
                       size="16"
                       color="#999"></up-icon>
            </view>
          </view>
          <up-divider></up-divider>
          <view class="item-details">
            <view class="detail-grid">
              <view class="grid-item">
                <text class="grid-label">产量</text>
                <text class="grid-value">{{ item.finishedNum || 0 }}</text>
              </view>
              <view class="grid-item">
                <text class="grid-label">工资</text>
                <text class="grid-value highlight">Â¥{{ item.wages || 0 }}</text>
              </view>
              <view class="grid-item">
                <text class="grid-label">合格率</text>
                <text class="grid-value">{{ formatOutputRate(item.outputRate) }}</text>
              </view>
            </view>
          </view>
        </view>
      </view>
      <view v-else
            class="no-data">
        <up-empty mode="data"
                  text="暂无汇总数据" />
      </view>
    </view>
    <!-- æ˜Žç»†åˆ—表 (点击汇总行后显示) -->
    <view class="detail-section"
          v-else>
      <view class="section-header back-bar"
            @click="showDetail = false">
        <up-icon name="arrow-left"
                 size="16"
                 color="#2979ff"></up-icon>
        <text class="back-text">返回汇总 ({{ currentUserName }})</text>
      </view>
      <view class="ledger-list"
            v-if="detailList.length > 0">
        <view v-for="(item, index) in detailList"
              :key="index"
              class="ledger-item no-click">
          <view class="item-header">
            <view class="item-left">
              <view class="product-icon">
                <up-icon name="shopping-cart"
                         size="16"
                         color="#ffffff"></up-icon>
              </view>
              <text class="item-id">{{ item.productName }}</text>
            </view>
            <view class="item-tag">
              <text class="tag-text">{{ item.schedulingDate }}</text>
            </view>
          </view>
          <up-divider></up-divider>
          <view class="item-details">
            <view class="detail-row">
              <text class="detail-label">生产日期</text>
              <text class="detail-value">{{ item.schedulingDate || '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">生产人</text>
              <text class="detail-value">{{ item.schedulingUserName || '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">规格型号</text>
              <text class="detail-value">{{ item.productModelName }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">工序</text>
              <text class="detail-value">{{ item.process }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">生产数量</text>
              <text class="detail-value">{{ item.quantity }} {{ item.unit }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">工时(h)</text>
              <text class="detail-value">{{ item.workHour || 0 }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">工时定额</text>
              <text class="detail-value">{{ item.workHours }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">工资</text>
              <text class="detail-value highlight">Â¥{{ item.wages }}</text>
            </view>
          </view>
        </view>
        <up-loadmore :status="loadStatus"
                     @loadmore="getDetailList" />
      </view>
      <view v-else
            class="no-data">
        <up-empty mode="data"
                  text="暂无明细数据" />
      </view>
    </view>
    <!-- æ—¥æœŸé€‰æ‹©å™¨ -->
    <up-datetime-picker :show="showDatePicker"
                        v-model="pickerValue"
                        :mode="currentDateType === 'day' ? 'date' : 'year-month'"
                        @confirm="handleDateConfirm"
                        @cancel="showDatePicker = false" />
  </view>
</template>
<script setup>
  import { ref, reactive, computed, onMounted } from "vue";
  import {
    salesLedgerProductionAccountingList,
    salesLedgerProductionAccountingListProductionDetails,
  } from "@/api/productionManagement/productionCosting";
  import PageHeader from "@/components/PageHeader.vue";
  import dayjs from "dayjs";
  // ç­›é€‰ç›¸å…³
  const dateTypeList = [{ name: "日" }, { name: "月" }];
  const currentDateTypeIndex = ref(0);
  const currentDateType = computed(() =>
    currentDateTypeIndex.value === 0 ? "day" : "month"
  );
  const showDatePicker = ref(false);
  const pickerValue = ref(Date.now());
  const selectedDate = ref(dayjs().format("YYYY-MM-DD"));
  const dateDisplayText = computed(() => {
    return currentDateType.value === "day"
      ? selectedDate.value
      : dayjs(selectedDate.value).format("YYYY-MM");
  });
  // æ•°æ®ç›¸å…³
  const summaryList = ref([]);
  const detailList = ref([]);
  const showDetail = ref(false);
  const currentUserName = ref("");
  const loadStatus = ref("loadmore");
  const page = reactive({
    current: 1,
    size: 20,
    total: 0,
  });
  const page1 = reactive({
    current: 1,
    size: 20,
    total: 0,
  });
  // è¿”回上一页
  const goBack = () => {
    uni.navigateBack();
  };
  // åˆ‡æ¢æ—¥æœŸç±»åž‹
  const handleDateTypeChange = index => {
    currentDateTypeIndex.value = index.index;
    if (currentDateType.value === "day") {
      selectedDate.value = dayjs().format("YYYY-MM-DD");
    } else {
      selectedDate.value = dayjs().startOf("month").format("YYYY-MM-DD");
    }
    reloadData();
  };
  // ç¡®è®¤æ—¥æœŸé€‰æ‹©
  const handleDateConfirm = e => {
    selectedDate.value = dayjs(e.value).format("YYYY-MM-DD");
    showDatePicker.value = false;
    reloadData();
  };
  // æ ¼å¼åŒ–合格率
  const formatOutputRate = val => {
    if (val == null || val === "") return "-";
    return parseFloat(val).toFixed(2) + "%";
  };
  // åŠ è½½æ±‡æ€»åˆ—è¡¨
  const getSummaryList = () => {
    uni.showLoading({ title: "加载中..." });
    const params = {
      dateType: currentDateType.value,
      entryDate: currentDateType.value === "day" ? selectedDate.value : undefined,
      entryDateStart:
        currentDateType.value === "month"
          ? dayjs(selectedDate.value).startOf("month").format("YYYY-MM-DD")
          : undefined,
      entryDateEnd:
        currentDateType.value === "month"
          ? dayjs(selectedDate.value).endOf("month").format("YYYY-MM-DD")
          : undefined,
      pageNum: page.current,
      pageSize: page.size,
    };
    salesLedgerProductionAccountingList(params)
      .then(res => {
        summaryList.value = res.data.records || [];
        page.total = res.data.total || 0;
      })
      .finally(() => {
        uni.hideLoading();
      });
  };
  // åŠ è½½æ˜Žç»†åˆ—è¡¨
  const getDetailList = (isLoadMore = false) => {
    if (!isLoadMore) {
      page1.current = 1;
      detailList.value = [];
    }
    loadStatus.value = "loading";
    const params = {
      schedulingUserName: currentUserName.value,
      dateType: currentDateType.value,
      entryDate: currentDateType.value === "day" ? selectedDate.value : undefined,
      entryDateStart:
        currentDateType.value === "month"
          ? dayjs(selectedDate.value).startOf("month").format("YYYY-MM-DD")
          : undefined,
      entryDateEnd:
        currentDateType.value === "month"
          ? dayjs(selectedDate.value).endOf("month").format("YYYY-MM-DD")
          : undefined,
      pageNum: page1.current,
      pageSize: page1.size,
    };
    salesLedgerProductionAccountingListProductionDetails(params)
      .then(res => {
        const records = res.data.records || [];
        detailList.value = isLoadMore
          ? [...detailList.value, ...records]
          : records;
        page1.total = res.data.total || 0;
        if (detailList.value.length >= page1.total) {
          loadStatus.value = "nomore";
        } else {
          loadStatus.value = "loadmore";
          page1.current++;
        }
      })
      .catch(() => {
        loadStatus.value = "loadmore";
      });
  };
  // ç‚¹å‡»æ±‡æ€»è¡Œ
  const handleRowClick = item => {
    currentUserName.value = item.schedulingUserName;
    showDetail.value = true;
    getDetailList();
  };
  // é‡æ–°åŠ è½½æ•°æ®
  const reloadData = () => {
    page.current = 1;
    showDetail.value = false;
    getSummaryList();
  };
  onMounted(() => {
    getSummaryList();
  });
</script>
<style scoped lang="scss">
  .production-accounting {
    background-color: #f5f7fa;
    min-height: 100vh;
    padding-bottom: 30rpx;
    .filter-section {
      background-color: #ffffff;
      padding: 20rpx 30rpx;
      margin-bottom: 20rpx;
      .date-type-selector {
        margin-bottom: 20rpx;
      }
      .date-picker-bar {
        display: flex;
        justify-content: space-between;
        align-items: center;
        background-color: #f0f4ff;
        padding: 16rpx 24rpx;
        border-radius: 8rpx;
        .date-display {
          display: flex;
          align-items: center;
          gap: 12rpx;
          .date-text {
            font-size: 28rpx;
            color: #2979ff;
            font-weight: bold;
          }
        }
      }
    }
    .section-header {
      padding: 20rpx 30rpx;
      .section-title {
        font-size: 30rpx;
        font-weight: bold;
        color: #333;
        border-left: 8rpx solid #2979ff;
        padding-left: 16rpx;
      }
      &.back-bar {
        display: flex;
        align-items: center;
        gap: 10rpx;
        background-color: #ffffff;
        margin-bottom: 20rpx;
        .back-text {
          font-size: 28rpx;
          color: #2979ff;
        }
      }
    }
    .ledger-list {
      padding: 0 20rpx;
      .ledger-item {
        background-color: #ffffff;
        border-radius: 16rpx;
        padding: 24rpx;
        margin-bottom: 20rpx;
        box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05);
        &:active {
          background-color: #f9f9f9;
        }
        &.no-click:active {
          background-color: #ffffff;
        }
        .item-header {
          display: flex;
          justify-content: space-between;
          align-items: center;
          margin-bottom: 16rpx;
          .item-left {
            display: flex;
            align-items: center;
            gap: 12rpx;
            .user-icon,
            .product-icon {
              width: 48rpx;
              height: 48rpx;
              background: linear-gradient(135deg, #2979ff, #64a1ff);
              border-radius: 8rpx;
              display: flex;
              align-items: center;
              justify-content: center;
            }
            .item-id {
              font-size: 28rpx;
              font-weight: bold;
              color: #333;
            }
          }
          .item-tag {
            background-color: #f0f4ff;
            padding: 4rpx 12rpx;
            border-radius: 4rpx;
            .tag-text {
              font-size: 24rpx;
              color: #2979ff;
            }
          }
        }
        .item-details {
          padding-top: 10rpx;
          .detail-grid {
            display: flex;
            justify-content: space-between;
            .grid-item {
              display: flex;
              flex-direction: column;
              align-items: center;
              flex: 1;
              .grid-label {
                font-size: 24rpx;
                color: #999;
                margin-bottom: 8rpx;
              }
              .grid-value {
                font-size: 28rpx;
                color: #333;
                font-weight: 500;
                &.highlight {
                  color: #ff5a5f;
                }
              }
            }
          }
          .detail-row {
            display: flex;
            justify-content: space-between;
            margin-bottom: 12rpx;
            .detail-label {
              font-size: 26rpx;
              color: #999;
            }
            .detail-value {
              font-size: 26rpx;
              color: #333;
              &.highlight {
                color: #ff5a5f;
                font-weight: bold;
              }
            }
          }
        }
      }
    }
    .no-data {
      padding-top: 100rpx;
    }
  }
</style>
src/pages/productionManagement/productionDispatching/components/DispatchModal.vue
@@ -1,399 +1,379 @@
<template>
    <up-popup
        v-model:show="show"
        mode="bottom"
        :round="20"
        :safeAreaInsetBottom="true"
        @close="handleClose"
        @open="handleOpen"
    >
        <view class="dispatch-modal">
            <!-- å¤´éƒ¨ -->
            <view class="modal-header">
                <text class="modal-title">生产派工</text>
                <view class="close-btn" @click="handleClose">
                    <up-icon name="close" size="20" color="#999"></up-icon>
                </view>
            </view>
            <!-- è¡¨å•内容 -->
            <view class="modal-content">
                <up-form
                    :model="form"
                    ref="formRef"
                    :rules="rules"
                    labelWidth="120"
                >
                    <!-- é¡¹ç›®åŸºæœ¬ä¿¡æ¯ -->
                    <view class="form-section">
                        <text class="section-title">项目信息</text>
                        <up-form-item label="项目名称" prop="projectName">
                            <up-input
                                v-model="form.projectName"
                                disabled
                                placeholder="项目名称"
                            />
                        </up-form-item>
                        <up-form-item label="产品大类" prop="productCategory">
                            <up-input
                                v-model="form.productCategory"
                                disabled
                                placeholder="产品大类"
                            />
                        </up-form-item>
                    </view>
                    <!-- æ•°é‡ä¿¡æ¯ -->
                    <view class="form-section">
                        <text class="section-title">数量信息</text>
                        <up-form-item label="总数量" prop="quantity">
                            <up-input
                                v-model="form.quantity"
                                disabled
                                placeholder="总数量"
                            />
                        </up-form-item>
                        <up-form-item label="待排产数量" prop="pendingQuantity">
                            <up-input
                                v-model="form.pendingQuantity"
                                disabled
                                placeholder="待排产数量"
                            />
                        </up-form-item>
                        <up-form-item label="本次排产数量" prop="schedulingNum" required>
                            <up-number-box
                                v-model="form.schedulingNum"
                                :min="0"
                                :max="form.pendingQuantity"
                                :step="0.1"
                                :precision="2"
                                @change="handleNumChange"
                            />
                        </up-form-item>
                    </view>
                    <!-- æ´¾å·¥ä¿¡æ¯ -->
                    <view class="form-section">
                        <text class="section-title">派工信息</text>
                        <up-form-item label="派工人" prop="schedulingUserId" required>
                            <up-input
                                v-model="selectedUserName"
                                placeholder="请选择派工人"
                                readonly
                                @click="showUserPicker = true"
                                suffixIcon="arrow-down"
                            />
                        </up-form-item>
                        <up-form-item label="派工日期" prop="schedulingDate" required>
                            <up-input
                                v-model="form.schedulingDate"
                                placeholder="请选择派工日期"
                                readonly
                                @click="showDatePicker = true"
                                suffixIcon="calendar"
                            />
                        </up-form-item>
                    </view>
                </up-form>
            </view>
            <!-- åº•部按钮 -->
            <view class="modal-footer">
                <up-button
                    @click="handleClose"
                    text="取消"
                    type="info"
                    plain
                    :customStyle="{ marginRight: '12px', flex: 1 }"
                />
                <up-button
                    @click="handleConfirm"
                    text="确认派工"
                    type="primary"
                    :customStyle="{ flex: 1 }"
                    :loading="submitting"
                />
            </view>
        </view>
        <!-- äººå‘˜é€‰æ‹©å™¨ -->
        <up-picker
            v-model="showUserPicker"
            :columns="userColumns"
            @confirm="handleUserSelect"
            @cancel="showUserPicker = false"
        />
        <!-- æ—¥æœŸé€‰æ‹©å™¨ -->
        <up-datetime-picker
            v-model="showDatePicker"
            mode="date"
            @confirm="handleDateSelect"
            @cancel="showDatePicker = false"
        />
    </up-popup>
  <up-popup v-model:show="show"
            mode="bottom"
            :round="20"
            :safeAreaInsetBottom="true"
            @close="handleClose"
            @open="handleOpen">
    <view class="dispatch-modal">
      <!-- å¤´éƒ¨ -->
      <view class="modal-header">
        <text class="modal-title">生产派工</text>
        <view class="close-btn"
              @click="handleClose">
          <up-icon name="close"
                   size="20"
                   color="#999"></up-icon>
        </view>
      </view>
      <!-- è¡¨å•内容 -->
      <view class="modal-content">
        <up-form :model="form"
                 ref="formRef"
                 :rules="rules"
                 labelWidth="120">
          <!-- é¡¹ç›®åŸºæœ¬ä¿¡æ¯ -->
          <view class="form-section">
            <text class="section-title">项目信息</text>
            <up-form-item label="项目名称"
                          prop="projectName">
              <up-input v-model="form.projectName"
                        disabled
                        placeholder="项目名称" />
            </up-form-item>
            <up-form-item label="产品大类"
                          prop="productCategory">
              <up-input v-model="form.productCategory"
                        disabled
                        placeholder="产品大类" />
            </up-form-item>
          </view>
          <!-- æ•°é‡ä¿¡æ¯ -->
          <view class="form-section">
            <text class="section-title">数量信息</text>
            <up-form-item label="总数量"
                          prop="quantity">
              <up-input v-model="form.quantity"
                        disabled
                        placeholder="总数量" />
            </up-form-item>
            <up-form-item label="待排产数量"
                          prop="pendingQuantity">
              <up-input v-model="form.pendingQuantity"
                        disabled
                        placeholder="待排产数量" />
            </up-form-item>
            <up-form-item label="本次排产数量"
                          prop="schedulingNum"
                          required>
              <up-number-box v-model="form.schedulingNum"
                             :min="0"
                             :max="form.pendingQuantity"
                             :step="0.1"
                             :precision="2"
                             @change="handleNumChange" />
            </up-form-item>
          </view>
          <!-- æ´¾å·¥ä¿¡æ¯ -->
          <view class="form-section">
            <text class="section-title">派工信息</text>
            <up-form-item label="派工人"
                          prop="schedulingUserId"
                          required>
              <up-input v-model="selectedUserName"
                        placeholder="请选择派工人"
                        readonly
                        @click="showUserPicker = true"
                        suffixIcon="arrow-down" />
            </up-form-item>
            <up-form-item label="派工日期"
                          prop="schedulingDate"
                          required>
              <up-input v-model="form.schedulingDate"
                        placeholder="请选择派工日期"
                        readonly
                        @click="showDatePicker = true"
                        suffixIcon="calendar" />
            </up-form-item>
          </view>
        </up-form>
      </view>
      <!-- åº•部按钮 -->
      <view class="modal-footer">
        <up-button @click="handleClose"
                   text="取消"
                   type="info"
                   plain
                   :customStyle="{ marginRight: '12px', flex: 1 }" />
        <up-button @click="handleConfirm"
                   text="确认派工"
                   type="primary"
                   :customStyle="{ flex: 1 }"
                   :loading="submitting" />
      </view>
    </view>
    <!-- äººå‘˜é€‰æ‹©å™¨ -->
    <up-picker v-model="showUserPicker"
               :columns="userColumns"
               @confirm="handleUserSelect"
               @cancel="showUserPicker = false" />
    <!-- æ—¥æœŸé€‰æ‹©å™¨ -->
    <up-datetime-picker v-model="showDatePicker"
                        mode="date"
                        @confirm="handleDateSelect"
                        @cancel="showDatePicker = false" />
  </up-popup>
</template>
<script setup>
import { ref, reactive, computed, getCurrentInstance } from 'vue';
import { userListNoPageByTenantId } from "@/api/system/user.js";
import { productionDispatch } from "@/api/productionManagement/productionOrder.js";
import useUserStore from "@/store/modules/user";
import dayjs from "dayjs";
  import { ref, reactive, computed, getCurrentInstance } from "vue";
  import { userListNoPageByTenantId } from "@/api/system/user.js";
  // import { productionDispatch } from "@/api/productionManagement/productionOrder.js";
  import useUserStore from "@/store/modules/user";
  import dayjs from "dayjs";
const { proxy } = getCurrentInstance();
const userStore = useUserStore();
const emit = defineEmits(['confirm']);
  const { proxy } = getCurrentInstance();
  const userStore = useUserStore();
  const emit = defineEmits(["confirm"]);
// å¼¹çª—显示状态
const show = ref(false);
const submitting = ref(false);
  // å¼¹çª—显示状态
  const show = ref(false);
  const submitting = ref(false);
// é€‰æ‹©å™¨æ˜¾ç¤ºçŠ¶æ€
const showUserPicker = ref(false);
const showDatePicker = ref(false);
  // é€‰æ‹©å™¨æ˜¾ç¤ºçŠ¶æ€
  const showUserPicker = ref(false);
  const showDatePicker = ref(false);
// ç”¨æˆ·åˆ—表
const userList = ref([]);
const userColumns = computed(() => [
    userList.value.map(user => ({
        label: user.nickName,
        value: user.userId
    }))
]);
  // ç”¨æˆ·åˆ—表
  const userList = ref([]);
  const userColumns = computed(() => [
    userList.value.map(user => ({
      label: user.nickName,
      value: user.userId,
    })),
  ]);
// é€‰ä¸­çš„用户名称(用于显示)
const selectedUserName = computed(() => {
    const user = userList.value.find(u => u.userId === form.schedulingUserId);
    return user ? user.nickName : '';
});
  // é€‰ä¸­çš„用户名称(用于显示)
  const selectedUserName = computed(() => {
    const user = userList.value.find(u => u.userId === form.schedulingUserId);
    return user ? user.nickName : "";
  });
// è¡¨å•数据
const form = reactive({
    projectName: "",
    productCategory: "",
    quantity: "",
    schedulingNum: 0,
    schedulingUserId: "",
    schedulingDate: "",
    pendingQuantity: 0,
    id: "" // åŽŸå§‹è®°å½•ID
});
  // è¡¨å•数据
  const form = reactive({
    projectName: "",
    productCategory: "",
    quantity: "",
    schedulingNum: 0,
    schedulingUserId: "",
    schedulingDate: "",
    pendingQuantity: 0,
    id: "", // åŽŸå§‹è®°å½•ID
  });
// è¡¨å•验证规则
const rules = reactive({
    schedulingNum: [
        { required: true, message: "请输入排产数量", trigger: "blur" }
    ],
    schedulingUserId: [
        { required: true, message: "请选择派工人", trigger: "change" }
    ],
    schedulingDate: [
        { required: true, message: "请选择派工日期", trigger: "change" }
    ]
});
  // è¡¨å•验证规则
  const rules = reactive({
    schedulingNum: [
      { required: true, message: "请输入排产数量", trigger: "blur" },
    ],
    schedulingUserId: [
      { required: true, message: "请选择派工人", trigger: "change" },
    ],
    schedulingDate: [
      { required: true, message: "请选择派工日期", trigger: "change" },
    ],
  });
// è¡¨å•引用
const formRef = ref();
  // è¡¨å•引用
  const formRef = ref();
// æ‰“开弹窗
const open = async (rowData) => {
    try {
        // åŠ è½½ç”¨æˆ·åˆ—è¡¨
        const res = await userListNoPageByTenantId();
        userList.value = res.data || [];
        // å¡«å……表单数据
        Object.assign(form, {
            ...rowData,
            schedulingNum: 0,
            schedulingUserId: userStore.id,
            schedulingDate: dayjs().format("YYYY-MM-DD")
        });
        show.value = true;
    } catch (error) {
        uni.showToast({
            title: '加载用户列表失败',
            icon: 'error'
        });
    }
};
  // æ‰“开弹窗
  const open = async rowData => {
    try {
      // åŠ è½½ç”¨æˆ·åˆ—è¡¨
      const res = await userListNoPageByTenantId();
      userList.value = res.data || [];
// å¤„理数量变化
const handleNumChange = (value) => {
    if (value > form.pendingQuantity) {
        form.schedulingNum = form.pendingQuantity;
        uni.showToast({
            title: '排产数量不可大于待排产数量',
            icon: 'none'
        });
    }
};
      // å¡«å……表单数据
      Object.assign(form, {
        ...rowData,
        schedulingNum: 0,
        schedulingUserId: userStore.id,
        schedulingDate: dayjs().format("YYYY-MM-DD"),
      });
// å¤„理用户选择
const handleUserSelect = (params) => {
    if (params.value && params.value.length > 0) {
        form.schedulingUserId = params.value[0];
    }
    showUserPicker.value = false;
};
      show.value = true;
    } catch (error) {
      uni.showToast({
        title: "加载用户列表失败",
        icon: "error",
      });
    }
  };
// å¤„理日期选择
const handleDateSelect = (params) => {
    if (params.value) {
        form.schedulingDate = dayjs(params.value).format("YYYY-MM-DD");
    }
    showDatePicker.value = false;
};
  // å¤„理数量变化
  const handleNumChange = value => {
    if (value > form.pendingQuantity) {
      form.schedulingNum = form.pendingQuantity;
      uni.showToast({
        title: "排产数量不可大于待排产数量",
        icon: "none",
      });
    }
  };
// ç¡®è®¤æ´¾å·¥
const handleConfirm = async () => {
    try {
        // è¡¨å•验证
        const valid = await formRef.value?.validate();
        if (!valid) return;
        if (form.schedulingNum <= 0) {
            uni.showToast({
                title: '排产数量必须大于0',
                icon: 'none'
            });
            return;
        }
        submitting.value = true;
        // æäº¤æ´¾å·¥æ•°æ®
        await productionDispatch(form);
        uni.showToast({
            title: '派工成功',
            icon: 'success'
        });
        handleClose();
        emit('confirm');
    } catch (error) {
        uni.showToast({
            title: '派工失败',
            icon: 'error'
        });
    } finally {
        submitting.value = false;
    }
};
  // å¤„理用户选择
  const handleUserSelect = params => {
    if (params.value && params.value.length > 0) {
      form.schedulingUserId = params.value[0];
    }
    showUserPicker.value = false;
  };
// å¼¹çª—打开事件
const handleOpen = () => {
    // å¼¹çª—打开时的处理
};
  // å¤„理日期选择
  const handleDateSelect = params => {
    if (params.value) {
      form.schedulingDate = dayjs(params.value).format("YYYY-MM-DD");
    }
    showDatePicker.value = false;
  };
// å…³é—­å¼¹çª—
const handleClose = () => {
    show.value = false;
    showUserPicker.value = false;
    showDatePicker.value = false;
    // é‡ç½®è¡¨å•
    Object.assign(form, {
        projectName: "",
        productCategory: "",
        quantity: "",
        schedulingNum: 0,
        schedulingUserId: "",
        schedulingDate: "",
        pendingQuantity: 0,
        id: ""
    });
};
  // ç¡®è®¤æ´¾å·¥
  const handleConfirm = async () => {
    try {
      // è¡¨å•验证
      const valid = await formRef.value?.validate();
      if (!valid) return;
// æš´éœ²æ–¹æ³•
defineExpose({
    open
});
      if (form.schedulingNum <= 0) {
        uni.showToast({
          title: "排产数量必须大于0",
          icon: "none",
        });
        return;
      }
      submitting.value = true;
      // æäº¤æ´¾å·¥æ•°æ®
      // await productionDispatch(form);
      uni.showToast({
        title: "派工成功",
        icon: "success",
      });
      handleClose();
      emit("confirm");
    } catch (error) {
      uni.showToast({
        title: "派工失败",
        icon: "error",
      });
    } finally {
      submitting.value = false;
    }
  };
  // å¼¹çª—打开事件
  const handleOpen = () => {
    // å¼¹çª—打开时的处理
  };
  // å…³é—­å¼¹çª—
  const handleClose = () => {
    show.value = false;
    showUserPicker.value = false;
    showDatePicker.value = false;
    // é‡ç½®è¡¨å•
    Object.assign(form, {
      projectName: "",
      productCategory: "",
      quantity: "",
      schedulingNum: 0,
      schedulingUserId: "",
      schedulingDate: "",
      pendingQuantity: 0,
      id: "",
    });
  };
  // æš´éœ²æ–¹æ³•
  defineExpose({
    open,
  });
</script>
<style scoped lang="scss">
.dispatch-modal {
    background: #ffffff;
    border-radius: 20px 20px 0 0;
    max-height: 80vh;
    overflow: hidden;
    display: flex;
    flex-direction: column;
}
  .dispatch-modal {
    background: #ffffff;
    border-radius: 20px 20px 0 0;
    max-height: 80vh;
    overflow: hidden;
    display: flex;
    flex-direction: column;
  }
.modal-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 20px 20px 0 20px;
    border-bottom: 1px solid #f0f0f0;
    padding-bottom: 16px;
    margin-bottom: 20px;
}
  .modal-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 20px 20px 0 20px;
    border-bottom: 1px solid #f0f0f0;
    padding-bottom: 16px;
    margin-bottom: 20px;
  }
.modal-title {
    font-size: 18px;
    font-weight: 600;
    color: #333;
}
  .modal-title {
    font-size: 18px;
    font-weight: 600;
    color: #333;
  }
.close-btn {
    padding: 4px;
    &:active {
        opacity: 0.7;
    }
}
  .close-btn {
    padding: 4px;
.modal-content {
    flex: 1;
    padding: 0 20px;
    overflow-y: auto;
}
    &:active {
      opacity: 0.7;
    }
  }
.form-section {
    margin-bottom: 24px;
}
  .modal-content {
    flex: 1;
    padding: 0 20px;
    overflow-y: auto;
  }
.section-title {
    display: block;
    font-size: 16px;
    font-weight: 600;
    color: #333;
    margin-bottom: 16px;
    padding-left: 8px;
    border-left: 3px solid #2979ff;
}
  .form-section {
    margin-bottom: 24px;
  }
.modal-footer {
    display: flex;
    gap: 12px;
    padding: 20px;
    border-top: 1px solid #f0f0f0;
    background: #fafafa;
}
  .section-title {
    display: block;
    font-size: 16px;
    font-weight: 600;
    color: #333;
    margin-bottom: 16px;
    padding-left: 8px;
    border-left: 3px solid #2979ff;
  }
// uView ç»„件样式调整
:deep(.up-form-item) {
    margin-bottom: 20px;
}
  .modal-footer {
    display: flex;
    gap: 12px;
    padding: 20px;
    border-top: 1px solid #f0f0f0;
    background: #fafafa;
  }
:deep(.up-input) {
    background: #f8f9fa;
    border-radius: 8px;
}
  // uView ç»„件样式调整
  :deep(.up-form-item) {
    margin-bottom: 20px;
  }
:deep(.up-input--disabled) {
    background: #f0f0f0;
    color: #999;
}
  :deep(.up-input) {
    background: #f8f9fa;
    border-radius: 8px;
  }
:deep(.up-number-box) {
    background: #f8f9fa;
    border-radius: 8px;
}
  :deep(.up-input--disabled) {
    background: #f0f0f0;
    color: #999;
  }
  :deep(.up-number-box) {
    background: #f8f9fa;
    border-radius: 8px;
  }
</style>
src/pages/productionManagement/productionDispatching/components/formDia.vue
@@ -1,87 +1,101 @@
<template>
  <div>
    <el-dialog
        v-model="dialogFormVisible"
        title="生产派工"
        width="50%"
        @close="closeDia"
    >
      <el-form :model="form" label-width="140px" label-position="top" :rules="rules" ref="formRef">
    <el-dialog v-model="dialogFormVisible"
               title="生产派工"
               width="50%"
               @close="closeDia">
      <el-form :model="form"
               label-width="140px"
               label-position="top"
               :rules="rules"
               ref="formRef">
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="项目名称:" prop="projectName">
              <el-input v-model="form.projectName" placeholder="请输入" clearable disabled/>
            <el-form-item label="项目名称:"
                          prop="projectName">
              <el-input v-model="form.projectName"
                        placeholder="请输入"
                        clearable
                        disabled />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="产品大类:" prop="productCategory">
              <el-input v-model="form.productCategory" placeholder="请输入" clearable disabled/>
            <el-form-item label="产品大类:"
                          prop="productCategory">
              <el-input v-model="form.productCategory"
                        placeholder="请输入"
                        clearable
                        disabled />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="总数量:" prop="quantity">
              <el-input v-model="form.quantity" placeholder="请输入" clearable disabled/>
            <el-form-item label="总数量:"
                          prop="quantity">
              <el-input v-model="form.quantity"
                        placeholder="请输入"
                        clearable
                        disabled />
            </el-form-item>
          </el-col>
          <el-col :span="12">
                        <el-form-item label="待排产数量:" prop="pendingQuantity">
                            <el-input v-model="form.pendingQuantity" placeholder="请输入" clearable disabled/>
                        </el-form-item>
            <el-form-item label="待排产数量:"
                          prop="pendingQuantity">
              <el-input v-model="form.pendingQuantity"
                        placeholder="请输入"
                        clearable
                        disabled />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
                        <el-form-item label="本次排产数量:" prop="schedulingNum">
                            <el-input-number
                                v-model="form.schedulingNum"
                                placeholder="请输入"
                                :min="0"
                                :step="0.1"
                                :precision="2"
                                clearable
                                @change="changeNum"
                                style="width: 100%"
                            />
                        </el-form-item>
            <el-form-item label="本次排产数量:"
                          prop="schedulingNum">
              <el-input-number v-model="form.schedulingNum"
                               placeholder="请输入"
                               :min="0"
                               :step="0.1"
                               :precision="2"
                               clearable
                               @change="changeNum"
                               style="width: 100%" />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
                    <el-col :span="12">
                        <el-form-item label="派工人:" prop="schedulingUserId">
                            <el-select
                                v-model="form.schedulingUserId"
                                placeholder="选择人员"
                                style="width: 100%;"
                            >
                                <el-option
                                    v-for="user in userList"
                                    :key="user.userId"
                                    :label="user.nickName"
                                    :value="user.userId"
                                />
                            </el-select>
                        </el-form-item>
                    </el-col>
          <el-col :span="12">
            <el-form-item label="派工日期:" prop="schedulingDate">
              <el-date-picker
                  v-model="form.schedulingDate"
                  type="date"
                  placeholder="请选择日期"
                  value-format="YYYY-MM-DD"
                  format="YYYY-MM-DD"
                  clearable
                  style="width: 100%"
              />
            <el-form-item label="派工人:"
                          prop="schedulingUserId">
              <el-select v-model="form.schedulingUserId"
                         placeholder="选择人员"
                         style="width: 100%;">
                <el-option v-for="user in userList"
                           :key="user.userId"
                           :label="user.nickName"
                           :value="user.userId" />
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="派工日期:"
                          prop="schedulingDate">
              <el-date-picker v-model="form.schedulingDate"
                              type="date"
                              placeholder="请选择日期"
                              value-format="YYYY-MM-DD"
                              format="YYYY-MM-DD"
                              clearable
                              style="width: 100%" />
            </el-form-item>
          </el-col>
        </el-row>
      </el-form>
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary" @click="submitForm">确认</el-button>
          <el-button type="primary"
                     @click="submitForm">确认</el-button>
          <el-button @click="closeDia">取消</el-button>
        </div>
      </template>
@@ -90,80 +104,85 @@
</template>
<script setup>
import {ref} from "vue";
import {getStaffJoinInfo, staffJoinAdd, staffJoinUpdate} from "@/api/personnelManagement/onboarding.js";
import {userListNoPageByTenantId} from "@/api/system/user.js";
import {productionDispatch} from "@/api/productionManagement/productionOrder.js";
import useUserStore from "@/store/modules/user";
import dayjs from "dayjs";
const { proxy } = getCurrentInstance()
const emit = defineEmits(['close'])
  import { ref } from "vue";
  import {
    getStaffJoinInfo,
    staffJoinAdd,
    staffJoinUpdate,
  } from "@/api/personnelManagement/onboarding.js";
  import { userListNoPageByTenantId } from "@/api/system/user.js";
  // import {productionDispatch} from "@/api/productionManagement/productionOrder.js";
  import useUserStore from "@/store/modules/user";
  import dayjs from "dayjs";
  const { proxy } = getCurrentInstance();
  const emit = defineEmits(["close"]);
const dialogFormVisible = ref(false);
const operationType = ref('')
const data = reactive({
  form: {
        projectName: "",
        productCategory: "",
        quantity: "",
        schedulingNum: "",
        schedulingUserId: "",
        schedulingDate: "",
        pendingQuantity: "",
  },
  rules: {
        schedulingNum: [{ required: true, message: "请输入", trigger: "blur" },],
        schedulingUserId: [{ required: true, message: "请选择", trigger: "change" },],
        schedulingDate: [{ required: true, message: "请选择", trigger: "change" },],
  },
});
const { form, rules } = toRefs(data);
const userList = ref([])
const userStore = useUserStore()
  const dialogFormVisible = ref(false);
  const operationType = ref("");
  const data = reactive({
    form: {
      projectName: "",
      productCategory: "",
      quantity: "",
      schedulingNum: "",
      schedulingUserId: "",
      schedulingDate: "",
      pendingQuantity: "",
    },
    rules: {
      schedulingNum: [{ required: true, message: "请输入", trigger: "blur" }],
      schedulingUserId: [
        { required: true, message: "请选择", trigger: "change" },
      ],
      schedulingDate: [{ required: true, message: "请选择", trigger: "change" }],
    },
  });
  const { form, rules } = toRefs(data);
  const userList = ref([]);
  const userStore = useUserStore();
// æ‰“开弹框
const openDialog = (type, row) => {
  operationType.value = type;
  dialogFormVisible.value = true;
    userListNoPageByTenantId().then((res) => {
        userList.value = res.data;
    });
    form.value = {...row}
    form.value.schedulingNum = 0
    form.value.schedulingUserId = userStore.id
    form.value.schedulingDate = dayjs().format("YYYY-MM-DD");
}
  // æ‰“开弹框
  const openDialog = (type, row) => {
    operationType.value = type;
    dialogFormVisible.value = true;
    userListNoPageByTenantId().then(res => {
      userList.value = res.data;
    });
    form.value = { ...row };
    form.value.schedulingNum = 0;
    form.value.schedulingUserId = userStore.id;
    form.value.schedulingDate = dayjs().format("YYYY-MM-DD");
  };
//
const changeNum = (value) => {
    if (value > form.value.pendingQuantity) {
        form.value.schedulingNum = form.value.pendingQuantity;
        proxy.$modal.msgWarning('排产数量不可大于待排产数量')
    }
}
// æäº¤äº§å“è¡¨å•
const submitForm = () => {
  proxy.$refs.formRef.validate(valid => {
    if (valid) {
            productionDispatch(form.value).then(res => {
                proxy.$modal.msgSuccess("提交成功");
                closeDia();
            })
  //
  const changeNum = value => {
    if (value > form.value.pendingQuantity) {
      form.value.schedulingNum = form.value.pendingQuantity;
      proxy.$modal.msgWarning("排产数量不可大于待排产数量");
    }
  })
}
  };
  // æäº¤äº§å“è¡¨å•
  const submitForm = () => {
    proxy.$refs.formRef.validate(valid => {
      if (valid) {
        // productionDispatch(form.value).then(res => {
        //     proxy.$modal.msgSuccess("提交成功");
        //     closeDia();
        // })
      }
    });
  };
// å…³é—­å¼¹æ¡†
const closeDia = () => {
  proxy.resetForm("formRef");
  dialogFormVisible.value = false;
  emit('close')
};
defineExpose({
  openDialog,
});
  // å…³é—­å¼¹æ¡†
  const closeDia = () => {
    proxy.resetForm("formRef");
    dialogFormVisible.value = false;
    emit("close");
  };
  defineExpose({
    openDialog,
  });
</script>
<style scoped>
</style>
src/pages/productionManagement/productionDispatching/index.vue
@@ -1,235 +1,236 @@
<template>
    <view class="production-dispatching">
        <!-- ä½¿ç”¨é€šç”¨é¡µé¢å¤´éƒ¨ç»„ä»¶ -->
        <PageHeader title="生产派工" @back="goBack" />
        <!-- æœç´¢åŒºåŸŸ -->
        <view class="search-section">
            <view class="search-bar">
                <view class="search-input">
                    <up-input
                        class="search-text"
                        placeholder="请输入客户名称搜索"
                        v-model="searchForm.customerName"
                        @change="handleQuery"
                        clearable
                    />
                </view>
                <view class="filter-button" @click="handleQuery">
                    <up-icon name="search" size="24" color="#999"></up-icon>
                </view>
            </view>
        </view>
        <!-- ç”Ÿäº§æ´¾å·¥åˆ—表 -->
        <view class="ledger-list" v-if="tableData.length > 0">
            <view v-for="(item, index) in tableData" :key="item.id || index">
                <view class="ledger-item">
                    <view class="item-header">
                        <view class="item-left">
                            <view class="document-icon">
                                <up-icon name="file-text" size="16" color="#ffffff"></up-icon>
                            </view>
                            <text class="item-id">{{ item.salesContractNo }}</text>
                        </view>
                    </view>
                    <up-divider></up-divider>
                    <view class="item-details">
                        <view class="detail-row">
                            <text class="detail-label">录入日期</text>
                            <text class="detail-value">{{ item.entryDate }}</text>
                        </view>
                        <view class="detail-row">
                            <text class="detail-label">客户合同号</text>
                            <text class="detail-value">{{ item.customerContractNo }}</text>
                        </view>
                        <view class="detail-row">
                            <text class="detail-label">客户名称</text>
                            <text class="detail-value">{{ item.customerName }}</text>
                        </view>
                        <view class="detail-row">
                            <text class="detail-label">项目名称</text>
                            <text class="detail-value">{{ item.projectName }}</text>
                        </view>
                        <view class="detail-row">
                            <text class="detail-label">产品大类</text>
                            <text class="detail-value">{{ item.productCategory }}</text>
                        </view>
                        <view class="detail-row">
                            <text class="detail-label">规格型号</text>
                            <text class="detail-value">{{ item.specificationModel }}</text>
                        </view>
                        <view class="detail-row">
                            <text class="detail-label">总数量</text>
                            <text class="detail-value">{{ item.quantity }} {{ item.unit }}</text>
                        </view>
                        <view class="detail-row">
                            <text class="detail-label">排产数量</text>
                            <text class="detail-value highlight">{{ item.schedulingNum }}</text>
                        </view>
                        <view class="detail-row">
                            <text class="detail-label">待排数量</text>
                            <text class="detail-value" :class="{ 'danger': item.pendingQuantity <= 0 }">{{ item.pendingQuantity }}</text>
                        </view>
                    </view>
                    <!-- æ“ä½œæŒ‰é’®åŒºåŸŸ -->
                    <view class="action-buttons">
                        <up-button
                            type="primary"
                            size="small"
                            @click="handleDispatch(item)"
                            class="action-btn"
                            :disabled="item.pendingQuantity <= 0"
                        >
                            ç”Ÿäº§æ´¾å·¥
                        </up-button>
                    </view>
                </view>
            </view>
        </view>
        <view v-else class="no-data">
            <text>暂无生产派工数据</text>
        </view>
        <!-- æ´¾å·¥å¼¹çª— -->
        <DispatchModal ref="dispatchModalRef" @confirm="handleDispatchConfirm" />
    </view>
  <view class="production-dispatching">
    <!-- ä½¿ç”¨é€šç”¨é¡µé¢å¤´éƒ¨ç»„ä»¶ -->
    <PageHeader title="生产派工"
                @back="goBack" />
    <!-- æœç´¢åŒºåŸŸ -->
    <view class="search-section">
      <view class="search-bar">
        <view class="search-input">
          <up-input class="search-text"
                    placeholder="请输入客户名称搜索"
                    v-model="searchForm.customerName"
                    @change="handleQuery"
                    clearable />
        </view>
        <view class="filter-button"
              @click="handleQuery">
          <up-icon name="search"
                   size="24"
                   color="#999"></up-icon>
        </view>
      </view>
    </view>
    <!-- ç”Ÿäº§æ´¾å·¥åˆ—表 -->
    <view class="ledger-list"
          v-if="tableData.length > 0">
      <view v-for="(item, index) in tableData"
            :key="item.id || index">
        <view class="ledger-item">
          <view class="item-header">
            <view class="item-left">
              <view class="document-icon">
                <up-icon name="file-text"
                         size="16"
                         color="#ffffff"></up-icon>
              </view>
              <text class="item-id">{{ item.salesContractNo }}</text>
            </view>
          </view>
          <up-divider></up-divider>
          <view class="item-details">
            <view class="detail-row">
              <text class="detail-label">录入日期</text>
              <text class="detail-value">{{ item.entryDate }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">客户合同号</text>
              <text class="detail-value">{{ item.customerContractNo }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">客户名称</text>
              <text class="detail-value">{{ item.customerName }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">项目名称</text>
              <text class="detail-value">{{ item.projectName }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">产品大类</text>
              <text class="detail-value">{{ item.productCategory }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">规格型号</text>
              <text class="detail-value">{{ item.specificationModel }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">总数量</text>
              <text class="detail-value">{{ item.quantity }} {{ item.unit }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">排产数量</text>
              <text class="detail-value highlight">{{ item.schedulingNum }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">待排数量</text>
              <text class="detail-value"
                    :class="{ 'danger': item.pendingQuantity <= 0 }">{{ item.pendingQuantity }}</text>
            </view>
          </view>
          <!-- æ“ä½œæŒ‰é’®åŒºåŸŸ -->
          <view class="action-buttons">
            <up-button type="primary"
                       size="small"
                       @click="handleDispatch(item)"
                       class="action-btn"
                       :disabled="item.pendingQuantity <= 0">
              ç”Ÿäº§æ´¾å·¥
            </up-button>
          </view>
        </view>
      </view>
    </view>
    <view v-else
          class="no-data">
      <text>暂无生产派工数据</text>
    </view>
    <!-- æ´¾å·¥å¼¹çª— -->
    <DispatchModal ref="dispatchModalRef"
                   @confirm="handleDispatchConfirm" />
  </view>
</template>
<script setup>
import { ref, reactive, toRefs, getCurrentInstance } from "vue";
import { onShow } from '@dcloudio/uni-app';
import dayjs from "dayjs";
import {schedulingListPage} from "@/api/productionManagement/productionOrder.js";
import PageHeader from "@/components/PageHeader.vue";
import DispatchModal from "./components/DispatchModal.vue";
const { proxy } = getCurrentInstance();
  import { ref, reactive, toRefs, getCurrentInstance } from "vue";
  import { onShow } from "@dcloudio/uni-app";
  import dayjs from "dayjs";
  // import {schedulingListPage} from "@/api/productionManagement/productionOrder.js";
  import PageHeader from "@/components/PageHeader.vue";
  import DispatchModal from "./components/DispatchModal.vue";
  const { proxy } = getCurrentInstance();
// åŠ è½½çŠ¶æ€
const loading = ref(false);
  // åŠ è½½çŠ¶æ€
  const loading = ref(false);
// åˆ—表数据
const tableData = ref([]);
  // åˆ—表数据
  const tableData = ref([]);
  // æœç´¢è¡¨å•数据
  const data = reactive({
    searchForm: {
      customerName: "",
    },
  });
  const { searchForm } = toRefs(data);
// æœç´¢è¡¨å•数据
const data = reactive({
    searchForm: {
        customerName: "",
    },
});
const { searchForm } = toRefs(data);
  // åˆ†é¡µé…ç½®
  const page = reactive({
    current: -1,
    size: -1,
  });
// åˆ†é¡µé…ç½®
const page = reactive({
    current: -1,
    size: -1,
});
  // æ´¾å·¥å¼¹çª—引用
  const dispatchModalRef = ref();
// æ´¾å·¥å¼¹çª—引用
const dispatchModalRef = ref();
  // é€šç”¨æç¤ºå‡½æ•°
  const showLoadingToast = message => {
    uni.showLoading({
      title: message,
      mask: true,
    });
  };
// é€šç”¨æç¤ºå‡½æ•°
const showLoadingToast = (message) => {
    uni.showLoading({
        title: message,
        mask: true
    });
};
  const closeToast = () => {
    uni.hideLoading();
  };
const closeToast = () => {
    uni.hideLoading();
};
  // è¿”回上一页
  const goBack = () => {
    uni.navigateBack();
  };
// è¿”回上一页
const goBack = () => {
    uni.navigateBack();
};
  // æŸ¥è¯¢åˆ—表
  const handleQuery = () => {
    getList();
  };
// æŸ¥è¯¢åˆ—表
const handleQuery = () => {
    getList();
};
  // èŽ·å–åˆ—è¡¨æ•°æ®
  const getList = () => {
    loading.value = true;
    showLoadingToast("加载中...");
// èŽ·å–åˆ—è¡¨æ•°æ®
const getList = () => {
    loading.value = true;
    showLoadingToast('加载中...');
    // æž„造请求参数
    const params = { ...searchForm.value, ...page };
    schedulingListPage(params).then((res) => {
        loading.value = false;
        closeToast();
        // å¤„理每条数据,增加pendingQuantity字段
        tableData.value = (res.data.records || []).map(item => ({
            ...item,
            pendingQuantity: (Number(item.quantity) || 0) - (Number(item.schedulingNum) || 0)
        }));
    }).catch(() => {
        loading.value = false;
        closeToast();
        uni.showToast({
            title: '加载失败',
            icon: 'error'
        });
    });
};
    // æž„造请求参数
    const params = { ...searchForm.value, ...page };
// å¤„理派工操作
const handleDispatch = (item) => {
    if (item.pendingQuantity <= 0) {
        uni.showToast({
            title: '该项目无需再派工',
            icon: 'none'
        });
        return;
    }
    dispatchModalRef.value?.open(item);
};
    // schedulingListPage(params).then((res) => {
    //     loading.value = false;
    //     closeToast();
// å¤„理派工确认
const handleDispatchConfirm = () => {
    getList(); // åˆ·æ–°åˆ—表
};
    //     // å¤„理每条数据,增加pendingQuantity字段
    //     tableData.value = (res.data.records || []).map(item => ({
    //         ...item,
    //         pendingQuantity: (Number(item.quantity) || 0) - (Number(item.schedulingNum) || 0)
    //     }));
    // }).catch(() => {
    //     loading.value = false;
    //     closeToast();
    //     uni.showToast({
    //         title: '加载失败',
    //         icon: 'error'
    //     });
    // });
  };
// é¡µé¢æ˜¾ç¤ºæ—¶åŠ è½½æ•°æ®
onShow(() => {
    // åŠ è½½åˆ—è¡¨æ•°æ®
    getList();
});
  // å¤„理派工操作
  const handleDispatch = item => {
    if (item.pendingQuantity <= 0) {
      uni.showToast({
        title: "该项目无需再派工",
        icon: "none",
      });
      return;
    }
    dispatchModalRef.value?.open(item);
  };
  // å¤„理派工确认
  const handleDispatchConfirm = () => {
    getList(); // åˆ·æ–°åˆ—表
  };
  // é¡µé¢æ˜¾ç¤ºæ—¶åŠ è½½æ•°æ®
  onShow(() => {
    // åŠ è½½åˆ—è¡¨æ•°æ®
    getList();
  });
</script>
<style scoped lang="scss">
@import '@/styles/sales-common.scss';
  @import "@/styles/sales-common.scss";
// ç”Ÿäº§æ´¾å·¥é¡µé¢æ ·å¼
.production-dispatching {
    min-height: 100vh;
    background: #f8f9fa;
    position: relative;
}
  // ç”Ÿäº§æ´¾å·¥é¡µé¢æ ·å¼
  .production-dispatching {
    min-height: 100vh;
    background: #f8f9fa;
    position: relative;
  }
// åˆ—表项样式
.ledger-item {
    .detail-value.highlight {
        color: #ff6b35;
        font-weight: 600;
    }
    .detail-value.danger {
        color: #ee0a24;
        font-weight: 600;
    }
}
  // åˆ—表项样式
  .ledger-item {
    .detail-value.highlight {
      color: #ff6b35;
      font-weight: 600;
    }
// é€‚配 uView ç»„件样式
:deep(.up-input) {
    background: transparent;
}
    .detail-value.danger {
      color: #ee0a24;
      font-weight: 600;
    }
  }
  // é€‚配 uView ç»„件样式
  :deep(.up-input) {
    background: transparent;
  }
</style>
src/pages/productionManagement/productionOrder/index.vue
@@ -1,193 +1,546 @@
<template>
    <view class="production-order">
        <!-- ä½¿ç”¨é€šç”¨é¡µé¢å¤´éƒ¨ç»„ä»¶ -->
        <PageHeader title="生产订单" @back="goBack" />
        <!-- æœç´¢åŒºåŸŸ -->
        <view class="search-section">
            <view class="search-bar">
                <view class="search-input">
                    <up-input
                        class="search-text"
                        placeholder="请输入客户名称搜索"
                        v-model="searchForm.customerName"
                        @change="handleQuery"
                        clearable
                    />
                </view>
                <view class="filter-button" @click="handleQuery">
                    <up-icon name="search" size="24" color="#999"></up-icon>
                </view>
            </view>
        </view>
        <!-- ç”Ÿäº§è®¢å•列表 -->
        <view class="ledger-list" v-if="tableData.length > 0">
            <view v-for="(item, index) in tableData" :key="item.id || index">
                <view class="ledger-item">
                    <view class="item-header">
                        <view class="item-left">
                            <view class="document-icon">
                                <up-icon name="file-text" size="16" color="#ffffff"></up-icon>
                            </view>
                            <text class="item-id">{{ item.salesContractNo }}</text>
                        </view>
                    </view>
                    <up-divider></up-divider>
                    <view class="item-details">
                        <view class="detail-row">
                            <text class="detail-label">录入日期</text>
                            <text class="detail-value">{{ item.entryDate }}</text>
                        </view>
                        <view class="detail-row">
                            <text class="detail-label">客户合同号</text>
                            <text class="detail-value">{{ item.customerContractNo }}</text>
                        </view>
                        <view class="detail-row">
                            <text class="detail-label">客户名称</text>
                            <text class="detail-value">{{ item.customerName }}</text>
                        </view>
                        <view class="detail-row">
                            <text class="detail-label">项目名称</text>
                            <text class="detail-value">{{ item.projectName }}</text>
                        </view>
                        <view class="detail-row">
                            <text class="detail-label">产品大类</text>
                            <text class="detail-value">{{ item.productCategory }}</text>
                        </view>
                        <view class="detail-row">
                            <text class="detail-label">规格型号</text>
                            <text class="detail-value">{{ item.specificationModel }}</text>
                        </view>
                        <view class="detail-row">
                            <text class="detail-label">数量</text>
                            <text class="detail-value">{{ item.quantity }} {{ item.unit }}</text>
                        </view>
                        <view class="detail-row">
                            <text class="detail-label">排产数量</text>
                            <text class="detail-value highlight">{{ item.schedulingNum }}</text>
                        </view>
                        <view class="detail-row">
                            <text class="detail-label">完工数量</text>
                            <text class="detail-value highlight">{{ item.successNum }}</text>
                        </view>
                    </view>
                </view>
            </view>
        </view>
        <view v-else class="no-data">
            <text>暂无生产订单数据</text>
        </view>
    </view>
  <view class="production-order">
    <!-- ä½¿ç”¨é€šç”¨é¡µé¢å¤´éƒ¨ç»„ä»¶ -->
    <PageHeader title="生产订单"
                @back="goBack" />
    <!-- æœç´¢åŒºåŸŸ -->
    <view class="search-section">
      <view class="search-bar">
        <view class="search-input">
          <up-input class="search-text"
                    placeholder="请输入订单号或产品名称"
                    v-model="searchForm.keyword"
                    @change="handleQuery"
                    clearable />
        </view>
        <view class="filter-button"
              @click="handleQuery">
          <up-icon name="search"
                   size="24"
                   color="#999"></up-icon>
        </view>
      </view>
    </view>
    <!-- åˆ—表区域 -->
    <scroll-view scroll-y
                 class="list-container"
                 v-if="tableData.length > 0"
                 @scrolltolower="loadMore">
      <view v-for="(item, index) in tableData"
            :key="item.id || index">
        <view class="ledger-item">
          <view class="item-header">
            <view class="item-left">
              <view class="document-icon">
                <up-icon name="file-text"
                         size="16"
                         color="#ffffff"></up-icon>
              </view>
              <text class="item-id">{{ item.npsNo }}</text>
            </view>
            <view class="item-right">
              <up-tag :text="getStatusText(item.status)"
                      :type="getStatusType(item.status)"
                      size="mini" />
            </view>
          </view>
          <up-divider></up-divider>
          <view class="item-details">
            <view class="detail-row">
              <text class="detail-label">产品名称</text>
              <text class="detail-value font-bold">{{ item.productName || '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">规格型号</text>
              <text class="detail-value">{{ item.model || '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">订单数量</text>
              <text class="detail-value">{{ item.quantity || 0 }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">完成进度</text>
              <view class="progress-box">
                <up-line-progress :percentage="toProgressPercentage(item.completionStatus)"
                                  :activeColor="progressColor(item.completionStatus)"
                                  height="10"></up-line-progress>
                <text class="progress-text">{{ item.completeQuantity || 0 }} / {{ item.quantity || 0 }}</text>
              </view>
            </view>
            <!-- å·¥åºç”Ÿäº§è¿›åº¦å±•示 -->
            <view class="detail-row process-row">
              <text class="detail-label">工序进度</text>
              <scroll-view scroll-x
                           class="process-scroll">
                <view class="process-container">
                  <view v-for="(process, pIdx) in item.processRouteStatus"
                        :key="pIdx"
                        class="process-item">
                    <view class="process-node">
                      <view class="node-circle"
                            :class="{ 'is-complete': process.percentage >= 100 }">
                        <text class="node-percentage"
                              :style="{ color: process.percentage >= 100 ? '#52c41a' : (process.percentage >= 70 ? '#f56c6c' : '#3c9cff') }">{{ process.percentage }}%</text>
                      </view>
                      <text class="node-name">{{ process.name }}</text>
                    </view>
                    <view v-if="pIdx < item.processRouteStatus.length - 1"
                          class="node-line"></view>
                  </view>
                  <view v-if="!item.processRouteStatus || !item.processRouteStatus.length"
                        class="no-process">-</view>
                </view>
              </scroll-view>
            </view>
            <view class="detail-row">
              <text class="detail-label">计划完成</text>
              <text class="detail-value">{{ formatDate(item.planCompleteTime) }}</text>
            </view>
          </view>
          <view class="item-footer">
            <view class="action-btns">
              <up-button type="info"
                         size="small"
                         plain
                         text="生产追溯"
                         @click="goTraceability(item)"></up-button>
              <up-button type="info"
                         size="small"
                         plain
                         text="工艺路线"
                         @click="goProcessRoute(item)"></up-button>
              <up-button type="primary"
                         size="small"
                         plain
                         text="来源"
                         @click="goSource(item)"></up-button>
              <up-button type="success"
                         size="small"
                         plain
                         text="领料详情"
                         @click="goPickingDetail(item)"></up-button>
            </view>
          </view>
        </view>
      </view>
      <up-loadmore :status="loadStatus"
                   v-if="tableData.length >= page.size" />
    </scroll-view>
    <view v-else
          class="no-data">
      <up-empty mode="data"
                text="暂无生产订单数据"></up-empty>
    </view>
  </view>
</template>
<script setup>
import { ref, reactive, toRefs, getCurrentInstance } from "vue";
import { onShow } from '@dcloudio/uni-app';
import dayjs from "dayjs";
import {schedulingListPage} from "@/api/productionManagement/productionOrder.js";
import PageHeader from "@/components/PageHeader.vue";
const { proxy } = getCurrentInstance();
  import { ref, reactive, toRefs, getCurrentInstance } from "vue";
  import { onShow } from "@dcloudio/uni-app";
  import dayjs from "dayjs";
  import {
    productOrderListPage,
    getOrderProcessRouteMain,
  } from "@/api/productionManagement/productionOrder.js";
  import { productWorkOrderPage } from "@/api/productionManagement/workOrder.js";
  import PageHeader from "@/components/PageHeader.vue";
// åŠ è½½çŠ¶æ€
const loading = ref(false);
// åˆ—表数据
const tableData = ref([]);
  const { proxy } = getCurrentInstance();
// åˆ†é¡µé…ç½®
const page = reactive({
    current: -1,
    size: -1,
    total: 0,
});
  // åŠ è½½çŠ¶æ€
  const loading = ref(false);
  const loadStatus = ref("loadmore");
  // åˆ—表数据
  const tableData = ref([]);
// æœç´¢è¡¨å•数据
const data = reactive({
    searchForm: {
        customerName: "",
    },
});
const { searchForm } = toRefs(data);
  // åˆ†é¡µé…ç½®
  const page = reactive({
    current: 1,
    size: 10,
    total: 0,
  });
// é€šç”¨æç¤ºå‡½æ•°
const showLoadingToast = (message) => {
    uni.showLoading({
        title: message,
        mask: true
    });
};
  // æœç´¢è¡¨å•数据
  const data = reactive({
    searchForm: {
      keyword: "",
    },
  });
  const { searchForm } = toRefs(data);
const closeToast = () => {
    uni.hideLoading();
};
  // è¿”回上一页
  const goBack = () => {
    uni.navigateBack();
  };
// è¿”回上一页
const goBack = () => {
    uni.navigateBack();
};
  // æ ¼å¼åŒ–日期
  const formatDate = date => {
    return date ? dayjs(date).format("YYYY-MM-DD") : "-";
  };
// æŸ¥è¯¢åˆ—表
const handleQuery = () => {
    page.current = 1;
    tableData.value = []; // é‡ç½®åˆ—表数据
    getList();
};
  // èŽ·å–çŠ¶æ€æ–‡æœ¬
  const getStatusText = status => {
    const statusMap = {
      1: "待开始",
      2: "进行中",
      3: "已完成",
      4: "已取消",
      5: "已结束",
    };
    return statusMap[status] || "未知";
  };
// èŽ·å–åˆ—è¡¨æ•°æ®
const getList = () => {
    loading.value = true;
    showLoadingToast('加载中...');
    // æž„造请求参数
    const params = { ...searchForm.value, ...page };
    schedulingListPage(params).then((res) => {
        loading.value = false;
        closeToast();
        tableData.value = res.data.records || [];
    }).catch(() => {
        loading.value = false;
        closeToast();
        uni.showToast({
            title: '加载失败',
            icon: 'error'
        });
    });
};
  // èŽ·å–çŠ¶æ€ç±»åž‹
  const getStatusType = status => {
    const typeMap = {
      1: "primary",
      2: "warning",
      3: "success",
      4: "info",
      5: "error",
    };
    return typeMap[status] || "info";
  };
// é¡µé¢æ˜¾ç¤ºæ—¶åŠ è½½æ•°æ®
onShow(() => {
    // åŠ è½½åˆ—è¡¨æ•°æ®
    getList();
});
  // å®Œæˆè¿›åº¦ç™¾åˆ†æ¯”
  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 handleQuery = () => {
    page.current = 1;
    tableData.value = [];
    getList();
  };
  // åŠ è½½æ›´å¤š
  const loadMore = () => {
    if (loadStatus.value === "nomore" || loading.value) return;
    page.current++;
    getList();
  };
  // èŽ·å–åˆ—è¡¨æ•°æ®
  const getList = () => {
    loading.value = true;
    loadStatus.value = "loading";
    const params = {
      current: page.current,
      size: page.size,
      npsNo: searchForm.value.keyword,
      productName: searchForm.value.keyword,
    };
    productOrderListPage(params)
      .then(async res => {
        const records = res.data.records || [];
        // ä¸ºæ¯ä¸ªè®¢å•并行查询工序进度
        const processPromises = records.map(async item => {
          if (item.npsNo) {
            try {
              const workOrderRes = await productWorkOrderPage({
                npsNo: item.npsNo,
                size: 100,
              });
              const workOrders = workOrderRes.data.records || [];
              const processRouteStatus = workOrders.map(wo => ({
                name: wo.operationName || "未知工序",
                percentage:
                  Number(wo.completionStatus) > 100
                    ? 100
                    : Number(wo.completionStatus || 0),
              }));
              return { ...item, processRouteStatus };
            } catch (error) {
              console.error(`获取工单 ${item.npsNo} è¿›åº¦å¤±è´¥:`, error);
              return { ...item, processRouteStatus: [] };
            }
          }
          return { ...item, processRouteStatus: [] };
        });
        const updatedRecords = await Promise.all(processPromises);
        loading.value = false;
        if (page.current === 1) {
          tableData.value = updatedRecords;
        } else {
          tableData.value = [...tableData.value, ...updatedRecords];
        }
        if (updatedRecords.length < page.size) {
          loadStatus.value = "nomore";
        } else {
          loadStatus.value = "loadmore";
        }
        page.total = res.data.total || 0;
      })
      .catch(() => {
        loading.value = false;
        loadStatus.value = "loadmore";
        uni.showToast({
          title: "加载失败",
          icon: "error",
        });
      });
  };
  // è·³è½¬å·¥è‰ºè·¯çº¿ (BOM)
  const goProcessRoute = item => {
    getOrderProcessRouteMain(item.id)
      .then(res => {
        const data = res.data || {};
        if (!data.id) {
          uni.showToast({ title: "未找到工艺路线", icon: "none" });
          return;
        }
        uni.navigateTo({
          url: `/pages/productionManagement/processRoute/items?id=${
            data.id
          }&bomId=${data.orderBomId}&processRouteCode=${
            data.processRouteCode || ""
          }&productName=${encodeURIComponent(
            item.productName || ""
          )}&model=${encodeURIComponent(item.model || "")}&orderId=${
            item.id
          }&type=order`,
        });
      })
      .catch(() => {
        uni.showToast({ title: "获取路线失败", icon: "none" });
      });
  };
  // è·³è½¬æ¥æº
  const goSource = item => {
    uni.navigateTo({
      url: `/pages/productionManagement/productionOrder/source?id=${
        item.id
      }&productName=${encodeURIComponent(
        item.productName
      )}&model=${encodeURIComponent(item.model)}&quantity=${item.quantity}`,
    });
  };
  // è·³è½¬é¢†æ–™è¯¦æƒ…
  const goPickingDetail = item => {
    uni.navigateTo({
      url: `/pages/productionManagement/productionOrder/pickingDetail?id=${item.id}&npsNo=${item.npsNo}`,
    });
  };
  // è·³è½¬ç”Ÿäº§è¿½æº¯
  const goTraceability = item => {
    uni.navigateTo({
      url: `/pages/productionManagement/productionTraceability/index?npsNo=${item.npsNo}`,
    });
  };
  // é¡µé¢æ˜¾ç¤ºæ—¶åŠ è½½æ•°æ®
  onShow(() => {
    handleQuery();
  });
</script>
<style scoped lang="scss">
@import '@/styles/sales-common.scss';
  @import "@/styles/sales-common.scss";
// ç”Ÿäº§è®¢å•页面样式
.production-order {
    min-height: 100vh;
    background: #f8f9fa;
    position: relative;
}
  .production-order {
    min-height: 100vh;
    background: #f8f9fa;
    display: flex;
    flex-direction: column;
  }
// é‡å†™éƒ¨åˆ†æ ·å¼ä»¥é€‚配生产订单
.ledger-item {
    .detail-value.highlight {
        color: #ff6b35;
        font-weight: 600;
    }
}
  .list-container {
    flex: 1;
    height: 0;
  }
// é€‚配 uView ç»„件样式
:deep(.up-input) {
    background: transparent;
}
  .ledger-item {
    background: #fff;
    margin: 20rpx;
    padding: 24rpx;
    border-radius: 16rpx;
    box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
:deep(.up-datetime-picker) {
    width: 100%;
}
    .item-header {
      display: flex;
      justify-content: space-between;
      align-items: center;
      padding-bottom: 12rpx;
      .item-left {
        display: flex;
        align-items: center;
        .document-icon {
          width: 44rpx;
          height: 44rpx;
          background: #3c9cff;
          border-radius: 10rpx;
          display: flex;
          justify-content: center;
          align-items: center;
          margin-right: 20rpx;
        }
        .item-id {
          font-size: 30rpx;
          font-weight: bold;
          color: #333;
        }
      }
    }
    .item-details {
      padding: 16rpx 0;
      .detail-row {
        display: flex;
        justify-content: space-between;
        align-items: flex-start;
        margin-bottom: 16rpx;
        .detail-label {
          font-size: 26rpx;
          color: #999;
          min-width: 140rpx;
        }
        .detail-value {
          font-size: 26rpx;
          color: #333;
          text-align: right;
          &.font-bold {
            font-weight: bold;
          }
        }
        .progress-box {
          flex: 1;
          margin-left: 40rpx;
          .progress-text {
            font-size: 22rpx;
            color: #999;
            margin-top: 4rpx;
            display: block;
            text-align: right;
          }
        }
        &.process-row {
          flex-direction: column;
          margin: 20rpx 0;
          .process-scroll {
            width: 100%;
            margin-top: 16rpx;
            .process-container {
              display: flex;
              align-items: flex-start;
              padding: 10rpx 0;
              min-height: 120rpx;
              .process-item {
                display: flex;
                align-items: center;
                .process-node {
                  display: flex;
                  flex-direction: column;
                  align-items: center;
                  width: 100rpx;
                  .node-circle {
                    width: 60rpx;
                    height: 60rpx;
                    border-radius: 50%;
                    border: 2rpx solid #3c9cff;
                    display: flex;
                    align-items: center;
                    justify-content: center;
                    background: #fff;
                    margin-bottom: 8rpx;
                    .node-percentage {
                      font-size: 18rpx;
                      color: #3c9cff;
                      font-weight: bold;
                    }
                    &.is-complete {
                      border-color: #52c41a;
                      background: #f6ffed;
                      .node-percentage {
                        color: #52c41a;
                      }
                    }
                  }
                  .node-name {
                    font-size: 20rpx;
                    color: #666;
                    text-align: center;
                    width: 120rpx;
                    white-space: nowrap;
                    overflow: hidden;
                    text-overflow: ellipsis;
                  }
                }
                .node-line {
                  width: 40rpx;
                  height: 2rpx;
                  background: #e8e8e8;
                  margin: -30rpx 0 0 0;
                }
              }
              .no-process {
                font-size: 24rpx;
                color: #ccc;
              }
            }
          }
        }
      }
    }
    .item-footer {
      padding-top: 20rpx;
      border-top: 1rpx solid #f0f0f0;
      display: flex;
      justify-content: flex-end;
      .action-btns {
        display: flex;
        gap: 20rpx;
      }
    }
  }
  .no-data {
    padding-top: 200rpx;
  }
</style>
src/pages/productionManagement/productionOrder/pickingDetail.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,350 @@
<template>
  <view class="picking-detail">
    <PageHeader title="领料详情"
                @back="goBack" />
    <scroll-view scroll-y
                 class="detail-list"
                 v-if="detailList.length > 0">
      <view v-for="(item, index) in detailList"
            :key="index"
            class="material-card">
        <view class="card-header">
          <text class="material-name">{{ item.materialName || item.productName || '-' }}</text>
          <up-tag :text="item.operationName || '-'"
                  type="info"
                  size="mini"
                  plain />
        </view>
        <view class="card-content">
          <view class="info-grid">
            <view class="info-item">
              <text class="label">规格型号</text>
              <text class="value">{{ item.model || '-' }}</text>
            </view>
            <view class="info-item">
              <text class="label">单位</text>
              <text class="value">{{ item.unit || '-' }}</text>
            </view>
            <view class="info-item">
              <text class="label">需领数量</text>
              <text class="value highlight">{{ item.qtyRequired || item.demandedQuantity || 0 }}</text>
            </view>
            <view class="info-item">
              <text class="label">已领数量</text>
              <text class="value success">{{ item.qtyPicked || item.pickQuantity || 0 }}</text>
            </view>
            <view class="info-item">
              <text class="label">补料数量</text>
              <view class="value link"
                    @click="showSupplementDetail(item)">
                {{ item.qtySupplement || item.feedingQty || 0 }}
              </view>
            </view>
            <view class="info-item">
              <text class="label">退料数量</text>
              <text class="value">{{ item.returnQty || 0 }}</text>
            </view>
          </view>
          <view class="remark-row"
                v-if="item.remark">
            <text class="label">备注:</text>
            <text class="value">{{ item.remark }}</text>
          </view>
        </view>
      </view>
    </scroll-view>
    <view v-else
          class="no-data">
      <up-empty mode="data"
                text="暂无领料详情"></up-empty>
    </view>
    <!-- è¡¥æ–™è®°å½•弹窗 -->
    <up-popup :show="showPopup"
              mode="bottom"
              @close="showPopup = false"
              round="10">
      <view class="popup-content">
        <view class="popup-header">
          <text class="title">补料记录</text>
          <up-icon name="close"
                   size="20"
                   @click="showPopup = false"></up-icon>
        </view>
        <scroll-view scroll-y
                     class="record-list">
          <view v-if="supplementRecords.length > 0">
            <view v-for="(record, rIndex) in supplementRecords"
                  :key="rIndex"
                  class="record-item">
              <view class="record-row">
                <text class="record-label">补料数量:</text>
                <text class="record-value highlight">{{ record.pickQuantity || 0 }}</text>
              </view>
              <view class="record-row">
                <text class="record-label">补料人:</text>
                <text class="record-value">{{ record.supplementUserName || '-' }}</text>
              </view>
              <view class="record-row">
                <text class="record-label">补料日期:</text>
                <text class="record-value">{{ record.supplementTime || '-' }}</text>
              </view>
              <view class="record-row">
                <text class="record-label">补料原因:</text>
                <text class="record-value">{{ record.feedingReason || '-' }}</text>
              </view>
            </view>
          </view>
          <view v-else
                class="no-record">
            <text>暂无补料记录</text>
          </view>
        </scroll-view>
      </view>
    </up-popup>
  </view>
</template>
<script setup>
  import { ref, reactive } from "vue";
  import { onLoad } from "@dcloudio/uni-app";
  import {
    listMaterialPickingDetail,
    listMaterialSupplementRecord,
  } from "@/api/productionManagement/productionOrder.js";
  import PageHeader from "@/components/PageHeader.vue";
  const npsNo = ref("");
  const productionOrderId = ref("");
  const detailList = ref([]);
  const loading = ref(false);
  // å¼¹çª—相关
  const showPopup = ref(false);
  const supplementRecords = ref([]);
  const recordLoading = ref(false);
  const goBack = () => {
    uni.navigateBack();
  };
  const calculatePending = item => {
    const required = Number(item.qtyRequired || item.demandedQuantity || 0);
    const picked = Number(item.qtyPicked || item.pickQuantity || 0);
    return Math.max(0, required - picked);
  };
  onLoad(options => {
    if (options.id) {
      productionOrderId.value = options.id;
      npsNo.value = options.npsNo || "";
      fetchDetail(options.id);
    }
  });
  const fetchDetail = id => {
    loading.value = true;
    listMaterialPickingDetail(id)
      .then(res => {
        detailList.value = res.data?.records || res.data || [];
        loading.value = false;
      })
      .catch(() => {
        loading.value = false;
        uni.showToast({
          title: "获取详情失败",
          icon: "error",
        });
      });
  };
  const showSupplementDetail = item => {
    const qty = Number(item.qtySupplement || item.feedingQty || 0);
    if (qty <= 0) return;
    showPopup.value = true;
    recordLoading.value = true;
    supplementRecords.value = [];
    listMaterialSupplementRecord({
      pickId: item.id,
      productionOrderId: productionOrderId.value,
    })
      .then(res => {
        supplementRecords.value = res.data || [];
        recordLoading.value = false;
      })
      .catch(() => {
        recordLoading.value = false;
        uni.showToast({
          title: "获取补料记录失败",
          icon: "error",
        });
      });
  };
</script>
<style scoped lang="scss">
  .picking-detail {
    min-height: 100vh;
    background: #f8f9fa;
    display: flex;
    flex-direction: column;
  }
  .detail-list {
    flex: 1;
    height: 0;
    padding: 20rpx;
  }
  .material-card {
    background: #fff;
    margin-bottom: 24rpx;
    padding: 24rpx;
    border-radius: 16rpx;
    box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.05);
    .card-header {
      display: flex;
      justify-content: space-between;
      align-items: center;
      padding-bottom: 20rpx;
      border-bottom: 1rpx solid #f5f5f5;
      margin-bottom: 20rpx;
      .material-name {
        font-size: 30rpx;
        font-weight: bold;
        color: #333;
      }
    }
    .info-grid {
      display: grid;
      grid-template-columns: repeat(2, 1fr);
      gap: 20rpx;
      .info-item {
        display: flex;
        flex-direction: column;
        .label {
          font-size: 24rpx;
          color: #999;
          margin-bottom: 4rpx;
        }
        .value {
          font-size: 26rpx;
          color: #333;
          &.highlight {
            color: #f56c6c;
            font-weight: bold;
          }
          &.success {
            color: #67c23a;
            font-weight: bold;
          }
          &.warning {
            color: #e6a23c;
            font-weight: bold;
          }
          &.link {
            color: #3c9cff;
            text-decoration: underline;
          }
        }
      }
    }
    .remark-row {
      margin-top: 20rpx;
      padding-top: 16rpx;
      border-top: 1rpx dashed #eee;
      display: flex;
      .label {
        font-size: 24rpx;
        color: #999;
      }
      .value {
        font-size: 24rpx;
        color: #666;
        flex: 1;
      }
    }
  }
  .no-data {
    padding-top: 200rpx;
  }
  /* å¼¹çª—样式 */
  .popup-content {
    background: #fff;
    padding: 30rpx;
    max-height: 70vh;
    display: flex;
    flex-direction: column;
  }
  .popup-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding-bottom: 30rpx;
    border-bottom: 1rpx solid #eee;
    .title {
      font-size: 32rpx;
      font-weight: bold;
      color: #333;
    }
  }
  .record-list {
    flex: 1;
    height: 0;
    padding-top: 20rpx;
  }
  .record-item {
    padding: 24rpx;
    background: #f9f9f9;
    border-radius: 12rpx;
    margin-bottom: 20rpx;
    .record-row {
      display: flex;
      margin-bottom: 10rpx;
      &:last-child {
        margin-bottom: 0;
      }
      .record-label {
        font-size: 26rpx;
        color: #999;
        width: 140rpx;
      }
      .record-value {
        font-size: 26rpx;
        color: #333;
        flex: 1;
        &.highlight {
          color: #f56c6c;
          font-weight: bold;
        }
      }
    }
  }
  .no-record {
    padding: 100rpx 0;
    text-align: center;
    color: #999;
    font-size: 28rpx;
  }
</style>
src/pages/productionManagement/productionOrder/source.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,166 @@
<template>
    <view class="production-order-source">
        <PageHeader title="来源数据" @back="goBack" />
        <view class="summary-card" v-if="summary">
            <view class="summary-item">
                <text class="label">产品名称</text>
                <up-tag :text="summary.productName || '-'" type="primary" size="mini" />
            </view>
            <view class="summary-item">
                <text class="label">规格型号</text>
                <text class="value">{{ summary.model || '-' }}</text>
            </view>
            <view class="summary-item">
                <text class="label">需求数量</text>
                <text class="value highlight">{{ summary.quantity || 0 }}</text>
            </view>
        </view>
        <scroll-view scroll-y class="source-list" v-if="sourceList.length > 0">
            <view v-for="(item, index) in sourceList" :key="index" class="source-card">
                <view class="card-header">
                    <text class="plan-no">计划号: {{ item.mpsNo || '-' }}</text>
                    <up-tag :text="item.source || '未知'" :type="item.source === '销售' ? 'primary' : 'warning'" size="mini" />
                </view>
                <view class="card-content">
                    <view class="info-row">
                        <text class="info-label">合同号</text>
                        <text class="info-value">{{ item.salesContractNo || '-' }}</text>
                    </view>
                    <view class="info-row">
                        <text class="info-label">客户名称</text>
                        <text class="info-value">{{ item.customerName || '-' }}</text>
                    </view>
                    <view class="info-row">
                        <text class="info-label">项目名称</text>
                        <text class="info-value">{{ item.projectName || '-' }}</text>
                    </view>
                    <view class="info-row">
                        <text class="info-label">需求数量</text>
                        <text class="info-value">{{ item.qtyRequired || 0 }} {{ item.unit || '' }}</text>
                    </view>
                    <view class="info-row">
                        <text class="info-label">需求日期</text>
                        <text class="info-value">{{ formatDate(item.requiredDate) }}</text>
                    </view>
                </view>
            </view>
        </scroll-view>
        <view v-else class="no-data">
            <up-empty mode="data" text="暂无来源数据"></up-empty>
        </view>
    </view>
</template>
<script setup>
import { ref, reactive, onMounted } from "vue";
import { onLoad } from '@dcloudio/uni-app';
import dayjs from "dayjs";
import { getProductOrderSource } from "@/api/productionManagement/productionOrder.js";
import PageHeader from "@/components/PageHeader.vue";
const summary = ref(null);
const sourceList = ref([]);
const loading = ref(false);
const goBack = () => {
    uni.navigateBack();
};
const formatDate = (date) => {
    return date ? dayjs(date).format('YYYY-MM-DD') : '-';
};
onLoad((options) => {
    if (options.id) {
        summary.value = {
            productName: decodeURIComponent(options.productName || ''),
            model: decodeURIComponent(options.model || ''),
            quantity: options.quantity || 0
        };
        fetchSource(options.id);
    }
});
const fetchSource = (id) => {
    loading.value = true;
    getProductOrderSource(id).then(res => {
        sourceList.value = res.data || [];
        loading.value = false;
    }).catch(() => {
        loading.value = false;
        uni.showToast({
            title: '获取来源失败',
            icon: 'error'
        });
    });
};
</script>
<style scoped lang="scss">
.production-order-source {
    min-height: 100vh;
    background: #f8f9fa;
    display: flex;
    flex-direction: column;
}
.summary-card {
    background: #fff;
    margin: 20rpx;
    padding: 24rpx;
    border-radius: 16rpx;
    box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.05);
    .summary-item {
        display: flex;
        justify-content: space-between;
        align-items: center;
        margin-bottom: 12rpx;
        &:last-child { margin-bottom: 0; }
        .label { font-size: 26rpx; color: #999; }
        .value { font-size: 26rpx; color: #333; }
        .highlight { color: #f56c6c; font-weight: bold; }
    }
}
.source-list {
    flex: 1;
    height: 0;
    padding: 0 20rpx;
}
.source-card {
    background: #fff;
    margin-bottom: 20rpx;
    padding: 24rpx;
    border-radius: 16rpx;
    box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.03);
    .card-header {
        display: flex;
        justify-content: space-between;
        align-items: center;
        padding-bottom: 16rpx;
        border-bottom: 1rpx solid #f9f9f9;
        margin-bottom: 16rpx;
        .plan-no { font-size: 28rpx; font-weight: bold; color: #333; }
    }
    .info-row {
        display: flex;
        justify-content: space-between;
        margin-bottom: 12rpx;
        &:last-child { margin-bottom: 0; }
        .info-label { font-size: 24rpx; color: #999; }
        .info-value { font-size: 24rpx; color: #666; }
    }
}
.no-data { padding-top: 100rpx; }
</style>
src/pages/productionManagement/productionReport/index.vue
@@ -18,7 +18,7 @@
                   placeholder="自动填充"
                   disabled />
        </u-form-item>
        <u-form-item label="本次生产数量"
        <u-form-item label="生产合格数量"
                     prop="quantity"
                     required>
          <u-input v-model="form.quantity"
@@ -40,6 +40,67 @@
                   @click="openProducerPicker"
                   suffix-icon="arrow-down" />
        </u-form-item>
        <!-- å·¥æ—¶ -->
        <u-form-item label="工时"
                     v-if="form.type == 0"
                     prop="workHour">
          <u-input v-model="form.workHour"
                   placeholder="请输入工时"
                   type="number" />
          <text class="param-unit">h</text>
        </u-form-item>
      </view>
      <!-- åŠ¨æ€å‚æ•°åŒºåŸŸ -->
      <view class="form-section"
            v-if="params.length > 0">
        <view class="section-title">工序参数</view>
        <u-form-item v-for="param in params"
                     :key="param.id"
                     :label="param.paramName"
                     :label-width="110"
                     :required="param.required === '1'">
          <!-- æ•°å­—类型 -->
          <template v-if="param.paramType == '1'">
            <u-input v-model="form.paramGroups[param.id]"
                     type="number"
                     :placeholder="'请输入' + param.paramName"
                     :key="param.id" />
            <text v-if="param.unit && param.unit != '/'"
                  class="param-unit">{{ param.unit }}</text>
          </template>
          <!-- æ–‡æœ¬ç±»åž‹ -->
          <template v-else-if="param.paramType == '2'">
            <u-input v-model="form.paramGroups[param.id]"
                     :placeholder="'请输入' + param.paramName"
                     :key="param.id" />
            <text v-if="param.unit && param.unit != '/'"
                  class="param-unit">{{ param.unit }}</text>
          </template>
          <!-- é€‰æ‹©ç±»åž‹ -->
          <template v-else-if="param.paramType == '3'">
            <u-input v-model="form.paramGroups[param.id]"
                     readonly
                     :placeholder="'请选择' + param.paramName"
                     @click="openParamSelect(param)"
                     suffix-icon="arrow-down" />
          </template>
          <!-- æ—¥æœŸç±»åž‹ -->
          <template v-else-if="param.paramType == '4'">
            <u-input v-model="form.paramGroups[param.id]"
                     readonly
                     :placeholder="'请选择' + param.paramName"
                     @click="openDateParamPicker(param)"
                     suffix-icon="arrow-down" />
            <text v-if="param.unit && param.unit != '/'"
                  class="param-unit">{{ param.unit }}</text>
          </template>
          <!-- é»˜è®¤æ–‡æœ¬ -->
          <template v-else>
            <u-input v-model="form.paramGroups[param.id]"
                     :placeholder="'请输入' + param.paramName"
                     :key="param.id" />
          </template>
        </u-form-item>
      </view>
      <!-- ä½¿ç”¨FooterButtons组件 -->
      <FooterButtons @cancel="goBack"
@@ -54,6 +115,18 @@
                     title="选择生产人"
                     @select="onProducerConfirm"
                     @close="showProducerPicker = false" />
    <!-- å‚数选择器 -->
    <up-action-sheet :show="showParamSelect"
                     :actions="paramOptions"
                     :title="currentParam?.paramName || '选择'"
                     @select="onParamConfirm"
                     @close="showParamSelect = false" />
    <!-- æ—¥æœŸé€‰æ‹©å™¨ -->
    <up-datetime-picker :show="showDatePicker"
                        v-model="datePickerValue"
                        :mode="datePickerMode"
                        @confirm="onDateConfirm"
                        @cancel="showDatePicker = false" />
  </view>
</template>
@@ -71,34 +144,50 @@
  import { addProductMain } from "@/api/productionManagement/productionReporting";
  import { getInfo } from "@/api/login";
  import { userListNoPageByTenantId } from "@/api/system/user";
  import {
    findProcessParamListOrder,
    listMaterialPickingDetail,
  } from "@/api/productionManagement/productionOrder.js";
  import { getDicts } from "@/api/system/dict/data";
  import { formatDateToYMD, parseTime } from "@/utils/ruoyi";
  // è¡¨å•引用
  const formRef = ref();
  // è¡¨å•数据
  let form = ref({
    planQuantity: "",
    quantity: "",
    scrapQty: "",
    userName: "",
    workOrderId: "",
    productProcessRouteItemId: "",
    userId: "",
    schedulingUserId: "",
    reportWork: "",
    productionOrderRoutingOperationId: "",
    productionOrderId: "",
    workHour: 0,
    type: null,
    paramGroups: {},
  });
  // ç”Ÿäº§äººé€‰æ‹©å™¨çŠ¶æ€
  const showProducerPicker = ref(false);
  const producerList = ref([]);
  // æ‰“开生产人选择器
  const params = ref([]);
  const dictOptions = ref({});
  const showParamSelect = ref(false);
  const currentParam = ref(null);
  const paramOptions = ref([]);
  const showDatePicker = ref(false);
  const datePickerValue = ref(Date.now());
  const datePickerMode = ref("date");
  const currentDateParam = ref(null);
  const openProducerPicker = async () => {
    if (producerList.value.length === 0) {
      // å¦‚果列表为空,先加载用户列表
      try {
        const res = await userListNoPageByTenantId();
        const users = res.data || [];
        // è½¬æ¢ä¸º action-sheet éœ€è¦çš„æ ¼å¼
        producerList.value = users.map(user => ({
          name: user.nickName || user.userName,
          value: user.userId,
@@ -112,102 +201,265 @@
    showProducerPicker.value = true;
  };
  // ç”Ÿäº§äººé€‰æ‹©ç¡®è®¤
  const onProducerConfirm = e => {
    form.value.schedulingUserId = e.value;
    form.value.userName = e.name;
    form.value.userId = e.value; // åŒæ—¶æ›´æ–° userId
    form.value.userId = e.value;
    showProducerPicker.value = false;
  };
  // æäº¤çŠ¶æ€
  const openParamSelect = async param => {
    currentParam.value = param;
    if (param.paramType == "3" && param.paramFormat) {
      const options = await getDictOptions(param.paramFormat);
      paramOptions.value = options.map(opt => ({
        name: opt.dictLabel,
        value: opt.dictLabel,
      }));
    }
    showParamSelect.value = true;
  };
  const onParamConfirm = e => {
    if (currentParam.value) {
      form.value.paramGroups[currentParam.value.id] = e.value;
    }
    showParamSelect.value = false;
  };
  const openDateParamPicker = param => {
    currentDateParam.value = param;
    const currentValue = form.value.paramGroups[param.id];
    datePickerValue.value = currentValue
      ? new Date(currentValue).getTime()
      : Date.now();
    // å‚ç…§ PC ç«¯é€»è¾‘:如果格式是 yyyy-MM-dd åˆ™ä¸º date æ¨¡å¼ï¼Œå¦åˆ™ä¸º datetime æ¨¡å¼
    datePickerMode.value =
      param.paramFormat === "yyyy-MM-dd" ? "date" : "datetime";
    showDatePicker.value = true;
  };
  const onDateConfirm = e => {
    if (currentDateParam.value) {
      const format =
        currentDateParam.value.paramFormat === "yyyy-MM-dd"
          ? "{y}-{m}-{d}"
          : "{y}-{m}-{d} {h}:{i}:{s}";
      form.value.paramGroups[currentDateParam.value.id] = parseTime(
        e.value,
        format
      );
    }
    showDatePicker.value = false;
  };
  const getDictOptions = async dictType => {
    if (!dictType) return [];
    if (dictOptions.value[dictType]) return dictOptions.value[dictType];
    try {
      const res = await getDicts(dictType);
      if (res.code === 200) {
        dictOptions.value[dictType] = res.data;
        return res.data;
      }
      return [];
    } catch (error) {
      console.error("获取字典数据失败:", error);
      return [];
    }
  };
  const loadParams = (productionOrderRoutingOperationId, productionOrderId) => {
    findProcessParamListOrder({
      productionOrderRoutingOperationId,
      productionOrderId,
    })
      .then(res => {
        if (res.code === 200) {
          console.log(res.data, "res.data========");
          const paramList = res.data || [];
          params.value = paramList;
          form.value.paramGroups = {};
          paramList.forEach(param => {
            if (!form.value.paramGroups[param.id]) {
              form.value.paramGroups[param.id] = "";
            }
            if (param.paramType == "3" && param.paramFormat) {
              getDictOptions(param.paramFormat);
            }
          });
        }
      })
      .catch(err => {
        console.error("获取工序参数失败:", err);
      });
  };
  const submitting = ref(false);
  // è¿”回上一页
  const goBack = () => {
    uni.navigateBack();
  };
  // æäº¤è¡¨å•
  const submitForm = async () => {
    submitting.value = true;
    // æ ¡éªŒè¡¨å•
    if (!form.value.quantity) {
      submitting.value = false;
      showToast("请输入本次生产数量");
      showToast("请输入生产合格数量");
      return;
    }
    if (!form.value.schedulingUserId) {
      submitting.value = false;
      showToast("请选择生产人");
      return;
    }
    // è½¬æ¢ä¸ºæ•°å­—进行比较
    const quantity = Number(form.value.quantity) || 0;
    const scrapQty = Number(form.value.scrapQty) || 0;
    const planQuantity = Number(form.value.planQuantity);
    // éªŒè¯ç”Ÿäº§æ•°é‡å’ŒæŠ¥åºŸæ•°é‡çš„和不能超过待生产数量
    if (quantity + scrapQty > planQuantity) {
    if (quantity < 0) {
      submitting.value = false;
      showToast("生产数量和报废数量的和不能超过待生产数量");
      showToast("生产合格数量必须大于等于0");
      return;
    }
    if (quantity > planQuantity) {
    // if (quantity + scrapQty > planQuantity) {
    //   submitting.value = false;
    //   showToast("生产数量和报废数量的和不能超过待生产数量");
    //   return;
    // }
    if (scrapQty < 0) {
      submitting.value = false;
      showToast("本次生产数量不能大于待生产数量");
      showToast("报废数量不能小于0");
      return;
    }
    // å‡†å¤‡æäº¤æ•°æ®ï¼Œç¡®ä¿æ•°é‡å­—段为数字类型
    // if (scrapQty > quantity) {
    //   submitting.value = false;
    //   showToast("报废数量不能大于本次生产数量");
    //   return;
    // }
    const productionOperationParamList = params.value.map(param => ({
      ...param,
      inputValue: form.value.paramGroups[param.id] ?? "",
    }));
    const submitData = {
      ...form.value,
      quantity: Number(form.value.quantity),
      scrapQty: Number(form.value.scrapQty) || 0,
      planQuantity: Number(form.value.planQuantity) || 0,
      quantity: quantity,
      scrapQty: scrapQty,
      userId: form.value.userId,
      userName: form.value.userName,
      productionOperationTaskId: form.value.workOrderId,
      reportWork: form.value.reportWork,
      productionOrderRoutingOperationId:
        form.value.productionOrderRoutingOperationId,
      productionOrderId: form.value.productionOrderId,
      workHour: form.value.workHour,
      productionOperationParamList: productionOperationParamList,
    };
    console.log(submitData, "submitData");
    addProductMain(submitData).then(res => {
      if (res.code === 200) {
        showToast("报工成功");
    addProductMain(submitData)
      .then(res => {
        if (res.code === 200) {
          showToast("报工成功");
          submitting.value = false;
          setTimeout(() => {
            goBack();
          }, 1000);
        } else {
          showToast(res.msg || "报工失败");
          submitting.value = false;
        }
      })
      .catch(() => {
        showToast("报工失败");
        submitting.value = false;
        setTimeout(() => {
          goBack();
        }, 1000);
      } else {
        showToast(res.msg || "报工失败");
        submitting.value = false;
      }
    });
      });
  };
  // é¡µé¢åŠ è½½æ—¶åˆå§‹åŒ–æ•°æ®
  onLoad(options => {
  onLoad(async options => {
    console.log(options, "options");
    // å¦‚果没有 orderRow å‚数,说明是从首页直接跳转,需要用户手动选择订单
    if (!options.orderRow) {
      console.log("从首页跳转,无订单数据");
      getInfo().then(res => {
        // é»˜è®¤ä½¿ç”¨å½“前登录用户
        form.value.userId = res.user.userId;
        form.value.userName = res.user.userName;
        form.value.userName = res.user.nickName || res.user.userName;
        form.value.schedulingUserId = res.user.userId;
      });
      return;
    }
    try {
      const orderRow = JSON.parse(options.orderRow);
      const orderRow = JSON.parse(decodeURIComponent(options.orderRow));
      console.log("构造的orderRow:", orderRow);
      console.log(orderRow, "orderRow======########");
      // ç¡®ä¿ planQuantity è½¬æ¢ä¸ºå­—符串,以便在 u-input ä¸­æ­£ç¡®æ˜¾ç¤º
      form.value.planQuantity = orderRow.planQuantity != null ? String(orderRow.planQuantity) : "";
      form.value.productProcessRouteItemId = orderRow.productProcessRouteItemId || "";
      // å‚ç…§ PC ç«¯é€»è¾‘:未领料无法报工
      if (orderRow.productionOrderId) {
        try {
          const res = await listMaterialPickingDetail(orderRow.productionOrderId);
          const records = Array.isArray(res.data)
            ? res.data
            : res.data?.records || [];
          if (res.code === 200 && records.length === 0) {
            uni.showModal({
              title: "提示",
              content: "未领料无法报工",
              showCancel: false,
              success: () => {
                goBack();
              },
            });
            return;
          }
        } catch (error) {
          console.error("查询领料详情失败:", error);
        }
      }
      const planQuantity = Number(orderRow.planQuantity || 0);
      const completeQuantity = Number(orderRow.completeQuantity || 0);
      form.value.planQuantity = String(
        Math.max(0, planQuantity - completeQuantity)
      );
      form.value.workOrderId = orderRow.id || "";
      form.value.reportWork = orderRow.reportWork || "";
      form.value.productionOrderRoutingOperationId =
        orderRow.productionOrderRoutingOperationId || "";
      form.value.productionOrderId = orderRow.productionOrderId || "";
      form.value.type = orderRow.type;
      if (orderRow.type == 0) {
        form.value.workHour = orderRow.workHour || 0;
      } else {
        form.value.workHour = 0;
      }
      getInfo().then(res => {
        // é»˜è®¤ä½¿ç”¨å½“前登录用户,但允许用户修改
        form.value.userId = res.user.userId;
        form.value.userName = res.user.userName;
        form.value.userName = res.user.nickName || res.user.userName;
        form.value.schedulingUserId = res.user.userId;
      });
      // ä½¿ç”¨ nextTick ç¡®ä¿ DOM æ›´æ–°
      console.log(orderRow, "orderRow=====");
      if (
        orderRow.productionOrderRoutingOperationId &&
        orderRow.productionOrderId
      ) {
        nextTick(() => {
          loadParams(
            orderRow.productionOrderRoutingOperationId,
            orderRow.productionOrderId
          );
        });
      }
      nextTick(() => {
        console.log("form.value after assignment:", form.value);
      });
@@ -215,13 +467,30 @@
      console.error("订单解析失败:", error);
      showToast("订单解析失败");
      goBack();
      return;
    }
  });
</script>
<style scoped lang="scss">
  @import "@/static/scss/form-common.scss";
  .form-section {
    background: #fff;
    margin-bottom: 12px;
    padding: 0 16px;
  }
  .section-title {
    font-size: 28rpx;
    font-weight: bold;
    color: #303133;
    padding: 24rpx 0 16rpx;
    border-bottom: 1px solid #f0f0f0;
  }
  .param-unit {
    margin-left: 8rpx;
    color: #909399;
    font-size: 24rpx;
  }
</style>
src/pages/productionManagement/productionReporting/ledger.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,424 @@
<template>
  <view class="reporting-ledger">
    <PageHeader title="报工台账"
                @back="goBack" />
    <!-- æœç´¢åŒºåŸŸ - å‚考采购台账样式 -->
    <view class="search-section">
      <view class="search-bar">
        <view class="search-input">
          <up-input class="search-text"
                    placeholder="请输入工单号搜索"
                    v-model="searchForm.keyword"
                    @change="handleQuery"
                    clearable />
        </view>
        <view class="filter-button"
              @click="handleQuery">
          <up-icon name="search"
                   size="24"
                   color="#999"></up-icon>
        </view>
      </view>
    </view>
    <!-- åˆ—表区域 -->
    <scroll-view scroll-y
                 class="list-container"
                 v-if="tableData.length > 0"
                 @scrolltolower="loadMore">
      <view class="ledger-list">
        <view v-for="(item, index) in tableData"
              :key="item.id || index">
          <view class="ledger-item">
            <view class="item-header">
              <view class="item-left">
                <view class="document-icon">
                  <up-icon name="file-text"
                           size="16"
                           color="#ffffff"></up-icon>
                </view>
                <text class="item-id">{{ item.productNo || '-' }}</text>
              </view>
              <view class="item-tag">
                <text class="create-time">{{ formatDate(item.createTime) }}</text>
              </view>
            </view>
            <up-divider></up-divider>
            <view class="item-details">
              <view class="detail-row">
                <text class="detail-label">报工人员</text>
                <text class="detail-value highlight">{{ item.nickName || '-' }}</text>
              </view>
              <view class="detail-row">
                <text class="detail-label">工时(h)</text>
                <text class="detail-value">{{ item.workHour || 0 }}</text>
              </view>
              <view class="detail-row">
                <text class="detail-label">所属工序</text>
                <view class="detail-value">
                  <up-tag :text="item.process || '-'"
                          type="primary"
                          size="mini"
                          plain />
                </view>
              </view>
              <view class="detail-row">
                <text class="detail-label">工单编号</text>
                <text class="detail-value">{{ item.workOrderNo || '-' }}</text>
              </view>
              <view class="detail-row">
                <text class="detail-label">销售合同号</text>
                <text class="detail-value">{{ item.salesContractNo || '-' }}</text>
              </view>
              <view class="detail-row">
                <text class="detail-label">产品名称</text>
                <text class="detail-value font-bold">{{ item.productName || '-' }}</text>
              </view>
              <view class="detail-row">
                <text class="detail-label">规格型号</text>
                <text class="detail-value">{{ item.productModelName || '-' }}</text>
              </view>
              <view class="quantity-section">
                <view class="qty-item">
                  <text class="qty-label">产出数量</text>
                  <text class="qty-value success">{{ item.quantity || 0 }} {{ item.unit || '' }}</text>
                </view>
                <view class="qty-item">
                  <text class="qty-label">报废数量</text>
                  <text class="qty-value error">{{ item.scrapQty || 0 }}</text>
                </view>
              </view>
              <view class="item-footer">
                <view class="action-buttons">
                  <up-button type="primary"
                             size="small"
                             plain
                             text="查看投入"
                             @click="handleShowInput(item)"></up-button>
                  <up-button type="info"
                             size="small"
                             plain
                             text="参数详情"
                             @click="handleShowParams(item)"></up-button>
                  <!-- <up-button type="error"
                             size="small"
                             plain
                             text="删除"
                             @click="handleDelete(item)"></up-button> -->
                </view>
              </view>
            </view>
          </view>
        </view>
      </view>
      <up-loadmore :status="loadStatus"
                   v-if="tableData.length >= page.size" />
    </scroll-view>
    <view v-else
          class="empty-state">
      <up-empty mode="data"
                text="暂无报工台账数据"></up-empty>
    </view>
    <!-- æŠ•入详情弹窗 -->
    <up-modal :show="inputVisible"
              title="投入详情"
              @confirm="inputVisible = false">
      <view class="modal-content scroll-view">
        <view v-if="inputList.length > 0">
          <view v-for="(input, idx) in inputList"
                :key="idx"
                class="detail-item">
            <view class="detail-row">
              <text class="detail-label">报工单号</text>
              <text class="detail-value">{{ input.productNo || '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">投入产品名称</text>
              <text class="detail-value font-bold">{{ input.productName || '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">投入产品型号</text>
              <text class="detail-value">{{ input.model || '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">投入数量</text>
              <text class="detail-value highlight">{{ input.quantity || 0 }} {{ input.unit || '' }}</text>
            </view>
            <up-divider></up-divider>
          </view>
        </view>
        <up-empty v-else
                  mode="data"
                  text="暂无投入数据" />
      </view>
    </up-modal>
    <!-- å‚数详情弹窗 -->
    <up-modal :show="paramsVisible"
              title="参数详情"
              @confirm="paramsVisible = false">
      <view class="modal-content">
        <view v-if="currentParams.length > 0">
          <view v-for="(param, idx) in currentParams"
                :key="idx"
                class="detail-row">
            <text class="detail-label">{{ param.paramName }}</text>
            <text class="detail-value">{{ param.inputValue }} {{ param.unit && param.unit !== '/' ? '(' + param.unit + ')' : '' }}</text>
          </view>
        </view>
        <up-empty v-else
                  mode="data"
                  text="暂无参数数据" />
      </view>
    </up-modal>
  </view>
</template>
<script setup>
  import { ref, reactive, onMounted } from "vue";
  import dayjs from "dayjs";
  import {
    productionProductMainListPage,
    productionReportDelete,
    productionProductInputListPage,
  } from "@/api/productionManagement/productionProductMain.js";
  import PageHeader from "@/components/PageHeader.vue";
  import modal from "@/plugins/modal";
  const tableData = ref([]);
  const loading = ref(false);
  const loadStatus = ref("loadmore");
  const page = reactive({
    current: 1,
    size: 10,
    total: 0,
  });
  const searchForm = reactive({
    keyword: "",
  });
  // æŠ•入详情相关
  const inputVisible = ref(false);
  const inputList = ref([]);
  // å‚数详情相关
  const paramsVisible = ref(false);
  const currentParams = ref([]);
  const goBack = () => {
    uni.navigateBack();
  };
  const formatDate = date => {
    return date ? dayjs(date).format("YYYY-MM-DD HH:mm") : "-";
  };
  const handleQuery = () => {
    page.current = 1;
    tableData.value = [];
    getList();
  };
  const loadMore = () => {
    if (loadStatus.value === "nomore" || loading.value) return;
    page.current++;
    getList();
  };
  const getList = () => {
    loading.value = true;
    loadStatus.value = "loading";
    const params = {
      current: page.current,
      size: page.size,
      workOrderNo: searchForm.keyword,
    };
    productionProductMainListPage(params)
      .then(res => {
        loading.value = false;
        const records = res.data.records || [];
        if (page.current === 1) {
          tableData.value = records;
        } else {
          tableData.value = [...tableData.value, ...records];
        }
        if (records.length < page.size) {
          loadStatus.value = "nomore";
        } else {
          loadStatus.value = "loadmore";
        }
        page.total = res.data.total || 0;
      })
      .catch(() => {
        loading.value = false;
        loadStatus.value = "loadmore";
        modal.msgError("加载失败");
      });
  };
  const handleShowInput = item => {
    modal.loading("加载中...");
    productionProductInputListPage({
      productMainId: item.id,
      current: 1,
      size: 100,
    })
      .then(res => {
        modal.closeLoading();
        inputList.value = res.data.records || [];
        inputVisible.value = true;
      })
      .catch(() => {
        modal.closeLoading();
        modal.msgError("加载投入数据失败");
      });
  };
  const handleShowParams = item => {
    currentParams.value = item.productionOperationParamList || [];
    paramsVisible.value = true;
  };
  const handleDelete = item => {
    uni.showModal({
      title: "提示",
      content: "确定要删除该报工记录吗?",
      success: res => {
        if (res.confirm) {
          productionReportDelete({ id: item.id }).then(res => {
            if (res.code === 200) {
              modal.msgSuccess("删除成功");
              handleQuery();
            } else {
              modal.msgError(res.msg || "删除失败");
            }
          });
        }
      },
    });
  };
  onMounted(() => {
    getList();
  });
</script>
<style scoped lang="scss">
  @import "@/styles/procurement-common.scss";
  .reporting-ledger {
    min-height: 100vh;
    background-color: #f8f9fa;
    display: flex;
    flex-direction: column;
  }
  .list-container {
    flex: 1;
    height: 0;
  }
  .ledger-item {
    .item-header {
      .item-tag {
        .create-time {
          font-size: 24rpx;
          color: #999;
        }
      }
    }
    .item-details {
      .quantity-section {
        display: flex;
        background-color: #f9f9f9;
        border-radius: 8rpx;
        padding: 20rpx;
        margin: 20rpx 0;
        .qty-item {
          flex: 1;
          display: flex;
          flex-direction: column;
          align-items: center;
          &:first-child {
            border-right: 1rpx solid #eee;
          }
          .qty-label {
            font-size: 24rpx;
            color: #999;
            margin-bottom: 8rpx;
          }
          .qty-value {
            font-size: 32rpx;
            font-weight: bold;
            &.success {
              color: #52c41a;
            }
            &.error {
              color: #f56c6c;
            }
          }
        }
      }
      .item-footer {
        padding-top: 20rpx;
        border-top: 1rpx solid #f0f0f0;
        .action-buttons {
          display: flex;
          justify-content: flex-end;
          gap: 20rpx;
        }
      }
    }
  }
  .modal-content {
    width: 100%;
    padding: 20rpx 0;
    max-height: 60vh;
    overflow-y: auto;
    .detail-item {
      padding-bottom: 20rpx;
    }
    .detail-row {
      display: flex;
      justify-content: space-between;
      margin-bottom: 16rpx;
      .detail-label {
        font-size: 26rpx;
        color: #999;
      }
      .detail-value {
        font-size: 26rpx;
        color: #333;
        &.font-bold {
          font-weight: bold;
        }
        &.highlight {
          color: #3c9cff;
        }
      }
    }
  }
  .empty-state {
    padding-top: 200rpx;
  }
</style>
src/pages/productionManagement/productionScheduling/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,241 @@
<template>
  <view class="production-scheduling">
    <!-- é€šç”¨é¡µé¢å¤´éƒ¨ -->
    <PageHeader title="生产排产"
                @back="goBack" />
    <!-- æœç´¢åŒºåŸŸ -->
    <view class="search-section">
      <view class="search-bar">
        <view class="search-input">
          <up-input class="search-text"
                    placeholder="请输入工单编号"
                    v-model="searchForm.workOrderNo"
                    @confirm="handleQuery"
                    clearable />
        </view>
        <view class="filter-button"
              @click="handleQuery">
          <up-icon name="search"
                   size="24"
                   color="#999"></up-icon>
        </view>
      </view>
    </view>
    <!-- åˆ—表 -->
    <scroll-view scroll-y
                 class="ledger-list"
                 v-if="tableData.length > 0"
                 @scrolltolower="loadMore">
      <view v-for="(item, index) in tableData"
            :key="item.id || index"
            class="ledger-item">
        <view class="item-header">
          <view class="item-left">
            <view class="document-icon">
              <up-icon name="file-text"
                       size="16"
                       color="#ffffff"></up-icon>
            </view>
            <text class="item-id">{{ item.workOrderNo }}</text>
          </view>
          <view class="item-right">
            <up-tag :text="item.workOrderType"
                    size="mini"
                    type="primary"
                    plain></up-tag>
          </view>
        </view>
        <up-divider></up-divider>
        <view class="item-details">
          <view class="detail-row">
            <text class="detail-label">生产订单号</text>
            <text class="detail-value">{{ item.npsNo || '-' }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">产品名称</text>
            <text class="detail-value">{{ item.productName || '-' }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">规格型号</text>
            <text class="detail-value">{{ item.model || '-' }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">单位</text>
            <text class="detail-value">{{ item.unit || '-' }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">工序名称</text>
            <text class="detail-value">{{ item.operationName || '-' }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">需求数量</text>
            <text class="detail-value">{{ item.planQuantity || 0 }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">完成数量</text>
            <text class="detail-value">{{ item.completeQuantity || 0 }}</text>
          </view>
          <view class="progress-section">
            <text class="detail-label">完成进度</text>
            <view class="progress-bar">
              <up-line-progress :percentage="toProgressPercentage(item.completionStatus)"
                                :activeColor="progressColor(item.completionStatus)"
                                :showText="true"></up-line-progress>
            </view>
          </view>
          <view class="detail-row">
            <text class="detail-label">计划开始</text>
            <text class="detail-value">{{ item.planStartTime || '-' }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">计划结束</text>
            <text class="detail-value">{{ item.planEndTime || '-' }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">实际开始</text>
            <text class="detail-value">{{ item.actualStartTime || '-' }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">实际结束</text>
            <text class="detail-value">{{ item.actualEndTime || '-' }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">指定报工人</text>
            <view class="detail-value tags-box">
              <template v-if="item.userNames">
                <up-tag v-for="(name, idx) in item.userNames.split(',')"
                        :key="idx"
                        :text="name"
                        size="mini"
                        type="info"
                        plain
                        class="user-tag"></up-tag>
              </template>
              <text v-else>-</text>
            </view>
          </view>
        </view>
      </view>
      <up-loadmore :status="loadStatus" />
    </scroll-view>
    <view v-else-if="!loading"
          class="no-data">
      <up-empty mode="data"
                text="暂无数据"></up-empty>
    </view>
  </view>
</template>
<script setup>
  import { ref, reactive, toRefs, getCurrentInstance } from "vue";
  import { onShow } from "@dcloudio/uni-app";
  import { productWorkOrderPage } from "@/api/productionManagement/workOrder.js";
  import PageHeader from "@/components/PageHeader.vue";
  const { proxy } = getCurrentInstance();
  const loading = ref(false);
  const tableData = ref([]);
  const loadStatus = ref("loadmore");
  const page = reactive({
    current: 1,
    size: 10,
    total: 0,
  });
  const data = reactive({
    searchForm: {
      workOrderNo: "",
    },
  });
  const { searchForm } = toRefs(data);
  const goBack = () => {
    uni.navigateBack();
  };
  const handleQuery = () => {
    page.current = 1;
    tableData.value = [];
    getList();
  };
  const getList = () => {
    if (loading.value) return;
    loading.value = true;
    const params = {
      ...searchForm.value,
      ...page,
    };
    productWorkOrderPage(params)
      .then(res => {
        loading.value = false;
        const records = res.data.records || [];
        tableData.value =
          page.current === 1 ? records : [...tableData.value, ...records];
        page.total = res.data.total;
        if (tableData.value.length >= page.total) {
          loadStatus.value = "nomore";
        } else {
          loadStatus.value = "loadmore";
        }
      })
      .catch(() => {
        loading.value = false;
        uni.showToast({ title: "加载失败", icon: "error" });
      });
  };
  const loadMore = () => {
    if (loadStatus.value === "nomore" || loading.value) return;
    page.current++;
    getList();
  };
  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";
  };
  onShow(() => {
    handleQuery();
  });
</script>
<style scoped lang="scss">
  @import "@/styles/sales-common.scss";
  .production-scheduling {
    padding-bottom: 20rpx;
  }
  .progress-bar {
    margin-top: 20rpx;
    margin-bottom: 20rpx;
  }
  .tags-box {
    display: flex;
    flex-wrap: wrap;
    gap: 8rpx;
    justify-content: flex-end;
  }
  .user-tag {
    margin-bottom: 4rpx;
  }
</style>
src/pages/productionManagement/productionTraceability/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,1032 @@
<template>
  <view class="production-traceability">
    <PageHeader title="生产追溯"
                @back="goBack" />
    <!-- æœç´¢åŒºåŸŸ -->
    <view class="search-section">
      <view class="search-bar"
            @click="openNpsNoSelector">
        <view class="search-input">
          <text v-if="!selectedNpsNo"
                class="placeholder">请选择生产订单号</text>
          <text v-else
                class="value">{{ selectedNpsNoLabel }}</text>
        </view>
        <view class="search-button">
          <up-icon name="arrow-down"
                   size="20"
                   color="#999"></up-icon>
        </view>
      </view>
    </view>
    <!-- å†…容区域 -->
    <view class="content-container"
          v-if="rowData.productionOrderDto">
      <!-- åŸºç¡€ä¿¡æ¯ -->
      <view class="info-card">
        <view class="card-title">基础信息</view>
        <view class="base-info">
          <view class="info-row">
            <text class="label">生产订单号:</text>
            <text class="value">{{ rowData.productionOrderDto?.npsNo || '-' }}</text>
          </view>
          <view class="info-row">
            <text class="label">产品名称:</text>
            <text class="value">{{ rowData.productionOrderDto?.productName || '-' }}</text>
          </view>
          <view class="info-row">
            <text class="label">规格型号:</text>
            <text class="value">{{ rowData.productionOrderDto?.model || '-' }}</text>
          </view>
          <view class="info-row">
            <text class="label">计划数量:</text>
            <text class="value">{{ rowData.productionOrderDto?.quantity || 0 }} {{ rowData.productionOrderDto?.unit }}</text>
          </view>
          <view class="info-row">
            <text class="label">当前状态:</text>
            <view class="value">
              <up-tag :text="getStatusText(rowData.productionOrderDto?.status)"
                      :type="getStatusType(rowData.productionOrderDto?.status)"
                      size="mini" />
            </view>
          </view>
          <view class="info-row">
            <text class="label">客户名称:</text>
            <text class="value">{{ rowData.productionOrderDto?.customerName || '-' }}</text>
          </view>
          <view class="info-row">
            <text class="label">开始日期:</text>
            <text class="value">{{ formatDate(rowData.productionOrderDto?.startTime) }}</text>
          </view>
          <view class="info-row">
            <text class="label">完成进度:</text>
            <view class="value progress-box">
              <up-line-progress :percentage="formatProgress(rowData.productionOrderDto?.completionStatus)"
                                :activeColor="progressColor(formatProgress(rowData.productionOrderDto?.completionStatus))"
                                :showText="true" />
            </view>
          </view>
        </view>
      </view>
      <!-- å·¥å•信息 -->
      <view class="work-order-section"
            v-if="rowData.productionRecords && rowData.productionRecords.length > 0">
        <view class="section-title">工单信息</view>
        <view v-for="(item, index) in rowData.productionRecords"
              :key="index"
              class="work-order-card">
          <view class="card-header">
            <text class="work-order-no">{{ item.workOrder.workOrderNo }}</text>
            <text class="progress-tag"
                  :style="{ color: progressColor(item.workOrder.completionStatus) }">{{ item.workOrder.completionStatus || 0 }}%</text>
          </view>
          <view class="card-content">
            <view class="content-row">
              <text class="label">产品/规格:</text>
              <text class="value">{{ item.workOrder.productName }} / {{ item.workOrder.model }}</text>
            </view>
            <view class="content-row">
              <text class="label">当前工序:</text>
              <text class="value">{{ item.workOrder.operationName || '-' }}</text>
            </view>
            <view class="content-row">
              <text class="label">需求/完成:</text>
              <text class="value">{{ item.workOrder.planQuantity }} / {{ item.workOrder.completeQuantity }}</text>
            </view>
            <view class="content-row">
              <text class="label">报废数量:</text>
              <text class="value error-text">{{ item.workOrder.scrapQty || 0 }}</text>
            </view>
          </view>
          <view class="card-footer">
            <up-button type="primary"
                       size="small"
                       plain
                       text="报工记录"
                       @click="handleShowReports(item)"></up-button>
            <up-button type="success"
                       size="small"
                       plain
                       text="质检信息"
                       @click="handleShowQuality(item)"></up-button>
          </view>
        </view>
      </view>
      <view v-else
            class="no-data-minor">
        <up-empty mode="data"
                  text="暂无工单信息"
                  icon-size="40"></up-empty>
      </view>
    </view>
    <view v-else
          class="no-data">
      <up-empty mode="search"
                text="请选择生产订单号查看追溯信息"></up-empty>
    </view>
    <!-- ç”Ÿäº§è®¢å•号选择弹窗 -->
    <up-popup :show="showNpsNoSelector"
              mode="bottom"
              @close="showNpsNoSelector = false"
              round="10">
      <view class="selector-popup">
        <view class="popup-header">
          <text class="popup-title">选择生产订单号</text>
          <up-icon name="close"
                   size="20"
                   @click="showNpsNoSelector = false"></up-icon>
        </view>
        <view class="search-box">
          <up-search placeholder="输入关键字搜索"
                     v-model="npsNoQuery"
                     :show-action="false"
                     @change="handleNpsNoSearch"
                     @search="handleNpsNoSearch"
                     :loading="npsNoLoading"></up-search>
        </view>
        <scroll-view scroll-y
                     class="options-list">
          <view v-for="item in npsNoOptions"
                :key="item.id"
                class="option-item"
                @click="onSelectNpsNo(item)">
            <view class="option-main">
              <text class="nps-no">{{ item.npsNo }}</text>
              <text class="product-info">{{ item.productName }} / {{ item.model }}</text>
            </view>
            <up-icon v-if="selectedNpsNo === item.id"
                     name="checkbox-mark"
                     color="#3c9cff"
                     size="20"></up-icon>
          </view>
          <view v-if="npsNoOptions.length === 0"
                class="no-options">
            <text>{{ npsNoLoading ? '加载中...' : '暂无选项' }}</text>
          </view>
        </scroll-view>
      </view>
    </up-popup>
    <!-- æŠ¥å·¥è¯¦æƒ…弹窗 -->
    <up-popup :show="reportPopupVisible"
              mode="bottom"
              @close="reportPopupVisible = false"
              round="10">
      <view class="popup-content">
        <view class="popup-header">
          <text class="popup-title">生产报工详情</text>
          <up-icon name="close"
                   size="20"
                   @click="reportPopupVisible = false"></up-icon>
        </view>
        <scroll-view scroll-y
                     class="popup-scroll">
          <view class="detail-info">
            <view class="info-row"><text class="label">工单号:</text><text class="value">{{ detailData.workOrder.workOrderNo }}</text></view>
            <view class="info-row"><text class="label">计划/完成:</text><text class="value">{{ detailData.workOrder.planQuantity }} / {{ detailData.workOrder.completeQuantity }}</text></view>
            <view class="info-row"><text class="label">实际时间:</text><text class="value">{{ formatDate(detailData.workOrder.actualStartTime) }} è‡³ {{ formatDate(detailData.workOrder.actualEndTime) }}</text></view>
          </view>
          <view class="list-title">报工明细</view>
          <view v-for="(report, idx) in detailData.reports"
                :key="idx"
                class="detail-item">
            <view class="item-main">
              <view class="item-row"><text class="label">报工单号:</text><text class="value">{{ report.productNo }}</text></view>
              <view class="item-row"><text class="label">产出数量:</text><text class="value">{{ report.quantity || 0 }}</text></view>
              <view class="item-row"><text class="label">报废数量:</text><text class="value error-text">{{ report.scrapQty || 0 }}</text></view>
              <view class="item-row"><text class="label">工时(h):</text><text class="value">{{ report.workHour || 0 }}</text></view>
              <view class="item-row"><text class="label">创建人:</text><text class="value">{{ report.userName }}</text></view>
              <view class="item-row"><text class="label">创建时间:</text><text class="value">{{ formatDate(report.createTime, '{y}-{m}-{d} {h}:{i}') }}</text></view>
            </view>
            <view class="item-actions">
              <text class="action-link"
                    @click="showParams(report.productionOperationParamList)">参数详情</text>
              <text class="action-link green"
                    @click="handleShowInput(report.id)">投入详情</text>
            </view>
          </view>
          <view v-if="!detailData.reports || detailData.reports.length === 0"
                class="no-data-minor">暂无报工明细</view>
        </scroll-view>
      </view>
    </up-popup>
    <!-- æŠ•入详情弹窗 -->
    <up-popup :show="inputPopupVisible"
              mode="bottom"
              @close="inputPopupVisible = false"
              round="10">
      <view class="popup-content">
        <view class="popup-header">
          <text class="popup-title">投入信息详情</text>
          <up-icon name="close"
                   size="20"
                   @click="inputPopupVisible = false"></up-icon>
        </view>
        <scroll-view scroll-y
                     class="popup-scroll">
          <view class="input-list-popup">
            <view v-for="(item, idx) in inputListData"
                  :key="idx"
                  class="quality-record">
              <view class="record-title">投入记录 {{ idx + 1 }}</view>
              <view class="info-grid">
                <view class="info-item"><text class="label">报工单号</text><text class="value">{{ item.productNo || '-' }}</text></view>
                <view class="info-item"><text class="label">投入数量</text><text class="value">{{ item.quantity || 0 }} {{ item.unit || '' }}</text></view>
                <view class="info-item full-width"><text class="label">投入产品名称</text><text class="value">{{ item.productName || '-' }}</text></view>
                <view class="info-item full-width"><text class="label">投入产品型号</text><text class="value">{{ item.model || '-' }}</text></view>
              </view>
            </view>
            <view v-if="!inputListData || inputListData.length === 0"
                  class="no-data-minor">{{ inputLoading ? '加载中...' : '暂无投入记录' }}</view>
          </view>
        </scroll-view>
      </view>
    </up-popup>
    <!-- è´¨æ£€è¯¦æƒ…弹窗 -->
    <up-popup :show="qualityPopupVisible"
              mode="bottom"
              @close="qualityPopupVisible = false"
              round="10">
      <view class="popup-content">
        <view class="popup-header">
          <text class="popup-title">质检详情</text>
          <up-icon name="close"
                   size="20"
                   @click="qualityPopupVisible = false"></up-icon>
        </view>
        <scroll-view scroll-y
                     class="popup-scroll">
          <view v-for="(record, idx) in qualityRecords"
                :key="idx"
                class="quality-record">
            <view class="record-title">检测记录 {{ idx + 1 }}</view>
            <view class="info-grid">
              <view class="info-item"><text class="label">检测日期</text><text class="value">{{ formatDate(record.createTime) }}</text></view>
              <view class="info-item"><text class="label">检测结果</text><up-tag style="width:100rpx"
                        :text="record.checkResult || '待检测'"
                        :type="record.checkResult === '合格' ? 'success' : 'error'"
                        size="mini" /></view>
              <view class="info-item"><text class="label">检验员</text><text class="value">{{ record.userName }}</text></view>
              <view class="info-item"><text class="label">数量</text><text class="value">{{ record.quantity }} {{ record.unit }}</text></view>
              <view class="info-item"><text class="label">报工单号</text><text class="value">{{ record.reportNo || '-' }}</text></view>
              <view class="info-item"><text class="label">产品名称</text><text class="value">{{ record.productName || '-' }}</text></view>
              <view class="info-item"><text class="label">规格型号</text><text class="value">{{ record.model || '-' }}</text></view>
              <view class="info-item"><text class="label">检测单位</text><text class="value">{{ record.checkCompany || '-' }}</text></view>
            </view>
            <view class="params-table">
              <view class="table-header">
                <text class="col">指标</text>
                <text class="col">单位</text>
                <text class="col">标准值</text>
                <text class="col">内控值</text>
                <text class="col">实际值</text>
              </view>
              <view v-for="(param, pIdx) in record.inspectParamList"
                    :key="pIdx"
                    class="table-row">
                <text class="col">{{ param.parameterItem }}</text>
                <text class="col">{{ param.unit || '-' }}</text>
                <text class="col">{{ param.standardValue }}</text>
                <text class="col">{{ param.controlValue || '-' }}</text>
                <text class="col"
                      :class="{ 'error-text': param.testValue != param.standardValue }">{{ param.testValue }}</text>
              </view>
            </view>
          </view>
          <view v-if="!qualityRecords || qualityRecords.length === 0"
                class="no-data-minor">暂无质检记录</view>
        </scroll-view>
      </view>
    </up-popup>
    <!-- å‚数详情弹窗 -->
    <up-modal :show="paramModalVisible"
              title="参数详情"
              @confirm="paramModalVisible = false">
      <view class="modal-content">
        <view v-for="(param, idx) in currentParams"
              :key="idx"
              class="param-row">
          <text class="label">{{ param.paramName }}:</text>
          <text class="value">{{ param.inputValue }} {{ param.unit && param.unit !== '/' ? param.unit : '' }}</text>
        </view>
        <view v-if="!currentParams || currentParams.length === 0"
              class="no-data-minor">暂无参数数据</view>
      </view>
    </up-modal>
  </view>
</template>
<script setup>
  import { ref, reactive, computed } from "vue";
  import { onLoad } from "@dcloudio/uni-app";
  import {
    getOrderDetail,
    productOrderListPage,
  } from "@/api/productionManagement/productionOrder";
  import { productionProductInputListPage } from "@/api/productionManagement/productionProductMain";
  import PageHeader from "@/components/PageHeader.vue";
  import { parseTime } from "@/utils/ruoyi";
  // é€‰æ‹©å™¨ç›¸å…³
  const showNpsNoSelector = ref(false);
  const npsNoQuery = ref("");
  const npsNoOptions = ref([]);
  const npsNoLoading = ref(false);
  const selectedNpsNo = ref(null);
  const selectedNpsNoLabel = ref("");
  const rowData = reactive({
    productionOrderDto: null,
    productionRecords: [],
  });
  // æŠ¥å·¥è¯¦æƒ…
  const reportPopupVisible = ref(false);
  const detailData = ref({ workOrder: {}, reports: [] });
  // æŠ•入详情
  const inputPopupVisible = ref(false);
  const inputListData = ref([]);
  const inputLoading = ref(false);
  // è´¨æ£€è¯¦æƒ…
  const qualityPopupVisible = ref(false);
  const qualityRecords = ref([]);
  // å‚数详情
  const paramModalVisible = ref(false);
  const currentParams = ref([]);
  const goBack = () => {
    uni.navigateBack();
  };
  const openNpsNoSelector = () => {
    showNpsNoSelector.value = true;
    if (npsNoOptions.value.length === 0) {
      handleNpsNoSearch();
    }
  };
  const handleNpsNoSearch = async () => {
    npsNoLoading.value = true;
    try {
      const res = await productOrderListPage({
        npsNo: npsNoQuery.value || "",
        pageNum: 1,
        pageSize: 50,
      });
      npsNoOptions.value = res.data?.records || res.rows || [];
    } catch (error) {
      console.error(error);
    } finally {
      npsNoLoading.value = false;
    }
  };
  const onSelectNpsNo = async item => {
    selectedNpsNo.value = item.id;
    selectedNpsNoLabel.value = item.npsNo;
    showNpsNoSelector.value = false;
    uni.showLoading({ title: "加载中..." });
    try {
      const res = await getOrderDetail(item.npsNo);
      if (res.code === 200 && res.data) {
        const { productionOrder, workOrderList } = res.data;
        rowData.productionOrderDto = productionOrder || item;
        rowData.productionRecords = workOrderList || [];
      } else {
        rowData.productionOrderDto = item;
        rowData.productionRecords = [];
      }
    } catch (error) {
      console.error(error);
      rowData.productionOrderDto = item;
      rowData.productionRecords = [];
      uni.showToast({ title: "获取详情失败", icon: "none" });
    } finally {
      uni.hideLoading();
    }
  };
  onLoad(async options => {
    if (options.npsNo) {
      uni.showLoading({ title: "加载中..." });
      try {
        const res = await productOrderListPage({
          npsNo: options.npsNo,
          pageNum: 1,
          pageSize: 10,
        });
        const records = res.data?.records || res.rows || [];
        if (records.length > 0) {
          onSelectNpsNo(records[0]);
        } else {
          uni.showToast({ title: "未找到相关订单", icon: "none" });
        }
      } catch (error) {
        console.error(error);
      } finally {
        uni.hideLoading();
      }
    }
  });
  const getStatusText = status => {
    const statusMap = { 1: "待开始", 2: "进行中", 3: "已完成", 5: "已结束" };
    return statusMap[status] || "已取消";
  };
  const getStatusType = status => {
    const typeMap = { 1: "primary", 2: "warning", 3: "success", 5: "error" };
    return typeMap[status] || "info";
  };
  const formatDate = (date, pattern = "{y}-{m}-{d}") => {
    return parseTime(date, pattern) || "-";
  };
  const formatProgress = val => {
    const p = parseFloat(val || 0);
    return p >= 100 ? 100 : p;
  };
  const progressColor = percentage => {
    if (percentage < 30) return "#f56c6c";
    if (percentage < 70) return "#e6a23c";
    return "#67c23a";
  };
  const handleShowReports = row => {
    detailData.value = {
      workOrder: row.workOrder || {},
      reports: (row.reportList || []).map(r => ({
        ...r.reportMain,
        ...(r.reportOutputList ? r.reportOutputList[0] : {}),
        id: r.reportMain.id,
        productionOperationParamList: r.reportParamList || [],
      })),
    };
    reportPopupVisible.value = true;
  };
  const handleShowInput = async reportId => {
    inputPopupVisible.value = true;
    inputLoading.value = true;
    inputListData.value = [];
    try {
      const res = await productionProductInputListPage({
        productMainId: reportId,
        pageNum: 1,
        pageSize: 100,
      });
      inputListData.value = res.data?.records || res.rows || [];
    } catch (error) {
      console.error(error);
      uni.showToast({ title: "获取投入信息失败", icon: "none" });
    } finally {
      inputLoading.value = false;
    }
  };
  const handleShowQuality = row => {
    const inspects = row.inspectList || [];
    qualityRecords.value = inspects.map(i => ({
      ...i.inspect,
      reportNo: i.reportNo,
      productName: row.workOrder?.productName || "-",
      model: row.workOrder?.model || "-",
      userName: i.reportMain?.userName || "-",
      inspectParamList: i.inspectParamList || [],
    }));
    qualityPopupVisible.value = true;
  };
  const showParams = params => {
    currentParams.value = params || [];
    paramModalVisible.value = true;
  };
</script>
<style scoped lang="scss">
  @import "@/styles/procurement-common.scss";
  .production-traceability {
    min-height: 100vh;
    background-color: #f5f7fa;
  }
  .search-section {
    background-color: #fff;
    padding: 20rpx 24rpx;
    margin-bottom: 20rpx;
  }
  .search-bar {
    display: flex;
    align-items: center;
    background-color: #f2f2f2;
    border-radius: 8rpx;
    padding: 0 20rpx;
    height: 80rpx;
    .search-input {
      flex: 1;
      display: flex;
      align-items: center;
      .placeholder {
        font-size: 28rpx;
        color: #999;
      }
      .value {
        font-size: 28rpx;
        color: #333;
        font-weight: 500;
      }
    }
    .search-button {
      padding: 0 10rpx;
    }
  }
  .selector-popup {
    background: #fff;
    padding: 30rpx;
    max-height: 70vh;
    display: flex;
    flex-direction: column;
    .popup-header {
      display: flex;
      justify-content: space-between;
      align-items: center;
      margin-bottom: 24rpx;
      .popup-title {
        font-size: 32rpx;
        font-weight: bold;
      }
    }
    .search-box {
      margin-bottom: 20rpx;
    }
    .options-list {
      flex: 1;
      overflow: hidden;
    }
    .option-item {
      display: flex;
      justify-content: space-between;
      align-items: center;
      padding: 24rpx 0;
      border-bottom: 1rpx solid #f0f0f0;
      .option-main {
        flex: 1;
        .nps-no {
          font-size: 28rpx;
          font-weight: bold;
          color: #333;
          display: block;
          margin-bottom: 4rpx;
        }
        .product-info {
          font-size: 24rpx;
          color: #999;
        }
      }
    }
    .no-options {
      text-align: center;
      padding: 40rpx;
      color: #999;
      font-size: 26rpx;
    }
  }
  .content-container {
    padding: 0 24rpx 40rpx;
  }
  .info-card {
    background: #fff;
    border-radius: 16rpx;
    padding: 24rpx;
    margin-bottom: 24rpx;
    box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
    .card-title {
      font-size: 32rpx;
      font-weight: bold;
      color: #333;
      margin-bottom: 24rpx;
      padding-left: 16rpx;
      border-left: 8rpx solid #3c9cff;
    }
  }
  .info-grid {
    display: flex;
    flex-wrap: wrap;
    .info-item {
      width: 50%;
      margin-bottom: 20rpx;
      display: flex;
      flex-direction: column;
      &.full-width {
        width: 100%;
      }
      .label {
        font-size: 24rpx;
        color: #999;
        margin-bottom: 8rpx;
      }
      .value {
        font-size: 28rpx;
        color: #333;
        word-break: break-all;
      }
    }
  }
  .progress-container {
    display: flex;
    align-items: center;
    gap: 20rpx;
    up-line-progress {
      flex: 1;
    }
    .progress-text {
      font-size: 24rpx;
      color: #666;
      min-width: 60rpx;
    }
  }
  .section-title {
    font-size: 32rpx;
    font-weight: bold;
    color: #333;
    margin: 32rpx 0 20rpx;
  }
  .work-order-card {
    background: #fff;
    border-radius: 16rpx;
    padding: 24rpx;
    margin-bottom: 20rpx;
    box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
    .card-header {
      display: flex;
      justify-content: space-between;
      align-items: center;
      margin-bottom: 20rpx;
      padding-bottom: 16rpx;
      border-bottom: 1rpx solid #f0f0f0;
      .work-order-no {
        font-size: 28rpx;
        font-weight: bold;
        color: #3c9cff;
      }
      .progress-tag {
        font-size: 28rpx;
        font-weight: bold;
      }
    }
    .card-content {
      .content-row {
        margin-bottom: 12rpx;
        font-size: 26rpx;
        .label {
          color: #999;
        }
        .value {
          color: #333;
        }
      }
    }
    .card-footer {
      display: flex;
      justify-content: flex-end;
      gap: 20rpx;
      margin-top: 20rpx;
    }
  }
  .base-info {
    background: #fff;
    padding: 24rpx;
    border-radius: 16rpx;
    margin-bottom: 30rpx;
    .info-row {
      margin-bottom: 16rpx;
      font-size: 28rpx;
      display: flex;
      align-items: center;
      &:last-child {
        margin-bottom: 0;
      }
      .label {
        color: #999;
        min-width: 180rpx;
      }
      .value {
        color: #333;
        flex: 1;
        font-weight: 500;
        &.progress-box {
          flex: 1;
        }
      }
    }
  }
  .info-grid {
    display: flex;
    flex-wrap: wrap;
    padding: 10rpx 0;
    .info-item {
      width: 50%;
      margin-bottom: 20rpx;
      display: flex;
      flex-direction: column;
      &.full-width {
        width: 100%;
      }
      .label {
        font-size: 24rpx;
        color: #999;
        margin-bottom: 4rpx;
      }
      .value {
        font-size: 28rpx;
        color: #333;
        font-weight: 500;
      }
    }
  }
  .popup-content {
    background: #fff;
    padding: 30rpx;
    max-height: 80vh;
    display: flex;
    flex-direction: column;
    border-radius: 20rpx 20rpx 0 0;
    .popup-header {
      display: flex;
      justify-content: space-between;
      align-items: center;
      margin-bottom: 30rpx;
      .popup-title {
        font-size: 34rpx;
        font-weight: bold;
        color: #333;
      }
    }
    .popup-scroll {
      flex: 1;
      overflow: hidden;
    }
  }
  .detail-info {
    background: #f8f9fa;
    padding: 24rpx;
    border-radius: 16rpx;
    margin-bottom: 30rpx;
    flex-direction: column;
    .info-row {
      margin-bottom: 12rpx;
      font-size: 28rpx;
      display: flex;
      &:last-child {
        margin-bottom: 0;
      }
      .label {
        color: #999;
        min-width: 140rpx;
      }
      .value {
        color: #333;
        flex: 1;
        font-weight: 500;
      }
    }
  }
  .list-title {
    font-size: 30rpx;
    font-weight: bold;
    margin-bottom: 20rpx;
    color: #333;
    display: flex;
    align-items: center;
    &::before {
      content: "";
      width: 6rpx;
      height: 28rpx;
      background: #3c9cff;
      margin-right: 12rpx;
      border-radius: 4rpx;
    }
  }
  .detail-item {
    background: #fff;
    border: 1rpx solid #f0f0f0;
    border-radius: 12rpx;
    padding: 24rpx;
    margin-bottom: 20rpx;
    display: flex;
    justify-content: space-between;
    align-items: center;
    .item-main {
      flex: 1;
      .item-row {
        font-size: 26rpx;
        margin-bottom: 8rpx;
        display: flex;
        &:last-child {
          margin-bottom: 0;
        }
        .label {
          color: #999;
          min-width: 130rpx;
        }
        .value {
          color: #333;
          flex: 1;
        }
      }
    }
    .item-actions {
      display: flex;
      flex-direction: column;
      gap: 16rpx;
      padding-left: 20rpx;
      border-left: 1rpx solid #f0f0f0;
      .action-link {
        font-size: 26rpx;
        color: #3c9cff;
        white-space: nowrap;
        &.green {
          color: #52c41a;
        }
      }
    }
  }
  .quality-record {
    background: #fff;
    border: 1rpx solid #f0f0f0;
    border-radius: 16rpx;
    padding: 24rpx;
    margin-bottom: 30rpx;
    .record-title {
      font-size: 30rpx;
      font-weight: bold;
      color: #3c9cff;
      margin-bottom: 24rpx;
      display: flex;
      justify-content: space-between;
    }
  }
  .params-table {
    margin-top: 24rpx;
    border: 1rpx solid #f0f0f0;
    border-radius: 12rpx;
    overflow: hidden;
    .table-header {
      display: flex;
      background: #f8f9fa;
      padding: 20rpx 16rpx;
      font-size: 26rpx;
      font-weight: bold;
      color: #666;
    }
    .table-row {
      display: flex;
      padding: 20rpx 16rpx;
      font-size: 26rpx;
      border-top: 1rpx solid #f0f0f0;
      color: #333;
      &:nth-child(even) {
        background: #fafafa;
      }
    }
    .col {
      flex: 1;
      text-align: center;
      word-break: break-all;
    }
  }
  .modal-content {
    padding: 30rpx;
    .param-row {
      margin-bottom: 20rpx;
      font-size: 28rpx;
      display: flex;
      &:last-child {
        margin-bottom: 0;
      }
      .label {
        color: #666;
        min-width: 160rpx;
      }
      .value {
        color: #333;
        font-weight: 500;
        flex: 1;
      }
    }
  }
  .input-list-popup {
    .input-item {
      background: #fff;
      border: 1rpx solid #f0f0f0;
      border-radius: 12rpx;
      padding: 20rpx;
      margin-bottom: 20rpx;
      .input-row {
        display: flex;
        font-size: 26rpx;
        margin-bottom: 8rpx;
        &:last-child {
          margin-bottom: 0;
        }
        .label {
          color: #999;
          min-width: 160rpx;
        }
        .value {
          color: #333;
          flex: 1;
        }
      }
    }
  }
  .error-text {
    color: #f56c6c;
    font-weight: bold;
  }
  .no-data-minor {
    text-align: center;
    padding: 60rpx 40rpx;
    color: #999;
    font-size: 28rpx;
  }
</style>
在上述文件截断后对比
src/pages/qualityManagement/finalInspection/add.vue src/pages/qualityManagement/finalInspection/detail.vue src/pages/qualityManagement/finalInspection/index.vue src/pages/qualityManagement/materialInspection/add.vue src/pages/qualityManagement/materialInspection/detail.vue src/pages/qualityManagement/materialInspection/index.vue src/pages/qualityManagement/processInspection/add.vue src/pages/qualityManagement/processInspection/detail.vue src/pages/qualityManagement/processInspection/index.vue src/pages/sales/salesAccount/goOut.vue src/pages/sales/salesQuotation/detail.vue src/pages/sales/salesQuotation/edit.vue src/pages/sales/salesQuotation/index.vue src/pages/works.vue src/static/images/icon/baogongtaizhang.svg src/static/images/icon/bom.svg src/static/images/icon/gongxuguanli.svg src/static/images/icon/gongyiluxian.svg src/static/images/icon/guihuandengji.svg src/static/images/icon/jichucanshu.svg src/static/images/icon/jieyuedengji.svg src/static/images/icon/kucunguanli.svg src/static/images/icon/shengchandingdan.svg src/static/images/icon/shengchanhesuan.svg src/static/images/icon/shengchanjihua.svg src/static/images/icon/shengchanpaichan.svg src/static/images/icon/shengchanshikuang.svg src/static/images/icon/shengchanzhuisu.svg src/utils/versionUpgrade.js