zhangwencui
7 小时以前 c7043051efed6648a4ae3d9aceaa70fc34171a58
生产计划下发
已添加2个文件
已修改4个文件
1252 ■■■■ 文件已修改
src/api/productionManagement/productionPlan.js 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages.json 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/inventoryManagement/stockManagement/selectProductModel.vue 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/productionManagement/mainProductionPlan/edit.vue 275 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/productionManagement/mainProductionPlan/index.vue 770 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/productionManagement/mainProductionPlan/issue.vue 142 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/productionManagement/productionPlan.js
@@ -27,3 +27,48 @@
    params: query,
  });
}
// ç”Ÿäº§è®¡åˆ’-新增
export function productionPlanAdd(data) {
  return request({
    url: "/productionPlan/addProductionPlan",
    method: "post",
    data,
  });
}
// ç”Ÿäº§è®¡åˆ’-修改
export function productionPlanUpdate(data) {
  return request({
    url: "/productionPlan/updateProductionPlan",
    method: "put",
    data,
  });
}
// ç”Ÿäº§è®¡åˆ’-删除
export function productionPlanDelete(data) {
  return request({
    url: "/productionPlan/deleteProductionPlan",
    method: "delete",
    data,
  });
}
// åˆå¹¶ä¸‹å‘
export function productionPlanCombine(data) {
  return request({
    url: "/productionPlan/combine",
    method: "post",
    data,
  });
}
// è¿½è¸ªè¿›åº¦
export function trackProgressByNo(query) {
  return request({
    url: "/track/trackProgressByNo",
    method: "get",
    params: query,
  });
}
src/pages.json
@@ -880,6 +880,20 @@
      }
    },
    {
      "path": "pages/productionManagement/mainProductionPlan/edit",
      "style": {
        "navigationBarTitleText": "生产计划",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/productionManagement/mainProductionPlan/issue",
      "style": {
        "navigationBarTitleText": "下发",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/productionManagement/productionScheduling/index",
      "style": {
        "navigationBarTitleText": "生产排产",
src/pages/inventoryManagement/stockManagement/selectProductModel.vue
@@ -93,7 +93,11 @@
      .then(res => {
        const records = res?.records || res?.data?.records || res?.data || [];
        const rows = Array.isArray(records) ? records : [];
        list.value = page.current === 1 ? rows : [...list.value, ...rows];
        const normalized = rows.map(r => {
          const modelId = r?.id ?? r?.productModelId ?? r?.materialModelId;
          return modelId == null ? r : { ...r, id: modelId };
        });
        list.value = page.current === 1 ? normalized : [...list.value, ...normalized];
        total.value = Number(res?.total ?? res?.data?.total ?? list.value.length);
        loadStatus.value = list.value.length >= total.value ? "nomore" : "loadmore";
      })
src/pages/productionManagement/mainProductionPlan/edit.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,275 @@
<template>
  <view class="account-detail">
    <PageHeader :title="pageTitle"
                @back="goBack" />
    <up-form ref="formRef"
             :model="form"
             :rules="rules"
             label-width="110">
      <up-form-item label="主生产计划号"
                    prop="mpsNo">
        <up-input v-model="form.mpsNo"
                  placeholder="新增后自动生成"
                  disabled />
      </up-form-item>
      <up-form-item label="产品"
                    prop="productModelId"
                    required>
        <up-input v-model="productText"
                  placeholder="请选择"
                  readonly
                  @click="goSelectProductModel" />
        <template #right>
          <up-icon name="arrow-right"
                   @click="goSelectProductModel"></up-icon>
        </template>
      </up-form-item>
      <up-form-item label="所需数量"
                    prop="qtyRequired"
                    required>
        <up-input v-model="form.qtyRequired"
                  type="number"
                  placeholder="请输入"
                  clearable />
      </up-form-item>
      <up-form-item label="单位"
                    prop="unit">
        <up-input v-model="form.unit"
                  placeholder="自动填充"
                  disabled />
      </up-form-item>
      <up-form-item label="需求日期"
                    prop="requiredDate"
                    required>
        <up-input v-model="form.requiredDate"
                  placeholder="请选择"
                  readonly
                  @click="showRequiredDatePicker = true" />
        <template #right>
          <up-icon name="arrow-right"
                   @click="showRequiredDatePicker = true"></up-icon>
        </template>
      </up-form-item>
      <up-form-item label="承诺日期"
                    prop="promisedDeliveryDate">
        <up-input v-model="form.promisedDeliveryDate"
                  placeholder="请选择"
                  readonly
                  @click="showPromisedDatePicker = true" />
        <template #right>
          <up-icon name="arrow-right"
                   @click="showPromisedDatePicker = true"></up-icon>
        </template>
      </up-form-item>
      <up-form-item label="备注"
                    prop="remark">
        <up-textarea v-model="form.remark"
                     placeholder="请输入"
                     auto-height />
      </up-form-item>
    </up-form>
    <FooterButtons :loading="loading"
                   confirmText="保存"
                   @cancel="goBack"
                   @confirm="handleSubmit" />
    <up-datetime-picker :show="showRequiredDatePicker"
                        v-model="requiredDateValue"
                        mode="date"
                        @confirm="onRequiredDateConfirm"
                        @cancel="showRequiredDatePicker = false" />
    <up-datetime-picker :show="showPromisedDatePicker"
                        v-model="promisedDateValue"
                        mode="date"
                        @confirm="onPromisedDateConfirm"
                        @cancel="showPromisedDatePicker = false" />
  </view>
</template>
<script setup>
  import { computed, onMounted, ref } from "vue";
  import { onLoad } from "@dcloudio/uni-app";
  import FooterButtons from "@/components/FooterButtons.vue";
  import PageHeader from "@/components/PageHeader.vue";
  import { formatDateToYMD } from "@/utils/ruoyi";
  import {
    productionPlanAdd,
    productionPlanUpdate,
  } from "@/api/productionManagement/productionPlan";
  const formRef = ref();
  const loading = ref(false);
  const editId = ref(undefined);
  const readonly = ref(false);
  const selectedProductRow = ref(null);
  const form = ref({
    id: undefined,
    mpsNo: "",
    productId: undefined,
    productModelId: undefined,
    productName: "",
    model: "",
    qtyRequired: "",
    unit: "",
    requiredDate: "",
    promisedDeliveryDate: "",
    remark: "",
  });
  const rules = {
    qtyRequired: [{ required: true, message: "请输入数量", trigger: "blur" }],
    requiredDate: [
      { required: true, message: "请选择需求日期", trigger: "change" },
    ],
  };
  const pageTitle = computed(() =>
    editId.value ? "编辑生产计划" : "新增生产计划"
  );
  const productText = computed(() => {
    const name = form.value.productName || "";
    const model = form.value.model || "";
    if (!name && !model) return "";
    return model ? `${name} / ${model}` : name;
  });
  const showRequiredDatePicker = ref(false);
  const showPromisedDatePicker = ref(false);
  const requiredDateValue = ref(Date.now());
  const promisedDateValue = ref(Date.now());
  const goBack = () => {
    uni.navigateBack();
  };
  const goSelectProductModel = () => {
    if (readonly.value) return;
    const onSelected = row => {
      selectedProductRow.value = row || null;
      const productId = row?.productId ?? row?.productMainId ?? row?.productID;
      const productModelId =
        row?.id ??
        row?.productModelId ??
        row?.productModelID ??
        row?.materialModelId ??
        row?.materialModelID ??
        row?.modelId ??
        row?.modelID;
      form.value.productId = productId ?? undefined;
      form.value.productModelId = productModelId ?? undefined;
      form.value.productName = row.productName || "";
      form.value.model = row.model || "";
      form.value.unit = row.unit || "方";
    };
    uni.$once("stockManagement:selectedProductModel", onSelected);
    uni.navigateTo({
      url: "/pages/inventoryManagement/stockManagement/selectProductModel",
      events: {
        selected: onSelected,
      },
    });
  };
  const onRequiredDateConfirm = e => {
    const value = e?.value ?? requiredDateValue.value;
    form.value.requiredDate = formatDateToYMD(value);
    showRequiredDatePicker.value = false;
  };
  const onPromisedDateConfirm = e => {
    const value = e?.value ?? promisedDateValue.value;
    form.value.promisedDeliveryDate = formatDateToYMD(value);
    showPromisedDatePicker.value = false;
  };
  const handleSubmit = async () => {
    if (readonly.value) return;
    if (!form.value.productModelId && selectedProductRow.value) {
      const row = selectedProductRow.value;
      const fallback =
        row?.id ??
        row?.productModelId ??
        row?.productModelID ??
        row?.materialModelId ??
        row?.materialModelID ??
        row?.modelId ??
        row?.modelID;
      if (fallback != null) form.value.productModelId = fallback;
    }
    if (!form.value.productModelId) {
      uni.showToast({ title: "请选择产品", icon: "none" });
      return;
    }
    const valid = await formRef.value.validate().catch(() => false);
    if (!valid) return;
    const qty = Number(form.value.qtyRequired);
    if (!qty || qty <= 0) {
      uni.showToast({ title: "数量必须大于0", icon: "none" });
      return;
    }
    loading.value = true;
    const payload = { ...form.value, qtyRequired: qty };
    const action = editId.value ? productionPlanUpdate : productionPlanAdd;
    if (!editId.value) payload.id = null;
    action(payload)
      .then(() => {
        uni.showToast({ title: "保存成功", icon: "success" });
        uni.$emit("mainProductionPlan:refresh");
        goBack();
      })
      .catch(err => {
        const msg =
          err?.msg || err?.message || err?.data?.msg || err?.data?.message;
        uni.showToast({ title: msg ? String(msg) : "保存失败", icon: "none" });
      })
      .finally(() => {
        loading.value = false;
      });
  };
  const initFromRow = row => {
    if (!row || typeof row !== "object") return;
    editId.value = row.id;
    readonly.value = Boolean(row.source === "销售" || String(row.status) !== "0");
    form.value = {
      ...form.value,
      id: row.id,
      mpsNo: row.mpsNo || "",
      productId: row.productId ?? undefined,
      productModelId: row.productModelId ?? undefined,
      productName: row.productName || "",
      model: row.model || "",
      qtyRequired: String(row.qtyRequired ?? ""),
      unit: row.unit || "方",
      requiredDate: row.requiredDate ? formatDateToYMD(row.requiredDate) : "",
      promisedDeliveryDate: row.promisedDeliveryDate
        ? formatDateToYMD(row.promisedDeliveryDate)
        : "",
      remark: row.remark || "",
    };
  };
  onLoad(options => {
    if (options?.data) {
      try {
        const row = JSON.parse(decodeURIComponent(options.data));
        initFromRow(row);
      } catch {
        uni.showToast({ title: "数据加载失败", icon: "none" });
      }
    }
  });
  onMounted(() => {
    if (!editId.value) {
      form.value.requiredDate = formatDateToYMD(Date.now());
    }
  });
</script>
<style scoped lang="scss">
  @import "@/static/scss/form-common.scss";
</style>
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>
src/pages/productionManagement/mainProductionPlan/issue.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,142 @@
<template>
  <view class="account-detail">
    <PageHeader title="下发" @back="goBack" />
    <up-form ref="formRef" :model="form" :rules="rules" label-width="110">
      <up-form-item label="产品名称">
        <up-input v-model="form.productName" disabled placeholder="自动填充" />
      </up-form-item>
      <up-form-item label="规格型号">
        <up-input v-model="form.model" disabled placeholder="自动填充" />
      </up-form-item>
      <up-form-item label="计划完成时间" prop="planCompleteTime" required>
        <up-input v-model="form.planCompleteTime" placeholder="请选择" readonly @click="showPlanDatePicker = true" />
        <template #right>
          <up-icon name="arrow-right" @click="showPlanDatePicker = true"></up-icon>
        </template>
      </up-form-item>
      <up-form-item label="生产数量" prop="totalAssignedQuantity" required>
        <up-input v-model="form.totalAssignedQuantity" type="number" placeholder="请输入" clearable />
      </up-form-item>
      <up-form-item label="可下发数量">
        <up-input :model-value="String(maxQuantity)" disabled placeholder="自动计算" />
      </up-form-item>
    </up-form>
    <FooterButtons :loading="loading" confirmText="确定下发" @cancel="goBack" @confirm="handleSubmit" />
    <up-datetime-picker
      :show="showPlanDatePicker"
      v-model="planDateValue"
      mode="date"
      @confirm="onPlanDateConfirm"
      @cancel="showPlanDatePicker = false"
    />
  </view>
</template>
<script setup>
  import { ref } from "vue";
  import { onLoad } from "@dcloudio/uni-app";
  import FooterButtons from "@/components/FooterButtons.vue";
  import PageHeader from "@/components/PageHeader.vue";
  import { formatDateToYMD } from "@/utils/ruoyi";
  import { productionPlanCombine } from "@/api/productionManagement/productionPlan";
  const formRef = ref();
  const loading = ref(false);
  const maxQuantity = ref(0);
  const showPlanDatePicker = ref(false);
  const planDateValue = ref(Date.now());
  const form = ref({
    productName: "",
    model: "",
    totalAssignedQuantity: "",
    planCompleteTime: "",
    productId: undefined,
    ids: [],
  });
  const rules = {
    planCompleteTime: [{ required: true, message: "请选择计划完成时间", trigger: "change" }],
    totalAssignedQuantity: [{ required: true, message: "请输入生产数量", trigger: "blur" }],
  };
  const goBack = () => {
    uni.navigateBack();
  };
  const onPlanDateConfirm = e => {
    const value = e?.value ?? planDateValue.value;
    form.value.planCompleteTime = formatDateToYMD(value);
    showPlanDatePicker.value = false;
  };
  const handleSubmit = async () => {
    const valid = await formRef.value.validate().catch(() => false);
    if (!valid) return;
    const qty = Number(form.value.totalAssignedQuantity);
    if (!qty || qty <= 0) {
      uni.showToast({ title: "生产数量必须大于0", icon: "none" });
      return;
    }
    if (qty > Number(maxQuantity.value || 0)) {
      uni.showToast({ title: "生产数量不能大于可下发数量", icon: "none" });
      return;
    }
    if (!Array.isArray(form.value.ids) || form.value.ids.length === 0) {
      uni.showToast({ title: "请选择要下发的计划", icon: "none" });
      return;
    }
    loading.value = true;
    const payload = {
      ...form.value,
      totalAssignedQuantity: Number(qty.toFixed(4)),
    };
    productionPlanCombine(payload)
      .then(res => {
        if (res?.code && Number(res.code) !== 200) {
          const msg = res?.msg || res?.message || "下发失败";
          uni.showToast({ title: String(msg), icon: "none" });
          return;
        }
        uni.showToast({ title: "下发成功", icon: "success" });
        uni.$emit("mainProductionPlan:refresh");
        goBack();
      })
      .catch(err => {
        const msg = err?.msg || err?.message || err?.data?.msg || err?.data?.message;
        uni.showToast({ title: msg ? String(msg) : "下发失败", icon: "none" });
      })
      .finally(() => {
        loading.value = false;
      });
  };
  onLoad(options => {
    if (!options?.data) return;
    try {
      const payload = JSON.parse(decodeURIComponent(options.data));
      form.value.productName = payload.productName || "";
      form.value.model = payload.model || "";
      form.value.productId = payload.productId ?? undefined;
      form.value.ids = Array.isArray(payload.ids) ? payload.ids : [];
      form.value.planCompleteTime = payload.planCompleteTime || "";
      form.value.totalAssignedQuantity = String(payload.totalAssignedQuantity ?? "");
      maxQuantity.value = Number(payload.maxQuantity ?? 0);
      if (!form.value.planCompleteTime) {
        form.value.planCompleteTime = formatDateToYMD(Date.now());
      }
    } catch {
      uni.showToast({ title: "数据加载失败", icon: "none" });
    }
  });
</script>
<style scoped lang="scss">
  @import "@/static/scss/form-common.scss";
</style>