spring
5 天以前 e0fc86fda41a8777a0cec5a9de7aa9f1474f5dc8
fix: 库存操作原型绘制。单丝领用,添加字段
已修改10个文件
已添加10个文件
1733 ■■■■■ 文件已修改
.env.development 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/product/inventoryOperation.ts 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/product/twist.ts 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/system/user.ts 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/manifest.json 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages.json 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/index/index.vue 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/production/inventory/form.vue 633 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/production/inventory/index.vue 283 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/production/inventory/mock.ts 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/production/inventory/shared.ts 144 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/production/twist/components/MonofilCard.vue 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/production/twist/receive/plate/form.vue 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/production/twist/receive/plate/index.vue 439 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/production/twist/report/index.vue 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/static/icons/inspection.png 补丁 | 查看 | 原始文档 | blame | 历史
src/static/icons/inventoryOperation.png 补丁 | 查看 | 原始文档 | blame | 历史
src/static/icons/outbound.png 补丁 | 查看 | 原始文档 | blame | 历史
src/static/icons/production.png 补丁 | 查看 | 原始文档 | blame | 历史
src/static/icons/timely.png 补丁 | 查看 | 原始文档 | blame | 历史
.env.development
@@ -7,8 +7,8 @@
VITE_APP_PORT = 4096
# API åŸºç¡€è·¯å¾„,开发环境下的请求前缀
VITE_APP_BASE_API = 'http://114.132.189.42:7002/mes'
# VITE_APP_BASE_API = 'http://114.132.189.42:7002/mes'
VITE_APP_BASE_API = 'http://114.132.189.42:7002/mes'
# VITE_APP_BASE_API = 'http://192.168.10.119:7002/mes' # é‚¹è£•
# VITE_APP_BASE_API = 'http://192.168.100.131:7002/mes' #
src/api/product/inventoryOperation.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,34 @@
import request from "@/utils/request";
import { BaseResult } from "@/models/base";
const BASE = "/inventory/operation";
/** åº“存操作(成品拆盘 / ç»žçº¿å¤ç»•) */
const InventoryOperationApi = {
  // æ–°å¢žæ“ä½œå•
  createOperation(data: any) {
    return request<BaseResult<any>>({
      url: `${BASE}/create`,
      method: "POST",
      data,
    });
  },
  // åº“存盘号搜索(成品/半成品),keyword æ”¯æŒç›˜å·/规格型号
  searchStock(data: {
    keyword?: string;
    materialType?: string;
    ncReturnFlag?: string;
    model?: string;
    current?: number;
    size?: number;
  }) {
    return request<BaseResult<any>>({
      url: `${BASE}/searchStock`,
      method: "POST",
      data,
    });
  },
};
export default InventoryOperationApi;
src/api/product/twist.ts
@@ -55,6 +55,22 @@
    });
  },
  // ç›˜å…·ç§°é‡å°è´¦åˆ†é¡µæŸ¥è¯¢ï¼ˆæ‰«ç é¢†ç”¨ç›˜å…·æ—¶æŒ‰ç§°é‡ç¼–号取台账数据)
  queryBobbinWeighingPage(params: {
    current?: number;
    size?: number;
    weighingCode?: string;
    specificationModel?: string;
    diskMaterial?: string;
    received?: number | string;
  }) {
    return request<BaseResult<any>>({
      url: "/bobbinWeighing/queryPage",
      method: "GET",
      data: params,
    });
  },
  saveAge(data: any) {
    return request<BaseResult<any>>({
      url: "/app/ageing",
src/api/system/user.ts
@@ -16,6 +16,17 @@
  },
  /**
   * äººå‘˜ä¸‹æ‹‰åˆ—表(与 PC ç«¯ getUserSelect åŒä¸€æŽ¥å£ï¼‰
   * è¿”回 [{ nickName / nickname / userName, ... }]
   */
  getUserSelect() {
    return request<any>({
      url: "/team/getUser",
      method: "GET",
    });
  },
  /**
   * èŽ·å–ç”¨æˆ·åˆ†é¡µåˆ—è¡¨
   *
   * @param queryParams æŸ¥è¯¢å‚æ•°
src/manifest.json
@@ -2,7 +2,7 @@
    "name" : "线缆上报",
    "appid" : "__UNI__F64E0A4",
    "description" : "",
    "versionName" : "1.0.30",
    "versionName" : "1.0.37",
    "versionCode" : "100",
    "transformPx" : false,
    /* 5+App特有相关 */
src/pages.json
@@ -266,6 +266,18 @@
      }
    },
    {
      "path": "pages/production/inventory/index",
      "style": {
        "navigationBarTitleText": "库存操作"
      }
    },
    {
      "path": "pages/production/inventory/form",
      "style": {
        "navigationBarTitleText": "库存操作办理"
      }
    },
    {
      "path": "pages/timely/index",
      "style": {
        "navigationBarTitleText": "时效任务"
src/pages/index/index.vue
@@ -189,7 +189,7 @@
  //   prem: "sys:user:query",
  // },
  {
    icon: "/static/icons/user.png",
    icon: "/static/icons/production.png",
    title: "生产管理",
    url: "/pages/production/index",
    show: false,
@@ -219,19 +219,25 @@
  //   show: false,
  // },
  {
    icon: "/static/icons/routingInspection.png",
    icon: "/static/icons/inspection.png",
    title: "巡检",
    url: "/pages/routingInspection/index",
    show: false,
  },
  {
    icon: "/static/icons/log.png",
    icon: "/static/icons/outbound.png",
    title: "出库",
    url: "/pages/outbound/index",
    show: false,
  },
  {
    icon: "/static/icons/log.png",
    icon: "/static/icons/inventoryOperation.png",
    title: "库存操作",
    url: "/pages/production/inventory/index",
    show: false,
  },
  {
    icon: "/static/icons/timely.png",
    title: "时效",
    url: "/pages/timely/index",
    show: false,
@@ -301,8 +307,15 @@
  const canAccessTimely = deviceGroupName.includes("时效") || isAdmin.value;
  const productionNav = navList.find((item) => item.url === "/pages/production/index");
  const timelyNav = navList.find((item) => item.url === "/pages/timely/index");
  // åº“存操作(成品拆盘 / ç»žçº¿å¤ç»•):权限与「生产管理」一致
  const inventoryNav = navList.find(
    (item) => item.url === "/pages/production/inventory/index"
  );
  if (productionNav) productionNav.show = canAccessProduction;
  if (timelyNav) timelyNav.show = canAccessTimely;
  if (inventoryNav) {
    inventoryNav.show = canAccessProduction;
  }
  console.log("isInspector.value", isInspector.value);
  if (isInspector.value || isAdmin.value) {
    // å¦‚果是巡检员,显示巡检菜单
src/pages/production/inventory/form.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,633 @@
<template>
  <view class="form-page">
    <CardTitle :title="pageTitle" :hideAction="false" />
    <!-- æºç›˜ï¼šä¸Šä¸€æ­¥å·²æ‰«ç é¢†ç”¨ï¼Œè¿™é‡Œåªåšå±•示 -->
    <view class="section">
      <view class="section-title">源盘(成品)</view>
      <view class="card source-card">
        <view class="kv">
          <text class="kv-label">盘号</text>
          <text class="kv-value">{{ source.batchCode || "-" }}</text>
        </view>
        <view class="kv">
          <text class="kv-label">规格型号</text>
          <text class="kv-value">{{ source.model || "-" }}</text>
        </view>
        <view class="kv">
          <text class="kv-label">长度(m)</text>
          <text class="kv-value">{{ source.currentLength ?? "-" }}</text>
        </view>
        <view class="kv">
          <text class="kv-label">重量(kg)</text>
          <text class="kv-value">{{ source.currentWeight ?? "-" }}</text>
        </view>
        <view class="kv">
          <text class="kv-label">米重(kg/km)</text>
          <text class="kv-value">{{ source.meterWeight ?? "-" }}</text>
        </view>
        <view class="kv">
          <text class="kv-label">状态</text>
          <text class="kv-value ok">
            {{ source.qualityStatus || "-" }} Â· {{ source.enableStatus || "-" }}
          </text>
        </view>
      </view>
    </view>
    <!-- å­ç›˜ / ç›®æ ‡ç›˜ -->
    <view class="section">
      <view class="section-title">{{ isRewind ? "目标盘" : "子盘" }}</view>
      <view v-for="(target, index) in targets" :key="index" class="card target-card">
        <view class="row">
          <text class="row-label">{{ isRewind ? "目标盘" : `子盘${index + 1}` }}</text>
          <view class="row-value">
            <wd-input v-model="target.length" type="number" no-border placeholder="请输入长度(m)" />
          </view>
          <text class="row-unit">ç±³</text>
          <view v-if="!isRewind" class="row-btns">
            <wd-button size="small" plain :round="false" type="error" @click="removeTarget(index)">
              åˆ é™¤
            </wd-button>
          </view>
        </view>
        <view class="row">
          <text class="row-label">领用盘具</text>
          <view class="row-value">
            <wd-input
              v-model="target.codeInput"
              no-border
              clearable
              :placeholder="dishPlaceholder"
            />
          </view>
        </view>
        <view class="row-btns">
          <wd-button size="small" plain :round="false" @click="scanDish(index)">
            æ‰«ç é€‰æ‹©ç›˜å…·
          </wd-button>
          <wd-button size="small" plain :round="false" @click="searchDish(index)">查询</wd-button>
        </view>
        <view v-if="target.dishCode" class="kv">
          <text class="kv-label">盘具规格</text>
          <text class="kv-value">
            {{ target.dishModel || "-" }}
            <text v-if="target.dishWeight !== undefined && target.dishWeight !== null">
              Â· {{ target.dishWeight }}kg
            </text>
          </text>
        </view>
        <view class="kv">
          <text class="kv-label">{{ isRewind ? "目标盘号" : "子盘盘号" }}</text>
          <view class="chip-switch">
            <view
              v-for="opt in batchCodeOptions"
              :key="String(opt.value)"
              class="chip-item"
              :class="{ 'chip-item-active': target.useCurrentBatchCode === opt.value }"
              @click="selectBatchCodeMode(index, opt.value)"
            >
              {{ opt.label }}
            </view>
          </view>
        </view>
        <view class="kv">
          <text class="kv-label">实际盘号</text>
          <text class="kv-value">{{ targetBatchCode(index) || "提交后自动生成" }}</text>
        </view>
      </view>
      <view v-if="!isRewind" class="add-target" @click="addTarget">+ æ·»åŠ å­ç›˜</view>
    </view>
    <!-- æ“ä½œäºº -->
    <view class="section">
      <view class="section-title">操作人</view>
      <view class="card">
        <wd-select-picker
          v-model="operatorName"
          type="radio"
          :columns="userOptions"
          label="操作人"
          label-width="100px"
          filterable
          filter-placeholder="搜索人员"
          placeholder="请选择操作人"
          :loading="userLoading"
          :z-index="30"
        />
      </view>
    </view>
    <!-- å¤‡æ³¨ -->
    <view class="section">
      <view class="section-title">备注</view>
      <view class="card">
        <wd-textarea v-model="remark" placeholder="可选备注" :maxlength="200" show-word-limit />
      </view>
    </view>
    <view class="footer">
      <wd-button block :round="false" :loading="submitting" @click="submit">
        <text class="text-[#fff]">提交</text>
      </wd-button>
    </view>
    <Scan ref="scanRef" emitName="scanInventoryDish" />
    <wd-toast />
  </view>
</template>
<script setup lang="ts">
import CardTitle from "@/components/card-title/index.vue";
import Scan from "@/components/scan/index.vue";
import { useToast } from "wot-design-uni";
import { onLoad, onUnload, onShow, onHide } from "@dcloudio/uni-app";
import InventoryOperationApi from "@/api/product/inventoryOperation";
import UserAPI from "@/api/system/user";
import { useUserStore } from "@/store/modules/user";
import { MOCK_BOBBIN, MOCK_ENABLED } from "./mock";
import {
  DISH_CODE_KEYS,
  MATERIAL_TYPE_FG,
  OP_SPLIT,
  OP_STRAND_REWIND,
  SOURCE_STORAGE_KEY,
  findBobbin,
  isBobbinReceived,
  parseScanCode,
  pickValue,
} from "./shared";
interface TargetItem {
  /** ç›®æ ‡é•¿åº¦(m) */
  length: any;
  /** é¢†ç”¨ç›˜å…·ç¼–号(盘具称重台账的称重编号) */
  dishCode: string;
  dishModel: string;
  dishWeight: any;
  /** ç›˜å…·ç¼–号输入框 */
  codeInput: string;
  /** è¯¥ç›˜æ˜¯å¦æ²¿ç”¨åŽŸç›˜å·ï¼ˆæ‹†ç›˜æ—¶æœ€å¤šä¸€ç›˜å¯ä»¥æ²¿ç”¨ï¼‰ */
  useCurrentBatchCode: boolean;
}
const createTarget = (length: any = "", useCurrentBatchCode = false): TargetItem => ({
  length,
  dishCode: "",
  dishModel: "",
  dishWeight: undefined,
  codeInput: "",
  useCurrentBatchCode,
});
const toast = useToast();
const scanRef = ref();
const operationType = ref(OP_SPLIT);
const isRewind = computed(() => operationType.value === OP_STRAND_REWIND);
const pageTitle = computed(() => (isRewind.value ? "绞线复绕" : "成品拆盘"));
/** æºç›˜ï¼šæ¥è‡ªä¸Šä¸€æ­¥æ‰«ç é¢†ç”¨ */
const source = ref<any>({});
// ç¬¬ä¸€ä¸ªç›˜é»˜è®¤æ²¿ç”¨åŽŸç›˜å·ï¼ˆå¤ç»•æ•´ç›˜æ²¿ç”¨ï¼‰ï¼›åŽç»­æ·»åŠ çš„å­ç›˜é»˜è®¤ç”Ÿæˆæ–°ç›˜å·
const targets = ref<TargetItem[]>([createTarget("", true)]);
/** ç›˜å·æ¨¡å¼ï¼šæ¯ä¸ªç›˜å•独选 */
const batchCodeOptions = [
  { label: "沿用原盘号", value: true },
  { label: "生成新盘号", value: false },
];
const remark = ref("");
const submitting = ref(false);
const isPageVisible = ref(true);
/* ==================== æ“ä½œäºº ==================== */
const userStore = useUserStore();
/** æ“ä½œäººï¼šé»˜è®¤å½“前登录人,可改成其它人员 */
const operatorName = ref("");
const userOptions = ref<{ label: string; value: string }[]>([]);
const userLoading = ref(false);
const loadUserOptions = async () => {
  if (userLoading.value) return;
  userLoading.value = true;
  try {
    const { code, data } = await UserAPI.getUserSelect();
    if (code === 200) {
      const names = (data || [])
        .map((item: any) => String(item?.nickName || item?.nickname || item?.userName || "").trim())
        .filter(Boolean);
      userOptions.value = Array.from(new Set<string>(names)).map((name) => ({
        label: name,
        value: name,
      }));
    }
  } catch (error) {
    // äººå‘˜æŽ¥å£å¼‚常时不阻塞办理,操作人仍保留当前登录人
  } finally {
    userLoading.value = false;
  }
};
/** æ¨¡æ‹Ÿæ¨¡å¼ä¸‹é»˜è®¤å¸¦å‡ºä¸€æ¡ç›˜å…·ï¼Œè¾“入框里给出默认盘具编号提示 */
const dishPlaceholder = computed(() =>
  MOCK_ENABLED ? `扫码或输入盘具编号,如 ${MOCK_BOBBIN.weighingCode}` : "扫码或输入盘具编号"
);
/** æ‰«ç ç›®æ ‡ï¼šæŸä¸ªç›®æ ‡ç›˜/子盘的领用盘具 */
const scanDishIndex = ref<number | null>(null);
/* ==================== é¢†ç”¨ç›˜å…· ==================== */
const applyDishCode = async (index: number, code: string, options: { silent?: boolean } = {}) => {
  const target = targets.value[index];
  if (!target) return false;
  if (targets.value.some((item, i) => i !== index && String(item.dishCode ?? "") === code)) {
    toast.error("该盘具已被本单其它盘使用,请更换盘具");
    return false;
  }
  const { record, mocked } = await findBobbin(code);
  if (!record) {
    toast.error("盘具称重台账中未找到该盘具,请先在盘具称重台账中称重");
    return false;
  }
  if (isBobbinReceived(record)) {
    toast.error("该盘具已被领用,请更换盘具");
    return false;
  }
  target.dishCode = String(record.weighingCode ?? code).trim();
  target.dishModel = record.specificationModel ?? "";
  target.dishWeight = record.weight;
  target.codeInput = target.dishCode;
  if (!options.silent) {
    toast.success(mocked ? "未取到真实数据,使用模拟盘具数据" : "盘具已选择");
  }
  return true;
};
const searchDish = async (index: number) => {
  const code = String(targets.value[index]?.codeInput ?? "").trim();
  if (!code) {
    toast.error("请输入或扫描盘具编号");
    return;
  }
  try {
    await applyDishCode(index, code);
  } catch (error) {
    // è¯·æ±‚层已统一提示
  }
};
const scanDish = (index: number) => {
  scanDishIndex.value = index;
  // #ifdef APP-PLUS
  scanRef.value?.triggerScan();
  // #endif
  // #ifndef APP-PLUS
  toast.warning("当前环境不支持硬件扫码,请用输入框手动查询盘具编号");
  // #endif
};
/* ==================== ç›®æ ‡ç›˜ ==================== */
/** è¯¥ç›˜ç›˜å·ï¼šé€‰äº†ã€Œæ²¿ç”¨åŽŸç›˜å·ã€å°±ç”¨æºç›˜ç›˜å·ï¼Œå¦åˆ™æäº¤ç©ºå€¼ç”±åŽç«¯ç”Ÿæˆæ–°ç›˜å· */
const targetBatchCode = (index: number) => {
  const target = targets.value[index];
  return target?.useCurrentBatchCode ? source.value?.batchCode || "" : "";
};
/** æ²¿ç”¨åŽŸç›˜å·æœ€å¤šåªèƒ½æœ‰ä¸€ç›˜ï¼šæŸä¸€ç›˜é€‰äº†ï¼Œå…¶å®ƒç›˜è‡ªåŠ¨æ”¹å›žç”Ÿæˆæ–°ç›˜å· */
const selectBatchCodeMode = (index: number, value: boolean) => {
  const target = targets.value[index];
  if (!target) return;
  target.useCurrentBatchCode = value;
  if (!value) return;
  targets.value.forEach((item, i) => {
    if (i !== index) item.useCurrentBatchCode = false;
  });
};
const addTarget = () => {
  // æ–°å¢žçš„子盘默认「生成新盘号」
  targets.value.push(createTarget());
};
const removeTarget = (index: number) => {
  targets.value.splice(index, 1);
  if (targets.value.length === 0) {
    targets.value.push(createTarget("", true));
  }
};
/* ==================== æ‰«ç å›žè°ƒï¼ˆåªå¤„理盘具) ==================== */
const getScanCode = async (payload: any) => {
  if (!isPageVisible.value) return;
  const index = scanDishIndex.value;
  if (index === null) return;
  const parsed = parseScanCode(payload);
  if (!parsed) {
    toast.error("扫码内容为空,请重新扫码");
    return;
  }
  const code = pickValue(parsed, DISH_CODE_KEYS);
  if (!code) {
    toast.error("二维码无法识别盘具编号,请更换二维码");
    return;
  }
  try {
    await applyDishCode(index, code);
  } catch (error) {
    // è¯·æ±‚层已统一提示
  } finally {
    scanDishIndex.value = null;
  }
};
/* ==================== æäº¤ ==================== */
const submit = async () => {
  if (!source.value?.batchCode) {
    toast.error("未获取到源盘信息,请返回重新扫码");
    return;
  }
  if (targets.value.length === 0) {
    toast.error(isRewind.value ? "请添加目标盘" : "请至少添加一个子盘");
    return;
  }
  if (isRewind.value && targets.value.length > 1) {
    toast.error("绞线复绕只生成一盘");
    return;
  }
  for (let i = 0; i < targets.value.length; i++) {
    const target = targets.value[i];
    const label = isRewind.value ? "目标盘" : `子盘${i + 1}`;
    if (!target.length || Number(target.length) <= 0) {
      toast.error(`${label}:请输入长度`);
      return;
    }
    // æ¯ä¸€ä¸ªæ–°ç›˜ï¼ˆå­ç›˜/目标盘)都必须选择领用盘具
    if (!target.dishCode) {
      toast.error(`${label}:请扫码选择领用盘具`);
      return;
    }
  }
  const totalLength = targets.value.reduce((sum, item) => sum + Number(item.length || 0), 0);
  const sourceLength = Number(source.value?.currentLength || 0);
  if (sourceLength > 0 && totalLength > sourceLength) {
    toast.error(
      isRewind.value
        ? `目标盘长度(${totalLength})不能超过源盘长度(${sourceLength})`
        : `子盘长度之和(${totalLength})超过源盘长度(${sourceLength})`
    );
    return;
  }
  const sources = [
    {
      batchCode: source.value.batchCode,
      materialType: MATERIAL_TYPE_FG,
      model: source.value.model,
      length: sourceLength || undefined,
      weight: source.value.currentWeight || undefined,
      meterWeight: source.value.meterWeight,
      sortOrder: 1,
    },
  ];
  const targetDtos = targets.value.map((item, index) => ({
    // å•盘可选沿用原盘号,其余提交空值由后端生成新盘号
    batchCode: targetBatchCode(index),
    // é¢†ç”¨çš„盘具编号
    dishCode: String(item.dishCode ?? "").trim(),
    materialType: MATERIAL_TYPE_FG,
    model: source.value.model,
    length: Number(item.length),
    meterWeight: source.value.meterWeight,
    sortOrder: index + 1,
  }));
  submitting.value = true;
  try {
    const { code, msg } = await InventoryOperationApi.createOperation({
      operationType: operationType.value,
      remark: remark.value,
      // æ“ä½œäººï¼ˆäººå‘˜é€‰æ‹©ï¼Œé»˜è®¤å½“前登录人)
      createUserName: operatorName.value,
      sources,
      targets: targetDtos,
    });
    if (code === 200) {
      toast.success("提交成功");
      uni.removeStorageSync(SOURCE_STORAGE_KEY);
      setTimeout(() => uni.navigateBack(), 600);
    } else {
      toast.error(msg || "提交失败");
    }
  } catch (error) {
    // è¯·æ±‚层已统一提示
  } finally {
    submitting.value = false;
  }
};
onLoad(async (options: any) => {
  operationType.value = options?.type === OP_STRAND_REWIND ? OP_STRAND_REWIND : OP_SPLIT;
  // æ“ä½œäººé»˜è®¤å½“前登录人,人员列表异步加载后可改选
  const info: any = userStore.userInfo;
  operatorName.value = info?.nickname || info?.nickName || info?.userName || "";
  loadUserOptions();
  const cached = uni.getStorageSync(SOURCE_STORAGE_KEY);
  if (cached && cached.batchCode) {
    source.value = { ...cached };
    // ç»žçº¿å¤ç»•是整盘复绕:目标盘长度默认与源盘一致
    if (isRewind.value) {
      targets.value[0].length = cached.currentLength ?? "";
    }
    // æ¨¡æ‹Ÿæ¨¡å¼ä¸‹é»˜è®¤å¸¦å‡ºä¸€æ¡ç›˜å…·æ•°æ®ï¼Œæ–¹ä¾¿ç›´æŽ¥è·‘通提交
    if (MOCK_ENABLED && MOCK_BOBBIN.weighingCode) {
      await applyDishCode(0, MOCK_BOBBIN.weighingCode, { silent: true });
    }
  } else {
    toast.error("未获取到源盘信息,请返回重新扫码领用");
    setTimeout(() => uni.navigateBack(), 1000);
  }
  uni.$off("scanInventoryDish", getScanCode);
  uni.$on("scanInventoryDish", getScanCode);
});
onShow(() => {
  isPageVisible.value = true;
});
onHide(() => {
  isPageVisible.value = false;
});
onUnload(() => {
  uni.$off("scanInventoryDish", getScanCode);
  isPageVisible.value = false;
  // ç¦»å¼€åŠžç†é¡µå³æ¸…æŽ‰æºç›˜ç¼“å­˜ï¼Œä¸‹æ¬¡ä»Žé€‰ç±»åž‹é‡æ–°å¼€å§‹
  uni.removeStorageSync(SOURCE_STORAGE_KEY);
});
</script>
<style lang="scss" scoped>
.form-page {
  min-height: 100vh;
  padding: 12px 12px 96px;
  background: #f3f9f8;
}
.section {
  margin-bottom: 12px;
}
.section-title {
  margin-bottom: 8px;
  font-size: 14px;
  font-weight: 600;
  color: #252525;
}
.card {
  padding: 12px;
  background: #ffffff;
  border: 1px solid #e6e6e6;
  border-radius: 6px;
}
.source-card {
  border-color: #0d867f;
}
.target-card {
  margin-bottom: 10px;
  border-color: #cfe8e5;
}
.row {
  display: flex;
  align-items: center;
  min-height: 40px;
}
.row-label {
  flex-shrink: 0;
  width: 72px;
  font-size: 13px;
  color: #646874;
}
.row-value {
  flex: 1;
  min-width: 0;
}
.row-unit {
  margin-left: 4px;
  font-size: 13px;
  color: #646874;
}
.row-btns {
  display: flex;
  justify-content: flex-end;
  margin-top: 6px;
  :deep(.wd-button) {
    margin-left: 8px;
  }
}
.kv {
  display: flex;
  align-items: center;
  margin-top: 6px;
  font-size: 13px;
}
.kv-label {
  width: 88px;
  color: #646874;
}
.kv-value {
  flex: 1;
  color: #252525;
}
.ok {
  color: #0d867f;
}
.batch-code-mode {
  display: flex;
  align-items: center;
  margin-bottom: 10px;
}
.chip-switch {
  display: flex;
  align-items: center;
}
.chip-item {
  margin-right: 8px;
  padding: 5px 14px;
  font-size: 13px;
  line-height: 20px;
  color: #0d867f;
  background: #ffffff;
  border: 1px solid #0d867f;
  border-radius: 16px;
  &:last-child {
    margin-right: 0;
  }
}
.chip-item-active {
  color: #ffffff;
  background: #0d867f;
}
.mode-hint {
  margin-bottom: 10px;
  font-size: 12px;
  color: #8a8a8a;
}
.add-target {
  height: 36px;
  font-size: 14px;
  line-height: 36px;
  color: #0d867f;
  text-align: center;
  border: 1px dashed #0d867f;
  border-radius: 6px;
}
.footer {
  position: fixed;
  right: 0;
  bottom: 0;
  left: 0;
  z-index: 20;
  padding: 8px 12px calc(8px + env(safe-area-inset-bottom));
  background: #ffffff;
  border-top: 1px solid #eeeeee;
  box-shadow: 0 -2px 10px rgb(0 0 0 / 6%);
  :deep(.wd-button) {
    --wot-button-primary-bg-color: #0d867f;
  }
}
</style>
src/pages/production/inventory/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,283 @@
<template>
  <view class="choose-page">
    <CardTitle title="库存操作" :hideAction="false" />
    <!-- ä¸¤ä¸ªå¤§æŒ‰é’®ï¼šæˆå“æ‹†ç›˜ / ç»žçº¿å¤ç»• -->
    <view
      v-for="opt in OPERATION_TYPES"
      :key="opt.value"
      class="type-card"
      :class="{ 'type-card-active': pendingType === opt.value }"
      @click="chooseType(opt.value)"
    >
      <view class="type-icon">
        <wd-icon :name="opt.icon" size="26px" color="#0D867F"></wd-icon>
      </view>
      <view class="type-body">
        <view class="type-title">{{ opt.label }}</view>
        <view class="type-desc">{{ opt.desc }}</view>
      </view>
      <wd-icon name="arrow-right" size="16px" color="#C8C8C8"></wd-icon>
    </view>
    <!-- æ‰«ç é¢†ç”¨æºç›˜ -->
    <view class="scan-panel">
      <view v-if="pendingType" class="scan-status">
        <wd-icon name="scan" size="16px" color="#0D867F"></wd-icon>
        <text class="scan-text">正在扫码领用源盘({{ currentTypeLabel }}),扫到后进入办理</text>
      </view>
      <view v-else class="scan-status">
        <wd-icon name="scan" size="16px" color="#9A9A9A"></wd-icon>
        <text class="scan-text scan-text-idle">点击上方按钮后直接扫码领用源盘</text>
      </view>
      <!-- æ‰«ç æžªä¸å¯ç”¨æ—¶çš„æ‰‹åŠ¨å…œåº• -->
      <view class="manual-row">
        <view class="manual-input">
          <wd-input
            v-model="manualCode"
            no-border
            clearable
            placeholder="或手动输入成品盘号,如 74082609001"
          />
        </view>
      </view>
      <view class="manual-btns">
        <wd-button size="small" plain :round="false" @click="scanSource">扫码</wd-button>
        <wd-button size="small" plain :round="false" @click="searchManual">查询并进入</wd-button>
      </view>
    </view>
    <Scan ref="scanRef" emitName="scanInventorySource" />
    <wd-toast />
  </view>
</template>
<script setup lang="ts">
import CardTitle from "@/components/card-title/index.vue";
import Scan from "@/components/scan/index.vue";
import { useToast } from "wot-design-uni";
import { onLoad, onUnload, onShow, onHide } from "@dcloudio/uni-app";
import {
  OPERATION_TYPES,
  OP_SPLIT,
  REEL_CODE_KEYS,
  SOURCE_STORAGE_KEY,
  findSourceStock,
  parseScanCode,
  pickValue,
} from "./shared";
const toast = useToast();
const scanRef = ref();
/** å·²é€‰ä¸­çš„æ“ä½œç±»åž‹ï¼Œä¸ºç©ºè¡¨ç¤ºè¿˜æ²¡ç‚¹æŒ‰é’® */
const pendingType = ref("");
const manualCode = ref("");
/** é˜²æ­¢æ‰«ç é‡å¤è§¦å‘ */
const submitting = ref(false);
const isPageVisible = ref(true);
const currentTypeLabel = computed(
  () => OPERATION_TYPES.find((opt) => opt.value === pendingType.value)?.label || ""
);
/** ç‚¹å‡»å¤§æŒ‰é’®ï¼šè®°å½•类型并立即扫码领用源盘 */
const chooseType = (type: string) => {
  pendingType.value = type;
  manualCode.value = "";
  scanSource();
};
const scanSource = () => {
  if (!pendingType.value) {
    toast.warning("请先选择成品拆盘或绞线复绕");
    return;
  }
  // #ifdef APP-PLUS
  scanRef.value?.triggerScan();
  // #endif
  // #ifndef APP-PLUS
  toast.warning("当前环境不支持硬件扫码,请用下方输入框手动查询");
  // #endif
};
const searchManual = () => {
  const code = String(manualCode.value ?? "").trim();
  if (!code) {
    toast.error("请输入成品盘号");
    return;
  }
  applySourceCode(code);
};
/** é¢†ç”¨æºç›˜ï¼šæ ¡éªŒé€šè¿‡åŽå¸¦ç€æºç›˜ä¿¡æ¯è¿›å…¥åŠžç†é¡µ */
const applySourceCode = async (code: string) => {
  if (submitting.value) return;
  submitting.value = true;
  try {
    const { record, mocked } = await findSourceStock(code);
    if (!record) {
      toast.error(`未找到成品盘号 ${code} çš„库存`);
      return;
    }
    if (!(record.enableStatus === "启用" && record.qualityStatus === "合格")) {
      toast.error(
        `该盘状态不可用(${record.qualityStatus || "-"}·${record.enableStatus || "-"}),请更换源盘`
      );
      return;
    }
    // æºç›˜ä¿¡æ¯é€šè¿‡æœ¬åœ°ç¼“存传给办理页
    uni.setStorageSync(SOURCE_STORAGE_KEY, { ...record, code, mocked });
    if (mocked) {
      toast.success("接口未联调,使用模拟源盘数据");
    } else {
      toast.success("源盘领用成功");
    }
    setTimeout(() => {
      uni.navigateTo({
        url: `/pages/production/inventory/form?type=${pendingType.value || OP_SPLIT}`,
      });
    }, 300);
  } catch (error) {
    // è¯·æ±‚层已统一提示
  } finally {
    submitting.value = false;
  }
};
/** æ‰«ç å›žè°ƒï¼šæŠŠæ‰«åˆ°çš„内容当成源盘盘号 */
const getScanCode = async (payload: any) => {
  if (!isPageVisible.value || !pendingType.value) return;
  const parsed = parseScanCode(payload);
  if (!parsed) {
    toast.error("扫码内容为空,请重新扫码");
    return;
  }
  const code = pickValue(parsed, REEL_CODE_KEYS);
  if (!code) {
    toast.error("二维码无法识别盘号,请更换二维码");
    return;
  }
  await applySourceCode(code);
};
onLoad(() => {
  // æ¯æ¬¡è¿›æ¥éƒ½ä»Žã€Œæœªé€‰ç±»åž‹ã€å¼€å§‹ï¼Œé¿å…ç”¨åˆ°ä¸Šä¸€æ¬¡çš„缓存
  uni.removeStorageSync(SOURCE_STORAGE_KEY);
  uni.$off("scanInventorySource", getScanCode);
  uni.$on("scanInventorySource", getScanCode);
});
onShow(() => {
  isPageVisible.value = true;
});
onHide(() => {
  isPageVisible.value = false;
});
onUnload(() => {
  uni.$off("scanInventorySource", getScanCode);
  isPageVisible.value = false;
});
</script>
<style lang="scss" scoped>
.choose-page {
  min-height: 100vh;
  padding: 12px;
  background: #f3f9f8;
}
.type-card {
  display: flex;
  align-items: center;
  padding: 20px 16px;
  margin-bottom: 14px;
  background: #ffffff;
  border: 1px solid #e2ecea;
  border-radius: 10px;
  box-shadow: 0 2px 8px rgb(13 134 127 / 6%);
}
.type-card-active {
  border-color: #0d867f;
  box-shadow: 0 2px 12px rgb(13 134 127 / 18%);
}
.type-icon {
  display: flex;
  flex-shrink: 0;
  align-items: center;
  justify-content: center;
  width: 48px;
  height: 48px;
  margin-right: 14px;
  background: #e7f4f2;
  border-radius: 50%;
}
.type-body {
  flex: 1;
  min-width: 0;
}
.type-title {
  font-size: 17px;
  font-weight: 600;
  color: #252525;
}
.type-desc {
  margin-top: 4px;
  font-size: 12px;
  color: #8a8a8a;
}
.scan-panel {
  padding: 14px 16px;
  background: #ffffff;
  border: 1px solid #e2ecea;
  border-radius: 10px;
}
.scan-status {
  display: flex;
  align-items: center;
}
.scan-text {
  margin-left: 6px;
  font-size: 13px;
  color: #0d867f;
}
.scan-text-idle {
  color: #9a9a9a;
}
.manual-row {
  margin-top: 8px;
}
.manual-input {
  display: flex;
  align-items: center;
  height: 36px;
  padding: 0 10px;
  background: #f7faf9;
  border: 1px solid #e2ecea;
  border-radius: 6px;
}
.manual-btns {
  display: flex;
  justify-content: flex-end;
  margin-top: 8px;
  :deep(.wd-button) {
    margin-left: 8px;
  }
}
</style>
src/pages/production/inventory/mock.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,40 @@
/**
 * ä¸´æ—¶æ¨¡æ‹Ÿæ•°æ®
 *
 * èƒŒæ™¯ï¼šã€Œåº“存操作」(成品拆盘 / ç»žçº¿å¤ç»•)依赖的成品库存、盘具称重台账接口
 * å°šæœªè”调完成,这里先给一条源盘 + ä¸€æ¡ç›˜å…·çš„æ¨¡æ‹Ÿæ•°æ®ï¼Œç”¨æ¥æŠŠ
 * ã€Œé¦–页 â†’ é€‰ç±»åž‹ â†’ æ‰«ç é¢†ç”¨æºç›˜ â†’ åŠžç†å­ç›˜/盘具 â†’ æäº¤ã€æ•´æ¡æµç¨‹è·‘通。
 *
 * è”调完成后:把 MOCK_ENABLED æ”¹æˆ false(或删除本文件并清理 shared.ts ä¸­çš„引用)。
 */
export const MOCK_ENABLED = true;
/** æ¨¡æ‹Ÿæºç›˜ï¼ˆæˆå“åº“存):扫码枪不可用时,可用这个盘号手动查询 */
export const MOCK_SOURCE_STOCK = {
  batchCode: "74082609001",
  materialType: "成品",
  model: "JL3-GIA-630-45",
  materialName: "绞线成品",
  currentLength: 5000,
  currentWeight: 1268.5,
  meterWeight: 253.7,
  qualityStatus: "合格",
  enableStatus: "启用",
  ncReturnFlag: "Y",
  createUserName: "admin",
};
/**
 * æ¨¡æ‹Ÿç›˜å…·ï¼ˆç›˜å…·ç§°é‡å°è´¦ï¼‰ï¼š
 * - è¿›å…¥åŠžç†é¡µæ—¶é»˜è®¤å¸¦å‡ºè¿™ä¸€æ¡ç›˜å…·ï¼Œå¯ç›´æŽ¥æäº¤ï¼›
 * - æ‰‹è¾“/扫到其它编号时,除编号外的字段沿用本条(方便一个单子测多个盘具)。
 */
export const MOCK_BOBBIN = {
  id: 900001,
  weighingCode: "260922-1",
  specificationModel: "JLB1A-3.73",
  diskMaterial: "钢质",
  weight: 1820.5,
  weighingTime: "2026-09-22 09:15:00",
  received: 0,
};
src/pages/production/inventory/shared.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,144 @@
import InventoryOperationApi from "@/api/product/inventoryOperation";
import TwistApi from "@/api/product/twist";
import { MOCK_BOBBIN, MOCK_ENABLED, MOCK_SOURCE_STOCK } from "./mock";
/** æ“ä½œç±»åž‹ */
export const OP_SPLIT = "SPLIT";
export const OP_STRAND_REWIND = "STRAND_REWIND";
/** ç‰©æ–™ç±»åž‹ï¼šæºç›˜ä¸Žç›®æ ‡ç›˜éƒ½èµ°ã€Œæˆå“ã€ï¼ˆä¸Ž PC ç«¯æˆå“æ‹†ç›˜ä¸€è‡´ï¼‰ */
export const MATERIAL_TYPE_FG = "成品";
/** ä¸Šä¸€æ­¥æ‰«ç é¢†ç”¨åˆ°çš„æºç›˜ï¼Œé€šè¿‡æœ¬åœ°ç¼“存传给办理页 */
export const SOURCE_STORAGE_KEY = "inventoryOperationSourceReel";
/** å¯é€‰çš„两种操作 */
export const OPERATION_TYPES = [
  {
    value: OP_SPLIT,
    label: "成品拆盘",
    desc: "一个源盘拆成多个子盘",
    icon: "fork",
  },
  {
    value: OP_STRAND_REWIND,
    label: "绞线复绕",
    desc: "整盘复绕,可沿用原盘号或生成新盘号",
    icon: "rotate",
  },
];
/** æ‰«ç å†…容里可能表示「盘号」的字段 */
export const REEL_CODE_KEYS = [
  "batchCode",
  "dishNo",
  "monofilamentNumber",
  "systemNo",
  "code",
  "盘号",
  "批次号",
];
/** æ‰«ç å†…容里可能表示「盘具编号」的字段 */
export const DISH_CODE_KEYS = [
  "weighingCode",
  "weighingNo",
  "dishCode",
  "receiveDishNo",
  "code",
  "称重编号",
  "盘具编号",
];
/** è§£æžæ‰«ç å†…容:兼容 JSON ä¸Žçº¯æ–‡æœ¬ */
export function parseScanCode(payload: any) {
  const raw = payload?.code ?? payload;
  if (raw === undefined || raw === null) return null;
  if (typeof raw === "object") return raw;
  const text = String(raw).trim();
  if (!text) return null;
  if (text.startsWith("{")) {
    try {
      return JSON.parse(text);
    } catch {
      // JSON è§£æžå¤±è´¥æ—¶æŒ‰çº¯æ–‡æœ¬ç¼–号处理
    }
  }
  return { code: text };
}
/** ä»Žè§£æžåŽçš„对象里按候选字段取编号 */
export function pickValue(data: any, keys: string[]) {
  for (const key of keys) {
    const value = data?.[key];
    if (value !== undefined && value !== null && String(value).trim() !== "") {
      return String(value).trim();
    }
  }
  return "";
}
/** ç»Ÿä¸€ä»Žåˆ†é¡µ/数组响应里取列表 */
function pickRecords(data: any): any[] {
  const pageData: any = data ?? {};
  return (
    pageData?.records ??
    pageData?.list ??
    pageData?.rows ??
    (Array.isArray(pageData) ? pageData : [])
  );
}
/**
 * æºç›˜ï¼ˆæˆå“åº“存)查询:真实接口优先;接口未联调/查不到时回落模拟数据。
 * æ¨¡æ‹Ÿæ¨¡å¼ä¸‹ç›˜å·å–扫码/输入的值,方便连续测试。
 * @returns record ä¸º null è¡¨ç¤ºç¡®å®žæŸ¥ä¸åˆ°
 */
export async function findSourceStock(code: string) {
  try {
    const { data } = await InventoryOperationApi.searchStock({
      keyword: code,
      materialType: MATERIAL_TYPE_FG,
      ncReturnFlag: "Y",
    });
    const records = pickRecords(data);
    const exact = (records || []).find((row: any) => String(row?.batchCode ?? "").trim() === code);
    const record = exact ?? (records?.length === 1 ? records[0] : null);
    if (record) return { record, mocked: false };
  } catch {
    // æŽ¥å£æœªå°±ç»ªï¼Œèµ°ä¸‹é¢çš„æ¨¡æ‹Ÿæ•°æ®
  }
  if (!MOCK_ENABLED) return { record: null, mocked: false };
  return { record: { ...MOCK_SOURCE_STOCK, batchCode: code }, mocked: true };
}
/**
 * ç›˜å…·ï¼ˆç›˜å…·ç§°é‡å°è´¦ï¼‰æŸ¥è¯¢ï¼šçœŸå®žæŽ¥å£ä¼˜å…ˆï¼›æŽ¥å£æœªè”è°ƒ/查不到时回落模拟数据。
 * æ¨¡æ‹Ÿæ¨¡å¼ä¸‹ç›˜å…·ç¼–号取扫码/输入的值,方便一个单子里测试多个盘具。
 * @returns record ä¸º null è¡¨ç¤ºç¡®å®žæŸ¥ä¸åˆ°
 */
export async function findBobbin(code: string) {
  try {
    const { data } = await TwistApi.queryBobbinWeighingPage({
      current: 1,
      size: 50,
      weighingCode: code,
    });
    const records = pickRecords(data);
    const exact = (records || []).find(
      (row: any) => String(row?.weighingCode ?? "").trim() === code
    );
    const record = exact ?? (records?.length === 1 ? records[0] : null);
    if (record) return { record, mocked: false };
  } catch {
    // æŽ¥å£æœªå°±ç»ªï¼Œèµ°ä¸‹é¢çš„æ¨¡æ‹Ÿæ•°æ®
  }
  if (!MOCK_ENABLED) return { record: null, mocked: false };
  return { record: { ...MOCK_BOBBIN, weighingCode: code }, mocked: true };
}
/** ç›˜å…·æ˜¯å¦å·²è¢«é¢†ç”¨ */
export function isBobbinReceived(row: any) {
  const value = row?.received ?? row?.isReceived;
  return [true, 1, "1", "true", "是"].includes(value);
}
src/pages/production/twist/components/MonofilCard.vue
@@ -9,10 +9,12 @@
    >
      <wd-card>
        <wd-cell-group :border="true">
          <wd-cell title="单丝编号" :value="data.monofilamentNumber" />
          <wd-cell title="理论长度" :value="data.amount + ' (m)'" />
          <wd-cell title="生产长度" :value="data.actuallyLength + ' (m)'" />
          <wd-cell title="重量" :value="data.actuallyWeight + ' (kg)'" />
          <wd-cell title="单丝编号" :value="data.monofilamentNumber || '-'" />
          <wd-cell title="单丝盘长度" :value="reelLengthText" />
          <wd-cell title="绞层" :value="layerText" />
          <wd-cell title="规格型号" :value="specificationText" />
          <wd-cell title="理论长度" :value="amountText" />
          <wd-cell title="重量" :value="weightText" />
        </wd-cell-group>
      </wd-card>
    </view>
@@ -23,7 +25,7 @@
</template>
<script setup lang="ts">
import { ref } from "vue";
import { ref, computed } from "vue";
const props = defineProps({
  data: {
@@ -32,6 +34,21 @@
  },
});
/** æœ‰å€¼æ‰æ‹¼å•位,空值显示 "-",避免出现 "undefined (m)" */
const withUnit = (value: any, unit: string) =>
  value === undefined || value === null || value === "" ? "-" : `${value} (${unit})`;
/** å•丝盘长度:对应原来的实际长度 actuallyLength */
const reelLengthText = computed(() => withUnit(props.data?.actuallyLength, "m"));
const amountText = computed(() => withUnit(props.data?.amount, "m"));
const weightText = computed(() => withUnit(props.data?.actuallyWeight, "kg"));
/** ç»žå±‚:优先 twistedLayer,兜底 layer */
const layerText = computed(() => props.data?.twistedLayer || props.data?.layer || "-");
/** è§„格型号:优先 specificationModel,兜底 model */
const specificationText = computed(
  () => props.data?.specificationModel || props.data?.model || "-"
);
const emit = defineEmits(["delete", "swipe-open"]);
const translateX = ref(0);
src/pages/production/twist/receive/plate/form.vue
@@ -13,11 +13,11 @@
      />
      <wd-input
        v-model="model.model"
        label="尺寸"
        label="规格型号"
        label-width="100px"
        prop="model"
        clearable
        placeholder="请输入尺寸"
        placeholder="请输入规格型号"
      />
      <wd-input
        v-model="model.amount"
@@ -36,16 +36,6 @@
        placeholder="请选择单位"
        clearable
        @confirm="handleUnitChange"
      />
      <wd-picker
        v-model="model.supplier"
        :columns="supplierOptions"
        label="厂家"
        label-width="100px"
        prop="supplier"
        placeholder="请选择厂家"
        clearable
        @confirm="handleSupplierChange"
      />
    </wd-cell-group>
    <wd-toast />
@@ -78,7 +68,6 @@
  model: undefined, // å°ºå¯¸
  amount: undefined, // æ•°é‡
  unit: "只", // å•位,默认值为"只"
  supplier: undefined,
  type: "盘具",
});
@@ -92,9 +81,6 @@
// å•位字典数据
const unitOptions = ref<Array<{ label: string; value: string }>>([]);
// åŽ‚å®¶å­—å…¸æ•°æ®
const supplierOptions = ref<Array<{ label: string; value: string }>>([]);
// åŠ è½½ç›˜å…·ç±»åž‹å­—å…¸æ•°æ®
const loadDiskMaterialDict = async () => {
@@ -133,21 +119,6 @@
  }
};
// åŠ è½½åŽ‚å®¶å­—å…¸æ•°æ®
const loadSupplierDict = async () => {
  try {
    const res = await ManageApi.dictAPI("reel_factory");
    if (res.data && Array.isArray(res.data)) {
      supplierOptions.value = res.data.map((item: any) => ({
        label: item.dictLabel || "",
        value: item.dictValue || "",
      }));
    }
  } catch (error) {
    // åŠ è½½å­—å…¸å¤±è´¥ï¼Œé™é»˜å¤„ç†
  }
};
// å¤„理盘具类型选择
const handleDiskMaterialChange = (val: any) => {
  model.diskMaterial = val.value;
@@ -156,11 +127,6 @@
// å¤„理单位选择
const handleUnitChange = (val: any) => {
  model.unit = val.value;
};
// å¤„理厂家选择
const handleSupplierChange = (val: any) => {
  model.supplier = val.value;
};
// ç›‘听 model.diskMaterial å˜åŒ–,同步选择器显示
@@ -200,14 +166,13 @@
  // æ›´æ–°åˆ—表中对应的数据项
  const updatedList = currentList.map((item) => {
    if (item.id === currentId) {
      // ä¿ç•™åŽŸæœ‰æ•°æ®ï¼Œç„¶åŽæ›´æ–°ä¿®æ”¹çš„å­—æ®µ
      // ä¿ç•™åŽŸæœ‰æ•°æ®ï¼ˆå«åŽ‚å®¶ç­‰åŽ†å²å­—æ®µï¼‰ï¼Œåªæ›´æ–°å¯ç¼–è¾‘å­—æ®µ
      const updatedItem = {
        ...item, // å…ˆä¿ç•™åŽŸæœ‰çš„æ‰€æœ‰æ•°æ®
        ...item,
        diskMaterial: model.diskMaterial,
        model: model.model,
        amount: model.amount,
        unit: model.unit,
        supplier: model.supplier,
        type: model.type,
      };
      return updatedItem;
@@ -244,7 +209,6 @@
    model.model = currentItem.model;
    model.amount = currentItem.amount;
    model.unit = currentItem.unit || "只";
    model.supplier = currentItem.supplier;
    model.type = currentItem.type || "盘具";
    // è®¾ç½®ç›˜å…·ç±»åž‹çš„回显值
    diskMaterialValue.value = currentItem.diskMaterial || "";
@@ -258,7 +222,6 @@
onMounted(async () => {
  await loadDiskMaterialDict();
  await loadUnitDict();
  await loadSupplierDict();
});
// ç›‘听编辑数据变化,自动回显
@@ -270,7 +233,6 @@
      model.model = newData.model || "";
      model.amount = newData.amount || "";
      model.unit = newData.unit || "只";
      model.supplier = newData.supplier || "";
      model.type = newData.type || "盘具";
      diskMaterialValue.value = newData.diskMaterial || "";
    }
@@ -284,7 +246,6 @@
  model.model = undefined;
  model.amount = undefined;
  model.unit = "只";
  model.supplier = undefined;
  model.type = "盘具";
  diskMaterialValue.value = "";
};
src/pages/production/twist/receive/plate/index.vue
@@ -1,36 +1,61 @@
<template>
  <view class="list">
  <view class="list_box">
    <z-paging
      ref="pagingRef"
      v-model="cardList"
      v-model="plateData"
      :fixed="false"
      :auto-show-back-to-top="true"
      :loading-more-enabled="false"
      @query="getList"
    >
      <template #top>
        <CardTitle title="盘具领用" :hideAction="true" :full="false" @action="addReport" />
      </template>
      <wd-card v-for="(item, index) in cardList" :key="index" type="rectangle" custom-class="round">
        <template #title>
          <view class="flex justify-between">
            <view>
              <wd-icon name="a-rootlist" color="#0D867F"></wd-icon>
              <text class="text-[#252525] ml-2 font-medium">{{ item.diskMaterial }}</text>
        <CardTitle title="盘具领用" :hideAction="false">
          <template #action>
            <view class="action-wrap">
              <!-- å·¥å•要求的盘具规格型号 / æè´¨ï¼šå±•示在扫码(新增)按钮旁 -->
              <view class="reel-defaults">
                <text class="reel-default-item">规格型号:{{ defaultDishModel || "-" }}</text>
                <text class="reel-default-item">材质:{{ defaultDishMaterial || "-" }}</text>
              </view>
              <wd-button type="icon" icon="scan" color="#0D867F" @click="openScan"></wd-button>
            </view>
            <view class="text-[#A8A8A8]" @click="toEdit(item.id)">编辑</view>
          </view>
        </template>
        <ProductionCard :data="cardAttr" :value="item" color="#0D867F" />
      </wd-card>
    </z-paging>
    <wd-popup v-model="addDialog.visible" position="bottom" custom-class="yl-popup">
      <view class="action px-3">
        <wd-button type="text" @click="cancelAdd">取消</wd-button>
        <wd-button type="text" @click="submitAdd">确定</wd-button>
          </template>
        </CardTitle>
        <view class="scan-tip">请扫描盘具标签二维码领用盘具(盘具需先在盘具称重台账中称重)</view>
      </template>
      <view class="content">
        <wd-card
          v-for="(item, index) in plateData"
          :key="item.id ?? item.dishCode ?? index"
          type="rectangle"
          custom-class="round"
        >
          <template #title>
            <view class="flex justify-between">
              <view>
                <wd-icon name="a-rootlist" color="#0D867F"></wd-icon>
                <text class="text-[#252525] ml-2 font-medium">{{ item.diskMaterial }}</text>
              </view>
              <view class="flex items-center">
                <text v-if="!item.id" class="text-[#E6A23C] mr-3 text-[12px]">待保存</text>
                <view v-if="!item.id" class="text-[#A8A8A8]" @click="removeDish(item)">移除</view>
                <view v-else class="text-[#A8A8A8]" @click="toEdit(item)">编辑</view>
              </view>
            </view>
          </template>
          <ProductionCard :data="cardAttr" :value="item" color="#0D867F" />
        </wd-card>
      </view>
      <PlateForm ref="addFormRef" mode="add" @refresh="reloadList" />
    </wd-popup>
    <wd-popup v-model="editDialog.visible" position="bottom" custom-class="yl-popup">
    </z-paging>
    <view class="save-footer">
      <wd-button block @click="save">
        <text class="text-[#fff]">保存</text>
      </wd-button>
    </view>
    <wd-popup v-model="editDialog.visible" position="bottom" custom-class="yl-popup" :z-index="30">
      <view class="action px-3">
        <wd-button type="text" @click="cancelEdit">取消</wd-button>
        <wd-button type="text" @click="submitEdit">确定</wd-button>
@@ -42,6 +67,8 @@
        @refresh="reloadList"
      />
    </wd-popup>
    <Scan ref="scanRef" emitName="scanPlate" />
    <wd-toast />
  </view>
</template>
@@ -51,28 +78,37 @@
import ProductionCard from "../../../components/ProductionCard.vue";
import { useToast } from "wot-design-uni";
import PlateForm from "./form.vue";
import { onLoad, onShow } from "@dcloudio/uni-app";
import { onLoad, onUnload, onShow, onHide } from "@dcloudio/uni-app";
import ManageApi from "@/api/product/manage";
import TwistApi from "@/api/product/twist";
import zPaging from "@/components/z-paging/z-paging.vue";
import Scan from "@/components/scan/index.vue";
const paramsId = ref();
const pagingRef = ref();
const addFormRef = ref();
const editFormRef = ref();
const scanRef = ref();
const toast = useToast();
const addDialog = reactive({
  visible: false,
});
const editDialog = reactive({
  visible: false,
  currentItem: null as any,
  editId: undefined as number | undefined,
});
const cardList = ref<any[]>([]);
const plateData = ref<any[]>([]);
const isPageVisible = ref(true);
/** å·¥å•要求的盘具规格型号 / æè´¨ï¼ˆå±•示在新增按钮旁,领用后作为规格型号默认值) */
const defaultDishModel = ref("");
const defaultDishMaterial = ref("");
const cardAttr = ref<any[]>([
  {
    label: "尺寸",
    label: "盘具编号",
    prop: "dishCode",
  },
  {
    label: "规格型号",
    prop: "model",
  },
  {
@@ -88,48 +124,39 @@
    label: "单位",
    prop: "unit",
  },
  {
    label: "厂家",
    prop: "supplier",
    span: 14,
  },
]);
const toEdit = (id: number) => {
  // æ‰¾åˆ°éœ€è¦ç¼–辑的项
  const itemToEdit = cardList.value.find((item) => item.id === id);
  if (itemToEdit) {
    editDialog.currentItem = itemToEdit;
    editDialog.editId = id;
    editDialog.visible = true;
  }
/** ç»Ÿä¸€ç›˜å…·æ•°æ®ï¼šç›˜å…·ç¼–号兼容 weighingCode,数量/单位补默认值 */
const normalizeDish = (item: any) => ({
  ...item,
  dishCode: item?.dishCode ?? item?.weighingCode ?? "",
  weighingCode: item?.weighingCode ?? item?.dishCode ?? "",
  amount: item?.amount ?? 1,
  unit: item?.unit ?? "只",
});
/** æ˜¯å¦å·²è¢«é¢†ç”¨ï¼šå…¼å®¹ 1/"1"/true/"是" */
const isReceived = (row: any) => {
  const value = row?.received ?? row?.isReceived;
  return [true, 1, "1", "true", "是"].includes(value);
};
const addReport = () => {
  // é‡ç½®è¡¨å•数据
  if (addFormRef.value) {
    addFormRef.value.resetFormData();
  }
  addDialog.visible = true;
const toEdit = (item: any) => {
  editDialog.currentItem = item;
  editDialog.editId = item?.id;
  editDialog.visible = true;
};
const submitAdd = async () => {
  toast.show("提交");
  const success = await addFormRef.value.submit();
  if (success) {
    addDialog.visible = false;
/** ç§»é™¤å°šæœªä¿å­˜çš„æ‰«ç è®°å½• */
const removeDish = (item: any) => {
  const index = plateData.value.findIndex((row) => row.dishCode === item.dishCode && !row.id);
  if (index > -1) {
    plateData.value.splice(index, 1);
  }
};
const cancelAdd = () => {
  toast.show("取消");
  addDialog.visible = false;
  // æ¸…空表单数据
  addFormRef.value?.$refs?.form?.resetFields();
};
const submitEdit = async () => {
  const success = await editFormRef.value.submitEdit(cardList.value, editDialog.editId);
  const success = await editFormRef.value.submitEdit(plateData.value, editDialog.editId);
  if (success) {
    editDialog.visible = false;
    reloadList();
@@ -137,33 +164,268 @@
};
const cancelEdit = () => {
  toast.show("取消");
  editDialog.visible = false;
};
const reloadList = () => {
  pagingRef.value.refresh();
  pagingRef.value?.refresh();
};
const getList = async () => {
  const { code, data } = await ManageApi.getStrandedWireDish({
    wireId: paramsId.value,
    type: "盘具",
  });
  if (code == 200) {
    pagingRef.value.complete(data);
  try {
    const { code, data } = await ManageApi.getStrandedWireDish({
      wireId: paramsId.value,
      type: "盘具",
    });
    if (code == 200) {
      const list = (Array.isArray(data) ? data : []).map(normalizeDish);
      pagingRef.value.complete(list);
    } else {
      pagingRef.value.complete([]);
    }
  } catch (error) {
    pagingRef.value.complete([]);
  }
};
// ============ æ‰«ç æ–°å¢žç›˜å…· ============
/** è§£æžæ‰«ç å†…容:兼容 JSON ä¸Žçº¯æ–‡æœ¬ç§°é‡ç¼–号 */
const parseScanCode = (payload: any) => {
  const raw = payload?.code ?? payload;
  if (raw === undefined || raw === null) return null;
  if (typeof raw === "object") return raw;
  const text = String(raw).trim();
  if (!text) return null;
  if (text.startsWith("{")) {
    try {
      return JSON.parse(text);
    } catch {
      // JSON è§£æžå¤±è´¥æ—¶æŒ‰çº¯æ–‡æœ¬ç§°é‡ç¼–号处理
    }
  }
  return { weighingCode: text };
};
/** ä»Žæ‰«ç å†…容里取盘具(称重)编号 */
const pickWeighingCode = (data: any) => {
  const keys = [
    "weighingCode",
    "weighingNo",
    "dishCode",
    "receiveDishNo",
    "batchCode",
    "code",
    "称重编号",
    "盘具编号",
    "盘号",
  ];
  for (const key of keys) {
    const value = data?.[key];
    if (value !== undefined && value !== null && String(value).trim() !== "") {
      return String(value).trim();
    }
  }
  return "";
};
/** å°è´¦è¡Œ â†’ ç›˜å…·é¢†ç”¨è¡Œ */
const buildDishFromLedger = (record: any) =>
  normalizeDish({
    wireId: paramsId.value,
    diskMaterial: record?.diskMaterial ?? undefined,
    // è§„格型号取该盘具在称重台账里的规格型号
    model: record?.specificationModel ?? undefined,
    weight: record?.weight ?? undefined,
    amount: 1,
    unit: "只",
    dishCode: String(record?.weighingCode ?? "").trim(),
    weighingCode: String(record?.weighingCode ?? "").trim(),
    type: "盘具",
  });
const getScanCode = async (payload: any) => {
  if (!isPageVisible.value) return;
  const parsed = parseScanCode(payload);
  if (!parsed) {
    toast.error("扫码内容为空,请重新扫码");
    return;
  }
  const code = pickWeighingCode(parsed);
  if (!code) {
    toast.error("二维码无法识别盘具编号,请更换二维码");
    return;
  }
  if (plateData.value.some((item) => String(item.dishCode ?? item.weighingCode ?? "") === code)) {
    toast.error("该盘具已在本单领用列表中,请勿重复扫码");
    return;
  }
  try {
    const { data } = await TwistApi.queryBobbinWeighingPage({
      current: 1,
      size: 50,
      weighingCode: code,
    });
    const pageData: any = data ?? {};
    const records =
      pageData?.records ??
      pageData?.list ??
      pageData?.rows ??
      (Array.isArray(pageData) ? pageData : []);
    const exact = (records || []).find((row: any) => String(row?.weighingCode ?? "") === code);
    const record = exact ?? (records?.length === 1 ? records[0] : null);
    if (!record) {
      toast.error("盘具称重台账中未找到该盘具,请先在盘具称重台账中称重");
      return;
    }
    if (isReceived(record)) {
      toast.error("该盘具已被领用,请更换盘具");
      return;
    }
    plateData.value.push(buildDishFromLedger(record));
    toast.success("扫码成功,请点击保存提交");
  } catch (error) {
    // è¯·æ±‚层已统一提示失败原因
  }
};
const openScan = () => {
  scanRef.value?.triggerScan();
};
// ============ ä¿å­˜ ============
const isNewDish = (item: any) => item?.id === undefined || item?.id === null;
const save = () => {
  const newCount = plateData.value.filter(isNewDish).length;
  if (!newCount) {
    toast.error("没有新扫码的盘具需要保存");
    return;
  }
  uni.showModal({
    title: "提示",
    content: `确认保存 ${newCount} æ¡ç›˜å…·é¢†ç”¨æ•°æ®å—?`,
    success: async (res) => {
      if (res.confirm) {
        await handleConfirmSave();
      }
    },
  });
};
const handleConfirmSave = async () => {
  const newData = plateData.value.filter(isNewDish);
  if (newData.length === 0) {
    toast.error("没有新扫码的盘具需要保存");
    return;
  }
  const invalid = newData.find((item) => !item.dishCode || !item.diskMaterial);
  if (invalid) {
    toast.error(`${invalid.dishCode || "盘具"}:盘具数据不完整,请重新扫码`);
    return;
  }
  const { code, msg } = await TwistApi.addStrandedWireDish(newData);
  if (code == 200) {
    toast.success(msg || "保存成功");
    pagingRef.value?.refresh();
  } else {
    toast.error(msg || "保存失败");
  }
};
/**
 * å–工单要求的盘具型号 / ç›˜å…·æè´¨ï¼ˆå±•示在扫码按钮旁)
 * åªå–盘具相关字段 diskModel / diskMaterial,不拿产品规格 model å…œåº•
 */
const loadDishDefaults = async () => {
  if (!paramsId.value) return;
  try {
    const { data } = await TwistApi.getTwistDetailById({ id: paramsId.value });
    defaultDishModel.value = data?.diskModel || "";
    defaultDishMaterial.value = data?.diskMaterial || "";
  } catch (error) {
    // å–不到就展示 "-",不阻塞领用
  }
};
onLoad((options: any) => {
  paramsId.value = options.id;
  uni.$off("scanPlate", getScanCode);
  uni.$on("scanPlate", getScanCode);
  paramsId.value = options?.id;
  loadDishDefaults();
});
// ç›˜å…·é¢†ç”¨æ˜¯ã€ŒèŠ¯çº¿/单丝/盘具」页签之一,组件可能晚于页面 onShow æ‰æŒ‚载,
// è¿™é‡Œå…œåº•置为可见,避免扫码被误判为不可见而静默丢弃。
onMounted(() => {
  isPageVisible.value = true;
});
onShow(() => {
  isPageVisible.value = true;
});
onHide(() => {
  isPageVisible.value = false;
});
onUnload(() => {
  uni.$off("scanPlate", getScanCode);
  isPageVisible.value = false;
});
</script>
<style lang="scss" scoped>
.list {
  height: calc(100vh - 120px);
  padding: 12px;
.list_box {
  height: calc(100vh - 160px);
  background: #f3f9f8;
  display: flex;
  flex-direction: column;
}
.action {
  display: flex;
  justify-content: space-between;
}
:deep(.wd-button__content) {
  color: #0d867f;
}
.scan-tip {
  padding: 0 12px 8px;
  font-size: 12px;
  color: #8a8a8a;
}
.action-wrap {
  display: flex;
  align-items: center;
}
.reel-defaults {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  margin-right: 8px;
}
.reel-default-item {
  font-size: 11px;
  line-height: 16px;
  color: #646874;
}
.content {
  width: 100%;
  padding-bottom: 76px;
  box-sizing: border-box;
  :deep() {
    .round {
@@ -172,8 +434,33 @@
  }
}
.action {
.save-footer {
  position: fixed;
  right: 0;
  bottom: 0;
  left: 0;
  z-index: 20;
  padding: 8px 12px calc(8px + env(safe-area-inset-bottom));
  background: #ffffff;
  border-top: 1px solid #eeeeee;
  box-shadow: 0 -2px 10px rgb(0 0 0 / 6%);
}
:deep(.zp-paging-container) {
  background: transparent !important;
  flex: 1;
  display: flex;
  justify-content: space-between;
  flex-direction: column;
}
:deep(.zp-paging-container-content) {
  background: transparent !important;
  flex: 1;
  display: flex;
  flex-direction: column;
}
:deep(.zp-paging-container-top) {
  flex-shrink: 0;
}
</style>
src/pages/production/twist/report/index.vue
@@ -221,7 +221,29 @@
  }
};
// å¿…须有盘具领用记录才能报工:接口异常时不拦截(避免网络问题阻断生产),
// åªæœ‰æ˜Žç¡®æŸ¥è¯¢æˆåŠŸä¸”æ²¡æœ‰ä»»ä½•ç›˜å…·é¢†ç”¨æ—¶æ‰æ‹¦æˆªã€‚
const ensureReelDish = async () => {
  try {
    const { code, data } = await ManageApi.getStrandedWireDish({
      wireId: paramsId.value,
      type: "盘具",
    });
    if (code !== 200) return true;
    const list = Array.isArray(data) ? data : [];
    if (list.length === 0) {
      toast.warning("请先在【盘具领用】中扫码领用盘具,再进行绞线报工");
      return false;
    }
    return true;
  } catch (error) {
    return true;
  }
};
const addReport = async () => {
  // å¿…须有盘具领用记录才能绞线报工
  if (!(await ensureReelDish())) return;
  // æ£€æŸ¥æ˜¯å¦æ‰€æœ‰æ•°æ®éƒ½å·²ä¿å­˜ï¼ˆéƒ½æœ‰id)
  if (twistReportList.value.length > 0 && twistReportList.value.every((item) => item.id)) {
    // ç¡®ä¿ä¸»è¡¨æ•°æ®å·²èŽ·å–
src/static/icons/inspection.png
src/static/icons/inventoryOperation.png
src/static/icons/outbound.png
src/static/icons/production.png
src/static/icons/timely.png