280 lines
9.2 KiB
TypeScript
280 lines
9.2 KiB
TypeScript
/**
|
|
* 储物格下拉选项:设施树 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
|
|
}
|
|
|
|
/** 解析 slotCode「{prefix}{row}-{col}」 */
|
|
export function parseStorageSlotCode(code: string): { row: number; col: number } | null {
|
|
const match = (code || '').trim().match(/^(.*?)(\d+)-(\d+)$/)
|
|
if (!match) return null
|
|
return { row: parseInt(match[2], 10), col: parseInt(match[3], 10) }
|
|
}
|
|
|
|
/** 从根到节点的 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 codeA = String(a.slotCode ?? a.slot_code ?? '')
|
|
const codeB = String(b.slotCode ?? b.slot_code ?? '')
|
|
const pa = parseStorageSlotCode(codeA)
|
|
const pb = parseStorageSlotCode(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 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
|
|
}
|