spring
14 小时以前 3ea1ff641e1c680a5a1727fb4034797bfe65d93e
fix: 质量、耗材物流
已添加3个文件
已修改22个文件
已删除1个文件
2229 ■■■■■ 文件已修改
src/pages.json 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/consumablesLogistics/dispatchLog/Record.vue 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/consumablesLogistics/dispatchLog/index.vue 59 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/consumablesLogistics/dispatchLog/view.vue 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/consumablesLogistics/receiptManagement/Record.vue 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/consumablesLogistics/receiptManagement/index.vue 59 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/consumablesLogistics/receiptManagement/view.vue 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/consumablesLogistics/stockManagement/Unqualified.vue 91 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/consumablesLogistics/stockManagement/add.vue 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/consumablesLogistics/stockManagement/index.vue 45 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/consumablesLogistics/stockManagement/subtract.vue 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/consumablesLogistics/stockManagement/view.vue 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/index.vue 60 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/inventoryManagement/dispatchLog/index.vue 59 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/inventoryManagement/dispatchLog/view.vue 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/inventoryManagement/receiptManagement/index.vue 70 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/inventoryManagement/receiptManagement/view.vue 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/inventoryManagement/stockManagement/add.vue 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/inventoryManagement/stockManagement/index.vue 70 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/inventoryManagement/stockManagement/subtract.vue 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/qualityManagement/InspectItem/index.vue 22 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/qualityManagement/nonconformingManagement/form.vue 458 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/qualityManagement/nonconformingManagement/index.vue 457 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/qualityManagement/rawMaterial/files.vue 159 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/qualityManagement/rawMaterial/form.vue 418 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/qualityManagement/rawMaterial/index.vue 50 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages.json
@@ -1142,6 +1142,13 @@
      }
    },
    {
      "path": "pages/qualityManagement/nonconformingManagement/form",
      "style": {
        "navigationBarTitleText": "不合格品管理",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/qualityManagement/rawMaterial/index",
      "style": {
        "navigationBarTitleText": "原材料",
@@ -1149,6 +1156,20 @@
      }
    },
    {
      "path": "pages/qualityManagement/rawMaterial/form",
      "style": {
        "navigationBarTitleText": "原料检",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/qualityManagement/rawMaterial/files",
      "style": {
        "navigationBarTitleText": "附件管理",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/qualityManagement/visualization/qualityDashboard",
      "style": {
        "navigationBarTitleText": "质量看板",
src/pages/consumablesLogistics/dispatchLog/Record.vue
@@ -86,7 +86,6 @@
} from "@/api/consumablesLogistics/consumablesOutRecord.js";
import {
  findAllQualifiedStockOutRecordTypeOptions,
  findAllUnQualifiedStockOutRecordTypeOptions,
} from "@/api/basicData/enum.js";
const props = defineProps({
@@ -122,11 +121,7 @@
};
const fetchStockRecordTypeOptions = () => {
  const api =
    props.type === "1"
      ? findAllUnQualifiedStockOutRecordTypeOptions
      : findAllQualifiedStockOutRecordTypeOptions;
  api()
  findAllQualifiedStockOutRecordTypeOptions()
    .then((res) => {
      stockRecordTypeOptions.value = res.data || [];
    })
@@ -148,7 +143,7 @@
const getList = () => {
  tableLoading.value = true;
  getConsumablesOutRecordPage({ ...searchForm.value, ...page, type: props.type })
  getConsumablesOutRecordPage({ ...searchForm.value, ...page, type: "0" })
    .then(res => {
      tableData.value = res?.data?.records || [];
      total.value = res?.data?.total || 0;
src/pages/consumablesLogistics/dispatchLog/index.vue
@@ -1,17 +1,6 @@
<template>
  <view class="dispatch-page">
    <PageHeader title="出库台账" @back="goBack" />
    <view class="tabs-wrap">
      <view
        v-for="tab in tabs"
        :key="tab.name"
        class="tab-item"
        :class="{ active: activeTab === tab.name }"
        @click="activeTab = tab.name"
      >
        <text>{{ tab.label }}</text>
      </view>
    </view>
    <view class="search-section">
      <view class="search-row">
        <view class="search-input-wrap">
@@ -63,26 +52,20 @@
</template>
<script setup>
import { reactive, ref, toRefs, watch } from "vue";
import { reactive, ref, toRefs } from "vue";
import { onReachBottom, onShow } from "@dcloudio/uni-app";
import PageHeader from "@/components/PageHeader.vue";
import { getConsumablesOutRecordPage, delConsumablesOutRecord } from "@/api/consumablesLogistics/consumablesOutRecord.js";
import { findAllQualifiedStockOutRecordTypeOptions, findAllUnQualifiedStockOutRecordTypeOptions } from "@/api/basicData/enum.js";
import { findAllQualifiedStockOutRecordTypeOptions } from "@/api/basicData/enum.js";
const activeTab = ref("qualified");
const stockRecordTypeOptions = ref([]);
const tabs = [
  { label: "合格出库", name: "qualified", type: "0" },
  { label: "不合格出库", name: "unqualified", type: "1" },
];
const tableData = ref([]);
const total = ref(0);
const loadStatus = ref("loadmore");
const page = reactive({ current: 1, size: 20 });
const data = reactive({ searchForm: { productName: "" } });
const { searchForm } = toRefs(data);
const currentType = () => tabs.find((t) => t.name === activeTab.value)?.type || "0";
const currentType = () => "0";
function getRecordType(recordType) {
  if (recordType == null || recordType === "") return "";
@@ -90,11 +73,7 @@
}
function fetchRecordTypeOptions() {
  const api =
    currentType() === "1"
      ? findAllUnQualifiedStockOutRecordTypeOptions
      : findAllQualifiedStockOutRecordTypeOptions;
  api()
  findAllQualifiedStockOutRecordTypeOptions()
    .then((res) => {
      const list = res.data != null ? res.data : res;
      stockRecordTypeOptions.value = Array.isArray(list) ? list : [];
@@ -143,13 +122,6 @@
  getList();
};
watch(activeTab, () => {
  page.current = 1;
  loadStatus.value = "loadmore";
  stockRecordTypeOptions.value = [];
  getList();
});
const handleQuery = () => {
  page.current = 1;
  loadStatus.value = "loadmore";
@@ -163,7 +135,7 @@
      "dispatchDetailItem",
      JSON.stringify({
        item,
        type: currentType(),
        type: "0",
      })
    );
  } catch (e) {}
@@ -198,9 +170,6 @@
<style lang="scss" scoped>
.dispatch-page { min-height: 100vh; background: #f5f5f5; padding-bottom: 40rpx; }
.tabs-wrap { display: flex; background: #fff; padding: 24rpx; gap: 24rpx; }
.tab-item { flex: 1; text-align: center; padding: 20rpx; border-radius: 12rpx; background: #f0f0f0; font-size: 28rpx; color: #666; }
.tab-item.active { background: #2979ff; color: #fff; }
.search-section { background: #fff; margin: 24rpx; padding: 24rpx; border-radius: 16rpx; }
.search-row { display: flex; align-items: center; }
.search-input-wrap { flex: 1; margin-right: 20rpx; min-width: 0; }
@@ -216,8 +185,22 @@
.card-body .l { color: #666; }
.card-body .r { color: #333; }
.card-body .r.highlight { color: #2979ff; font-weight: 500; }
.card-actions { display: flex; justify-content: flex-end; margin-top: 12rpx; }
.btn-delete { color: #f56c6c; font-size: 28rpx; }
.card-actions {
  display: flex;
  justify-content: center;
  align-items: center;
  margin-top: 16rpx;
  padding-top: 16rpx;
  border-top: 1rpx solid #eee;
}
.btn-delete {
  color: #f56c6c;
  font-size: 28rpx;
  padding: 12rpx 36rpx;
  border-radius: 999rpx;
  border: 1rpx solid rgba(245, 108, 108, 0.55);
  background: rgba(245, 108, 108, 0.08);
}
.no-data { text-align: center; padding: 60rpx 0; color: #999; font-size: 28rpx; }
.load-more-wrap { padding: 24rpx 24rpx 8rpx; }
</style>
src/pages/consumablesLogistics/dispatchLog/view.vue
@@ -92,7 +92,7 @@
import { ref } from "vue";
import { onLoad } from "@dcloudio/uni-app";
import PageHeader from "@/components/PageHeader.vue";
import { findAllQualifiedStockOutRecordTypeOptions, findAllUnQualifiedStockOutRecordTypeOptions } from "@/api/basicData/enum.js";
import { findAllQualifiedStockOutRecordTypeOptions } from "@/api/basicData/enum.js";
const detail = ref(null);
const loading = ref(true);
@@ -126,9 +126,7 @@
}
function fetchRecordTypeOptions(type) {
  const api =
    type === "1" ? findAllUnQualifiedStockOutRecordTypeOptions : findAllQualifiedStockOutRecordTypeOptions;
  api()
  findAllQualifiedStockOutRecordTypeOptions()
    .then((res) => {
      const data = res.data != null ? res.data : res;
      stockRecordTypeOptions.value = Array.isArray(data) ? data : [];
@@ -144,9 +142,8 @@
    try {
      const payload = typeof cached === "string" ? JSON.parse(cached) : cached;
      const item = payload && payload.item != null ? payload.item : payload;
      const type = payload && payload.type != null ? payload.type : "0";
      detail.value = normalizeDetail({ ...item, index: 1 });
      fetchRecordTypeOptions(type);
      fetchRecordTypeOptions("0");
      uni.removeStorageSync("dispatchDetailItem");
    } catch (e) {
      uni.removeStorageSync("dispatchDetailItem");
src/pages/consumablesLogistics/receiptManagement/Record.vue
@@ -86,7 +86,6 @@
} from "@/api/consumablesLogistics/consumablesInRecord.js";
import {
  findAllQualifiedStockInRecordTypeOptions,
  findAllUnQualifiedStockInRecordTypeOptions,
} from "@/api/basicData/enum.js";
const props = defineProps({
@@ -122,11 +121,7 @@
};
const fetchStockRecordTypeOptions = () => {
  const api =
    props.type === "1"
      ? findAllUnQualifiedStockInRecordTypeOptions
      : findAllQualifiedStockInRecordTypeOptions;
  api()
  findAllQualifiedStockInRecordTypeOptions()
    .then((res) => {
      stockRecordTypeOptions.value = res.data || [];
    })
@@ -148,7 +143,7 @@
const getList = () => {
  tableLoading.value = true;
  getConsumablesInRecordListPage({ ...searchForm.value, ...page, type: props.type })
  getConsumablesInRecordListPage({ ...searchForm.value, ...page, type: "0" })
    .then(res => {
      tableData.value = res?.data?.records || [];
      total.value = res?.data?.total || 0;
src/pages/consumablesLogistics/receiptManagement/index.vue
@@ -1,17 +1,6 @@
<template>
  <view class="receipt-page">
    <PageHeader title="入库管理" @back="goBack" />
    <view class="tabs-wrap">
      <view
        v-for="tab in tabs"
        :key="tab.name"
        class="tab-item"
        :class="{ active: activeTab === tab.name }"
        @click="activeTab = tab.name"
      >
        <text>{{ tab.label }}</text>
      </view>
    </view>
    <view class="search-section">
      <view class="search-row">
        <view class="search-input-wrap">
@@ -58,26 +47,20 @@
</template>
<script setup>
import { reactive, ref, toRefs, watch } from "vue";
import { reactive, ref, toRefs } from "vue";
import { onReachBottom, onShow } from "@dcloudio/uni-app";
import PageHeader from "@/components/PageHeader.vue";
import request from "@/utils/request";
import { findAllQualifiedStockInRecordTypeOptions, findAllUnQualifiedStockInRecordTypeOptions } from "@/api/basicData/enum.js";
import { findAllQualifiedStockInRecordTypeOptions } from "@/api/basicData/enum.js";
const activeTab = ref("qualified");
const stockRecordTypeOptions = ref([]);
const tabs = [
  { label: "合格入库", name: "qualified", type: "0" },
  { label: "不合格入库", name: "unqualified", type: "1" },
];
const tableData = ref([]);
const total = ref(0);
const loadStatus = ref("loadmore");
const page = reactive({ current: 1, size: 4 });
const data = reactive({ searchForm: { productName: "" } });
const { searchForm } = toRefs(data);
const currentType = () => tabs.find((t) => t.name === activeTab.value)?.type || "0";
const currentType = () => "0";
function getRecordType(recordType) {
  if (recordType == null || recordType === "") return "";
@@ -85,11 +68,7 @@
}
function fetchRecordTypeOptions() {
  const api =
    currentType() === "1"
      ? findAllUnQualifiedStockInRecordTypeOptions
      : findAllQualifiedStockInRecordTypeOptions;
  api()
  findAllQualifiedStockInRecordTypeOptions()
    .then((res) => {
      const data = res.data != null ? res.data : res;
      stockRecordTypeOptions.value = Array.isArray(data) ? data : [];
@@ -142,13 +121,6 @@
  getList();
};
watch(activeTab, () => {
  page.current = 1;
  loadStatus.value = "loadmore";
  stockRecordTypeOptions.value = [];
  getList();
});
const handleQuery = () => {
  page.current = 1;
  loadStatus.value = "loadmore";
@@ -162,7 +134,7 @@
      "receiptDetailItem",
      JSON.stringify({
        item,
        type: currentType(),
        type: "0",
      })
    );
  } catch (e) {}
@@ -206,9 +178,6 @@
<style lang="scss" scoped>
.receipt-page { min-height: 100vh; background: #f5f5f5; padding-bottom: 40rpx; }
.tabs-wrap { display: flex; background: #fff; padding: 24rpx; gap: 24rpx; }
.tab-item { flex: 1; text-align: center; padding: 20rpx; border-radius: 12rpx; background: #f0f0f0; font-size: 28rpx; color: #666; }
.tab-item.active { background: #2979ff; color: #fff; }
.search-section { background: #fff; margin: 24rpx; padding: 24rpx; border-radius: 16rpx; }
.search-row { display: flex; align-items: center; }
.search-input-wrap { flex: 1; margin-right: 20rpx; min-width: 0; }
@@ -224,8 +193,22 @@
.card-body .l { color: #666; }
.card-body .r { color: #333; }
.card-body .r.highlight { color: #2979ff; font-weight: 500; }
.card-actions { display: flex; justify-content: flex-end; margin-top: 12rpx; }
.btn-delete { color: #f56c6c; font-size: 28rpx; }
.card-actions {
  display: flex;
  justify-content: center;
  align-items: center;
  margin-top: 16rpx;
  padding-top: 16rpx;
  border-top: 1rpx solid #eee;
}
.btn-delete {
  color: #f56c6c;
  font-size: 28rpx;
  padding: 12rpx 36rpx;
  border-radius: 999rpx;
  border: 1rpx solid rgba(245, 108, 108, 0.55);
  background: rgba(245, 108, 108, 0.08);
}
.no-data { text-align: center; padding: 60rpx 0; color: #999; font-size: 28rpx; }
.load-more-wrap { padding: 24rpx 24rpx 8rpx; }
</style>
src/pages/consumablesLogistics/receiptManagement/view.vue
@@ -92,7 +92,7 @@
import { ref } from "vue";
import { onLoad } from "@dcloudio/uni-app";
import PageHeader from "@/components/PageHeader.vue";
import { findAllQualifiedStockInRecordTypeOptions, findAllUnQualifiedStockInRecordTypeOptions } from "@/api/basicData/enum.js";
import { findAllQualifiedStockInRecordTypeOptions } from "@/api/basicData/enum.js";
const detail = ref(null);
const loading = ref(true);
@@ -126,9 +126,7 @@
}
function fetchRecordTypeOptions(type) {
  const api =
    type === "1" ? findAllUnQualifiedStockInRecordTypeOptions : findAllQualifiedStockInRecordTypeOptions;
  api()
  findAllQualifiedStockInRecordTypeOptions()
    .then((res) => {
      const data = res.data != null ? res.data : res;
      stockRecordTypeOptions.value = Array.isArray(data) ? data : [];
@@ -144,9 +142,8 @@
    try {
      const payload = typeof cached === "string" ? JSON.parse(cached) : cached;
      const item = payload && payload.item != null ? payload.item : payload;
      const type = payload && payload.type != null ? payload.type : "0";
      detail.value = normalizeDetail({ ...item, index: 1 });
      fetchRecordTypeOptions(type);
      fetchRecordTypeOptions("0");
      uni.removeStorageSync("receiptDetailItem");
    } catch (e) {
      uni.removeStorageSync("receiptDetailItem");
src/pages/consumablesLogistics/stockManagement/Unqualified.vue
ÎļþÒÑɾ³ý
src/pages/consumablesLogistics/stockManagement/add.vue
@@ -23,7 +23,7 @@
        </view>
      </view>
      <view v-if="isQualified" class="form-section">
      <view class="form-section">
        <view class="section-title">过磅信息</view>
        <view class="form-row">
          <text class="form-label">车牌号</text>
@@ -114,7 +114,6 @@
import dayjs from "dayjs";
import PageHeader from "@/components/PageHeader.vue";
import { createConsumablesIn } from "@/api/consumablesLogistics/consumablesIn.js";
import { createConsumablesUnInventory } from "@/api/consumablesLogistics/consumablesUninventory.js";
import { productModelList } from "@/api/basicData/productModel.js";
const form = reactive({
@@ -133,8 +132,8 @@
  remark: "",
});
const type = ref("0");
const isQualified = computed(() => type.value === "0");
const type = ref("0"); // å›ºå®šåˆæ ¼åº“å­˜
const isQualified = computed(() => true);
const showProductPopup = ref(false);
const productQuery = reactive({
@@ -148,9 +147,7 @@
const weighingDateValue = ref(Date.now());
onLoad((options) => {
  if (options && options.type != null) {
    type.value = options.type;
  }
  type.value = "0";
});
const openProductSelector = () => {
@@ -238,8 +235,7 @@
    weighingOperator: form.weighingOperator,
    remark: form.remark,
  };
  const api = isQualified.value ? createConsumablesIn : createConsumablesUnInventory;
  api(payload)
  createConsumablesIn(payload)
    .then(() => {
      uni.showToast({ title: "新增成功", icon: "success" });
      setTimeout(() => {
src/pages/consumablesLogistics/stockManagement/index.vue
@@ -2,18 +2,6 @@
  <view class="stock-mgmt-page">
    <PageHeader title="库存管理" @back="goBack" />
    <view class="tabs-wrap">
      <view
        v-for="tab in tabs"
        :key="tab.name"
        class="tab-item"
        :class="{ active: activeTab === tab.name }"
        @click="activeTab = tab.name"
      >
        <text>{{ tab.label }}</text>
      </view>
    </view>
    <view class="search-section">
      <view class="search-row">
        <view class="search-input-wrap">
@@ -102,17 +90,11 @@
</template>
<script setup>
import { computed, reactive, ref, toRefs, watch } from "vue";
import { computed, reactive, ref, toRefs } from "vue";
import { onReachBottom, onShow } from "@dcloudio/uni-app";
import PageHeader from "@/components/PageHeader.vue";
import { frozenConsumablesIn, getConsumablesInListPage, thawConsumablesIn } from "@/api/consumablesLogistics/consumablesIn.js";
import { frozenConsumablesUninventory, getConsumablesUninventoryListPage, thawConsumablesUninventory } from "@/api/consumablesLogistics/consumablesUninventory.js";
const activeTab = ref("qualified");
const tabs = [
  { label: "合格库存", name: "qualified" },
  { label: "不合格库存", name: "unqualified" },
];
const tableData = ref([]);
const total = ref(0);
const loadStatus = ref("loadmore");
@@ -126,15 +108,13 @@
});
const { searchForm, quantityForm } = toRefs(data);
const isQualified = () => activeTab.value === "qualified";
const getList = () => {
  const isFirstPage = page.current === 1;
  if (isFirstPage) {
    uni.showLoading({ title: "加载中...", mask: true });
  }
  const params = { ...page, productName: searchForm.value.productName };
  const api = isQualified() ? getConsumablesInListPage : getConsumablesUninventoryListPage;
  api(params)
  getConsumablesInListPage(params)
    .then((res) => {
      uni.hideLoading();
      const records = res.data?.records || [];
@@ -163,12 +143,6 @@
  getList();
};
watch(activeTab, () => {
  page.current = 1;
  loadStatus.value = "loadmore";
  getList();
});
const handleQuery = () => {
  page.current = 1;
  loadStatus.value = "loadmore";
@@ -176,9 +150,8 @@
};
const goAdd = () => {
  const type = isQualified() ? "0" : "1";
  uni.navigateTo({
    url: `/pages/consumablesLogistics/stockManagement/add?type=${type}`,
    url: `/pages/consumablesLogistics/stockManagement/add?type=0`,
  });
};
@@ -202,13 +175,12 @@
      "stockSubtractRecord",
      JSON.stringify({
        item: row,
        type: isQualified() ? "0" : "1",
        type: "0",
      })
    );
  } catch (e) {}
  const typeParam = isQualified() ? "0" : "1";
  uni.navigateTo({
    url: `/pages/consumablesLogistics/stockManagement/subtract?type=${typeParam}&id=${row.id}`,
    url: `/pages/consumablesLogistics/stockManagement/subtract?type=0&id=${row.id}`,
  });
};
@@ -243,9 +215,9 @@
  const base = { id, lockedQuantity: num };
  let promise;
  if (quantityOp.value === "frozen") {
    promise = isQualified() ? frozenConsumablesIn(base) : frozenConsumablesUninventory(base);
    promise = frozenConsumablesIn(base);
  } else {
    promise = isQualified() ? thawConsumablesIn(base) : thawConsumablesUninventory(base);
    promise = thawConsumablesIn(base);
  }
  promise
    .then(() => {
@@ -283,9 +255,6 @@
<style lang="scss" scoped>
.stock-mgmt-page { min-height: 100vh; background: #f5f5f5; padding-bottom: 120rpx; }
.tabs-wrap { display: flex; background: #fff; padding: 24rpx; gap: 24rpx; }
.tab-item { flex: 1; text-align: center; padding: 20rpx; border-radius: 12rpx; background: #f0f0f0; font-size: 28rpx; color: #666; }
.tab-item.active { background: #2979ff; color: #fff; }
.search-section { background: #fff; margin: 24rpx; padding: 24rpx; border-radius: 16rpx; }
.search-row { display: flex; align-items: center; }
.search-input-wrap { flex: 1; margin-right: 20rpx; min-width: 0; }
src/pages/consumablesLogistics/stockManagement/subtract.vue
@@ -25,23 +25,23 @@
          <text class="form-label required">出库数量</text>
          <up-input v-model="form.stockOutNum" type="number" :placeholder="'最大' + stockRecord.unLockedQuantity" />
        </view>
        <view class="form-row" v-if="isQualified">
        <view class="form-row">
          <text class="form-label">车牌号</text>
          <up-input v-model="form.licensePlateNo" placeholder="请输入车牌号" />
        </view>
        <view class="form-row" v-if="isQualified">
        <view class="form-row">
          <text class="form-label">毛重(吨)</text>
          <up-input v-model="form.grossWeight" type="number" placeholder="请输入毛重" />
        </view>
        <view class="form-row" v-if="isQualified">
        <view class="form-row">
          <text class="form-label">皮重(吨)</text>
          <up-input v-model="form.tareWeight" type="number" placeholder="请输入皮重" />
        </view>
        <view class="form-row" v-if="isQualified">
        <view class="form-row">
          <text class="form-label">净重(吨)</text>
          <up-input v-model="form.netWeight" type="number" disabled placeholder="自动计算" />
        </view>
        <view class="form-row" v-if="isQualified">
        <view class="form-row">
          <text class="form-label">过磅日期</text>
          <view class="selector-trigger" @click="openWeighingDatePicker">
            <text class="selector-text" :class="{ placeholder: !form.weighingDate }">
@@ -50,7 +50,7 @@
            <up-icon name="calendar" size="16" color="#999"></up-icon>
          </view>
        </view>
        <view class="form-row" v-if="isQualified">
        <view class="form-row">
          <text class="form-label">过磅员</text>
          <up-input v-model="form.weighingOperator" placeholder="请输入过磅员" />
        </view>
@@ -83,10 +83,9 @@
import dayjs from "dayjs";
import PageHeader from "@/components/PageHeader.vue";
import { subtractConsumablesIn } from "@/api/consumablesLogistics/consumablesIn.js";
import { subtractConsumablesUnInventory } from "@/api/consumablesLogistics/consumablesUninventory.js";
const type = ref("0");
const isQualified = computed(() => type.value === "0");
const isQualified = computed(() => true);
const stockRecord = reactive({
  id: "",
@@ -110,9 +109,7 @@
const weighingDateValue = ref(Date.now());
onLoad((options) => {
  if (options && options.type != null) {
    type.value = options.type;
  }
  type.value = "0";
  const cached = uni.getStorageSync("stockSubtractRecord");
  if (cached) {
    try {
@@ -176,8 +173,7 @@
      return;
    }
  }
  const api = isQualified.value ? subtractConsumablesIn : subtractConsumablesUnInventory;
  api({
  subtractConsumablesIn({
    id: stockRecord.id,
    stockOutNum: outNum,
    licensePlateNo: form.licensePlateNo,
src/pages/consumablesLogistics/stockManagement/view.vue
@@ -64,7 +64,7 @@
const detail = ref(null);
const loading = ref(true);
function normalizeDetail(raw, type) {
function normalizeDetail(raw) {
  if (!raw) return null;
  const d = typeof raw === "object" ? raw : {};
  return {
@@ -76,7 +76,7 @@
    lockedQuantity: d.lockedQuantity,
    unLockedQuantity: d.unLockedQuantity ?? (d.qualitity - (d.lockedQuantity || 0)),
    updateTime: d.updateTime,
    typeLabel: type === "1" ? "不合格库存" : "合格库存",
    typeLabel: "合格库存",
  };
}
@@ -86,8 +86,7 @@
    try {
      const payload = typeof cached === "string" ? JSON.parse(cached) : cached;
      const item = payload && payload.item != null ? payload.item : payload;
      const type = payload && payload.type != null ? payload.type : "0";
      detail.value = normalizeDetail({ ...item, index: 1 }, type);
      detail.value = normalizeDetail({ ...item, index: 1 });
      uni.removeStorageSync("stockDetailItem");
    } catch (e) {
      uni.removeStorageSync("stockDetailItem");
src/pages/index.vue
@@ -426,18 +426,18 @@
  // è´¨é‡ç®¡ç†
  const qualityItems = reactive([
    {
      icon: "/static/images/icon/caigoutaizhang@2x.png",
      label: "原材料检验",
    },
    {
      icon: "/static/images/icon/caigoutaizhang@2x.png",
      label: "过程检验",
    },
    {
      icon: "/static/images/icon/caigoutaizhang@2x.png",
      label: "出厂检验",
    },
    // {
    //   icon: "/static/images/icon/caigoutaizhang@2x.png",
    //   label: "原材料检验",
    // },
    // {
    //   icon: "/static/images/icon/caigoutaizhang@2x.png",
    //   label: "过程检验",
    // },
    // {
    //   icon: "/static/images/icon/caigoutaizhang@2x.png",
    //   label: "出厂检验",
    // },
  ]);
  // ååŒåŠžå…¬åŠŸèƒ½æ•°æ®
  const collaborationItems = reactive([
@@ -949,7 +949,7 @@
          url: "/pages/qualityManagement/nonconformingManagement/index",
        });
        break;
      case "原材料":
      case "原料检":
        uni.navigateTo({
          url: "/pages/qualityManagement/rawMaterial/index",
        });
@@ -1206,41 +1206,13 @@
    });
    purchaseItems.splice(0, purchaseItems.length, ...filteredPurchase);
    // è¿‡æ»¤è´¨é‡ç®¡ç†èœå•
    // è´¨é‡ç®¡ç†èœå•:固定只展示 3 ä¸ªå…¥å£
    const originalQuality = [
      { icon: "/static/images/icon/caigoutaizhang@2x.png", label: "原材料检验" },
      { icon: "/static/images/icon/caigoutaizhang@2x.png", label: "过程检验" },
      { icon: "/static/images/icon/caigoutaizhang@2x.png", label: "出厂检验" },
      { icon: "/static/images/icon/caigoutaizhang@2x.png", label: "检测项维护" },
      { icon: "/static/images/icon/caigoutaizhang@2x.png", label: "指标维护" },
      { icon: "/static/images/icon/caigoutaizhang@2x.png", label: "指标绑定" },
      { icon: "/static/images/icon/caigoutaizhang@2x.png", label: "原料检" },
      { icon: "/static/images/icon/caigoutaizhang@2x.png", label: "不合格品管理" },
      { icon: "/static/images/icon/caigoutaizhang@2x.png", label: "原材料" },
      { icon: "/static/images/icon/caigoutaizhang@2x.png", label: "近效期退货" },
      { icon: "/static/images/icon/caigoutaizhang@2x.png", label: "质量看板" },
    ];
    const hasAllowedTitleLike = label => {
      if (allowedMenuTitles.has(label)) return true;
      return Array.from(allowedMenuTitles).some(
        title =>
          typeof title === "string" &&
          (title.includes(label) || label.includes(title))
      );
    };
    const filteredQuality = originalQuality.filter(item => {
      return hasAllowedTitleLike(item.label);
    });
    const hasQualityModulePermission = Array.from(allowedMenuTitles).some(
      title =>
        typeof title === "string" &&
        (title.includes("质量") || title.includes("检验"))
    );
    const finalQualityItems = filteredQuality.length
      ? filteredQuality
      : hasQualityModulePermission
      ? originalQuality
      : [];
    qualityItems.splice(0, qualityItems.length, ...finalQualityItems);
    qualityItems.splice(0, qualityItems.length, ...originalQuality);
    // è¿‡æ»¤å®‰å…¨ç”Ÿäº§èœå•
    const originalSafety = [
src/pages/inventoryManagement/dispatchLog/index.vue
@@ -2,19 +2,6 @@
  <view class="dispatch-page">
    <PageHeader title="出库台账" @back="goBack" />
    <!-- æ ‡ç­¾ï¼šåˆæ ¼å‡ºåº“ / ä¸åˆæ ¼å‡ºåº“ -->
    <view class="tabs-wrap">
      <view
        v-for="tab in tabs"
        :key="tab.name"
        class="tab-item"
        :class="{ active: activeTab === tab.name }"
        @click="activeTab = tab.name"
      >
        <text>{{ tab.label }}</text>
      </view>
    </view>
    <!-- æœç´¢åŒºåŸŸ -->
    <view class="search-section">
      <view class="search-row">
@@ -78,21 +65,16 @@
</template>
<script setup>
import { ref, reactive, toRefs, watch } from 'vue'
import { ref, reactive, toRefs } from 'vue'
import { onShow, onReachBottom } from '@dcloudio/uni-app'
import PageHeader from '@/components/PageHeader.vue'
import { getStockOutPage, delStockOut } from '@/api/inventoryManagement/stockOutRecord.js'
import {
  findAllQualifiedStockOutRecordTypeOptions,
  findAllUnQualifiedStockOutRecordTypeOptions
  findAllQualifiedStockOutRecordTypeOptions
} from '@/api/basicData/enum.js'
const activeTab = ref('qualified')
const stockRecordTypeOptions = ref([])
const tabs = [
  { label: '合格出库', name: 'qualified', type: '0' },
  { label: '不合格出库', name: 'unqualified', type: '1' }
]
const currentType = () => '0'
const tableData = ref([])
const total = ref(0)
const loadStatus = ref('loadmore')
@@ -104,18 +86,13 @@
})
const { searchForm } = toRefs(data)
const currentType = () => tabs.find(t => t.name === activeTab.value)?.type || '0'
function getRecordType(recordType) {
  if (recordType == null || recordType === '') return ''
  return stockRecordTypeOptions.value.find(item => item.value === recordType)?.label || ''
}
function fetchRecordTypeOptions() {
  const api = currentType() === '1'
    ? findAllUnQualifiedStockOutRecordTypeOptions
    : findAllQualifiedStockOutRecordTypeOptions
  api()
  findAllQualifiedStockOutRecordTypeOptions()
    .then(res => {
      const list = res.data != null ? res.data : res
      stockRecordTypeOptions.value = Array.isArray(list) ? list : []
@@ -168,13 +145,6 @@
  getList()
}
watch(activeTab, () => {
  page.current = 1
  loadStatus.value = 'loadmore'
  stockRecordTypeOptions.value = []
  getList()
})
const handleQuery = () => {
  page.current = 1
  loadStatus.value = 'loadmore'
@@ -186,7 +156,7 @@
  try {
    uni.setStorageSync('dispatchDetailItem', JSON.stringify({
      item,
      type: currentType()
      type: '0'
    }))
  } catch (e) {}
  uni.navigateTo({
@@ -224,25 +194,6 @@
  min-height: 100vh;
  background: #f5f5f5;
  padding-bottom: 40rpx;
}
.tabs-wrap {
  display: flex;
  background: #fff;
  padding: 24rpx;
  gap: 24rpx;
}
.tab-item {
  flex: 1;
  text-align: center;
  padding: 20rpx;
  border-radius: 12rpx;
  background: #f0f0f0;
  font-size: 28rpx;
  color: #666;
}
.tab-item.active {
  background: #2979ff;
  color: #fff;
}
.search-section {
  background: #fff;
src/pages/inventoryManagement/dispatchLog/view.vue
@@ -95,8 +95,7 @@
import { onLoad } from '@dcloudio/uni-app'
import PageHeader from '@/components/PageHeader.vue'
import {
  findAllQualifiedStockOutRecordTypeOptions,
  findAllUnQualifiedStockOutRecordTypeOptions
  findAllQualifiedStockOutRecordTypeOptions
} from '@/api/basicData/enum.js'
const detail = ref(null)
@@ -130,11 +129,8 @@
  return stockRecordTypeOptions.value.find(item => item.value === recordType)?.label || ''
}
function fetchRecordTypeOptions(type) {
  const api = type === '1'
    ? findAllUnQualifiedStockOutRecordTypeOptions
    : findAllQualifiedStockOutRecordTypeOptions
  api()
function fetchRecordTypeOptions() {
  findAllQualifiedStockOutRecordTypeOptions()
    .then(res => {
      const data = res.data != null ? res.data : res
      stockRecordTypeOptions.value = Array.isArray(data) ? data : []
@@ -150,9 +146,8 @@
    try {
      const payload = typeof cached === 'string' ? JSON.parse(cached) : cached
      const item = payload && payload.item != null ? payload.item : payload
      const type = payload && payload.type != null ? payload.type : '0'
      detail.value = normalizeDetail({ ...item, index: 1 })
      fetchRecordTypeOptions(type)
      fetchRecordTypeOptions()
      uni.removeStorageSync('dispatchDetailItem')
    } catch (e) {
      uni.removeStorageSync('dispatchDetailItem')
src/pages/inventoryManagement/receiptManagement/index.vue
@@ -2,19 +2,6 @@
  <view class="receipt-page">
    <PageHeader title="入库管理" @back="goBack" />
    <!-- æ ‡ç­¾ï¼šåˆæ ¼å…¥åº“ / ä¸åˆæ ¼å…¥åº“ -->
    <view class="tabs-wrap">
      <view
        v-for="tab in tabs"
        :key="tab.name"
        class="tab-item"
        :class="{ active: activeTab === tab.name }"
        @click="activeTab = tab.name"
      >
        <text>{{ tab.label }}</text>
      </view>
    </view>
    <!-- æœç´¢åŒºåŸŸ -->
    <view class="search-section">
      <view class="search-row">
@@ -34,7 +21,7 @@
      </view>
    </view>
    <!-- åˆ—表(合格/不合格共用接口 type åŒºåˆ†ï¼‰ -->
    <!-- åˆ—表 -->
    <view class="list-section" v-if="activeTab !== 'custom'">
      <view v-if="tableData.length > 0">
        <view
@@ -73,7 +60,7 @@
</template>
<script setup>
import { ref, reactive, toRefs, watch } from 'vue'
import { ref, reactive, toRefs } from 'vue'
import { onShow, onReachBottom } from '@dcloudio/uni-app'
import PageHeader from '@/components/PageHeader.vue'
import {
@@ -81,16 +68,11 @@
  batchDeleteStockInRecords
} from '@/api/inventoryManagement/stockInRecord.js'
import {
  findAllQualifiedStockInRecordTypeOptions,
  findAllUnQualifiedStockInRecordTypeOptions
  findAllQualifiedStockInRecordTypeOptions
} from '@/api/basicData/enum.js'
const activeTab = ref('qualified')
const stockRecordTypeOptions = ref([])
const tabs = [
  { label: '合格入库', name: 'qualified', type: '0' },
  { label: '不合格入库', name: 'unqualified', type: '1' }
]
const currentType = () => '0'
const tableData = ref([])
const total = ref(0)
@@ -104,18 +86,13 @@
})
const { searchForm } = toRefs(data)
const currentType = () => tabs.find(t => t.name === activeTab.value)?.type || '0'
function getRecordType(recordType) {
  if (recordType == null || recordType === '') return ''
  return stockRecordTypeOptions.value.find(item => item.value === recordType)?.label || ''
}
function fetchRecordTypeOptions() {
  const api = currentType() === '1'
    ? findAllUnQualifiedStockInRecordTypeOptions
    : findAllQualifiedStockInRecordTypeOptions
  api()
  findAllQualifiedStockInRecordTypeOptions()
    .then(res => {
      const data = res.data != null ? res.data : res
      stockRecordTypeOptions.value = Array.isArray(data) ? data : []
@@ -126,7 +103,6 @@
}
const getList = () => {
  if (activeTab.value === 'custom') return
  const isFirstPage = page.current === 1
  if (isFirstPage) {
    uni.showLoading({ title: '加载中...', mask: true })
@@ -170,13 +146,6 @@
  getList()
}
watch(activeTab, () => {
  page.current = 1
  loadStatus.value = 'loadmore'
  stockRecordTypeOptions.value = []
  getList()
})
const handleQuery = () => {
  page.current = 1
  loadStatus.value = 'loadmore'
@@ -188,7 +157,7 @@
  try {
    uni.setStorageSync('receiptDetailItem', JSON.stringify({
      item,
      type: currentType()
      type: '0'
    }))
  } catch (e) {}
  uni.navigateTo({
@@ -218,7 +187,7 @@
const goBack = () => uni.navigateBack()
onShow(() => {
  if (activeTab.value !== 'custom') getList()
  getList()
})
onReachBottom(() => {
@@ -231,25 +200,6 @@
  min-height: 100vh;
  background: #f5f5f5;
  padding-bottom: 40rpx;
}
.tabs-wrap {
  display: flex;
  background: #fff;
  padding: 24rpx;
  gap: 24rpx;
}
.tab-item {
  flex: 1;
  text-align: center;
  padding: 20rpx;
  border-radius: 12rpx;
  background: #f0f0f0;
  font-size: 28rpx;
  color: #666;
}
.tab-item.active {
  background: #2979ff;
  color: #fff;
}
.search-section {
  background: #fff;
@@ -340,11 +290,17 @@
  display: flex;
  justify-content: center;
  align-items: center;
  width: 100%;
  text-align: center;
}
.btn-delete {
  font-size: 28rpx;
  color: #f56c6c;
  padding: 12rpx 32rpx;
  margin: 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.no-data {
  text-align: center;
src/pages/inventoryManagement/receiptManagement/view.vue
@@ -95,8 +95,7 @@
import { onLoad } from '@dcloudio/uni-app'
import PageHeader from '@/components/PageHeader.vue'
import {
  findAllQualifiedStockInRecordTypeOptions,
  findAllUnQualifiedStockInRecordTypeOptions
  findAllQualifiedStockInRecordTypeOptions
} from '@/api/basicData/enum.js'
const detail = ref(null)
@@ -131,11 +130,8 @@
  return stockRecordTypeOptions.value.find(item => item.value === recordType)?.label || ''
}
function fetchRecordTypeOptions(type) {
  const api = type === '1'
    ? findAllUnQualifiedStockInRecordTypeOptions
    : findAllQualifiedStockInRecordTypeOptions
  api()
function fetchRecordTypeOptions() {
  findAllQualifiedStockInRecordTypeOptions()
    .then(res => {
      const data = res.data != null ? res.data : res
      stockRecordTypeOptions.value = Array.isArray(data) ? data : []
@@ -151,9 +147,8 @@
    try {
      const payload = typeof cached === 'string' ? JSON.parse(cached) : cached
      const item = payload && payload.item != null ? payload.item : payload
      const type = payload && payload.type != null ? payload.type : '0'
      detail.value = normalizeDetail({ ...item, index: 1 })
      fetchRecordTypeOptions(type)
      fetchRecordTypeOptions()
      uni.removeStorageSync('receiptDetailItem')
    } catch (e) {
      uni.removeStorageSync('receiptDetailItem')
src/pages/inventoryManagement/stockManagement/add.vue
@@ -23,8 +23,8 @@
        </view>
      </view>
      <!-- åˆæ ¼åº“存时显示过磅相关字段 -->
      <view v-if="isQualified" class="form-section">
      <!-- è¿‡ç£…相关字段 -->
      <view class="form-section">
        <view class="section-title">过磅信息</view>
        <view class="form-row">
          <text class="form-label">车牌号</text>
@@ -145,7 +145,6 @@
import dayjs from 'dayjs'
import PageHeader from '@/components/PageHeader.vue'
import { createStockInventory } from '@/api/inventoryManagement/stockInventory.js'
import { createStockUnInventory } from '@/api/inventoryManagement/stockUninventory.js'
import { productModelList } from '@/api/basicData/productModel.js'
const form = reactive({
@@ -165,8 +164,8 @@
  remark: ''
})
const type = ref('0') // 0 åˆæ ¼åº“存,1 ä¸åˆæ ¼åº“å­˜
const isQualified = computed(() => type.value === '0')
const type = ref('0') // å›ºå®šåˆæ ¼åº“å­˜
const isQualified = computed(() => true)
const showProductPopup = ref(false)
const productQuery = reactive({
@@ -180,9 +179,7 @@
const weighingDateValue = ref(Date.now())
onLoad((options) => {
  if (options && options.type != null) {
    type.value = options.type
  }
  type.value = '0'
})
const openProductSelector = () => {
@@ -286,8 +283,7 @@
    weighingOperator: form.weighingOperator,
    remark: form.remark
  }
  const api = isQualified.value ? createStockInventory : createStockUnInventory
  api(payload)
  createStockInventory(payload)
    .then(() => {
      uni.showToast({ title: '新增成功', icon: 'success' })
      setTimeout(() => {
src/pages/inventoryManagement/stockManagement/index.vue
@@ -2,19 +2,6 @@
  <view class="stock-mgmt-page">
    <PageHeader title="库存管理" @back="goBack" />
    <!-- æ ‡ç­¾ï¼šåˆæ ¼åº“å­˜ / ä¸åˆæ ¼åº“å­˜ -->
    <view class="tabs-wrap">
      <view
        v-for="tab in tabs"
        :key="tab.name"
        class="tab-item"
        :class="{ active: activeTab === tab.name }"
        @click="activeTab = tab.name"
      >
        <text>{{ tab.label }}</text>
      </view>
    </view>
    <!-- æœç´¢åŒºåŸŸ -->
    <view class="search-section">
      <view class="search-row">
@@ -107,7 +94,7 @@
</template>
<script setup>
import { ref, reactive, toRefs, watch, computed } from 'vue'
import { ref, reactive, toRefs, computed } from 'vue'
import { onShow, onReachBottom } from '@dcloudio/uni-app'
import PageHeader from '@/components/PageHeader.vue'
import {
@@ -117,19 +104,7 @@
  frozenStockInventory,
  thawStockInventory
} from '@/api/inventoryManagement/stockInventory.js'
import {
  getStockUninventoryListPage,
  createStockUnInventory,
  subtractStockUnInventory,
  frozenStockUninventory,
  thawStockUninventory
} from '@/api/inventoryManagement/stockUninventory.js'
const activeTab = ref('qualified')
const tabs = [
  { label: '合格库存', name: 'qualified' },
  { label: '不合格库存', name: 'unqualified' }
]
const tableData = ref([])
const total = ref(0)
const loadStatus = ref('loadmore') // loadmore | loading | nomore | error
@@ -143,15 +118,13 @@
})
const { searchForm, quantityForm } = toRefs(data)
const isQualified = () => activeTab.value === 'qualified'
const getList = () => {
  const isFirstPage = page.current === 1
  if (isFirstPage) {
    uni.showLoading({ title: '加载中...', mask: true })
  }
  const params = { ...page, productName: searchForm.value.productName }
  const api = isQualified() ? getStockInventoryListPage : getStockUninventoryListPage
  api(params)
  getStockInventoryListPage(params)
    .then(res => {
      uni.hideLoading()
      const records = res.data?.records || []
@@ -184,12 +157,6 @@
  getList()
}
watch(activeTab, () => {
  page.current = 1
  loadStatus.value = 'loadmore'
  getList()
})
const handleQuery = () => {
  page.current = 1
  loadStatus.value = 'loadmore'
@@ -197,9 +164,8 @@
}
const goAdd = () => {
  const type = isQualified() ? '0' : '1'
  uni.navigateTo({
    url: `/pages/inventoryManagement/stockManagement/add?type=${type}`
    url: `/pages/inventoryManagement/stockManagement/add?type=0`
  })
}
@@ -220,12 +186,11 @@
  try {
    uni.setStorageSync('stockSubtractRecord', JSON.stringify({
      item: row,
      type: isQualified() ? '0' : '1'
      type: '0'
    }))
  } catch (e) {}
  const typeParam = isQualified() ? '0' : '1'
  uni.navigateTo({
    url: `/pages/inventoryManagement/stockManagement/subtract?type=${typeParam}&id=${row.id}`
    url: `/pages/inventoryManagement/stockManagement/subtract?type=0&id=${row.id}`
  })
}
const openFrozen = (row) => {
@@ -256,9 +221,9 @@
  const base = { id, lockedQuantity: num }
  let promise
  if (quantityOp.value === 'frozen') {
    promise = isQualified() ? frozenStockInventory(base) : frozenStockUninventory(base)
    promise = frozenStockInventory(base)
  } else {
    promise = isQualified() ? thawStockInventory(base) : thawStockUninventory(base)
    promise = thawStockInventory(base)
  }
  promise.then(() => {
    uni.showToast({ title: '操作成功', icon: 'success' })
@@ -272,7 +237,7 @@
  try {
    uni.setStorageSync('stockDetailItem', JSON.stringify({
      item,
      type: isQualified() ? '0' : '1'
      type: '0'
    }))
  } catch (e) {}
  if (!item.id) {
@@ -294,25 +259,6 @@
  min-height: 100vh;
  background: #f5f5f5;
  padding-bottom: 120rpx;
}
.tabs-wrap {
  display: flex;
  background: #fff;
  padding: 24rpx;
  gap: 24rpx;
}
.tab-item {
  flex: 1;
  text-align: center;
  padding: 20rpx;
  border-radius: 12rpx;
  background: #f0f0f0;
  font-size: 28rpx;
  color: #666;
}
.tab-item.active {
  background: #2979ff;
  color: #fff;
}
.search-section {
  background: #fff;
src/pages/inventoryManagement/stockManagement/subtract.vue
@@ -92,7 +92,6 @@
import dayjs from 'dayjs'
import PageHeader from '@/components/PageHeader.vue'
import { subtractStockInventory } from '@/api/inventoryManagement/stockInventory.js'
import { subtractStockUnInventory } from '@/api/inventoryManagement/stockUninventory.js'
const form = reactive({
  id: undefined,
@@ -124,7 +123,7 @@
  remark: ''
})
const type = ref('0') // 0 åˆæ ¼åº“存,1 ä¸åˆæ ¼åº“å­˜
const type = ref('0') // å›ºå®šåˆæ ¼åº“å­˜
const showWeighingDatePicker = ref(false)
const weighingDateValue = ref(Date.now())
@@ -135,9 +134,7 @@
})
onLoad((options) => {
  if (options && options.type != null) {
    type.value = options.type
  }
  type.value = '0'
  const cached = uni.getStorageSync('stockSubtractRecord')
  if (cached) {
    try {
@@ -195,8 +192,7 @@
    return
  }
  const payload = { ...form }
  const api = type.value === '0' ? subtractStockInventory : subtractStockUnInventory
  api(payload)
  subtractStockInventory(payload)
    .then(() => {
      uni.showToast({ title: '出库成功', icon: 'success' })
      setTimeout(() => {
src/pages/qualityManagement/InspectItem/index.vue
@@ -64,7 +64,7 @@
        <view class="dialog-header">
          <text class="dialog-title">{{ operationType === 'add' ? '新增检测项目' : '修改检测项目' }}</text>
        </view>
        <up-form :model="form" ref="formRef" label-width="100" label-position="top">
        <up-form :model="form" :rules="rules" ref="formRef" label-width="100" label-position="top">
          <up-form-item label="检测项目" prop="name" required borderBottom>
            <up-input v-model="form.name" placeholder="请输入检测项目名称" border="surround" />
          </up-form-item>
@@ -204,25 +204,27 @@
  dialogVisible.value = false;
};
const submitForm = () => {
  formRef.value.validate().then(res => {
    submitLoading.value = true;
    qualityInspectItemSave(form).then(() => {
const submitForm = async () => {
  if (!formRef.value) return
  const valid = await formRef.value.validate().catch(() => false)
  if (!valid) return
  submitLoading.value = true;
  qualityInspectItemSave(form)
    .then(() => {
      toast(operationType.value === 'add' ? '新增成功' : '修改成功');
      dialogVisible.value = false;
      handleQuery();
    }).finally(() => {
    })
    .finally(() => {
      submitLoading.value = false;
    });
  }).catch(errors => {
    console.log('验证失败', errors);
  });
};
const handleDelete = (row) => {
  showConfirm('确认删除该检测项目吗?').then(res => {
    if (res.confirm) {
      qualityInspectItemDelete({ id: row.id }).then(() => {
      // å¯¹é½ PC ç«¯ï¼šåˆ é™¤æŽ¥å£æŽ¥æ”¶ id æ•°ç»„
      qualityInspectItemDelete([row.id]).then(() => {
        toast('删除成功');
        handleQuery();
      });
src/pages/qualityManagement/nonconformingManagement/form.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,458 @@
<template>
  <view class="nonconforming-form-page">
    <PageHeader :title="pageTitle" @back="goBack" />
    <scroll-view scroll-y class="content-scroll">
      <view class="form-section">
        <view class="form-row">
          <text class="form-label required">产品名称</text>
          <view class="selector-trigger" @click="openProductSelector" :class="{ disabled: isEdit }">
            <text class="selector-text" :class="{ placeholder: !form.productName }">
              {{ form.productName || '请选择' }}
            </text>
            <up-icon name="arrow-right" size="16" color="#999"></up-icon>
          </view>
        </view>
        <view class="form-row">
          <text class="form-label required">规格型号</text>
          <view class="selector-trigger" @click="openModelSelector" :class="{ disabled: !form.productId || isEdit }">
            <text class="selector-text" :class="{ placeholder: !form.model }">
              {{ form.model || '请选择' }}
            </text>
            <up-icon name="arrow-right" size="16" color="#999"></up-icon>
          </view>
        </view>
        <view class="form-row">
          <text class="form-label">单位</text>
          <up-input v-model="form.unit" disabled placeholder="自动带出" />
        </view>
        <view class="form-row">
          <text class="form-label required">批号</text>
          <up-input v-model="form.batchNo" placeholder="请输入" />
        </view>
        <view class="form-row">
          <text class="form-label required">检验类型</text>
          <view class="selector-trigger" @click="showTypeSelect = true">
            <text class="selector-text" :class="{ placeholder: form.checkType === undefined || form.checkType === '' }">
              {{ checkTypeLabel || '请选择' }}
            </text>
            <up-icon name="arrow-down" size="14" color="#999"></up-icon>
          </view>
        </view>
        <view class="form-row">
          <text class="form-label required">检验员</text>
          <view class="selector-trigger" @click="showUserSelect = true">
            <text class="selector-text" :class="{ placeholder: !form.checkName }">
              {{ form.checkName || '请选择' }}
            </text>
            <up-icon name="arrow-down" size="14" color="#999"></up-icon>
          </view>
        </view>
        <view class="form-row">
          <text class="form-label required">检测日期</text>
          <view class="selector-trigger" @click="openCheckTimePicker">
            <text class="selector-text" :class="{ placeholder: !form.checkTime }">
              {{ form.checkTime || '请选择' }}
            </text>
            <up-icon name="calendar" size="16" color="#999"></up-icon>
          </view>
        </view>
        <view class="form-row">
          <text class="form-label required">不合格现象</text>
          <up-textarea v-model="form.defectivePhenomena" placeholder="请输入" count border="surround" />
        </view>
      </view>
      <view class="form-section">
        <view class="section-title">默认处理信息</view>
        <view class="form-row">
          <text class="form-label required">处理结果</text>
          <up-input :modelValue="dealResultLabel || '报废'" disabled />
        </view>
        <view class="form-row">
          <text class="form-label">处理人</text>
          <up-input v-model="form.dealName" placeholder="选填" />
        </view>
        <view class="form-row">
          <text class="form-label">处理日期</text>
          <view class="selector-trigger" @click="openDealTimePicker">
            <text class="selector-text" :class="{ placeholder: !form.dealTime }">
              {{ form.dealTime || '请选择' }}
            </text>
            <up-icon name="calendar" size="16" color="#999"></up-icon>
          </view>
        </view>
      </view>
    </scroll-view>
    <view class="bottom-bar">
      <view class="btn-submit" @click="handleSubmit" :class="{ disabled: submitting }">
        {{ submitting ? '提交中...' : '提交' }}
      </view>
    </view>
    <!-- äº§å“é€‰æ‹© -->
    <up-popup :show="showProductPopup" mode="bottom" @close="showProductPopup = false">
      <view class="popup">
        <view class="popup-header">
          <text class="popup-title">选择产品</text>
        </view>
        <scroll-view scroll-y class="popup-list">
          <view
            v-for="(item, idx) in productOptions"
            :key="item.value || idx"
            class="popup-item"
            @click="selectProduct(item)"
          >
            <text class="popup-item-title">{{ item.label }}</text>
          </view>
          <view v-if="!productLoading && productOptions.length === 0" class="no-data">暂无数据</view>
        </scroll-view>
      </view>
    </up-popup>
    <!-- åž‹å·é€‰æ‹© -->
    <up-popup :show="showModelPopup" mode="bottom" @close="showModelPopup = false">
      <view class="popup">
        <view class="popup-header">
          <text class="popup-title">选择规格型号</text>
        </view>
        <scroll-view scroll-y class="popup-list">
          <view
            v-for="(item, idx) in modelOptions"
            :key="item.id || idx"
            class="popup-item"
            @click="selectModel(item)"
          >
            <view class="popup-item-row">
              <text class="popup-item-title">{{ item.model }}</text>
              <text class="popup-item-sub">{{ item.unit }}</text>
            </view>
          </view>
          <view v-if="!modelLoading && modelOptions.length === 0" class="no-data">暂无数据</view>
        </scroll-view>
      </view>
    </up-popup>
    <!-- æ£€éªŒç±»åž‹ -->
    <up-action-sheet
      :actions="checkTypeActions"
      :show="showTypeSelect"
      @close="showTypeSelect = false"
      @select="selectCheckType"
      title="请选择检验类型"
    />
    <!-- æ£€éªŒå‘˜ -->
    <up-action-sheet
      :actions="userActions"
      :show="showUserSelect"
      @close="showUserSelect = false"
      @select="selectUser"
      title="请选择检验员"
    />
    <!-- æ—¥æœŸé€‰æ‹©å™¨ï¼šæ£€æµ‹æ—¥æœŸ -->
    <up-datetime-picker
      :show="showCheckTimePicker"
      v-model="checkTimeValue"
      mode="date"
      @confirm="confirmCheckTime"
      @cancel="showCheckTimePicker = false"
    />
    <!-- æ—¥æœŸé€‰æ‹©å™¨ï¼šå¤„理日期 -->
    <up-datetime-picker
      :show="showDealTimePicker"
      v-model="dealTimeValue"
      mode="date"
      @confirm="confirmDealTime"
      @cancel="showDealTimePicker = false"
    />
  </view>
</template>
<script setup>
import { computed, reactive, ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import dayjs from 'dayjs'
import PageHeader from '@/components/PageHeader.vue'
import { useDict } from '@/utils/dict'
import { toast } from '@/utils/common'
import { productTreeList, modelList } from '@/api/basicData/product.js'
import { userListNoPage } from '@/api/system/user.js'
import {
  getQualityUnqualifiedInfo,
  qualityUnqualifiedAdd,
  qualityUnqualifiedUpdate
} from '@/api/qualityManagement/nonconformingManagement.js'
const pageType = ref('add') // add | edit
const id = ref('')
const submitting = ref(false)
const isEdit = computed(() => pageType.value === 'edit')
const pageTitle = computed(() => (isEdit.value ? '编辑不合格管理' : '新增不合格管理'))
const { rejection_handling } = useDict('rejection_handling')
const dealResultLabel = computed(() => {
  const list = rejection_handling?.value || []
  const v = form.dealResult
  return (list || []).find(it => String(it.value) === String(v))?.label || ''
})
const getScrapDealResultValue = () => {
  const list = rejection_handling?.value || []
  const scrap = (list || []).find(it => String(it?.label ?? '') === '报废')
  return scrap?.value ?? ''
}
const form = reactive({
  id: undefined,
  productId: '',
  productName: '',
  productModelId: '',
  model: '',
  unit: '',
  batchNo: '',
  checkType: undefined,
  checkName: '',
  checkTime: '',
  defectivePhenomena: '',
  dealResult: '',
  dealName: '',
  dealTime: ''
})
// é€‰æ‹©å™¨æ•°æ®
const showProductPopup = ref(false)
const productLoading = ref(false)
const productOptions = ref([])
const showModelPopup = ref(false)
const modelLoading = ref(false)
const modelOptions = ref([])
const showTypeSelect = ref(false)
const checkTypeActions = [
  { name: '入厂检', value: 0 },
  { name: '车间检', value: 1 },
  { name: '出厂检', value: 2 }
]
const checkTypeLabel = computed(() => {
  const v = form.checkType
  return checkTypeActions.find(it => String(it.value) === String(v))?.name || ''
})
const showUserSelect = ref(false)
const userActions = ref([])
const showCheckTimePicker = ref(false)
const checkTimeValue = ref(Date.now())
const showDealTimePicker = ref(false)
const dealTimeValue = ref(Date.now())
const loadProducts = async () => {
  productLoading.value = true
  try {
    const res = await productTreeList()
    const list =
      (Array.isArray(res) ? res : null) ||
      (Array.isArray(res?.data) ? res.data : null) ||
      (Array.isArray(res?.records) ? res.records : null) ||
      (Array.isArray(res?.data?.records) ? res.data.records : null) ||
      []
    // ä»…取叶子节点(可选产品);并转为 action åˆ—表
    const flat = []
    const walk = (nodes) => {
      ;(nodes || []).forEach(n => {
        const children = Array.isArray(n?.children) ? n.children : []
        if (children.length > 0) walk(children)
        else {
          const value = n?.value ?? n?.id
          const label = n?.label || n?.productName || n?.name || ''
          flat.push({ label, value })
        }
      })
    }
    walk(list)
    productOptions.value = flat.filter(it => it.value)
  } finally {
    productLoading.value = false
  }
}
const openProductSelector = async () => {
  if (isEdit.value) return
  showProductPopup.value = true
  if (productOptions.value.length === 0) {
    await loadProducts()
  }
}
const selectProduct = (item) => {
  if (isEdit.value) return
  form.productId = item.value
  form.productName = item.label
  form.productModelId = ''
  form.model = ''
  form.unit = ''
  modelOptions.value = []
  showProductPopup.value = false
}
const loadModels = async () => {
  if (!form.productId) return
  modelLoading.value = true
  try {
    const res = await modelList({ id: form.productId })
    modelOptions.value = Array.isArray(res) ? res : (res?.data || [])
  } finally {
    modelLoading.value = false
  }
}
const openModelSelector = async () => {
  if (isEdit.value) return
  if (!form.productId) {
    toast('请先选择产品名称')
    return
  }
  showModelPopup.value = true
  if (modelOptions.value.length === 0) {
    await loadModels()
  }
}
const selectModel = (item) => {
  if (isEdit.value) return
  form.productModelId = item?.id
  form.model = item?.model || ''
  form.unit = item?.unit || ''
  showModelPopup.value = false
}
const selectCheckType = (e) => {
  form.checkType = e.value
  showTypeSelect.value = false
}
const loadUsers = async () => {
  const res = await userListNoPage()
  const list = res?.data || []
  userActions.value = (list || []).map(u => ({ name: u.nickName, value: u.nickName })).filter(it => it.value)
}
const selectUser = (e) => {
  form.checkName = e.value
  showUserSelect.value = false
}
const openCheckTimePicker = () => {
  checkTimeValue.value = form.checkTime ? dayjs(form.checkTime, 'YYYY-MM-DD').valueOf() : Date.now()
  showCheckTimePicker.value = true
}
const confirmCheckTime = (e) => {
  const ts = e?.value ?? checkTimeValue.value
  form.checkTime = dayjs(ts).format('YYYY-MM-DD')
  showCheckTimePicker.value = false
}
const openDealTimePicker = () => {
  dealTimeValue.value = form.dealTime ? dayjs(form.dealTime, 'YYYY-MM-DD').valueOf() : Date.now()
  showDealTimePicker.value = true
}
const confirmDealTime = (e) => {
  const ts = e?.value ?? dealTimeValue.value
  form.dealTime = dayjs(ts).format('YYYY-MM-DD')
  showDealTimePicker.value = false
}
const loadDetail = async () => {
  if (!id.value) return
  const res = await getQualityUnqualifiedInfo(id.value)
  const d = res?.data || {}
  Object.assign(form, {
    id: d.id,
    productId: d.productId,
    productName: d.productName,
    productModelId: d.productModelId,
    model: d.model,
    unit: d.unit,
    batchNo: d.batchNo,
    // å…¼å®¹åŽç«¯è¿”回字段:优先 checkType,其次 inspectType
    checkType: d.checkType ?? d.inspectType,
    checkName: d.checkName,
    checkTime: d.checkTime,
    defectivePhenomena: d.defectivePhenomena,
    dealResult: d.dealResult,
    dealName: d.dealName,
    dealTime: d.dealTime
  })
}
const validate = () => {
  if (!form.productId) return toast('请选择产品名称'), false
  if (!form.productModelId) return toast('请选择规格型号'), false
  if (!form.batchNo) return toast('请输入批号'), false
  if (form.checkType === undefined || form.checkType === '') return toast('请选择检验类型'), false
  if (!form.checkName) return toast('请选择检验员'), false
  if (!form.checkTime) return toast('请选择检测日期'), false
  if (!form.defectivePhenomena) return toast('请输入不合格现象'), false
  return true
}
const handleSubmit = async () => {
  if (submitting.value) return
  if (!validate()) return
  submitting.value = true
  try {
    // å¤„理结果默认报废
    if (!form.dealResult) form.dealResult = getScrapDealResultValue()
    const payload = { ...form }
    delete payload.inspectState
    const api = isEdit.value ? qualityUnqualifiedUpdate : qualityUnqualifiedAdd
    await api(payload)
    toast('提交成功')
    setTimeout(() => uni.navigateBack(), 400)
  } catch (e) {
    toast('提交失败')
  } finally {
    submitting.value = false
  }
}
const goBack = () => uni.navigateBack()
onLoad(async (options) => {
  pageType.value = options?.type || 'add'
  id.value = options?.id || ''
  form.dealResult = getScrapDealResultValue()
  await loadUsers()
  if (isEdit.value) {
    await loadDetail()
  }
})
</script>
<style lang="scss" scoped>
.nonconforming-form-page { min-height: 100vh; background: #f5f5f5; padding-bottom: 120rpx; }
.content-scroll { height: calc(100vh - 100rpx); }
.form-section { background: #fff; margin: 24rpx; padding: 24rpx; border-radius: 16rpx; }
.section-title { font-size: 28rpx; font-weight: 500; color: #333; margin-bottom: 12rpx; }
.form-row { margin-bottom: 24rpx; }
.form-label { display: block; font-size: 26rpx; color: #666; margin-bottom: 12rpx; }
.form-label.required:before { content: "*"; color: #f56c6c; margin-right: 6rpx; }
.selector-trigger { display: flex; align-items: center; justify-content: space-between; padding: 20rpx 24rpx; background: #f5f5f5; border-radius: 12rpx; }
.selector-trigger.disabled { opacity: 0.6; }
.selector-text { font-size: 28rpx; color: #333; }
.selector-text.placeholder { color: #999; }
.bottom-bar { position: fixed; left: 0; right: 0; bottom: 0; padding: 16rpx 24rpx calc(16rpx + env(safe-area-inset-bottom)); background: #fff; box-shadow: 0 -4rpx 16rpx rgba(0,0,0,0.04); }
.btn-submit { height: 88rpx; border-radius: 999rpx; background: #2979ff; color: #fff; font-size: 30rpx; display: flex; align-items: center; justify-content: center; }
.btn-submit.disabled { opacity: 0.6; }
.popup { background: #fff; border-top-left-radius: 16rpx; border-top-right-radius: 16rpx; overflow: hidden; }
.popup-header { padding: 24rpx; border-bottom: 1rpx solid #eee; }
.popup-title { font-size: 30rpx; font-weight: 500; color: #333; }
.popup-list { max-height: 60vh; padding: 12rpx 0; }
.popup-item { padding: 22rpx 24rpx; border-bottom: 1rpx solid #f0f0f0; }
.popup-item-title { font-size: 28rpx; color: #333; }
.popup-item-row { display: flex; justify-content: space-between; gap: 16rpx; }
.popup-item-sub { font-size: 24rpx; color: #999; }
.no-data { text-align: center; padding: 60rpx 0; color: #999; font-size: 28rpx; }
</style>
src/pages/qualityManagement/nonconformingManagement/index.vue
@@ -2,74 +2,69 @@
  <view class="nonconforming-management-page">
    <PageHeader title="不合格品管理" @back="goBack" />
    
    <!-- æœç´¢ä¸Žç­›é€‰ -->
    <!-- æœç´¢ä¸Žç­›é€‰ï¼ˆæ ·å¼å‚照仓储物流模块) -->
    <view class="search-section">
      <up-search
        placeholder="请输入产品名称搜索"
        v-model="searchForm.productName"
        @search="handleQuery"
        @custom="handleQuery"
        @clear="handleQuery"
        :show-action="true"
        action-text="搜索"
        :animation="true"
        customStyle="margin-bottom: 20rpx"
      ></up-search>
      <view class="search-row">
        <view class="search-input-wrap">
          <up-input v-model="searchForm.productName" placeholder="产品名称" clearable />
        </view>
        <view class="btn-search" @click="handleQuery">
          <view class="btn-search-inner">
            <up-icon name="search" size="22" color="#fff"></up-icon>
            <text>搜索</text>
          </view>
        </view>
      </view>
      <view class="filter-row">
        <view class="filter-item" @click="showTypeSelect = true">
          <text>{{ typeLabel }}</text>
          <up-icon name="arrow-down" size="14" color="#999"></up-icon>
        </view>
        <view class="filter-item" @click="showStatusSelect = true">
          <text>{{ statusLabel }}</text>
          <up-icon name="arrow-down" size="14" color="#999"></up-icon>
        <view class="filter-item" @click="openDateRange">
          <text>{{ dateRangeLabel }}</text>
          <up-icon name="calendar" size="14" color="#999"></up-icon>
        </view>
      </view>
    </view>
    <!-- åˆ—表区域 -->
    <view class="list-container" v-if="tableData.length > 0">
      <view v-for="(item, index) in tableData" :key="index" class="list-item">
        <view class="item-header">
          <text class="product-name">{{ item.productName }}</text>
          <up-tag :text="getStatusText(item.inspectState)" :type="getStatusType(item.inspectState)" size="mini"></up-tag>
        </view>
        <view class="item-content">
          <view class="item-row">
            <text class="item-label">类别:</text>
            <text class="item-value">{{ getInspectTypeText(item.inspectType) }}</text>
    <view class="list-section">
      <view v-if="tableData.length > 0">
        <view v-for="(item, index) in tableData" :key="item.id || index" class="card-item">
          <view class="card-click" @click="openForm('edit', item)">
            <view class="card-header">
              <view class="header-main">
                <text class="product-name">{{ item.productName }}</text>
              </view>
              <view class="header-sub">
                <text class="sub-title">{{ item.model || '-' }}</text>
                <up-tag :text="getInspectTypeText(item.checkType ?? item.inspectType)" type="primary" size="mini" />
              </view>
            </view>
            <up-divider />
            <view class="card-body">
              <view class="row"><text class="l">检测日期</text><text class="r">{{ item.checkTime || '-' }}</text></view>
              <view class="row"><text class="l">批号</text><text class="r">{{ item.batchNo || '-' }}</text></view>
              <view class="row"><text class="l">检验员</text><text class="r">{{ item.checkName || '-' }}</text></view>
              <view class="row"><text class="l">不合格现象</text><text class="r text-error">{{ item.defectivePhenomena || '-' }}</text></view>
              <view class="row" v-if="item.inspectState == 1"><text class="l">处理结果</text><text class="r text-success">{{ getDealResultLabel(item.dealResult) || item.dealResult || '-' }}</text></view>
              <view class="row" v-if="item.inspectState == 1"><text class="l">处理人</text><text class="r">{{ item.dealName || '-' }}</text></view>
              <view class="row" v-if="item.inspectState == 1"><text class="l">处理日期</text><text class="r">{{ item.dealTime || '-' }}</text></view>
            </view>
          </view>
          <view class="item-row">
            <text class="item-label">检测日期:</text>
            <text class="item-value">{{ item.checkTime || '-' }}</text>
          </view>
          <view class="item-row">
            <text class="item-label">规格型号:</text>
            <text class="item-value">{{ item.model || '-' }}</text>
          </view>
          <view class="item-row">
            <text class="item-label">不合格现象:</text>
            <text class="item-value text-error">{{ item.defectivePhenomena || '-' }}</text>
          </view>
          <view class="item-row" v-if="item.inspectState === 1">
            <text class="item-label">处理结果:</text>
            <text class="item-value text-success">{{ item.dealResult || '-' }}</text>
          <view class="card-actions">
            <view class="btn-link btn-link-primary" v-if="item.inspectState == 0" @click.stop="openDealDialog(item)">处理</view>
            <view class="btn-link btn-link-plain" v-if="item.inspectState == 0" @click.stop="openForm('edit', item)">编辑</view>
            <view class="btn-link btn-link-warn" @click.stop="handleDelete(item)">删除</view>
          </view>
        </view>
        <view class="item-actions">
          <up-button v-if="item.inspectState === 0" type="primary" size="mini" @click.stop="openDealDialog(item)">处理</up-button>
          <up-button type="error" size="mini" @click.stop="handleDelete(item)">删除</up-button>
        <view class="load-more-wrap">
          <u-loadmore :status="loadStatus" @loadmore="loadMore" />
        </view>
      </view>
      <view class="pagination-container">
        <up-loadmore :status="loadStatus" @loadmore="getList" />
      </view>
      <view v-else class="no-data">暂无数据</view>
    </view>
    
    <view v-else class="no-data">
      <up-empty mode="data" text="暂无数据"></up-empty>
    </view>
    <!-- ç±»åž‹é€‰æ‹©å™¨ -->
    <up-action-sheet
      :actions="typeActions"
@@ -77,15 +72,6 @@
      @close="showTypeSelect = false"
      @select="selectType"
      title="请选择类别"
    ></up-action-sheet>
    <!-- çŠ¶æ€é€‰æ‹©å™¨ -->
    <up-action-sheet
      :actions="statusActions"
      :show="showStatusSelect"
      @close="showStatusSelect = false"
      @select="selectStatus"
      title="请选择状态"
    ></up-action-sheet>
    <!-- å¤„理弹窗 -->
@@ -97,10 +83,19 @@
        <up-form :model="dealForm" ref="dealFormRef" label-width="100" label-position="top">
          <view class="info-summary">
            <text class="summary-text">产品:{{ currentItem?.productName }}</text>
            <text class="summary-text">不合格现象:{{ currentItem?.defectivePhenomena }}</text>
            <text class="summary-text">检测日期:{{ currentItem?.checkTime || '-' }}</text>
          </view>
          <up-form-item label="不合格现象" prop="defectivePhenomena" required borderBottom>
            <up-textarea v-model="dealForm.defectivePhenomena" placeholder="请输入不合格现象" count border="surround" />
          </up-form-item>
          <up-form-item label="处理结果" prop="dealResult" required borderBottom>
            <up-textarea v-model="dealForm.dealResult" placeholder="请输入处理结果" count border="surround" />
            <view class="selector-trigger" @click="showDealResultSelect = true">
              <text class="selector-text" :class="{ placeholder: !dealResultLabel }">{{ dealResultLabel || '请选择处理结果' }}</text>
              <up-icon name="arrow-down" size="14" color="#999"></up-icon>
            </view>
          </up-form-item>
          <up-form-item label="处理人" prop="dealName" required borderBottom>
            <up-input v-model="dealForm.dealName" placeholder="请输入处理人" border="surround" />
          </up-form-item>
          <up-form-item label="处理日期" prop="dealTime" required borderBottom>
            <up-input
@@ -119,6 +114,15 @@
      </view>
    </up-popup>
    <!-- å¤„理结果选择器 -->
    <up-action-sheet
      :actions="dealResultActions"
      :show="showDealResultSelect"
      @close="showDealResultSelect = false"
      @select="selectDealResult"
      title="请选择处理结果"
    />
    <!-- æ—¥æœŸé€‰æ‹©å™¨ -->
    <up-datetime-picker
      :show="showDatePicker"
@@ -127,23 +131,49 @@
      @confirm="confirmDate"
      @cancel="showDatePicker = false"
    ></up-datetime-picker>
    <!-- å½•入日期范围选择:开始 -->
    <up-datetime-picker
      :show="showEntryStartPicker"
      v-model="entryStartValue"
      mode="date"
      @confirm="confirmEntryStart"
      @cancel="showEntryStartPicker = false"
    />
    <!-- å½•入日期范围选择:结束 -->
    <up-datetime-picker
      :show="showEntryEndPicker"
      v-model="entryEndValue"
      mode="date"
      @confirm="confirmEntryEnd"
      @cancel="showEntryEndPicker = false"
    />
    <!-- å³ä¸‹è§’新增按钮 -->
    <view class="fab-button" @click="openForm('add')">
      <up-icon name="plus" size="24" color="#ffffff"></up-icon>
    </view>
  </view>
</template>
<script setup>
import { ref, reactive, onMounted, computed } from 'vue';
import { ref, reactive, computed } from 'vue';
import {
  qualityUnqualifiedListPage,
  qualityUnqualifiedDeal,
  qualityUnqualifiedDel
} from '@/api/qualityManagement/nonconformingManagement.js';
import { toast, showConfirm } from '@/utils/common';
import { useDict } from '@/utils/dict'
import dayjs from 'dayjs';
import PageHeader from '@/components/PageHeader.vue'
import { onReachBottom, onShow } from '@dcloudio/uni-app'
const searchForm = reactive({
  productName: '',
  inspectType: '',
  inspectState: ''
  checkType: '',
  entryDateStart: undefined,
  entryDateEnd: undefined
});
const tableData = ref([]);
@@ -162,54 +192,67 @@
  { name: '出厂检', value: '2' }
];
const typeLabel = computed(() => {
  const action = typeActions.find(a => a.value === searchForm.inspectType);
  const action = typeActions.find(a => a.value === String(searchForm.checkType ?? ''));
  return action ? action.name : '全部类别';
});
const showStatusSelect = ref(false);
const statusActions = [
  { name: '全部', value: '' },
  { name: '待处理', value: '0' },
  { name: '已处理', value: '1' }
];
const statusLabel = computed(() => {
  const action = statusActions.find(a => a.value === searchForm.inspectState);
  return action ? action.name : '全部状态';
});
const dateRangeLabel = computed(() => {
  if (searchForm.entryDateStart && searchForm.entryDateEnd) return `${searchForm.entryDateStart}~${searchForm.entryDateEnd}`
  if (searchForm.entryDateStart) return `${searchForm.entryDateStart}~`
  if (searchForm.entryDateEnd) return `~${searchForm.entryDateEnd}`
  return '检测日期'
})
const dealDialogVisible = ref(false);
const submitLoading = ref(false);
const currentItem = ref(null);
const dealForm = reactive({
  id: null,
  defectivePhenomena: '',
  dealResult: '',
  dealName: '',
  dealTime: dayjs().format('YYYY-MM-DD')
});
const { rejection_handling } = useDict('rejection_handling')
const showDealResultSelect = ref(false)
const dealResultActions = computed(() => {
  const list = rejection_handling?.value || []
  return (list || []).map(it => ({ name: it.label, value: it.value }))
})
const dealResultLabel = computed(() => {
  const list = rejection_handling?.value || []
  const v = dealForm.dealResult
  return (list || []).find(it => String(it.value) === String(v))?.label || ''
})
function getDealResultLabel(value) {
  const list = rejection_handling?.value || []
  return (list || []).find(it => String(it.value) === String(value))?.label || ''
}
const showDatePicker = ref(false);
const dateValue = ref(Number(new Date()));
const showEntryStartPicker = ref(false)
const showEntryEndPicker = ref(false)
const entryStartValue = ref(Date.now())
const entryEndValue = ref(Date.now())
const getInspectTypeText = (type) => {
  const types = { '0': '入厂检', '1': '车间检', '2': '出厂检' };
  return types[type] || '-';
};
const getStatusText = (state) => {
  return state === 1 ? '已处理' : '待处理';
};
const getStatusType = (state) => {
  return state === 1 ? 'success' : 'warning';
  return types[String(type ?? '')] || '-';
};
const getList = () => {
  if (loadStatus.value === 'loading' || (page.total > 0 && tableData.value.length >= page.total)) return;
  loadStatus.value = 'loading';
  const isFirstPage = page.current === 1
  if (loadStatus.value === 'loading' || (!isFirstPage && page.total > 0 && tableData.value.length >= page.total)) return
  loadStatus.value = 'loading'
  const params = {
    productName: searchForm.productName || null,
    inspectType: searchForm.inspectType || null,
    inspectState: searchForm.inspectState || null,
    checkType: searchForm.checkType === '' ? null : searchForm.checkType,
    entryDateStart: searchForm.entryDateStart,
    entryDateEnd: searchForm.entryDateEnd,
    current: page.current,
    size: page.size
  };
@@ -227,12 +270,18 @@
      loadStatus.value = 'nomore';
    } else {
      loadStatus.value = 'loadmore';
      page.current++;
    }
  }).catch(() => {
    loadStatus.value = 'loadmore';
    loadStatus.value = 'error';
  });
};
const loadMore = () => {
  if (loadStatus.value === 'nomore' || loadStatus.value === 'loading') return
  loadStatus.value = 'loading'
  page.current++
  getList()
}
const handleQuery = () => {
  page.current = 1;
@@ -243,27 +292,55 @@
};
const selectType = (e) => {
  searchForm.inspectType = e.value;
  searchForm.checkType = e.value;
  handleQuery();
};
const selectStatus = (e) => {
  searchForm.inspectState = e.value;
  handleQuery();
};
const openDateRange = () => {
  entryStartValue.value = searchForm.entryDateStart ? dayjs(searchForm.entryDateStart, 'YYYY-MM-DD').valueOf() : Date.now()
  showEntryStartPicker.value = true
}
const confirmEntryStart = (e) => {
  const ts = e?.value ?? entryStartValue.value
  searchForm.entryDateStart = dayjs(ts).format('YYYY-MM-DD')
  showEntryStartPicker.value = false
  entryEndValue.value = searchForm.entryDateEnd ? dayjs(searchForm.entryDateEnd, 'YYYY-MM-DD').valueOf() : Date.now()
  showEntryEndPicker.value = true
}
const confirmEntryEnd = (e) => {
  const ts = e?.value ?? entryEndValue.value
  searchForm.entryDateEnd = dayjs(ts).format('YYYY-MM-DD')
  showEntryEndPicker.value = false
  handleQuery()
}
const openDealDialog = (item) => {
  currentItem.value = item;
  dealForm.id = item.id;
  dealForm.dealResult = '';
  dealForm.defectivePhenomena = item.defectivePhenomena || ''
  dealForm.dealResult = item.dealResult || '';
  dealForm.dealName = item.dealName || ''
  dealForm.dealTime = dayjs().format('YYYY-MM-DD');
  dealDialogVisible.value = true;
};
const selectDealResult = (e) => {
  dealForm.dealResult = e.value
  showDealResultSelect.value = false
}
const submitDeal = () => {
  if (!dealForm.dealResult) {
    toast('请输入处理结果');
  if (!dealForm.defectivePhenomena) {
    toast('请输入不合格现象')
    return;
  }
  if (!dealForm.dealResult) {
    toast('请选择处理结果')
    return
  }
  if (!dealForm.dealName) {
    toast('请输入处理人')
    return
  }
  submitLoading.value = true;
  qualityUnqualifiedDeal(dealForm).then(() => {
@@ -291,89 +368,115 @@
  showDatePicker.value = false;
};
const openForm = (type, row) => {
  if (type !== 'add' && row?.inspectState == 1) {
    toast('已处理的数据不能再编辑')
    return
  }
  const id = row?.id
  uni.navigateTo({
    url: `/pages/qualityManagement/nonconformingManagement/form?type=${type}${id ? `&id=${id}` : ''}`
  })
}
const goBack = () => {
  uni.navigateBack();
};
onMounted(() => {
  handleQuery();
});
onShow(() => {
  handleQuery()
})
onReachBottom(() => {
  loadMore()
})
</script>
<style lang="scss" scoped>
.nonconforming-management-page {
  padding-bottom: 20rpx;
  background-color: #f5f7fa;
  min-height: 100vh;
  background: #f5f5f5;
  padding-bottom: 120rpx;
}
.search-section {
  padding: 20rpx 30rpx;
  background-color: #ffffff;
  position: sticky;
  top: 0;
  z-index: 10;
  background: #fff;
  margin: 24rpx;
  padding: 24rpx;
  border-radius: 16rpx;
}
.search-row { display: flex; align-items: center; margin-bottom: 20rpx; }
.search-input-wrap { flex: 1; margin-right: 20rpx; min-width: 0; }
.btn-search {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 180rpx;
  min-height: 72rpx;
  flex-shrink: 0;
  padding: 20rpx 24rpx;
  background: #2979ff;
  color: #fff;
  border-radius: 12rpx;
  font-size: 28rpx;
  box-sizing: border-box;
  text-align: center;
}
.btn-search-inner { display: flex; flex-direction: row; align-items: center; justify-content: center; gap: 8rpx; }
.filter-row {
  display: flex;
  justify-content: space-around;
  padding: 10rpx 0;
  gap: 20rpx;
}
.filter-item {
  display: flex;
  align-items: center;
  gap: 10rpx;
  font-size: 28rpx;
  color: #606266;
}
.list-container {
  padding: 20rpx;
}
.list-item {
  background-color: #ffffff;
  border-radius: 16rpx;
  padding: 30rpx;
  margin-bottom: 20rpx;
  box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
}
.item-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 20rpx;
}
.product-name {
  font-size: 30rpx;
  font-weight: bold;
  color: #303133;
}
.item-content {
  margin-bottom: 20rpx;
}
.item-row {
  display: flex;
  margin-bottom: 10rpx;
}
.item-label {
  color: #909399;
  width: 180rpx;
  font-size: 28rpx;
}
.item-value {
  flex: 1;
  color: #303133;
  font-size: 28rpx;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 18rpx 20rpx;
  background: #f5f5f5;
  border-radius: 12rpx;
  font-size: 26rpx;
  color: #666;
}
.list-section { padding: 0 24rpx; }
.card-item {
  background: #fff;
  border-radius: 16rpx;
  padding: 16rpx 20rpx;
  margin-bottom: 20rpx;
  box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
}
.card-header { padding: 2rpx 0 6rpx; }
.header-main { display: flex; justify-content: space-between; align-items: center; gap: 16rpx; }
.product-name { font-size: 30rpx; font-weight: 500; color: #333; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.header-sub { display: flex; justify-content: space-between; gap: 16rpx; margin-top: 6rpx; }
.sub-title { font-size: 24rpx; color: #999; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sub-right { font-size: 24rpx; color: #999; flex-shrink: 0; }
.card-body .row { display: flex; justify-content: space-between; padding: 6rpx 0; font-size: 26rpx; }
.card-body .l { color: #666; width: 180rpx; flex-shrink: 0; }
.card-body .r { color: #333; flex: 1; text-align: right; word-break: break-all; }
.card-actions {
  display: flex;
  gap: 16rpx;
  justify-content: flex-end;
  align-items: center;
  margin-top: 12rpx;
  padding-top: 14rpx;
  border-top: 1rpx solid #eee;
}
.btn-link {
  font-size: 28rpx;
  padding: 10rpx 22rpx;
  border-radius: 999rpx;
  border: 1rpx solid transparent;
}
.btn-link-primary { color: #2979ff; border-color: rgba(41, 121, 255, 0.4); background: rgba(41, 121, 255, 0.08); }
.btn-link-plain { color: #606266; border-color: rgba(96, 98, 102, 0.35); background: rgba(96, 98, 102, 0.06); }
.btn-link-warn { color: #f56c6c; border-color: rgba(245, 108, 108, 0.55); background: rgba(245, 108, 108, 0.08); }
.text-error {
  color: #f56c6c;
@@ -383,17 +486,8 @@
  color: #67c23a;
}
.item-actions {
  display: flex;
  justify-content: flex-end;
  gap: 20rpx;
  border-top: 1rpx solid #ebeef5;
  padding-top: 20rpx;
}
.no-data {
  padding-top: 200rpx;
}
.no-data { text-align: center; padding: 60rpx 0; color: #999; font-size: 28rpx; }
.load-more-wrap { padding: 24rpx 24rpx 8rpx; }
.dialog-content {
  width: 650rpx;
@@ -431,4 +525,23 @@
.dialog-footer {
  margin-top: 40rpx;
}
.selector-trigger { display: flex; align-items: center; justify-content: space-between; padding: 20rpx 24rpx; background: #f5f5f5; border-radius: 12rpx; }
.selector-text { font-size: 28rpx; color: #333; }
.selector-text.placeholder { color: #999; }
.fab-button {
  position: fixed;
  right: 36rpx;
  bottom: 72rpx;
  width: 104rpx;
  height: 104rpx;
  border-radius: 52rpx;
  background: #2979ff;
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow: 0 10rpx 26rpx rgba(41, 121, 255, 0.35);
  z-index: 20;
}
</style>
src/pages/qualityManagement/rawMaterial/files.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,159 @@
<template>
  <view class="file-page">
    <PageHeader title="附件管理" @back="goBack" />
    <view class="file-list">
      <view v-if="files.length > 0">
        <view v-for="(f, idx) in files" :key="f.id || idx" class="file-item">
          <view class="file-info">
            <text class="file-name">{{ f.name }}</text>
          </view>
          <view class="file-actions">
            <view class="btn-link" @click="previewFile(f)">预览</view>
            <view class="btn-link danger" @click="confirmDelete(f)">删除</view>
          </view>
        </view>
      </view>
      <view v-else class="empty">暂无附件</view>
    </view>
    <view class="upload-bar">
      <view class="btn-upload" @click="chooseFile">上传附件</view>
    </view>
  </view>
</template>
<script setup>
import { ref } from 'vue'
import { onLoad, onShow } from '@dcloudio/uni-app'
import PageHeader from '@/components/PageHeader.vue'
import config from '@/config'
import { getToken } from '@/utils/auth'
import { qualityInspectFileAdd, qualityInspectFileDel, qualityInspectFileListPage } from '@/api/qualityManagement/qualityInspectFile.js'
const inspectId = ref('')
const files = ref([])
const getList = () => {
  if (!inspectId.value) return
  qualityInspectFileListPage({ inspectId: inspectId.value, current: 1, size: 200 })
    .then(res => {
      files.value = res?.data?.records || []
    })
    .catch(() => { files.value = [] })
}
const chooseFile = () => {
  if (!inspectId.value) {
    uni.showToast({ title: '缺少检验记录ID', icon: 'none' })
    return
  }
  uni.chooseFile({
    count: 1,
    success: (res) => {
      const path = res?.tempFiles?.[0]?.path
      const name = res?.tempFiles?.[0]?.name
      if (!path) return
      uploadOne(path, name)
    }
  })
}
const uploadOne = (filePath, originalName) => {
  const token = getToken()
  if (!token) {
    uni.showToast({ title: '未登录', icon: 'none' })
    return
  }
  uni.showLoading({ title: '上传中...', mask: true })
  uni.uploadFile({
    url: config.baseUrl + '/file/upload',
    filePath,
    name: 'file',
    header: { Authorization: 'Bearer ' + token },
    success: (res) => {
      uni.hideLoading()
      try {
        const resp = JSON.parse(res.data || '{}')
        if (resp.code !== 200) throw new Error('upload fail')
        const fileRow = {
          inspectId: inspectId.value,
          name: resp.data?.originalName || originalName || '附件',
          url: resp.data?.tempPath
        }
        qualityInspectFileAdd(fileRow).then(() => {
          uni.showToast({ title: '上传成功', icon: 'success' })
          getList()
        })
      } catch (e) {
        uni.showToast({ title: '上传失败', icon: 'none' })
      }
    },
    fail: () => {
      uni.hideLoading()
      uni.showToast({ title: '上传失败', icon: 'none' })
    }
  })
}
const previewFile = (f) => {
  const url = f?.url
  if (!url) return
  // H5/APP ç»Ÿä¸€ç”¨å¤–部打开
  uni.navigateTo({
    url: `/pages/inspectionUpload/filePreview?url=${encodeURIComponent(url)}`
  })
}
const confirmDelete = (f) => {
  if (!f?.id) return
  uni.showModal({
    title: '删除',
    content: '确认删除该附件?',
    success: (r) => {
      if (!r.confirm) return
      qualityInspectFileDel([f.id]).then(() => {
        uni.showToast({ title: '删除成功', icon: 'success' })
        getList()
      })
    }
  })
}
onLoad((options) => {
  inspectId.value = options?.id || ''
  if (!inspectId.value) {
    const cached = uni.getStorageSync('rawMaterialFilesCtx')
    if (cached) {
      try {
        const payload = typeof cached === 'string' ? JSON.parse(cached) : cached
        inspectId.value = payload?.id || ''
        uni.removeStorageSync('rawMaterialFilesCtx')
      } catch (e) {
        uni.removeStorageSync('rawMaterialFilesCtx')
      }
    }
  }
})
onShow(() => {
  getList()
})
const goBack = () => uni.navigateBack()
</script>
<style lang="scss" scoped>
.file-page { min-height: 100vh; background: #f5f5f5; padding-bottom: 120rpx; }
.file-list { margin: 24rpx; background: #fff; border-radius: 16rpx; padding: 12rpx 24rpx; }
.file-item { padding: 20rpx 0; border-bottom: 1rpx solid #eee; display: flex; justify-content: space-between; align-items: center; gap: 16rpx; }
.file-item:last-child { border-bottom: 0; }
.file-name { font-size: 28rpx; color: #333; }
.file-actions { display: flex; gap: 20rpx; }
.btn-link { color: #2979ff; font-size: 26rpx; }
.btn-link.danger { color: #f56c6c; }
.empty { text-align: center; padding: 60rpx 0; color: #999; font-size: 28rpx; }
.upload-bar { position: fixed; left: 0; right: 0; bottom: 0; padding: 16rpx 24rpx calc(16rpx + env(safe-area-inset-bottom)); background: #fff; box-shadow: 0 -4rpx 16rpx rgba(0,0,0,0.04); }
.btn-upload { height: 88rpx; border-radius: 999rpx; background: #2979ff; color: #fff; font-size: 30rpx; display: flex; align-items: center; justify-content: center; }
</style>
src/pages/qualityManagement/rawMaterial/form.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,418 @@
<template>
  <view class="rm-form-page">
    <PageHeader :title="operationType === 'add' ? '新增原料检' : '编辑原料检'" @back="goBack" />
    <scroll-view scroll-y class="content-scroll">
      <view class="section-card">
        <view class="section-title">基础信息</view>
        <view class="form-row">
          <text class="form-label required">产品名称</text>
          <view class="selector-trigger" @click="openProductSelector" :class="{ disabled: operationType === 'edit' }">
            <text class="selector-text" :class="{ placeholder: !form.productName }">
              {{ form.productName || '请选择产品' }}
            </text>
            <up-icon name="arrow-right" size="16" color="#999"></up-icon>
          </view>
        </view>
        <view class="form-row">
          <text class="form-label required">规格型号</text>
          <up-input v-model="form.model" disabled placeholder="请选择产品后自动带出" />
        </view>
        <view class="form-row">
          <text class="form-label">单位</text>
          <up-input v-model="form.unit" disabled placeholder="自动带出" />
        </view>
        <view class="form-row">
          <text class="form-label required">批号</text>
          <up-input v-model="form.batchNo" placeholder="请输入批号" />
        </view>
        <view class="form-row">
          <text class="form-label required">检验类型</text>
          <view class="selector-trigger" @click="showCheckTypeSheet = true">
            <text class="selector-text" :class="{ placeholder: form.checkType === '' || form.checkType == null }">
              {{ checkTypeLabel }}
            </text>
            <up-icon name="arrow-down" size="14" color="#999"></up-icon>
          </view>
        </view>
        <view class="form-row">
          <text class="form-label required">检测结果</text>
          <view class="selector-trigger" @click="showCheckResultSheet = true">
            <text class="selector-text" :class="{ placeholder: form.checkResult === '' || form.checkResult == null }">
              {{ checkResultLabel }}
            </text>
            <up-icon name="arrow-down" size="14" color="#999"></up-icon>
          </view>
        </view>
        <view class="form-row">
          <text class="form-label required">检验员</text>
          <up-input v-model="form.checkUserName" placeholder="请输入检验员" />
        </view>
        <view class="form-row">
          <text class="form-label required">检测日期</text>
          <view class="selector-trigger" @click="openCheckDatePicker">
            <text class="selector-text" :class="{ placeholder: !form.checkTime }">
              {{ form.checkTime || '请选择检测日期' }}
            </text>
            <up-icon name="calendar" size="16" color="#999"></up-icon>
          </view>
        </view>
      </view>
      <view class="section-card">
        <view class="section-title row-between">
          <text>检测项目</text>
          <view class="btn-inline" @click="openItemSelector">添加检测项目</view>
        </view>
        <view v-if="inspectItems.length > 0">
          <view v-for="(it, idx) in inspectItems" :key="it.id || idx" class="item-card">
            <view class="item-head">
              <text class="item-name">{{ it.name }}</text>
              <view class="item-del" @click="removeItem(it.id)">删除</view>
            </view>
            <view class="item-row"><text class="l">单位</text><text class="r">{{ it.unit || '-' }}</text></view>
            <view class="item-row"><text class="l">标准值</text><text class="r">{{ it.standardValue || '-' }}</text></view>
            <view class="item-row"><text class="l">内控值</text><text class="r">{{ it.internalControl || '-' }}</text></view>
            <view class="item-row input-row">
              <text class="l">化验值</text>
              <up-input v-model="it.testValue" placeholder="请输入" class="test-value-input" />
            </view>
          </view>
        </view>
        <view v-else class="no-data">请添加检测项目</view>
      </view>
    </scroll-view>
    <view class="bottom-bar">
      <view class="btn-submit" :class="{ disabled: submitLoading }" @click="handleSubmit">
        {{ submitLoading ? '提交中...' : '保存' }}
      </view>
    </view>
    <!-- äº§å“é€‰æ‹©å¼¹çª—(复用 pageModel æŽ¥å£ï¼‰ -->
    <up-popup :show="showProductPopup" mode="bottom" @close="showProductPopup = false">
      <view class="popup-wrap">
        <view class="popup-header">
          <text class="popup-title">选择产品</text>
        </view>
        <view class="popup-search">
          <up-input v-model="productQuery.productName" placeholder="产品大类" clearable />
          <up-input v-model="productQuery.model" placeholder="型号名称" clearable />
          <view class="popup-search-btn" @click="loadProductList">搜索</view>
        </view>
        <scroll-view scroll-y class="popup-list">
          <view v-for="row in productList" :key="row.id" class="popup-item" @click="selectProduct(row)">
            <view class="popup-item-top">
              <text class="popup-item-name">{{ row.productName }}</text>
              <text class="popup-item-unit">{{ row.unit }}</text>
            </view>
            <view class="popup-item-sub">型号:{{ row.model }}</view>
          </view>
          <view v-if="!productLoading && productList.length === 0" class="no-data">暂无数据</view>
        </scroll-view>
      </view>
    </up-popup>
    <!-- æ£€æµ‹é¡¹ç›®é€‰æ‹©å¼¹çª—(简化:从检测项维护表里选) -->
    <up-popup :show="showItemPopup" mode="bottom" @close="showItemPopup = false">
      <view class="popup-wrap">
        <view class="popup-header">
          <text class="popup-title">选择检测项目</text>
        </view>
        <view class="popup-search">
          <up-input v-model="itemQuery.name" placeholder="检测项目名称" clearable />
          <view class="popup-search-btn" @click="loadItemList">搜索</view>
        </view>
        <scroll-view scroll-y class="popup-list">
          <view
            v-for="row in itemList"
            :key="row.id"
            class="popup-item"
            :class="{ selected: isItemSelected(row.id) }"
            @click="toggleItem(row)"
          >
            <view class="popup-item-top">
              <text class="popup-item-name">{{ row.name }}</text>
              <view class="right-wrap">
                <text class="popup-item-unit">{{ row.unit }}</text>
                <up-icon
                  v-if="isItemSelected(row.id)"
                  name="checkbox-mark"
                  size="18"
                  color="#2979ff"
                ></up-icon>
              </view>
            </view>
            <view class="popup-item-sub">标准值:{{ row.standardValue || '-' }}|内控值:{{ row.internalControl || '-' }}</view>
          </view>
          <view v-if="!itemLoading && itemList.length === 0" class="no-data">暂无数据</view>
        </scroll-view>
        <view class="popup-footer">
          <view class="btn-cancel" @click="showItemPopup = false">取消</view>
          <view class="btn-ok" @click="confirmItems">确定</view>
        </view>
      </view>
    </up-popup>
    <!-- é€‰æ‹©å™¨ï¼šæ£€éªŒç±»åž‹/结果 -->
    <up-action-sheet :actions="checkTypeActions" :show="showCheckTypeSheet" @close="showCheckTypeSheet = false" @select="onSelectCheckType" title="检验类型" />
    <up-action-sheet :actions="checkResultActions" :show="showCheckResultSheet" @close="showCheckResultSheet = false" @select="onSelectCheckResult" title="检测结果" />
    <!-- æ—¥æœŸé€‰æ‹© -->
    <up-popup :show="showCheckDatePicker" mode="bottom" @close="showCheckDatePicker = false">
      <up-datetime-picker :show="true" v-model="checkDateValue" mode="date" @confirm="onCheckDateConfirm" @cancel="showCheckDatePicker = false" />
    </up-popup>
  </view>
</template>
<script setup>
import { computed, reactive, ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import dayjs from 'dayjs'
import PageHeader from '@/components/PageHeader.vue'
import useUserStore from '@/store/modules/user'
import { productModelList } from '@/api/basicData/productModel.js'
import { qualityInspectItemListPage } from '@/api/qualityManagement/inspectItem.js'
import { createRawMaterial, findRawMaterialDetail, updateRawMaterial } from '@/api/qualityManagement/rawMaterial.js'
const userStore = useUserStore()
const operationType = ref('add')
const submitLoading = ref(false)
const form = reactive({
  id: null,
  productId: '',
  productModelId: '',
  productName: '',
  model: '',
  unit: '',
  batchNo: '',
  checkType: '',
  checkResult: '',
  checkUserName: userStore?.nickName || '',
  checkTime: ''
})
const inspectItems = ref([]) // qualityInspectItem
// æ£€éªŒç±»åž‹/结果
const checkTypeActions = [
  { name: '入厂检', value: 0 },
  { name: '车间检', value: 1 },
  { name: '出厂检', value: 2 }
]
const checkResultActions = [
  { name: '合格', value: 1 },
  { name: '不合格', value: 0 }
]
const showCheckTypeSheet = ref(false)
const showCheckResultSheet = ref(false)
const checkTypeLabel = computed(() => checkTypeActions.find(a => a.value === form.checkType)?.name || '请选择')
const checkResultLabel = computed(() => checkResultActions.find(a => a.value === form.checkResult)?.name || '请选择')
const onSelectCheckType = (e) => { form.checkType = e.value; showCheckTypeSheet.value = false }
const onSelectCheckResult = (e) => { form.checkResult = e.value; showCheckResultSheet.value = false }
// æ—¥æœŸé€‰æ‹©
const showCheckDatePicker = ref(false)
const checkDateValue = ref(Date.now())
const openCheckDatePicker = () => {
  checkDateValue.value = form.checkTime ? dayjs(form.checkTime, 'YYYY-MM-DD').valueOf() : Date.now()
  showCheckDatePicker.value = true
}
const onCheckDateConfirm = (e) => {
  form.checkTime = dayjs(e.value).format('YYYY-MM-DD')
  showCheckDatePicker.value = false
}
// äº§å“é€‰æ‹©
const showProductPopup = ref(false)
const productQuery = reactive({ productName: '', model: '' })
const productList = ref([])
const productLoading = ref(false)
const openProductSelector = () => {
  if (operationType.value === 'edit') return
  showProductPopup.value = true
  if (productList.value.length === 0) loadProductList()
}
const loadProductList = () => {
  productLoading.value = true
  productModelList({ productName: productQuery.productName || '', model: productQuery.model || '', current: 1, size: 20 })
    .then(res => {
      const data = res?.records || res?.data?.records || []
      productList.value = Array.isArray(data) ? data : []
    })
    .finally(() => { productLoading.value = false })
}
const selectProduct = (row) => {
  form.productId = row.productId
  form.productModelId = row.id
  form.productName = row.productName
  form.model = row.model
  form.unit = row.unit
  showProductPopup.value = false
}
// æ£€æµ‹é¡¹ç›®é€‰æ‹©
const showItemPopup = ref(false)
const itemQuery = reactive({ name: '' })
const itemList = ref([])
const itemLoading = ref(false)
const selectedItemIds = ref(new Set())
const isItemSelected = (id) => selectedItemIds.value.has(id)
const openItemSelector = () => {
  showItemPopup.value = true
  selectedItemIds.value = new Set(inspectItems.value.map(i => i.id))
  if (itemList.value.length === 0) loadItemList()
}
const loadItemList = () => {
  itemLoading.value = true
  qualityInspectItemListPage({ name: itemQuery.name || null, current: 1, size: 50 })
    .then(res => {
      const records = res?.data?.records || []
      itemList.value = Array.isArray(records) ? records : []
    })
    .finally(() => { itemLoading.value = false })
}
const toggleItem = (row) => {
  const set = selectedItemIds.value
  if (set.has(row.id)) set.delete(row.id)
  else set.add(row.id)
  // ç®€å•提示选中状态(可按需做 icon)
}
const confirmItems = () => {
  const set = selectedItemIds.value
  const existingMap = new Map(inspectItems.value.map(i => [i.id, i]))
  const next = []
  for (const id of set) {
    const exist = existingMap.get(id)
    if (exist) next.push(exist)
    else {
      const row = itemList.value.find(r => r.id === id)
      if (row) next.push({ ...row, testValue: '' })
    }
  }
  inspectItems.value = next
  showItemPopup.value = false
}
const removeItem = (id) => {
  inspectItems.value = inspectItems.value.filter(i => i.id !== id)
}
const validate = () => {
  if (!form.productModelId) return '请选择产品'
  if (!form.batchNo) return '请输入批号'
  if (form.checkType === '' || form.checkType == null) return '请选择检验类型'
  if (form.checkResult === '' || form.checkResult == null) return '请选择检测结果'
  if (!form.checkUserName) return '请输入检验员'
  if (!form.checkTime) return '请选择检测日期'
  if (!inspectItems.value.length) return '请添加检测项目'
  return ''
}
const handleSubmit = () => {
  if (submitLoading.value) return
  const msg = validate()
  if (msg) {
    uni.showToast({ title: msg, icon: 'none' })
    return
  }
  submitLoading.value = true
  const payload = { ...form, qualityInspectItem: inspectItems.value }
  const api = operationType.value === 'add' ? createRawMaterial : updateRawMaterial
  api(payload)
    .then(() => {
      uni.showToast({ title: '保存成功', icon: 'success' })
      setTimeout(() => uni.navigateBack(), 400)
    })
    .catch(() => {
      uni.showToast({ title: '保存失败', icon: 'none' })
    })
    .finally(() => { submitLoading.value = false })
}
onLoad((options) => {
  operationType.value = options?.type || 'add'
  const id = options?.id
  if (operationType.value === 'edit' && id) {
    findRawMaterialDetail(id).then(res => {
      const d = res?.data || {}
      form.id = d.id
      form.productId = d.productId
      form.productModelId = d.productModelId
      form.productName = d.productName
      form.model = d.model
      form.unit = d.unit
      form.batchNo = d.batchNo
      form.checkType = d.checkType
      form.checkResult = d.checkResult
      form.checkUserName = d.checkUserName || form.checkUserName
      form.checkTime = d.checkTime
      inspectItems.value = Array.isArray(d.qualityInspectItem) ? d.qualityInspectItem : []
    })
  }
})
const goBack = () => uni.navigateBack()
</script>
<style lang="scss" scoped>
.rm-form-page { min-height: 100vh; background: #f5f5f5; padding-bottom: 110rpx; }
.content-scroll { height: calc(100vh - 110rpx); }
.section-card { background: #fff; margin: 24rpx; padding: 24rpx; border-radius: 16rpx; box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06); }
.section-title { font-size: 28rpx; font-weight: 500; color: #333; margin-bottom: 12rpx; display: flex; align-items: center; }
.row-between { justify-content: space-between; }
.btn-inline { font-size: 26rpx; color: #2979ff; }
.form-row { margin-bottom: 22rpx; }
.form-label { display: block; font-size: 26rpx; color: #666; margin-bottom: 12rpx; }
.form-label.required:before { content: "*"; color: #f56c6c; margin-right: 6rpx; }
.selector-trigger { display: flex; align-items: center; justify-content: space-between; padding: 20rpx 24rpx; background: #f5f5f5; border-radius: 12rpx; }
.selector-trigger.disabled { opacity: 0.6; }
.selector-text { font-size: 28rpx; color: #333; }
.selector-text.placeholder { color: #999; }
.no-data { text-align: center; padding: 40rpx 0; color: #999; font-size: 26rpx; }
.item-card { padding: 18rpx 0; border-top: 1rpx solid #eee; }
.item-head { display: flex; justify-content: space-between; align-items: center; padding: 6rpx 0 10rpx; }
.item-name { font-size: 28rpx; font-weight: 500; color: #333; }
.item-del { font-size: 26rpx; color: #f56c6c; }
.item-row { display: flex; justify-content: space-between; align-items: center; padding: 6rpx 0; font-size: 26rpx; }
.item-row .l { color: #666; }
.item-row .r { color: #333; }
.input-row { align-items: center; }
.test-value-input {
  flex: 0 0 220rpx;
}
:deep(.test-value-input .u-input),
:deep(.test-value-input .up-input) {
  width: 100%;
}
.bottom-bar { position: fixed; left: 0; right: 0; bottom: 0; padding: 16rpx 24rpx calc(16rpx + env(safe-area-inset-bottom)); background: #fff; box-shadow: 0 -4rpx 16rpx rgba(0,0,0,0.04); }
.btn-submit { height: 88rpx; border-radius: 999rpx; background: #2979ff; color: #fff; font-size: 30rpx; display: flex; align-items: center; justify-content: center; }
.btn-submit.disabled { opacity: 0.6; }
.popup-wrap { background: #fff; border-radius: 24rpx 24rpx 0 0; padding-bottom: env(safe-area-inset-bottom); }
.popup-header { padding: 24rpx; border-bottom: 1rpx solid #eee; text-align: center; }
.popup-title { font-size: 30rpx; font-weight: 500; color: #333; }
.popup-search { padding: 16rpx 24rpx; display: grid; grid-template-columns: 1fr 1fr 140rpx; gap: 16rpx; align-items: center; }
.popup-search-btn { height: 72rpx; border-radius: 12rpx; background: #2979ff; color: #fff; display: flex; align-items: center; justify-content: center; font-size: 28rpx; }
.popup-list { max-height: 60vh; padding: 0 24rpx 24rpx; }
.popup-item { padding: 18rpx 0; border-bottom: 1rpx solid #f0f0f0; }
.popup-item.selected { background: rgba(41, 121, 255, 0.06); }
.popup-item-top { display: flex; justify-content: space-between; align-items: center; margin-bottom: 6rpx; }
.right-wrap { display: inline-flex; align-items: center; gap: 12rpx; }
.popup-item-name { font-size: 28rpx; color: #333; }
.popup-item-unit { font-size: 24rpx; color: #999; }
.popup-item-sub { font-size: 24rpx; color: #666; }
.popup-footer { display: flex; gap: 24rpx; padding: 16rpx 24rpx 24rpx; }
.btn-cancel { flex: 1; text-align: center; padding: 24rpx; background: #f0f0f0; border-radius: 12rpx; }
.btn-ok { flex: 1; text-align: center; padding: 24rpx; background: #2979ff; color: #fff; border-radius: 12rpx; }
</style>
src/pages/qualityManagement/rawMaterial/index.vue
@@ -31,7 +31,11 @@
      <view v-for="(item, index) in tableData" :key="index" class="list-item">
        <view class="item-header">
          <text class="product-name">{{ item.productName }}</text>
          <up-tag :text="item.inspectState ? '已提交' : '未提交'" :type="item.inspectState ? 'success' : 'warning'" size="mini"></up-tag>
          <up-tag
            :text="item.inspectState == 1 ? '已提交' : '未提交'"
            :type="item.inspectState == 1 ? 'success' : 'warning'"
            size="mini"
          ></up-tag>
        </view>
        <view class="item-content">
          <view class="item-row">
@@ -56,8 +60,9 @@
          </view>
        </view>
        <view class="item-actions">
          <up-button v-if="!item.inspectState" type="primary" size="mini" @click.stop="openForm('edit', item)">编辑</up-button>
          <up-button v-if="!item.inspectState" type="success" size="mini" @click.stop="handleConfirmSubmit(item)">提交</up-button>
          <up-button v-if="item.inspectState != 1" type="primary" size="mini" @click.stop="openForm('edit', item)">编辑</up-button>
          <up-button type="info" size="mini" @click.stop="openFiles(item)">附件</up-button>
          <up-button v-if="item.inspectState != 1" type="success" size="mini" @click.stop="handleConfirmSubmit(item)">提交</up-button>
          <up-button type="error" size="mini" @click.stop="handleDelete(item)">删除</up-button>
        </view>
      </view>
@@ -96,7 +101,8 @@
</template>
<script setup>
import { ref, reactive, onMounted, computed } from 'vue';
import { ref, reactive, computed } from 'vue';
import { onShow } from '@dcloudio/uni-app';
import {
  findRawMaterialListPage,
  submitRawMaterial,
@@ -195,9 +201,10 @@
};
const openForm = (type, item) => {
  // Mobile usually navigates to a new page for add/edit if complex
  // Here we'll just show a toast for now as the actual form components are many
  toast('功能开发中,请在PC端操作');
  const id = item?.id
  uni.navigateTo({
    url: `/pages/qualityManagement/rawMaterial/form?type=${type}${id ? `&id=${id}` : ''}`
  })
};
const handleConfirmSubmit = (row) => {
@@ -214,7 +221,8 @@
const handleDelete = (row) => {
  showConfirm('确认删除该记录吗?').then(res => {
    if (res.confirm) {
      deleteRawMaterial({ id: row.id }).then(() => {
      // å¯¹é½ PC ç«¯ï¼šåˆ é™¤æŽ¥å£æŽ¥æ”¶ id æ•°ç»„
      deleteRawMaterial([row.id]).then(() => {
        toast('删除成功');
        handleQuery();
      });
@@ -222,11 +230,24 @@
  });
};
const openFiles = (row) => {
  if (!row?.id) {
    toast('请先保存后再上传附件')
    return
  }
  try {
    uni.setStorageSync('rawMaterialFilesCtx', JSON.stringify({ id: row.id }))
  } catch (e) {}
  uni.navigateTo({
    url: `/pages/qualityManagement/rawMaterial/files?id=${row.id}`
  })
}
const goBack = () => {
  uni.navigateBack();
};
onMounted(() => {
onShow(() => {
  handleQuery();
});
</script>
@@ -309,10 +330,19 @@
.item-actions {
  display: flex;
  justify-content: flex-end;
  gap: 20rpx;
  gap: 16rpx;
  border-top: 1rpx solid #ebeef5;
  padding-top: 20rpx;
}
.item-actions :deep(.u-button),
.item-actions :deep(.up-button) {
  min-width: 140rpx;
  height: 64rpx;
  line-height: 64rpx;
  font-size: 26rpx;
  border-radius: 999rpx;
  padding: 0 24rpx;
}
.no-data {
  padding-top: 200rpx;