zhangwencui
7 小时以前 c7043051efed6648a4ae3d9aceaa70fc34171a58
src/pages/productionManagement/mainProductionPlan/index.vue
@@ -1,300 +1,520 @@
<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>
  <view class="main-production-plan">
    <!-- 使用通用页面头部组件 -->
    <PageHeader title="主生产计划"
                @back="goBack">
      <!-- <template #right>
        <view class="header-right">
          <u-button v-if="!selectMode"
                    size="mini"
                    @click="enterSelectMode">合并下发</u-button>
          <u-button v-else
                    size="mini"
                    @click="exitSelectMode">取消</u-button>
        </view>
      </template> -->
    </PageHeader>
    <!-- 搜索区域 -->
    <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">
      <up-checkbox-group v-if="selectMode"
                         v-model="selectedIds"
                         placement="column">
        <view v-for="(item, index) in tableData"
              :key="item.id || index"
              @click="toggleSelect(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-checkbox :name="String(item.id)"
                             :label="''"
                             @click.stop></up-checkbox>
              </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">{{ getRemainingQty(item) }} {{ item.unit || '方' }}</text>
              </view>
            </view>
          </view>
        </view>
      </up-checkbox-group>
      <view v-else>
        <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">{{ item.quantityIssued || 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="action-buttons">
              <u-button v-if="canEdit(item)"
                        size="small"
                        class="action-btn"
                        @click.stop="goEdit(item)">编辑</u-button>
              <u-button v-if="canIssue(item)"
                        size="small"
                        class="action-btn"
                        type="primary"
                        @click.stop="goIssue([item])">下发</u-button>
              <u-button v-if="canDelete(item)"
                        size="small"
                        class="action-btn"
                        type="error"
                        @click.stop="handleDelete(item)">删除</u-button>
            </view>
          </view>
        </view>
      </view>
    </scroll-view>
    <view v-else
          class="no-data">
      <up-empty mode="data"
                text="暂无主生产计划数据"></up-empty>
    </view>
    <FooterButtons v-if="selectMode"
                   cancelText="取消"
                   confirmText="下发"
                   :confirmDisabled="selectedIds.length === 0"
                   @cancel="exitSelectMode"
                   @confirm="goIssueSelected" />
    <view class="fab-button"
          @click="goAdd">
      <up-icon name="plus"
               size="24"
               color="#ffffff"></up-icon>
    </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";
  import { ref, reactive, toRefs } from "vue";
  import { onShow } from "@dcloudio/uni-app";
  import dayjs from "dayjs";
  import {
    productionPlanDelete,
    productionPlanListPage,
  } from "@/api/productionManagement/productionPlan.js";
  import PageHeader from "@/components/PageHeader.vue";
  import FooterButtons from "@/components/FooterButtons.vue";
const { proxy } = getCurrentInstance();
  // 加载状态
  const loading = ref(false);
  // 列表数据
  const tableData = ref([]);
// 加载状态
const loading = ref(false);
const loadStatus = ref('loadmore');
// 列表数据
const tableData = ref([]);
  const page = reactive({
    current: -1,
    size: -1,
  });
// 分页配置
const page = reactive({
   current: 1,
   size: 10,
   total: 0,
});
  // 搜索表单数据
  const data = reactive({
    searchForm: {
      keyword: "",
      mpsNo: "",
      productName: "",
    },
  });
  const { searchForm } = toRefs(data);
// 搜索表单数据
const data = reactive({
   searchForm: {
      keyword: "",
      mpsNo: "",
      productName: ""
   },
});
const { searchForm } = toRefs(data);
  const selectMode = ref(false);
  const selectedIds = ref([]);
// 返回上一页
const goBack = () => {
   uni.navigateBack();
};
  // 返回上一页
  const goBack = () => {
    uni.navigateBack();
  };
// 格式化日期
const formatDate = (date) => {
   return date ? dayjs(date).format('YYYY-MM-DD') : '-';
};
  // 格式化日期
  const formatDate = date => {
    return date ? dayjs(date).format("YYYY-MM-DD") : "-";
  };
// 获取状态文本
const getStatusText = (status) => {
   const statusMap = {
      0: "待下发",
      1: "部分下发",
      2: "已下发",
   };
   return statusMap[status] || "未知";
};
  // 获取状态文本
  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";
};
  // 获取状态类型 (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 getRemainingQty = row => {
    const required = Number(row?.qtyRequired || 0);
    const issued = Number(row?.quantityIssued || 0);
    const remaining = required - issued;
    return Number((remaining > 0 ? remaining : 0).toFixed(4));
  };
// 加载更多
const loadMore = () => {
   if (loadStatus.value === 'nomore' || loading.value) return;
   page.current++;
   getList();
};
  const canEdit = row => {
    return String(row?.status) === "0" && row?.source !== "销售";
  };
// 获取列表数据
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 canDelete = row => {
    return String(row?.status) === "0";
  };
// 跳转详情
const goDetail = (item) => {
   uni.navigateTo({
      url: `/pages/productionManagement/mainProductionPlan/detail?data=${encodeURIComponent(JSON.stringify(item))}`
   });
};
  const canIssue = row => {
    return String(row?.status) !== "2" && getRemainingQty(row) > 0;
  };
