From 386ff8cced6d11f82d2851caafc9903bb1c6e9fc Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Sun, 20 Sep 2026 21:52:15 +0800 Subject: [PATCH 1/6] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/dashboard/overview.test.mjs | 12 +++-- src/modules/dashboard/views/DashboardView.vue | 49 ++++++++++++------- .../components/FirstLegOrderEditorShell.vue | 4 +- .../views/FirstLegOrderCreateHubView.vue | 13 +++-- .../views/FirstLegOrdersView.test.mjs | 12 +++-- .../first-leg/views/FirstLegOrdersView.vue | 46 +++++++++++------ 6 files changed, 91 insertions(+), 45 deletions(-) diff --git a/src/modules/dashboard/overview.test.mjs b/src/modules/dashboard/overview.test.mjs index 2e2e350..c0df45d 100644 --- a/src/modules/dashboard/overview.test.mjs +++ b/src/modules/dashboard/overview.test.mjs @@ -131,8 +131,14 @@ test('keeps absent business numbers null and internal identifiers out of display } }) -test('keeps all six quick actions visually neutral until hover or keyboard focus', () => { - assert.doesNotMatch(dashboardView, /featured|quick-action--featured/) - assert.match(dashboardView, /\.quick-action:hover \{[^}]*border-color:[^}]*transform: translateY\(-5px\);/) +test('matches the updated six-action icon and motion treatment', () => { + assert.match(dashboardView, /icon: Forklift/) + assert.doesNotMatch(dashboardView, /PanelsTopLeft/) + assert.match(dashboardView, /featured: true/) + assert.match(dashboardView, /quick-action--featured/) + assert.match(dashboardView, /featured-aura 5s ease-in-out infinite/) + assert.match(dashboardView, /featured-shine 3\.6s ease-in-out infinite/) + assert.match(dashboardView, /\.quick-action:hover \{[^}]*border-color:[^}]*transform: translateY\(-9px\);/) + assert.match(dashboardView, /prefers-reduced-motion: reduce/) assert.match(dashboardView, /\.client-dashboard a:focus-visible/) }) diff --git a/src/modules/dashboard/views/DashboardView.vue b/src/modules/dashboard/views/DashboardView.vue index d94ef63..4884d89 100644 --- a/src/modules/dashboard/views/DashboardView.vue +++ b/src/modules/dashboard/views/DashboardView.vue @@ -1,18 +1,18 @@ @@ -25,10 +25,10 @@ const primaryActions = computed(() => [
- - + + {{ action.title }}{{ action.description }} - 开始下单 + 开始下单

当前账号暂无常用业务权限,请通过已授权菜单办理业务,或联系企业管理员。

@@ -51,7 +51,7 @@ const primaryActions = computed(() => [ .quick-actions::-webkit-scrollbar-thumb { border-radius: 999px; background: #bdc9dc; } .quick-actions::-webkit-scrollbar-track { background: transparent; } .quick-action { scroll-snap-align: start; } -.quick-action { --action-a: #2563eb; --action-b: #3730a3; --action-soft: rgba(37, 99, 235, .18); position: relative; display: flex; min-width: 0; min-height: 154px; overflow: hidden; align-items: center; flex-direction: column; padding: 15px 9px 13px; border: 1px solid rgba(148, 163, 184, .22); border-radius: 14px; background: linear-gradient(155deg, #fff 8%, rgba(248, 250, 252, .96) 100%); color: inherit; text-align: center; text-decoration: none; box-shadow: 0 8px 22px rgba(15, 23, 42, .055); animation: action-enter .4s cubic-bezier(.22, 1, .36, 1) both; animation-delay: calc(var(--entry-index) * 55ms); transition: border-color .35s ease, box-shadow .35s ease, transform .35s cubic-bezier(.22, 1, .36, 1); } +.quick-action { --action-a: #2563eb; --action-b: #3730a3; --action-soft: rgba(37, 99, 235, .18); position: relative; isolation: isolate; display: flex; min-width: 0; min-height: 190px; overflow: hidden; align-items: center; flex-direction: column; padding: 21px 10px 18px; border: 1px solid rgba(148, 163, 184, .2); border-radius: 17px; background: linear-gradient(155deg, #fff 8%, rgba(248, 250, 252, .96) 100%); color: inherit; text-align: center; text-decoration: none; box-shadow: 0 12px 34px rgba(15, 23, 42, .07); backdrop-filter: blur(10px); animation: action-enter .4s cubic-bezier(.22, 1, .36, 1) both; animation-delay: calc(var(--entry-index) * 55ms); transition: border-color .35s ease, box-shadow .35s ease, transform .35s cubic-bezier(.22, 1, .36, 1); } .quick-action::before { position: absolute; width: 120px; height: 120px; border-radius: 50%; background: radial-gradient(circle, var(--action-soft), transparent 70%); content: ''; right: -62px; top: -66px; transition: transform .4s ease; } .quick-action::after { position: absolute; right: 20%; bottom: 0; left: 20%; height: 3px; border-radius: 99px 99px 0 0; background: linear-gradient(90deg, var(--action-a), var(--action-b)); content: ''; opacity: .55; transform: scaleX(.35); transition: all .35s ease; } .quick-action--blue { --action-a: #2563eb; --action-b: #3730a3; --action-soft: rgba(37, 99, 235, .18); } @@ -60,18 +60,29 @@ const primaryActions = computed(() => [ .quick-action--cyan { --action-a: #0891b2; --action-b: #0e7490; --action-soft: rgba(8, 145, 178, .18); } .quick-action--orange { --action-a: #f59e0b; --action-b: #dc6b12; --action-soft: rgba(245, 158, 11, .2); } .quick-action--rose { --action-a: #e11d48; --action-b: #be185d; --action-soft: rgba(225, 29, 72, .17); } -.quick-action:hover { border-color: color-mix(in srgb, var(--action-a) 35%, transparent); box-shadow: 0 16px 34px rgba(15, 23, 42, .12); transform: translateY(-5px); } +.quick-action:hover { border-color: color-mix(in srgb, var(--action-a) 35%, transparent); box-shadow: 0 24px 54px rgba(15, 23, 42, .14), 0 0 0 1px color-mix(in srgb, var(--action-a) 16%, transparent); transform: translateY(-9px); } .quick-action:hover::before { transform: scale(1.25); } .quick-action:hover::after { right: 12%; left: 12%; opacity: 1; transform: scaleX(1); } -.action-icon { position: relative; display: grid; width: 48px; height: 48px; flex: 0 0 auto; place-items: center; margin-bottom: 11px; border-radius: 14px; background: linear-gradient(135deg, var(--action-a), var(--action-b)); color: #fff; box-shadow: 0 8px 17px color-mix(in srgb, var(--action-a) 24%, transparent); transition: transform .35s ease; } -.action-icon::after { position: absolute; border: 1px solid color-mix(in srgb, var(--action-a) 20%, transparent); border-radius: 18px; content: ''; inset: -4px; } +.action-icon { position: relative; display: grid; width: 58px; height: 58px; flex: 0 0 auto; place-items: center; margin-bottom: 16px; border-radius: 18px; background: linear-gradient(135deg, var(--action-a), var(--action-b)); color: #fff; box-shadow: 0 11px 22px color-mix(in srgb, var(--action-a) 25%, transparent); transition: transform .35s ease; } +.action-icon::before { position: absolute; inset: 7px; border-radius: 12px; background: linear-gradient(135deg, rgba(255, 255, 255, .32), transparent 42%); content: ''; pointer-events: none; } +.action-icon::after { position: absolute; border: 1px solid color-mix(in srgb, var(--action-a) 20%, transparent); border-radius: 22px; content: ''; inset: -5px; } .quick-action:hover .action-icon { transform: translateY(-3px) scale(1.06) rotate(-3deg); } .action-copy { position: relative; display: flex; min-width: 0; width: 100%; align-items: center; flex-direction: column; } -.action-copy strong { color: #172033; font-size: 15px; font-weight: 750; line-height: 1.35; white-space: nowrap; } -.action-copy small { overflow: hidden; width: 100%; margin-top: 4px; color: #718096; font-size: 10px; line-height: 1.45; text-overflow: ellipsis; white-space: nowrap; } -.action-enter { position: relative; display: inline-flex; align-items: center; gap: 5px; margin-top: auto; padding-top: 7px; color: var(--action-a); font-size: 10px; font-weight: 700; } +.action-copy strong { color: #172033; font-size: 16px; font-weight: 800; line-height: 1.35; white-space: nowrap; } +.action-copy small { overflow: hidden; width: 100%; margin-top: 6px; color: #718096; font-size: 11px; line-height: 1.5; text-overflow: ellipsis; white-space: nowrap; } +.action-enter { position: relative; display: inline-flex; align-items: center; gap: 6px; margin-top: auto; color: var(--action-a); font-size: 11px; font-weight: 750; } .action-enter svg { transition: transform .25s ease; } .quick-action:hover .action-enter svg { transform: translateX(4px); } +.quick-action--featured { border-color: rgba(49, 126, 255, .62); background: radial-gradient(circle at 8% 2%, rgba(34, 211, 238, .23), transparent 32%), radial-gradient(circle at 100% 100%, rgba(79, 70, 229, .22), transparent 44%), linear-gradient(145deg, #f2fbff 0%, #f8faff 45%, #eef2ff 100%); box-shadow: 0 20px 48px rgba(37, 99, 235, .2), 0 0 0 1px rgba(255, 255, 255, .88) inset, 0 0 34px rgba(34, 211, 238, .1); } +.quick-action--featured::before { width: 220px; height: 220px; right: -88px; top: -112px; background: radial-gradient(circle, rgba(34, 211, 238, .35) 0%, rgba(59, 130, 246, .2) 34%, transparent 69%); filter: blur(1px); animation: featured-aura 5s ease-in-out infinite; } +.quick-action--featured::after { right: 8%; left: 8%; height: 4px; background: linear-gradient(90deg, #22d3ee 0%, #2563eb 48%, #4f46e5 100%); box-shadow: 0 -4px 16px rgba(37, 99, 235, .44), 0 -1px 6px rgba(34, 211, 238, .55); opacity: 1; transform: scaleX(1); } +.quick-action--featured:hover { border-color: rgba(34, 211, 238, .8); box-shadow: 0 30px 70px rgba(37, 99, 235, .27), 0 0 0 1px rgba(255, 255, 255, .95) inset, 0 0 40px rgba(34, 211, 238, .15); transform: translateY(-11px) scale(1.015); } +.quick-action--featured .action-icon { width: 64px; height: 64px; margin-bottom: 17px; border-radius: 20px; background: linear-gradient(145deg, #22c7df 0%, #2875ef 48%, #4338ca 100%); box-shadow: 0 16px 32px rgba(37, 99, 235, .37), 0 0 0 7px rgba(59, 130, 246, .09), 0 0 26px rgba(34, 211, 238, .24); } +.quick-action--featured .action-icon::after { inset: -6px; border-color: rgba(59, 130, 246, .25); border-radius: 25px; box-shadow: 0 0 0 1px rgba(34, 211, 238, .09); } +.quick-action--featured .action-copy strong { color: #15233e; font-size: 17px; } +.quick-action--featured .action-enter { overflow: hidden; padding: 8px 15px; border-radius: 999px; background: linear-gradient(100deg, #12b8d0 0%, #2563eb 52%, #4338ca 100%); box-shadow: 0 10px 24px rgba(37, 99, 235, .3), 0 0 18px rgba(34, 211, 238, .12); color: #fff; } +.quick-action--featured .action-enter::before { position: absolute; top: -60%; bottom: -60%; left: -55%; width: 38%; transform: rotate(18deg); background: linear-gradient(90deg, transparent, rgba(255, 255, 255, .55), transparent); content: ''; animation: featured-shine 3.6s ease-in-out infinite; } +.quick-action--featured .action-enter > * { position: relative; z-index: 1; } .no-actions { margin: 0; padding: 20px; border: 1px solid var(--border); border-radius: 10px; background: #fff; color: #667085; font-size: 14px; } .dashboard-order-list { min-width: 0; } .orders-permission-empty { display: flex; min-height: 180px; align-items: center; justify-content: center; flex-direction: column; gap: 8px; border: 1px solid var(--border); border-radius: 10px; background: #fff; color: #7b8494; } @@ -80,8 +91,10 @@ const primaryActions = computed(() => [ .client-dashboard a:focus-visible { border-radius: 6px; outline: 2px solid var(--primary); outline-offset: 4px; } @keyframes dashboard-enter { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: translateY(0); } } @keyframes action-enter { from { opacity: 0; transform: translateY(7px); } to { opacity: 1; transform: translateY(0); } } +@keyframes featured-aura { 0%, 100% { opacity: .86; transform: translate3d(0, 0, 0) scale(1); } 50% { opacity: 1; transform: translate3d(-10px, 9px, 0) scale(1.08); } } +@keyframes featured-shine { 0%, 55% { left: -55%; opacity: 0; } 65% { opacity: 1; } 88%, 100% { left: 125%; opacity: 0; } } @container dashboard-content (max-width: 1180px) { .quick-actions { grid-template-columns: repeat(6, minmax(178px, 1fr)); } } @media (max-width: 720px) { .client-dashboard { padding: 0; gap: 16px; } .business-intro h1 { font-size: 25px; } .quick-actions { grid-template-columns: repeat(2, minmax(150px, 1fr)); } } @media (max-width: 420px) { .quick-actions { grid-template-columns: repeat(6, minmax(168px, 1fr)); } } -@media (prefers-reduced-motion: reduce) { .client-dashboard, .quick-action { animation: none; } .quick-action, .quick-action::before, .quick-action::after, .action-icon, .action-enter svg { transition: none; } } +@media (prefers-reduced-motion: reduce) { .client-dashboard, .quick-action, .quick-action--featured::before, .quick-action--featured .action-enter::before { animation: none; } .quick-action, .quick-action::before, .quick-action::after, .action-icon, .action-enter svg { transition: none; } } diff --git a/src/modules/first-leg/components/FirstLegOrderEditorShell.vue b/src/modules/first-leg/components/FirstLegOrderEditorShell.vue index faa6a22..cd8fb68 100644 --- a/src/modules/first-leg/components/FirstLegOrderEditorShell.vue +++ b/src/modules/first-leg/components/FirstLegOrderEditorShell.vue @@ -43,9 +43,9 @@ const emit = defineEmits<{ diff --git a/src/modules/first-leg/views/FirstLegOrdersView.test.mjs b/src/modules/first-leg/views/FirstLegOrdersView.test.mjs index e076417..56962c2 100644 --- a/src/modules/first-leg/views/FirstLegOrdersView.test.mjs +++ b/src/modules/first-leg/views/FirstLegOrdersView.test.mjs @@ -427,10 +427,12 @@ test('distills order creation into the confirmed prototype sections with server test('matches the prototype two-column create layout and reset action', () => { assert.match(source, /class="order-form-section prototype-section order-basic-section"/) - assert.match(source, /

客户下单<\/h3>

创建新订单<\/p>/) + assert.match(source, /01<\/span>

客户下单<\/h3>国内交货与运输信息<\/span>/) + assert.match(source, /02<\/span>

目的地信息<\/h3>美国目的仓与收件信息<\/span>/) assert.match(source, /\.is-create-route \.prototype-order-form \{[^}]*grid-template-columns: repeat\(2, minmax\(0, 1fr\)\)/) assert.match(source, /\.is-create-route \.cargo-section, \.is-create-route \.cost-estimate-section \{ grid-column: 1 \/ -1; \}/) - assert.match(source, /\.is-create-route \.prototype-form-row :deep\(\.el-form-item__label\) \{[^}]*height: 40px[^}]*align-items: center[^}]*justify-content: flex-end/) + assert.match(source, /\.is-create-route \.prototype-form-row[^\n]*grid-template-columns: 108px minmax\(0, 1fr\)/) + assert.match(source, /\.is-create-route \.prototype-form-row :deep\(\.el-form-item__label\) \{[^}]*height: 36px[^}]*justify-content: flex-end[^}]*padding: 0 10px 0 0/) assert.match(source, /\.is-create-route \.compact-transport-grid > :deep\(\.el-form-item__label\) \{[^}]*align-items: center[^}]*text-align: right/) assert.match(source, /async function resetCreateForm\(\)/) assert.match(source, /确认清空当前填写的订单内容并恢复默认值/) @@ -442,11 +444,15 @@ test('matches the prototype two-column create layout and reset action', () => { assert.match(editorShellSource, /submit-shine 3\.2s ease-in-out infinite/) assert.match(editorShellSource, /@media \(prefers-reduced-motion: reduce\)/) assert.match(createHubSource, /

头程下单<\/h1>/) + assert.match(createHubSource, /
[\s\S]*order-mode-switch[\s\S]*选择下单方式/) assert.doesNotMatch(createHubSource, /填写货物信息,创建新的头程运输订单/) assert.match(source, /\.is-create-route \.prototype-order-form \{[^}]*align-items: start/) assert.doesNotMatch(source, /\.is-create-route \.order-basic-section, \.is-create-route \.destination-section \{ height: 100%; \}/) - assert.match(source, /\.is-create-route \.order-basic-section > \.order-section-heading, \.is-create-route \.destination-section > \.order-section-heading \{[^}]*height: 74px[^}]*min-height: 74px/) + assert.doesNotMatch(source, /\.is-create-route \.order-basic-section > \.order-section-heading, \.is-create-route \.destination-section > \.order-section-heading \{[^}]*(?:height|min-height):/) assert.match(source, /\.is-create-route \.order-basic-section > \.order-section-heading h3, \.is-create-route \.destination-section > \.order-section-heading h3 \{[^}]*font-size: 17px[^}]*line-height: 24px/) + assert.match(source, /\.is-create-route \.order-step-badge \{[^}]*width: 30px[^}]*height: 30px/) + assert.match(source, /\.is-create-route \.order-section-context \{[^}]*margin-left: auto/) + assert.match(source, /\.is-create-route \.warehouse-options :deep\(\.el-radio\.is-checked\) \{[^}]*linear-gradient/) }) test('shows an optional LX inbound order number for one-click dropship orders', () => { diff --git a/src/modules/first-leg/views/FirstLegOrdersView.vue b/src/modules/first-leg/views/FirstLegOrdersView.vue index e6b078c..348de66 100644 --- a/src/modules/first-leg/views/FirstLegOrdersView.vue +++ b/src/modules/first-leg/views/FirstLegOrdersView.vue @@ -2158,7 +2158,7 @@ async function searchOrders(model: QueryModel) {
-

客户下单

创建新订单

+
01

客户下单

国内交货与运输信息
普通头程一件代发测量完成后不办理国内库存入库,将直接推送 PDA 合托。 用于绑定领星入库单,提交后可在尾程订单、入库单和出库单中查询。 @@ -2197,7 +2197,7 @@ async function searchOrders(model: QueryModel) {
-

目的地信息

+
02

目的地信息

美国目的仓与收件信息
FBA 仓海外仓私人仓 @@ -2550,24 +2550,42 @@ async function searchOrders(model: QueryModel) { .is-create-route { gap: 0; } .is-create-route .first-leg-editor-page { width: 100%; } .is-create-route .supporting-data-error, .is-create-route > .el-alert { margin-bottom: 14px; } -.is-create-route .prototype-order-form { display: grid; width: 100%; grid-template-columns: repeat(2, minmax(0, 1fr)); align-items: start; gap: 14px; margin: 0; padding: 0 0 14px; } -.is-create-route .prototype-section { min-width: 0; margin: 0; padding: 0 20px 18px; border: 1px solid #dfe6ee; border-radius: 8px; background: #fff; } +.is-create-route .prototype-order-form { display: grid; width: 100%; grid-template-columns: repeat(2, minmax(0, 1fr)); align-items: start; gap: 10px; margin: 0; padding: 0 0 10px; } +.is-create-route .prototype-section { position: relative; min-width: 0; margin: 0; padding: 0 16px 14px; overflow: hidden; border: 1px solid #d9e4ea; border-radius: 8px; background: #fff; box-shadow: 0 8px 26px rgba(24, 55, 75, .065); transition: border-color .22s ease, box-shadow .22s ease; } +.is-create-route .order-basic-section::before, .is-create-route .destination-section::before { position: absolute; z-index: 2; top: 0; right: 0; left: 0; height: 3px; background: linear-gradient(90deg, #16a394, #176b87 55%, transparent 92%); content: ''; } +.is-create-route .destination-section::before { background: linear-gradient(90deg, #2563eb, #4f46e5 55%, transparent 92%); } +.is-create-route .order-basic-section:focus-within { border-color: rgba(22, 163, 148, .45); box-shadow: 0 12px 34px rgba(22, 113, 113, .1); } +.is-create-route .destination-section:focus-within { border-color: rgba(37, 99, 235, .4); box-shadow: 0 12px 34px rgba(37, 99, 235, .1); } .is-create-route .prototype-section + .prototype-section { margin-top: 0; } .is-create-route .cargo-section, .is-create-route .cost-estimate-section { grid-column: 1 / -1; } -.is-create-route .order-basic-section > .order-section-heading, .is-create-route .destination-section > .order-section-heading { box-sizing: border-box; height: 74px; min-height: 74px; align-items: flex-start; margin: 0 -20px 16px; padding: 12px 20px; border-bottom: 1px solid #e7ecf2; } +.is-create-route .order-basic-section > .order-section-heading, .is-create-route .destination-section > .order-section-heading { box-sizing: border-box; align-items: center; margin: 0 -16px 12px; padding: 10px 16px; border-bottom: 1px solid #e7ecf2; background: linear-gradient(90deg, #f8fbfc, #fff 58%); } +.is-create-route .destination-section > .order-section-heading { background: linear-gradient(90deg, #f7f9ff, #fff 58%); } .is-create-route .order-basic-section > .order-section-heading h3, .is-create-route .destination-section > .order-section-heading h3 { margin: 0; color: var(--navy); font-size: 17px; font-weight: 700; line-height: 24px; } -.is-create-route .cargo-section > .order-section-heading, .is-create-route .cost-estimate-section > .order-section-heading { min-height: 58px; align-items: flex-start; margin: 0 -20px 16px; padding: 12px 20px; border-bottom: 1px solid #e7ecf2; } -.is-create-route .order-section-heading p { margin-top: 2px; color: #8a98a8; } +.is-create-route .order-step-badge { display: grid; width: 30px; height: 30px; flex: 0 0 30px; color: #fff; border-radius: 8px; background: linear-gradient(135deg, #16a394, #176b87); box-shadow: 0 5px 12px rgba(23, 107, 135, .2); font-size: 11px; font-weight: 800; place-items: center; } +.is-create-route .destination-section .order-step-badge { background: linear-gradient(135deg, #367ae8, #4f46e5); box-shadow: 0 5px 12px rgba(37, 99, 235, .2); } +.is-create-route .order-section-context { margin-left: auto; color: #8a98a8; font-size: 11px; font-weight: 500; } +.is-create-route .cargo-section > .order-section-heading, .is-create-route .cost-estimate-section > .order-section-heading { align-items: center; margin: 0 -16px 10px; padding: 9px 16px; border-bottom: 1px solid #e7ecf2; } +.is-create-route .order-section-heading p { margin: 0; color: #8a98a8; } .is-create-route .prototype-form-flow, .is-create-route .destination-form-flow { width: 100%; max-width: none; } -.is-create-route .prototype-form-row, .is-create-route .prototype-form-row--wide, .is-create-route .prototype-form-row--compact, .is-create-route .prototype-form-row--short, .is-create-route .prototype-form-row--tabs { grid-template-columns: 120px minmax(0, 1fr); } -.is-create-route .prototype-form-row :deep(.el-form-item__label) { display: flex; height: 40px; min-height: 40px; align-items: center; justify-content: flex-end; padding: 0 18px 0 0; font-size: 13px; line-height: 1.35; text-align: right; } +.is-create-route .prototype-form-row, .is-create-route .prototype-form-row--wide, .is-create-route .prototype-form-row--compact, .is-create-route .prototype-form-row--short, .is-create-route .prototype-form-row--tabs { grid-template-columns: 108px minmax(0, 1fr); } +.is-create-route .prototype-form-row :deep(.el-form-item__label) { display: flex; height: 36px; min-height: 36px; align-items: center; justify-content: flex-end; padding: 0 10px 0 0; font-size: 13px; line-height: 1.35; text-align: right; } .is-create-route .segmented-options, .is-create-route .destination-type-options { width: 100%; max-width: 560px; background: #f3f6f9; } +.is-create-route .order-basic-section .segmented-options :deep(.el-radio-button__original-radio:checked + .el-radio-button__inner) { background: linear-gradient(105deg, #1596a6, #176b87); } +.is-create-route .destination-section .segmented-options :deep(.el-radio-button__original-radio:checked + .el-radio-button__inner) { background: linear-gradient(105deg, #367ae8, #4f46e5); box-shadow: 0 4px 10px rgba(37, 99, 235, .2); } .is-create-route .compact-transport-grid { width: 100%; margin-top: 4px; } -.is-create-route .compact-transport-grid > :deep(.el-form-item), .is-create-route .compact-transport-grid > :deep(.el-form-item:nth-child(1)), .is-create-route .compact-transport-grid > :deep(.el-form-item:nth-child(6)), .is-create-route .compact-transport-grid > :deep(.el-form-item:nth-child(7)) { grid-template-columns: 120px minmax(0, 1fr); } -.is-create-route .compact-transport-grid > :deep(.el-form-item__label) { display: flex; height: 40px; min-height: 40px; align-items: center; justify-content: flex-end; padding: 0 18px 0 0; line-height: 1.35; text-align: right; } -.is-create-route .selected-destination { width: auto; margin-left: 138px; } -.is-create-route .shipping-form-flow .prototype-form-row, .is-create-route .destination-form-flow .prototype-form-row, .is-create-route .compact-transport-grid > :deep(.el-form-item) { margin-bottom: 8px; } -.is-create-route .cargo-table-heading, .is-create-route .cost-estimate-section .order-section-heading { margin-bottom: 16px; } +.is-create-route .compact-transport-grid > :deep(.el-form-item), .is-create-route .compact-transport-grid > :deep(.el-form-item:nth-child(1)), .is-create-route .compact-transport-grid > :deep(.el-form-item:nth-child(6)), .is-create-route .compact-transport-grid > :deep(.el-form-item:nth-child(7)) { grid-template-columns: 108px minmax(0, 1fr); } +.is-create-route .compact-transport-grid > :deep(.el-form-item__label) { display: flex; height: 36px; min-height: 36px; align-items: center; justify-content: flex-end; padding: 0 10px 0 0; line-height: 1.35; text-align: right; } +.is-create-route .selected-destination { width: auto; margin-left: 108px; } +.is-create-route .shipping-form-flow .prototype-form-row, .is-create-route .destination-form-flow .prototype-form-row, .is-create-route .compact-transport-grid > :deep(.el-form-item) { margin-bottom: 5px; } +.is-create-route .prototype-form-row :deep(.el-input__wrapper), .is-create-route .prototype-form-row :deep(.el-select__wrapper), .is-create-route .compact-transport-grid :deep(.el-input__wrapper), .is-create-route .compact-transport-grid :deep(.el-select__wrapper) { min-height: 36px; } +.is-create-route .segmented-options :deep(.el-radio-button__inner) { min-height: 36px; padding: 8px 12px; } +.is-create-route .warehouse-options { gap: 8px; padding-top: 1px; } +.is-create-route .warehouse-options :deep(.el-radio) { min-height: 32px; margin: 0; padding: 5px 11px; border: 1px solid #d7e0e7; border-radius: 7px; background: #f8fafb; color: #596779; transition: color .18s ease, border-color .18s ease, background .18s ease, box-shadow .18s ease, transform .18s ease; } +.is-create-route .warehouse-options :deep(.el-radio:hover) { transform: translateY(-1px); border-color: #91b8c3; background: #f3fafb; color: var(--primary); } +.is-create-route .warehouse-options :deep(.el-radio__input) { position: absolute; width: 1px; height: 1px; overflow: hidden; opacity: 0; pointer-events: none; } +.is-create-route .warehouse-options :deep(.el-radio__label) { padding-left: 0; color: inherit; } +.is-create-route .warehouse-options :deep(.el-radio.is-checked) { border-color: var(--primary); background: linear-gradient(105deg, #1596a6, #176b87); box-shadow: 0 4px 10px rgba(23, 107, 135, .2); color: #fff; } +.is-create-route .cargo-table-heading, .is-create-route .cost-estimate-section .order-section-heading { margin-bottom: 10px; } .order-form { width: min(100%, 1000px); margin: 0; padding: 0 0 12px; } .order-form.is-dialog-order-form { width: 100%; max-width: none; } .is-dialog-order-form > .cargo-section, .is-dialog-order-form > .cost-estimate-section { width: 100%; max-width: none; } From 3cdd04e9b71db00f3b4b05379394a87eb01260ba Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Sun, 20 Sep 2026 22:13:59 +0800 Subject: [PATCH 2/6] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/first-leg/views/QuickOrderView.test.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/first-leg/views/QuickOrderView.test.mjs b/src/modules/first-leg/views/QuickOrderView.test.mjs index b6b4187..4ea3c3c 100644 --- a/src/modules/first-leg/views/QuickOrderView.test.mjs +++ b/src/modules/first-leg/views/QuickOrderView.test.mjs @@ -19,7 +19,7 @@ test('keeps quick order active but moves it from the menu into the head-leg crea assert.match(routesSource, /path: 'orders\/first-leg\/create'[\s\S]*?FirstLegOrderCreateHubView\.vue/) assert.match(createHubSource, />普通下单<\/button>/) assert.match(createHubSource, />快速下单<\/button>/) - assert.match(createHubSource, /

头程下单<\/h1>/) + assert.match(createHubSource, /]*>头程下单<\/h1>/) assert.match(createHubSource, /auth\.hasPermission\('order:first-leg:quick-create'\)/) assert.match(createHubSource, //) assert.match(createHubSource, //) From a96cbbd57d4090f70d85f508572ebbd65eedd857 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Sun, 20 Sep 2026 23:39:39 +0800 Subject: [PATCH 3/6] fix: preserve stable Client proxy port --- scripts/deploy-production.sh | 6 +++--- src/deployment/blueGreenDeployment.test.mjs | 4 ++++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/scripts/deploy-production.sh b/scripts/deploy-production.sh index e3499ef..c27fb0b 100644 --- a/scripts/deploy-production.sh +++ b/scripts/deploy-production.sh @@ -62,12 +62,12 @@ compose() { case "$service" in admin-web) base_port=9001 - nginx_config=/www/server/panel/vhost/nginx/admin.buggoglobal.com.conf + nginx_config=/www/server/panel/vhost/nginx/00-blue-green-admin-web.conf legacy_container=firstleg-prod-admin-web ;; client-web) base_port=9002 - nginx_config=/www/server/panel/vhost/nginx/app.buggoglobal.com.conf + nginx_config=/www/server/panel/vhost/nginx/00-blue-green-client-web.conf legacy_container=firstleg-prod-client-web ;; *) echo "不支持蓝绿部署的前端服务: $service" >&2; exit 2 ;; @@ -102,7 +102,7 @@ fi if [ "$active_slot" = blue ]; then target_slot=green; else target_slot=blue; fi [ "$active_slot" = legacy ] && target_slot=green -if [ "$target_slot" = blue ]; then target_port="$base_port"; else target_port=$((base_port + 10000)); fi +if [ "$target_slot" = blue ]; then target_port=$((base_port + 20000)); else target_port=$((base_port + 10000)); fi candidate="${legacy_container}-${target_slot}" docker rm -f "$candidate" >/dev/null 2>&1 || true diff --git a/src/deployment/blueGreenDeployment.test.mjs b/src/deployment/blueGreenDeployment.test.mjs index 8576a08..35a34f9 100644 --- a/src/deployment/blueGreenDeployment.test.mjs +++ b/src/deployment/blueGreenDeployment.test.mjs @@ -13,7 +13,11 @@ test('client production deployment switches healthy blue-green slots atomically' const nginx = script.indexOf('nginx -t || ! nginx -s reload') const stop = script.indexOf('docker stop --time 30 "$active_container"') assert.ok(start >= 0 && start < health && health < nginx && nginx < stop) + assert.match(script, /target_port=\$\(\(base_port \+ 20000\)\)/) assert.match(script, /target_port=\$\(\(base_port \+ 10000\)\)/) + assert.match(script, /00-blue-green-admin-web\.conf/) + assert.match(script, /00-blue-green-client-web\.conf/) + assert.doesNotMatch(script, /nginx_config=\/www\/server\/panel\/vhost\/nginx\/(?:admin|app)\.buggoglobal\.com\.conf/) assert.match(script, /cp -f "\$backup" "\$nginx_config"/) }) From bdb42f1256c1cafd8e30f8edc9e429b10c71bcc5 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Mon, 21 Sep 2026 10:44:30 +0800 Subject: [PATCH 4/6] fix: allow manual quote without carrier rate --- src/modules/first-leg/types/inboundBilling.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/modules/first-leg/types/inboundBilling.ts b/src/modules/first-leg/types/inboundBilling.ts index 101a2a5..51d3db9 100644 --- a/src/modules/first-leg/types/inboundBilling.ts +++ b/src/modules/first-leg/types/inboundBilling.ts @@ -68,9 +68,9 @@ export interface MerchantInboundFeeSnapshot { chargeRuleId: string ruleCode: string chargeRuleVersion: number - carrierRateId: string + carrierRateId: string | null rateCode: string - carrierRateVersion: number + carrierRateVersion: number | null customerId: string customerName: string originWarehouseId: string From befd22af96664e76eefd4a965dfb87912fbb1569 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Mon, 21 Sep 2026 11:17:13 +0800 Subject: [PATCH 5/6] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/dashboard/overview.test.mjs | 12 +++++ .../overseas-warehouse/permissions.test.mjs | 4 +- src/modules/overseas-warehouse/routes.ts | 4 +- .../views/ReturnOrdersView.test.mjs | 8 ++- .../views/ReturnOrdersView.vue | 52 ++++++++++++++++--- 5 files changed, 67 insertions(+), 13 deletions(-) diff --git a/src/modules/dashboard/overview.test.mjs b/src/modules/dashboard/overview.test.mjs index c0df45d..da51657 100644 --- a/src/modules/dashboard/overview.test.mjs +++ b/src/modules/dashboard/overview.test.mjs @@ -142,3 +142,15 @@ test('matches the updated six-action icon and motion treatment', () => { assert.match(dashboardView, /prefers-reduced-motion: reduce/) assert.match(dashboardView, /\.client-dashboard a:focus-visible/) }) + +test('opens all six dashboard businesses through their dedicated create pages', () => { + for (const route of [ + '/orders/first-leg/create', + '/overseas-warehouse/return-relabel/create', + '/cross-border/domestic-trucking/create', + '/cross-border/pallet-shipping/create', + '/cross-border/procurement/create', + '/overseas-warehouse/labeling/create', + ]) assert.match(dashboardView, new RegExp(`route: '${route.replaceAll('/', '\\/')}'`)) + assert.match(dashboardView, /title: '退货换标'[\s\S]*?permission: 'merchant:overseas-warehouse:return-order:create'/) +}) diff --git a/src/modules/overseas-warehouse/permissions.test.mjs b/src/modules/overseas-warehouse/permissions.test.mjs index cd405e3..b5c9791 100644 --- a/src/modules/overseas-warehouse/permissions.test.mjs +++ b/src/modules/overseas-warehouse/permissions.test.mjs @@ -20,9 +20,11 @@ test('protects every overseas warehouse page with its independent list permissio test('opens labeling and return relabeling on dedicated protected create pages', () => { assert.match(routes, /path: 'overseas-warehouse\/labeling\/create'[\s\S]*?props: \{ service: 'labeling', createPage: true \}[\s\S]*?permission: 'merchant:overseas-warehouse:relabel:create'/) - assert.match(routes, /path: 'overseas-warehouse\/return-relabel\/create'[\s\S]*?props: \{ service: 'relabel', createPage: true \}[\s\S]*?permission: 'merchant:overseas-warehouse:return-order:create'/) + assert.match(routes, /path: 'overseas-warehouse\/return-relabel\/create'[\s\S]*?ReturnOrdersView\.vue'[\s\S]*?props: \{ createPage: true \}[\s\S]*?title: '新建退货预报'[\s\S]*?permission: 'merchant:overseas-warehouse:return-order:create'/) assert.match(services, /props\.createPage \? 'create' : 'list'/) assert.match(services, /function returnToList\(\)/) + assert.match(returns, /props\.createPage/) + assert.match(returns, /class="return-create-surface"/) }) test('opens warehouse service and return receipt details as dedicated protected workspace pages', () => { diff --git a/src/modules/overseas-warehouse/routes.ts b/src/modules/overseas-warehouse/routes.ts index 93ee9f0..f9a225c 100644 --- a/src/modules/overseas-warehouse/routes.ts +++ b/src/modules/overseas-warehouse/routes.ts @@ -24,8 +24,8 @@ export const overseasWarehouseModule = { }, { path: 'overseas-warehouse/return-relabel/create', name: 'overseas-return-relabel-create', - component: () => import('./views/WarehouseServiceView.vue'), props: { service: 'relabel', createPage: true }, release: 'active', - meta: { title: '新建退货换标单', workspaceTitle: '退货换标 · 新建', description: '填写并提交退货换标订单', permission: 'merchant:overseas-warehouse:return-order:create' }, + component: () => import('./views/ReturnOrdersView.vue'), props: { createPage: true }, release: 'active', + meta: { title: '新建退货预报', workspaceTitle: '退货预报 · 新建', description: '填写并提交海外仓退货预报', permission: 'merchant:overseas-warehouse:return-order:create', keepAlive: false }, }, { path: 'overseas-warehouse/return-relabel/:returnNo/detail', name: 'overseas-return-relabel-detail', diff --git a/src/modules/overseas-warehouse/views/ReturnOrdersView.test.mjs b/src/modules/overseas-warehouse/views/ReturnOrdersView.test.mjs index 22d2073..77975ae 100644 --- a/src/modules/overseas-warehouse/views/ReturnOrdersView.test.mjs +++ b/src/modules/overseas-warehouse/views/ReturnOrdersView.test.mjs @@ -27,9 +27,13 @@ test('新建退货预报选择海外仓后带出联系人和电话', () => { assert.match(view, /warehouseContactPhone: form\.warehouseContactPhone\.trim\(\)/) }) -test('新建退货预报使用加宽的双列表单弹窗', () => { +test('新建退货预报使用独立路由页面,编辑仍保留加宽弹框', () => { + assert.match(view, /defineProps<\{ createPage\?: boolean; detailPage\?: boolean/) + assert.match(view, /name: 'overseas-return-relabel-create'/) + assert.match(view, /v-if="props\.createPage"/) + assert.match(view, /class="return-create-surface"/) + assert.match(view, /

新建退货预报<\/h2>/) assert.match(view, /:title="editingReturnNo \? '编辑退货预报' : '新建退货预报'" width="1120px"/) - assert.match(view, //) assert.match(view, /return-create-form \{ display:grid; grid-template-columns:repeat\(2,minmax\(0,1fr\)\)/) }) diff --git a/src/modules/overseas-warehouse/views/ReturnOrdersView.vue b/src/modules/overseas-warehouse/views/ReturnOrdersView.vue index 01de759..e8737cd 100644 --- a/src/modules/overseas-warehouse/views/ReturnOrdersView.vue +++ b/src/modules/overseas-warehouse/views/ReturnOrdersView.vue @@ -10,7 +10,7 @@ import { useAuthStore } from '@/modules/auth/public' import { getMerchantWarehouses, type MerchantWarehouse } from '@/modules/first-leg/public' import { createReturnOrder, downloadReturnPhoto, getReturnOrder, listReturnOrders, updateReturnOrder, type ReturnOrderDetail, type ReturnOrderSummary, type ReturnReceipt } from '../api/returnOrders' -const props = withDefaults(defineProps<{ detailPage?: boolean; returnNo?: string }>(), { detailPage: false, returnNo: '' }) +const props = withDefaults(defineProps<{ createPage?: boolean; detailPage?: boolean; returnNo?: string }>(), { createPage: false, detailPage: false, returnNo: '' }) const router = useRouter() const route = useRoute() const detailReturnNo = computed(() => props.returnNo || String(route.params.returnNo || '')) @@ -110,10 +110,7 @@ async function loadOperationWarehouses() { } finally { operationWarehousesLoading.value = false } } function openCreate() { - editingReturnNo.value = '' - resetForm() - showCreate.value = true - if (!operationWarehouses.value.length && !operationWarehousesLoading.value) void loadOperationWarehouses() + void router.push({ name: 'overseas-return-relabel-create' }) } async function openEdit(returnNo: string) { const detail = await getReturnOrder(returnNo) @@ -176,6 +173,7 @@ function previewPhoto(fileId: string | number) { } function openDetail(returnNo: string) { void router.push({ name: 'overseas-return-relabel-detail', params: { returnNo } }) } function returnToList() { void router.push({ name: 'overseas-return-relabel' }) } +function cancelCreate() { resetForm(); void router.push({ name: 'overseas-return-relabel' }) } function canApplyRelabel(order: { status?: unknown; needRelabel?: unknown; relabelNo?: unknown }) { return order.status === 'COMPLETED' && order.needRelabel === true && !order.relabelNo } function openRelabel(order: Pick) { void router.push({ name: 'overseas-labeling-create', query: { returnNo: order.returnNo, warehouseId: String(order.overseasWarehouseId) } }) @@ -218,19 +216,48 @@ function formatDateTime(value?: string | null) { return new Intl.DateTimeFormat('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }).format(date) } onMounted(() => { - if (props.detailPage) void Promise.all([loadDetail(), loadOperationWarehouses()]) + if (props.createPage) { resetForm(); void loadOperationWarehouses() } + else if (props.detailPage) void Promise.all([loadDetail(), loadOperationWarehouses()]) else void Promise.all([load(), loadOperationWarehouses()]) }) watch(() => [query.page, query.size] as const, ([nextPage, nextSize], [previousPage, previousSize]) => { if (nextSize !== previousSize && nextPage !== 1) { query.page = 1; return } - if (!props.detailPage) void load() + if (!props.createPage && !props.detailPage) void load() }) onBeforeUnmount(clearPhotoUrls)