import { ref, onMounted } from 'vue' import { showToast } from 'vant' import { expenseItemApi } from '@/api' export interface ExpenseItem { id: string parentId: string itemName: string itemCode: string level: number sortOrder: number status: number | string childCount: number children?: ExpenseItem[] } function normalizeItem(raw: any): ExpenseItem { const children = Array.isArray(raw?.children) ? raw.children.map(normalizeItem) : undefined return { id: String(raw?.id ?? ''), parentId: raw?.parentId == null || raw?.parentId === '' ? '0' : String(raw.parentId), itemName: raw?.itemName ?? '', itemCode: raw?.itemCode ?? '', level: Number(raw?.level ?? 0), sortOrder: Number(raw?.sortOrder ?? 0), status: raw?.status, childCount: Number(raw?.childCount ?? children?.length ?? 0), children, } } export function useExpenseItemTree() { const expenseTree = ref([]) const loading = ref(false) const flatList = ref([]) async function load() { loading.value = true try { const res: any = await expenseItemApi.tree() const tree: ExpenseItem[] = (res.data ?? []).map(normalizeItem) expenseTree.value = tree flatList.value = flattenTree(tree) } catch (e: any) { showToast(e?.message || '加载费用项目失败') } finally { loading.value = false } } onMounted(load) return { expenseTree, flatList, loading, load } } function flattenTree(tree: ExpenseItem[]): ExpenseItem[] { const result: ExpenseItem[] = [] for (const node of tree) { result.push(node) if (node.children?.length) result.push(...flattenTree(node.children)) } return result } export function formatExpenseName( id: string | number | null | undefined, flatList: ExpenseItem[], ): string { if (id == null || id === '') return '-' const sid = String(id) const parts: string[] = [] let cur: ExpenseItem | undefined = flatList.find((i) => String(i.id) === sid) const guard = new Set() while (cur && !guard.has(cur.id)) { guard.add(cur.id) parts.unshift(cur.itemName) const pid = cur.parentId cur = pid != null && pid !== '' && pid !== '0' ? flatList.find((i) => String(i.id) === pid) : undefined } return parts.length ? parts.join('/') : '-' } /** 从列表/详情行上常见的 camel/snake 字段取费用项 ID */ export function pickExpenseItemId(row: Record | null | undefined): string | null { if (!row || typeof row !== 'object') return null const raw = row.expenseId ?? row.expense_id ?? row.expenseItemId ?? row.expense_item_id if (raw == null || raw === '') return null return String(raw) } /** * 优先用费用树拼出「一级/二级」;无 ID 或树未加载时用接口返回的 expenseName。 */ export function resolveExpenseDisplay( row: Record | null | undefined, flatList: ExpenseItem[], ): string { if (!row) return '-' const id = pickExpenseItemId(row) const nameFb = row.expenseName ?? row.expense_name if (id != null && flatList.length) { const p = formatExpenseName(id, flatList) if (p && p !== '-') return p } const fb = nameFb != null ? String(nameFb).trim() : '' return fb || '-' }