Files
weihai_erp/src/views/base/WhSrfRdcPane.vue
T
xiaojibeier 31ed31da92 20260708001
2026-07-08 16:47:32 +08:00

1193 lines
35 KiB
Vue
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="wh-srf-rdc">
<div class="wh-srf-rdc-toolbar">
<span class="wh-srf-rdc-title">已关联物料</span>
<el-input
v-model="searchKeyword"
clearable
placeholder="搜索名称或编码"
class="wh-srf-search"
size="small"
/>
<div class="wh-srf-display">
<span class="wh-srf-display-label">展示</span>
<el-radio-group v-model="displayMode" size="small" class="wh-srf-display-group">
<el-radio-button value="tree">树形</el-radio-button>
<el-radio-button value="flat">平铺</el-radio-button>
</el-radio-group>
</div>
<el-button
v-if="canUpdate && goodsRows.length"
type="primary"
plain
size="small"
class="wh-srf-rdc-batch-btn"
:disabled="!effectiveSelectedRows.length || unlinking"
@click="batchRegionVisible = true"
>
批量设置售卖区域
</el-button>
<el-button
v-if="canUpdate && goodsRows.length"
type="primary"
plain
size="small"
class="wh-srf-rdc-batch-btn"
:disabled="!effectiveSelectedRows.length || unlinking"
@click="openBatchDeployDlg"
>
批量关联源采购仓
</el-button>
<el-button
v-if="canUpdate && goodsRows.length"
type="danger"
plain
size="small"
class="wh-srf-rdc-batch-btn"
:disabled="!effectiveSelectedRows.length || unlinking"
:loading="unlinking"
@click="unlinkRows(effectiveSelectedRows)"
>
批量移出 RDC
</el-button>
<span v-if="effectiveSelectedRows.length" class="wh-srf-rdc-sel-hint">
已选 {{ effectiveSelectedRows.length }} 条
</span>
<span v-if="warehouseCode && (goodsTotal > 0 || goodsRows.length)" class="wh-srf-rdc-meta">
{{ listCountLabel }}
</span>
</div>
<div
v-loading="goodsLoading || linking || (isTreeMode && categoryLoading)"
class="wh-srf-rdc-body"
:class="{ 'is-tree-mode': isTreeMode }"
:element-loading-text="linking ? '关联中' : 'AI Thinking · RDC 物料'"
>
<template v-if="warehouseCode">
<div v-if="isTreeMode && filteredGoodsRows.length" class="wh-srf-tree-wrap wh-srf-tree-wrap--rdc">
<div class="wh-srf-tree-head">
<span class="wh-srf-tree-col wh-srf-tree-col--name wh-srf-tree-head-name">物料名称</span>
<div
class="wh-srf-tree-fixed-cols wh-srf-tree-fixed-cols--rdc"
:class="{ 'has-actions': canUpdate }"
>
<span class="wh-srf-tree-col wh-srf-tree-col--lot">管理精度</span>
<span v-if="canUpdate" class="wh-srf-tree-col wh-srf-tree-col--act">操作</span>
</div>
</div>
<el-tree
ref="goodsTreeRef"
class="wh-srf-goods-tree wh-srf-goods-tree--grid wh-srf-goods-tree--rdc"
:data="treeCheckData"
node-key="key"
:indent="0"
show-checkbox
check-strictly
default-expand-all
expand-on-click-node
:check-on-click-node="false"
:check-on-click-leaf="false"
:props="{ label: 'label', children: 'children', disabled: 'disabled' }"
@check="onGoodsTreeCheck"
>
<template #default="{ node, data }">
<div
class="wh-srf-tree-row"
:class="{
'is-cat': data.nodeType === 'cat',
'is-goods': data.nodeType === 'goods',
'is-active': data.nodeType === 'goods' && isActiveGoodsRow(data.goodsRow),
}"
@click="data.nodeType === 'goods' && selectGoodsRow(data.goodsRow)"
>
<span
class="wh-srf-tree-col wh-srf-tree-col--name"
:style="treeNameIndentStyle(node)"
>
<span
class="wh-srf-tree-name-text"
:title="data.nodeType === 'goods' ? goodsCategoryHoverTitle(data.goodsRow) : data.label"
>{{ data.label }}</span>
<el-tag
v-if="data.nodeType === 'cat' && data.isFinal"
size="small"
type="success"
effect="plain"
class="wh-srf-goods-final-tag"
>末端</el-tag>
<WhGoodsRelatedPopover
v-if="data.nodeType === 'goods'"
:goods-code="data.goodsRow?.goods_code"
/>
</span>
<div
v-if="data.nodeType === 'goods'"
class="wh-srf-tree-fixed-cols wh-srf-tree-fixed-cols--rdc"
:class="{ 'has-actions': canUpdate }"
>
<span class="wh-srf-tree-col wh-srf-tree-col--lot">
<WhSrfGoodsLotTypeCell
class="wh-srf-lot-select--rdc"
:model-value="data.goodsRow.log_type"
:editable="canUpdate"
:disabled="unlinking"
:loading="isRowLotTypeSaving(data.goodsRow)"
:has-code="rowLotTypeHasCode(data.goodsRow)"
@change="(v) => saveRowLotType(data.goodsRow, v)"
/>
</span>
<span v-if="canUpdate" class="wh-srf-tree-col wh-srf-tree-col--act">
<el-button
link
type="danger"
size="small"
:disabled="unlinking"
@click.stop="unlinkRows([data.goodsRow])"
>
移除
</el-button>
</span>
</div>
</div>
</template>
</el-tree>
</div>
<!-- 平铺:与树形同一套行 grid(1fr 名称 + 右侧固定列),不再用 el-table 分列 -->
<div
v-else-if="!isTreeMode && filteredGoodsRows.length"
class="wh-srf-tree-wrap wh-srf-tree-wrap--rdc wh-srf-flat-wrap"
>
<div class="wh-srf-flat-head-row">
<el-checkbox
v-if="canUpdate"
class="wh-srf-flat-check-slot"
:model-value="flatPageAllSelected"
:indeterminate="flatPageIndeterminate"
:disabled="unlinking"
@click.stop
@change="toggleFlatPageSelection"
/>
<div class="wh-srf-tree-head wh-srf-flat-head-grid">
<span class="wh-srf-tree-col wh-srf-tree-col--name wh-srf-tree-head-name wh-srf-flat-head-name">物料名称</span>
<div
class="wh-srf-tree-fixed-cols wh-srf-tree-fixed-cols--rdc"
:class="{ 'has-actions': canUpdate }"
>
<span class="wh-srf-tree-col wh-srf-tree-col--lot">管理精度</span>
<span v-if="canUpdate" class="wh-srf-tree-col wh-srf-tree-col--act">操作</span>
</div>
</div>
</div>
<div class="wh-srf-flat-list">
<div
v-for="row in filteredGoodsRows"
:key="row.rowKey || row.linkKey"
class="wh-srf-flat-row"
:class="{ 'is-active': isActiveGoodsRow(row) }"
@click="selectGoodsRow(row)"
>
<el-checkbox
v-if="canUpdate"
class="wh-srf-flat-check-slot"
:model-value="isFlatRowSelected(row)"
:disabled="unlinking"
@click.stop
@change="(checked) => toggleFlatRowSelection(row, checked)"
/>
<div class="wh-srf-tree-row is-goods">
<span class="wh-srf-tree-col wh-srf-tree-col--name">
<span
class="wh-srf-tree-name-text"
:title="goodsCategoryHoverTitle(row)"
>{{ row.name || row.goods_name || row.goods_code || '—' }}</span>
<WhGoodsRelatedPopover :goods-code="row.goods_code" />
</span>
<div
class="wh-srf-tree-fixed-cols wh-srf-tree-fixed-cols--rdc"
:class="{ 'has-actions': canUpdate }"
>
<span class="wh-srf-tree-col wh-srf-tree-col--lot">
<WhSrfGoodsLotTypeCell
class="wh-srf-lot-select--rdc"
:model-value="row.log_type"
:editable="canUpdate"
:disabled="unlinking"
:loading="isRowLotTypeSaving(row)"
:has-code="rowLotTypeHasCode(row)"
@change="(v) => saveRowLotType(row, v)"
/>
</span>
<span v-if="canUpdate" class="wh-srf-tree-col wh-srf-tree-col--act">
<el-button
link
type="danger"
size="small"
:disabled="unlinking"
@click.stop="unlinkRows([row])"
>
移除
</el-button>
</span>
</div>
</div>
</div>
</div>
</div>
<div v-else-if="!goodsLoading" class="wh-srf-rdc-empty">
<p>{{ emptyListTitle }}</p>
<p v-if="emptyListSub" class="wh-srf-rdc-empty-sub">{{ emptyListSub }}</p>
</div>
</template>
<div v-else class="wh-srf-rdc-empty">
<p>请先选择仓库</p>
</div>
</div>
<footer v-if="!regionSplitLayout && warehouseCode && goodsTotal > 0 && !isTreeMode" class="wh-srf-rdc-ft">
<el-pagination
:current-page="pageCurrent"
:page-size="pageSize"
:total="goodsTotal"
:page-sizes="[20, 50, 100]"
layout="total, sizes, prev, pager, next"
size="small"
background
:disabled="goodsLoading"
@current-change="onPageCurrentChange"
@size-change="onPageSizeChange"
/>
</footer>
<WhRdcRegionPickDialog
v-model="batchRegionVisible"
:goods-rows="effectiveSelectedRows"
:warehouse-code="warehouseCode"
:can-update="canUpdate"
/>
<WhRdcDeployLinkDialog
ref="batchDeployDlgRef"
v-model="batchDeployVisible"
:link-deploy-anchors="effectiveSelectedRows"
:warehouse-code="warehouseCode"
:can-update="canUpdate"
/>
</div>
</template>
<script setup>
import { computed, ref, watch } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import WhGoodsRelatedPopover from './WhGoodsRelatedPopover.vue'
import {
warehouseRdcBatchLink,
warehouseRdcUnlinkGoods,
fetchWarehouseRdcGoodsPage,
fetchAllWarehouseRdcGoods,
pickRdcCodeFromGoodsRow,
} from '@/api/warehouseRdc'
import { pickLogTypeFromRow, canSetWarehouseGoodsLotType } from '@/constants/whLotType'
import { normalizeRdcGoodsRows, warehouseGoodsLinkKey, assignWarehouseGoodsRowKeys, rowSelectKey } from '@/utils/rdcGoodsLink'
import { useWhSrfGoodsDisplay } from '@/composables/useWhSrfGoodsDisplay'
import { useWhSrfGoodsLotTypeSave } from '@/composables/useWhSrfGoodsLotTypeSave'
import WhSrfGoodsLotTypeCell from './WhSrfGoodsLotTypeCell.vue'
import WhRdcRegionPickDialog from './WhRdcRegionPickDialog.vue'
import WhRdcDeployLinkDialog from './WhRdcDeployLinkDialog.vue'
import {
getWhSrfCheckedGoodsNodes,
handleWhSrfGoodsTreeCheck,
} from '@/utils/whSrfGoodsTreeCheck'
import { formatGoodsCategoryPath, loadCategoryPathLabelMap } from '@/utils/rfSceneGoodsFilter'
import { filterWhSrfGoodsByKeyword } from '@/utils/whSrfGoodsSearch'
/** el-tree indent=0 时,名称列每层缩进像素(与 Element 默认 indent 一致) */
const WH_SRF_TREE_INDENT = 18
const props = defineProps({
warehouseCode: { type: String, default: '' },
/** RDC 头部选中的场景;空表示不加载列表,需先点选场景 */
sceneCode: { type: String, default: '' },
canUpdate: { type: Boolean, default: false },
/** 与右侧可售卖区域并排时,分页底栏由外层 split 统一渲染 */
regionSplitLayout: { type: Boolean, default: false },
})
const emit = defineEmits(['linked-keys-change', 'busy-change', 'active-goods-change', 'split-footer-change'])
const goodsLoading = ref(false)
const linking = ref(false)
const unlinking = ref(false)
const batchRegionVisible = ref(false)
const batchDeployVisible = ref(false)
const batchDeployDlgRef = ref(null)
const goodsTotal = ref(0)
/** @type {import('vue').Ref<object[]>} */
const goodsRows = ref([])
/** @type {import('vue').Ref<object[]>} */
const flatSelectedRows = ref([])
/** @type {import('vue').Ref<object[]>} */
const treeSelectedRows = ref([])
const goodsTreeRef = ref(null)
const pageCurrent = ref(1)
const pageSize = ref(20)
const searchKeyword = ref('')
/** @type {import('vue').Ref<object|null>} */
const activeGoodsRow = ref(null)
/** @type {import('vue').Ref<Map<string, string>>} */
const categoryPathMap = ref(new Map())
const { isRowLotTypeSaving, saveRowLotType } = useWhSrfGoodsLotTypeSave('RDC', () => props.warehouseCode)
function rowLotTypeHasCode(row) {
return canSetWarehouseGoodsLotType(row, props.warehouseCode)
}
const effectiveSelectedRows = computed(() => (
isTreeMode.value ? treeSelectedRows.value : flatSelectedRows.value
))
const activeGoodsKey = computed(() => rowSelectKey(activeGoodsRow.value) || undefined)
function rowToLinkKey(row) {
return warehouseGoodsLinkKey(row?.goods_code)
}
/** 树不缩进整行(indent=0),仅名称列按层级缩进,右栏与表头列对齐 */
function treeNameIndentStyle(node) {
const level = Math.max(1, Number(node?.level) || 1)
const pad = (level - 1) * WH_SRF_TREE_INDENT
if (!pad) return undefined
return { paddingLeft: `${pad}px` }
}
/** 去掉最顶一级分类名(如工业制成品) */
function categoryPathWithoutTopLevel(pathStr) {
const parts = String(pathStr || '')
.split(/\s*\/\s*/)
.map((s) => s.trim())
.filter(Boolean)
if (parts.length <= 1) return parts.join('/') || '—'
return parts.slice(1).join('/')
}
/** 平铺分类列:category/listed 全路径(省略最顶一级),用 / 连接 */
function goodsCategoryPathFlat(row) {
const full = formatGoodsCategoryPath(row, categoryPathMap.value)
if (!full || full === '—') {
const fallback = String(row?.category_name || row?.category_code || '').trim()
return fallback || '—'
}
return categoryPathWithoutTopLevel(full.replace(/ \/ /g, '/'))
}
/** 分类全路径(含最顶一级),悬停物料名称用 */
function goodsCategoryPathFull(row) {
const full = formatGoodsCategoryPath(row, categoryPathMap.value)
if (!full || full === '—') {
const fallback = String(row?.category_name || row?.category_code || '').trim()
return fallback || '—'
}
return full.replace(/ \/ /g, '/')
}
/** 悬停物料名称时展示分类路径 */
function goodsCategoryHoverTitle(row) {
const path = goodsCategoryPathFull(row)
if (!path || path === '—') return ''
return `分类:${path}`
}
const filteredGoodsRows = computed(() => filterWhSrfGoodsByKeyword(
goodsRows.value,
searchKeyword.value,
(row) => goodsCategoryPathFlat(row),
))
const {
displayMode,
isTreeMode,
treeCheckData,
categoryLoading,
} = useWhSrfGoodsDisplay(filteredGoodsRows, { omitTopCategoryLevel: true })
const listCountLabel = computed(() => {
const total = goodsTotal.value
const pageRows = goodsRows.value.length
const shown = filteredGoodsRows.value.length
const q = String(searchKeyword.value || '').trim()
if (q && shown !== pageRows) return `本页 ${shown} / ${pageRows} · 共 ${total} 条`
return `共 ${total} 条`
})
const emptyListTitle = computed(() => {
if (!String(props.sceneCode || '').trim()) return '请先选择上方场景'
if (goodsTotal.value > 0 && String(searchKeyword.value || '').trim()) return '本页无匹配物料'
if (goodsTotal.value > 0) return '暂无关联物料'
return '该场景暂无已关联 RDC 物料'
})
const emptyListSub = computed(() => {
if (!String(props.sceneCode || '').trim()) return '点击场景查看已关联物料,或通过「关联物料」添加'
if (goodsTotal.value > 0 && String(searchKeyword.value || '').trim()) return '请调整搜索词'
if (goodsTotal.value > 0) return ''
return '可点击上方其他场景切换,或通过「关联物料」添加'
})
async function ensureCategoryPathMap() {
if (categoryPathMap.value.size) return
categoryPathMap.value = await loadCategoryPathLabelMap()
}
function emitLinkedKeys(keys) {
emit('linked-keys-change', Array.isArray(keys) ? keys : [])
}
function emitActiveGoods() {
emit('active-goods-change', activeGoodsRow.value)
}
function rowToUnlinkItem(row) {
const goods_code = String(row?.goods_code || row?.linkKey || '').trim()
const warehouse_code = String(
row?.warehouse_code
|| pickRdcCodeFromGoodsRow(row, props.warehouseCode)
|| props.warehouseCode
|| '',
).trim()
return { warehouse_code, goods_code }
}
function buildUnlinkList(rows) {
return (rows || [])
.map(rowToUnlinkItem)
.filter((x) => x.warehouse_code && x.goods_code)
}
function syncTreeSelectedRows() {
treeSelectedRows.value = getWhSrfCheckedGoodsNodes(goodsTreeRef)
.map((n) => n.goodsRow)
.filter(Boolean)
}
function onGoodsTreeCheck(nodeData, payload) {
handleWhSrfGoodsTreeCheck(goodsTreeRef, nodeData, payload, syncTreeSelectedRows)
}
function isFlatRowSelected(row) {
const key = rowSelectKey(row)
if (!key) return false
return flatSelectedRows.value.some((r) => rowSelectKey(r) === key)
}
const flatPageSelectableRows = computed(() => (
filteredGoodsRows.value.filter((row) => rowSelectKey(row))
))
const flatPageAllSelected = computed(() => {
const rows = flatPageSelectableRows.value
if (!rows.length) return false
return rows.every((row) => isFlatRowSelected(row))
})
const flatPageIndeterminate = computed(() => {
const rows = flatPageSelectableRows.value
if (!rows.length) return false
const selectedCount = rows.filter((row) => isFlatRowSelected(row)).length
return selectedCount > 0 && selectedCount < rows.length
})
function toggleFlatPageSelection(checked) {
const rows = flatPageSelectableRows.value
if (!rows.length) return
if (checked) {
const keys = new Set(flatSelectedRows.value.map((r) => rowSelectKey(r)).filter(Boolean))
const merged = [...flatSelectedRows.value]
for (const row of rows) {
const key = rowSelectKey(row)
if (key && !keys.has(key)) {
keys.add(key)
merged.push(row)
}
}
flatSelectedRows.value = merged
return
}
const pageKeys = new Set(rows.map((r) => rowSelectKey(r)).filter(Boolean))
flatSelectedRows.value = flatSelectedRows.value.filter((r) => !pageKeys.has(rowSelectKey(r)))
}
function toggleFlatRowSelection(row, checked) {
const key = rowSelectKey(row)
if (!key) return
if (checked) {
if (!isFlatRowSelected(row)) {
flatSelectedRows.value = [...flatSelectedRows.value, row]
}
return
}
flatSelectedRows.value = flatSelectedRows.value.filter((r) => rowSelectKey(r) !== key)
}
function clearAllSelection() {
flatSelectedRows.value = []
treeSelectedRows.value = []
goodsTreeRef.value?.setCheckedKeys([])
}
function isActiveGoodsRow(row) {
if (!row || !activeGoodsRow.value) return false
return rowSelectKey(row) === rowSelectKey(activeGoodsRow.value)
}
function selectGoodsRow(row) {
if (!row?.goods_code && !row?.linkKey) return
activeGoodsRow.value = row
emitActiveGoods()
}
function syncActiveGoodsAfterLoad() {
const key = activeGoodsKey.value
if (!key) {
emitActiveGoods()
return
}
const found = goodsRows.value.find((row) => rowSelectKey(row) === key)
activeGoodsRow.value = found || null
emitActiveGoods()
}
let loadGoodsSeq = 0
let loadLinkedKeysSeq = 0
function normalizeGoodsRows(rawList) {
const base = normalizeRdcGoodsRows(rawList)
.map((row) => ({
...row,
log_type: pickLogTypeFromRow(row),
}))
return assignWarehouseGoodsRowKeys(base, rowToLinkKey)
}
function applyGoodsPage(list, total) {
goodsRows.value = normalizeGoodsRows(list)
const n = Number(total)
goodsTotal.value = Number.isFinite(n) && n >= 0 ? n : goodsRows.value.length
syncActiveGoodsAfterLoad()
}
function applyGoodsFromRaw(raw) {
if (raw && typeof raw === 'object' && !Array.isArray(raw) && ('list' in raw || 'total' in raw)) {
applyGoodsPage(raw.list, raw.total)
return
}
const rows = normalizeGoodsRows(raw)
goodsRows.value = rows
goodsTotal.value = rows.length
pageCurrent.value = 1
syncActiveGoodsAfterLoad()
}
async function refreshLinkedKeys() {
const wh = String(props.warehouseCode || '').trim()
if (!wh) {
emitLinkedKeys([])
return
}
const seq = ++loadLinkedKeysSeq
try {
const raw = await fetchAllWarehouseRdcGoods({ warehouse_code: wh, scene_code: '' })
if (seq !== loadLinkedKeysSeq) return
const keys = normalizeGoodsRows(raw).map(rowToLinkKey).filter(Boolean)
emitLinkedKeys(keys)
} catch {
if (seq === loadLinkedKeysSeq) emitLinkedKeys([])
}
}
function clearGoodsDisplay() {
goodsRows.value = []
goodsTotal.value = 0
activeGoodsRow.value = null
emitActiveGoods()
clearAllSelection()
}
async function loadGoods() {
const wh = String(props.warehouseCode || '').trim()
const sc = String(props.sceneCode || '').trim()
if (!wh) {
clearGoodsDisplay()
emitLinkedKeys([])
return
}
if (!sc) {
clearGoodsDisplay()
return
}
goodsLoading.value = true
clearAllSelection()
const seq = ++loadGoodsSeq
try {
const [, pageRes] = await Promise.all([
ensureCategoryPathMap(),
fetchWarehouseRdcGoodsPage({
warehouse_code: wh,
scene_code: String(props.sceneCode || '').trim(),
page: pageCurrent.value,
limit: pageSize.value,
}),
])
if (seq !== loadGoodsSeq) return
applyGoodsPage(pageRes.list, pageRes.total)
} catch (e) {
goodsRows.value = []
goodsTotal.value = 0
ElMessage.error('加载 RDC 物料失败:' + (e?.data?.message || e?.message || '未知错误'))
} finally {
if (seq === loadGoodsSeq) goodsLoading.value = false
}
}
function onPageCurrentChange(page) {
pageCurrent.value = Number(page) || 1
void loadGoods()
}
function onPageSizeChange(size) {
pageSize.value = Number(size) || 20
pageCurrent.value = 1
void loadGoods()
}
async function linkGoods(items) {
const wh = String(props.warehouseCode || '').trim()
if (!wh) {
ElMessage.warning('请先选择仓库')
return false
}
const list = (items || [])
.map((item) => ({
goods_code: String(item.goods_code || '').trim(),
}))
.filter((x) => x.goods_code)
if (!list.length) return false
linking.value = true
try {
await warehouseRdcBatchLink({ warehouse_code: wh, list })
ElMessage.success(`已关联 ${list.length} 条物料`)
await Promise.all([loadGoods(), refreshLinkedKeys()])
return true
} catch (e) {
ElMessage.error('关联失败:' + (e?.data?.message || e?.message || '未知错误'))
return false
} finally {
linking.value = false
}
}
async function openBatchDeployDlg() {
const rows = effectiveSelectedRows.value.filter(Boolean)
if (!rows.length) {
ElMessage.warning('请先勾选要关联源采购仓的 RDC 物料')
return
}
const prep = await batchDeployDlgRef.value?.prepareLinkDeploy?.(rows)
if (prep?.handled) return
batchDeployVisible.value = true
}
async function unlinkRows(rows) {
const wh = String(props.warehouseCode || '').trim()
const list = buildUnlinkList(rows)
if (!wh || !list.length) {
ElMessage.warning('无法移出:缺少物料编码')
return
}
try {
await ElMessageBox.confirm(
`确定将 ${list.length} 条物料移出 RDC?移出后物料将回到左侧物料池。`,
'批量移出 RDC',
{ type: 'warning', confirmButtonText: '确定', cancelButtonText: '取消' },
)
} catch {
return
}
unlinking.value = true
try {
await warehouseRdcUnlinkGoods({ warehouse_code: wh, list })
await Promise.all([loadGoods(), refreshLinkedKeys()])
ElMessage.success(`已移出 ${list.length} 条物料`)
} catch (e) {
ElMessage.error('移出失败:' + (e?.data?.message || e?.message || '未知错误'))
} finally {
unlinking.value = false
}
}
watch(searchKeyword, () => {
clearAllSelection()
})
watch(displayMode, () => {
clearAllSelection()
})
watch(
() => props.warehouseCode,
(wh) => {
goodsRows.value = []
goodsTotal.value = 0
searchKeyword.value = ''
pageCurrent.value = 1
activeGoodsRow.value = null
emitActiveGoods()
clearAllSelection()
if (wh) {
void refreshLinkedKeys()
void loadGoods()
} else {
emitLinkedKeys([])
}
},
{ immediate: true },
)
watch(
() => props.sceneCode,
() => {
pageCurrent.value = 1
void loadGoods()
},
)
watch([linking, unlinking], () => {
emit('busy-change', { linking: linking.value, unlinking: unlinking.value })
}, { immediate: true })
const splitFooterState = computed(() => ({
visible: props.regionSplitLayout
&& !!props.warehouseCode
&& goodsTotal.value > 0
&& !isTreeMode.value,
total: goodsTotal.value,
page: pageCurrent.value,
size: pageSize.value,
}))
watch(
splitFooterState,
(state) => {
if (props.regionSplitLayout) emit('split-footer-change', { ...state })
},
{ immediate: true, deep: true },
)
function setPageCurrent(page) {
onPageCurrentChange(page)
}
function setPageSize(size) {
onPageSizeChange(size)
}
async function refresh() {
await Promise.all([loadGoods(), refreshLinkedKeys()])
}
defineExpose({
linkGoods,
refresh,
applyGoodsFromRaw,
setPageCurrent,
setPageSize,
})
</script>
<style scoped>
.wh-srf-rdc {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
overflow: hidden;
}
.wh-srf-rdc-toolbar {
flex-shrink: 0;
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px 10px;
padding: 8px 10px;
border-bottom: 1px solid #f0f0f0;
background: #fff;
}
.wh-srf-rdc-title {
font-size: calc(12px + var(--wh-fs-delta, 0px));
font-weight: 600;
color: #303133;
}
.wh-srf-search {
flex: 1 1 120px;
min-width: 100px;
max-width: 200px;
}
.wh-srf-search :deep(.el-input__wrapper) {
padding: 0 8px;
}
.wh-srf-search :deep(.el-input__inner) {
font-size: calc(12px + var(--wh-fs-delta, 0px));
}
.wh-srf-display {
display: inline-flex;
align-items: center;
gap: 6px;
}
.wh-srf-display-label {
font-size: calc(11px + var(--wh-fs-delta, 0px));
color: #909399;
white-space: nowrap;
}
.wh-srf-display-group {
font-size: inherit;
display: inline-flex;
flex-wrap: nowrap;
}
.wh-srf-display-group :deep(.el-radio-button__inner) {
padding: 0 0.9em;
line-height: 1.846em;
font-size: calc(11px + var(--wh-fs-delta, 0px));
}
.wh-srf-lot-type {
display: inline-flex;
align-items: center;
gap: 6px;
}
.wh-srf-lot-type-label {
font-size: calc(11px + var(--wh-fs-delta, 0px));
color: #909399;
white-space: nowrap;
}
.wh-srf-lot-type-select {
width: 108px;
}
.wh-srf-lot-type-select :deep(.el-input__inner) {
font-size: calc(12px + var(--wh-fs-delta, 0px));
}
.wh-srf-rdc-batch-btn {
margin-left: 0;
}
.wh-srf-rdc-sel-hint {
font-size: calc(11px + var(--wh-fs-delta, 0px));
color: #409eff;
}
.wh-srf-rdc-meta {
margin-left: auto;
font-size: calc(11px + var(--wh-fs-delta, 0px));
color: #909399;
white-space: nowrap;
}
.wh-srf-rdc-body {
flex: 1;
min-height: 0;
overflow: hidden;
display: flex;
flex-direction: column;
}
.wh-srf-rdc-body.is-tree-mode {
overflow: auto;
}
.wh-srf-tree-wrap {
flex: 1;
min-height: 0;
min-width: 0;
display: flex;
flex-direction: column;
overflow-x: hidden;
overflow-y: auto;
}
.wh-srf-tree-wrap--rdc {
--wh-srf-tree-fixed-w: calc(7.5em + 4px + 52px);
}
.wh-srf-tree-wrap--rdc:not(:has(.has-actions)) {
--wh-srf-tree-fixed-w: 7.5em;
}
.wh-srf-tree-wrap--rdc .wh-srf-tree-head {
flex-shrink: 0;
position: sticky;
top: 0;
z-index: 2;
display: grid;
grid-template-columns: minmax(0, 1fr) var(--wh-srf-tree-fixed-w, 120px);
align-items: center;
column-gap: 0;
padding: 6px 0;
font-size: calc(12px + var(--wh-fs-delta, 0px));
font-weight: 600;
color: #909399;
background: #fafbfc;
border-bottom: 1px solid #ebeef5;
box-sizing: border-box;
}
.wh-srf-tree-head-name {
min-width: 0;
padding-left: 24px;
font-weight: 600;
}
.wh-srf-tree-wrap--rdc .wh-srf-tree-row {
display: grid;
grid-template-columns: minmax(0, 1fr) var(--wh-srf-tree-fixed-w, 120px);
align-items: center;
column-gap: 0;
width: 100%;
min-width: 0;
box-sizing: border-box;
}
.wh-srf-tree-row.is-cat .wh-srf-tree-col--name {
font-weight: 600;
color: #303133;
}
.wh-srf-tree-row.is-goods {
cursor: pointer;
}
.wh-srf-tree-row.is-goods.is-active {
background: rgba(64, 158, 255, 0.08);
border-radius: 4px;
}
.wh-srf-tree-fixed-cols {
display: grid;
width: var(--wh-srf-tree-fixed-w, 120px);
align-items: center;
gap: 4px;
min-width: 0;
box-sizing: border-box;
}
.wh-srf-tree-fixed-cols--rdc {
grid-template-columns: 7.5em;
}
.wh-srf-tree-fixed-cols--rdc.has-actions {
grid-template-columns: 7.5em 52px;
}
.wh-srf-tree-wrap--rdc .wh-srf-tree-col--lot,
.wh-srf-tree-wrap--rdc .wh-srf-tree-col--act {
justify-self: center;
}
.wh-srf-tree-fixed-cols--rdc .wh-srf-tree-col--lot {
flex: none;
width: 100%;
display: flex;
justify-content: center;
}
.wh-srf-tree-fixed-cols--rdc .wh-srf-tree-col--act {
flex: none;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
box-sizing: border-box;
}
.wh-srf-tree-col--lot {
overflow: visible;
}
/* 平铺列表:行结构与树形物料行一致(勾选 + 同一 grid) */
.wh-srf-flat-wrap {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
overflow: hidden;
}
.wh-srf-flat-head-row,
.wh-srf-flat-row {
display: flex;
align-items: center;
min-width: 0;
}
.wh-srf-flat-check-slot {
flex: 0 0 36px;
display: inline-flex;
align-items: center;
justify-content: center;
margin: 0;
}
.wh-srf-flat-head-grid {
flex: 1;
min-width: 0;
display: grid;
grid-template-columns: minmax(0, 1fr) var(--wh-srf-tree-fixed-w, 120px);
align-items: center;
column-gap: 0;
padding: 6px 0;
font-size: calc(12px + var(--wh-fs-delta, 0px));
font-weight: 600;
color: #909399;
background: #fafbfc;
border-bottom: 1px solid #ebeef5;
box-sizing: border-box;
}
.wh-srf-flat-head-name {
padding-left: 0;
}
.wh-srf-flat-list {
flex: 1;
min-height: 0;
overflow-y: auto;
}
.wh-srf-flat-row {
min-height: calc(28px + var(--wh-fs-delta, 0px));
cursor: pointer;
border-bottom: 1px solid #f5f7fa;
}
.wh-srf-flat-row:hover {
background: #fafbfc;
}
.wh-srf-flat-row.is-active {
background: rgba(64, 158, 255, 0.08);
}
.wh-srf-flat-row .wh-srf-tree-row {
flex: 1;
min-width: 0;
}
.wh-srf-tree-fixed-cols--fdc .wh-srf-tree-col--code {
flex: 0 0 76px;
width: 76px;
}
.wh-srf-tree-fixed-cols--fdc .wh-srf-tree-col--wh {
flex: 0 0 64px;
width: 64px;
}
.wh-srf-tree-fixed-cols--fdc.has-actions .wh-srf-tree-col--act {
flex: 0 0 88px;
width: 88px;
}
.wh-srf-tree-col {
min-width: 0;
font-size: calc(12px + var(--wh-fs-delta, 0px));
line-height: 1.35;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.wh-srf-tree-row .wh-srf-tree-col--name {
flex: 1;
min-width: 0;
display: inline-flex;
align-items: center;
gap: 6px;
color: #303133;
}
.wh-srf-tree-name-text {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.wh-srf-tree-col--code {
color: #606266;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
font-size: calc(11px + var(--wh-fs-delta, 0px));
}
.wh-srf-tree-col--wh.is-linked {
color: #409eff;
}
.wh-srf-tree-col--wh.is-unlinked {
color: #e6a23c;
}
.wh-srf-tree-col--act {
white-space: nowrap;
min-width: 0;
overflow: visible;
}
.wh-srf-tree-col--act .el-button {
margin: 0;
padding: 0;
font-size: calc(12px + var(--wh-fs-delta, 0px));
line-height: 1.35;
}
.wh-srf-goods-tree {
flex: 1;
min-height: 120px;
background: transparent;
}
.wh-srf-goods-tree--rdc :deep(.el-tree-node__content) {
height: auto;
min-height: calc(28px + var(--wh-fs-delta, 0px));
padding: 0 !important;
align-items: stretch;
display: flex;
width: 100%;
min-width: 0;
box-sizing: border-box;
overflow: visible;
}
.wh-srf-goods-tree--rdc :deep(.el-tree-node__expand-icon) {
margin-right: 4px;
}
.wh-srf-goods-tree--grid :deep(.el-tree-node__content) {
height: auto;
min-height: calc(28px + var(--wh-fs-delta, 0px));
padding-top: 0;
padding-bottom: 0;
align-items: center;
display: flex;
width: 100%;
min-width: 0;
box-sizing: border-box;
}
.wh-srf-goods-tree--rdc :deep(.el-tree-node__content > .wh-srf-tree-row) {
flex: 1;
min-width: 0;
}
.wh-srf-goods-tree--grid :deep(.el-tree-node__content > .wh-srf-tree-row) {
flex: 1;
min-width: 0;
}
.wh-srf-goods-final-tag {
flex-shrink: 0;
}
.wh-srf-rdc-empty {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 20px;
text-align: center;
font-size: calc(12px + var(--wh-fs-delta, 0px));
color: #909399;
}
.wh-srf-rdc-empty-sub {
margin: 6px 0 0;
font-size: calc(11px + var(--wh-fs-delta, 0px));
color: #c0c4cc;
}
.wh-srf-rdc-ft {
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: flex-end;
gap: 8px;
padding: 6px 10px;
border-top: 1px solid #f0f0f0;
background: #fafbfc;
}
/* Element Plus:表格 / 按钮 / 分页 / 标签 */
.wh-srf-rdc :deep(.el-button--small) {
font-size: calc(12px + var(--wh-fs-delta, 0px));
}
.wh-srf-rdc :deep(.el-tag--small) {
font-size: calc(11px + var(--wh-fs-delta, 0px));
height: calc(20px + var(--wh-fs-delta, 0px));
}
.wh-srf-rdc-ft :deep(.el-pagination),
.wh-srf-rdc-ft :deep(.el-pagination button),
.wh-srf-rdc-ft :deep(.el-pagination .el-select .el-input__inner) {
font-size: calc(12px + var(--wh-fs-delta, 0px));
}
</style>