From e0fc86fda41a8777a0cec5a9de7aa9f1474f5dc8 Mon Sep 17 00:00:00 2001
From: spring <2396852758@qq.com>
Date: 星期四, 08 十月 2026 17:09:52 +0800
Subject: [PATCH] fix: 库存操作原型绘制。单丝领用,添加字段

---
 src/pages.json                                        |   12 
 src/pages/production/twist/receive/plate/index.vue    |  439 ++++++++++++--
 src/pages/production/inventory/form.vue               |  633 +++++++++++++++++++++
 src/pages/production/inventory/index.vue              |  283 +++++++++
 src/pages/production/twist/receive/plate/form.vue     |   47 -
 src/pages/index/index.vue                             |   21 
 src/pages/production/twist/components/MonofilCard.vue |   27 
 src/api/system/user.ts                                |   11 
 .env.development                                      |    2 
 src/pages/production/twist/report/index.vue           |   22 
 src/static/icons/inventoryOperation.png               |    0 
 src/api/product/twist.ts                              |   16 
 src/static/icons/inspection.png                       |    0 
 src/static/icons/timely.png                           |    0 
 src/manifest.json                                     |    2 
 src/static/icons/production.png                       |    0 
 src/api/product/inventoryOperation.ts                 |   34 +
 src/pages/production/inventory/mock.ts                |   40 +
 src/pages/production/inventory/shared.ts              |  144 ++++
 src/static/icons/outbound.png                         |    0 
 20 files changed, 1,603 insertions(+), 130 deletions(-)

diff --git a/.env.development b/.env.development
index 4ca4dc4..e8b473b 100644
--- a/.env.development
+++ b/.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' #
 
