diff --git a/src/api/firstLeg.ts b/src/api/firstLeg.ts index c56d3cc..e7fde50 100644 --- a/src/api/firstLeg.ts +++ b/src/api/firstLeg.ts @@ -79,6 +79,7 @@ export interface FirstLegOrderCargoPayload { dangerousGoods?: boolean cargoRemark?: string skuNo?: string + upcNo?: string cargoNameEn?: string totalQuantity?: number netWeightKg?: number diff --git a/src/api/http.ts b/src/api/http.ts index 7ca074b..1453ae1 100644 --- a/src/api/http.ts +++ b/src/api/http.ts @@ -14,7 +14,7 @@ interface RetryableRequest extends InternalAxiosRequestConfig { } function unwrap(envelope: ApiEnvelope) { - if (envelope.code !== '0') { + if (envelope.code !== '200') { throw new ApiError(envelope.message || '请求失败', envelope.code, undefined, asFieldErrors(envelope.data), envelope.traceId) } return envelope.data @@ -40,6 +40,18 @@ function mapError(error: unknown) { } let refreshPromise: Promise | null = null +let redirectingToLogin = false + +function redirectToLoginAfterSessionExpiry() { + if (redirectingToLogin || typeof window === 'undefined') return + + const { pathname, search, hash } = window.location + if (pathname === '/login' || pathname === '/register') return + + redirectingToLogin = true + const redirect = `${pathname}${search}${hash}` + window.location.replace(`/login?redirect=${encodeURIComponent(redirect)}`) +} async function refreshSession() { const refreshToken = getRefreshToken() @@ -85,6 +97,7 @@ transport.interceptors.response.use( if (mappedError.status === 401 || mappedError.code === 'A0401') { clearSessionTokens() window.dispatchEvent(new CustomEvent('cc-client-session-expired')) + redirectToLoginAfterSessionExpiry() } throw mappedError } diff --git a/src/components.d.ts b/src/components.d.ts index 1a30d16..45dabc4 100644 --- a/src/components.d.ts +++ b/src/components.d.ts @@ -11,8 +11,12 @@ export {} /* prettier-ignore */ declare module 'vue' { export interface GlobalComponents { + AppDialog: typeof import('./components/common/AppDialog.vue')['default'] + AppDrawer: typeof import('./components/common/AppDrawer.vue')['default'] ClientSidebarMenu: typeof import('./components/ClientSidebarMenu.vue')['default'] ClientSidebarMenuNode: typeof import('./components/ClientSidebarMenuNode.vue')['default'] + CopyButton: typeof import('./components/common/CopyButton.vue')['default'] + DataTable: typeof import('./components/common/DataTable.vue')['default'] ElButton: typeof import('element-plus/es')['ElButton'] ElCheckbox: typeof import('element-plus/es')['ElCheckbox'] ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup'] @@ -37,6 +41,8 @@ declare module 'vue' { ElTimeline: typeof import('element-plus/es')['ElTimeline'] ElTimelineItem: typeof import('element-plus/es')['ElTimelineItem'] ElUpload: typeof import('element-plus/es')['ElUpload'] + LoadErrorState: typeof import('./components/common/LoadErrorState.vue')['default'] + QueryToolbar: typeof import('./components/common/QueryToolbar.vue')['default'] RouterLink: typeof import('vue-router')['RouterLink'] RouterView: typeof import('vue-router')['RouterView'] } diff --git a/src/components/common/AppDialog.vue b/src/components/common/AppDialog.vue new file mode 100644 index 0000000..2eb557c --- /dev/null +++ b/src/components/common/AppDialog.vue @@ -0,0 +1,95 @@ + + + + + diff --git a/src/components/common/AppDrawer.vue b/src/components/common/AppDrawer.vue new file mode 100644 index 0000000..bf9cfbf --- /dev/null +++ b/src/components/common/AppDrawer.vue @@ -0,0 +1,95 @@ + + + + + diff --git a/src/components/common/CopyButton.vue b/src/components/common/CopyButton.vue new file mode 100644 index 0000000..58bcb48 --- /dev/null +++ b/src/components/common/CopyButton.vue @@ -0,0 +1,62 @@ + + + diff --git a/src/components/common/DataTable.vue b/src/components/common/DataTable.vue new file mode 100644 index 0000000..6965929 --- /dev/null +++ b/src/components/common/DataTable.vue @@ -0,0 +1,180 @@ + + + + + + + diff --git a/src/components/common/LoadErrorState.vue b/src/components/common/LoadErrorState.vue new file mode 100644 index 0000000..3e2ef8a --- /dev/null +++ b/src/components/common/LoadErrorState.vue @@ -0,0 +1,48 @@ + + + + + diff --git a/src/components/common/QueryToolbar.vue b/src/components/common/QueryToolbar.vue new file mode 100644 index 0000000..42f3356 --- /dev/null +++ b/src/components/common/QueryToolbar.vue @@ -0,0 +1,133 @@ + + + + + diff --git a/src/stores/firstLegOrders.ts b/src/stores/firstLegOrders.ts index 08461ed..6e4e948 100644 --- a/src/stores/firstLegOrders.ts +++ b/src/stores/firstLegOrders.ts @@ -88,6 +88,7 @@ function savePayload(draft: FirstLegOrderDraft, warehouse: PlatformReceivingWare dangerousGoods: item.dangerousGoods, cargoRemark: item.remark || undefined, skuNo: item.sku || undefined, + upcNo: item.upc || undefined, totalQuantity: item.pieces, })), pickupMethod: draft.pickupMethod, @@ -158,7 +159,7 @@ function mapOrder(order: MerchantFirstLegOrder, warehouses: PlatformReceivingWar expressCompanyName: order.expressCompany ?? '', trackingNumber: order.expressTrackingNo ?? '', priority: 'NORMAL', customerRemark: order.customerRemark ?? '', operatorName: '', status: clientStatus(order.status), version: order.version ?? 0, items: (detail?.cargoes ?? []).map((item) => ({ - id: String(item.id), sku: item.skuNo ?? '', cargoNameCn: item.cargoName, cargoNameEn: item.cargoNameEn ?? '', + id: String(item.id), sku: item.skuNo ?? '', upc: item.upcNo ?? '', cargoNameCn: item.cargoName, cargoNameEn: item.cargoNameEn ?? '', pieces: item.totalQuantity ?? item.packageCount, packageCount: item.packageCount, grossWeightKg: item.grossWeightKg, volumeCbm: item.volumeCbm, containerType: '', containerCount: 0, remark: item.cargoRemark ?? '', packageType: item.packageType, hsCode: item.hsCode ?? '', declaredValue: item.declaredValue ?? 0, diff --git a/src/styles/foundation.css b/src/styles/foundation.css new file mode 100644 index 0000000..ae3d2e6 --- /dev/null +++ b/src/styles/foundation.css @@ -0,0 +1,6 @@ +:root { font-family: Inter, "PingFang SC", "Microsoft YaHei", sans-serif; color: #172033; background: #f5f7f8; font-synthesis: none; --primary: #075e54; --primary-dark: #064b44; --navy: #17324d; --border: #e1e7ea; --muted: #687780; } +* { box-sizing: border-box; } +html, body, #app { min-width: 320px; min-height: 100%; margin: 0; } +body { min-height: 100vh; background: #f5f7f8; } +button, input { font: inherit; } +a { color: inherit; } diff --git a/src/styles/index.css b/src/styles/index.css index 3093944..52c394c 100644 --- a/src/styles/index.css +++ b/src/styles/index.css @@ -1,56 +1,5 @@ -:root { font-family: Inter, "PingFang SC", "Microsoft YaHei", sans-serif; color: #172033; background: #f5f7f8; font-synthesis: none; --primary: #075e54; --primary-dark: #064b44; --navy: #17324d; --border: #e1e7ea; --muted: #687780; } -* { box-sizing: border-box; } -html, body, #app { min-width: 320px; min-height: 100%; margin: 0; } -body { min-height: 100vh; background: #f5f7f8; } -button, input { font: inherit; } -a { color: inherit; } - -.client-shell { min-height: 100vh; overflow-x: hidden; } -.top-header { position: fixed; inset: 0 0 auto 0; z-index: 20; display: flex; width: 100%; max-width: 100vw; height: 68px; align-items: center; padding: 0 28px; border-bottom: 1px solid var(--border); background: #fff; } -.client-brand { display: flex; width: 232px; min-width: 0; align-items: center; color: var(--navy); text-decoration: none; } -.client-brand-logo { display: block; width: 150px; height: 54px; object-fit: contain; object-position: left center; } -.tenant-name { display: flex; flex: 1; align-items: center; gap: 8px; color: #475467; font-size: 13px; } -.client-user { display: flex; align-items: center; gap: 10px; } -.client-avatar { display: grid; width: 34px; height: 34px; place-items: center; border-radius: 50%; background: #e7f3f1; color: var(--primary); font-weight: 750; } -.client-user-copy { display: flex; flex-direction: column; } -.client-user-copy strong { font-size: 12px; } -.client-user-copy small { color: var(--muted); font-size: 10px; } -.client-sidebar { position: fixed; inset: 68px auto 0 0; display: flex; width: 232px; flex-direction: column; overflow: hidden; padding: 20px 14px; border-right: 1px solid var(--border); background: #fff; } -.client-sidebar nav { display: flex; flex: 1; min-width: 0; min-height: 0; flex-direction: column; overflow-x: hidden; overflow-y: auto; } -.client-nav-menu, -.client-nav-submenu { min-width: 0; margin: 0; padding: 0; list-style: none; } -.client-nav-menu { flex: 1; background: transparent; } -.client-nav-submenu { padding-left: 37px; } -.client-nav-node { min-width: 0; } -.client-sidebar-group-trigger, -.client-sidebar-link { display: flex; width: 100%; height: 44px; min-width: 0; align-items: center; gap: 11px; margin: 2px 0; padding: 0 10px; border-radius: 8px; color: #52616b; } -.client-sidebar-group-trigger { border: 0; background: transparent; cursor: pointer; text-align: left; } -.client-sidebar-link { text-decoration: none; } -.client-sidebar-group-trigger:hover, -.client-sidebar-link:hover { background: #f2f7f6; color: var(--primary); } -.client-sidebar-link[aria-current="page"] { background: #e9f4f2; color: var(--primary); font-weight: 650; } -.client-nav-node.is-active-parent > .client-sidebar-group-trigger { background: #f2f7f6; color: var(--primary); font-weight: 600; } -.client-sidebar .client-sidebar-group-trigger svg, -.client-sidebar .client-sidebar-link svg { flex: 0 0 18px; } -.client-sidebar .client-sidebar-group-chevron { width: 16px; height: 16px; flex: 0 0 16px; margin-left: auto; transition: transform .2s ease; } -.client-sidebar-group-trigger[aria-expanded="true"] .client-sidebar-group-chevron { transform: rotate(180deg); } -.client-sidebar-group-trigger span, -.client-sidebar-link span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } -.client-sidebar-group-trigger:focus-visible, -.client-sidebar-link:focus-visible, -.client-page-heading > button:focus-visible, -.client-sidebar-header button:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; } -.client-sidebar-header, -.client-sidebar-scrim { display: none; } -.sidebar-help { display: flex; flex: 0 0 auto; flex-direction: column; gap: 4px; margin-top: 16px; padding: 14px; border-left: 3px solid var(--primary); background: #f2f7f6; } -.sidebar-help strong { font-size: 12px; } -.sidebar-help span { color: var(--muted); font-size: 11px; } -.client-main { width: calc(100% - 232px); max-width: 100%; min-height: 100vh; margin-left: 232px; padding: 92px 28px 40px; } -.client-page-heading { display: flex; min-width: 0; align-items: center; gap: 12px; margin-bottom: 22px; } -.client-page-heading > div { min-width: 0; } -.client-page-heading button { display: none; width: 44px; height: 44px; flex: 0 0 44px; padding: 0; border: 0; border-radius: 8px; background: none; color: var(--navy); } -.client-page-heading h1 { margin: 0; font-size: 22px; } -.client-page-heading p { margin: 5px 0 0; color: var(--muted); font-size: 12px; } +@import './foundation.css'; +@import './shell.css'; .auth-page { min-height: 100vh; background: #eef3f3; } .auth-header { display: flex; height: 72px; align-items: center; justify-content: space-between; padding: 0 clamp(24px, 6vw, 90px); border-bottom: 1px solid var(--border); background: #fff; color: var(--muted); font-size: 13px; } @@ -86,6 +35,7 @@ a { color: inherit; } .agreement-item { margin-bottom: 8px; } .agreement-control { display: flex; min-height: 44px; align-items: center; gap: 4px; flex-wrap: wrap; color: var(--muted); } .agreement-item .el-checkbox { min-height: 44px; white-space: normal; } +.agreement-required { margin-right: 2px; color: var(--el-color-danger); } .agreement-item button { padding: 0; border: 0; background: transparent; color: var(--primary); cursor: pointer; } .agreement-item button:hover { text-decoration: underline; } .agreement-item button:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; } diff --git a/src/styles/navigationStyles.test.mjs b/src/styles/navigationStyles.test.mjs index 27a816d..33a412d 100644 --- a/src/styles/navigationStyles.test.mjs +++ b/src/styles/navigationStyles.test.mjs @@ -2,7 +2,9 @@ import assert from 'node:assert/strict' import { readFileSync } from 'node:fs' import test from 'node:test' -const styles = readFileSync(new URL('./index.css', import.meta.url), 'utf8') +const styles = ['foundation.css', 'shell.css', 'index.css'] + .map((file) => readFileSync(new URL(file, import.meta.url), 'utf8')) + .join('\n') const compact = styles.replace(/\/\*[\s\S]*?\*\//g, '').replace(/\s+/g, ' ') const mobileStyles = compact.slice(compact.indexOf('@media (max-width: 720px)')) diff --git a/src/styles/shell.css b/src/styles/shell.css new file mode 100644 index 0000000..9342d4d --- /dev/null +++ b/src/styles/shell.css @@ -0,0 +1,46 @@ +.client-shell { min-height: 100vh; overflow-x: hidden; } +.top-header { position: fixed; inset: 0 0 auto 0; z-index: 20; display: flex; width: 100%; max-width: 100vw; height: 68px; align-items: center; padding: 0 28px; border-bottom: 1px solid var(--border); background: #fff; } +.client-brand { display: flex; width: 232px; min-width: 0; align-items: center; color: var(--navy); text-decoration: none; } +.client-brand-logo { display: block; width: 150px; height: 54px; object-fit: contain; object-position: left center; } +.tenant-name { display: flex; flex: 1; align-items: center; gap: 8px; color: #475467; font-size: 13px; } +.client-user { display: flex; align-items: center; gap: 10px; } +.client-avatar { display: grid; width: 34px; height: 34px; place-items: center; border-radius: 50%; background: #e7f3f1; color: var(--primary); font-weight: 750; } +.client-user-copy { display: flex; flex-direction: column; } +.client-user-copy strong { font-size: 12px; } +.client-user-copy small { color: var(--muted); font-size: 10px; } +.client-sidebar { position: fixed; inset: 68px auto 0 0; display: flex; width: 232px; flex-direction: column; overflow: hidden; padding: 20px 14px; border-right: 1px solid var(--border); background: #fff; } +.client-sidebar nav { display: flex; flex: 1; min-width: 0; min-height: 0; flex-direction: column; overflow-x: hidden; overflow-y: auto; } +.client-nav-menu, +.client-nav-submenu { min-width: 0; margin: 0; padding: 0; list-style: none; } +.client-nav-menu { flex: 1; background: transparent; } +.client-nav-submenu { padding-left: 37px; } +.client-nav-node { min-width: 0; } +.client-sidebar-group-trigger, +.client-sidebar-link { display: flex; width: 100%; height: 44px; min-width: 0; align-items: center; gap: 11px; margin: 2px 0; padding: 0 10px; border-radius: 8px; color: #52616b; } +.client-sidebar-group-trigger { border: 0; background: transparent; cursor: pointer; text-align: left; } +.client-sidebar-link { text-decoration: none; } +.client-sidebar-group-trigger:hover, +.client-sidebar-link:hover { background: #f2f7f6; color: var(--primary); } +.client-sidebar-link[aria-current="page"] { background: #e9f4f2; color: var(--primary); font-weight: 650; } +.client-nav-node.is-active-parent > .client-sidebar-group-trigger { background: #f2f7f6; color: var(--primary); font-weight: 600; } +.client-sidebar .client-sidebar-group-trigger svg, +.client-sidebar .client-sidebar-link svg { flex: 0 0 18px; } +.client-sidebar .client-sidebar-group-chevron { width: 16px; height: 16px; flex: 0 0 16px; margin-left: auto; transition: transform .2s ease; } +.client-sidebar-group-trigger[aria-expanded="true"] .client-sidebar-group-chevron { transform: rotate(180deg); } +.client-sidebar-group-trigger span, +.client-sidebar-link span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.client-sidebar-group-trigger:focus-visible, +.client-sidebar-link:focus-visible, +.client-page-heading > button:focus-visible, +.client-sidebar-header button:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; } +.client-sidebar-header, +.client-sidebar-scrim { display: none; } +.sidebar-help { display: flex; flex: 0 0 auto; flex-direction: column; gap: 4px; margin-top: 16px; padding: 14px; border-left: 3px solid var(--primary); background: #f2f7f6; } +.sidebar-help strong { font-size: 12px; } +.sidebar-help span { color: var(--muted); font-size: 11px; } +.client-main { width: calc(100% - 232px); max-width: 100%; min-height: 100vh; margin-left: 232px; padding: 92px 28px 40px; } +.client-page-heading { display: flex; min-width: 0; align-items: center; gap: 12px; margin-bottom: 22px; } +.client-page-heading > div { min-width: 0; } +.client-page-heading button { display: none; width: 44px; height: 44px; flex: 0 0 44px; padding: 0; border: 0; border-radius: 8px; background: none; color: var(--navy); } +.client-page-heading h1 { margin: 0; font-size: 22px; } +.client-page-heading p { margin: 5px 0 0; color: var(--muted); font-size: 12px; } diff --git a/src/types/firstLegOrder.ts b/src/types/firstLegOrder.ts index 0584e75..3018a30 100644 --- a/src/types/firstLegOrder.ts +++ b/src/types/firstLegOrder.ts @@ -11,6 +11,7 @@ export type FirstLegPriority = 'NORMAL' | 'URGENT' | 'EXPEDITED' export interface FirstLegCargoItem { id: string sku: string + upc: string cargoNameCn: string cargoNameEn: string pieces: number diff --git a/src/utils/copyText.ts b/src/utils/copyText.ts new file mode 100644 index 0000000..bbc3acb --- /dev/null +++ b/src/utils/copyText.ts @@ -0,0 +1,35 @@ +export async function copyText(value: string) { + if (typeof navigator !== 'undefined' && navigator.clipboard?.writeText) { + try { + await navigator.clipboard.writeText(value) + return + } catch { + // In insecure development contexts the Clipboard API may exist but reject. + } + } + + if (typeof document === 'undefined' || !document.body) { + throw new Error('Clipboard is unavailable') + } + + const textarea = document.createElement('textarea') + textarea.value = value + textarea.readOnly = true + textarea.setAttribute('aria-hidden', 'true') + Object.assign(textarea.style, { + position: 'fixed', + inset: '0 auto auto -9999px', + opacity: '0', + pointerEvents: 'none', + }) + document.body.appendChild(textarea) + textarea.select() + + try { + if (typeof document.execCommand !== 'function' || !document.execCommand('copy')) { + throw new Error('Clipboard copy failed') + } + } finally { + textarea.remove() + } +} diff --git a/src/utils/firstLegOrder.test.mjs b/src/utils/firstLegOrder.test.mjs index 590d251..7c46610 100644 --- a/src/utils/firstLegOrder.test.mjs +++ b/src/utils/firstLegOrder.test.mjs @@ -50,7 +50,7 @@ function validDraft() { destinationType: 'FBA', amazonWarehouseCode: 'ONT8', shipmentId: 'FBA-001', - items: [{ ...createEmptyCargoItem('cargo-1'), cargoNameCn: '家居用品', pieces: 100, grossWeightKg: 1200, volumeCbm: 3.5, containerType: '40HQ', containerCount: 1 }], + items: [{ ...createEmptyCargoItem('cargo-1'), sku: 'SKU-001', upc: '012345678905', cargoNameCn: '家居用品', pieces: 100, grossWeightKg: 1200, volumeCbm: 3.5, containerType: '40HQ', containerCount: 1 }], } } @@ -97,6 +97,22 @@ test('requires enabled-party snapshots and order addresses before submission', ( assert.ok(errors.includes('请填写本单收货地址')) }) +test('requires FBA reference, warehouse, shipment, SKU and UPC fields', () => { + const draft = validDraft() + draft.referenceNo = '' + draft.amazonWarehouseCode = '' + draft.shipmentId = '' + draft.items[0].sku = '' + draft.items[0].upc = '' + + const errors = validateFirstLegOrderDraft(draft) + assert.ok(errors.includes('FBA 订单请填写客户参考号')) + assert.ok(errors.includes('FBA 订单请填写 Amazon 仓库编码')) + assert.ok(errors.includes('FBA 订单请填写 Shipment ID')) + assert.ok(errors.includes('第 1 条货物缺少客户 SKU')) + assert.ok(errors.includes('第 1 条货物缺少 UPC')) +}) + test('does not require container details for non-FCL cargo', () => { const draft = validDraft() draft.transportMode = 'AIR' diff --git a/src/utils/firstLegOrder.ts b/src/utils/firstLegOrder.ts index 508c6a7..63e88b9 100644 --- a/src/utils/firstLegOrder.ts +++ b/src/utils/firstLegOrder.ts @@ -41,6 +41,7 @@ export function createEmptyCargoItem(id = `cargo-${Date.now()}`): FirstLegCargoI return { id, sku: '', + upc: '', cargoNameCn: '', cargoNameEn: '', pieces: 1, @@ -63,20 +64,32 @@ export function validateFirstLegOrderDraft(draft: FirstLegOrderDraft) { if (!draft.origin.trim()) errors.push('请填写起运地') if (!draft.destination.trim()) errors.push('请填写目的地') - if (!draft.destinationCountryCode.trim()) errors.push('请填写目的国两位代码') + if (!/^[A-Za-z]{2}$/.test(draft.destinationCountryCode.trim())) errors.push('请输入两位英文字母的目的国代码') if (!draft.destinationWarehouseName.trim()) errors.push('请填写目的仓名称') if (!draft.channelCode.trim() || !draft.channelName.trim()) errors.push('请填写渠道编码和渠道名称') if (!draft.receivingWarehouseId) errors.push('请选择平台收货仓') if (!draft.shipperPartyId || !draft.shipperPartySnapshot || !draft.shipper.trim()) errors.push('请选择发货单位') + if (draft.shipperPartySnapshot && !draft.shipperPartySnapshot.contactName.trim()) errors.push('请填写发货联系人') + if (draft.shipperPartySnapshot && !draft.shipperPartySnapshot.phone.trim()) errors.push('请填写发货联系电话') if (draft.shipperPartySnapshot && !draft.shipperPartySnapshot.address.trim()) errors.push('请填写本单发货地址') if (!draft.consigneePartyId || !draft.consigneePartySnapshot || !draft.consignee.trim()) errors.push('请选择收货单位') + if (draft.consigneePartySnapshot && !draft.consigneePartySnapshot.contactName.trim()) errors.push('请填写收货联系人') + if (draft.consigneePartySnapshot && !draft.consigneePartySnapshot.phone.trim()) errors.push('请填写收货联系电话') if (draft.consigneePartySnapshot && !draft.consigneePartySnapshot.address.trim()) errors.push('请填写本单收货地址') if (draft.items.length === 0) errors.push('请至少添加一条货物明细') + if (draft.destinationType === 'FBA') { + if (!draft.referenceNo.trim()) errors.push('FBA 订单请填写客户参考号') + if (!draft.amazonWarehouseCode.trim()) errors.push('FBA 订单请填写 Amazon 仓库编码') + if (!draft.shipmentId.trim()) errors.push('FBA 订单请填写 Shipment ID') + } + draft.items.forEach((item, index) => { const line = `第 ${index + 1} 条货物` if (!item.cargoNameCn.trim()) errors.push(`${line}缺少中文品名`) if (!item.packageType.trim()) errors.push(`${line}缺少包装类型`) + if (draft.destinationType === 'FBA' && !item.sku.trim()) errors.push(`${line}缺少客户 SKU`) + if (draft.destinationType === 'FBA' && !item.upc.trim()) errors.push(`${line}缺少 UPC`) if (!Number.isInteger(item.pieces) || item.pieces <= 0) errors.push(`${line}件数必须为正整数`) if (item.grossWeightKg <= 0) errors.push(`${line}毛重必须大于 0`) if (item.volumeCbm <= 0) errors.push(`${line}体积必须大于 0`) diff --git a/src/views/AccountProfileView.test.mjs b/src/views/AccountProfileView.test.mjs index d599e89..dd36b8e 100644 --- a/src/views/AccountProfileView.test.mjs +++ b/src/views/AccountProfileView.test.mjs @@ -16,6 +16,7 @@ test('registers the account profile route and navigation entry', () => { test('implements profile view, edit, save and cancel states', () => { assert.match(viewSource, /基本信息/) + assert.doesNotMatch(viewSource, /account-identity/) assert.match(viewSource, /function startEditing\(\)/) assert.match(viewSource, /function cancelEditing\(\)/) assert.match(viewSource, /async function saveProfile\(\)/) diff --git a/src/views/AccountProfileView.vue b/src/views/AccountProfileView.vue index 3bcd2b5..3f1ad27 100644 --- a/src/views/AccountProfileView.vue +++ b/src/views/AccountProfileView.vue @@ -4,13 +4,10 @@ import { useRouter } from 'vue-router' import type { FormInstance, FormRules } from 'element-plus' import { ElMessage } from 'element-plus' import { - Building2, - CheckCircle2, KeyRound, Pencil, Save, ShieldCheck, - UserRound, X, } from '@lucide/vue' import { useAuthStore } from '@/stores/auth' @@ -170,19 +167,6 @@ async function changePassword() {