| # |
- SKU NO.{{ form.destinationType === 'FBA' || form.fulfillmentType === 'ONE_CLICK_DROPSHIP' ? '必填' : '选填' }} | 中文品名 | 图片 | 英文品名 | 报关 HS CODE | 清关 HS CODE | 包装类型 | 材质 | 危险品 | 木制品勾选后须商检 |
+ SKU NO.{{ form.destinationType === 'FBA' || form.fulfillmentType === 'ONE_CLICK_DROPSHIP' ? '必填' : '选填' }} | 中文品名 | 图片 | 英文品名 | 报关 HS CODE | 清关 HS CODE | 包装类型 | 材质 | 危险品 | 木制品勾选后自动商检 |
箱规长 cm | 箱规宽 cm | 箱规高 cm | 每箱数量 | 箱数 | *总毛重 | *总体积 | 总数量 | 总净重 |
清关申报单价 USD | 清关申报金额 USD | 用途 | 型号 | 品牌 | 产品规格 长×宽×高 | 货件编号货件编号必填 | 跟踪编号跟踪编号必填 | UPC NO. | 备注 | 操作 |
@@ -2249,7 +2246,7 @@ async function searchOrders(model: QueryModel) {
|
| | |
同箱 | |
- | 第 {{ index + 1 }} 条须商检 |
+ | 自动商检 |
同上 | 同上 | 同上 |
| 同箱 | 同箱 | 同箱 | {{ item.pieces }} | 同箱 |
| | | | |
From 97d0f82256630e15a987996dc7b1e334b5f97b71 Mon Sep 17 00:00:00 2001
From: ouyangxq <775153912@qq.com>
Date: Sun, 20 Sep 2026 21:16:20 +0800
Subject: [PATCH 3/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
---
.../views/FirstLegOrderCreateHubView.vue | 3 ++-
.../first-leg/views/FirstLegOrdersView.test.mjs | 9 +++++++--
.../first-leg/views/FirstLegOrdersView.vue | 15 ++++++++-------
3 files changed, 17 insertions(+), 10 deletions(-)
diff --git a/src/modules/first-leg/views/FirstLegOrderCreateHubView.vue b/src/modules/first-leg/views/FirstLegOrderCreateHubView.vue
index 90ed0b8..8fc576c 100644
--- a/src/modules/first-leg/views/FirstLegOrderCreateHubView.vue
+++ b/src/modules/first-leg/views/FirstLegOrderCreateHubView.vue
@@ -23,6 +23,7 @@ function selectMode(mode: 'standard' | 'quick') {
+ 头程下单
diff --git a/src/modules/first-leg/views/FirstLegOrdersView.test.mjs b/src/modules/first-leg/views/FirstLegOrdersView.test.mjs
index 222f461..e076417 100644
--- a/src/modules/first-leg/views/FirstLegOrdersView.test.mjs
+++ b/src/modules/first-leg/views/FirstLegOrdersView.test.mjs
@@ -375,7 +375,7 @@ test('collects the backend-required first-leg fields', () => {
assert.match(source, /form\.customsInspection = form\.items\.some\(\(item\) => item\.woodenProduct\) \? true : null/)
assert.match(source, /木制品勾选后自动商检<\/small>/)
assert.doesNotMatch(source, /label="是否商检"/)
- assert.match(source, /第 \{\{ index \+ 1 \}\} 条须商检/)
+ assert.match(source, /自动商检<\/small>/)
})
test('distills order creation into the confirmed prototype sections with server pricing', () => {
@@ -441,7 +441,12 @@ test('matches the prototype two-column create layout and reset action', () => {
assert.match(editorShellSource, /submit-gradient 5s ease-in-out infinite/)
assert.match(editorShellSource, /submit-shine 3\.2s ease-in-out infinite/)
assert.match(editorShellSource, /@media \(prefers-reduced-motion: reduce\)/)
- assert.doesNotMatch(createHubSource, /头程下单<\/h1>|填写货物信息,创建新的头程运输订单/)
+ assert.match(createHubSource, /头程下单<\/h1>/)
+ 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.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/)
})
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 90868a8..e6b078c 100644
--- a/src/modules/first-leg/views/FirstLegOrdersView.vue
+++ b/src/modules/first-leg/views/FirstLegOrdersView.vue
@@ -2550,14 +2550,14 @@ 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: stretch; gap: 16px; margin: 0; padding: 0 0 16px; }
-.is-create-route .prototype-section { min-width: 0; margin: 0; padding: 0 24px 24px; 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: 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-section + .prototype-section { margin-top: 0; }
-.is-create-route .order-basic-section, .is-create-route .destination-section { height: 100%; }
.is-create-route .cargo-section, .is-create-route .cost-estimate-section { grid-column: 1 / -1; }
-.is-create-route .order-section-heading { min-height: 66px; align-items: center; margin: 0 -24px 22px; padding: 15px 24px; border-bottom: 1px solid #e7ecf2; }
-.is-create-route .order-section-heading h3, .is-create-route .prototype-section:first-child > .order-section-heading h3 { margin: 0; color: var(--navy); font-size: 17px; font-weight: 700; }
-.is-create-route .order-section-heading p { margin-top: 4px; color: #8a98a8; }
+.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 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 .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; }
@@ -2566,12 +2566,13 @@ async function searchOrders(model: QueryModel) {
.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; }
.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; }
.order-form :deep(.el-form-item__error) { position: static; padding-top: 6px; overflow-wrap: anywhere; line-height: 1.45; }
-.order-form-section + .order-form-section { margin-top: 30px; padding-top: 28px; border-top: 1px solid #edf0f5; }
+.order-form-section + .order-form-section { border-top: 1px solid #edf0f5; }
.order-form-section h3 { margin: 0 0 16px; color: var(--navy); font-size: 16px; }
.order-form-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); column-gap: 18px; }
.order-form-grid > *, .cargo-form-grid > * { min-width: 0; }
From 5887837407a327d8fd90e4017b25f36fff62e470 Mon Sep 17 00:00:00 2001
From: ouyangxq <775153912@qq.com>
Date: Sun, 20 Sep 2026 21:44:56 +0800
Subject: [PATCH 4/6] feat: add production blue-green frontend deployment
---
docker-compose.production.yml | 8 +-
scripts/deploy-production.sh | 113 +++++++++++++++-----
src/deployment/blueGreenDeployment.test.mjs | 24 +++++
src/deployment/nginxConfig.test.mjs | 4 +-
4 files changed, 118 insertions(+), 31 deletions(-)
create mode 100644 src/deployment/blueGreenDeployment.test.mjs
diff --git a/docker-compose.production.yml b/docker-compose.production.yml
index 167b779..d721c62 100644
--- a/docker-compose.production.yml
+++ b/docker-compose.production.yml
@@ -8,12 +8,16 @@ services:
ports:
- "127.0.0.1:${HOST_PORT:-9002}:80"
environment:
- API_UPSTREAM: "http://gateway:9000"
- OVERSEAS_API_UPSTREAM: "http://gateway-overseas:9300"
+ API_UPSTREAM: "http://gateway-active:9000"
+ OVERSEAS_API_UPSTREAM: "http://gateway-overseas-active:9300"
networks:
- infra-network
+ - routing-network
networks:
infra-network:
external: true
name: firstleg-prod-infra-network
+ routing-network:
+ external: true
+ name: firstleg-production-routing
diff --git a/scripts/deploy-production.sh b/scripts/deploy-production.sh
index 821a711..e3499ef 100644
--- a/scripts/deploy-production.sh
+++ b/scripts/deploy-production.sh
@@ -59,35 +59,92 @@ compose() {
docker compose --project-name "$project_name" -f "$compose_file" "$@"
}
-container_id="$(compose ps --all --quiet "$service" | head -n 1)"
-previous_image=""
-if [ -n "$container_id" ]; then
- previous_image="$(docker inspect --format '{{.Config.Image}}' "$container_id")"
-fi
-
-if compose up -d --no-deps --pull never "$service" && \
- compose exec -T "$service" wget -qO- http://127.0.0.1/healthz >/dev/null; then
- echo "生产前端部署成功: $image_repository:$image_tag"
- compose ps "$service"
- exit 0
-fi
-
-echo "生产前端健康检查失败,准备回滚" >&2
-compose logs --tail=100 "$service" >&2 || true
-case "$previous_image" in
- "$image_repository:"*) ;;
- '') echo "首次部署无旧镜像可回滚" >&2; exit 1 ;;
- *) echo "旧镜像不属于当前应用,拒绝回滚: $previous_image" >&2; exit 1 ;;
+case "$service" in
+ admin-web)
+ base_port=9001
+ nginx_config=/www/server/panel/vhost/nginx/admin.buggoglobal.com.conf
+ legacy_container=firstleg-prod-admin-web
+ ;;
+ client-web)
+ base_port=9002
+ nginx_config=/www/server/panel/vhost/nginx/app.buggoglobal.com.conf
+ legacy_container=firstleg-prod-client-web
+ ;;
+ *) echo "不支持蓝绿部署的前端服务: $service" >&2; exit 2 ;;
esac
-previous_tag="${previous_image#"$image_repository:"}"
-if [ "$previous_tag" = "$image_tag" ]; then
- echo "旧镜像与新镜像一致,无法回滚" >&2
+
+state_root=/data/firstleg/deploy/blue-green-state
+state_file="$state_root/frontend-$service.active"
+routing_network=firstleg-production-routing
+mkdir -p "$state_root"
+chmod 750 "$state_root"
+docker network inspect "$routing_network" >/dev/null 2>&1 || docker network create "$routing_network" >/dev/null
+
+for pair in gateway-active:firstleg-production-gateway gateway-overseas-active:firstleg-production-gateway-overseas; do
+ alias_name="${pair%%:*}"
+ container="${pair#*:}"
+ if docker inspect "$container" >/dev/null 2>&1; then
+ docker network inspect "$routing_network" --format '{{json .Containers}}' | grep -q "\"$container\"" \
+ || docker network connect --alias "$alias_name" "$routing_network" "$container"
+ fi
+done
+
+active_slot=""
+active_container=""
+active_port=""
+if [ -s "$state_file" ]; then
+ IFS='|' read -r active_slot active_container active_port active_image < "$state_file"
+elif docker inspect "$legacy_container" >/dev/null 2>&1; then
+ active_slot=legacy
+ active_container="$legacy_container"
+ active_port="$base_port"
+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
+candidate="${legacy_container}-${target_slot}"
+docker rm -f "$candidate" >/dev/null 2>&1 || true
+
+if ! compose run -d --no-deps --name "$candidate" \
+ -p "127.0.0.1:$target_port:80" "$service" >/dev/null; then
+ docker rm -f "$candidate" >/dev/null 2>&1 || true
exit 1
fi
-docker pull "$previous_image"
-export IMAGE_TAG="$previous_tag"
-compose up -d --no-deps --pull never "$service"
-compose exec -T "$service" wget -qO- http://127.0.0.1/healthz >/dev/null || true
-echo "已回滚至: $previous_image" >&2
-exit 1
+
+healthy=false
+elapsed=0
+while [ "$elapsed" -lt 120 ]; do
+ if docker exec "$candidate" wget -qO- http://127.0.0.1/healthz >/dev/null 2>&1; then
+ healthy=true
+ break
+ fi
+ sleep 2
+ elapsed=$((elapsed + 2))
+done
+if [ "$healthy" != true ]; then
+ docker logs --tail 100 "$candidate" >&2 || true
+ docker rm -f "$candidate" >/dev/null 2>&1 || true
+ exit 1
+fi
+
+backup="$nginx_config.blue-green-backup"
+cp -f "$nginx_config" "$backup"
+if ! sed -i "s#127\.0\.0\.1:${active_port:-$base_port}#127.0.0.1:$target_port#g" "$nginx_config" \
+ || cmp -s "$nginx_config" "$backup" || ! nginx -t || ! nginx -s reload; then
+ cp -f "$backup" "$nginx_config"
+ nginx -t && nginx -s reload || true
+ docker rm -f "$candidate" >/dev/null 2>&1 || true
+ exit 1
+fi
+rm -f "$backup"
+
+if [ -n "$active_container" ] && docker inspect "$active_container" >/dev/null 2>&1; then
+ docker stop --time 30 "$active_container" >/dev/null
+ docker rm "$active_container" >/dev/null
+fi
+printf '%s|%s|%s|%s\n' "$target_slot" "$candidate" "$target_port" "$image_repository:$image_tag" > "$state_file.tmp"
+chmod 640 "$state_file.tmp"
+mv -f "$state_file.tmp" "$state_file"
+echo "生产前端蓝绿部署成功: service=$service slot=$target_slot port=$target_port image=$image_repository:$image_tag"
REMOTE_DEPLOY
diff --git a/src/deployment/blueGreenDeployment.test.mjs b/src/deployment/blueGreenDeployment.test.mjs
new file mode 100644
index 0000000..8576a08
--- /dev/null
+++ b/src/deployment/blueGreenDeployment.test.mjs
@@ -0,0 +1,24 @@
+import assert from 'node:assert/strict'
+import { readFile } from 'node:fs/promises'
+import test from 'node:test'
+
+const script = (await readFile(new URL('../../scripts/deploy-production.sh', import.meta.url), 'utf8'))
+ .replaceAll('\r\n', '\n')
+const compose = (await readFile(new URL('../../docker-compose.production.yml', import.meta.url), 'utf8'))
+ .replaceAll('\r\n', '\n')
+
+test('client production deployment switches healthy blue-green slots atomically', () => {
+ const start = script.indexOf('compose run -d --no-deps --name "$candidate"')
+ const health = script.indexOf('docker exec "$candidate" wget')
+ 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 \+ 10000\)\)/)
+ assert.match(script, /cp -f "\$backup" "\$nginx_config"/)
+})
+
+test('client uses stable active gateway aliases on the routing network', () => {
+ assert.match(compose, /API_UPSTREAM: "http:\/\/gateway-active:9000"/)
+ assert.match(compose, /OVERSEAS_API_UPSTREAM: "http:\/\/gateway-overseas-active:9300"/)
+ assert.match(compose, /name: firstleg-production-routing/)
+})
diff --git a/src/deployment/nginxConfig.test.mjs b/src/deployment/nginxConfig.test.mjs
index 2a1ced2..1934f09 100644
--- a/src/deployment/nginxConfig.test.mjs
+++ b/src/deployment/nginxConfig.test.mjs
@@ -49,7 +49,9 @@ test('keeps local environments on target nodes and uses credential-managed SSH f
assert.match(jenkinsfile, /credentialsId: 'firstleg-production-ssh'/)
assert.match(jenkinsfile, /git rev-parse origin\/main/)
assert.match(productionCompose, /127\.0\.0\.1:\$\{HOST_PORT:-9002\}:80/)
- assert.match(productionCompose, /API_UPSTREAM: "http:\/\/gateway:9000"/)
+ assert.match(productionCompose, /API_UPSTREAM: "http:\/\/gateway-active:9000"/)
+ assert.match(productionCompose, /OVERSEAS_API_UPSTREAM: "http:\/\/gateway-overseas-active:9300"/)
+ assert.match(productionCompose, /name: firstleg-production-routing/)
assert.match(productionCompose, /name: firstleg-prod-infra-network/)
})
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 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 +++--
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, /