Files
zkxw/web-admin/src/utils/storage-slot-options.ts
T
2026-06-25 12:41:07 +08:00

320 lines
11 KiB
TypeScript
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.
/**
* 储物格下拉选项:设施树 DFS(order_num)+ 盒内格位按行/列升序
*/
import { buildTreePathLabelFromFlat } from '@/utils/tree-path'
import { fetchStorageSlotSelectOptions } from '@/api/admin'
export type StorageSlotSelectOption = { id: string; name: string }
export type LoadStorageSlotSelectOptions = {
/** 仅返回空格子(编辑时可配合 includeSlotIds 保留当前格) */
availableOnly?: boolean
/** 额外保留在选项中的格子 ID(如编辑态当前储物格) */
includeSlotIds?: string[]
}
export function getStorageFacilityOrderNum(item: Record<string, unknown>): number {
const raw = item?.orderNum ?? item?.order_num ?? item?.sortOrder ?? item?.sort_order ?? 0
const parsed = Number(raw)
return Number.isFinite(parsed) ? parsed : 0
}
export function getStorageSlotOrderNum(slot: Record<string, unknown>): number {
const raw = slot?.orderNum ?? slot?.order_num ?? 0
const parsed = Number(raw)
return Number.isFinite(parsed) ? parsed : 0
}
/** 货架布局 order_num 步长(row * STRIDE + col) */
export const SLOT_LAYOUT_ROW_STRIDE = 1000
export function decodeSlotLayoutOrder(orderNum: number): { row: number; col: number } {
const order = Number(orderNum) || 0
if (order >= SLOT_LAYOUT_ROW_STRIDE) {
return { row: Math.trunc(order / SLOT_LAYOUT_ROW_STRIDE), col: order % SLOT_LAYOUT_ROW_STRIDE }
}
return { row: 0, col: order }
}
/** 解析旧版 slotCode「{prefix}{row}-{col}」;纯数字序号返回 null */
export function parseLegacyRowColSlotCode(code: string): { row: number; col: number; prefix: string } | null {
const match = (code || '').trim().match(/^(.*?)(\d+)-(\d+)$/)
if (!match) return null
return { prefix: match[1], row: parseInt(match[2], 10), col: parseInt(match[3], 10) }
}
/** @deprecated 兼容旧调用;新数据请用 decodeSlotLayoutOrder + 序号 slotCode */
export function parseStorageSlotCode(code: string): { row: number; col: number } | null {
const legacy = parseLegacyRowColSlotCode(code)
if (legacy) return { row: legacy.row, col: legacy.col }
return null
}
/** 从根到节点的 node_code 路径,用 `-` 连接 */
export function buildNodeCodePathFromFlat(
nodeId: string,
flatList: Record<string, unknown>[]
): string {
const byId = new Map<string, Record<string, unknown>>()
for (const item of flatList || []) {
const id = String(item?.id ?? '').trim()
if (id) byId.set(id, item)
}
const parts: string[] = []
let curId = String(nodeId ?? '').trim()
const visited = new Set<string>()
while (curId && curId !== '0' && !visited.has(curId)) {
visited.add(curId)
const node = byId.get(curId)
if (!node) break
const code = String(node.nodeCode ?? node.node_code ?? '').trim()
if (code) parts.unshift(code)
curId = String(node.parentId ?? node.parent_id ?? '').trim()
}
return parts.join('-')
}
/** 完整储物格编号:祖先 node_code + 本地 slot_code */
export function buildFullSlotCodeFromFlat(
facilityNodeId: string,
slotCode: string,
flatList: Record<string, unknown>[]
): string {
const path = buildNodeCodePathFromFlat(facilityNodeId, flatList)
const local = String(slotCode ?? '').trim()
if (path && local) return `${path}-${local}`
return local || path
}
/** 盒内格位:order_num → 布局行列 → 序号 */
export function compareStorageFacilitySlots(
a: Record<string, unknown>,
b: Record<string, unknown>
): number {
const orderDiff = getStorageSlotOrderNum(a) - getStorageSlotOrderNum(b)
if (orderDiff !== 0) return orderDiff
const oa = getStorageSlotOrderNum(a)
const ob = getStorageSlotOrderNum(b)
if (oa >= SLOT_LAYOUT_ROW_STRIDE && ob >= SLOT_LAYOUT_ROW_STRIDE) {
const da = decodeSlotLayoutOrder(oa)
const db = decodeSlotLayoutOrder(ob)
if (da.row !== db.row) return da.row - db.row
if (da.col !== db.col) return da.col - db.col
}
const codeA = String(a.slotCode ?? a.slot_code ?? '')
const codeB = String(b.slotCode ?? b.slot_code ?? '')
const pa = parseLegacyRowColSlotCode(codeA)
const pb = parseLegacyRowColSlotCode(codeB)
if (pa && pb) {
if (pa.row !== pb.row) return pa.row - pb.row
if (pa.col !== pb.col) return pa.col - pb.col
} else if (pa && !pb) return -1
else if (!pa && pb) return 1
const codeCmp = codeA.localeCompare(codeB, 'zh-Hans-CN', { numeric: true })
if (codeCmp !== 0) return codeCmp
return String(a.id ?? '').localeCompare(String(b.id ?? ''))
}
export function sortStorageFacilitySlots<T extends Record<string, unknown>>(slots: T[]): T[] {
return [...slots].sort(compareStorageFacilitySlots)
}
export function isStorageSlotAvailable(slot: Record<string, unknown>): boolean {
const status = String(slot.slotStatus ?? slot.slot_status ?? '')
const occupied = Number(slot.occupiedCount ?? slot.occupied_count ?? 0)
return status === '空' || occupied === 0
}
/**
* 按设施树顺序收集叶子节点(罐 → 层 → 盒…),同级按 order_num、名称排序。
*/
export function collectLeafFacilityNodesInTreeOrder(
flatList: Record<string, unknown>[]
): Record<string, unknown>[] {
const map = new Map<string, Record<string, unknown> & { children: unknown[] }>()
const roots: (Record<string, unknown> & { children: unknown[] })[] = []
for (const item of flatList || []) {
const id = String(item?.id ?? '').trim()
if (!id) continue
map.set(id, { ...item, children: [] })
}
for (const item of flatList || []) {
const id = String(item?.id ?? '').trim()
const node = map.get(id)
if (!node) continue
const parentId = String(item?.parentId ?? item?.parent_id ?? '').trim()
if (parentId && parentId !== '0' && map.has(parentId)) {
;(map.get(parentId)!.children as typeof roots).push(node)
} else {
roots.push(node)
}
}
const sortNodes = (nodes: (typeof roots)[number][]) => {
nodes.sort((a, b) => {
const orderDiff = getStorageFacilityOrderNum(a) - getStorageFacilityOrderNum(b)
if (orderDiff !== 0) return orderDiff
return String(a.nodeName ?? '').localeCompare(String(b.nodeName ?? ''), 'zh-Hans-CN')
})
nodes.forEach((node) => {
const ch = node.children
if (Array.isArray(ch) && ch.length) sortNodes(ch as typeof roots)
})
}
sortNodes(roots)
const leaves: Record<string, unknown>[] = []
const walk = (nodes: typeof roots) => {
for (const n of nodes) {
const isLeaf = n.isLeaf === 1 || n.is_leaf === 1
const ch = n.children
if (isLeaf) {
leaves.push(n)
} else if (Array.isArray(ch) && ch.length) {
walk(ch as typeof roots)
}
}
}
walk(roots)
return leaves
}
export function mapSlotsToSelectOptions(
facilityNodeId: string,
facilities: Record<string, unknown>[],
slots: Record<string, unknown>[],
opts?: LoadStorageSlotSelectOptions
): StorageSlotSelectOption[] {
const includeSet = new Set((opts?.includeSlotIds || []).map((id) => String(id).trim()).filter(Boolean))
const nodePath = buildTreePathLabelFromFlat(facilityNodeId, facilities, {
nameKey: 'nodeName'
})
return sortStorageFacilitySlots(slots)
.filter((slot) => {
const id = String(slot.id ?? '').trim()
if (includeSet.has(id)) return true
if (opts?.availableOnly) return isStorageSlotAvailable(slot)
return true
})
.map((slot) => {
const id = String(slot.id ?? '').trim()
const code = String(slot.slotCode ?? slot.slot_code ?? '')
const fullFromApi = String(slot.fullSlotCode ?? slot.full_slot_code ?? '').trim()
const fullCode =
fullFromApi || buildFullSlotCodeFromFlat(facilityNodeId, code, facilities)
const name = fullCode || code || nodePath
return { id, name }
})
}
export async function loadStorageSlotSelectOptions(
facilities: Record<string, unknown>[],
fetchStorageSlotList: (params: {
facilityNodeId: string
availableOnly?: boolean
}) => Promise<unknown>,
opts?: LoadStorageSlotSelectOptions
): Promise<StorageSlotSelectOption[]> {
const leafNodes = collectLeafFacilityNodesInTreeOrder(facilities)
if (!leafNodes.length) return []
const results = await Promise.allSettled(
leafNodes.map(async (node) => {
const nodeId = String(node.id ?? '').trim()
const res: any = await fetchStorageSlotList({
facilityNodeId: nodeId,
availableOnly: opts?.availableOnly ? true : undefined
})
const slots = (res?.list || res || []) as Record<string, unknown>[]
return mapSlotsToSelectOptions(nodeId, facilities, slots, opts)
})
)
const options: StorageSlotSelectOption[] = []
const seen = new Set<string>()
results.forEach((r) => {
if (r.status === 'fulfilled') {
r.value.forEach((opt) => {
if (!seen.has(opt.id)) {
seen.add(opt.id)
options.push(opt)
}
})
}
})
return options
}
export async function remoteSearchStorageSlotSelectOptions(
query: string,
opts?: LoadStorageSlotSelectOptions
): Promise<StorageSlotSelectOption[]> {
const includeSlotIds = (opts?.includeSlotIds || [])
.map((id) => String(id).trim())
.filter(Boolean)
.join(',')
try {
const res = await fetchStorageSlotSelectOptions({
keyword: query?.trim() || undefined,
availableOnly: opts?.availableOnly ? true : undefined,
includeSlotIds: includeSlotIds || undefined,
page: 1,
pageSize: 100
})
return (res?.list || []).map((item) => ({
id: String(item.id || ''),
name: String(item.name || item.fullSlotCode || item.slotCode || item.id || '')
}))
} catch {
return []
}
}
export function ensureStorageSlotsInOptions(
options: StorageSlotSelectOption[],
stubs: Array<{ id?: string; name?: string }>
): StorageSlotSelectOption[] {
const next = [...options]
const seen = new Set(next.map((o) => o.id))
for (const stub of stubs) {
const id = String(stub.id || '').trim()
if (!id || seen.has(id)) continue
seen.add(id)
next.unshift({
id,
name: String(stub.name || id)
})
}
return next
}
/** 下拉选项中排除已占用/已选格子,保留当前行已选格便于展示 */
export function filterStorageSlotSelectOptions(
options: StorageSlotSelectOption[],
excludeSlotIds: Array<string | undefined | null>,
keepSlotId?: string
): StorageSlotSelectOption[] {
const exclude = new Set(
excludeSlotIds.map((id) => String(id ?? '').trim()).filter(Boolean)
)
const keep = String(keepSlotId ?? '').trim()
if (keep) exclude.delete(keep)
return options.filter((opt) => !exclude.has(opt.id))
}
/** 校验表单内储物格不可重复(一格一物) */
export function validateUniqueStorageSlotIds(
slotIds: Array<string | undefined | null>,
message = '同一表单内不能选择重复的储物格,每份产品须使用不同储物格'
): boolean {
const seen = new Set<string>()
for (const raw of slotIds) {
const id = String(raw ?? '').trim()
if (!id) continue
if (seen.has(id)) return false
seen.add(id)
}
return true
}