zhangwencui
2026-04-30 fa403c58d2bea76a7131ea9dbb1d60247ea9ca77
生产计划和生产订单模块开发(仅查看)
已添加5个文件
已修改4个文件
1478 ■■■■■ 文件已修改
src/api/productionManagement/productionOrder.js 43 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/productionManagement/productionPlan.js 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages.json 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/productionManagement/mainProductionPlan/detail.vue 252 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/productionManagement/mainProductionPlan/index.vue 300 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/productionManagement/productionOrder/index.vue 293 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/productionManagement/productionOrder/pickingDetail.vue 350 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/productionManagement/productionOrder/source.vue 166 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/works.vue 17 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/productionManagement/productionOrder.js
@@ -1,19 +1,44 @@
// ç”Ÿäº§è®¢å•页面接口
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 getProductOrderSource(id) {
  return request({
    url: "/salesLedger/scheduling/productionDispatch",
    method: "post",
    data: query,
    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",
  });
}
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/pages.json
@@ -775,6 +775,20 @@
      }
    },
    {
      "path": "pages/productionManagement/productionOrder/source",
      "style": {
        "navigationBarTitleText": "来源数据",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/productionManagement/productionOrder/pickingDetail",
      "style": {
        "navigationBarTitleText": "领料详情",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/productionManagement/productionDispatching/index",
      "style": {
        "navigationBarTitleText": "生产派工",
@@ -802,6 +816,20 @@
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/productionManagement/mainProductionPlan/index",
      "style": {
        "navigationBarTitleText": "主生产计划",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/productionManagement/mainProductionPlan/detail",
      "style": {
        "navigationBarTitleText": "计划详情",
        "navigationStyle": "custom"
      }
    },
    // {
    //   "path": "pages/productionManagement/productionCosting/index",
    //   "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/productionOrder/index.vue
@@ -9,8 +9,8 @@
                <view class="search-input">
                    <up-input
                        class="search-text"
                        placeholder="请输入客户名称搜索"
                        v-model="searchForm.customerName"
                        placeholder="请输入订单号或产品名称"
                        v-model="searchForm.keyword"
                        @change="handleQuery"
                        clearable
                    />
@@ -21,8 +21,8 @@
            </view>
        </view>
        
        <!-- ç”Ÿäº§è®¢å•列表 -->
        <view class="ledger-list" v-if="tableData.length > 0">
        <!-- åˆ—表区域 -->
        <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">
@@ -30,54 +30,55 @@
                            <view class="document-icon">
                                <up-icon name="file-text" size="16" color="#ffffff"></up-icon>
                            </view>
                            <text class="item-id">{{ item.salesContractNo }}</text>
                            <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">{{ 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>
                            <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.specificationModel }}</text>
                            <text class="detail-value">{{ item.model || '-' }}</text>
                        </view>
                        <view class="detail-row">
                            <text class="detail-label">数量</text>
                            <text class="detail-value">{{ item.quantity }} {{ item.unit }}</text>
                            <text class="detail-label">订单数量</text>
                            <text class="detail-value">{{ item.quantity || 0 }}</text>
                        </view>
                        <view class="detail-row">
                            <text class="detail-label">排产数量</text>
                            <text class="detail-value highlight">{{ item.schedulingNum }}</text>
                            <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">
                            <text class="detail-label">完工数量</text>
                            <text class="detail-value highlight">{{ item.successNum }}</text>
                            <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="primary" size="mini" plain text="来源" @click="goSource(item)"></up-button>
                            <up-button type="success" size="mini" plain text="领料详情" @click="goPickingDetail(item)"></up-button>
                        </view>
                    </view>
                </view>
            </view>
        </view>
            <up-loadmore :status="loadStatus" v-if="tableData.length >= page.size" />
        </scroll-view>
        <view v-else class="no-data">
            <text>暂无生产订单数据</text>
            <up-empty mode="data" text="暂无生产订单数据"></up-empty>
        </view>
    </view>
</template>
@@ -86,70 +87,126 @@
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 { productOrderListPage } from "@/api/productionManagement/productionOrder.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: -1,
    current: 1,
    size: 10,
    total: 0,
});
// æœç´¢è¡¨å•数据
const data = reactive({
    searchForm: {
        customerName: "",
        keyword: "",
    },
});
const { searchForm } = toRefs(data);
// é€šç”¨æç¤ºå‡½æ•°
const showLoadingToast = (message) => {
    uni.showLoading({
        title: message,
        mask: true
    });
};
const closeToast = () => {
    uni.hideLoading();
};
// è¿”回上一页
const goBack = () => {
    uni.navigateBack();
};
// æ ¼å¼åŒ–日期
const formatDate = (date) => {
    return date ? dayjs(date).format('YYYY-MM-DD') : '-';
};
// èŽ·å–çŠ¶æ€æ–‡æœ¬
const getStatusText = (status) => {
    const statusMap = {
        1: "待开始",
        2: "进行中",
        3: "已完成",
        4: "已取消",
    };
    return statusMap[status] || "未知";
};
// èŽ·å–çŠ¶æ€ç±»åž‹
const getStatusType = (status) => {
    const typeMap = {
        1: "primary",
        2: "warning",
        3: "success",
        4: "danger",
    };
    return typeMap[status] || "info";
};
// å®Œæˆè¿›åº¦ç™¾åˆ†æ¯”
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 = []; // é‡ç½®åˆ—表数据
    tableData.value = [];
    getList();
};
// åŠ è½½æ›´å¤š
const loadMore = () => {
    if (loadStatus.value === 'nomore' || loading.value) return;
    page.current++;
    getList();
};
// èŽ·å–åˆ—è¡¨æ•°æ®
const getList = () => {
    loading.value = true;
    showLoadingToast('加载中...');
    loadStatus.value = 'loading';
    
    // æž„造请求参数
    const params = { ...searchForm.value, ...page };
    const params = {
        current: page.current,
        size: page.size,
        npsNo: searchForm.value.keyword,
        productName: searchForm.value.keyword
    };
    
    schedulingListPage(params).then((res) => {
    productOrderListPage(params).then((res) => {
        loading.value = false;
        closeToast();
        const records = res.data.records || [];
        if (page.current === 1) {
            tableData.value = records;
        } else {
            tableData.value = [...tableData.value, ...records];
        }
        
        tableData.value = res.data.records || [];
        if (records.length < page.size) {
            loadStatus.value = 'nomore';
        } else {
            loadStatus.value = 'loadmore';
        }
        page.total = res.data.total || 0;
    }).catch(() => {
        loading.value = false;
        closeToast();
        loadStatus.value = 'loadmore';
        uni.showToast({
            title: '加载失败',
            icon: 'error'
@@ -157,37 +214,131 @@
    });
};
// è·³è½¬æ¥æº
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}`
    });
};
// é¡µé¢æ˜¾ç¤ºæ—¶åŠ è½½æ•°æ®
onShow(() => {
    // åŠ è½½åˆ—è¡¨æ•°æ®
    getList();
    handleQuery();
});
</script>
<style scoped lang="scss">
@import '@/styles/sales-common.scss';
// ç”Ÿäº§è®¢å•页面样式
.production-order {
    min-height: 100vh;
    background: #f8f9fa;
    position: relative;
    display: flex;
    flex-direction: column;
}
// é‡å†™éƒ¨åˆ†æ ·å¼ä»¥é€‚配生产订单
.list-container {
    flex: 1;
    height: 0;
}
.ledger-item {
    .detail-value.highlight {
        color: #ff6b35;
        font-weight: 600;
    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;
                &.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;
                }
            }
        }
    }
    .item-footer {
        padding-top: 20rpx;
        border-top: 1rpx solid #f0f0f0;
        display: flex;
        justify-content: flex-end;
        .action-btns {
            display: flex;
            gap: 20rpx;
        }
    }
}
// é€‚配 uView ç»„件样式
:deep(.up-input) {
    background: transparent;
}
:deep(.up-datetime-picker) {
    width: 100%;
.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/works.vue
@@ -569,10 +569,10 @@
  // ç”Ÿäº§ç®¡æŽ§åŠŸèƒ½æ•°æ®
  const productionItems = reactive([
    // {
    //   icon: "/static/images/icon/shengchandingdan@2x.svg",
    //   label: "生产订单",
    // },
    {
      icon: "/static/images/icon/shengchandingdan@2x.svg",
      label: "生产订单",
    },
    // {
    //   icon: "/static/images/icon/shengchanpaigong@2x.svg",
    //   label: "生产派工",
@@ -581,6 +581,10 @@
    //   icon: "/static/images/icon/shengchanpaichan@2x.svg",
    //   label: "工序排产",
    // },
    {
      icon: "/static/images/icon/shengchanbaogong.svg",
      label: "主生产计划",
    },
    {
      icon: "/static/images/icon/shengchanbaogong.svg",
      label: "生产报工",
@@ -843,6 +847,11 @@
          url: "/pages/productionManagement/workOrder/index",
        });
        break;
      case "主生产计划":
        uni.navigateTo({
          url: "/pages/productionManagement/mainProductionPlan/index",
        });
        break;
      case "生产报工":
        getcode();
        break;