ZN
9 天以前 ea34f049755d89386a342f0eceb58a6bc4b23b91
feat: 新增采购退货单与供应商管理功能

- 在采购管理模块中增加采购退货单功能,包括列表、新增、详情、删除及选择产品页面
- 在基础数据模块中增加供应商管理功能,包括列表、新增、编辑、删除供应商信息
- 更新菜单配置和工作台,添加采购退货和供应商档案入口
- 新增采购退货单相关API接口和供应商管理API调用
已添加8个文件
已修改3个文件
1832 ■■■■■ 文件已修改
.vscode/settings.json 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/procurementManagement/purchaseReturnOrder.js 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages.json 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/basicData/supplierManage/edit.vue 216 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/basicData/supplierManage/index.vue 197 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/indexItem.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/procurementManagement/purchaseReturnOrder/add.vue 758 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/procurementManagement/purchaseReturnOrder/index.vue 185 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/procurementManagement/purchaseReturnOrder/productList.vue 163 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/procurementManagement/purchaseReturnOrder/view.vue 202 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/works.vue 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.vscode/settings.json
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,15 @@
{
    "i18n-ally.localesPaths": [
        "src/pages_template/common/locales",
        "src/uni_modules/uni-table/i18n",
        "src/uni_modules/uni-countdown/components/uni-countdown/i18n",
        "src/uni_modules/uni-calendar/components/uni-calendar/i18n",
        "src/uni_modules/uni-datetime-picker/components/uni-datetime-picker/i18n",
        "src/uni_modules/uni-fav/components/uni-fav/i18n",
        "src/uni_modules/uni-goods-nav/components/uni-goods-nav/i18n",
        "src/uni_modules/uni-load-more/components/uni-load-more/i18n",
        "src/uni_modules/uni-popup/components/uni-popup/i18n",
        "src/uni_modules/uni-pagination/components/uni-pagination/i18n",
        "src/uni_modules/uni-search-bar/components/uni-search-bar/i18n"
    ]
}
src/api/procurementManagement/purchaseReturnOrder.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,31 @@
import request from "@/utils/request";
export function findPurchaseReturnOrderListPage(query) {
  return request({
    url: "/purchaseReturnOrders/listPage",
    method: "get",
    params: query,
  });
}
export function createPurchaseReturnOrder(data) {
  return request({
    url: "/purchaseReturnOrders/add",
    method: "post",
    data,
  });
}
export function getPurchaseReturnOrderDetail(id) {
  return request({
    url: "/purchaseReturnOrders/selectById/" + id,
    method: "get",
  });
}
export function deletePurchaseReturnOrder(id) {
  return request({
    url: "/purchaseReturnOrders/deleteById/" + id,
    method: "post",
  });
}
src/pages.json
@@ -262,6 +262,48 @@
      }
    },
    {
      "path": "pages/procurementManagement/purchaseReturnOrder/index",
      "style": {
        "navigationBarTitleText": "采购退货单",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/procurementManagement/purchaseReturnOrder/add",
      "style": {
        "navigationBarTitleText": "新增采购退货",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/procurementManagement/purchaseReturnOrder/view",
      "style": {
        "navigationBarTitleText": "采购退货详情",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/procurementManagement/purchaseReturnOrder/productList",
      "style": {
        "navigationBarTitleText": "选择产品",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/basicData/supplierManage/index",
      "style": {
        "navigationBarTitleText": "供应商管理",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/basicData/supplierManage/edit",
      "style": {
        "navigationBarTitleText": "供应商信息",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/cooperativeOffice/collaborativeApproval/index1",
      "style": {
        "navigationBarTitleText": "公出管理",
src/pages/basicData/supplierManage/edit.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,216 @@
<template>
  <view class="account-detail">
    <PageHeader :title="pageTitle" @back="goBack" />
    <up-form ref="formRef" :model="form" :rules="rules" label-width="120">
      <u-cell-group title="供应商信息" class="form-section">
        <up-form-item label="供应商名称" prop="supplierName" required>
          <up-input v-model="form.supplierName" placeholder="请输入" clearable />
        </up-form-item>
        <up-form-item label="纳税人识别号" prop="taxpayerIdentificationNum" required>
          <up-input v-model="form.taxpayerIdentificationNum" placeholder="请输入" clearable />
        </up-form-item>
        <up-form-item label="公司地址" prop="companyAddress" required>
          <up-input v-model="form.companyAddress" placeholder="请输入" clearable />
        </up-form-item>
        <up-form-item label="公司电话" prop="companyPhone" required>
          <up-input v-model="form.companyPhone" placeholder="请输入" clearable />
        </up-form-item>
        <up-form-item label="开户行" prop="bankAccountName" required>
          <up-input v-model="form.bankAccountName" placeholder="请输入" clearable />
        </up-form-item>
        <up-form-item label="账号" prop="bankAccountNum" required>
          <up-input v-model="form.bankAccountNum" placeholder="请输入" clearable />
        </up-form-item>
        <up-form-item label="联系人" prop="contactUserName">
          <up-input v-model="form.contactUserName" placeholder="请输入" clearable />
        </up-form-item>
        <up-form-item label="联系电话" prop="contactUserPhone">
          <up-input v-model="form.contactUserPhone" placeholder="请输入" clearable />
        </up-form-item>
        <up-form-item label="供应商类型" prop="supplierType" required>
          <up-input
            v-model="supplierTypeText"
            placeholder="请选择"
            readonly
            @click="showSupplierTypeSheet = true"
          />
          <template #right>
            <up-icon name="arrow-right" @click="showSupplierTypeSheet = true"></up-icon>
          </template>
        </up-form-item>
        <up-form-item label="是否白名单" prop="isWhite" required>
          <up-input v-model="isWhiteText" placeholder="请选择" readonly @click="showIsWhiteSheet = true" />
          <template #right>
            <up-icon name="arrow-right" @click="showIsWhiteSheet = true"></up-icon>
          </template>
        </up-form-item>
        <up-form-item label="维护人" prop="maintainUserName">
          <up-input v-model="form.maintainUserName" disabled placeholder="自动填充" />
        </up-form-item>
        <up-form-item label="维护时间" prop="maintainTime">
          <up-input v-model="form.maintainTime" disabled placeholder="自动填充" />
        </up-form-item>
      </u-cell-group>
    </up-form>
    <FooterButtons :loading="loading" confirmText="保存" @cancel="goBack" @confirm="handleSubmit" />
    <up-action-sheet
      :show="showSupplierTypeSheet"
      title="选择供应商类型"
      :actions="supplierTypeActions"
      @select="onSelectSupplierType"
      @close="showSupplierTypeSheet = false"
    />
    <up-action-sheet
      :show="showIsWhiteSheet"
      title="选择白名单"
      :actions="isWhiteActions"
      @select="onSelectIsWhite"
      @close="showIsWhiteSheet = false"
    />
  </view>
</template>
<script setup>
  import { computed, onMounted, ref } from "vue";
  import { onLoad } from "@dcloudio/uni-app";
  import FooterButtons from "@/components/FooterButtons.vue";
  import useUserStore from "@/store/modules/user";
  import { formatDateToYMD } from "@/utils/ruoyi";
  import { addSupplier, getSupplier, updateSupplier } from "@/api/basicData/supplierManageFile";
  const userStore = useUserStore();
  const formRef = ref();
  const loading = ref(false);
  const supplierId = ref(undefined);
  const form = ref({
    supplierName: "",
    taxpayerIdentificationNum: "",
    companyAddress: "",
    companyPhone: "",
    bankAccountName: "",
    bankAccountNum: "",
    contactUserName: "",
    contactUserPhone: "",
    maintainUserId: "",
    maintainUserName: "",
    maintainTime: "",
    supplierType: "",
    isWhite: 0,
  });
  const rules = {
    supplierName: [{ required: true, message: "请输入供应商名称", trigger: "blur" }],
    taxpayerIdentificationNum: [{ required: true, message: "请输入纳税人识别号", trigger: "blur" }],
    companyAddress: [{ required: true, message: "请输入公司地址", trigger: "blur" }],
    companyPhone: [{ required: true, message: "请输入公司电话", trigger: "blur" }],
    bankAccountName: [{ required: true, message: "请输入开户行", trigger: "blur" }],
    bankAccountNum: [{ required: true, message: "请输入账号", trigger: "blur" }],
    supplierType: [{ required: true, message: "请选择供应商类型", trigger: "change" }],
    isWhite: [{ required: true, message: "请选择白名单", trigger: "change" }],
  };
  const pageTitle = computed(() => {
    return supplierId.value ? "编辑供应商" : "新增供应商";
  });
  const supplierTypeActions = [
    { name: "甲", value: "甲" },
    { name: "乙", value: "乙" },
    { name: "丙", value: "丙" },
    { name: "丁", value: "丁" },
  ];
  const isWhiteActions = [
    { name: "是", value: 0 },
    { name: "否", value: 1 },
  ];
  const showSupplierTypeSheet = ref(false);
  const showIsWhiteSheet = ref(false);
  const supplierTypeText = computed(() => form.value.supplierType || "");
  const isWhiteText = computed(() => {
    return String(form.value.isWhite) === "0" ? "是" : "否";
  });
  const onSelectSupplierType = action => {
    form.value.supplierType = action.value;
    showSupplierTypeSheet.value = false;
  };
  const onSelectIsWhite = action => {
    form.value.isWhite = action.value;
    showIsWhiteSheet.value = false;
  };
  const initForAdd = () => {
    form.value.maintainUserId = userStore.id;
    form.value.maintainUserName = userStore.nickName;
    form.value.maintainTime = formatDateToYMD(Date.now());
    form.value.isWhite = 0;
  };
  const loadDetail = () => {
    if (!supplierId.value) return;
    uni.showLoading({ title: "加载中...", mask: true });
    getSupplier(supplierId.value)
      .then(res => {
        form.value = { ...form.value, ...(res.data || {}) };
      })
      .catch(() => {
        uni.showToast({ title: "获取详情失败", icon: "error" });
      })
      .finally(() => {
        uni.hideLoading();
      });
  };
  const goBack = () => {
    uni.navigateBack();
  };
  const handleSubmit = async () => {
    const valid = await formRef.value.validate().catch(() => false);
    if (!valid) return;
    loading.value = true;
    const action = supplierId.value ? updateSupplier : addSupplier;
    action({ ...form.value, id: supplierId.value })
      .then(() => {
        uni.showToast({ title: "保存成功", icon: "success" });
        uni.navigateBack();
      })
      .catch(() => {
        uni.showToast({ title: "保存失败", icon: "error" });
      })
      .finally(() => {
        loading.value = false;
      });
  };
  onMounted(() => {
    userStore.getInfo();
    initForAdd();
  });
  onLoad(options => {
    if (options?.id) supplierId.value = options.id;
    if (supplierId.value) loadDetail();
  });
</script>
<style scoped lang="scss">
  @import "@/styles/procurement-common.scss";
  .account-detail {
    min-height: 100vh;
    background: #f8f9fa;
    padding-bottom: 90px;
  }
  .form-section {
    margin: 12px;
    border-radius: 12px;
    overflow: hidden;
  }
</style>
src/pages/basicData/supplierManage/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,197 @@
<template>
  <view class="sales-account">
    <PageHeader title="供应商管理" @back="goBack">
      <template #right>
        <up-button
          type="primary"
          size="small"
          text="新增"
          :customStyle="{ marginRight: '12px' }"
          @click="goAdd"
        />
      </template>
    </PageHeader>
    <view class="search-section">
      <view class="search-bar">
        <view class="search-input">
          <up-input
            class="search-text"
            placeholder="请输入供应商名称"
            v-model="supplierName"
            @change="getList"
            clearable
          />
        </view>
        <view class="filter-button" @click="getList">
          <up-icon name="search" size="24" color="#999"></up-icon>
        </view>
      </view>
    </view>
    <view class="tabs-section">
      <up-tabs
        v-model="tabValue"
        :list="tabList"
        itemStyle="width: 50%;height: 80rpx;"
        @change="onTabChange"
      >
      </up-tabs>
    </view>
    <view class="ledger-list" v-if="list.length > 0">
      <view v-for="item in list" :key="item.id">
        <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.supplierName || "-" }}</text>
            </view>
          </view>
          <up-divider></up-divider>
          <view class="item-details">
            <view class="detail-row">
              <text class="detail-label">供应商类型</text>
              <text class="detail-value">{{ item.supplierType || "-" }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">纳税人识别号</text>
              <text class="detail-value">{{ item.taxpayerIdentificationNum || "-" }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">公司电话</text>
              <text class="detail-value">{{ item.companyPhone || "-" }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">联系人</text>
              <text class="detail-value">{{ item.contactUserName || "-" }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">联系电话</text>
              <text class="detail-value">{{ item.contactUserPhone || "-" }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">维护人</text>
              <text class="detail-value">{{ item.maintainUserName || "-" }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">维护时间</text>
              <text class="detail-value">{{ item.maintainTime || "-" }}</text>
            </view>
          </view>
          <view class="action-buttons">
            <u-button size="small" class="action-btn" @click="goEdit(item)">编辑</u-button>
            <u-button
              type="error"
              size="small"
              class="action-btn"
              @click="handleDelete(item)"
            >
              åˆ é™¤
            </u-button>
          </view>
        </view>
      </view>
    </view>
    <view v-else class="no-data">
      <text>暂无供应商数据</text>
    </view>
  </view>
</template>
<script setup>
  import { reactive, ref } from "vue";
  import { onShow } from "@dcloudio/uni-app";
  import useUserStore from "@/store/modules/user";
  import { delSupplier, listSupplier } from "@/api/basicData/supplierManageFile";
  const userStore = useUserStore();
  const supplierName = ref("");
  const list = ref([]);
  const tabList = reactive([
    { name: "正常供应商", value: 0 },
    { name: "黑名单", value: 1 },
  ]);
  const tabValue = ref(0);
  const page = {
    current: -1,
    size: -1,
  };
  const goBack = () => {
    uni.navigateBack();
  };
  const goAdd = () => {
    uni.navigateTo({ url: "/pages/basicData/supplierManage/edit" });
  };
  const goEdit = item => {
    uni.navigateTo({ url: `/pages/basicData/supplierManage/edit?id=${item.id}` });
  };
  const onTabChange = val => {
    tabValue.value = val.value;
    getList();
  };
  const getList = () => {
    uni.showLoading({ title: "加载中...", mask: true });
    listSupplier({
      ...page,
      supplierName: supplierName.value,
      isWhite: tabValue.value,
    })
      .then(res => {
        list.value = res?.data?.records || [];
      })
      .catch(() => {
        uni.showToast({ title: "查询失败", icon: "error" });
      })
      .finally(() => {
        uni.hideLoading();
      });
  };
  const handleDelete = item => {
    if (!item?.id) return;
    if (item.maintainUserName && item.maintainUserName !== userStore.nickName) {
      uni.showToast({ title: "不可删除他人维护的数据", icon: "none" });
      return;
    }
    uni.showModal({
      title: "删除提示",
      content: "确定要删除吗?删除后无法恢复",
      success: res => {
        if (!res.confirm) return;
        uni.showLoading({ title: "删除中...", mask: true });
        delSupplier([item.id])
          .then(() => {
            uni.showToast({ title: "删除成功", icon: "success" });
            getList();
          })
          .catch(() => {
            uni.showToast({ title: "删除失败", icon: "error" });
          })
          .finally(() => {
            uni.hideLoading();
          });
      },
    });
  };
  onShow(() => {
    userStore.getInfo();
    getList();
  });
</script>
<style scoped lang="scss">
  @import "@/styles/procurement-common.scss";
  .tabs-section {
    background: #fff;
    padding: 0 12px 8px 12px;
  }
</style>
src/pages/indexItem.vue
@@ -49,6 +49,8 @@
    "付款登记": "/pages/procurementManagement/paymentEntry/index",
    "付款流水": "/pages/procurementManagement/receiptPaymentHistory/index",
    "供应商往来": "/pages/procurementManagement/paymentLedger/index",
    "采购退货单": "/pages/procurementManagement/purchaseReturnOrder/index",
    "供应商管理": "/pages/basicData/supplierManage/index",
    "公出管理": "/pages/cooperativeOffice/collaborativeApproval/index1",
    "请假管理": "/pages/cooperativeOffice/collaborativeApproval/index2",
    "出差管理": "/pages/cooperativeOffice/collaborativeApproval/index3",
src/pages/procurementManagement/purchaseReturnOrder/add.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,758 @@
<template>
  <view class="account-detail">
    <PageHeader title="新增采购退货" @back="goBack" />
    <up-form ref="formRef" :model="form" :rules="rules" label-width="120">
      <u-cell-group title="基本信息" class="form-section">
        <up-form-item label="退料单号" prop="no">
          <up-input
            v-model="form.no"
            :disabled="form.isDefaultNo"
            :placeholder="form.isDefaultNo ? '使用系统编号' : '请输入退料单号'"
            clearable
          />
          <template #right>
            <up-switch v-model="form.isDefaultNo" @change="onDefaultNoChange" />
          </template>
        </up-form-item>
        <up-form-item label="退货方式" prop="returnType" required>
          <up-input
            v-model="returnTypeText"
            placeholder="请选择"
            readonly
            @click="showReturnTypeSheet = true"
          />
          <template #right>
            <up-icon name="arrow-right" @click="showReturnTypeSheet = true"></up-icon>
          </template>
        </up-form-item>
        <up-form-item label="供应商" prop="supplierId" required>
          <up-input
            v-model="supplierText"
            placeholder="请选择"
            readonly
            @click="showSupplierSheet = true"
          />
          <template #right>
            <up-icon name="arrow-right" @click="showSupplierSheet = true"></up-icon>
          </template>
        </up-form-item>
        <up-form-item label="项目阶段" prop="projectPhase">
          <up-input
            v-model="projectPhaseText"
            placeholder="请选择"
            readonly
            @click="showProjectPhaseSheet = true"
          />
          <template #right>
            <up-icon name="arrow-right" @click="showProjectPhaseSheet = true"></up-icon>
          </template>
        </up-form-item>
        <up-form-item label="制作日期" prop="preparedAt" required>
          <up-input
            v-model="form.preparedAt"
            placeholder="请选择"
            readonly
            @click="showPreparedAtPicker = true"
          />
          <template #right>
            <up-icon name="arrow-right" @click="showPreparedAtPicker = true"></up-icon>
          </template>
        </up-form-item>
        <up-form-item label="制单人" prop="preparedUserId" required>
          <up-input
            v-model="preparedUserText"
            placeholder="请选择"
            readonly
            @click="showPreparedUserSheet = true"
          />
          <template #right>
            <up-icon name="arrow-right" @click="showPreparedUserSheet = true"></up-icon>
          </template>
        </up-form-item>
        <up-form-item label="退料人" prop="returnUserId" required>
          <up-input
            v-model="returnUserText"
            placeholder="请选择"
            readonly
            @click="showReturnUserSheet = true"
          />
          <template #right>
            <up-icon name="arrow-right" @click="showReturnUserSheet = true"></up-icon>
          </template>
        </up-form-item>
        <up-form-item label="采购合同号" prop="purchaseLedgerId" required>
          <up-input
            v-model="purchaseContractText"
            placeholder="请选择"
            readonly
            @click="showPurchaseLedgerSheet = true"
          />
          <template #right>
            <up-icon name="arrow-right" @click="showPurchaseLedgerSheet = 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>
      </u-cell-group>
      <u-cell-group title="产品列表" class="form-section">
        <view class="product-actions">
          <up-button
            type="primary"
            size="small"
            text="选择产品"
            :disabled="!form.purchaseLedgerId"
            @click="goSelectProducts"
          />
          <view class="amount-summary">
            <text class="amount-text">合计:{{ formatAmount(baseAmount) }}</text>
          </view>
        </view>
        <view v-if="form.purchaseReturnOrderProductsDtos.length === 0" class="empty-products">
          <text>暂无产品,请先选择产品</text>
        </view>
        <view v-else class="product-list">
          <view
            v-for="(item, index) in form.purchaseReturnOrderProductsDtos"
            :key="item.salesLedgerProductId || item.id || index"
            class="product-card"
          >
            <view class="product-header">
              <view class="product-title">
                <view class="document-icon">
                  <up-icon name="file-text" size="16" color="#ffffff"></up-icon>
                </view>
                <text class="product-name">产品 {{ index + 1 }}</text>
              </view>
              <up-icon name="trash" size="18" color="#ee0a24" @click="removeProduct(index)" />
            </view>
            <up-divider></up-divider>
            <view class="product-body">
              <view class="detail-row">
                <text class="detail-label">产品大类</text>
                <text class="detail-value">{{ item.productCategory || "-" }}</text>
              </view>
              <view class="detail-row">
                <text class="detail-label">规格型号</text>
                <text class="detail-value">{{ item.specificationModel || "-" }}</text>
              </view>
              <view class="detail-row">
                <text class="detail-label">数量</text>
                <text class="detail-value">{{ item.quantity ?? "-" }}</text>
              </view>
              <view class="detail-row">
                <text class="detail-label">含税单价(元)</text>
                <text class="detail-value">{{ formatAmount(item.taxInclusiveUnitPrice) }}</text>
              </view>
              <view class="qty-row">
                <text class="qty-label">退货数量</text>
                <up-number-box
                  v-model="item.returnQuantity"
                  :min="0"
                  :max="getReturnQtyMax(item)"
                  :step="1"
                  @change="syncRowTotal(item)"
                />
              </view>
              <view class="detail-row">
                <text class="detail-label">退货总价(元)</text>
                <text class="detail-value highlight">{{ formatAmount(item.taxInclusiveTotalPrice) }}</text>
              </view>
            </view>
          </view>
        </view>
      </u-cell-group>
      <u-cell-group title="费用信息" class="form-section">
        <up-form-item label="整单折扣额" prop="totalDiscountAmount">
          <up-input
            v-model="form.totalDiscountAmount"
            type="number"
            placeholder="请输入"
            @blur="onDiscountAmountBlur"
            clearable
          />
        </up-form-item>
        <up-form-item label="整单折扣率(%)" prop="totalDiscountRate">
          <up-input
            v-model="form.totalDiscountRate"
            type="number"
            placeholder="请输入"
            @blur="onDiscountRateBlur"
            clearable
          />
        </up-form-item>
        <up-form-item label="成交金额" prop="totalAmount" required>
          <up-input v-model="form.totalAmount" disabled placeholder="自动计算" />
        </up-form-item>
        <up-form-item label="收款方式" prop="incomeType" required>
          <up-input
            v-model="incomeTypeText"
            placeholder="请选择"
            readonly
            @click="showIncomeTypeSheet = true"
          />
          <template #right>
            <up-icon name="arrow-right" @click="showIncomeTypeSheet = true"></up-icon>
          </template>
        </up-form-item>
      </u-cell-group>
    </up-form>
    <FooterButtons :loading="loading" confirmText="提交" @cancel="goBack" @confirm="handleSubmit" />
    <up-action-sheet
      :show="showReturnTypeSheet"
      title="选择退货方式"
      :actions="returnTypeActions"
      @select="onSelectReturnType"
      @close="showReturnTypeSheet = false"
    />
    <up-action-sheet
      :show="showProjectPhaseSheet"
      title="选择项目阶段"
      :actions="projectPhaseActions"
      @select="onSelectProjectPhase"
      @close="showProjectPhaseSheet = false"
    />
    <up-action-sheet
      :show="showSupplierSheet"
      title="选择供应商"
      :actions="supplierActions"
      @select="onSelectSupplier"
      @close="showSupplierSheet = false"
    />
    <up-action-sheet
      :show="showPreparedUserSheet"
      title="选择制单人"
      :actions="userActions"
      @select="onSelectPreparedUser"
      @close="showPreparedUserSheet = false"
    />
    <up-action-sheet
      :show="showReturnUserSheet"
      title="选择退料人"
      :actions="userActions"
      @select="onSelectReturnUser"
      @close="showReturnUserSheet = false"
    />
    <up-action-sheet
      :show="showPurchaseLedgerSheet"
      title="选择采购合同号"
      :actions="purchaseLedgerActions"
      @select="onSelectPurchaseLedger"
      @close="showPurchaseLedgerSheet = false"
    />
    <up-action-sheet
      :show="showIncomeTypeSheet"
      title="选择收款方式"
      :actions="incomeTypeActions"
      @select="onSelectIncomeType"
      @close="showIncomeTypeSheet = false"
    />
    <up-datetime-picker
      :show="showPreparedAtPicker"
      v-model="preparedAtPickerValue"
      mode="date"
      @confirm="onPreparedAtConfirm"
      @cancel="showPreparedAtPicker = false"
    />
  </view>
</template>
<script setup>
  import { computed, onMounted, ref, watch } from "vue";
  import { onShow } from "@dcloudio/uni-app";
  import FooterButtons from "@/components/FooterButtons.vue";
  import { formatDateToYMD } from "@/utils/ruoyi";
  import { createPurchaseReturnOrder } from "@/api/procurementManagement/purchaseReturnOrder";
  import { getOptions, purchaseListPage, productList } from "@/api/procurementManagement/procurementLedger";
  import { userListNoPageByTenantId } from "@/api/system/user";
  const formRef = ref();
  const loading = ref(false);
  const form = ref({
    no: "",
    isDefaultNo: true,
    returnType: 0,
    supplierId: undefined,
    supplierName: "",
    projectPhase: undefined,
    preparedAt: "",
    preparedUserId: undefined,
    preparedUserName: "",
    returnUserId: undefined,
    returnUserName: "",
    purchaseLedgerId: undefined,
    purchaseContractNumber: "",
    remark: "",
    totalDiscountAmount: 0,
    totalDiscountRate: "",
    totalAmount: 0,
    incomeType: undefined,
    purchaseReturnOrderProductsDtos: [],
  });
  const rules = {
    returnType: [{ required: true, message: "请选择退货方式", trigger: "change" }],
    supplierId: [{ required: true, message: "请选择供应商", trigger: "change" }],
    preparedAt: [{ required: true, message: "请选择制作日期", trigger: "change" }],
    preparedUserId: [{ required: true, message: "请选择制单人", trigger: "change" }],
    returnUserId: [{ required: true, message: "请选择退料人", trigger: "change" }],
    purchaseLedgerId: [{ required: true, message: "请选择采购合同号", trigger: "change" }],
    totalAmount: [{ required: true, message: "成交金额不能为空", trigger: "change" }],
    incomeType: [{ required: true, message: "请选择收款方式", trigger: "change" }],
  };
  const showReturnTypeSheet = ref(false);
  const showProjectPhaseSheet = ref(false);
  const showSupplierSheet = ref(false);
  const showPreparedUserSheet = ref(false);
  const showReturnUserSheet = ref(false);
  const showPurchaseLedgerSheet = ref(false);
  const showIncomeTypeSheet = ref(false);
  const showPreparedAtPicker = ref(false);
  const preparedAtPickerValue = ref(Date.now());
  const supplierOptions = ref([]);
  const userOptions = ref([]);
  const purchaseLedgerOptions = ref([]);
  const returnTypeActions = [
    { name: "退货退款", value: 0 },
    { name: "拒收", value: 1 },
  ];
  const projectPhaseActions = [
    { name: "立项", value: 0 },
    { name: "设计", value: 1 },
    { name: "采购", value: 2 },
    { name: "生产", value: 3 },
    { name: "出货", value: 4 },
  ];
  const incomeTypeActions = [
    { name: "现金", value: "0" },
    { name: "支票", value: "1" },
    { name: "银行转账", value: "2" },
    { name: "其他", value: "3" },
  ];
  const returnTypeText = computed(() => {
    return returnTypeActions.find(i => String(i.value) === String(form.value.returnType))?.name || "";
  });
  const projectPhaseText = computed(() => {
    return projectPhaseActions.find(i => String(i.value) === String(form.value.projectPhase))?.name || "";
  });
  const supplierText = computed(() => {
    return supplierOptions.value.find(i => String(i.id) === String(form.value.supplierId))?.supplierName || "";
  });
  const preparedUserText = computed(() => {
    return userOptions.value.find(i => String(i.userId) === String(form.value.preparedUserId))?.nickName || "";
  });
  const returnUserText = computed(() => {
    return userOptions.value.find(i => String(i.userId) === String(form.value.returnUserId))?.nickName || "";
  });
  const purchaseContractText = computed(() => {
    return purchaseLedgerOptions.value.find(i => String(i.id) === String(form.value.purchaseLedgerId))?.purchaseContractNumber || "";
  });
  const incomeTypeText = computed(() => {
    return incomeTypeActions.find(i => String(i.value) === String(form.value.incomeType))?.name || "";
  });
  const supplierActions = computed(() => {
    return supplierOptions.value.map(i => ({ name: i.supplierName, value: i.id }));
  });
  const userActions = computed(() => {
    return userOptions.value.map(i => ({ name: i.nickName, value: i.userId }));
  });
  const purchaseLedgerActions = computed(() => {
    return purchaseLedgerOptions.value.map(i => ({ name: i.purchaseContractNumber, value: i.id }));
  });
  const toNumber = val => {
    const num = Number(val);
    return Number.isNaN(num) ? 0 : num;
  };
  const formatAmount = value => {
    if (value === null || value === undefined || value === "") return "0.00";
    const num = Number(value);
    if (Number.isNaN(num)) return "0.00";
    return num.toFixed(2);
  };
  const baseAmount = computed(() => {
    const rows = form.value.purchaseReturnOrderProductsDtos || [];
    return rows.reduce((sum, item) => sum + toNumber(item.taxInclusiveTotalPrice), 0);
  });
  const syncTotalAmount = () => {
    const total = baseAmount.value - toNumber(form.value.totalDiscountAmount);
    form.value.totalAmount = Number(total.toFixed(2));
  };
  const getReturnQtyMax = row => {
    const qty = Number(row?.quantity);
    if (Number.isNaN(qty) || qty < 0) return 0;
    return qty;
  };
  const syncRowTotal = row => {
    if (!row) return;
    const qty = toNumber(row.returnQuantity);
    const unitPrice = toNumber(row.taxInclusiveUnitPrice);
    row.taxInclusiveTotalPrice = Number((qty * unitPrice).toFixed(2));
    syncTotalAmount();
  };
  const removeProduct = index => {
    form.value.purchaseReturnOrderProductsDtos.splice(index, 1);
    syncTotalAmount();
  };
  const resetFeeInfo = () => {
    form.value.totalDiscountAmount = 0;
    form.value.totalDiscountRate = "";
    form.value.totalAmount = 0;
    form.value.incomeType = undefined;
  };
  const onDefaultNoChange = checked => {
    if (checked) form.value.no = "";
  };
  const onSelectReturnType = action => {
    form.value.returnType = action.value;
    showReturnTypeSheet.value = false;
  };
  const onSelectProjectPhase = action => {
    form.value.projectPhase = action.value;
    showProjectPhaseSheet.value = false;
  };
  const onSelectSupplier = action => {
    form.value.supplierId = action.value;
    form.value.supplierName = supplierOptions.value.find(i => String(i.id) === String(action.value))?.supplierName || "";
    form.value.purchaseLedgerId = undefined;
    form.value.purchaseContractNumber = "";
    form.value.purchaseReturnOrderProductsDtos = [];
    resetFeeInfo();
    showSupplierSheet.value = false;
    fetchPurchaseLedgerOptions();
  };
  const onSelectPreparedUser = action => {
    form.value.preparedUserId = action.value;
    form.value.preparedUserName = userOptions.value.find(i => String(i.userId) === String(action.value))?.nickName || "";
    showPreparedUserSheet.value = false;
  };
  const onSelectReturnUser = action => {
    form.value.returnUserId = action.value;
    form.value.returnUserName = userOptions.value.find(i => String(i.userId) === String(action.value))?.nickName || "";
    showReturnUserSheet.value = false;
  };
  const onSelectPurchaseLedger = action => {
    form.value.purchaseLedgerId = action.value;
    form.value.purchaseContractNumber =
      purchaseLedgerOptions.value.find(i => String(i.id) === String(action.value))?.purchaseContractNumber || "";
    form.value.purchaseReturnOrderProductsDtos = [];
    resetFeeInfo();
    showPurchaseLedgerSheet.value = false;
  };
  const onSelectIncomeType = action => {
    form.value.incomeType = action.value;
    showIncomeTypeSheet.value = false;
  };
  const onPreparedAtConfirm = e => {
    form.value.preparedAt = formatDateToYMD(e.value);
    showPreparedAtPicker.value = false;
  };
  const onDiscountRateBlur = () => {
    const rate = toNumber(form.value.totalDiscountRate);
    if (rate < 0 || rate > 100) {
      uni.showToast({ title: "折扣率需在0-100", icon: "none" });
      return;
    }
    form.value.totalDiscountAmount = Number((baseAmount.value * (rate / 100)).toFixed(2));
    syncTotalAmount();
  };
  const onDiscountAmountBlur = () => {
    const amount = toNumber(form.value.totalDiscountAmount);
    if (amount < 0) {
      form.value.totalDiscountAmount = 0;
    }
    const base = baseAmount.value;
    if (base <= 0) {
      form.value.totalDiscountRate = "";
      syncTotalAmount();
      return;
    }
    if (toNumber(form.value.totalDiscountAmount) > base) {
      form.value.totalDiscountAmount = Number(base.toFixed(2));
    }
    const rate = (toNumber(form.value.totalDiscountAmount) / base) * 100;
    form.value.totalDiscountRate = Number(rate.toFixed(2));
    syncTotalAmount();
  };
  const goBack = () => {
    uni.removeStorageSync("purchaseReturnOrderSelectedProducts");
    uni.navigateBack();
  };
  const goSelectProducts = () => {
    if (!form.value.purchaseLedgerId) return;
    uni.navigateTo({
      url: `/pages/procurementManagement/purchaseReturnOrder/productList?purchaseLedgerId=${form.value.purchaseLedgerId}`,
    });
  };
  const fetchSupplierOptions = () => {
    getOptions()
      .then(res => {
        supplierOptions.value = res.data || [];
      })
      .catch(() => {
        supplierOptions.value = [];
      });
  };
  const fetchUserOptions = () => {
    userListNoPageByTenantId()
      .then(res => {
        userOptions.value = res.data || [];
      })
      .catch(() => {
        userOptions.value = [];
      });
  };
  const fetchPurchaseLedgerOptions = () => {
    purchaseLedgerOptions.value = [];
    if (!form.value.supplierId) return;
    purchaseListPage({
      current: -1,
      size: -1,
      supplierId: form.value.supplierId,
      approvalStatus: 3,
    })
      .then(res => {
        purchaseLedgerOptions.value = res?.data?.records || [];
      })
      .catch(() => {
        purchaseLedgerOptions.value = [];
      });
  };
  const mergeSelectedProducts = selectedRows => {
    const existing = new Set((form.value.purchaseReturnOrderProductsDtos || []).map(i => String(i.salesLedgerProductId || i.id)));
    const toAdd = (selectedRows || [])
      .filter(i => !existing.has(String(i.id)))
      .map(i => ({
        ...i,
        salesLedgerProductId: i.id,
        returnQuantity: 0,
        taxInclusiveTotalPrice: 0,
      }));
    form.value.purchaseReturnOrderProductsDtos.push(...toAdd);
    syncTotalAmount();
  };
  const loadProductsFromPurchaseLedger = () => {
    if (!form.value.purchaseLedgerId) return;
    uni.showLoading({ title: "加载产品...", mask: true });
    productList({ salesLedgerId: form.value.purchaseLedgerId, type: 2 })
      .then(res => {
        const rows = res.data || [];
        mergeSelectedProducts(rows);
      })
      .catch(() => {
        uni.showToast({ title: "加载产品失败", icon: "error" });
      })
      .finally(() => {
        uni.hideLoading();
      });
  };
  const validateProducts = () => {
    const rows = form.value.purchaseReturnOrderProductsDtos || [];
    if (rows.length === 0) {
      uni.showToast({ title: "请先选择产品", icon: "none" });
      return false;
    }
    const invalid = rows.findIndex(i => {
      const qty = toNumber(i.returnQuantity);
      if (qty <= 0) return true;
      if (qty > getReturnQtyMax(i)) return true;
      return false;
    });
    if (invalid !== -1) {
      uni.showToast({ title: `第${invalid + 1}行退货数量不合法`, icon: "none" });
      return false;
    }
    return true;
  };
  const handleSubmit = async () => {
    if (!validateProducts()) return;
    const valid = await formRef.value.validate().catch(() => false);
    if (!valid) return;
    loading.value = true;
    const rows = (form.value.purchaseReturnOrderProductsDtos || []).map(i => {
      const cloned = { ...i };
      syncRowTotal(cloned);
      return cloned;
    });
    const payload = {
      ...form.value,
      purchaseReturnOrderProductsDtos: rows.filter(i => toNumber(i.returnQuantity) > 0),
    };
    createPurchaseReturnOrder(payload)
      .then(() => {
        uni.showToast({ title: "提交成功", icon: "success" });
        uni.removeStorageSync("purchaseReturnOrderSelectedProducts");
        uni.navigateBack();
      })
      .catch(() => {
        uni.showToast({ title: "提交失败", icon: "error" });
      })
      .finally(() => {
        loading.value = false;
      });
  };
  watch(
    () => baseAmount.value,
    () => {
      syncTotalAmount();
    }
  );
  onMounted(() => {
    form.value.preparedAt = formatDateToYMD(Date.now());
    preparedAtPickerValue.value = Date.now();
    fetchSupplierOptions();
    fetchUserOptions();
  });
  onShow(() => {
    const stored = uni.getStorageSync("purchaseReturnOrderSelectedProducts");
    if (stored) {
      try {
        const rows = JSON.parse(stored);
        mergeSelectedProducts(rows);
        uni.removeStorageSync("purchaseReturnOrderSelectedProducts");
      } catch {
        uni.removeStorageSync("purchaseReturnOrderSelectedProducts");
      }
    }
  });
  const showAutoLoadModalOnceKey = "purchaseReturnOrderAutoLoadShown";
  watch(
    () => form.value.purchaseLedgerId,
    (val, oldVal) => {
      if (!val || String(val) === String(oldVal)) return;
      if (uni.getStorageSync(showAutoLoadModalOnceKey)) return;
      uni.setStorageSync(showAutoLoadModalOnceKey, "1");
      uni.showModal({
        title: "提示",
        content: "是否自动加载该采购合同下全部产品?",
        success: res => {
          if (res.confirm) loadProductsFromPurchaseLedger();
        },
      });
    }
  );
</script>
<style scoped lang="scss">
  @import "@/styles/procurement-common.scss";
  .account-detail {
    min-height: 100vh;
    background: #f8f9fa;
    padding-bottom: 90px;
  }
  .form-section {
    margin: 12px;
    border-radius: 12px;
    overflow: hidden;
  }
  .product-actions {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 12px 12px 0 12px;
  }
  .amount-summary {
    display: flex;
    align-items: center;
  }
  .amount-text {
    font-size: 14px;
    color: #333;
    font-weight: 600;
  }
  .empty-products {
    padding: 16px 12px;
    color: #999;
    font-size: 14px;
  }
  .product-list {
    padding: 12px;
    display: flex;
    flex-direction: column;
    gap: 12px;
  }
  .product-card {
    background: #fff;
    border-radius: 12px;
    padding: 0 12px;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
  }
  .product-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 12px 0;
  }
  .product-title {
    display: flex;
    align-items: center;
    gap: 8px;
  }
  .product-name {
    font-size: 14px;
    color: #333;
    font-weight: 500;
  }
  .product-body {
    padding: 12px 0;
  }
  .qty-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 8px 0;
  }
  .qty-label {
    font-size: 12px;
    color: #777;
  }
</style>
src/pages/procurementManagement/purchaseReturnOrder/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,185 @@
<template>
  <view class="sales-account">
    <PageHeader title="采购退货单" @back="goBack">
      <template #right>
        <up-button
          type="primary"
          size="small"
          text="新增"
          :customStyle="{ marginRight: '12px' }"
          @click="goAdd"
        />
      </template>
    </PageHeader>
    <view class="search-section">
      <view class="search-bar">
        <view class="search-input">
          <up-input
            class="search-text"
            placeholder="请输入退料单号"
            v-model="searchNo"
            @change="getList"
            clearable
          />
        </view>
        <view class="filter-button" @click="getList">
          <up-icon name="search" size="24" color="#999"></up-icon>
        </view>
      </view>
    </view>
    <view class="ledger-list" v-if="list.length > 0">
      <view v-for="item in list" :key="item.id">
        <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.no || "-" }}</text>
            </view>
          </view>
          <up-divider></up-divider>
          <view class="item-details">
            <view class="detail-row">
              <text class="detail-label">退货方式</text>
              <text class="detail-value">{{ getReturnTypeLabel(item.returnType) }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">供应商名称</text>
              <text class="detail-value">{{ item.supplierName || "-" }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">项目阶段</text>
              <text class="detail-value">{{ getProjectPhaseLabel(item.projectPhase) }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">制作日期</text>
              <text class="detail-value">{{ item.preparedAt || "-" }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">成交金额(元)</text>
              <text class="detail-value highlight">{{ formatAmount(item.totalAmount) }}</text>
            </view>
          </view>
          <view class="action-buttons">
            <u-button size="small" class="action-btn" @click="goView(item)">详情</u-button>
            <u-button
              type="error"
              size="small"
              class="action-btn"
              @click="handleDelete(item)"
            >
              åˆ é™¤
            </u-button>
          </view>
        </view>
      </view>
    </view>
    <view v-else class="no-data">
      <text>暂无采购退货单数据</text>
    </view>
  </view>
</template>
<script setup>
  import { ref } from "vue";
  import { onShow } from "@dcloudio/uni-app";
  import { findPurchaseReturnOrderListPage, deletePurchaseReturnOrder } from "@/api/procurementManagement/purchaseReturnOrder";
  const searchNo = ref("");
  const list = ref([]);
  const page = {
    current: -1,
    size: -1,
  };
  const goBack = () => {
    uni.navigateBack();
  };
  const goAdd = () => {
    uni.navigateTo({
      url: "/pages/procurementManagement/purchaseReturnOrder/add",
    });
  };
  const goView = item => {
    uni.navigateTo({
      url: `/pages/procurementManagement/purchaseReturnOrder/view?id=${item.id}`,
    });
  };
  const getReturnTypeLabel = value => {
    if (String(value) === "0") return "退货退款";
    if (String(value) === "1") return "拒收";
    return "-";
  };
  const getProjectPhaseLabel = value => {
    const map = {
      0: "立项",
      1: "设计",
      2: "采购",
      3: "生产",
      4: "出货",
    };
    const key = String(value);
    return map[key] || "-";
  };
  const formatAmount = value => {
    if (value === null || value === undefined || value === "") return "-";
    const num = Number(value);
    if (Number.isNaN(num)) return "-";
    return num.toFixed(2);
  };
  const getList = () => {
    uni.showLoading({ title: "加载中...", mask: true });
    findPurchaseReturnOrderListPage({
      ...page,
      no: searchNo.value,
    })
      .then(res => {
        list.value = res?.data?.records || [];
      })
      .catch(() => {
        uni.showToast({ title: "查询失败", icon: "error" });
      })
      .finally(() => {
        uni.hideLoading();
      });
  };
  const handleDelete = item => {
    if (!item?.id) return;
    uni.showModal({
      title: "删除提示",
      content: "确定要删除吗?删除后无法恢复",
      success: res => {
        if (!res.confirm) return;
        uni.showLoading({ title: "删除中...", mask: true });
        deletePurchaseReturnOrder(item.id)
          .then(() => {
            uni.showToast({ title: "删除成功", icon: "success" });
            getList();
          })
          .catch(() => {
            uni.showToast({ title: "删除失败", icon: "error" });
          })
          .finally(() => {
            uni.hideLoading();
          });
      },
    });
  };
  onShow(() => {
    getList();
  });
</script>
<style scoped lang="scss">
  @import "@/styles/procurement-common.scss";
</style>
src/pages/procurementManagement/purchaseReturnOrder/productList.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,163 @@
<template>
  <view class="sales-account">
    <PageHeader title="选择产品" @back="goBack" />
    <view class="ledger-list" v-if="list.length > 0">
      <view v-for="item in list" :key="item.id">
        <view class="ledger-item" @click="toggle(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.productCategory || "产品" }}</text>
            </view>
            <view class="item-right">
              <u-tag :type="isSelected(item.id) ? 'success' : 'info'">
                {{ isSelected(item.id) ? "已选" : "未选" }}
              </u-tag>
            </view>
          </view>
          <up-divider></up-divider>
          <view class="item-details">
            <view class="detail-row">
              <text class="detail-label">规格型号</text>
              <text class="detail-value">{{ item.specificationModel || "-" }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">单位</text>
              <text class="detail-value">{{ item.unit || "-" }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">数量</text>
              <text class="detail-value">{{ item.quantity ?? "-" }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">含税单价(元)</text>
              <text class="detail-value highlight">{{ formatAmount(item.taxInclusiveUnitPrice) }}</text>
            </view>
          </view>
        </view>
      </view>
    </view>
    <view v-else class="no-data">
      <text>暂无产品数据</text>
    </view>
    <view class="footer">
      <u-button class="cancel-btn" @click="goBack">取消</u-button>
      <u-button class="save-btn" type="primary" @click="confirm">确认({{ selectedIds.size }})</u-button>
    </view>
  </view>
</template>
<script setup>
  import { onLoad } from "@dcloudio/uni-app";
  import { ref } from "vue";
  import { productList } from "@/api/procurementManagement/procurementLedger";
  const purchaseLedgerId = ref(undefined);
  const list = ref([]);
  const selectedIds = ref(new Set());
  const selectedRows = ref(new Map());
  const formatAmount = value => {
    if (value === null || value === undefined || value === "") return "0.00";
    const num = Number(value);
    if (Number.isNaN(num)) return "0.00";
    return num.toFixed(2);
  };
  const isSelected = id => {
    return selectedIds.value.has(String(id));
  };
  const toggle = item => {
    const key = String(item.id);
    if (selectedIds.value.has(key)) {
      selectedIds.value.delete(key);
      selectedRows.value.delete(key);
    } else {
      selectedIds.value.add(key);
      selectedRows.value.set(key, item);
    }
  };
  const goBack = () => {
    uni.navigateBack();
  };
  const confirm = () => {
    const rows = Array.from(selectedRows.value.values());
    uni.setStorageSync("purchaseReturnOrderSelectedProducts", JSON.stringify(rows));
    uni.navigateBack();
  };
  const loadList = () => {
    if (!purchaseLedgerId.value) return;
    uni.showLoading({ title: "加载中...", mask: true });
    productList({ salesLedgerId: purchaseLedgerId.value, type: 2 })
      .then(res => {
        list.value = res.data || [];
      })
      .catch(() => {
        uni.showToast({ title: "加载失败", icon: "error" });
      })
      .finally(() => {
        uni.hideLoading();
      });
  };
  const initSelectionFromStorage = () => {
    const stored = uni.getStorageSync("purchaseReturnOrderSelectedProducts");
    if (!stored) return;
    try {
      const rows = JSON.parse(stored) || [];
      rows.forEach(r => {
        const key = String(r.id);
        selectedIds.value.add(key);
        selectedRows.value.set(key, r);
      });
    } catch {}
  };
  onLoad(options => {
    if (options?.purchaseLedgerId) {
      purchaseLedgerId.value = options.purchaseLedgerId;
    }
    initSelectionFromStorage();
    loadList();
  });
</script>
<style scoped lang="scss">
  @import "@/styles/procurement-common.scss";
  .footer {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    background: #fff;
    display: flex;
    justify-content: space-around;
    align-items: center;
    padding: 12px 0;
    box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.05);
    z-index: 1000;
  }
  .cancel-btn {
    width: 120px;
    background: #c7c9cc;
    color: #fff;
    border-radius: 40px;
  }
  .save-btn {
    width: 200px;
    border-radius: 40px;
  }
  .sales-account {
    padding-bottom: 90px;
  }
</style>
src/pages/procurementManagement/purchaseReturnOrder/view.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,202 @@
<template>
  <view class="sales-account">
    <PageHeader title="采购退货详情" @back="goBack" />
    <view class="ledger-list" v-if="loaded">
      <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">{{ detail.no || "-" }}</text>
          </view>
        </view>
        <up-divider></up-divider>
        <view class="item-details">
          <view class="detail-row">
            <text class="detail-label">退货方式</text>
            <text class="detail-value">{{ getReturnTypeLabel(detail.returnType) }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">供应商名称</text>
            <text class="detail-value">{{ detail.supplierName || "-" }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">项目阶段</text>
            <text class="detail-value">{{ getProjectPhaseLabel(detail.projectPhase) }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">关联单号</text>
            <text class="detail-value">{{ detail.purchaseContractNumber || "-" }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">制作日期</text>
            <text class="detail-value">{{ detail.preparedAt || "-" }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">制单人</text>
            <text class="detail-value">{{ detail.preparedUserName || "-" }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">退料人</text>
            <text class="detail-value">{{ detail.returnUserName || "-" }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">整单折扣额</text>
            <text class="detail-value">{{ formatAmount(detail.totalDiscountAmount) }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">整单折扣率</text>
            <text class="detail-value">{{ detail.totalDiscountRate ?? "-" }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">成交金额</text>
            <text class="detail-value highlight">{{ formatAmount(detail.totalAmount) }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">备注</text>
            <text class="detail-value">{{ detail.remark || "-" }}</text>
          </view>
        </view>
      </view>
      <view class="section-title">
        <text class="section-text">产品列表</text>
      </view>
      <view v-if="products.length === 0" class="no-data">
        <text>暂无产品数据</text>
      </view>
      <view v-else>
        <view v-for="(p, idx) in products" :key="idx" 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">产品 {{ idx + 1 }}</text>
            </view>
          </view>
          <up-divider></up-divider>
          <view class="item-details">
            <view class="detail-row">
              <text class="detail-label">产品大类</text>
              <text class="detail-value">{{ p.productCategory || "-" }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">规格型号</text>
              <text class="detail-value">{{ p.specificationModel || "-" }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">单位</text>
              <text class="detail-value">{{ p.unit || "-" }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">数量</text>
              <text class="detail-value">{{ p.quantity ?? "-" }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">退货数量</text>
              <text class="detail-value highlight">{{ p.returnQuantity ?? "-" }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">税率(%)</text>
              <text class="detail-value">{{ p.taxRate ?? "-" }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">含税单价(元)</text>
              <text class="detail-value">{{ formatAmount(p.taxInclusiveUnitPrice) }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">含税总价(元)</text>
              <text class="detail-value">{{ formatAmount(p.taxInclusiveTotalPrice) }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">是否质检</text>
              <text class="detail-value">{{ p.isChecked ? "是" : "否" }}</text>
            </view>
          </view>
        </view>
      </view>
    </view>
  </view>
</template>
<script setup>
  import { ref } from "vue";
  import { onLoad } from "@dcloudio/uni-app";
  import { getPurchaseReturnOrderDetail } from "@/api/procurementManagement/purchaseReturnOrder";
  const id = ref(undefined);
  const loaded = ref(false);
  const detail = ref({});
  const products = ref([]);
  const goBack = () => {
    uni.navigateBack();
  };
  const getReturnTypeLabel = value => {
    if (String(value) === "0") return "退货退款";
    if (String(value) === "1") return "拒收";
    return "-";
  };
  const getProjectPhaseLabel = value => {
    const map = {
      0: "立项",
      1: "设计",
      2: "采购",
      3: "生产",
      4: "出货",
    };
    const key = String(value);
    return map[key] || "-";
  };
  const formatAmount = value => {
    if (value === null || value === undefined || value === "") return "-";
    const num = Number(value);
    if (Number.isNaN(num)) return "-";
    return num.toFixed(2);
  };
  const loadDetail = () => {
    if (!id.value) return;
    uni.showLoading({ title: "加载中...", mask: true });
    getPurchaseReturnOrderDetail(id.value)
      .then(res => {
        const payload = res?.data || {};
        detail.value = payload;
        const rows = payload.purchaseReturnOrderProductsDetailVoList || [];
        products.value = rows.map(i => ({ ...i, ...(i.salesLedgerProduct || {}) }));
        loaded.value = true;
      })
      .catch(() => {
        uni.showToast({ title: "获取详情失败", icon: "error" });
      })
      .finally(() => {
        uni.hideLoading();
      });
  };
  onLoad(options => {
    if (options?.id) id.value = options.id;
    loadDetail();
  });
</script>
<style scoped lang="scss">
  @import "@/styles/procurement-common.scss";
  .section-title {
    padding: 0 20px;
    margin: 12px 0 8px 0;
  }
  .section-text {
    font-size: 14px;
    color: #333;
    font-weight: 600;
  }
</style>
src/pages/works.vue
@@ -297,6 +297,14 @@
      icon: "/static/images/icon/gongyingshangwanglai.svg",
      label: "供应商往来",
    },
    {
      icon: "/static/images/icon/caigouguanli.svg",
      label: "采购退货",
    },
    {
      icon: "/static/images/icon/gongchuguanli.svg",
      label: "供应商档案",
    },
  ]);
  // è´¢åŠ¡ç®¡ç†åŠŸèƒ½æ•°æ®
@@ -547,6 +555,16 @@
      case "供应商往来":
        uni.navigateTo({
          url: "/pages/procurementManagement/paymentLedger/index",
        });
        break;
      case "采购退货":
        uni.navigateTo({
          url: "/pages/procurementManagement/purchaseReturnOrder/index",
        });
        break;
      case "供应商档案":
        uni.navigateTo({
          url: "/pages/basicData/supplierManage/index",
        });
        break;
      case "公出管理":
@@ -963,6 +981,7 @@
    // æ”¶é›†æ‰€æœ‰æœ‰æƒé™çš„菜单标题(根据 meta.title)
    const allowedMenuTitles = new Set();
    const alwaysShowTitles = new Set(["采购退货单", "供应商管理"]);
    const collectMenuTitles = routes => {
      if (!Array.isArray(routes)) return;
      routes.forEach(route => {
@@ -980,7 +999,7 @@
    const menuMapping = {
      collaboration: { target: collaborationItems, specialMapping: { "规章制度": "规章制度管理" } },
    };
    console.log(allowedMenuTitles)
    // é€šç”¨è¿‡æ»¤å‡½æ•°
    const filterArray = (targetArray, specialMapping) => {
      const filtered = targetArray.filter(item => {