yinxia.yang 1 deň pred
rodič
commit
563aa9c351

+ 9 - 0
README.md

@@ -21,6 +21,15 @@ npm run build
 npm run preview
 ```
 
+当前 `package.json` 的 `scripts` 配置如下:
+
+| 命令 | 实际执行 | 用途 |
+| --- | --- | --- |
+| `npm run dev` | `vite` | 启动本地开发服务 |
+| `npm run build` | `vue-tsc -b && vite build` | 先执行 TypeScript / Vue 类型检查,再生成生产构建产物 |
+| `npm run preview` | `vite preview` | 本地预览生产构建产物,需先执行 `npm run build`,不作为生产服务器使用 |
+| `npm test` | `vitest run` | 单次运行全部测试,不进入监听模式 |
+
 开发模式默认开启 Mock。生产构建默认关闭 Mock,`preview` 需要配套后端,不会自动使用演示数据。构建有大 chunk 警告,不影响运行;正式发布前建议进行路由和组件按需拆包。
 
 ## 功能与数据

BIN
public/favicon.ico


+ 4 - 0
src/api/types/WmsDashboardResponse.ts

@@ -9,6 +9,10 @@ export type WmsDashboardResponse = {
      * SKU 总数(已启用)
      */
     skuCount?: number;
+    /**
+     * 今日操作流水轨迹,最多 10 条
+     */
+    todayFlows?: Array<string>;
     /**
      * 当日入库笔数(已完成)
      */

+ 1 - 0
src/api/types/WmsInboundRecordResponse.ts

@@ -64,6 +64,7 @@ export type WmsInboundRecordResponse = {
      * 入库数量
      */
     qty?: number;
+    relatedReversalFlows?: Array<string>;
     /**
      * 备注
      */

+ 4 - 0
src/api/types/WmsLedgerFlowVO.ts

@@ -65,6 +65,10 @@ export type WmsLedgerFlowVO = {
      * 原始记录 ID(用于查详情/凭证)
      */
     recordId?: number;
+    /**
+     * 关联冲销流水详情列表
+     */
+    relatedReversalFlows?: Array<string>;
     /**
      * 备注
      */

+ 1 - 0
src/api/types/WmsOutboundRecordResponse.ts

@@ -68,6 +68,7 @@ export type WmsOutboundRecordResponse = {
      * 出库数量
      */
     qty?: number;
+    relatedReversalFlows?: Array<string>;
     /**
      * 备注
      */

+ 1 - 1
src/http/index.ts

@@ -47,4 +47,4 @@ export const services = {
 }
 
 export const http = services.warehouse
-export const authHttp = services.files
+export const authHttp = createServiceHttp(import.meta.env.VITE_API_AUTH_URL || defaultBaseURL, false)

+ 15 - 4
src/views/Inbound.vue

@@ -5,7 +5,6 @@ import { Plus } from '@element-plus/icons-vue'
 import { ElMessage, ElMessageBox } from 'element-plus'
 import type { FormInstance, FormRules } from 'element-plus'
 import EvidenceUpload from '../components/EvidenceUpload.vue'
-import EntryAudit from '../components/EntryAudit.vue'
 import { api } from '../api'
 import { blankEntry } from '../utils/entry'
 import { useUploads } from '../utils/uploads'
@@ -13,7 +12,7 @@ import type { Attachment, Entry } from '../api/indexType'
 import type { WmsInboundRecordResponse, WmsInboundSaveRequest, WmsSkuResponse, WmsReversalPreCheckResponse } from '../api/indexType'
 import { dateTime, fieldErrors, report, reversalLimit, refreshAfterWrite } from '../utils/ui'
 
-type InboundEntry = Entry & { recordId?: number; alreadyReversedQty: number }
+type InboundEntry = Entry & { recordId?: number; alreadyReversedQty: number; relatedReversalFlows?: WmsInboundRecordResponse['relatedReversalFlows'] }
 const inboundRows = ref<InboundEntry[]>([])
 const skuOptions = ref<WmsSkuResponse[]>([])
 const pendingUploads = useUploads()
@@ -27,6 +26,7 @@ function toEntry(item: WmsInboundRecordResponse): InboundEntry {
   return {
     ...blankEntry('inbound'), recordId: item.id, id: item.flowNo || String(item.id ?? ''),
     alreadyReversedQty: item.alreadyReversedQty ?? 0,
+    relatedReversalFlows: item.relatedReversalFlows,
     status: item.status === 1 ? 'completed' : 'draft', skuId: item.skuId || '', quantity: item.qty ?? null,
     logistics: ({ 1: 'express', 2: 'instant', 3: 'other' } as Record<number, string>)[item.logisticsType ?? 1] || '',
     company: item.expressCompany || '', tracking: item.expressNo || '', remark: item.remark || '',
@@ -54,7 +54,9 @@ async function loadSkus(keyword = '') {
 }
 function searchSkus(keyword: string) { loadSkus(keyword).catch(report) }
 const loading = ref(false)
+let listLoadVersion = 0
 async function loadData() {
+  const version = ++listLoadVersion
   loading.value = true
   try {
     const filter = applied.value
@@ -74,10 +76,12 @@ async function loadData() {
       startTime: dates[0] ? `${dates[0]} 00:00:00` : undefined,
       endTime: dates[1] ? `${dates[1]} 23:59:59` : undefined,
     })
+    if (version !== listLoadVersion) return
     inboundRows.value = (response.rows as WmsInboundRecordResponse[]).map(toEntry)
     total.value = response.total
   }
-  finally { loading.value = false }
+  catch (error) { if (version === listLoadVersion) throw error }
+  finally { if (version === listLoadVersion) loading.value = false }
 }
 const route = useRoute()
 const filters = reactive({ period: '', keyword: '', status: '', logistics: '', dates: [] as string[] })
@@ -263,7 +267,7 @@ async function submitReverse() {
     <el-dialog :model-value="!!detail" class="inbound-dialog" modal-class="inbound-overlay" width="760px" align-center @close="detail = undefined">
       <template #header="{ titleId, titleClass }"><h2 :id="titleId" :class="titleClass">流水详情 · {{ detail?.id }}</h2><p v-if="detail" class="inbound-subtitle">已完成 · 原流水状态:{{ reversalStatus(detail) }} · 创建时间:{{ dateTime(detail.createdAt) }}</p></template>
       <template v-if="detail">
-        <EntryAudit :entry="detail" :reversed-qty="detail.alreadyReversedQty" />
+        <!-- <EntryAudit :entry="detail" :reversed-qty="detail.alreadyReversedQty" /> -->
         <div class="inbound-grid inbound-detail-fields">
           <div><label>skuID / 商品</label><div class="inbound-readonly">{{ detail.skuId }} · {{ entrySku(detail)?.name }}</div></div>
           <div><label>库存变动</label><div class="inbound-readonly">{{ detail.direction === 'inbound' ? '入库 +' : '出库 −' }}{{ detail.quantity }}</div></div>
@@ -275,6 +279,12 @@ async function submitReverse() {
           <div v-for="(file, index) in attachments(detail)" :key="file.id" class="inbound-evidence-item"><el-image :src="file.url" :preview-src-list="attachments(detail).map(item => item.url)" :initial-index="index" preview-teleported fit="cover" /></div>
           <span v-if="detail.tracking">{{ detail.company }} · 快递单号 {{ detail.tracking }}</span>
         </div>
+        <div v-if="detail.relatedReversalFlows?.length">
+          <div class="inbound-evidence-label">关联冲销流水</div>
+          <div class="inbound-readonly">
+            <div v-for="(flow, index) in detail.relatedReversalFlows" :key="index" class="inbound-related-reversal-item">{{ flow }}</div>
+          </div>
+        </div>
       </template>
       <template #footer><el-button @click="detail = undefined">关闭</el-button><el-button v-if="detail && canReverse(detail)" type="warning" plain @click="openReverse(detail)">冲销 / 更正</el-button></template>
     </el-dialog>
@@ -332,6 +342,7 @@ async function submitReverse() {
 .inbound-evidence-item { display: flex; align-items: center; gap: 8px; max-width: 100%; }
 .inbound-evidence-item .el-image { width: 46px; height: 42px; border-radius: 4px; flex-shrink: 0; cursor: zoom-in; }
 .inbound-related, .inbound-reverse-form { margin-top: 16px; }
+.inbound-related-reversal-item { min-height: 28px; padding: 4px 0; box-sizing: border-box; }
 .inbound-reverse-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
 .inbound-stock-note { margin: -2px 0 14px; color: #7b8d91; font-size: 12px; }
 @media (max-width: 1000px) { .inbound-filters { grid-template-columns: repeat(2, minmax(0, 1fr)); } }

+ 17 - 20
src/views/Ledger.vue

@@ -7,10 +7,10 @@ import { api } from '../api'
 import { blankEntry } from '../utils/entry'
 import { useUploads } from '../utils/uploads'
 import type { Attachment, Entry } from '../api/indexType'
-import type { WmsLedgerAdminSummaryResponse, WmsLedgerFlowVO, WmsReversalPreCheckResponse, WmsReversalRecordResponse } from '../api/indexType'
+import type { WmsLedgerAdminSummaryResponse, WmsLedgerFlowVO, WmsReversalPreCheckResponse } from '../api/indexType'
 import { dateTime, fieldErrors, report, reversalLimit, refreshAfterWrite } from '../utils/ui'
 
-type LedgerEntry = Entry & { recordId?: number; flowType?: number; alreadyReversedQty: number; reversalState?: number; originFlowDetail?: string }
+type LedgerEntry = Entry & { recordId?: number; flowType?: number; alreadyReversedQty: number; reversalState?: number; originFlowDetail?: string; relatedReversalFlows?: string[] }
 type LedgerDetail = WmsLedgerFlowVO
 const entries = ref<LedgerEntry[]>([])
 const pendingUploads = useUploads()
@@ -34,6 +34,7 @@ function toEntry(item: LedgerDetail): LedgerEntry {
     alreadyReversedQty: item.alreadyReversedQty ?? 0, reversalState: item.reversalStatus,
     originalId: item.originFlowNo || '', createdAt: item.createTime || '', operator: item.updateUser || item.createUser || '',
     originFlowDetail: item.originFlowDetail,
+    relatedReversalFlows: item.relatedReversalFlows,
     remark: item.reversalReason || item.remark || '', orderType: item.orderType === 1 ? 'internal' : item.orderType === 2 ? 'external' : '',
     so: item.soNo || '', company: item.expressCompany || '', tracking: item.expressNo || '',
     evidence: (item.attachments || []).map((file, index) => ({ id: String(file.id ?? index), url: file.fileUrl || '', name: file.fileName || '凭证' })),
@@ -42,7 +43,9 @@ function toEntry(item: LedgerDetail): LedgerEntry {
   }
 }
 const loading = ref(false)
+let listLoadVersion = 0
 async function loadData() {
+  const version = ++listLoadVersion
   loading.value = true
   try {
     const filter = applied.value
@@ -54,10 +57,12 @@ async function loadData() {
       flowType: filter.direction ? Number(filter.direction) : undefined,
       flowSource: filter.source || undefined, reversalStatus: reversalStates[filter.reversal],
     })
+    if (version !== listLoadVersion) return
     entries.value = (result.rows as WmsLedgerFlowVO[]).map(toEntry)
     total.value = result.total
   }
-  finally { loading.value = false }
+  catch (error) { if (version === listLoadVersion) throw error }
+  finally { if (version === listLoadVersion) loading.value = false }
 }
 const filters = reactive({ keyword: '', direction: '', reversal: '', source: '' })
 const applied = ref({ ...filters })
@@ -94,22 +99,6 @@ function filterRows() { applied.value = { ...filters }; page.value = 1; loadData
 async function showDetail(entry: LedgerEntry) {
   try {
     if (entry.recordId == null || entry.flowType == null) throw new Error('接口未返回流水类型或记录 ID')
-    if (entry.flowType === 3 || entry.source === 'reversal') {
-      const result = await api.wmsApi.reversal.detail({ id: entry.recordId }) as WmsReversalRecordResponse
-      if (!result || typeof result !== 'object') throw new Error('冲销详情格式不符合预期')
-      const direction = result.reversalDirection === 1 ? 'inbound' : result.reversalDirection === 2 ? 'outbound'
-        : result.originFlowType === 1 ? 'outbound' : result.originFlowType === 2 ? 'inbound' : entry.direction
-      detail.value = {
-        ...toEntry({
-          ...result, flowNo: result.flowNo || entry.id, recordId: result.id ?? entry.recordId,
-          flowType: 3, flowSource: 'REVERSAL', status: result.status ?? 1,
-          skuName: result.skuNameSnapshot ?? entry.snapshot?.name,
-          spec: result.specSnapshot ?? entry.snapshot?.spec,
-          configDetail: result.configDetailSnapshot ?? entry.snapshot?.config,
-        }), direction,
-      }
-      return
-    }
     const result = await api.wmsApi.ledger.flowDetail({ flowNo: entry.id, flowType: entry.flowType, recordId: entry.recordId })
     if (!result || typeof result !== 'object') throw new Error('流水详情格式不符合预期')
     detail.value = toEntry({ flowNo: entry.id, recordId: entry.recordId, flowType: entry.flowType, status: 1, ...result } as LedgerDetail)
@@ -148,7 +137,7 @@ async function submitReverse() {
     <el-config-provider :empty-values="[null, undefined]">
       <form class="ledger-filters" @submit.prevent="filterRows">
         <el-input v-model="filters.keyword" placeholder="skuID / 商品 / 订单号" aria-label="skuID / 商品名称 / 订单号" clearable />
-        <el-select v-model="filters.direction" aria-label="流水类型"><el-option label="流水类型:全部" value="" /><el-option label="入库" value="1" /><el-option label="出库" value="2" /><el-option label="冲销" value="3" /></el-select>
+        <el-select v-model="filters.direction" aria-label="出入库类型"><el-option label="出入库类型:全部" value="" /><el-option label="入库" value="1" /><el-option label="出库" value="2" /></el-select>
         <el-select model-value="completed" aria-label="单据状态"><el-option label="单据状态:已完成" value="completed" /></el-select>
         <el-select v-model="filters.reversal" aria-label="原流水状态"><el-option label="原流水状态:全部" value="" /><el-option v-for="value in ['未冲销', '部分冲销', '已全额冲销']" :key="value" :value="value" :label="value" /></el-select>
         <el-select v-model="filters.source" aria-label="流水来源"><el-option label="流水来源:全部" value="" /><el-option label="正常录入" value="NORMAL" /><el-option label="冲销" value="REVERSAL" /></el-select>
@@ -190,6 +179,12 @@ async function submitReverse() {
           <div class="ledger-evidence-label">关联原流水</div>
           <div class="ledger-readonly">{{ detail.originFlowDetail }}</div>
         </div>
+        <div v-if="detail.source === 'normal' && detail.relatedReversalFlows?.length" class="ledger-related-reversals">
+          <div class="ledger-evidence-label">关联冲销流水</div>
+          <div class="ledger-related-reversal-list">
+            <div v-for="flow in detail.relatedReversalFlows" :key="flow" class="ledger-related-reversal-item">{{ flow }}</div>
+          </div>
+        </div>
       </template>
       <template #footer><el-button @click="detail = undefined">关闭</el-button><el-button v-if="detail && canReverse(detail)" type="warning" plain @click="openReverse(detail)">冲销 / 更正</el-button></template>
     </el-dialog>
@@ -254,6 +249,8 @@ async function submitReverse() {
 .ledger-evidence-item { display: flex; align-items: center; gap: 8px; max-width: 100%; }
 .ledger-evidence-item .el-image { width: 44px; height: 40px; border-radius: 4px; flex-shrink: 0; cursor: zoom-in; }
 .ledger-related { margin-top: 16px; overflow-wrap: anywhere; }
+.ledger-related-reversal-list { padding: 4px 10px; border: 1px solid #dce5e1; border-radius: 7px; background: #f2f6f4; }
+.ledger-related-reversal-item { min-height: 28px; padding: 4px 0; box-sizing: border-box; color: #80969d; font-size: 14px; line-height: 20px; overflow-wrap: anywhere; }
 .ledger-stock-note { font-size: 12px; color: #7b8d91; margin: 0 0 14px; }
 @media (max-width: 1100px) { .ledger-filters > * { flex-basis: calc(33.333% - 8px); } .ledger-filters > :nth-child(8) { flex-grow: 1; } }
 @media (max-width: 600px) { .ledger-heading { align-items: flex-start; gap: 12px; } .ledger-heading h1 { font-size: 22px; } .ledger-stats { grid-template-columns: minmax(0, 1fr); } .ledger-filters > * { flex-basis: calc(50% - 8px); } .ledger-filters > .ledger-search { flex-basis: 100%; } .ledger-grid { grid-template-columns: minmax(0, 1fr); } }

+ 12 - 6
src/views/Master.vue

@@ -15,24 +15,30 @@ const pendingLoads = ref<Record<MasterKind, number>>({ category: 0, sport: 0, br
 const pages = ref<Record<MasterKind, number>>({ category: 1, sport: 1, brand: 1 })
 const totals = ref<Record<MasterKind, number>>({ category: 0, sport: 0, brand: 0 })
 const pageSize = 10
+const listLoadVersions: Record<MasterKind, number> = { category: 0, sport: 0, brand: 0 }
 async function loadData(masterKind: MasterKind = kind.value) {
-  pendingLoads.value[masterKind] += 1
+  const version = ++listLoadVersions[masterKind]
+  pendingLoads.value[masterKind] = 1
   try {
+    while (true) {
       const response = await api.wmsApi.basicData.list({ dataType: dataTypes[masterKind], pageNo: pages.value[masterKind], pageSize })
+      if (version !== listLoadVersions[masterKind]) return
       const records = response.rows
       totals.value[masterKind] = response.total
       const lastPage = Math.max(1, Math.ceil(response.total / pageSize))
       if (!records.length && pages.value[masterKind] > lastPage) {
         pages.value[masterKind] = lastPage
-        await loadData(masterKind)
-        return
+        continue
       }
       const loadedMasters = records.map(item => {
         return { id: String(item.id ?? ''), kind: masterKind, name: item.dataName || '', enabled: item.status === 1, order: item.sortOrder ?? 99, operator: item.updateUser || item.createUser || '', card: masterKind === 'category' && item.dataName === '球星卡', skuCount: item.skuCount ?? 0, updatedAt: item.updateTime || item.createTime || '' }
       })
       masters.value = [...masters.value.filter(item => item.kind !== masterKind), ...loadedMasters]
+      return
+    }
   }
-  finally { pendingLoads.value[masterKind] -= 1 }
+  catch (error) { if (version === listLoadVersions[masterKind]) throw error }
+  finally { if (version === listLoadVersions[masterKind]) pendingLoads.value[masterKind] = 0 }
 }
 const router = useRouter()
 const kind = ref<MasterKind>('category')
@@ -126,7 +132,7 @@ async function remove(item: Master) {
       <el-table-column label="已关联 SKU">
         <template #default="{ row }"><button v-if="row.skuCount" type="button" class="master-count master-count-link" :aria-label="`查看 ${row.name} 已关联的 ${row.skuCount} 个 SKU`" @click="viewLinkedSkus(row)">{{ row.skuCount }} 个</button><span v-else class="master-count master-count-empty">0 个</span></template>
       </el-table-column>
-      <el-table-column label="最近操作人">
+      <el-table-column label="最近操作人" min-width="120">
         <template #default="{ row }">
           <span v-if="row.operator">{{ row.operator }}<span v-if="relativeDay(row.updatedAt)"> · {{ relativeDay(row.updatedAt) }}</span></span>
           <span v-else>—</span>
@@ -178,7 +184,7 @@ async function remove(item: Master) {
 .master-table :deep(.cell) { padding-left: 20px; padding-right: 20px; }
 .master-name { font-weight: 600; color: #30483c; }
 .master-note { font-size: 11px; color: #96a09b; margin-left: 6px; }
-.master-status { display: inline-flex; align-items: center; gap: 5px; padding: 3px 9px; border-radius: 20px; background: #eaf5ed; color: #3d8059; font-size: 11px; line-height: 18px; }
+.master-status { display: inline-flex; align-items: center; gap: 5px; padding: 3px 9px; border-radius: 20px; background: #eaf5ed; color: #3d8059; font-size: 11px; line-height: 18px; white-space: nowrap; }
 .master-status i { width: 5px; height: 5px; border-radius: 50%; background: currentColor; }
 .master-status.disabled { background: #f0f2f1; color: #8b9590; }
 .master-count { color: #44845e; }

+ 18 - 7
src/views/Outbound.vue

@@ -5,7 +5,6 @@ import { Plus, Tickets } from '@element-plus/icons-vue'
 import { ElMessage, ElMessageBox } from 'element-plus'
 import type { FormInstance, FormRules } from 'element-plus'
 import EvidenceUpload from '../components/EvidenceUpload.vue'
-import EntryAudit from '../components/EntryAudit.vue'
 import { api } from '../api'
 import { blankEntry } from '../utils/entry'
 import { useUploads } from '../utils/uploads'
@@ -13,7 +12,7 @@ import type { Attachment, Entry } from '../api/indexType'
 import type { WmsOutboundRecordResponse, WmsOutboundSaveRequest, WmsSkuResponse, WmsReversalPreCheckResponse } from '../api/indexType'
 import { dateTime, fieldErrors, report, reversalLimit, refreshAfterWrite } from '../utils/ui'
 
-type OutboundEntry = Entry & { recordId?: number; alreadyReversedQty: number }
+type OutboundEntry = Entry & { recordId?: number; alreadyReversedQty: number; relatedReversalFlows?: WmsOutboundRecordResponse['relatedReversalFlows'] }
 type OutboundDetail = WmsOutboundRecordResponse
 const outboundRows = ref<OutboundEntry[]>([])
 const total = ref(0)
@@ -29,6 +28,7 @@ function toEntry(item: OutboundDetail): OutboundEntry {
   return {
     ...blankEntry('outbound'), recordId: item.id, id: item.flowNo || String(item.id ?? ''),
     alreadyReversedQty: item.alreadyReversedQty ?? 0, status: item.status === 1 ? 'completed' : 'draft',
+    relatedReversalFlows: item.relatedReversalFlows,
     skuId: item.skuId || '', quantity: item.qty ?? null, orderType: item.orderType === 1 ? 'internal' : item.orderType === 2 ? 'external' : '',
     so: item.soNo || '', logistics: Object.keys(logisticsTypes).find(key => logisticsTypes[key] === item.logisticsType) || '',
     company: item.expressCompany || '', tracking: item.expressNo || '', remark: item.remark || '',
@@ -56,7 +56,9 @@ async function loadSkus(keyword = '') {
 }
 function searchSkus(keyword: string) { loadSkus(keyword).catch(report) }
 const loading = ref(false)
+let listLoadVersion = 0
 async function loadData() {
+  const version = ++listLoadVersion
   loading.value = true
   try {
     const filter = applied.value
@@ -76,10 +78,12 @@ async function loadData() {
       startTime: dates[0] ? `${dates[0]} 00:00:00` : undefined,
       endTime: dates[1] ? `${dates[1]} 23:59:59` : undefined,
     })
+    if (version !== listLoadVersion) return
     outboundRows.value = (result.rows as WmsOutboundRecordResponse[]).map(toEntry)
     total.value = result.total
   }
-  finally { loading.value = false }
+  catch (error) { if (version === listLoadVersion) throw error }
+  finally { if (version === listLoadVersion) loading.value = false }
 }
 const route = useRoute()
 const filters = reactive({ period: '', keyword: '', status: '', orderType: '', dates: [] as string[] })
@@ -232,7 +236,7 @@ async function submitReverse() {
       <el-table-column label="SO 编号" width="164"><template #default="{ row }">{{ row.so || (row.orderType === 'external' ? '—' : '待填写') }}</template></el-table-column>
       <el-table-column label="物流信息" min-width="210"><template #default="{ row }">{{ logisticsText(row) }}</template></el-table-column>
       <el-table-column prop="operator" label="操作人" width="100"><template #default="{ row }">{{ row.operator || '—' }}</template></el-table-column>
-      <el-table-column prop="remark" label="备注" min-width="150" show-overflow-tooltip><template #default="{ row }">{{ row.remark || '—' }}</template></el-table-column>
+      <!-- <el-table-column prop="remark" label="备注" min-width="150" show-overflow-tooltip><template #default="{ row }">{{ row.remark || '—' }}</template></el-table-column> -->
       <el-table-column label="操作" width="190" fixed="right">
 		  <template #default="{ row }">
 			<template v-if="row.status === 'draft'">
@@ -273,7 +277,7 @@ async function submitReverse() {
           <div v-if="form.logistics === 'express'" class="outbound-grid"><el-form-item label="快递公司" prop="company" required :error="errors.company"><el-input v-model="form.company" placeholder="例如:顺丰速运" /></el-form-item><el-form-item label="快递单号" prop="tracking" required :error="errors.tracking"><el-input v-model="form.tracking" placeholder="填写快递单号" /></el-form-item></div>
           <el-form-item v-else label="物流截图" prop="screenshots" required :error="errors.screenshots"><EvidenceUpload :key="form.logistics" :active="dialog" :disabled="busy" v-model="form.screenshots" @update:model-value="validateAttachment('screenshots')" /></el-form-item>
         </template>
-        <el-form-item label="备注"><el-input v-model="form.remark" type="textarea" :rows="2" /></el-form-item>
+        <!-- <el-form-item label="备注"><el-input v-model="form.remark" type="textarea" :rows="2" /></el-form-item> -->
       </el-form>
       <template #footer><el-button :disabled="busy" @click="dialog = false">取消</el-button><el-button type="warning" plain :disabled="busy || pendingUploads > 0" @click="save(false)">保存草稿</el-button><el-button type="primary" :loading="busy" :disabled="pendingUploads > 0" @click="save(true)">提交出库</el-button></template>
     </el-dialog>
@@ -281,7 +285,7 @@ async function submitReverse() {
     <el-dialog :model-value="!!detail" class="outbound-dialog" modal-class="outbound-overlay" width="760px" align-center @close="detail = undefined">
       <template #header="{ titleId, titleClass }"><h2 :id="titleId" :class="titleClass">流水详情 · {{ detail?.id }}</h2><p v-if="detail" class="outbound-subtitle">已完成 · 原流水状态:{{ reversalStatus(detail) }} · 创建时间:{{ dateTime(detail.createdAt) }}</p></template>
       <template v-if="detail">
-        <EntryAudit :entry="detail" :reversed-qty="detail.alreadyReversedQty" />
+        <!-- <EntryAudit :entry="detail" :reversed-qty="detail.alreadyReversedQty" /> -->
         <div class="outbound-grid outbound-detail-fields">
           <div><label>skuID / 商品</label><div class="outbound-readonly">{{ detail.skuId }} · {{ entrySku(detail)?.name }}</div></div>
           <div><label>库存变动</label><div class="outbound-readonly">{{ detail.direction === 'outbound' ? '出库 −' : '入库 +' }}{{ detail.quantity }}</div></div>
@@ -297,7 +301,13 @@ async function submitReverse() {
             <span v-if="detail.source === 'normal'">物流信息:{{ logisticsText(detail) }}</span>
           </div>
         </template>
-        <div v-if="detail.remark" class="outbound-related">备注 / 冲销原因:{{ detail.remark }}</div>
+        <!-- <div v-if="detail.remark" class="outbound-related">备注 / 冲销原因:{{ detail.remark }}</div> -->
+        <div v-if="detail.relatedReversalFlows?.length">
+          <div class="outbound-evidence-label">关联冲销流水</div>
+          <div class="outbound-readonly">
+            <div v-for="(flow, index) in detail.relatedReversalFlows" :key="index" class="outbound-related-reversal-item">{{ flow }}</div>
+          </div>
+        </div>
       </template>
       <template #footer><el-button @click="detail = undefined">关闭</el-button><el-button v-if="detail && canReverse(detail)" type="warning" plain @click="openReverse(detail)">冲销 / 更正</el-button></template>
     </el-dialog>
@@ -354,6 +364,7 @@ async function submitReverse() {
 .outbound-evidence-item { display: flex; align-items: center; gap: 8px; max-width: 100%; }
 .outbound-evidence-item .el-image { width: 46px; height: 42px; border-radius: 4px; flex-shrink: 0; cursor: zoom-in; }
 .outbound-related, .outbound-reverse-form { margin-top: 16px; overflow-wrap: anywhere; }
+.outbound-related-reversal-item { min-height: 28px; padding: 4px 0; box-sizing: border-box; }
 .outbound-reverse-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
 @media (max-width: 1000px) { .outbound-filters { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
 @media (max-width: 600px) { .outbound-heading { flex-direction: column; align-items: flex-start; gap: 14px; } .outbound-grid { grid-template-columns: minmax(0, 1fr); } .outbound-heading h1 { font-size: 22px; } }

+ 6 - 2
src/views/Sku.vue

@@ -30,7 +30,9 @@ function recordId(item: SkuRow) {
   if (item.recordId == null) throw new Error('接口未返回 SKU 记录 ID')
   return item.recordId
 }
+let listLoadVersion = 0
 async function loadData() {
+  const version = ++listLoadVersion
   loading.value = true
   try {
     const filter = applied.value
@@ -41,10 +43,12 @@ async function loadData() {
         sportTypeId: filter.sport ? Number(filter.sport) : undefined,
         brandId: filter.brand ? Number(filter.brand) : undefined,
       })
-      skuRows.value = (response.rows as WmsSkuResponse[]).map(toSku)
+        if (version !== listLoadVersion) return
+        skuRows.value = (response.rows as WmsSkuResponse[]).map(toSku)
       total.value = response.total
   }
-  finally { loading.value = false }
+      catch (error) { if (version === listLoadVersion) throw error }
+      finally { if (version === listLoadVersion) loading.value = false }
 }
 async function loadMasters() {
   const types: Record<MasterKind, number> = { category: 1, sport: 2, brand: 3 }