From d4447cc7d4881d36fa487339973c9963141120a1 Mon Sep 17 00:00:00 2001 From: zzh <1368735060@qq.com> Date: Wed, 22 Jul 2026 09:25:00 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=95=B0=E6=8D=AE=E5=AD=97=E5=85=B8?= =?UTF-8?q?=E5=B7=A6=E5=8F=B3=E5=88=86=E6=A0=8F=E5=B9=B6=E6=8B=86=E5=88=86?= =?UTF-8?q?=E7=B1=BB=E5=9E=8B/=E6=98=8E=E7=BB=86=E5=88=86=E9=A1=B5?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 左侧仅拉取类型列表与项数,右侧按选中类型服务端分页;修复批量导入重复项识别、字段兼容及类型数量统计。 --- src/api/oms/common.ts | 14 + src/api/system/dict.ts | 86 ++- src/locale/en-US.ts | 158 ++++- src/locale/zh-CN.ts | 158 ++++- src/views/system/dict/dict-batch-import.ts | 385 ++++++++++-- src/views/system/dict/index.vue | 662 ++++++++++++++++----- 6 files changed, 1277 insertions(+), 186 deletions(-) diff --git a/src/api/oms/common.ts b/src/api/oms/common.ts index 113758e..79bf3eb 100644 --- a/src/api/oms/common.ts +++ b/src/api/oms/common.ts @@ -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) } diff --git a/src/api/system/dict.ts b/src/api/system/dict.ts index 11db70e..295f474 100644 --- a/src/api/system/dict.ts +++ b/src/api/system/dict.ts @@ -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): 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)), + total, + } } /** 按当前筛选条件拉取全部字典(分页拼接),用于前端按类型排序后再分页展示 */ @@ -48,8 +79,17 @@ export async function getDictListAll(params: Pick) { - const res = await axiosInstance.post(omsUrl('/system/dict/create'), body) - assertBusinessOk(res.data) + 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): 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[] | 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) +} diff --git a/src/locale/en-US.ts b/src/locale/en-US.ts index a3ad090..dafe23f 100644 --- a/src/locale/en-US.ts +++ b/src/locale/en-US.ts @@ -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', diff --git a/src/locale/zh-CN.ts b/src/locale/zh-CN.ts index 3f3b801..c45cafd 100644 --- a/src/locale/zh-CN.ts +++ b/src/locale/zh-CN.ts @@ -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 失败', diff --git a/src/views/system/dict/dict-batch-import.ts b/src/views/system/dict/dict-batch-import.ts index a0d4e39..f5f7ac4 100644 --- a/src/views/system/dict/dict-batch-import.ts +++ b/src/views/system/dict/dict-batch-import.ts @@ -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 = { 字典类型: '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 = { + 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 = 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,23 +491,24 @@ 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).DictType ?? '').trim() + const dictCode = String(row.dictCode ?? (row as Record).DictCode ?? '').trim() + if (!dictType || !dictCode) return null + return { dictType, dictCode } } async function fetchExistingDictKeys(): Promise> { const set = new Set() - 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 = 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 { 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') } diff --git a/src/views/system/dict/index.vue b/src/views/system/dict/index.vue index 9fb2aab..e8128ed 100644 --- a/src/views/system/dict/index.vue +++ b/src/views/system/dict/index.vue @@ -1,66 +1,131 @@ @@ -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([]) +const typeSearchKeyword = ref('') +const selectedDictType = ref('') +const expandedKeys = ref([]) + const dataSource = ref([]) -/** 筛选条件下的全量排序结果,翻页只切片不重请求 */ -const allSortedRows = ref([]) const batchImportModalRef = ref | null>(null) const exportLoading = ref(false) const modalVisible = ref(false) @@ -125,14 +236,18 @@ const isEdit = ref(false) const editingId = ref() 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(() => + 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 = '' @@ -193,7 +356,7 @@ function customCellRenderer(_row: number, col: number, _value: any, rowData: Dic function actionRenderer(_row: DictDto, rowIndex: number): string { const actions = [ - `${actionEditIcon}`, + `${actionEditIcon}`, `${actionDeleteIcon}`, ] return `
${actions.join('')}
` @@ -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, - }) - allSortedRows.value = sortDictRows(list) - syncTablePage() +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, + }) + 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 }) }) -