// 页面显示时加载数据
onShow(() => {
   handleQuery();
});
  const enterSelectMode = () => {
    selectMode.value = true;
    selectedIds.value = [];
  };
  const exitSelectMode = () => {
    selectMode.value = false;
    selectedIds.value = [];
  };
  const toggleSelect = item => {
    if (!item?.id) return;
    const id = String(item.id);
    const idx = selectedIds.value.indexOf(id);
    if (idx >= 0) selectedIds.value.splice(idx, 1);
    else selectedIds.value.push(id);
  };
  // 查询列表
  const handleQuery = () => {
    getList();
  };
  // 获取列表数据
  const getList = () => {
    loading.value = true;
    exitSelectMode();
    const params = {
      current: page.current,
      size: page.size,
      mpsNo: searchForm.value.keyword, // 简单处理:搜索号
      productName: searchForm.value.keyword, // 简单处理:搜索名称
    };
    productionPlanListPage(params)
      .then(res => {
        loading.value = false;
        const payload = res?.data;
        if (Array.isArray(payload)) {
          tableData.value = payload;
        } else if (payload && typeof payload === "object") {
          tableData.value = payload.records || payload.rows || payload.data || [];
        } else {
          tableData.value = [];
        }
      })
      .catch(() => {
        loading.value = false;
        uni.showToast({
          title: "加载失败",
          icon: "error",
        });
      });
  };
  // 跳转详情
  const goDetail = item => {
    if (selectMode.value) return;
    uni.navigateTo({
      url: `/pages/productionManagement/mainProductionPlan/detail?data=${encodeURIComponent(
        JSON.stringify(item)
      )}`,
    });
  };
  const goAdd = () => {
    uni.navigateTo({
      url: "/pages/productionManagement/mainProductionPlan/edit",
    });
  };
  const goEdit = item => {
    uni.navigateTo({
      url: `/pages/productionManagement/mainProductionPlan/edit?data=${encodeURIComponent(
        JSON.stringify(item)
      )}`,
    });
  };
  const goIssue = rows => {
    const list = Array.isArray(rows) ? rows : [];
    if (list.length === 0) return;
    const first = list[0];
    const sumRemaining = Number(
      list.reduce((sum, r) => sum + getRemainingQty(r), 0).toFixed(4)
    );
    const payload = {
      productName: first.productName || "",
      model: first.model || "",
      productId: first.productId ?? undefined,
      ids: list.map(r => r.id),
      planCompleteTime:
        formatDate(first.requiredDate) === "-"
          ? ""
          : formatDate(first.requiredDate),
      totalAssignedQuantity: sumRemaining,
      maxQuantity: sumRemaining,
    };
    uni.navigateTo({
      url: `/pages/productionManagement/mainProductionPlan/issue?data=${encodeURIComponent(
        JSON.stringify(payload)
      )}`,
    });
  };
  const goIssueSelected = () => {
    const rows = tableData.value.filter(r =>
      selectedIds.value.includes(String(r.id))
    );
    if (rows.length === 0) {
      uni.showToast({ title: "请选择要下发的计划", icon: "none" });
      return;
    }
    const first = rows[0];
    const modelId = first.productModelId;
    const invalid = rows.some(r => r.productModelId !== modelId || !canIssue(r));
    if (invalid) {
      uni.showToast({
        title: "只能合并下发相同规格且可下发的计划",
        icon: "none",
      });
      return;
    }
    goIssue(rows);
  };
  const handleDelete = item => {
    if (!item?.id) return;
    uni.showModal({
      title: "删除提示",
      content: "确认删除该生产计划?删除后无法恢复",
      success: res => {
        if (!res.confirm) return;
        uni.showLoading({ title: "删除中...", mask: true });
        productionPlanDelete([item.id])
          .then(() => {
            uni.showToast({ title: "删除成功", icon: "success" });
            getList();
          })
          .catch(() => {
            uni.showToast({ title: "删除失败", icon: "error" });
          })
          .finally(() => {
            uni.hideLoading();
          });
      },
    });
  };
  // 页面显示时加载数据
  onShow(() => {
    handleQuery();
  });
</script>
<style scoped lang="scss">
@import '@/styles/sales-common.scss';
  @import "@/styles/sales-common.scss";
.main-production-plan {
   min-height: 100vh;
   background: #f8f9fa;
   display: flex;
   flex-direction: column;
}
  .main-production-plan {
    min-height: 100vh;
    background: #f8f9fa;
    display: flex;
    flex-direction: column;
  }
.list-container {
   flex: 1;
   height: 0;
}
  .list-container {
    flex: 1;
    height: 0;
    padding-bottom: 140rpx;
  }
.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;
      }
   }
}
  .header-right {
    display: flex;
    align-items: center;
    padding-right: 12rpx;
  }
.no-data {
   padding-top: 200rpx;
}
  .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;
        }
      }
    }
    .action-buttons {
      display: flex;
      justify-content: flex-end;
      align-items: center;
      gap: 16rpx;
      padding-top: 16rpx;
      border-top: 1rpx solid #f0f0f0;
    }
    .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-right :deep(.up-checkbox__label) {
      display: none;
    }
  }
  .fab-button {
    position: fixed;
    right: 30rpx;
    bottom: 140rpx;
    width: 100rpx;
    height: 100rpx;
    background: #3c9cff;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    box-shadow: 0 8rpx 24rpx rgba(60, 156, 255, 0.3);
    z-index: 1001;
  }
  .no-data {
    padding-top: 200rpx;
  }
</style>