feat: 数据字典左右分栏并拆分类型/明细分页接口
左侧仅拉取类型列表与项数,右侧按选中类型服务端分页;修复批量导入重复项识别、字段兼容及类型数量统计。
This commit is contained in:
parent
00285ac6a8
commit
d4447cc7d4
@ -1,9 +1,23 @@
|
||||
/**
|
||||
* OMS admin 接口通用:与 /order 同源(开发环境走 Vite 代理路径前缀)
|
||||
*/
|
||||
import type { AxiosError } from 'axios'
|
||||
import { axiosInstance } from '@/api/rbac'
|
||||
import { resolveAdminApiPath, resolveAdminAssetOrigin } from '@/utils/adminApi'
|
||||
|
||||
/** 从 axios / 业务响应中提取可读错误信息 */
|
||||
export function extractOmsRequestError(err: unknown): string {
|
||||
const axiosErr = err as AxiosError<{ msg?: string; Msg?: string; message?: string; Message?: string }>
|
||||
const data = axiosErr?.response?.data
|
||||
if (data && typeof data === 'object') {
|
||||
const msg = data.msg ?? data.Msg ?? data.message ?? data.Message
|
||||
if (msg != null && String(msg).trim()) {
|
||||
return String(msg).trim()
|
||||
}
|
||||
}
|
||||
return err instanceof Error ? err.message : String(err)
|
||||
}
|
||||
|
||||
export function omsUrl(path: string): string {
|
||||
return resolveAdminApiPath(path)
|
||||
}
|
||||
|
||||
@ -1,5 +1,11 @@
|
||||
import { axiosInstance } from '@/api/rbac'
|
||||
import { assertBusinessOk, omsUrl, parseListPayload } from '@/api/oms/common'
|
||||
import {
|
||||
assertBusinessOk,
|
||||
extractOmsRequestError,
|
||||
omsUrl,
|
||||
parseListPayload,
|
||||
unwrapData,
|
||||
} from '@/api/oms/common'
|
||||
|
||||
export interface DictDto {
|
||||
id?: number | string
|
||||
@ -11,6 +17,14 @@ export interface DictDto {
|
||||
updateTime?: string
|
||||
}
|
||||
|
||||
export interface DictTypeDto {
|
||||
id?: number | string
|
||||
dictType?: string
|
||||
remark?: string
|
||||
typeSort?: number
|
||||
itemCount?: number
|
||||
}
|
||||
|
||||
export type DictListParams = {
|
||||
page?: number
|
||||
pageSize?: number
|
||||
@ -18,9 +32,26 @@ export type DictListParams = {
|
||||
keyword?: string
|
||||
}
|
||||
|
||||
function normalizeDictDto(row: Record<string, unknown>): DictDto {
|
||||
const dictSortRaw = row.dictSort ?? row.DictSort
|
||||
return {
|
||||
id: (row.id ?? row.Id) as number | string | undefined,
|
||||
dictType: String(row.dictType ?? row.DictType ?? '').trim(),
|
||||
dictSort: typeof dictSortRaw === 'number' ? dictSortRaw : Number(dictSortRaw) || 0,
|
||||
dictCode: String(row.dictCode ?? row.DictCode ?? '').trim(),
|
||||
dictName: String(row.dictName ?? row.DictName ?? '').trim(),
|
||||
createTime: (row.createTime ?? row.CreateTime) as string | undefined,
|
||||
updateTime: (row.updateTime ?? row.UpdateTime) as string | undefined,
|
||||
}
|
||||
}
|
||||
|
||||
export async function getDictList(params: DictListParams) {
|
||||
const res = await axiosInstance.get(omsUrl('/system/dict/list'), { params })
|
||||
return parseListPayload(res.data)
|
||||
const { items, total } = parseListPayload(res.data)
|
||||
return {
|
||||
items: (items || []).map((row) => normalizeDictDto(row as Record<string, unknown>)),
|
||||
total,
|
||||
}
|
||||
}
|
||||
|
||||
/** 按当前筛选条件拉取全部字典(分页拼接),用于前端按类型排序后再分页展示 */
|
||||
@ -48,8 +79,17 @@ export async function getDictListAll(params: Pick<DictListParams, 'dictType' | '
|
||||
}
|
||||
|
||||
export async function createDict(body: Omit<DictDto, 'id' | 'createTime' | 'updateTime'>) {
|
||||
const res = await axiosInstance.post(omsUrl('/system/dict/create'), body)
|
||||
try {
|
||||
const res = await axiosInstance.post(omsUrl('/system/dict/create'), {
|
||||
...body,
|
||||
dictType: String(body.dictType ?? '').trim(),
|
||||
dictCode: String(body.dictCode ?? '').trim(),
|
||||
dictName: String(body.dictName ?? '').trim(),
|
||||
})
|
||||
assertBusinessOk(res.data)
|
||||
} catch (e) {
|
||||
throw new Error(extractOmsRequestError(e))
|
||||
}
|
||||
}
|
||||
|
||||
export type DictUpdateBody = {
|
||||
@ -69,3 +109,41 @@ export async function deleteDict(id: number | string) {
|
||||
const res = await axiosInstance.post(omsUrl('/system/dict/delete'), { id })
|
||||
assertBusinessOk(res.data)
|
||||
}
|
||||
|
||||
function normalizeDictTypeDto(row: Record<string, unknown>): DictTypeDto {
|
||||
const itemCountRaw = row.itemCount ?? row.ItemCount
|
||||
const typeSortRaw = row.typeSort ?? row.TypeSort
|
||||
return {
|
||||
id: (row.id ?? row.Id) as number | string | undefined,
|
||||
dictType: String(row.dictType ?? row.DictType ?? '').trim(),
|
||||
remark: (row.remark ?? row.Remark) as string | undefined,
|
||||
typeSort: typeof typeSortRaw === 'number' ? typeSortRaw : Number(typeSortRaw) || 0,
|
||||
itemCount: typeof itemCountRaw === 'number' ? itemCountRaw : Number(itemCountRaw) || 0,
|
||||
}
|
||||
}
|
||||
|
||||
export async function getDictTypeList(keyword?: string) {
|
||||
const res = await axiosInstance.get(omsUrl('/system/dict/type/list'), {
|
||||
params: keyword ? { keyword } : undefined,
|
||||
})
|
||||
const payload = unwrapData<Record<string, unknown>[] | null>(res.data)
|
||||
if (!Array.isArray(payload)) return []
|
||||
return payload.map((row) => normalizeDictTypeDto(row))
|
||||
}
|
||||
|
||||
export type DictTypeSaveBody = {
|
||||
id?: number | string
|
||||
dictType: string
|
||||
remark?: string
|
||||
typeSort?: number
|
||||
}
|
||||
|
||||
export async function saveDictType(body: DictTypeSaveBody) {
|
||||
const res = await axiosInstance.post(omsUrl('/system/dict/type/save'), body)
|
||||
assertBusinessOk(res.data)
|
||||
}
|
||||
|
||||
export async function deleteDictType(id: number | string) {
|
||||
const res = await axiosInstance.post(omsUrl('/system/dict/type/delete'), { id })
|
||||
assertBusinessOk(res.data)
|
||||
}
|
||||
|
||||
@ -38,6 +38,7 @@ export default {
|
||||
|
||||
// Menus (keep existing demo keys)
|
||||
'menu.home': 'Home',
|
||||
'menu.assistant': 'Logistics Advisor',
|
||||
'menu.server.home': 'Home-Server',
|
||||
'menu.server.workplace': 'Workplace-Server',
|
||||
'menu.server.monitor': 'Monitor-Server',
|
||||
@ -470,6 +471,8 @@ export default {
|
||||
'order.platformStoreOrder.status.toShip': 'To Ship',
|
||||
'order.platformStoreOrder.status.shipped': 'Shipped',
|
||||
'order.platformStoreOrder.status.suspended': 'Suspended',
|
||||
'order.platformStoreOrder.status.canceled': 'Canceled',
|
||||
'order.platformStoreOrder.status.completed': 'Completed',
|
||||
'order.platformStoreOrder.table.platform': 'Platform',
|
||||
'order.platformStoreOrder.table.country': 'Country',
|
||||
'order.platformStoreOrder.table.itemCount': 'Items',
|
||||
@ -487,10 +490,34 @@ export default {
|
||||
'order.platformStoreOrder.summary.pending': 'Pending',
|
||||
'order.platformStoreOrder.summary.toShip': 'To Ship',
|
||||
'order.platformStoreOrder.drawer.detail': 'Order Detail',
|
||||
'order.platformStoreOrder.drawer.sectionBasic': 'Basic Info',
|
||||
'order.platformStoreOrder.drawer.sectionRecipient': 'Recipient',
|
||||
'order.platformStoreOrder.drawer.sectionShipping': 'Fulfillment & Shipping',
|
||||
'order.platformStoreOrder.drawer.sectionLines': 'Line Items',
|
||||
'order.platformStoreOrder.drawer.platformStatus': 'Platform Status',
|
||||
'order.platformStoreOrder.drawer.customTags': 'Custom Tags',
|
||||
'order.platformStoreOrder.drawer.multiLabel': 'Multi Label',
|
||||
'order.platformStoreOrder.drawer.openLabel': 'Open Label',
|
||||
'order.platformStoreOrder.recipient.name': 'Recipient',
|
||||
'order.platformStoreOrder.recipient.company': 'Company',
|
||||
'order.platformStoreOrder.recipient.phone': 'Phone',
|
||||
'order.platformStoreOrder.recipient.telePhone': 'Tel',
|
||||
'order.platformStoreOrder.recipient.email': 'Email',
|
||||
'order.platformStoreOrder.recipient.country': 'Country',
|
||||
'order.platformStoreOrder.recipient.province': 'State/Province',
|
||||
'order.platformStoreOrder.recipient.city': 'City',
|
||||
'order.platformStoreOrder.recipient.zipCode': 'Zip',
|
||||
'order.platformStoreOrder.recipient.address1': 'Address 1',
|
||||
'order.platformStoreOrder.recipient.address2': 'Address 2',
|
||||
'order.platformStoreOrder.table.recipient': 'Recipient',
|
||||
'order.platformStoreOrder.line.sku': 'Platform SKU',
|
||||
'order.platformStoreOrder.line.name': 'Product',
|
||||
'order.platformStoreOrder.line.qty': 'Qty',
|
||||
'order.platformStoreOrder.line.mappedSku': 'Mapped SKU',
|
||||
'order.platformStoreOrder.line.image': 'Image',
|
||||
'order.platformStoreOrder.line.variant': 'Variant',
|
||||
'order.platformStoreOrder.line.price': 'Unit Price',
|
||||
'order.platformStoreOrder.line.excluded': 'Excluded',
|
||||
'order.platformStoreOrder.actions.match': 'Match Inventory',
|
||||
'order.platformStoreOrder.actions.batchMatch': 'Batch Match',
|
||||
'order.platformStoreOrder.actions.suspend': 'Suspend',
|
||||
@ -501,6 +528,56 @@ export default {
|
||||
'order.platformStoreOrder.actions.syncStatus': 'Sync Status',
|
||||
'order.platformStoreOrder.actions.readyToShip': 'Ready to Ship',
|
||||
'order.platformStoreOrder.actions.fetchLabel': 'Re-fetch Label',
|
||||
'order.platformStoreOrder.actions.createLogistics': 'Create Logistics Order',
|
||||
'order.platformStoreOrder.actions.createFulfill': 'Create Logistics & Fulfill',
|
||||
'order.platformStoreOrder.createLogistics.title': 'Create Logistics Order',
|
||||
'order.platformStoreOrder.createLogistics.tip': 'Review recipient and line items for each order, then confirm the shipping product before submitting.',
|
||||
'order.platformStoreOrder.createLogistics.product': 'Shipping Product',
|
||||
'order.platformStoreOrder.createLogistics.productRequired': 'Please select a shipping product',
|
||||
'order.platformStoreOrder.createLogistics.orderList': 'Selected Orders',
|
||||
'order.platformStoreOrder.createLogistics.receiverTitle': 'Recipient Check',
|
||||
'order.platformStoreOrder.createLogistics.linesTitle': 'Line Items',
|
||||
'order.platformStoreOrder.createLogistics.readyMeta': '{total} selected · {ready} ready',
|
||||
'order.platformStoreOrder.createLogistics.tagReceiver': 'Missing recipient',
|
||||
'order.platformStoreOrder.createLogistics.tagLines': 'No lines',
|
||||
'order.platformStoreOrder.createLogistics.tagUnbound': 'Unbound',
|
||||
'order.platformStoreOrder.createLogistics.tagBound': '{n} bound',
|
||||
'order.platformStoreOrder.createLogistics.tagOk': 'Ready',
|
||||
'order.platformStoreOrder.createLogistics.selectRequired': 'Please select pending store orders first',
|
||||
'order.platformStoreOrder.createLogistics.maxLimit': 'Up to 50 orders per batch',
|
||||
'order.platformStoreOrder.createLogistics.invalidSelection': '{count} order(s) cannot be created (need pending, not suspended, no linked logistics order)',
|
||||
'order.platformStoreOrder.createLogistics.fixIncomplete': 'Please fix the highlighted order before submit',
|
||||
'order.platformStoreOrder.createLogistics.confirm': 'Create {count} logistics order(s) with product {product}. Continue?',
|
||||
'order.platformStoreOrder.createLogistics.submit': 'Confirm Create',
|
||||
'order.platformStoreOrder.createLogistics.success': 'Created {count} logistics order(s)',
|
||||
'order.platformStoreOrder.createLogistics.partial': 'Success {success}, failed {fail}. {detail}',
|
||||
'order.platformStoreOrder.createLogistics.failed': 'Failed to create logistics orders',
|
||||
'order.platformStoreOrder.createLogistics.unitOrders': 'orders',
|
||||
'order.platformStoreOrder.createFulfill.title': 'Create Logistics & Fulfill',
|
||||
'order.platformStoreOrder.createFulfill.tip': 'Compare order line items with bound inventory/parcels: check product name, SKU and quantity before submit.',
|
||||
'order.platformStoreOrder.createFulfill.bindTitle': 'Bind Parcel / Inventory',
|
||||
'order.platformStoreOrder.createFulfill.bindHint': 'Pick on the left, confirm qty on the right; review each order before submit',
|
||||
'order.platformStoreOrder.createFulfill.bindEmpty': 'Pick inventory or parcels below to bind on the right',
|
||||
'order.platformStoreOrder.createFulfill.orderSide': 'Order qty',
|
||||
'order.platformStoreOrder.createFulfill.bindSide': 'Bound qty',
|
||||
'order.platformStoreOrder.createFulfill.pcs': 'pcs',
|
||||
'order.platformStoreOrder.createFulfill.lineCount': '{n} line(s)',
|
||||
'order.platformStoreOrder.createFulfill.bindCount': '{n} resource(s)',
|
||||
'order.platformStoreOrder.createFulfill.compareNeedBind': 'Nothing bound yet — pick stock/parcels to match order lines',
|
||||
'order.platformStoreOrder.createFulfill.compareQtyDiff': 'Qty mismatch: order {order} vs bound {bind} — please recheck',
|
||||
'order.platformStoreOrder.createFulfill.compareQtyOk': 'Quantities match — still verify product names/SKUs',
|
||||
'order.platformStoreOrder.createFulfill.orderGoodsTitle': 'Order line items',
|
||||
'order.platformStoreOrder.createFulfill.orderGoodsHint': 'From the store order — check name and qty',
|
||||
'order.platformStoreOrder.createFulfill.boundTitle': 'Bound inventory / parcels',
|
||||
'order.platformStoreOrder.createFulfill.boundHint': 'What will ship from warehouse — match against the left',
|
||||
'order.platformStoreOrder.createFulfill.needQty': 'Need',
|
||||
'order.platformStoreOrder.createFulfill.pickTitle': 'Pick inventory / parcels',
|
||||
'order.platformStoreOrder.createFulfill.orderQtyShort': 'Need {n}',
|
||||
'order.platformStoreOrder.createFulfill.bindQtyShort': 'Bound {n}',
|
||||
'order.platformStoreOrder.createFulfill.tagQtyDiff': 'Qty mismatch',
|
||||
'order.platformStoreOrder.createFulfill.tagQtyOk': 'Qty aligned',
|
||||
'order.platformStoreOrder.createFulfill.confirm': 'Create {count} logistics order(s) and bind stock. Product: {product}; Warehouse: {warehouse}. Continue?',
|
||||
'order.platformStoreOrder.createFulfill.submit': 'Confirm Create & Bind',
|
||||
'order.platformStoreOrder.pull.store': 'Store',
|
||||
'order.platformStoreOrder.pull.storePlaceholder': 'Select stores',
|
||||
'order.platformStoreOrder.pull.status': 'Status',
|
||||
@ -800,13 +877,56 @@ export default {
|
||||
'nanaAssistant.open': 'Open Nana exclusive advisor',
|
||||
'nanaAssistant.tabAi': 'AI Nana',
|
||||
'nanaAssistant.tabAdvisor': 'Exclusive Advisor',
|
||||
'nanaAssistant.badge': 'Logistics Advisor',
|
||||
'nanaAssistant.welcomeTitle': 'What do you need help shipping?',
|
||||
'nanaAssistant.welcomeSub': 'Quotes, routes, tracking, or a tough order — ask here. One dedicated advisor from quote to delivery.',
|
||||
'nanaAssistant.quickLabel': 'Common questions',
|
||||
'nanaAssistant.welcomeMsg': "Hi — I'm your IDE logistics advisor. Pick a question below or type your own.",
|
||||
'nanaAssistant.historyTitle': 'Chat history',
|
||||
'nanaAssistant.newChat': 'New chat',
|
||||
'nanaAssistant.emptyHistory': 'No conversations yet',
|
||||
'nanaAssistant.searchHistory': 'Search chats or waybill no.',
|
||||
'nanaAssistant.noSearchResults': 'No matching conversations',
|
||||
'nanaAssistant.deleteThread': 'Delete conversation',
|
||||
'nanaAssistant.hint': 'Common questions show reference answers. Type in the box to ask more.',
|
||||
'nanaAssistant.hintLive': 'Smart advisor is connected — type a question or pick one below.',
|
||||
'nanaAssistant.human': 'Talk to a human advisor →',
|
||||
'nanaAssistant.you': 'You',
|
||||
'nanaAssistant.backToChat': 'Back to chat',
|
||||
'nanaAssistant.quickSmall': 'Small or difficult orders?',
|
||||
'nanaAssistant.quickPrice': 'US / EU shipping cost?',
|
||||
'nanaAssistant.quickQuoteSpeed': 'How fast is a quote?',
|
||||
'nanaAssistant.quickTrack': 'Track my shipment',
|
||||
'nanaAssistant.quickVisibility': 'Real-time tracking?',
|
||||
'nanaAssistant.quickAfterSales': 'Something went wrong?',
|
||||
'nanaAssistant.quickHuman': 'Talk to human advisor',
|
||||
'nanaAssistant.replySmall': 'Yes. IDE focuses on SMB sellers and shipments others decline — special routes, complex cargo, and low-volume orders are in scope. Tell me origin, destination, and cargo type to get started.',
|
||||
'nanaAssistant.replyPrice': 'Pricing depends on origin, weight, dimensions, and channel (air / sea / express). Share those details and we return a route-specific quote with transit options within 24 hours.',
|
||||
'nanaAssistant.replyQuoteSpeed': 'Route-specific quotes are typically returned within 24 hours after you share shipment details. Urgent lanes can be prioritized — mention your target ship date.',
|
||||
'nanaAssistant.trackHint': "Paste your tracking number in the box below. I'll walk you through what each milestone usually means.",
|
||||
'nanaAssistant.replyVisibility': 'You get milestone updates from pickup to delivery — pickup, export, line-haul, customs, and final delivery. We proactively flag delays before they escalate.',
|
||||
'nanaAssistant.replyAfterSales': "Your dedicated advisor handles exceptions, claims, and after-sales follow-up — you won't be left chasing tickets alone. Describe the issue and tracking number if you have one.",
|
||||
'nanaAssistant.replyHuman': 'Reach us via the contact page — phone 18123959245 or email bd01@1st56.com. Mention your route and cargo so we assign the right advisor.',
|
||||
'nanaAssistant.fallback': "Got it. Share origin, destination, cargo type, and weight for a route-specific quote — or talk to a human advisor and we'll respond within 24 hours.",
|
||||
'nanaAssistant.wechatId': 'WeChat ID',
|
||||
'nanaAssistant.noAdvisor': 'No exclusive advisor assigned yet',
|
||||
'nanaAssistant.noWechatQr': 'WeChat QR code not uploaded',
|
||||
'nanaAssistant.greeting': "Hi, I'm Nana, your AI assistant.\n\nAsk me about tracking, shipping times, declared values, customs clearance, and more. How can I help you today?",
|
||||
'nanaAssistant.inputPlaceholder': 'Type your logistics question, press Enter to send',
|
||||
'nanaAssistant.inputPlaceholder': 'Type your question…',
|
||||
'nanaAssistant.send': 'Send',
|
||||
'nanaAssistant.close': 'Close',
|
||||
'nanaAssistant.back': 'Back',
|
||||
'nanaAssistant.askAboutWaybill': 'Ask logistics advisor',
|
||||
'nanaAssistant.actionAdvisor': 'Advisor',
|
||||
'nanaAssistant.waybillContext': 'Current waybill: {no}',
|
||||
'nanaAssistant.viewWaybill': 'View waybill →',
|
||||
'nanaAssistant.queryTrack': 'Track shipment',
|
||||
'nanaAssistant.endWaybillConsult': 'End waybill consult',
|
||||
'nanaAssistant.waybillAutoAsk': 'Please track waybill {no} for me',
|
||||
'nanaAssistant.trackWaybillDemo': 'Got your request for waybill {no}. When the smart service is configured, we prefer the live API; this is a reference reply: milestones usually include pickup, export, line-haul, customs, and delivery. Your advisor will follow up on exceptions.',
|
||||
'nanaAssistant.copyWechat': 'Copy WeChat ID',
|
||||
'nanaAssistant.copyWechatSuccess': 'WeChat ID copied',
|
||||
'nanaAssistant.copyWechatFailed': 'Copy failed — please copy manually',
|
||||
'nanaAssistant.noProvider': 'AI service is not configured yet. Please contact your administrator.',
|
||||
'nanaAssistant.sendFailed': 'Failed to send. Please try again later.',
|
||||
'nav.balance.label': 'Balance',
|
||||
@ -2141,13 +2261,35 @@ export default {
|
||||
'system.dict.search.placeholder.dictNameOrValue': 'Enter dictionary name or value',
|
||||
'system.dict.search.placeholder': 'Enter dictionary code or name',
|
||||
'system.dict.filter.dictType': 'Dictionary Type',
|
||||
'system.dict.left.category': 'Dictionary Types',
|
||||
'system.dict.left.searchPlaceholder': 'Search name or type code',
|
||||
'system.dict.left.refresh': 'Refresh types',
|
||||
'system.dict.left.addType': 'Add type',
|
||||
'system.dict.left.editType': 'Edit type',
|
||||
'system.dict.left.deleteType': 'Delete type',
|
||||
'system.dict.left.loading': 'Loading dictionary types...',
|
||||
'system.dict.left.emptySelect': 'Select a dictionary type from the left',
|
||||
'system.dict.left.emptySelectDesc': 'Then view and manage entries under that type',
|
||||
'system.dict.left.emptyTypeList': 'No dictionary types yet. Click + to add one',
|
||||
'system.dict.panel.itemCount': '{count} entries',
|
||||
'system.dict.panel.editType': 'Edit type',
|
||||
'system.dict.form.typeRemark': 'Display name',
|
||||
'system.dict.form.typeSort': 'Type sort',
|
||||
'system.dict.placeholder.typeRemark': 'e.g. Order status',
|
||||
'system.dict.typeModal.addTitle': 'Add Dictionary Type',
|
||||
'system.dict.typeModal.editTitle': 'Edit Dictionary Type',
|
||||
'system.dict.typeModal.addSuccess': 'Dictionary type added',
|
||||
'system.dict.typeModal.editSuccess': 'Dictionary type updated',
|
||||
'system.dict.typeModal.deleteSuccess': 'Dictionary type metadata deleted',
|
||||
'system.dict.typeModal.deleteConfirm': 'Delete metadata for type [{dictType}]? Dictionary items will not be removed.',
|
||||
'system.dict.message.loadTypeFailed': 'Failed to load dictionary types',
|
||||
'system.dict.actions.add': 'Add',
|
||||
'system.dict.actions.edit': 'Edit',
|
||||
'system.dict.actions.delete': 'Delete',
|
||||
'system.dict.actions.batchImport': 'Batch import',
|
||||
'system.dict.batchImport.confirmTitle': 'Batch import dictionaries',
|
||||
'system.dict.batchImport.confirmContent':
|
||||
'Predefined dictionary rows from the OMS design spec will be created; existing rows with the same dictType and dictCode are skipped. Import order: dict types in alphabetical order; each type is fully written before the next. Rules: many rows may share one dictType; dictCode must be unique within that dictType (the same code may appear under another dictType, e.g. 0/1). Use yes_no for yes/no; use enable_status for disabled/enabled. Continue?',
|
||||
'Predefined dictionary rows from the OMS design spec will be created; existing rows with the same dictType and dictCode are skipped. Import order: dict types in alphabetical order; each type is fully written before the next. Rules: many rows may share one dictType; dictCode must be unique within that dictType (the same code may appear under another dictType, e.g. 1/2). Numeric dictCode starts at 1; 0 means None and is not seeded. Use yes_no for yes/no; use enable_status for disabled/enabled. Continue?',
|
||||
'system.dict.batchImport.ok': 'Import',
|
||||
'system.dict.batchImport.resultDetail': 'Done: created {created}, skipped {skipped}, failed {failed}.',
|
||||
'system.dict.batchImport.failed': 'Batch import failed: {error}',
|
||||
@ -3206,18 +3348,30 @@ export default {
|
||||
'channel.labelDesign.actions.create': 'New template',
|
||||
'channel.labelDesign.actions.edit': 'Design',
|
||||
'channel.labelDesign.actions.delete': 'Delete',
|
||||
'channel.labelDesign.actions.refresh': 'Refresh',
|
||||
'channel.labelDesign.actions.back': 'Back',
|
||||
'channel.labelDesign.actions.preview': 'Preview',
|
||||
'channel.labelDesign.actions.print': 'Print',
|
||||
'channel.labelDesign.actions.save': 'Save',
|
||||
'channel.labelDesign.actions.openTemplate': 'Open template',
|
||||
'channel.labelDesign.actions.open': 'Open',
|
||||
'channel.labelDesign.actions.startDesign': 'Start design',
|
||||
'channel.labelDesign.actions.cancel': 'Cancel',
|
||||
'channel.labelDesign.loadingList': 'Loading templates…',
|
||||
'channel.labelDesign.emptySelect': 'Select a template on the left, or create one',
|
||||
'channel.labelDesign.openModal.title': 'Open system template',
|
||||
'channel.labelDesign.openModal.confirmTitle': 'Switch template',
|
||||
'channel.labelDesign.openModal.confirmContent': 'Unsaved canvas changes will be lost. Open the selected template?',
|
||||
'channel.labelDesign.message.loadFailed': 'Failed to load templates',
|
||||
'channel.labelDesign.message.saveSuccess': 'Template saved',
|
||||
'channel.labelDesign.message.saveFailed': 'Save failed',
|
||||
'channel.labelDesign.message.deleteSuccess': 'Deleted',
|
||||
'channel.labelDesign.message.nameRequired': 'Template name is required',
|
||||
'channel.labelDesign.message.alreadyOpen': 'This template is already open',
|
||||
'channel.labelDesign.message.openSuccess': 'Opened: {name}',
|
||||
'channel.labelDesign.fieldDictionary': 'Order fields',
|
||||
'channel.labelDesign.fieldsInjectedHint': 'Order fields injected (platform/custom groups hidden)',
|
||||
'channel.labelDesign.fieldsInjectFailed': 'Failed to inject order fields into the designer. Please refresh.',
|
||||
'channel.labelDesign.message.fieldCopied': 'Copied field: {field}',
|
||||
'channel.labelDesign.message.previewEmpty': 'Nothing to preview',
|
||||
'channel.labelDesign.message.previewFailed': 'PDF preview failed',
|
||||
|
||||
@ -38,6 +38,7 @@ export default {
|
||||
|
||||
// 菜单(示例 / 保留原有)
|
||||
'menu.home': '首页',
|
||||
'menu.assistant': '物流顾问',
|
||||
'menu.server.home': '首页-服务端',
|
||||
'menu.server.workplace': '工作台-服务端',
|
||||
'menu.server.monitor': '实时监控-服务端',
|
||||
@ -470,6 +471,8 @@ export default {
|
||||
'order.platformStoreOrder.status.toShip': '待发货',
|
||||
'order.platformStoreOrder.status.shipped': '已发货',
|
||||
'order.platformStoreOrder.status.suspended': '已搁置',
|
||||
'order.platformStoreOrder.status.canceled': '已取消',
|
||||
'order.platformStoreOrder.status.completed': '已完成',
|
||||
'order.platformStoreOrder.table.platform': '平台',
|
||||
'order.platformStoreOrder.table.country': '国家',
|
||||
'order.platformStoreOrder.table.itemCount': '商品数',
|
||||
@ -487,10 +490,34 @@ export default {
|
||||
'order.platformStoreOrder.summary.pending': '待处理',
|
||||
'order.platformStoreOrder.summary.toShip': '待发货',
|
||||
'order.platformStoreOrder.drawer.detail': '订单详情',
|
||||
'order.platformStoreOrder.drawer.sectionBasic': '基本信息',
|
||||
'order.platformStoreOrder.drawer.sectionRecipient': '收件人信息',
|
||||
'order.platformStoreOrder.drawer.sectionShipping': '履约与物流',
|
||||
'order.platformStoreOrder.drawer.sectionLines': '商品明细',
|
||||
'order.platformStoreOrder.drawer.platformStatus': '平台状态',
|
||||
'order.platformStoreOrder.drawer.customTags': '自定义标签',
|
||||
'order.platformStoreOrder.drawer.multiLabel': '多面单',
|
||||
'order.platformStoreOrder.drawer.openLabel': '打开面单',
|
||||
'order.platformStoreOrder.recipient.name': '收件人',
|
||||
'order.platformStoreOrder.recipient.company': '公司',
|
||||
'order.platformStoreOrder.recipient.phone': '手机',
|
||||
'order.platformStoreOrder.recipient.telePhone': '电话',
|
||||
'order.platformStoreOrder.recipient.email': '邮箱',
|
||||
'order.platformStoreOrder.recipient.country': '国家',
|
||||
'order.platformStoreOrder.recipient.province': '省/州',
|
||||
'order.platformStoreOrder.recipient.city': '城市',
|
||||
'order.platformStoreOrder.recipient.zipCode': '邮编',
|
||||
'order.platformStoreOrder.recipient.address1': '地址1',
|
||||
'order.platformStoreOrder.recipient.address2': '地址2',
|
||||
'order.platformStoreOrder.table.recipient': '收件人',
|
||||
'order.platformStoreOrder.line.sku': '平台 SKU',
|
||||
'order.platformStoreOrder.line.name': '商品',
|
||||
'order.platformStoreOrder.line.qty': '数量',
|
||||
'order.platformStoreOrder.line.mappedSku': '库内 SKU',
|
||||
'order.platformStoreOrder.line.image': '图片',
|
||||
'order.platformStoreOrder.line.variant': '规格',
|
||||
'order.platformStoreOrder.line.price': '单价',
|
||||
'order.platformStoreOrder.line.excluded': '拒发',
|
||||
'order.platformStoreOrder.actions.match': '匹配库存',
|
||||
'order.platformStoreOrder.actions.batchMatch': '批量匹配',
|
||||
'order.platformStoreOrder.actions.suspend': '搁置',
|
||||
@ -501,6 +528,56 @@ export default {
|
||||
'order.platformStoreOrder.actions.syncStatus': '同步状态',
|
||||
'order.platformStoreOrder.actions.readyToShip': '准备发货',
|
||||
'order.platformStoreOrder.actions.fetchLabel': '重新获取面单',
|
||||
'order.platformStoreOrder.actions.createLogistics': '创建物流订单',
|
||||
'order.platformStoreOrder.actions.createFulfill': '创建物流&代发订单',
|
||||
'order.platformStoreOrder.createLogistics.title': '创建物流订单',
|
||||
'order.platformStoreOrder.createLogistics.tip': '请逐单核对收件人与商品明细,确认运输方式后再提交,避免下错单。',
|
||||
'order.platformStoreOrder.createLogistics.product': '运输方式',
|
||||
'order.platformStoreOrder.createLogistics.productRequired': '请选择运输方式',
|
||||
'order.platformStoreOrder.createLogistics.orderList': '待创建清单',
|
||||
'order.platformStoreOrder.createLogistics.receiverTitle': '收件人校对',
|
||||
'order.platformStoreOrder.createLogistics.linesTitle': '商品明细',
|
||||
'order.platformStoreOrder.createLogistics.readyMeta': '共 {total} 单 · 可提交 {ready} 单',
|
||||
'order.platformStoreOrder.createLogistics.tagReceiver': '缺收件人',
|
||||
'order.platformStoreOrder.createLogistics.tagLines': '无商品行',
|
||||
'order.platformStoreOrder.createLogistics.tagUnbound': '未绑定',
|
||||
'order.platformStoreOrder.createLogistics.tagBound': '已绑 {n} 项',
|
||||
'order.platformStoreOrder.createLogistics.tagOk': '可创建',
|
||||
'order.platformStoreOrder.createLogistics.selectRequired': '请先勾选待履约店铺订单',
|
||||
'order.platformStoreOrder.createLogistics.maxLimit': '单次最多创建 50 单',
|
||||
'order.platformStoreOrder.createLogistics.invalidSelection': '有 {count} 单不可创建(需 pending、未搁置、未关联物流单)',
|
||||
'order.platformStoreOrder.createLogistics.fixIncomplete': '请先补全当前标红订单的校对项',
|
||||
'order.platformStoreOrder.createLogistics.confirm': '将创建 {count} 张物流单,运输方式:{product}。确认提交?',
|
||||
'order.platformStoreOrder.createLogistics.submit': '确认创建',
|
||||
'order.platformStoreOrder.createLogistics.success': '成功创建 {count} 张物流单',
|
||||
'order.platformStoreOrder.createLogistics.partial': '成功 {success},失败 {fail}。{detail}',
|
||||
'order.platformStoreOrder.createLogistics.failed': '创建物流订单失败',
|
||||
'order.platformStoreOrder.createLogistics.unitOrders': '单',
|
||||
'order.platformStoreOrder.createFulfill.title': '创建物流&代发订单',
|
||||
'order.platformStoreOrder.createFulfill.tip': '请对照「订单要发的商品」与「已绑定的库存/包裹」:核对品名、SKU、数量是否一致,避免绑错货或数量不对。',
|
||||
'order.platformStoreOrder.createFulfill.bindTitle': '绑定包裹 / 库存',
|
||||
'order.platformStoreOrder.createFulfill.bindHint': '左侧挑选资源,右侧确认数量;请逐单核对后再提交',
|
||||
'order.platformStoreOrder.createFulfill.bindEmpty': '请从下方挑选库存或包裹加入右侧',
|
||||
'order.platformStoreOrder.createFulfill.orderSide': '订单应发',
|
||||
'order.platformStoreOrder.createFulfill.bindSide': '已绑定',
|
||||
'order.platformStoreOrder.createFulfill.pcs': '件',
|
||||
'order.platformStoreOrder.createFulfill.lineCount': '{n} 个商品行',
|
||||
'order.platformStoreOrder.createFulfill.bindCount': '{n} 项资源',
|
||||
'order.platformStoreOrder.createFulfill.compareNeedBind': '尚未绑定,请先对照订单商品挑选库存/包裹',
|
||||
'order.platformStoreOrder.createFulfill.compareQtyDiff': '数量不一致:订单 {order} 件,已绑 {bind} 件,请再核对',
|
||||
'order.platformStoreOrder.createFulfill.compareQtyOk': '数量一致,请再确认品名/SKU 是否对应',
|
||||
'order.platformStoreOrder.createFulfill.orderGoodsTitle': '订单要发的商品',
|
||||
'order.platformStoreOrder.createFulfill.orderGoodsHint': '以店铺订单明细为准,请核对品名与数量',
|
||||
'order.platformStoreOrder.createFulfill.boundTitle': '已绑定的库存 / 包裹',
|
||||
'order.platformStoreOrder.createFulfill.boundHint': '实际从仓发出的资源,请与左侧逐项对照',
|
||||
'order.platformStoreOrder.createFulfill.needQty': '应发数量',
|
||||
'order.platformStoreOrder.createFulfill.pickTitle': '挑选库存 / 包裹',
|
||||
'order.platformStoreOrder.createFulfill.orderQtyShort': '应发 {n}',
|
||||
'order.platformStoreOrder.createFulfill.bindQtyShort': '已绑 {n}',
|
||||
'order.platformStoreOrder.createFulfill.tagQtyDiff': '数量不一致',
|
||||
'order.platformStoreOrder.createFulfill.tagQtyOk': '数量已对齐',
|
||||
'order.platformStoreOrder.createFulfill.confirm': '将创建 {count} 张物流单并绑定代发资源。运输方式:{product};仓库:{warehouse}。确认提交?',
|
||||
'order.platformStoreOrder.createFulfill.submit': '确认创建并绑定',
|
||||
'order.platformStoreOrder.pull.store': '店铺',
|
||||
'order.platformStoreOrder.pull.storePlaceholder': '请选择店铺',
|
||||
'order.platformStoreOrder.pull.status': '类型',
|
||||
@ -628,13 +705,56 @@ export default {
|
||||
'nanaAssistant.open': '打开 Nana 专属顾问',
|
||||
'nanaAssistant.tabAi': 'AI Nana',
|
||||
'nanaAssistant.tabAdvisor': '专属顾问',
|
||||
'nanaAssistant.badge': '物流顾问',
|
||||
'nanaAssistant.welcomeTitle': '这次发货,需要什么帮助?',
|
||||
'nanaAssistant.welcomeSub': '报价、线路、查单、疑难单 — 都可以从这里开始。专属顾问从报价跟到签收。',
|
||||
'nanaAssistant.quickLabel': '常见问题',
|
||||
'nanaAssistant.welcomeMsg': '你好,我是 IDE 物流顾问。可以点选下方问题,或直接输入。',
|
||||
'nanaAssistant.historyTitle': '历史记录',
|
||||
'nanaAssistant.newChat': '新会话',
|
||||
'nanaAssistant.emptyHistory': '暂无会话',
|
||||
'nanaAssistant.searchHistory': '搜索会话或运单号',
|
||||
'nanaAssistant.noSearchResults': '未找到匹配的会话',
|
||||
'nanaAssistant.deleteThread': '删除会话',
|
||||
'nanaAssistant.hint': '以下为常见问题的参考回复,输入框可继续提问。',
|
||||
'nanaAssistant.hintLive': '已接入智能顾问,可直接输入或点选下方问题。',
|
||||
'nanaAssistant.human': '联系人工顾问 →',
|
||||
'nanaAssistant.you': '你',
|
||||
'nanaAssistant.backToChat': '返回对话',
|
||||
'nanaAssistant.quickSmall': '小批量 / 疑难单能发吗?',
|
||||
'nanaAssistant.quickPrice': '到美国 / 欧洲多少钱?',
|
||||
'nanaAssistant.quickQuoteSpeed': '多久出报价?',
|
||||
'nanaAssistant.quickTrack': '查物流单号',
|
||||
'nanaAssistant.quickVisibility': '能实时追踪吗?',
|
||||
'nanaAssistant.quickAfterSales': '出问题了怎么办?',
|
||||
'nanaAssistant.quickHuman': '联系人工顾问',
|
||||
'nanaAssistant.replySmall': '可以。IDE 专注中小卖家和别人不接的单 — 特殊线路、复杂货型、小批量都在服务范围内。告诉我起运地、目的地和货型即可开始。',
|
||||
'nanaAssistant.replyPrice': '价格取决于起运地、重量、尺寸和渠道(空运 / 海运 / 专线)。提供这些信息后,我们会在 24 小时内给出含时效选项的线路报价。',
|
||||
'nanaAssistant.replyQuoteSpeed': '提交货物信息后,通常 24 小时内返回线路报价。若有时效要求,请注明目标发货日期,我们会优先处理。',
|
||||
'nanaAssistant.trackHint': '请在下方输入框粘贴单号,我会说明各节点通常代表什么状态。',
|
||||
'nanaAssistant.replyVisibility': '从揽收到签收有关键节点更新 — 揽收、出口、干线、清关、妥投。异常升级前我们会主动预警。',
|
||||
'nanaAssistant.replyAfterSales': '专属顾问负责异常、理赔与售后跟进,不用自己排队工单。请说明问题,如有单号一并提供。',
|
||||
'nanaAssistant.replyHuman': '请前往联系页 — 电话 18123959245 或邮箱 bd01@1st56.com。说明线路和货型,我们会安排对应顾问。',
|
||||
'nanaAssistant.fallback': '收到。请补充起运地、目的地、货型与重量以便报价;也可联系人工顾问,24 小时内回复。',
|
||||
'nanaAssistant.wechatId': '微信号',
|
||||
'nanaAssistant.noAdvisor': '暂未分配专属顾问',
|
||||
'nanaAssistant.noWechatQr': '暂未上传微信二维码',
|
||||
'nanaAssistant.greeting': '嗨,我是 Nana~,我是你的AI智能客服\n\n无论是查轨迹、了解渠道时效,还是申报价值和清关问题,都可以问我。今天有什么可以帮你的吗?',
|
||||
'nanaAssistant.inputPlaceholder': '输入你的物流问题,Enter 发送',
|
||||
'nanaAssistant.inputPlaceholder': '输入您的问题…',
|
||||
'nanaAssistant.send': '发送',
|
||||
'nanaAssistant.close': '关闭',
|
||||
'nanaAssistant.back': '返回',
|
||||
'nanaAssistant.askAboutWaybill': '咨询物流顾问',
|
||||
'nanaAssistant.actionAdvisor': '问顾问',
|
||||
'nanaAssistant.waybillContext': '当前运单:{no}',
|
||||
'nanaAssistant.viewWaybill': '查看运单 →',
|
||||
'nanaAssistant.queryTrack': '查轨迹',
|
||||
'nanaAssistant.endWaybillConsult': '结束运单咨询',
|
||||
'nanaAssistant.waybillAutoAsk': '请帮我查询运单 {no} 的物流轨迹',
|
||||
'nanaAssistant.trackWaybillDemo': '已收到运单 {no} 的查询。若已配置智能服务,将优先走真实接口;当前为参考回复:一般可在揽收、出口、干线、清关、妥投等节点看到更新。如有异常,专属顾问会主动跟进。',
|
||||
'nanaAssistant.copyWechat': '复制微信号',
|
||||
'nanaAssistant.copyWechatSuccess': '微信号已复制',
|
||||
'nanaAssistant.copyWechatFailed': '复制失败,请手动复制',
|
||||
'nanaAssistant.noProvider': 'AI 服务暂未配置,请联系管理员开启',
|
||||
'nanaAssistant.sendFailed': '发送失败,请稍后重试',
|
||||
'nav.balance.label': '余额',
|
||||
@ -2027,6 +2147,28 @@ export default {
|
||||
'system.dict.search.placeholder.dictNameOrValue': '请输入字典名称或值',
|
||||
'system.dict.search.placeholder': '请输入字典编码或名称',
|
||||
'system.dict.filter.dictType': '字典类型',
|
||||
'system.dict.left.category': '字典类型',
|
||||
'system.dict.left.searchPlaceholder': '搜索类型名称或编码',
|
||||
'system.dict.left.refresh': '刷新类型列表',
|
||||
'system.dict.left.addType': '新增类型',
|
||||
'system.dict.left.editType': '编辑类型',
|
||||
'system.dict.left.deleteType': '删除类型',
|
||||
'system.dict.left.loading': '正在加载字典类型...',
|
||||
'system.dict.left.emptySelect': '请从左侧选择字典类型',
|
||||
'system.dict.left.emptySelectDesc': '选择后可查看并维护该类型下的字典项',
|
||||
'system.dict.left.emptyTypeList': '暂无字典类型,点击右上角 + 新增',
|
||||
'system.dict.panel.itemCount': '共 {count} 项',
|
||||
'system.dict.panel.editType': '编辑类型',
|
||||
'system.dict.form.typeRemark': '中文名称',
|
||||
'system.dict.form.typeSort': '类型排序',
|
||||
'system.dict.placeholder.typeRemark': '请输入中文名称,如:订单状态',
|
||||
'system.dict.typeModal.addTitle': '新增字典类型',
|
||||
'system.dict.typeModal.editTitle': '编辑字典类型',
|
||||
'system.dict.typeModal.addSuccess': '字典类型新增成功',
|
||||
'system.dict.typeModal.editSuccess': '字典类型更新成功',
|
||||
'system.dict.typeModal.deleteSuccess': '字典类型元数据已删除',
|
||||
'system.dict.typeModal.deleteConfirm': '确定删除字典类型【{dictType}】的元数据吗?字典项不会被删除。',
|
||||
'system.dict.message.loadTypeFailed': '加载字典类型失败',
|
||||
'system.dict.actions.add': '新增',
|
||||
'system.dict.actions.edit': '修改',
|
||||
'system.dict.actions.delete': '删除',
|
||||
@ -2166,7 +2308,7 @@ export default {
|
||||
'system.dict.export.failed': '导出失败',
|
||||
'system.dict.batchImport.confirmTitle': '批量导入字典',
|
||||
'system.dict.batchImport.confirmContent':
|
||||
'将按 OMS 设计文档写入预置字典项;已存在的「字典类型+字典编码」会自动跳过。导入顺序:字典类型按字母序,同一类型全部写入后再写下一类型。规则:字典类型可多条共用;同一字典类型下字典编码必须唯一(不同字典类型下可出现相同编码,如均为 0/1)。通用是否类请用 yes_no;启用/禁用类请用 enable_status。是否继续?',
|
||||
'将按 OMS 设计文档写入预置字典项;已存在的「字典类型+字典编码」会自动跳过。导入顺序:字典类型按字母序,同一类型全部写入后再写下一类型。规则:字典类型可多条共用;同一字典类型下字典编码必须唯一(不同字典类型下可出现相同编码,如均为 1/2)。数值 dictCode 从 1 起编,0 表示 None 不写入种子。通用是否类请用 yes_no;启用/禁用类请用 enable_status。是否继续?',
|
||||
'system.dict.batchImport.ok': '开始导入',
|
||||
'system.dict.batchImport.resultDetail': '导入完成:新增 {created} 条,跳过 {skipped} 条,失败 {failed} 条。',
|
||||
'system.dict.batchImport.failed': '批量导入失败:{error}',
|
||||
@ -3226,18 +3368,30 @@ export default {
|
||||
'channel.labelDesign.actions.create': '新建模板',
|
||||
'channel.labelDesign.actions.edit': '设计',
|
||||
'channel.labelDesign.actions.delete': '删除',
|
||||
'channel.labelDesign.actions.refresh': '刷新列表',
|
||||
'channel.labelDesign.actions.back': '返回列表',
|
||||
'channel.labelDesign.actions.preview': '预览',
|
||||
'channel.labelDesign.actions.print': '打印',
|
||||
'channel.labelDesign.actions.save': '保存',
|
||||
'channel.labelDesign.actions.openTemplate': '打开模板',
|
||||
'channel.labelDesign.actions.open': '打开',
|
||||
'channel.labelDesign.actions.startDesign': '开始设计',
|
||||
'channel.labelDesign.actions.cancel': '取消',
|
||||
'channel.labelDesign.loadingList': '正在加载模板…',
|
||||
'channel.labelDesign.emptySelect': '请在左侧选择模板,或新建一个',
|
||||
'channel.labelDesign.openModal.title': '打开系统模板',
|
||||
'channel.labelDesign.openModal.confirmTitle': '切换模板',
|
||||
'channel.labelDesign.openModal.confirmContent': '当前画布未保存的修改将丢失,确定打开所选模板?',
|
||||
'channel.labelDesign.message.loadFailed': '加载模板失败',
|
||||
'channel.labelDesign.message.saveSuccess': '模板已保存',
|
||||
'channel.labelDesign.message.saveFailed': '保存失败',
|
||||
'channel.labelDesign.message.deleteSuccess': '已删除',
|
||||
'channel.labelDesign.message.nameRequired': '请填写模板名称',
|
||||
'channel.labelDesign.message.alreadyOpen': '该模板已在设计器中',
|
||||
'channel.labelDesign.message.openSuccess': '已打开:{name}',
|
||||
'channel.labelDesign.fieldDictionary': '订单字段',
|
||||
'channel.labelDesign.fieldsInjectedHint': '已注入订单字段(已隐藏平台/自定义分组)',
|
||||
'channel.labelDesign.fieldsInjectFailed': '订单字段未能注入设计器,请刷新后重试',
|
||||
'channel.labelDesign.message.fieldCopied': '已复制字段:{field}',
|
||||
'channel.labelDesign.message.previewEmpty': '暂无可预览的模板',
|
||||
'channel.labelDesign.message.previewFailed': '预览 PDF 失败',
|
||||
|
||||
@ -1,14 +1,16 @@
|
||||
/**
|
||||
* OMS 设计文档(oms表结构与接口设计)字典种子数据 + 批量导入。
|
||||
* - 多条记录可共用同一字典类型(dictType);**同一 dictType 下字典编码(dictCode)唯一**,不允许重复。
|
||||
* - 全表业务唯一键为 **dictType + dictCode**;不同 dictType 下可出现相同 dictCode(如多个类型均有 `0`/`1`)。
|
||||
* - 全表业务唯一键为 **dictType + dictCode**;不同 dictType 下可出现相同 dictCode(如多个类型均有 `1`/`2`)。
|
||||
* - 通用是否类:`yes_no`;禁用/启用类:`enable_status`;发布与快照 operator:`operator_type`;
|
||||
* 子账号状态与客户状态编码一致,复用 `customer_status`。
|
||||
* - **数值 dictCode 从 1 起编**;`0` 保留表示 None/未设置,不写入字典种子。
|
||||
*/
|
||||
import * as XLSX from 'xlsx'
|
||||
import { createDict, getDictList, type DictDto } from '@/api/system/dict'
|
||||
import { createDict, getDictListAll, getDictTypeList, saveDictType, type DictDto } from '@/api/system/dict'
|
||||
|
||||
export const DICT_IMPORT_HEADERS = ['字典类型', '字典排序', '字典编码', '字典名称'] as const
|
||||
export const DICT_TYPE_IMPORT_HEADERS = ['字典类型', '类型排序', '类型备注'] as const
|
||||
|
||||
const HEADER_FIELD_MAP: Record<string, keyof DictSeedRow> = {
|
||||
字典类型: 'dictType',
|
||||
@ -30,6 +32,89 @@ export interface DictSeedRow {
|
||||
dictSort: number
|
||||
}
|
||||
|
||||
export interface DictTypeRemarkRow {
|
||||
dictType: string
|
||||
typeSort: number
|
||||
remark: string
|
||||
}
|
||||
|
||||
/** OMS 预置字典类型的中文名称(Remark 字段),供左侧类型菜单展示与批量导入同步 */
|
||||
export const OMS_DICT_TYPE_REMARKS: Record<string, string> = {
|
||||
yes_no: '通用是否',
|
||||
enable_status: '启用状态',
|
||||
order_status: '订单状态',
|
||||
cargo_attribute: '货物属性',
|
||||
order_weight_type: '件重尺类型',
|
||||
order_fee_type: '费用类型',
|
||||
currency: '币种',
|
||||
order_template_type: '订单模板类型',
|
||||
scheme: '排货方案',
|
||||
area_type: '分区类型',
|
||||
operator_type: '发布操作类型',
|
||||
forecast_type: '预报类型',
|
||||
cost_unit: '计费单位',
|
||||
calculation_formula: '计算公式',
|
||||
billable_weight: '计重取值',
|
||||
weight_rule_condition: '计重条件',
|
||||
sales_product_group: '卖价分组',
|
||||
customer_waybill: '客户面单',
|
||||
label_template_type: '面单模板类型',
|
||||
number_type: '打印号码类型',
|
||||
auto_trigger_timing: '自动触发时机',
|
||||
auto_trigger_operation: '自动触发操作',
|
||||
pricing_factor: '定价因素',
|
||||
customer_status: '客户状态',
|
||||
customer_group: '客户组',
|
||||
recharge_status: '充值审核状态',
|
||||
pay_type: '支付方式',
|
||||
principal_type: '主体类型',
|
||||
notice_type: '通知类型',
|
||||
receiver_type: '接收方类型',
|
||||
warehouse_type: '仓库类型',
|
||||
es_snapshot_status: '快照生效状态',
|
||||
publish_record_type: '发布记录类型',
|
||||
order_task_status: '订单导入任务状态',
|
||||
export_customs_mode: '出口报关方式',
|
||||
fee_item: '费用项',
|
||||
order_handle_status: '订单处理子状态',
|
||||
order_source: '订单来源',
|
||||
issue_type: '问题件类型',
|
||||
issue_notify_method: '问题通知方式',
|
||||
claim_type: '理赔类型',
|
||||
order_fee_charging_stage: '费用计费阶段',
|
||||
customer_address_type: '客户地址类型',
|
||||
label_generate_status: '面单生成状态',
|
||||
order_stock_bind_type: '库存绑定类型',
|
||||
order_stock_bind_status: '库存绑定状态',
|
||||
warehouse_inbound_type: '入库类型',
|
||||
warehouse_receipt_status: '收货状态',
|
||||
warehouse_parcel_lifecycle: '包裹生命周期',
|
||||
sales_store_auth_status: '店铺授权状态',
|
||||
sales_store_fulfill_status: '平台店铺履约状态',
|
||||
order_batch_job_status: '批量任务状态',
|
||||
order_batch_job_item_status: '批量任务明细状态',
|
||||
order_template_mapping_status: '模板映射状态',
|
||||
customer_account_flow_biz_type: '账户流水业务类型',
|
||||
secret_level: '密级',
|
||||
data_scope: '数据范围',
|
||||
org_scope: '组织范围',
|
||||
}
|
||||
|
||||
/** 按预置种子中的 dictType 生成类型备注行(含排序) */
|
||||
export function getOmsDictTypeRemarkRows(
|
||||
seedRows: DictSeedRow[] = OMS_DICT_BATCH_SEED,
|
||||
remarks: Record<string, string> = OMS_DICT_TYPE_REMARKS,
|
||||
): DictTypeRemarkRow[] {
|
||||
const types = [...new Set(seedRows.map((row) => row.dictType.trim()).filter(Boolean))].sort((a, b) =>
|
||||
a.localeCompare(b, 'en', { sensitivity: 'base' }),
|
||||
)
|
||||
return types.map((dictType, index) => ({
|
||||
dictType,
|
||||
typeSort: index + 1,
|
||||
remark: remarks[dictType] ?? '',
|
||||
}))
|
||||
}
|
||||
|
||||
function pack(dictType: string, pairs: [string, string][]): DictSeedRow[] {
|
||||
return pairs.map(([dictCode, dictName], i) => ({
|
||||
dictType,
|
||||
@ -42,8 +127,8 @@ function pack(dictType: string, pairs: [string, string][]): DictSeedRow[] {
|
||||
/** 与设计文档附录及表字段说明对齐;可按需增删后重新导入 */
|
||||
export const OMS_DICT_BATCH_SEED: DictSeedRow[] = [
|
||||
...pack('yes_no', [
|
||||
['0', '否'],
|
||||
['1', '是'],
|
||||
['1', '否'],
|
||||
['2', '是'],
|
||||
]),
|
||||
...pack('order_status', [
|
||||
['draft', '草稿'],
|
||||
@ -92,7 +177,7 @@ export const OMS_DICT_BATCH_SEED: DictSeedRow[] = [
|
||||
...pack('area_type', [
|
||||
['blacklist', '黑名单区'],
|
||||
['remote', '偏远分区'],
|
||||
['price', '价格分区'],
|
||||
['warning', '警告区'],
|
||||
]),
|
||||
/** 发布记录 OperatorType、ES 快照 operatorType 等均可复用 */
|
||||
...pack('operator_type', [
|
||||
@ -114,10 +199,10 @@ export const OMS_DICT_BATCH_SEED: DictSeedRow[] = [
|
||||
['unit', '单价'],
|
||||
['continue_unit', '续单价'],
|
||||
]),
|
||||
/** 卖价项/买价项启用、认证账号状态等:0-禁用 1-启用 */
|
||||
/** 卖价项/买价项启用、认证账号状态等:1-禁用 2-启用(0=None 不落库) */
|
||||
...pack('enable_status', [
|
||||
['0', '禁用'],
|
||||
['1', '启用'],
|
||||
['1', '禁用'],
|
||||
['2', '启用'],
|
||||
]),
|
||||
...pack('billable_weight', [
|
||||
['vw', '体积重'],
|
||||
@ -152,6 +237,9 @@ export const OMS_DICT_BATCH_SEED: DictSeedRow[] = [
|
||||
['customerOrder', '客户单号'],
|
||||
['tracking', '跟踪号'],
|
||||
['other', '其他单号'],
|
||||
['parcel', '包裹跟踪号'],
|
||||
['sku', 'SKU'],
|
||||
['auto', '自动识别'],
|
||||
]),
|
||||
...pack('auto_trigger_operation', [
|
||||
['intercept', '拦截'],
|
||||
@ -170,24 +258,24 @@ export const OMS_DICT_BATCH_SEED: DictSeedRow[] = [
|
||||
['warehouse', '仓库'],
|
||||
['customer', '客户'],
|
||||
]),
|
||||
/** 客户主表 Status;子账号 SubStatus 编码一致(0/1),可共用本类型 */
|
||||
/** 客户主表 Status;子账号 SubStatus 编码一致,可共用本类型(1 起编) */
|
||||
...pack('customer_status', [
|
||||
['0', '禁用'],
|
||||
['1', '正常'],
|
||||
['2', '冻结'],
|
||||
['1', '禁用'],
|
||||
['2', '正常'],
|
||||
['3', '冻结'],
|
||||
]),
|
||||
...pack('customer_group', [
|
||||
['0', '普通客户'],
|
||||
['1', 'VIP1'],
|
||||
['2', 'VIP2'],
|
||||
['3', 'VIP3'],
|
||||
['4', 'VIP4'],
|
||||
['5', 'VIP5'],
|
||||
['1', '普通客户'],
|
||||
['2', 'VIP1'],
|
||||
['3', 'VIP2'],
|
||||
['4', 'VIP3'],
|
||||
['5', 'VIP4'],
|
||||
['6', 'VIP5'],
|
||||
]),
|
||||
...pack('recharge_status', [
|
||||
['0', '待审核'],
|
||||
['1', '审核通过'],
|
||||
['2', '审核驳回'],
|
||||
['1', '待审核'],
|
||||
['2', '审核通过'],
|
||||
['3', '审核驳回'],
|
||||
]),
|
||||
...pack('pay_type', [
|
||||
['1', '微信'],
|
||||
@ -224,6 +312,147 @@ export const OMS_DICT_BATCH_SEED: DictSeedRow[] = [
|
||||
['processing', '处理中'],
|
||||
['completed', '已完成'],
|
||||
]),
|
||||
...pack('export_customs_mode', [
|
||||
['9610Simple', '9610简易申报'],
|
||||
['9610Summary', '9610汇总申报'],
|
||||
['9610Separate', '9610分离申报'],
|
||||
['9710', '9710'],
|
||||
['9810', '9810'],
|
||||
['0110', '0110'],
|
||||
]),
|
||||
/** 费用项:落库 CostItem 字段,编码与渠道/卖价配置一致 */
|
||||
...pack('fee_item', [
|
||||
['freight', '运费'],
|
||||
['handling', '操作费'],
|
||||
['insurance', '保险费'],
|
||||
['fuelSurcharge', '燃油附加费'],
|
||||
]),
|
||||
...pack('order_handle_status', [
|
||||
['pending', '待处理'],
|
||||
['processed', '已处理'],
|
||||
]),
|
||||
...pack('order_source', [
|
||||
['manual', '手工录入'],
|
||||
['pickup', '揽收'],
|
||||
['upload', '上传'],
|
||||
['batch', '批量'],
|
||||
['platform', '平台店铺'],
|
||||
]),
|
||||
...pack('issue_type', [
|
||||
['address', '地址异常'],
|
||||
['customs', '清关问题'],
|
||||
['weight', '重量异常'],
|
||||
['document', '单证不全'],
|
||||
['cargo', '货物异常'],
|
||||
['channel', '渠道异常'],
|
||||
['payment', '费用异常'],
|
||||
['other', '其他'],
|
||||
]),
|
||||
...pack('issue_notify_method', [
|
||||
['inApp', '站内信'],
|
||||
['email', '邮件'],
|
||||
['sms', '短信'],
|
||||
['feishu', '飞书'],
|
||||
['wechat', '微信'],
|
||||
]),
|
||||
...pack('claim_type', [
|
||||
['lost', '丢件'],
|
||||
['damaged', '破损'],
|
||||
['delay', '延误'],
|
||||
['customs', '清关理赔'],
|
||||
['fee', '费用争议'],
|
||||
['other', '其他'],
|
||||
]),
|
||||
...pack('order_fee_charging_stage', [
|
||||
['forecast', '预报'],
|
||||
['inbound', '入库'],
|
||||
['outbound', '出库'],
|
||||
]),
|
||||
...pack('customer_address_type', [
|
||||
['sender', '发件'],
|
||||
['return', '退件'],
|
||||
['receiver', '收件'],
|
||||
]),
|
||||
...pack('label_generate_status', [
|
||||
['Pending', '待生成'],
|
||||
['Ready', '已就绪'],
|
||||
['Failed', '生成失败'],
|
||||
]),
|
||||
...pack('order_stock_bind_type', [
|
||||
['inventory', '库存'],
|
||||
['parcelItem', '包裹明细'],
|
||||
]),
|
||||
...pack('order_stock_bind_status', [
|
||||
['bound', '已绑定'],
|
||||
['deducted', '已扣减'],
|
||||
['released', '已释放'],
|
||||
]),
|
||||
...pack('warehouse_inbound_type', [
|
||||
['normal', '正常入库'],
|
||||
['return', '退货入库'],
|
||||
]),
|
||||
...pack('warehouse_receipt_status', [
|
||||
['forecasted', '已预报'],
|
||||
['received', '已收货'],
|
||||
['completed', '已完成'],
|
||||
]),
|
||||
...pack('warehouse_parcel_lifecycle', [
|
||||
['inTransit', '在途'],
|
||||
['arrived', '已到仓'],
|
||||
['stored', '已上架'],
|
||||
]),
|
||||
...pack('sales_store_auth_status', [
|
||||
['active', '有效'],
|
||||
['expired', '已过期'],
|
||||
['revoked', '已撤销'],
|
||||
]),
|
||||
...pack('sales_store_fulfill_status', [
|
||||
['pending', '待处理'],
|
||||
['toShip', '待发货'],
|
||||
['canceled', '已取消'],
|
||||
['suspended', '挂起'],
|
||||
['shipped', '已发货'],
|
||||
['completed', '已完成'],
|
||||
]),
|
||||
...pack('order_batch_job_status', [
|
||||
['pending', '待处理'],
|
||||
['running', '执行中'],
|
||||
['completed', '已完成'],
|
||||
['failed', '失败'],
|
||||
['cancelled', '已取消'],
|
||||
]),
|
||||
...pack('order_batch_job_item_status', [
|
||||
['pending', '待处理'],
|
||||
['running', '执行中'],
|
||||
['success', '成功'],
|
||||
['failed', '失败'],
|
||||
['cancelled', '已取消'],
|
||||
]),
|
||||
...pack('order_template_mapping_status', [
|
||||
['draft', '草稿'],
|
||||
['ready', '就绪'],
|
||||
]),
|
||||
...pack('customer_account_flow_biz_type', [
|
||||
['recharge', '充值入账'],
|
||||
['orderPay', '订单支付'],
|
||||
]),
|
||||
...pack('secret_level', [
|
||||
['1', '公开'],
|
||||
['2', '内部'],
|
||||
['3', '机密'],
|
||||
['4', '绝密'],
|
||||
]),
|
||||
...pack('data_scope', [
|
||||
['1', '个人'],
|
||||
['2', '小组'],
|
||||
['3', '部门'],
|
||||
['4', '组织树'],
|
||||
['5', '全局'],
|
||||
]),
|
||||
...pack('org_scope', [
|
||||
['1', '仅本级'],
|
||||
['2', '本级及下级'],
|
||||
]),
|
||||
]
|
||||
|
||||
/** 与库表唯一约束一致:dictType + dictCode */
|
||||
@ -262,24 +491,25 @@ export function sortRowsByDictTypeAlphabetical(rows: DictSeedRow[]): DictSeedRow
|
||||
|
||||
function isDuplicateError(err: unknown): boolean {
|
||||
const msg = err instanceof Error ? err.message : String(err)
|
||||
return /重复|已存在|唯一|duplicate|unique|exist/i.test(msg)
|
||||
return /重复|已存在|唯一|duplicate|unique|exist|1062|Duplicate entry/i.test(msg)
|
||||
}
|
||||
|
||||
function normalizeDictRowKey(row: DictDto): { dictType: string; dictCode: string } | null {
|
||||
const dictType = String(row.dictType ?? (row as Record<string, unknown>).DictType ?? '').trim()
|
||||
const dictCode = String(row.dictCode ?? (row as Record<string, unknown>).DictCode ?? '').trim()
|
||||
if (!dictType || !dictCode) return null
|
||||
return { dictType, dictCode }
|
||||
}
|
||||
|
||||
async function fetchExistingDictKeys(): Promise<Set<string>> {
|
||||
const set = new Set<string>()
|
||||
let page = 1
|
||||
const pageSize = 500
|
||||
for (;;) {
|
||||
const { items, total } = await getDictList({ page, pageSize })
|
||||
const list: DictDto[] = items || []
|
||||
for (const row of list) {
|
||||
if (row.dictType != null && row.dictCode != null && row.dictCode !== '') {
|
||||
set.add(dictKey({ dictType: String(row.dictType), dictCode: String(row.dictCode) }))
|
||||
const rows = await getDictListAll()
|
||||
for (const row of rows) {
|
||||
const normalized = normalizeDictRowKey(row)
|
||||
if (normalized) {
|
||||
set.add(dictKey(normalized))
|
||||
}
|
||||
}
|
||||
if (list.length < pageSize || page * pageSize >= total) break
|
||||
page += 1
|
||||
}
|
||||
return set
|
||||
}
|
||||
|
||||
@ -288,6 +518,62 @@ export interface BatchImportResult {
|
||||
skipped: number
|
||||
failed: number
|
||||
errors: string[]
|
||||
/** 本次同步写入的类型备注数量(仅补空备注,不覆盖已有) */
|
||||
typeRemarksSynced?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 为字典类型补全中文备注(仅当库中该类型无备注时写入,避免覆盖人工维护内容)。
|
||||
*/
|
||||
export async function syncDictTypeRemarks(
|
||||
dictTypes: string[],
|
||||
remarks: Record<string, string> = OMS_DICT_TYPE_REMARKS,
|
||||
): Promise<{ synced: number; errors: string[] }> {
|
||||
const uniqueTypes = [...new Set(dictTypes.map((t) => t.trim()).filter(Boolean))].sort((a, b) =>
|
||||
a.localeCompare(b, 'en', { sensitivity: 'base' }),
|
||||
)
|
||||
if (!uniqueTypes.length) {
|
||||
return { synced: 0, errors: [] }
|
||||
}
|
||||
|
||||
const existing = await getDictTypeList()
|
||||
const existingMap = new Map(
|
||||
existing.map((item) => [String(item.dictType ?? '').trim(), item]),
|
||||
)
|
||||
const sortSeed = getOmsDictTypeRemarkRows()
|
||||
const sortMap = new Map(sortSeed.map((row) => [row.dictType, row.typeSort]))
|
||||
|
||||
let synced = 0
|
||||
const errors: string[] = []
|
||||
for (const dictType of uniqueTypes) {
|
||||
const remark = remarks[dictType]?.trim()
|
||||
if (!remark) continue
|
||||
|
||||
const meta = existingMap.get(dictType)
|
||||
if (meta?.remark?.trim()) {
|
||||
continue
|
||||
}
|
||||
|
||||
try {
|
||||
await saveDictType({
|
||||
id: meta?.id,
|
||||
dictType,
|
||||
remark,
|
||||
typeSort: meta?.typeSort ?? sortMap.get(dictType) ?? 0,
|
||||
})
|
||||
synced += 1
|
||||
if (meta) {
|
||||
meta.remark = remark
|
||||
} else {
|
||||
existingMap.set(dictType, { dictType, remark })
|
||||
}
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : String(e)
|
||||
errors.push(`${dictType}: ${msg}`)
|
||||
}
|
||||
}
|
||||
|
||||
return { synced, errors }
|
||||
}
|
||||
|
||||
/**
|
||||
@ -301,8 +587,15 @@ export async function runOmsDictBatchImport(
|
||||
): Promise<BatchImportResult> {
|
||||
assertSeedUniqueDictCodePerType(rows)
|
||||
const ordered = sortRowsByDictTypeAlphabetical(rows)
|
||||
const typeSync = await syncDictTypeRemarks([...new Set(ordered.map((row) => row.dictType))])
|
||||
const existing = await fetchExistingDictKeys()
|
||||
const result: BatchImportResult = { created: 0, skipped: 0, failed: 0, errors: [] }
|
||||
const result: BatchImportResult = {
|
||||
created: 0,
|
||||
skipped: 0,
|
||||
failed: 0,
|
||||
errors: [...typeSync.errors],
|
||||
typeRemarksSynced: typeSync.synced,
|
||||
}
|
||||
const total = ordered.length
|
||||
let i = 0
|
||||
for (const row of ordered) {
|
||||
@ -442,6 +735,14 @@ function dictRowsToSheetRows(rows: DictSeedRow[]) {
|
||||
}))
|
||||
}
|
||||
|
||||
function dictTypeRowsToSheetRows(rows: DictTypeRemarkRow[]) {
|
||||
return rows.map((row) => ({
|
||||
字典类型: row.dictType,
|
||||
类型排序: row.typeSort,
|
||||
类型备注: row.remark,
|
||||
}))
|
||||
}
|
||||
|
||||
function writeDictWorkbook(rows: DictSeedRow[], sheetName = '字典') {
|
||||
const wb = XLSX.utils.book_new()
|
||||
const sheet = XLSX.utils.json_to_sheet(dictRowsToSheetRows(rows), {
|
||||
@ -451,6 +752,16 @@ function writeDictWorkbook(rows: DictSeedRow[], sheetName = '字典') {
|
||||
return wb
|
||||
}
|
||||
|
||||
function writePresetDictWorkbook(rows: DictSeedRow[]) {
|
||||
const wb = writeDictWorkbook(rows, '预置字典')
|
||||
const typeRows = getOmsDictTypeRemarkRows(rows)
|
||||
const typeSheet = XLSX.utils.json_to_sheet(dictTypeRowsToSheetRows(typeRows), {
|
||||
header: [...DICT_TYPE_IMPORT_HEADERS],
|
||||
})
|
||||
XLSX.utils.book_append_sheet(wb, typeSheet, '字典类型')
|
||||
return wb
|
||||
}
|
||||
|
||||
/** 下载空白导入模板(含示例行) */
|
||||
export function downloadDictImportTemplate() {
|
||||
const wb = writeDictWorkbook([
|
||||
@ -458,15 +769,15 @@ export function downloadDictImportTemplate() {
|
||||
dictType: 'yes_no',
|
||||
dictSort: 1,
|
||||
dictCode: '1',
|
||||
dictName: '是',
|
||||
dictName: '否',
|
||||
},
|
||||
])
|
||||
XLSX.writeFile(wb, '数据字典导入模板.xlsx')
|
||||
}
|
||||
|
||||
/** 下载 OMS 设计文档预置字典(可编辑后重新导入) */
|
||||
/** 下载 OMS 设计文档预置字典(含「字典类型」备注 sheet,可编辑后重新导入) */
|
||||
export function downloadDictPresetTemplate() {
|
||||
const wb = writeDictWorkbook(sortRowsByDictTypeAlphabetical(OMS_DICT_BATCH_SEED), '预置字典')
|
||||
const wb = writePresetDictWorkbook(sortRowsByDictTypeAlphabetical(OMS_DICT_BATCH_SEED))
|
||||
XLSX.writeFile(wb, 'OMS预置字典模板.xlsx')
|
||||
}
|
||||
|
||||
|
||||
@ -1,8 +1,84 @@
|
||||
<template>
|
||||
<div class="dict-container">
|
||||
<div class="dict-card">
|
||||
<div class="page-header">
|
||||
<a-space>
|
||||
<div class="rbac-split-layout dict-split-layout">
|
||||
<CustomLeftMenu
|
||||
class="left-sidebar dict-left-menu"
|
||||
:tree-data="dictTypeTreeData"
|
||||
:selected-keys="selectedTypeKeys"
|
||||
v-model:expanded-keys="expandedKeys"
|
||||
v-model:search-keyword="typeSearchKeyword"
|
||||
:loading="typeLoading"
|
||||
:search-placeholder="t('system.dict.left.searchPlaceholder')"
|
||||
:refresh-title="t('system.dict.left.refresh')"
|
||||
:add-title="t('system.dict.left.addType')"
|
||||
:delete-label="t('system.dict.left.deleteType')"
|
||||
:loading-text="t('system.dict.left.loading')"
|
||||
:show-method="false"
|
||||
:show-node-index="false"
|
||||
:show-map-count="true"
|
||||
:show-node-code="true"
|
||||
:show-node-actions="true"
|
||||
:show-pagination="true"
|
||||
:page-size="30"
|
||||
@search="handleTypeSearch"
|
||||
@refresh="() => loadDictTypes(undefined, { loadDetails: false })"
|
||||
@add="handleAddType"
|
||||
@select="handleTypeSelect"
|
||||
@node-click="handleTypeNodeClick"
|
||||
>
|
||||
<template #node-menu="{ nodeData }">
|
||||
<a-doption @click="handleEditType(nodeData)">
|
||||
<icon-edit />
|
||||
{{ t('system.dict.left.editType') }}
|
||||
</a-doption>
|
||||
<a-doption
|
||||
v-if="nodeData?.id"
|
||||
@click="handleDeleteType(nodeData)"
|
||||
>
|
||||
<icon-delete />
|
||||
{{ t('system.dict.left.deleteType') }}
|
||||
</a-doption>
|
||||
</template>
|
||||
</CustomLeftMenu>
|
||||
|
||||
<div class="right-panel">
|
||||
<div class="dict-page-panel">
|
||||
<div v-if="!selectedDictType" class="panel-empty">
|
||||
<a-empty :description="t('system.dict.left.emptySelect')">
|
||||
<template #image>
|
||||
<icon-book class="panel-empty-icon" />
|
||||
</template>
|
||||
<p class="panel-empty-desc">{{ t('system.dict.left.emptySelectDesc') }}</p>
|
||||
</a-empty>
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<div class="dict-type-header">
|
||||
<div class="dict-type-header-main">
|
||||
<h3 class="panel-title">{{ displayTypeName }}</h3>
|
||||
<a-tag size="small" color="arcoblue" class="type-code-tag">{{ selectedDictType }}</a-tag>
|
||||
<span class="type-item-count">
|
||||
{{ t('system.dict.panel.itemCount', { count: selectedTypeMeta?.itemCount ?? pagination.total }) }}
|
||||
</span>
|
||||
</div>
|
||||
<a-button type="text" size="small" class="edit-type-btn" @click="handleEditSelectedType">
|
||||
<template #icon><icon-edit /></template>
|
||||
{{ t('system.dict.panel.editType') }}
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<div class="table-toolbar">
|
||||
<div class="toolbar-left">
|
||||
<a-input-search
|
||||
v-model="query.keyword"
|
||||
allow-clear
|
||||
:placeholder="t('system.dict.search.placeholder')"
|
||||
style="width: 240px"
|
||||
@search="handleSearch"
|
||||
@clear="handleSearch"
|
||||
/>
|
||||
</div>
|
||||
<div class="toolbar-right">
|
||||
<a-space wrap :size="8">
|
||||
<a-button type="primary" @click="handleAdd">
|
||||
<template #icon><icon-plus /></template>
|
||||
{{ t('system.dict.actions.add') }}
|
||||
@ -15,22 +91,6 @@
|
||||
<template #icon><icon-download /></template>
|
||||
{{ t('system.dict.actions.batchExport') }}
|
||||
</a-button>
|
||||
</a-space>
|
||||
|
||||
<a-space>
|
||||
<a-input
|
||||
v-model="query.dictType"
|
||||
:placeholder="t('system.dict.filter.dictType')"
|
||||
style="width: 180px"
|
||||
allow-clear
|
||||
@change="handleSearch"
|
||||
/>
|
||||
<a-input-search
|
||||
v-model="query.keyword"
|
||||
:placeholder="t('system.dict.search.placeholder')"
|
||||
style="width: 240px"
|
||||
@search="handleSearch"
|
||||
/>
|
||||
<a-button @click="loadData">
|
||||
<template #icon><icon-refresh /></template>
|
||||
</a-button>
|
||||
@ -43,8 +103,10 @@
|
||||
/>
|
||||
</a-space>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ref="tableContainerRef" class="list-wrap">
|
||||
<a-spin :loading="listLoading" class="list-spin">
|
||||
<HandsontablePro
|
||||
:columns="hotColumns"
|
||||
:data="dataSource"
|
||||
@ -61,6 +123,9 @@
|
||||
:read-only="true"
|
||||
@action-click="handleActionClick"
|
||||
/>
|
||||
</a-spin>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -73,7 +138,7 @@
|
||||
>
|
||||
<a-form ref="formRef" :model="formState" :rules="formRules" :label-col-props="{ span: 6 }" :wrapper-col-props="{ span: 16 }">
|
||||
<a-form-item :label="t('system.dict.form.dictType')" field="dictType">
|
||||
<a-input v-model="formState.dictType" :placeholder="t('system.dict.placeholder.input')" />
|
||||
<a-input v-model="formState.dictType" disabled :placeholder="t('system.dict.placeholder.input')" />
|
||||
</a-form-item>
|
||||
<a-form-item :label="t('system.dict.form.dictSort')" field="dictSort">
|
||||
<a-input-number v-model="formState.dictSort" :placeholder="t('system.dict.placeholder.input')" :min="0" :precision="0" style="width: 100%" />
|
||||
@ -87,7 +152,31 @@
|
||||
</a-form>
|
||||
</a-modal>
|
||||
|
||||
<DictBatchImportModal ref="batchImportModalRef" @success="loadData" />
|
||||
<a-modal
|
||||
v-model:visible="typeModalVisible"
|
||||
:title="typeIsEdit ? t('system.dict.typeModal.editTitle') : t('system.dict.typeModal.addTitle')"
|
||||
:confirm-loading="typeModalLoading"
|
||||
width="520px"
|
||||
@ok="handleTypeModalOk"
|
||||
>
|
||||
<a-form ref="typeFormRef" :model="typeFormState" :rules="typeFormRules" :label-col-props="{ span: 6 }" :wrapper-col-props="{ span: 16 }">
|
||||
<a-form-item :label="t('system.dict.form.dictType')" field="dictType">
|
||||
<a-input
|
||||
v-model="typeFormState.dictType"
|
||||
:disabled="typeIsEdit"
|
||||
:placeholder="t('system.dict.placeholder.input')"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item :label="t('system.dict.form.typeRemark')" field="remark">
|
||||
<a-input v-model="typeFormState.remark" allow-clear :placeholder="t('system.dict.placeholder.typeRemark')" />
|
||||
</a-form-item>
|
||||
<a-form-item :label="t('system.dict.form.typeSort')" field="typeSort">
|
||||
<a-input-number v-model="typeFormState.typeSort" :min="0" :precision="0" style="width: 100%" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
|
||||
<DictBatchImportModal ref="batchImportModalRef" @success="handleImportSuccess" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -96,27 +185,49 @@ import { computed, reactive, ref, onMounted } from 'vue'
|
||||
import dayjs from 'dayjs'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { Message, Modal } from '@arco-design/web-vue'
|
||||
import { IconPlus, IconRefresh, IconUpload, IconDownload } from '@arco-design/web-vue/es/icon'
|
||||
import {
|
||||
IconPlus,
|
||||
IconRefresh,
|
||||
IconUpload,
|
||||
IconDownload,
|
||||
IconEdit,
|
||||
IconDelete,
|
||||
IconBook,
|
||||
} from '@arco-design/web-vue/es/icon'
|
||||
import HandsontablePro from '@/components/HandsontablePro/index.vue'
|
||||
import PaginationPro from '@/components/PaginationPro/index.vue'
|
||||
import CustomLeftMenu from '@/components/CustomLeftMenu/index.vue'
|
||||
import type { CustomLeftMenuTreeNode } from '@/components/CustomLeftMenu/types'
|
||||
import { useTableContainerHeight } from '@/composables/useTableContainerHeight'
|
||||
import {
|
||||
createDict,
|
||||
deleteDict,
|
||||
deleteDictType,
|
||||
getDictList,
|
||||
getDictListAll,
|
||||
getDictTypeList,
|
||||
saveDictType,
|
||||
updateDict,
|
||||
type DictDto,
|
||||
type DictTypeDto,
|
||||
} from '@/api/system/dict'
|
||||
import { exportDictRowsToExcel } from './dict-batch-import'
|
||||
import DictBatchImportModal from './DictBatchImportModal.vue'
|
||||
|
||||
defineOptions({ name: 'SystemDict' })
|
||||
|
||||
const { t } = useI18n()
|
||||
const { tableContainerRef, tableHeight } = useTableContainerHeight()
|
||||
const handsontableHeight = computed(() => Math.max(tableHeight.value || 0, 400))
|
||||
|
||||
const typeLoading = ref(false)
|
||||
const listLoading = ref(false)
|
||||
const dictTypeList = ref<DictTypeDto[]>([])
|
||||
const typeSearchKeyword = ref('')
|
||||
const selectedDictType = ref('')
|
||||
const expandedKeys = ref<string[]>([])
|
||||
|
||||
const dataSource = ref<DictDto[]>([])
|
||||
/** 筛选条件下的全量排序结果,翻页只切片不重请求 */
|
||||
const allSortedRows = ref<DictDto[]>([])
|
||||
const batchImportModalRef = ref<InstanceType<typeof DictBatchImportModal> | null>(null)
|
||||
const exportLoading = ref(false)
|
||||
const modalVisible = ref(false)
|
||||
@ -125,14 +236,18 @@ const isEdit = ref(false)
|
||||
const editingId = ref<number | string>()
|
||||
const formRef = ref()
|
||||
|
||||
const typeModalVisible = ref(false)
|
||||
const typeModalLoading = ref(false)
|
||||
const typeIsEdit = ref(false)
|
||||
const typeFormRef = ref()
|
||||
|
||||
const query = reactive({
|
||||
dictType: '',
|
||||
keyword: '',
|
||||
})
|
||||
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 50,
|
||||
pageSize: 30,
|
||||
total: 0,
|
||||
})
|
||||
|
||||
@ -143,6 +258,13 @@ const formState = reactive({
|
||||
dictName: '',
|
||||
})
|
||||
|
||||
const typeFormState = reactive({
|
||||
id: undefined as number | string | undefined,
|
||||
dictType: '',
|
||||
typeSort: 0,
|
||||
remark: '',
|
||||
})
|
||||
|
||||
const formRules = computed(() => ({
|
||||
dictType: [{ required: true, message: t('system.dict.rule.dictTypeRequired') }],
|
||||
dictSort: [{ required: true, message: t('system.dict.rule.dictSortRequired') }],
|
||||
@ -150,34 +272,75 @@ const formRules = computed(() => ({
|
||||
dictName: [{ required: true, message: t('system.dict.rule.dictNameRequired') }],
|
||||
}))
|
||||
|
||||
const typeFormRules = computed(() => ({
|
||||
dictType: [{ required: true, message: t('system.dict.rule.dictTypeRequired') }],
|
||||
}))
|
||||
|
||||
const selectedTypeMeta = computed(() =>
|
||||
dictTypeList.value.find((item) => item.dictType === selectedDictType.value),
|
||||
)
|
||||
|
||||
const displayTypeName = computed(
|
||||
() => selectedTypeMeta.value?.remark?.trim() || selectedDictType.value,
|
||||
)
|
||||
|
||||
const filteredTypeList = computed(() => {
|
||||
const kw = typeSearchKeyword.value.trim().toLowerCase()
|
||||
if (!kw) return dictTypeList.value
|
||||
return dictTypeList.value.filter((item) => {
|
||||
const type = String(item.dictType ?? '').toLowerCase()
|
||||
const remark = String(item.remark ?? '').toLowerCase()
|
||||
return type.includes(kw) || remark.includes(kw)
|
||||
})
|
||||
})
|
||||
|
||||
function buildTypeTreeNode(item: DictTypeDto): CustomLeftMenuTreeNode {
|
||||
const dictType = String(item.dictType ?? '').trim()
|
||||
const remark = String(item.remark ?? '').trim()
|
||||
return {
|
||||
key: `type-${dictType}`,
|
||||
title: remark || dictType,
|
||||
code: remark ? dictType : undefined,
|
||||
mapCount: item.itemCount ?? 0,
|
||||
isLeaf: true,
|
||||
id: item.id,
|
||||
dictType,
|
||||
remark,
|
||||
typeSort: item.typeSort,
|
||||
}
|
||||
}
|
||||
|
||||
const dictTypeTreeData = computed<CustomLeftMenuTreeNode[]>(() =>
|
||||
filteredTypeList.value.map((item) => buildTypeTreeNode(item)),
|
||||
)
|
||||
|
||||
const selectedTypeKeys = computed(() =>
|
||||
selectedDictType.value ? [`type-${selectedDictType.value}`] : [],
|
||||
)
|
||||
|
||||
const hotColumns = computed(() => [
|
||||
{ key: 'dictType', label: t('system.dict.table.dictType'), width: 160 },
|
||||
{ key: 'dictSort', label: t('system.dict.table.dictSort'), width: 100 },
|
||||
{ key: 'dictCode', label: t('system.dict.table.dictCode'), width: 180 },
|
||||
{ key: 'dictName', label: t('system.dict.table.dictName'), width: 180 },
|
||||
{ key: 'dictName', label: t('system.dict.table.dictName'), width: 200 },
|
||||
{ key: 'createTime', label: t('system.dict.table.createTime'), width: 170 },
|
||||
])
|
||||
|
||||
function sortDictRows(rows: DictDto[]): DictDto[] {
|
||||
return [...rows].sort((a, b) => {
|
||||
const ta = String(a.dictType ?? '').trim()
|
||||
const tb = String(b.dictType ?? '').trim()
|
||||
const c = ta.localeCompare(tb, 'en', { sensitivity: 'base' })
|
||||
if (c !== 0) return c
|
||||
const sa = Number(a.dictSort ?? 0)
|
||||
const sb = Number(b.dictSort ?? 0)
|
||||
if (sa !== sb) return sa - sb
|
||||
return String(a.dictCode ?? '').localeCompare(String(b.dictCode ?? ''), 'en')
|
||||
})
|
||||
function syncSelectedTypeItemCount(count: number) {
|
||||
if (!selectedDictType.value) return
|
||||
const idx = dictTypeList.value.findIndex((item) => item.dictType === selectedDictType.value)
|
||||
if (idx < 0) return
|
||||
dictTypeList.value[idx] = { ...dictTypeList.value[idx], itemCount: count }
|
||||
}
|
||||
|
||||
function syncTablePage() {
|
||||
const total = allSortedRows.value.length
|
||||
pagination.total = total
|
||||
const maxPage = Math.max(1, Math.ceil(total / pagination.pageSize) || 1)
|
||||
if (pagination.current > maxPage) pagination.current = maxPage
|
||||
const start = (pagination.current - 1) * pagination.pageSize
|
||||
dataSource.value = allSortedRows.value.slice(start, start + pagination.pageSize)
|
||||
function handlePageChange(page: number) {
|
||||
pagination.current = page
|
||||
loadData()
|
||||
}
|
||||
|
||||
function handlePageSizeChange(size: number) {
|
||||
pagination.pageSize = size
|
||||
pagination.current = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
const actionEditIcon = '<svg viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="2" style="width:16px;height:16px"><path d="m30.48 19.038 5.733-5.734a1 1 0 0 0 0-1.414l-5.586-5.586a1 1 0 0 0-1.414 0l-5.734 5.734m7 7L15.763 33.754a1 1 0 0 1-.59.286l-6.048.708a1 1 0 0 1-1.113-1.069l.477-6.31a1 1 0 0 1 .29-.631l14.7-14.7m7 7-7-7M6 42h36"/></svg>'
|
||||
@ -193,7 +356,7 @@ function customCellRenderer(_row: number, col: number, _value: any, rowData: Dic
|
||||
|
||||
function actionRenderer(_row: DictDto, rowIndex: number): string {
|
||||
const actions = [
|
||||
`<span data-action="edit" data-row-index="${rowIndex}" style="cursor:pointer;color:#165dff;display:inline-flex;" title="${t('system.dict.actions.edit')}">${actionEditIcon}</span>`,
|
||||
`<span data-action="edit" data-row-index="${rowIndex}" style="cursor:pointer;color:var(--app-primary-color,#165dff);display:inline-flex;" title="${t('system.dict.actions.edit')}">${actionEditIcon}</span>`,
|
||||
`<span data-action="delete" data-row-index="${rowIndex}" style="cursor:pointer;color:#f53f3f;display:inline-flex;" title="${t('system.dict.actions.delete')}">${actionDeleteIcon}</span>`,
|
||||
]
|
||||
return `<div style="display:flex;align-items:center;justify-content:center;gap:20px;width:100%;">${actions.join('')}</div>`
|
||||
@ -212,35 +375,55 @@ function handleActionClick(row: DictDto, _rowIndex: number, action?: string) {
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
function selectDictTypeFromKey(key?: string) {
|
||||
if (!key || !key.startsWith('type-')) return
|
||||
const dictType = key.slice(5)
|
||||
if (!dictType) return
|
||||
if (selectedDictType.value === dictType) return
|
||||
selectedDictType.value = dictType
|
||||
pagination.current = 1
|
||||
query.keyword = ''
|
||||
loadData()
|
||||
}
|
||||
|
||||
function handlePageChange(page: number) {
|
||||
pagination.current = page
|
||||
syncTablePage()
|
||||
function handleTypeSelect(keys: string[]) {
|
||||
selectDictTypeFromKey(keys[0])
|
||||
}
|
||||
|
||||
function handlePageSizeChange(size: number) {
|
||||
pagination.pageSize = size
|
||||
function handleTypeNodeClick(key: string) {
|
||||
selectDictTypeFromKey(key)
|
||||
}
|
||||
|
||||
function handleTypeSearch() {
|
||||
// filteredTypeList 客户端过滤
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
pagination.current = 1
|
||||
syncTablePage()
|
||||
loadData()
|
||||
}
|
||||
|
||||
function openBatchImportModal() {
|
||||
batchImportModalRef.value?.open()
|
||||
}
|
||||
|
||||
function handleBatchExport() {
|
||||
if (!allSortedRows.value.length) {
|
||||
Message.warning(t('system.dict.export.empty'))
|
||||
async function handleBatchExport() {
|
||||
if (!selectedDictType.value) {
|
||||
Message.warning(t('system.dict.left.emptySelect'))
|
||||
return
|
||||
}
|
||||
exportLoading.value = true
|
||||
try {
|
||||
const suffix = query.dictType?.trim() ? `_${query.dictType.trim()}` : ''
|
||||
exportDictRowsToExcel(allSortedRows.value, `数据字典${suffix}_${dayjs().format('YYYY-MM-DD_HH-mm-ss')}.xlsx`)
|
||||
const rows = await getDictListAll({
|
||||
dictType: selectedDictType.value,
|
||||
keyword: query.keyword || undefined,
|
||||
})
|
||||
if (!rows.length) {
|
||||
Message.warning(t('system.dict.export.empty'))
|
||||
return
|
||||
}
|
||||
const suffix = selectedDictType.value ? `_${selectedDictType.value}` : ''
|
||||
exportDictRowsToExcel(rows, `数据字典${suffix}_${dayjs().format('YYYY-MM-DD_HH-mm-ss')}.xlsx`)
|
||||
Message.success(t('system.dict.export.success'))
|
||||
} catch (e: any) {
|
||||
Message.error(e?.message || t('system.dict.export.failed'))
|
||||
@ -250,11 +433,15 @@ function handleBatchExport() {
|
||||
}
|
||||
|
||||
function handleAdd() {
|
||||
if (!selectedDictType.value) {
|
||||
Message.warning(t('system.dict.left.emptySelect'))
|
||||
return
|
||||
}
|
||||
isEdit.value = false
|
||||
editingId.value = undefined
|
||||
Object.assign(formState, {
|
||||
dictType: '',
|
||||
dictSort: 1,
|
||||
dictType: selectedDictType.value,
|
||||
dictSort: (selectedTypeMeta.value?.itemCount ?? 0) + 1,
|
||||
dictCode: '',
|
||||
dictName: '',
|
||||
})
|
||||
@ -265,7 +452,7 @@ function handleEdit(row: DictDto) {
|
||||
isEdit.value = true
|
||||
editingId.value = row.id
|
||||
Object.assign(formState, {
|
||||
dictType: row.dictType || '',
|
||||
dictType: row.dictType || selectedDictType.value,
|
||||
dictSort: row.dictSort ?? 1,
|
||||
dictCode: row.dictCode || '',
|
||||
dictName: row.dictName || '',
|
||||
@ -273,11 +460,55 @@ function handleEdit(row: DictDto) {
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
function handleAddType() {
|
||||
typeIsEdit.value = false
|
||||
Object.assign(typeFormState, {
|
||||
id: undefined,
|
||||
dictType: '',
|
||||
typeSort: dictTypeList.value.length + 1,
|
||||
remark: '',
|
||||
})
|
||||
typeModalVisible.value = true
|
||||
}
|
||||
|
||||
function handleEditType(nodeData: CustomLeftMenuTreeNode | null) {
|
||||
if (!nodeData?.dictType) return
|
||||
typeIsEdit.value = true
|
||||
Object.assign(typeFormState, {
|
||||
id: nodeData.id,
|
||||
dictType: nodeData.dictType,
|
||||
typeSort: nodeData.typeSort ?? 0,
|
||||
remark: nodeData.remark ?? '',
|
||||
})
|
||||
typeModalVisible.value = true
|
||||
}
|
||||
|
||||
function handleEditSelectedType() {
|
||||
handleEditType(buildTypeTreeNode(selectedTypeMeta.value || { dictType: selectedDictType.value }))
|
||||
}
|
||||
|
||||
async function handleDeleteType(nodeData: CustomLeftMenuTreeNode | null) {
|
||||
if (!nodeData?.id) return
|
||||
Modal.confirm({
|
||||
content: t('system.dict.typeModal.deleteConfirm', { dictType: nodeData.dictType || '' }),
|
||||
onOk: async () => {
|
||||
await deleteDictType(nodeData.id!)
|
||||
Message.success(t('system.dict.typeModal.deleteSuccess'))
|
||||
if (selectedDictType.value === nodeData.dictType) {
|
||||
selectedDictType.value = ''
|
||||
dataSource.value = []
|
||||
pagination.total = 0
|
||||
}
|
||||
await loadDictTypes(undefined, { loadDetails: false })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async function handleDelete(id?: number | string) {
|
||||
if (!id) return
|
||||
await deleteDict(id)
|
||||
Message.success(t('system.dict.message.deleteSuccess'))
|
||||
loadData()
|
||||
await Promise.all([loadData(), loadDictTypes(undefined, { loadDetails: false })])
|
||||
}
|
||||
|
||||
async function handleModalOk() {
|
||||
@ -298,69 +529,218 @@ async function handleModalOk() {
|
||||
Message.success(t('system.dict.message.addSuccess'))
|
||||
}
|
||||
modalVisible.value = false
|
||||
loadData()
|
||||
await Promise.all([loadData(), loadDictTypes(undefined, { loadDetails: false })])
|
||||
} finally {
|
||||
modalLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
const list = await getDictListAll({
|
||||
dictType: query.dictType || undefined,
|
||||
keyword: query.keyword || undefined,
|
||||
async function handleTypeModalOk() {
|
||||
try {
|
||||
await typeFormRef.value.validate()
|
||||
typeModalLoading.value = true
|
||||
await saveDictType({
|
||||
id: typeFormState.id,
|
||||
dictType: typeFormState.dictType.trim(),
|
||||
remark: typeFormState.remark.trim() || undefined,
|
||||
typeSort: Number(typeFormState.typeSort) || 0,
|
||||
})
|
||||
allSortedRows.value = sortDictRows(list)
|
||||
syncTablePage()
|
||||
Message.success(typeIsEdit.value ? t('system.dict.typeModal.editSuccess') : t('system.dict.typeModal.addSuccess'))
|
||||
typeModalVisible.value = false
|
||||
const savedType = typeFormState.dictType.trim()
|
||||
await loadDictTypes(savedType, { loadDetails: false })
|
||||
} finally {
|
||||
typeModalLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
async function loadDictTypes(
|
||||
preferSelectType?: string,
|
||||
options: { loadDetails?: boolean } = {},
|
||||
) {
|
||||
const { loadDetails = false } = options
|
||||
typeLoading.value = true
|
||||
try {
|
||||
dictTypeList.value = await getDictTypeList()
|
||||
const types = dictTypeList.value.map((item) => item.dictType).filter(Boolean) as string[]
|
||||
if (preferSelectType && types.includes(preferSelectType)) {
|
||||
selectedDictType.value = preferSelectType
|
||||
} else if (!selectedDictType.value || !types.includes(selectedDictType.value)) {
|
||||
selectedDictType.value = types[0] || ''
|
||||
}
|
||||
if (loadDetails && selectedDictType.value) {
|
||||
await loadData()
|
||||
} else if (!selectedDictType.value) {
|
||||
dataSource.value = []
|
||||
pagination.total = 0
|
||||
}
|
||||
} catch (e: any) {
|
||||
dictTypeList.value = []
|
||||
Message.error(e?.message || t('system.dict.message.loadTypeFailed'))
|
||||
} finally {
|
||||
typeLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
if (!selectedDictType.value) {
|
||||
dataSource.value = []
|
||||
pagination.total = 0
|
||||
return
|
||||
}
|
||||
listLoading.value = true
|
||||
try {
|
||||
const { items, total } = await getDictList({
|
||||
page: pagination.current,
|
||||
pageSize: pagination.pageSize,
|
||||
dictType: selectedDictType.value,
|
||||
keyword: query.keyword.trim() || undefined,
|
||||
})
|
||||
dataSource.value = items
|
||||
pagination.total = total
|
||||
if (!query.keyword.trim()) {
|
||||
syncSelectedTypeItemCount(total)
|
||||
}
|
||||
} catch (e: any) {
|
||||
dataSource.value = []
|
||||
pagination.total = 0
|
||||
Message.error(e?.message || t('system.dict.message.loadDataFailed', { error: '' }))
|
||||
} finally {
|
||||
listLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleImportSuccess() {
|
||||
await loadDictTypes(undefined, { loadDetails: false })
|
||||
if (selectedDictType.value) {
|
||||
await loadData()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await loadDictTypes(undefined, { loadDetails: true })
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dict-container {
|
||||
width: 100%;
|
||||
<style scoped lang="less">
|
||||
@import '@/components/RbacSplit/rbac-split-layout.less';
|
||||
|
||||
.dict-split-layout {
|
||||
box-sizing: border-box;
|
||||
padding: 16px 16px 24px;
|
||||
background-color: #f0f3f4;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-x: hidden;
|
||||
overflow-y: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.dict-card {
|
||||
.dict-page-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 0;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
padding: 16px;
|
||||
box-sizing: border-box;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
.dict-type-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
padding: 16px 16px 0;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.dict-type-header-main {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-1);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.type-code-tag {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.type-item-count {
|
||||
font-size: 13px;
|
||||
color: var(--color-text-3);
|
||||
}
|
||||
|
||||
.edit-type-btn {
|
||||
flex-shrink: 0;
|
||||
color: var(--app-primary-color);
|
||||
}
|
||||
|
||||
.table-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
flex-shrink: 0;
|
||||
gap: 12px;
|
||||
flex-wrap: nowrap;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.toolbar-left,
|
||||
.toolbar-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.toolbar-left {
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.panel-empty {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 48px 16px;
|
||||
}
|
||||
|
||||
.panel-empty-icon {
|
||||
font-size: 56px;
|
||||
color: var(--color-text-4);
|
||||
}
|
||||
|
||||
.panel-empty-desc {
|
||||
margin: 8px 0 0;
|
||||
font-size: 13px;
|
||||
color: var(--color-text-3);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.list-wrap {
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
padding: 0 16px 16px;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.list-spin {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.list-spin :deep(.arco-spin-container) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.list-spin :deep(.handsontable-pro-container) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user