diff --git a/src/api/product/inventoryOperation.ts b/src/api/product/inventoryOperation.ts
new file mode 100644
index 0000000..bd6106e
--- /dev/null
+++ b/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,
+    });
+  },
+
+  // 搴撳瓨鐩樺彿鎼滅储锛堟垚鍝�/鍗婃垚鍝侊級锛宬eyword 鏀寔鐩樺彿/瑙勬牸鍨嬪彿
+  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;
diff --git a/src/api/product/twist.ts b/src/api/product/twist.ts
index 5677da7..8b3a684 100644
--- a/src/api/product/twist.ts
+++ b/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",
diff --git a/src/api/system/user.ts b/src/api/system/user.ts
index 5e1ee85..941d8f4 100644
--- a/src/api/system/user.ts
+++ b/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 鏌ヨ鍙傛暟
diff --git a/src/manifest.json b/src/manifest.json
index 79792d1..520a095 100644
--- a/src/manifest.json
+++ b/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鐗规湁鐩稿叧 */
diff --git a/src/pages.json b/src/pages.json
index 17e025e..ee2796f 100644
--- a/src/pages.json
+++ b/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": "鏃舵晥浠诲姟"
diff --git a/src/pages/index/index.vue b/src/pages/index/index.vue
index ce38cde..d16a5be 100644
--- a/src/pages/index/index.vue
+++ b/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) {
     // 濡傛灉鏄贰妫�鍛橈紝鏄剧ず宸℃鑿滃崟
diff --git a/src/pages/production/inventory/form.vue b/src/pages/production/inventory/form.vue
new file mode 100644
index 0000000..cc00599
--- /dev/null
+++ b/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>
diff --git a/src/pages/production/inventory/index.vue b/src/pages/production/inventory/index.vue
new file mode 100644
index 0000000..ac420b1
--- /dev/null
+++ b/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">姝e湪鎵爜棰嗙敤婧愮洏锛坽{ 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>
diff --git a/src/pages/production/inventory/mock.ts b/src/pages/production/inventory/mock.ts
new file mode 100644
index 0000000..08d3b2e
--- /dev/null
+++ b/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,
+};
diff --git a/src/pages/production/inventory/shared.ts b/src/pages/production/inventory/shared.ts
new file mode 100644
index 0000000..d2167de
--- /dev/null
+++ b/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",
+  "绉伴噸缂栧彿",
+  "鐩樺叿缂栧彿",
+];
+
+/** 瑙f瀽鎵爜鍐呭锛氬吋瀹� 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 瑙f瀽澶辫触鏃舵寜绾枃鏈紪鍙峰鐞�
+    }
+  }
+  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 : [])
+  );
+}
+
+/**
+ * 婧愮洏锛堟垚鍝佸簱瀛橈級鏌ヨ锛氱湡瀹炴帴鍙d紭鍏堬紱鎺ュ彛鏈仈璋�/鏌ヤ笉鍒版椂鍥炶惤妯℃嫙鏁版嵁銆�
+ * 妯℃嫙妯″紡涓嬬洏鍙峰彇鎵爜/杈撳叆鐨勫�硷紝鏂逛究杩炵画娴嬭瘯銆�
+ * @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 };
+}
+
+/**
+ * 鐩樺叿锛堢洏鍏风О閲嶅彴璐︼級鏌ヨ锛氱湡瀹炴帴鍙d紭鍏堬紱鎺ュ彛鏈仈璋�/鏌ヤ笉鍒版椂鍥炶惤妯℃嫙鏁版嵁銆�
+ * 妯℃嫙妯″紡涓嬬洏鍏风紪鍙峰彇鎵爜/杈撳叆鐨勫�硷紝鏂逛究涓�涓崟瀛愰噷娴嬭瘯澶氫釜鐩樺叿銆�
+ * @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);
+}
diff --git a/src/pages/production/twist/components/MonofilCard.vue b/src/pages/production/twist/components/MonofilCard.vue
index 972c910..a1ffcbc 100644
--- a/src/pages/production/twist/components/MonofilCard.vue
+++ b/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);
diff --git a/src/pages/production/twist/receive/plate/form.vue b/src/pages/production/twist/receive/plate/form.vue
index ed1b5e1..78bf4b5 100644
--- a/src/pages/production/twist/receive/plate/form.vue
+++ b/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 = "";
 };
diff --git a/src/pages/production/twist/receive/plate/index.vue b/src/pages/production/twist/receive/plate/index.vue
index 63a768e..9460126 100644
--- a/src/pages/production/twist/receive/plate/index.vue
+++ b/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([]);
   }
 };
+
+// ============ 鎵爜鏂板鐩樺叿 ============
+/** 瑙f瀽鎵爜鍐呭锛氬吋瀹� 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 瑙f瀽澶辫触鏃舵寜绾枃鏈О閲嶇紪鍙峰鐞�
+    }
+  }
+  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>
diff --git a/src/pages/production/twist/report/index.vue b/src/pages/production/twist/report/index.vue
index 7a90a68..18ef6a4 100644
--- a/src/pages/production/twist/report/index.vue
+++ b/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)) {
     // 纭繚涓昏〃鏁版嵁宸茶幏鍙�
diff --git a/src/static/icons/inspection.png b/src/static/icons/inspection.png
new file mode 100644
index 0000000..8924761
--- /dev/null
+++ b/src/static/icons/inspection.png
Binary files differ
diff --git a/src/static/icons/inventoryOperation.png b/src/static/icons/inventoryOperation.png
new file mode 100644
index 0000000..857b6b7
--- /dev/null
+++ b/src/static/icons/inventoryOperation.png
Binary files differ
diff --git a/src/static/icons/outbound.png b/src/static/icons/outbound.png
new file mode 100644
index 0000000..6b23be6
--- /dev/null
+++ b/src/static/icons/outbound.png
Binary files differ
diff --git a/src/static/icons/production.png b/src/static/icons/production.png
new file mode 100644
index 0000000..2110dda
--- /dev/null
+++ b/src/static/icons/production.png
Binary files differ
diff --git a/src/static/icons/timely.png b/src/static/icons/timely.png
new file mode 100644
index 0000000..2dff58a
--- /dev/null
+++ b/src/static/icons/timely.png
Binary files differ

--
Gitblit v1.9.3