From 893adc0d5639984a648656978e93aedf21440dea Mon Sep 17 00:00:00 2001 From: zzh <1368735060@qq.com> Date: Fri, 23 Oct 2026 11:19:14 +0800 Subject: [PATCH 1/6] =?UTF-8?q?fix:=20=E9=9D=A2=E5=8D=95=E6=9F=A5=E7=9C=8B?= =?UTF-8?q?=E7=BB=95=E8=BF=87=20COS=20=E7=AD=BE=E5=90=8D=E5=88=86=E5=8F=B7?= =?UTF-8?q?=E6=8B=86=E5=9D=8F=EF=BC=8C=E5=B9=B6=E8=A1=A5=20SKU=20=E4=BA=A7?= =?UTF-8?q?=E5=93=81=E6=A0=87=E8=AF=86=E4=B8=8E=E6=9F=A5=E4=BB=B7=E6=8F=8F?= =?UTF-8?q?=E8=BF=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 查看面单改走 stream/blob,代理 URL 将 ; 编成 %3B;SKU 申报扩展与查价卡片描述展示一并调整。 Co-authored-by: Cursor --- nginx-k3s.conf | 3 +- src/api/oms/common.ts | 12 ++++- .../PrintTemplate/OrderPrintModal.vue | 41 ++++++++++++--- src/locale/en-US.ts | 11 +++- src/locale/zh-CN.ts | 10 +++- src/utils/printPreview.ts | 10 +++- .../priceCheck/components/QuoteResultCard.vue | 32 +++++++----- src/views/warehouse/sku/index.vue | 52 +++++++++++++++++++ 8 files changed, 144 insertions(+), 27 deletions(-) diff --git a/nginx-k3s.conf b/nginx-k3s.conf index 6b3f6ab..5785f83 100644 --- a/nginx-k3s.conf +++ b/nginx-k3s.conf @@ -156,7 +156,8 @@ server { location ^~ /marketing/referralTree { try_files $uri @spa; } # 面单/截图同源代理:/__cos__// → http:/// - # cos.ideship.com 自定义域名 https 证书不可用,上游走 http;避免浏览器直打 CORS / 混合内容 + # cos.* 自定义域 https 证书常仍是 *.myqcloud.com;上游继续走 http。 + # COS 签名 q-sign-time 含 ";":前端须编成 %3B,否则查询串被按 ";" 拆参 → Request has expired。 location ~ ^/__cos__/([^/]+)(/.*)$ { set $cos_host $1; set $cos_uri $2; diff --git a/src/api/oms/common.ts b/src/api/oms/common.ts index 7af3918..0367c20 100644 --- a/src/api/oms/common.ts +++ b/src/api/oms/common.ts @@ -104,6 +104,16 @@ export async function omsPost(path: string, body?: Record) return unwrapData(res.data) } +/** + * COS 签名参数里的 `q-sign-time=start;end` 含分号。 + * 线上 nginx / 部分网关会把 `;` 当查询分隔符,拆成 `q-sign-time=start&end=…`, + * COS 随即返回 AccessDenied / Request has expired;本地 Vite 代理通常不拆,故本地正常。 + */ +export function encodeCosSignedQuery(search: string): string { + if (!search) return '' + return search.replace(/;/g, '%3B') +} + /** * COS 绝对地址 → 同源 `/__cos__//...`(开发 Vite / 生产 nginx 反代)。 * cos.ideship.com 等自定义域名 https 不可用,直链 http 又会在 https 页面触发混合内容。 @@ -113,7 +123,7 @@ function toCosSameOriginProxyUrl(absoluteUrl: string): string | null { const u = new URL(absoluteUrl) const host = u.hostname.toLowerCase() if (!host.startsWith('cos.') && !host.includes('myqcloud.com')) return null - return `/__cos__/${u.host}${u.pathname}${u.search}` + return `/__cos__/${u.host}${u.pathname}${encodeCosSignedQuery(u.search)}` } catch { return null } diff --git a/src/components/PrintTemplate/OrderPrintModal.vue b/src/components/PrintTemplate/OrderPrintModal.vue index c3b99cc..c9126ef 100644 --- a/src/components/PrintTemplate/OrderPrintModal.vue +++ b/src/components/PrintTemplate/OrderPrintModal.vue @@ -101,7 +101,7 @@ import { import { useHiprintClientState } from '@/utils/hiprint/hiprintClientState'; import { resolveOmsAssetUrl } from '@/api/oms/common'; import PrintOptionsForm from '@/components/PrintTemplate/PrintOptionsForm.vue'; -import { fetchPrintAssetBytes } from '@/utils/printPreview'; +import { fetchPrintAssetBytes, fetchPrintPreviewBlob } from '@/utils/printPreview'; import { buildHiprintBlobPrintExtras, HIPRINT_PAPER_PRESET_OPTIONS, @@ -258,12 +258,37 @@ function toAbsoluteUrl(url: string) { return `${window.location.origin}/${resolved}`; } -function openItemInNative(item: PrintItem) { +/** + * 查看面单:经 file-storage/stream(或 /__cos__)拉字节再开 blob。 + * 勿把带 `q-sign-time=a;b` 的 COS 链直接丢给浏览器——线上反代/跳转常把 `;` 拆成 `&`, + * COS 会回 AccessDenied/Request has expired(本地 Vite 不拆,故本地正常)。 + */ +async function openItemInNative(item: PrintItem) { if (!item?.url) { Message.warning('当前无可打开的打印链接'); return; } - window.open(toAbsoluteUrl(item.url) || item.url, '_blank', 'noopener,noreferrer'); + // 同步开空白页,避免 await 后被 Chrome 拦截弹窗 + const win = window.open('about:blank', '_blank'); + if (!win) { + Message.warning('请允许浏览器弹窗以查看面单'); + return; + } + try { + win.document.title = '面单加载中…'; + const blob = await fetchPrintPreviewBlob(item.url); + const blobUrl = URL.createObjectURL(blob); + win.location.replace(blobUrl); + window.setTimeout(() => URL.revokeObjectURL(blobUrl), 60_000); + } catch (e: unknown) { + try { + win.close(); + } catch { + // ignore + } + const msg = e instanceof Error ? e.message : String(e || ''); + Message.warning(msg || '面单打开失败'); + } } function openActiveInNative() { @@ -271,7 +296,7 @@ function openActiveInNative() { Message.warning('当前无可打开的打印链接'); return; } - openItemInNative(activeItem.value); + void openItemInNative(activeItem.value); } async function silentPrintByUrl(url: string) { @@ -320,8 +345,8 @@ async function printActive() { flashPrintSuccess(); Message.success('已发送到本机打印'); } catch (e: any) { - Message.warning(e?.message || '自动打印失败,将尝试浏览器打印'); - window.open(toAbsoluteUrl(activeItem.value.url) || activeItem.value.url, '_blank', 'noopener,noreferrer'); + Message.warning(e?.message || '自动打印失败,将尝试浏览器打开面单'); + await openItemInNative(activeItem.value); } finally { printingActive.value = false; } @@ -339,8 +364,8 @@ async function printAll() { printingAll.value = true; try { const tasks = printableItems.value.map((item) => - silentPrintByUrl(item.url).catch(() => { - window.open(toAbsoluteUrl(item.url) || item.url, '_blank', 'noopener,noreferrer'); + silentPrintByUrl(item.url).catch(async () => { + await openItemInNative(item); }), ); const results = await Promise.allSettled(tasks); diff --git a/src/locale/en-US.ts b/src/locale/en-US.ts index 4cb84b6..cb30d39 100644 --- a/src/locale/en-US.ts +++ b/src/locale/en-US.ts @@ -178,7 +178,14 @@ export default { 'warehouse.sku.form.imHsCodePlaceholder': 'Import HS code (optional)', 'warehouse.sku.form.declareElement': 'Export Declare Element', 'warehouse.sku.form.declareElementPlaceholder': 'Customs export declare elements (optional)', - 'warehouse.sku.form.showExtra': 'Show declaration extras (EN name / HS / declare element / remark)', + 'warehouse.sku.form.productId': 'Product ID', + 'warehouse.sku.form.productIdPlaceholder': 'Product ID (optional, max 100)', + 'warehouse.sku.form.nonStdProductId': 'Non-standard product ID', + 'warehouse.sku.form.nonStdProductIdPlaceholder': 'Non-standard product ID (optional, max 100)', + 'warehouse.sku.form.stdProductId': 'Standard product ID', + 'warehouse.sku.form.stdProductIdPlaceholder': 'e.g. GTIN / UPC / EAN (optional, max 100)', + 'warehouse.sku.form.showExtra': + 'Show declaration extras (EN name / HS / declare element / product IDs / remark)', 'warehouse.sku.form.hideExtra': 'Hide declaration extras', 'warehouse.modal.section.declarationExtra': 'Declaration extras', 'warehouse.sku.form.weightG': 'Weight', @@ -199,6 +206,8 @@ export default { 'warehouse.sku.tip.weightSize': 'Used for billing and packing estimates; all optional', 'warehouse.sku.tip.volume': 'Auto-estimated from L×W×H; shows - if size is empty', 'warehouse.sku.tip.skuNameEn': 'English name for customs declaration', + 'warehouse.sku.tip.stdProductId': + 'Enter a standard barcode such as GTIN-14, UPC-A, or EAN-13. Leave empty if none; use non-standard product ID instead.', 'warehouse.common.noCurrentWarehouse': 'Please select a warehouse in the top-right first', 'warehouse.sku.form.dimensions': 'Dimensions', 'warehouse.sku.form.length': 'L', diff --git a/src/locale/zh-CN.ts b/src/locale/zh-CN.ts index e80d0ab..e395522 100644 --- a/src/locale/zh-CN.ts +++ b/src/locale/zh-CN.ts @@ -179,7 +179,13 @@ export default { 'warehouse.sku.form.imHsCodePlaceholder': '进口海关编码(选填)', 'warehouse.sku.form.declareElement': '出口申报要素', 'warehouse.sku.form.declareElementPlaceholder': '海关出口申报要素(选填)', - 'warehouse.sku.form.showExtra': '展开申报扩展(英文名 / HS / 申报要素 / 备注)', + 'warehouse.sku.form.productId': '产品标识编码', + 'warehouse.sku.form.productIdPlaceholder': '产品标识编码(选填,最长 100)', + 'warehouse.sku.form.nonStdProductId': '非标产品标识编码', + 'warehouse.sku.form.nonStdProductIdPlaceholder': '非标产品标识编码(选填,最长 100)', + 'warehouse.sku.form.stdProductId': '标准产品标识编码', + 'warehouse.sku.form.stdProductIdPlaceholder': '如 GTIN / UPC / EAN(选填,最长 100)', + 'warehouse.sku.form.showExtra': '展开申报扩展(英文名 / HS / 申报要素 / 产品标识 / 备注)', 'warehouse.sku.form.hideExtra': '收起申报扩展', 'warehouse.modal.section.declarationExtra': '申报扩展信息', 'warehouse.sku.form.weightG': '重量', @@ -200,6 +206,8 @@ export default { 'warehouse.sku.tip.weightSize': '重量与尺寸用于计费与装箱估算,均可选填', 'warehouse.sku.tip.volume': '由长×宽×高自动估算,未填尺寸时显示 -', 'warehouse.sku.tip.skuNameEn': '用于报关申报的英文品名', + 'warehouse.sku.tip.stdProductId': + '填写国际通用标准商品条码,如 GTIN-14、UPC-A、EAN-13;无条码可留空,改用非标产品标识编码', 'warehouse.common.noCurrentWarehouse': '请先在右上角选择仓库', 'warehouse.sku.form.dimensions': '尺寸', 'warehouse.sku.form.length': '长', diff --git a/src/utils/printPreview.ts b/src/utils/printPreview.ts index c1af762..373d977 100644 --- a/src/utils/printPreview.ts +++ b/src/utils/printPreview.ts @@ -1,6 +1,11 @@ import { axiosInstance } from '@/api/rbac' import { normalizeFileStorageKey, streamFileBlob } from '@/api/fileStorage' -import { extractOmsRequestError, omsUrl, resolveOmsAssetUrl } from '@/api/oms/common' +import { + encodeCosSignedQuery, + extractOmsRequestError, + omsUrl, + resolveOmsAssetUrl, +} from '@/api/oms/common' const COS_PROXY_PREFIX = '/__cos__' const PREVIEW_FETCH_TIMEOUT_MS = 25000 @@ -49,7 +54,8 @@ export function toCosProxyUrl(url: string): string | null { try { const u = new URL(url) if (!isCosHost(u.host)) return null - return `${COS_PROXY_PREFIX}/${u.host}${u.pathname}${u.search}` + // 分号必须 %3B,否则线上反代会拆坏签名时间窗(见 encodeCosSignedQuery) + return `${COS_PROXY_PREFIX}/${u.host}${u.pathname}${encodeCosSignedQuery(u.search)}` } catch { return null } diff --git a/src/views/order/priceCheck/components/QuoteResultCard.vue b/src/views/order/priceCheck/components/QuoteResultCard.vue index f2637b9..6155f72 100644 --- a/src/views/order/priceCheck/components/QuoteResultCard.vue +++ b/src/views/order/priceCheck/components/QuoteResultCard.vue @@ -88,14 +88,20 @@ String(props.record.productDescription || '').trim(), ) - /** 每满 25 字换一行,最多展示两行 */ + /** 每满 32 字换一行,最多展示三行(超出靠 CSS clamp + title 悬停看全量) */ const productDescriptionDisplay = computed(() => { const text = productDescription.value || t('order.priceCheck.card.missingProductDescription') - if (text.length <= 25) return text - const first = text.slice(0, 25) - const rest = text.slice(25, 50) - return rest ? `${first}\n${rest}` : first + const perLine = 32 + const maxLines = 3 + const maxLen = perLine * maxLines + const clipped = text.length > maxLen ? text.slice(0, maxLen) : text + if (clipped.length <= perLine) return clipped + const lines: string[] = [] + for (let i = 0; i < clipped.length; i += perLine) { + lines.push(clipped.slice(i, i + perLine)) + } + return lines.join('\n') }) const hasFeeDetail = computed(() => { @@ -398,8 +404,8 @@ .quote-card__body { display: grid; grid-template-columns: - minmax(200px, 0.78fr) - minmax(240px, 1.45fr) + minmax(200px, 0.72fr) + minmax(300px, 1.7fr) auto auto; align-items: center; @@ -459,13 +465,13 @@ .quote-card__desc { margin: 0; min-width: 0; - max-height: 2.9em; + max-height: calc(1.45em * 3); color: var(--color-text-2); font-size: 12px; line-height: 1.45; - display: flex; - align-items: center; - justify-content: center; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 3; text-align: center; white-space: pre-line; word-break: break-word; @@ -778,8 +784,8 @@ .quote-card__body { column-gap: 16px; grid-template-columns: - minmax(170px, 0.75fr) - minmax(180px, 1.3fr) + minmax(170px, 0.7fr) + minmax(240px, 1.5fr) auto auto; } diff --git a/src/views/warehouse/sku/index.vue b/src/views/warehouse/sku/index.vue index 7e91c9b..5a9a505 100644 --- a/src/views/warehouse/sku/index.vue +++ b/src/views/warehouse/sku/index.vue @@ -81,6 +81,9 @@ const formModel = reactive({ exHsCode: '', imHsCode: '', declareElement: '', + productId: '', + nonStdProductId: '', + stdProductId: '', weightG: undefined as number | undefined, lengthCm: undefined as number | undefined, widthCm: undefined as number | undefined, @@ -152,6 +155,9 @@ function hasExtraFieldValues() { || formModel.exHsCode.trim() || formModel.imHsCode.trim() || formModel.declareElement.trim() + || formModel.productId.trim() + || formModel.nonStdProductId.trim() + || formModel.stdProductId.trim() || formModel.remark.trim() ) } @@ -198,6 +204,9 @@ function resetForm() { formModel.exHsCode = '' formModel.imHsCode = '' formModel.declareElement = '' + formModel.productId = '' + formModel.nonStdProductId = '' + formModel.stdProductId = '' formModel.weightG = undefined formModel.lengthCm = undefined formModel.widthCm = undefined @@ -383,6 +392,9 @@ function handleEdit(row: any) { formModel.exHsCode = String(pickField(raw, 'exHsCode', 'ExHsCode') ?? '') formModel.imHsCode = String(pickField(raw, 'imHsCode', 'ImHsCode') ?? '') formModel.declareElement = String(pickField(raw, 'declareElement', 'DeclareElement') ?? '') + formModel.productId = String(pickField(raw, 'productId', 'ProductId') ?? '') + formModel.nonStdProductId = String(pickField(raw, 'nonStdProductId', 'NonStdProductId') ?? '') + formModel.stdProductId = String(pickField(raw, 'stdProductId', 'StdProductId') ?? '') formModel.weightG = pickField(raw, 'weightG', 'WeightG') formModel.lengthCm = pickField(raw, 'lengthCm', 'LengthCm') formModel.widthCm = pickField(raw, 'widthCm', 'WidthCm') @@ -765,6 +777,46 @@ onMounted(async () => { :auto-size="{ minRows: 1, maxRows: 3 }" /> + + + + + + + + + + + + + + + + + + Date: Fri, 23 Oct 2026 12:24:36 +0800 Subject: [PATCH 2/6] =?UTF-8?q?chore:=20=E5=89=8D=E7=AB=AF=E9=83=A8?= =?UTF-8?q?=E7=BD=B2=E5=8E=BB=E6=8E=89=20nginx=EF=BC=8C=E9=9D=99=E6=80=81?= =?UTF-8?q?=E7=94=B1=20serve=EF=BC=8CTLS=20=E4=BA=A4=20Traefik?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 生产不再内置 nginx/`__cos__`;镜像用 serve -s,HTTPS 由 Traefik+cert-manager;面单预览走 stream。 Co-authored-by: Cursor --- .cursor/rules/frontend-design-spec.mdc | 2 +- .dockerignore | 4 +- Dockerfile | 16 +- Dockerfile.k3s | 13 +- README.md | 9 +- docs/H5客户中心设计方案.md | 2 +- docs/前端页面设计规范.md | 28 +- docs/在线客服模块设计方案.md | 20 +- docs/小红书模块联调与验收.md | 16 +- docs/小红书模块设计-063001.md | 14 +- docs/微信登录整合方案.md | 13 +- nginx-default.conf | 489 ------------------ nginx-docker.conf | 279 ---------- nginx-k3s.conf | 226 -------- src/api/mcp/index.ts | 2 +- src/api/oms/common.ts | 8 +- .../PrintTemplate/OrderPrintModal.vue | 25 +- src/utils/printPreview.ts | 93 +++- .../order/components/OrderAttachmentsTab.vue | 39 +- vite.config.ts | 4 +- 20 files changed, 162 insertions(+), 1140 deletions(-) delete mode 100644 nginx-default.conf delete mode 100644 nginx-docker.conf delete mode 100644 nginx-k3s.conf diff --git a/.cursor/rules/frontend-design-spec.mdc b/.cursor/rules/frontend-design-spec.mdc index bbe5f88..e2614eb 100644 --- a/.cursor/rules/frontend-design-spec.mdc +++ b/.cursor/rules/frontend-design-spec.mdc @@ -33,7 +33,7 @@ alwaysApply: true - HandsontablePro 固定列序:操作(可选,内容自适应宽)→ 序号(50px)→ 复选框(可选,60px)→ 数据;`stretch-columns` 只拉宽数据列,勿再传 `action-column-position` / `action-column-width` - 列表页高度:`flex: 1; min-height: 0`,`.list-wrap` 勿写死 min-height - 分页 10/30/50/100,默认 50 -- 新页面 path 与 API 前缀冲突时须配置 nginx SPA 回退(见规范 §十五) +- 生产前后端分域名 + Traefik TLS;前端容器仅静态 SPA(见规范 §十五) ### 权限 - `hasPermission('Module.Resource.Action')` 控制按钮/路由 diff --git a/.dockerignore b/.dockerignore index 4caeb06..5674289 100644 --- a/.dockerignore +++ b/.dockerignore @@ -26,7 +26,5 @@ xhs-agent-desktop .env* !.env.production -# 本地/旧部署用的 nginx,k3s 用 nginx-k3s.conf -nginx-default.conf -nginx-docker.conf +# 本地简易镜像另用 Dockerfile;k3s 构建用 Dockerfile.k3s Dockerfile diff --git a/Dockerfile b/Dockerfile index f7f517b..0112129 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,9 +1,9 @@ -FROM nginx:1.27-alpine - -# 静态资源 -COPY dist/ /usr/share/nginx/html/ - -# Docker 内只用 HTTP:80,SSL 在宿主机 nginx 或网关层处理 -COPY nginx-docker.conf /etc/nginx/conf.d/default.conf - +# 本地/简易 Docker:仅静态 SPA。HTTPS 由外层网关(如 Traefik)终结。 +# 构建前先执行:pnpm build +FROM docker.m.daocloud.io/library/node:20-alpine +WORKDIR /app +RUN npm config set registry https://registry.npmmirror.com \ + && npm install -g serve@14.2.4 +COPY dist/ ./dist/ EXPOSE 80 +CMD ["serve", "-s", "dist", "-l", "80"] diff --git a/Dockerfile.k3s b/Dockerfile.k3s index baaa055..88fa85e 100644 --- a/Dockerfile.k3s +++ b/Dockerfile.k3s @@ -1,4 +1,5 @@ -# k3s / Kaniko:静态 SPA(方案 A)。API 由浏览器按站点域名直连 +# k3s / Kaniko:静态 SPA(方案 A)。TLS 由 Traefik(websecure) + cert-manager 终结。 +# API 由浏览器按站点域名直连: # test.ideship.com → api.test.ideship.com;其余 → api.ideship.com # FRONT_BUILD_SCRIPT: # prod 默认 build(.env.production → api.ideship.com) @@ -16,7 +17,11 @@ RUN pnpm config set registry https://registry.npmmirror.com \ COPY . . RUN pnpm run ${FRONT_BUILD_SCRIPT} -FROM docker.m.daocloud.io/library/nginx:1.27-alpine -COPY --from=build /app/dist /usr/share/nginx/html/ -COPY nginx-k3s.conf /etc/nginx/conf.d/default.conf +# 仅静态托管 + SPA fallback;不再内置 nginx(网关层 Traefik 负责 HTTPS) +FROM docker.m.daocloud.io/library/node:20-alpine +WORKDIR /app +RUN npm config set registry https://registry.npmmirror.com \ + && npm install -g serve@14.2.4 +COPY --from=build /app/dist ./dist EXPOSE 80 +CMD ["serve", "-s", "dist", "-l", "80"] diff --git a/README.md b/README.md index ee7b479..5a4a972 100644 --- a/README.md +++ b/README.md @@ -98,17 +98,18 @@ npm run lint 1. 安装依赖 `pnpm i` 2. 构建 `pnpm build`(生成 `dist/`) -3. **宿主机 nginx(推荐)**:将 `dist/` 放到 `/usr/share/nginx/html`,使用 `nginx-default.conf`(含 SSL,证书在 `/etc/nginx/ssl/`) -4. **Docker 方式(7962 端口)**:使用 `nginx-docker.conf`(仅 HTTP,不含 SSL) +3. **k3s(推荐)**:`Dockerfile.k3s` 多阶段构建,容器内用 `serve` 提供静态 SPA(`:80`);**HTTPS 由 Traefik `websecure` + cert-manager(Let's Encrypt)终结**,经 Ingress(tls) → Service,不在前端容器内挂证书。 +4. **本地简易 Docker**:先 `pnpm build`,再 `docker build -t web1 .`(`Dockerfile` 拷贝已有 `dist/`) ```bash +# 本地试跑静态容器(HTTP:7962 → 容器 80) docker build -t web1 . docker rm -f web docker run -d -p 7962:80 --name web web1 -docker ps | grep web # 确认 Up 状态 +docker ps | grep web ``` -> 勿把 `nginx-default.conf`(含 SSL)直接打进 Docker,容器内没有 `/etc/nginx/ssl/` 会导致启动失败。 +> 生产勿在前端镜像内配置 SSL;证书由集群 cert-manager 写入 Secret,Traefik Ingress 引用即可。 ## 贡献指南 diff --git a/docs/H5客户中心设计方案.md b/docs/H5客户中心设计方案.md index 26c8762..b804afd 100644 --- a/docs/H5客户中心设计方案.md +++ b/docs/H5客户中心设计方案.md @@ -547,7 +547,7 @@ src/views/h5/customer-center/ - [x] 独立 H5 账号密码登录(`/h5/login`,不跳 PC 登录页) - [x] H5 微信扫码登录:`GET /wechat/mp/login-qrcode` + 长按识别 + 轮询登录 - [x] 独立 H5 充值(`/h5/recharge`,不跳 PC 充值页) -- [x] 中英文与 nginx `/h5` SPA 回退 +- [x] 中英文与 `/h5` SPA 回退(`serve -s` / Traefik) - [ ] 键盘适配与运单上下文在顾问页的完整预填联调 - [x] 手机完整下单(二期):`/h5/place-order` 三步精简下单 + 查价「去下单」草稿深链 diff --git a/docs/前端页面设计规范.md b/docs/前端页面设计规范.md index 0d4108c..0c6752e 100644 --- a/docs/前端页面设计规范.md +++ b/docs/前端页面设计规范.md @@ -555,33 +555,25 @@ const { t } = useI18n(); - [ ] 分页组件置于**列表上方工具栏右侧**(列表右上角),**§7.3**;分页 10/30/50/100,默认 50 - [ ] 操作按钮按权限使用 `hasPermission` - [ ] 路由 meta 配置 `permission` -- [ ] **新增前端页面路径**若落在 `/order/`、`/channel/`、`/system/` 等与 Admin API 同源前缀下,须在 **`nginx-default.conf` / `nginx-docker.conf`** 增加 `^~` 的 `try_files /index.html =200` 规则(**§十五**),避免刷新/直链 404 +- [ ] **部署**:前端与 Admin API **分域名**(如 `ideship.com` / `api.ideship.com`);TLS 走 Traefik + cert-manager(**§十五**) - [ ] 文案通过 i18n,不写死中文 - [ ] 命名符合本规范(文件、路由、变量、i18n key) --- -## 十五、部署与 nginx(SPA 路由) +## 十五、部署与网关(Traefik / SPA) -生产环境 nginx 将 `/order/`、`/channel/`、`/system/` 等前缀**同时**用于 Admin API 反代。浏览器刷新或直接打开前端 URL(如 `/order/batchJobs`)时,若未单独配置 SPA 回退,请求会被转发到后端 API 并返回 **404**。 +生产已弃用站内 nginx。静态资源由前端容器(`serve -s`)提供,**HTTPS** 由 k3s Traefik `websecure` + cert-manager(Let's Encrypt)签发;流量路径: + +`浏览器 → :443 Traefik(websecure) → Ingress(tls) → Service → 前端 Pod(:80)` **约定:** -1. 官网 **`/`** 为 Vue 路由(`src/views/site`);静态资源在 **`/site/`**;**`/9610.html`** 为 `public/9610.html`。客户登录为 **`/login`**,管理端为 **`/admin`**。勿再把 `/login` 重定向到 `/`。 -2. 每新增一个**前端页面路由**,若其 path 与 API 前缀冲突,必须在 `nginx-default.conf` 与 `nginx-docker.conf` 中、**API 正则代理块之前**增加更高优先级的 `^~` 规则,例如: - -```nginx -# 订单前端路由(^~ 须比下方 /order/ API 正则代理更优先) -location ^~ /order/batchJobs { - try_files /index.html =200; -} -``` - -3. 修改后执行 `nginx -t && nginx -s reload`(或重建前端 Docker 镜像使 `nginx-docker.conf` 生效)。 - -4. 本地 Vite 开发不受此限制;**仅在部署环境**需遵守本节前缀规则。`pnpm dev` 时 `/` 即为官网。 - -当前订单模块已配置的前端路径示例:`/order/priceCheck`、`/order/placeOrder`、`/order/myOrders`、`/order/batchJobs`。 +1. **前后端分域名**(方案 A):站点如 `ideship.com` / `test.ideship.com`,API 如 `api.ideship.com` / `api.test.ideship.com`。浏览器直连 API,前端容器不做 Admin 反代,也无 `/__cos__` 反代。 +2. SPA 回退由 `serve -s`(或等价静态服务器)处理;刷新 `/order/batchJobs` 等路径应回 `index.html`,不依赖 nginx `try_files`。 +3. 官网 **`/`** 为 Vue 路由(`src/views/site`);静态资源在 **`/site/`**;**`/9610.html`** 为 `public/9610.html`。客户登录 **`/login`**,管理端 **`/admin`**。 +4. 面单/附件预览:开发环境可用 Vite `/__cos__`;**生产**经鉴权 `file-storage/stream` → blob,勿直开 `https://cos.*`(自定义域证书常不匹配)。 +5. 本地 `pnpm dev` 不受 Ingress 限制;`/` 即为官网。 --- diff --git a/docs/在线客服模块设计方案.md b/docs/在线客服模块设计方案.md index d0e2bc9..163f162 100644 --- a/docs/在线客服模块设计方案.md +++ b/docs/在线客服模块设计方案.md @@ -181,7 +181,7 @@ | `OmsFileUploadHelper` | 聊天图片上传(新 folder:`cs`) | | `NoticeService` | 可选:新消息站内信 | | 数据权限 | 坐席列表/认领范围按客服数据权限过滤 | -| nginx | P1 必须补 WebSocket Upgrade | +| Traefik Ingress | P1 须允许 WebSocket Upgrade(SignalR) | ### 4.3 技术选型 @@ -197,16 +197,8 @@ ### 4.4 服务端部署注意 1. `AddSignalR().AddStackExchangeRedis(...)` 或与现有 FreeRedis 兼容的 backplane 方案(实现阶段选定一种,避免双 Redis 客户端混乱)。 -2. nginx 为 Hub 增加独立 location(建议 `/hubs/`),配置: - -```nginx -proxy_http_version 1.1; -proxy_set_header Upgrade $http_upgrade; -proxy_set_header Connection "upgrade"; -proxy_read_timeout 3600s; -``` - -3. 前端 Docker / `nginx-docker.conf`、`nginx-default.conf` 同步修改;新坐席路由若与 API 前缀冲突,按规范 §十五做 SPA `try_files`。 +2. Traefik / Ingress 对 Hub 路径(建议 `/hubs/` 或 SignalR 约定路径)开启 WebSocket;超时按长连接调大。前后端分域名时,浏览器直连 **api.***,前端静态容器无需反代 Hub。 +3. SPA 回退由前端容器 `serve -s` 处理;见规范 §十五。 --- @@ -610,7 +602,7 @@ src/composables/useCsHub.ts - [ ] 坐席右侧能看到客户与最近订单摘要 - [ ] 结束会话后客户再进线为新会话 - [ ] 双实例部署下消息不丢(Redis backplane) -- [ ] nginx WebSocket 升级验证通过 +- [ ] Traefik / Ingress WebSocket 升级验证通过 - [ ] 权限码控制认领 / 回复 / 结束;客户无法调坐席接口 ### 12.3 非功能 @@ -630,7 +622,7 @@ src/composables/useCsHub.ts | 2 | DDL:`t_cs_conversation` / `t_cs_message` + 仓储 | P1 | | 3 | HTTP 会话/消息/上传 API + 权限码种子 | P1 | | 4 | 坐席 Inbox 静态联调(可先轮询) | P1 | -| 5 | SignalR Hub + 前端 `useCsHub` + nginx | P1 | +| 5 | SignalR Hub + 前端 `useCsHub` + Traefik WS | P1 | | 6 | 分配逻辑 + 在线心跳 + 微信降级 | P1 | | 7 | AI 转人工 + 上下文侧栏 | P1 | | 8 | 未读角标 +(可选)站内信 | P1 | @@ -646,7 +638,7 @@ src/composables/useCsHub.ts | 风险 | 对策 | | --- | --- | | 坐席不用系统、仍只回微信 | P0 微信永远保留;P1 强调「转人工带单号」提升系统内处理价值 | -| SignalR 经 nginx 失败 | 独立 `/hubs/` location + 验收清单;可临时 `SignalREnabled=false` HTTP 轮询 | +| SignalR 经网关失败 | Ingress 允许 Upgrade + 验收清单;可临时 `SignalREnabled=false` HTTP 轮询 | | 多实例串消息 | 强制 Redis backplane,禁止单机假设 | | 客户把客服当即时 IM 闲聊 | 入口文案强调「物流咨询」;系统消息提示工作时间(可配置) | | AI 灌入消息过长 | 限制条数与单条长度;仅转人工时写入 | diff --git a/docs/小红书模块联调与验收.md b/docs/小红书模块联调与验收.md index 5da89af..3883fdb 100644 --- a/docs/小红书模块联调与验收.md +++ b/docs/小红书模块联调与验收.md @@ -46,7 +46,7 @@ pnpm install # 首次 pnpm dev ``` -生产构建:`pnpm build`,Nginx 配置见 `nginx-default.conf`(**仅** SPA 页面路径走 `index.html`,`/xhs/agent/list` 等 API 走 admin 后端,勿整段 `/xhs` 当 SPA)。 +生产构建:`pnpm build`;前后端分域名后,`/xhs/*` 页面由前端 `serve -s` SPA 回退,`/xhs/agent/*` 等 API 走 **api.*** Admin,勿把 API 打到前端域名。 ### 1.4 LIKE AI Agent @@ -158,12 +158,12 @@ Content-Type: application/json - [ ] **F2** 编辑提供商:BaseUrl、Model、ApiSecret 保存成功 - [ ] **F3** 对话发送消息 → 流式/完整回复正常 -### 阶段 G — 生产/nginx(上线前) +### 阶段 G — 生产 / Traefik(上线前) -- [ ] **G1** `nginx -t && nginx -s reload` 已执行 -- [ ] **G2** 刷新 `/xhs/analysis` 等页面返回 SPA,**不** 404 -- [ ] **G3** `POST https://api.nanafa.com/admin/xhs/agent/list`(带 Token)正常 -- [ ] **G4** Agent 使用生产 `--oms-url https://api.nanafa.com/admin` +- [ ] **G1** Ingress(tls) + cert-manager 证书就绪;前端 Service 健康 +- [ ] **G2** 刷新站点域名下 `/xhs/analysis` 等返回 SPA,**不** 404 +- [ ] **G3** `POST https://api.ideship.com/admin/xhs/agent/list`(带 Token)正常 +- [ ] **G4** Agent 使用生产 `--oms-url https://api.ideship.com/admin` --- @@ -232,8 +232,8 @@ curl.exe -s "http://localhost:5281/admin/rbac/auth/login" ... ### 5.5 前端 dev 正常、生产 404 -- Nginx 仅对 **页面路径** 回退 `index.html`(见 `nginx-default.conf` 第 63–66 行) -- API 请求应走 `VITE_API_ADMIN_URL`(如 `https://api.nanafa.com/admin`),不要打到 oms 静态站点的 `/xhs/agent/list` +- 站点域名下页面由 `serve -s` 回退 `index.html` +- API 请求应走 `VITE_API_ADMIN_URL`(如 `https://api.ideship.com/admin`),不要打到前端静态域名的 `/xhs/agent/list` ### 5.6 AI 生成 / 分析失败 diff --git a/docs/小红书模块设计-063001.md b/docs/小红书模块设计-063001.md index f139d17..485c47f 100644 --- a/docs/小红书模块设计-063001.md +++ b/docs/小红书模块设计-063001.md @@ -126,18 +126,12 @@ export default XHS; 'menu.system.xhsAiAssistant': 'AI 助手', ``` -### 2.3 Vite / Nginx SPA 规则 +### 2.3 Vite / SPA 规则 -`/xhs/*` 为纯前端路由,须加入: +`/xhs/*` 为纯前端路由: -- `vite.config.ts` → `frontendRoutes` 增加 `'/xhs'` -- `nginx-default.conf` → 在 API 反代规则之前增加: - -```nginx -location ~ ^/xhs { - try_files $uri /index.html; -} -``` +- `vite.config.ts` → `frontendRoutes` 增加 `'/xhs'`(开发 history fallback) +- 生产由前端容器 `serve -s` 回退 `index.html`;API 走分域名 `api.*`,无需站内 nginx --- diff --git a/docs/微信登录整合方案.md b/docs/微信登录整合方案.md index f30afbf..20c3229 100644 --- a/docs/微信登录整合方案.md +++ b/docs/微信登录整合方案.md @@ -422,17 +422,10 @@ src/SimpleAdmin.Api/ | Token | 与数据库/AppSettings 一致 | | 消息加解密 | **明文**(当前实现) | -### 7.3 Nginx +### 7.3 网关 / 回调 -`nginx-default.conf` 关键规则: - -```nginx -# 微信回调 -location ^~ /api/apiwechat/ { - rewrite ^/api/apiwechat/(.*)$ /admin/apiwechat/$1 break; - proxy_pass https://api.nanafa.com; -} -``` +生产已弃用站内 nginx。微信服务器回调应直连 **API 域名**(如 `https://api.ideship.com/admin/apiwechat/verification`),由 Traefik Ingress 转发到后端 Service;前端静态容器不做 `/api/apiwechat` 反代。 +本地开发仍可由 Vite 代理 `/api/apiwechat` → Admin。 --- diff --git a/nginx-default.conf b/nginx-default.conf deleted file mode 100644 index 6e9d274..0000000 --- a/nginx-default.conf +++ /dev/null @@ -1,489 +0,0 @@ -# ============ 生产环境部署必读(避免「本地可以、线上不行」)============ -# 1. 本文件必须真正生效:确认 oms.nanafa.com 的请求由「本 server 块」处理 -# - 若用 include:确保 include 的是含本 server 的 conf,且无其它 server 抢先匹配 -# - 更新后必须执行:nginx -t && nginx -s reload -# 2. /api、/Track、/rbac、/order 等 Admin 接口均由 nginx 代理到 api.nanafa.com,浏览器只访问 oms 同源 -# 3. HTTPS:证书部署到服务器(勿提交 .key 到 git) -# /etc/nginx/ssl/oms.nanafa.com.pem -# /etc/nginx/ssl/oms.nanafa.com.key -# 4. 官网 `/` 走 Vue SPA;静态资源在 /site/;客户登录 /login;管理端 /admin -# 5. 静态资源使用 /login-assets/,勿使用 /login/ 目录名 -# 6. 微信公众号服务器 URL 请使用 https://api.ideship.com/admin/apiwechat/verification -# (生产三套:admin / itfx / track,均挂在 https://api.ideship.com/{pathbase}) -# ============================================================================ - -# HTTP → HTTPS -server { - listen 80; - server_name oms.nanafa.com; - - return 301 https://$host$request_uri; -} - -server { - listen 443 ssl; - http2 on; - server_name oms.nanafa.com; - - ssl_certificate /etc/nginx/ssl/oms.nanafa.com.pem; - ssl_certificate_key /etc/nginx/ssl/oms.nanafa.com.key; - - ssl_session_timeout 1d; - ssl_session_cache shared:OMS_SSL:10m; - ssl_protocols TLSv1.2 TLSv1.3; - ssl_prefer_server_ciphers off; - - # 启用gzip压缩 - gzip on; - gzip_vary on; - gzip_min_length 1024; - gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json application/xml image/svg+xml; - gzip_comp_level 6; - gzip_disable "msie6"; - - root /usr/share/nginx/html; - - # ============ 官网首页(Vue SPA)============ - location = / { - try_files /index.html =200; - add_header Cache-Control "no-cache, no-store, must-revalidate"; - add_header Pragma "no-cache"; - add_header Expires "0"; - } - - location = /9610.html { - try_files /9610.html =404; - add_header Cache-Control "no-cache, no-store, must-revalidate"; - } - - # 官网仅静态资源在 /site/;首页保持在 / - location = /site { - return 301 /; - } - location = /site/ { - return 301 /; - } - location = /site/index.html { - return 301 /; - } - - location ^~ /site/ { - try_files $uri =404; - expires 7d; - add_header Cache-Control "public"; - access_log off; - } - - location = /favicon.ico { - try_files /favicon.png =404; - expires 7d; - add_header Cache-Control "public"; - access_log off; - } - - location = /favicon.png { - try_files /favicon.png =404; - expires 7d; - add_header Cache-Control "public"; - access_log off; - } - - # ============ 客户 / 管理端登录入口 ============ - location = /login { - try_files /index.html =200; - } - location = /login/ { - try_files /index.html =200; - } - - rewrite ^/admin/?$ /index.html last; - - location = /admin { - try_files /index.html =200; - } - location = /admin/ { - try_files /index.html =200; - } - - # ============ API 前端路由(必须先于 /api/ 代理)============ - location ~ ^/api/(upstreamInterface|upstreamInterfaceConfig|upstreamInterfaceConfigEdit|downstreamInterface(|/newPort)|appList(|/createApplication)|requestLog|callStatistics|apiDebugger|apiTest|openApi)(/|$) { - try_files /index.html =200; - } - - # ============ 小红书前端路由(^~ 须比下方 /xhs/ API 更具体)============ - location ^~ /xhs/startService { - try_files /index.html =200; - } - location ^~ /xhs/creation { - try_files /index.html =200; - } - location ^~ /xhs/publishHistory { - try_files /index.html =200; - } - location ^~ /xhs/skills { - try_files /index.html =200; - } - location ^~ /xhs/humanSettings { - try_files /index.html =200; - } - location ^~ /xhs/agentPair { - try_files /index.html =200; - } - location = /xhs/analysis { - try_files /index.html =200; - } - location = /xhs/analysis/ { - try_files /index.html =200; - } - - # ============ 微信 H5 / 操作页(^~ 须比下方 API 前缀更长或更具体)============ - location ^~ /wechat/register { - try_files /index.html =200; - } - location ^~ /wechat/bind { - try_files /index.html =200; - } - location ^~ /h5 { - try_files /index.html =200; - } - location ^~ /operation/inboundScan { - try_files /index.html =200; - } - location ^~ /operation/outboundScan { - try_files /index.html =200; - } - location ^~ /operation/printLabel { - try_files /index.html =200; - } - location ^~ /operation/parcelArrival { - try_files /index.html =200; - } - location ^~ /operation/fulfillPack { - try_files /index.html =200; - } - - # ============ 订单前端路由(^~ 须比下方 /order/ API 正则代理更优先)============ - location ^~ /order/priceCheck { - try_files /index.html =200; - } - location ^~ /order/placeOrder { - try_files /index.html =200; - } - location ^~ /order/myOrders { - try_files /index.html =200; - } - location ^~ /order/fulfillOrders { - try_files /index.html =200; - } - location ^~ /order/waybillManage { - try_files /index.html =200; - } - location ^~ /order/pickupOrder { - try_files /index.html =200; - } - location ^~ /order/customs9610Order { - try_files /index.html =200; - } - location ^~ /order/batchJobs { - try_files /index.html =200; - } - location ^~ /order/templateMapping { - try_files /index.html =200; - } - location ^~ /order/platformStore { - try_files /index.html =200; - } - location ^~ /order/platformStoreOrder { - try_files /index.html =200; - } - - # ============ 知识库前端路由(^~ 须比下方 /kb/ API 代理更优先)============ - location ^~ /kb/category { - try_files /index.html =200; - } - location ^~ /kb/article { - try_files /index.html =200; - } - - # ============ 仓储前端路由(^~ 须比下方 /warehouse/ API 正则代理更优先)============ - # 页面路径与 API 前缀相同者用精确匹配,避免 /warehouse/sku/list 等接口被误判为 SPA - location = /warehouse/parcel { - try_files /index.html =200; - } - location = /warehouse/inventory { - try_files /index.html =200; - } - location = /warehouse/sku { - try_files /index.html =200; - } - location = /warehouse/binLocation { - try_files /index.html =200; - } - location ^~ /warehouse/inboundNotice { - try_files /index.html =200; - } - - # 采集插件确认页(/Plugin/AddPackage);API 走 /plugin/browser/* - location ^~ /Plugin/ { - try_files /index.html =200; - } - - # ============ 渠道前端路由(^~ 须比下方 /channel/ API 正则代理更优先)============ - location ^~ /channel/salesProducts { try_files /index.html =200; } - location ^~ /channel/serviceChannels { try_files /index.html =200; } - location ^~ /channel/priceArea { try_files /index.html =200; } - location ^~ /channel/blacklist { try_files /index.html =200; } - location ^~ /channel/remoteZones { try_files /index.html =200; } - location ^~ /channel/weightRules { try_files /index.html =200; } - location ^~ /channel/weightRounding { try_files /index.html =200; } - location ^~ /channel/autoTrigger { try_files /index.html =200; } - location ^~ /channel/issueTemplate { try_files /index.html =200; } - location ^~ /channel/labelDesign { try_files /index.html =200; } - location ^~ /channel/guide { try_files /index.html =200; } - - # ============ 系统管理前端路由(^~ 须比下方 /system/ API 正则代理更优先)============ - # 页面路径为 camelCase;API 为 kebab(如 /system/dict/list、/system/batch-email/…),勿用宽泛前缀匹配 dict - location = /system/dict { - try_files /index.html =200; - } - location = /system/warehouseAddress { - try_files /index.html =200; - } - location = /system/announcement { - try_files /index.html =200; - } - location = /system/country { - try_files /index.html =200; - } - location ^~ /system/users { - try_files /index.html =200; - } - location ^~ /system/roles { - try_files /index.html =200; - } - location ^~ /system/menus { - try_files /index.html =200; - } - location ^~ /system/organizations { - try_files /index.html =200; - } - location ^~ /system/batchEmail { - try_files /index.html =200; - } - location ^~ /system/otherConfig { - try_files /index.html =200; - } - location ^~ /system/aiAssistant { - try_files /index.html =200; - } - location ^~ /system/aiSkills { - try_files /index.html =200; - } - location ^~ /system/mcpTools { - try_files /index.html =200; - } - location ^~ /system/dataPerm/ { - try_files /index.html =200; - } - - # ============ 营销前端路由(^~ 须比下方 /marketing/ API 正则代理更优先)============ - location ^~ /marketing/memberLevel { try_files /index.html =200; } - location ^~ /marketing/pointsRule { try_files /index.html =200; } - location ^~ /marketing/pointsFlow { try_files /index.html =200; } - location ^~ /marketing/couponTemplate { try_files /index.html =200; } - location ^~ /marketing/couponGrant { try_files /index.html =200; } - location ^~ /marketing/commissionRule { try_files /index.html =200; } - location ^~ /marketing/commissionDetail { try_files /index.html =200; } - location ^~ /marketing/commissionWithdraw { try_files /index.html =200; } - location ^~ /marketing/referralTree { try_files /index.html =200; } - - # ============ 微信公众号服务器回调(admin API,非 itfx)============ - location ^~ /api/apiwechat/ { - rewrite ^/api/apiwechat/(.*)$ /admin/apiwechat/$1 break; - proxy_pass http://api.nanafa.com; - proxy_http_version 1.1; - proxy_set_header Host api.nanafa.com; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_connect_timeout 30s; - proxy_send_timeout 30s; - proxy_read_timeout 30s; - } - - # ============ API 代理(仅真实后端接口,同源请求避免跨域 405)============ - location ~ ^/api/ { - rewrite ^/api/(.*)$ /itfx/$1 break; - proxy_pass http://api.nanafa.com; - proxy_http_version 1.1; - proxy_set_header Host api.nanafa.com; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_connect_timeout 30s; - proxy_send_timeout 30s; - proxy_read_timeout 30s; - } - - location ^~ /Track { - rewrite ^/Track(.*)$ /track/Track$1 break; - proxy_pass http://api.nanafa.com; - proxy_http_version 1.1; - proxy_set_header Host api.nanafa.com; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_connect_timeout 30s; - proxy_send_timeout 30s; - proxy_read_timeout 30s; - } - - # ============ Admin API 同源反代(/rbac /order /xhs … → api.nanafa.com/admin)============ - location ~ ^/(rbac|order|monitor|customer|recharge|channel|system|warehouse|plugin|marketing|kb)/ { - rewrite ^/(.*)$ /admin/$1 break; - proxy_pass http://api.nanafa.com; - proxy_http_version 1.1; - proxy_set_header Host api.nanafa.com; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_connect_timeout 30s; - proxy_send_timeout 30s; - proxy_read_timeout 120s; - } - - location ^~ /wechat/ { - rewrite ^/(.*)$ /admin/$1 break; - proxy_pass http://api.nanafa.com; - proxy_http_version 1.1; - proxy_set_header Host api.nanafa.com; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_connect_timeout 30s; - proxy_send_timeout 30s; - proxy_read_timeout 120s; - } - - location ^~ /operation/ { - rewrite ^/(.*)$ /admin/$1 break; - proxy_pass http://api.nanafa.com; - proxy_http_version 1.1; - proxy_set_header Host api.nanafa.com; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_connect_timeout 30s; - proxy_send_timeout 30s; - proxy_read_timeout 120s; - } - - location ^~ /xhs/ { - rewrite ^/(.*)$ /admin/$1 break; - proxy_pass http://api.nanafa.com; - proxy_http_version 1.1; - proxy_set_header Host api.nanafa.com; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_connect_timeout 30s; - proxy_send_timeout 30s; - proxy_read_timeout 300s; - } - - location ^~ /uploads/ { - rewrite ^/(.*)$ /admin/$1 break; - proxy_pass http://api.nanafa.com; - proxy_http_version 1.1; - proxy_set_header Host api.nanafa.com; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_connect_timeout 30s; - proxy_send_timeout 30s; - proxy_read_timeout 120s; - } - - location ^~ /mcp/ { - rewrite ^/(.*)$ /admin/$1 break; - proxy_pass http://api.nanafa.com; - proxy_http_version 1.1; - proxy_set_header Host api.nanafa.com; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_connect_timeout 30s; - proxy_send_timeout 30s; - proxy_read_timeout 300s; - } - - location ^~ /ai/ { - rewrite ^/(.*)$ /admin/$1 break; - proxy_pass http://api.nanafa.com; - proxy_http_version 1.1; - proxy_set_header Host api.nanafa.com; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_connect_timeout 30s; - proxy_send_timeout 30s; - proxy_read_timeout 300s; - } - - # HTML 不缓存 - location = /index.html { - add_header Cache-Control "no-cache, no-store, must-revalidate"; - add_header Pragma "no-cache"; - add_header Expires "0"; - } - - # 微信 JS 安全域名校验:原样返回 txt,禁止 SPA 回落 index.html - location ~* ^/MP_verify_[A-Za-z0-9]+\.txt$ { - default_type text/plain; - charset off; - add_header Cache-Control "no-store"; - try_files $uri =404; - } - - # ============ 静态资源缓存 ============ - - location ~* \.(?:js|css)$ { - try_files $uri =404; - expires 1y; - add_header Cache-Control "public, immutable"; - access_log off; - } - - location ~* \.(?:jpg|jpeg|png|gif|ico|svg|webp|avif)$ { - try_files $uri =404; - expires 1y; - add_header Cache-Control "public, immutable"; - access_log off; - } - - location ~* \.(?:woff2?|ttf|eot|otf)$ { - try_files $uri =404; - expires 1y; - add_header Cache-Control "public, immutable"; - access_log off; - } - - location ~* \.(?:pdf|zip|tar|gz|br)$ { - try_files $uri =404; - expires 1M; - add_header Cache-Control "public"; - access_log off; - } - - # ============ SPA 前端路由(放在最后)============ - # 不用 try_files $uri $uri/ —— 若磁盘存在同名目录且无 index,会 403 且不会回落 index.html - location / { - index index.html; - try_files $uri /index.html; - } - - error_page 500 502 503 504 /50x.html; - location = /50x.html { - root html; - } -} diff --git a/nginx-docker.conf b/nginx-docker.conf deleted file mode 100644 index 9cf8ed8..0000000 --- a/nginx-docker.conf +++ /dev/null @@ -1,279 +0,0 @@ -# Docker 容器专用:只监听 80,不配置 SSL(7962:80,由外层网关/宿主机 nginx 终结 HTTPS) -server { - listen 80; - server_name _; - - gzip on; - gzip_vary on; - gzip_min_length 1024; - gzip_types text/plain text/css text/xml text/javascript application/javascript application/json application/xml image/svg+xml; - - root /usr/share/nginx/html; - - location = / { - try_files /index.html =200; - add_header Cache-Control "no-cache, no-store, must-revalidate"; - add_header Pragma "no-cache"; - add_header Expires "0"; - } - - location = /9610.html { - try_files /9610.html =404; - add_header Cache-Control "no-cache, no-store, must-revalidate"; - } - - location = /site { - return 301 /; - } - location = /site/ { - return 301 /; - } - location = /site/index.html { - return 301 /; - } - - location ^~ /site/ { - try_files $uri =404; - expires 7d; - add_header Cache-Control "public"; - access_log off; - } - - location = /favicon.ico { - try_files /favicon.png =404; - expires 7d; - add_header Cache-Control "public"; - access_log off; - } - - location = /favicon.png { - try_files /favicon.png =404; - expires 7d; - add_header Cache-Control "public"; - access_log off; - } - - location = /login { - try_files /index.html =200; - } - location = /login/ { - try_files /index.html =200; - } - - rewrite ^/admin/?$ /index.html last; - - location = /admin { - try_files /index.html =200; - } - location = /admin/ { - try_files /index.html =200; - } - - location ~ ^/api/(upstreamInterface|upstreamInterfaceConfig|upstreamInterfaceConfigEdit|downstreamInterface(|/newPort)|appList(|/createApplication)|requestLog|callStatistics|apiDebugger|apiTest|openApi)(/|$) { - try_files /index.html =200; - } - - location ^~ /xhs/startService { try_files /index.html =200; } - location ^~ /xhs/creation { try_files /index.html =200; } - location ^~ /xhs/publishHistory { try_files /index.html =200; } - location ^~ /xhs/skills { try_files /index.html =200; } - location ^~ /xhs/humanSettings { try_files /index.html =200; } - location ^~ /xhs/agentPair { try_files /index.html =200; } - location = /xhs/analysis { try_files /index.html =200; } - location = /xhs/analysis/ { try_files /index.html =200; } - - location ^~ /wechat/register { try_files /index.html =200; } - location ^~ /wechat/bind { try_files /index.html =200; } - location ^~ /h5 { try_files /index.html =200; } - location ^~ /track/query { try_files /index.html =200; } - location ^~ /operation/inboundScan { try_files /index.html =200; } - location ^~ /operation/outboundScan { try_files /index.html =200; } - location ^~ /operation/printLabel { try_files /index.html =200; } - location ^~ /operation/parcelArrival { try_files /index.html =200; } - location ^~ /operation/fulfillPack { try_files /index.html =200; } - - # 订单前端路由(^~ 须比下方 /order/ API 正则代理更优先) - location ^~ /order/priceCheck { try_files /index.html =200; } - location ^~ /order/placeOrder { try_files /index.html =200; } - location ^~ /order/myOrders { try_files /index.html =200; } - location ^~ /order/fulfillOrders { try_files /index.html =200; } - location ^~ /order/waybillManage { try_files /index.html =200; } - location ^~ /order/pickupOrder { try_files /index.html =200; } - location ^~ /order/customs9610Order { try_files /index.html =200; } - location ^~ /order/batchJobs { try_files /index.html =200; } - location ^~ /order/templateMapping { try_files /index.html =200; } - location ^~ /order/platformStore { try_files /index.html =200; } - location ^~ /order/platformStoreOrder { try_files /index.html =200; } - - # 知识库前端路由(^~ 须比下方 /kb/ API 代理更优先) - location ^~ /kb/category { try_files /index.html =200; } - location ^~ /kb/article { try_files /index.html =200; } - - location = /warehouse/parcel { try_files /index.html =200; } - location = /warehouse/inventory { try_files /index.html =200; } - location = /warehouse/sku { try_files /index.html =200; } - location = /warehouse/binLocation { try_files /index.html =200; } - location ^~ /warehouse/inboundNotice { try_files /index.html =200; } - location ^~ /Plugin/ { try_files /index.html =200; } - - location ^~ /channel/salesProducts { try_files /index.html =200; } - location ^~ /channel/serviceChannels { try_files /index.html =200; } - location ^~ /channel/priceArea { try_files /index.html =200; } - location ^~ /channel/blacklist { try_files /index.html =200; } - location ^~ /channel/remoteZones { try_files /index.html =200; } - location ^~ /channel/weightRules { try_files /index.html =200; } - location ^~ /channel/weightRounding { try_files /index.html =200; } - location ^~ /channel/autoTrigger { try_files /index.html =200; } - location ^~ /channel/issueTemplate { try_files /index.html =200; } - location ^~ /channel/labelDesign { try_files /index.html =200; } - - # 营销前端路由(^~ 须比下方 /marketing/ API 正则代理更优先) - location ^~ /marketing/memberLevel { try_files /index.html =200; } - location ^~ /marketing/pointsRule { try_files /index.html =200; } - location ^~ /marketing/pointsFlow { try_files /index.html =200; } - location ^~ /marketing/couponTemplate { try_files /index.html =200; } - location ^~ /marketing/couponGrant { try_files /index.html =200; } - location ^~ /marketing/commissionRule { try_files /index.html =200; } - location ^~ /marketing/commissionDetail { try_files /index.html =200; } - location ^~ /marketing/commissionWithdraw { try_files /index.html =200; } - location ^~ /marketing/referralTree { try_files /index.html =200; } - - # 系统管理前端路由(^~ 须比下方 /system/ API 正则代理更优先) - location = /system/dict { try_files /index.html =200; } - location = /system/warehouseAddress { try_files /index.html =200; } - location = /system/announcement { try_files /index.html =200; } - location = /system/country { try_files /index.html =200; } - location ^~ /system/users { try_files /index.html =200; } - location ^~ /system/roles { try_files /index.html =200; } - location ^~ /system/menus { try_files /index.html =200; } - location ^~ /system/organizations { try_files /index.html =200; } - location ^~ /system/batchEmail { try_files /index.html =200; } - location ^~ /system/otherConfig { try_files /index.html =200; } - location ^~ /system/aiAssistant { try_files /index.html =200; } - location ^~ /system/aiSkills { try_files /index.html =200; } - location ^~ /system/mcpTools { try_files /index.html =200; } - location ^~ /system/dataPerm/ { try_files /index.html =200; } - - location ^~ /api/apiwechat/ { - rewrite ^/api/apiwechat/(.*)$ /admin/apiwechat/$1 break; - proxy_pass http://api.nanafa.com; - proxy_http_version 1.1; - proxy_set_header Host api.nanafa.com; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - } - - location ~ ^/api/ { - rewrite ^/api/(.*)$ /itfx/$1 break; - proxy_pass http://api.nanafa.com; - proxy_http_version 1.1; - proxy_set_header Host api.nanafa.com; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - } - - location ^~ /Track { - rewrite ^/Track(.*)$ /track/Track$1 break; - proxy_pass http://api.nanafa.com; - proxy_http_version 1.1; - proxy_set_header Host api.nanafa.com; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - } - - location ~ ^/(rbac|order|monitor|customer|recharge|channel|system|warehouse|plugin|marketing|kb)/ { - rewrite ^/(.*)$ /admin/$1 break; - proxy_pass http://api.nanafa.com; - proxy_http_version 1.1; - proxy_set_header Host api.nanafa.com; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_read_timeout 120s; - } - - location ^~ /wechat/ { - rewrite ^/(.*)$ /admin/$1 break; - proxy_pass http://api.nanafa.com; - proxy_http_version 1.1; - proxy_set_header Host api.nanafa.com; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - } - - location ^~ /operation/ { - rewrite ^/(.*)$ /admin/$1 break; - proxy_pass http://api.nanafa.com; - proxy_http_version 1.1; - proxy_set_header Host api.nanafa.com; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - } - - location ^~ /xhs/ { - rewrite ^/(.*)$ /admin/$1 break; - proxy_pass http://api.nanafa.com; - proxy_http_version 1.1; - proxy_set_header Host api.nanafa.com; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_read_timeout 300s; - } - - location ^~ /uploads/ { - rewrite ^/(.*)$ /admin/$1 break; - proxy_pass http://api.nanafa.com; - proxy_http_version 1.1; - proxy_set_header Host api.nanafa.com; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_read_timeout 120s; - } - - location ^~ /mcp/ { - rewrite ^/(.*)$ /admin/$1 break; - proxy_pass http://api.nanafa.com; - proxy_http_version 1.1; - proxy_set_header Host api.nanafa.com; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_read_timeout 300s; - } - - location ^~ /ai/ { - rewrite ^/(.*)$ /admin/$1 break; - proxy_pass http://api.nanafa.com; - proxy_http_version 1.1; - proxy_set_header Host api.nanafa.com; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_read_timeout 300s; - } - - location = /index.html { - add_header Cache-Control "no-cache, no-store, must-revalidate"; - add_header Pragma "no-cache"; - add_header Expires "0"; - } - - location ~* \.(?:js|css)$ { - try_files $uri =404; - expires 1y; - add_header Cache-Control "public, immutable"; - access_log off; - } - - location / { - index index.html; - try_files $uri /index.html; - } -} diff --git a/nginx-k3s.conf b/nginx-k3s.conf deleted file mode 100644 index 5785f83..0000000 --- a/nginx-k3s.conf +++ /dev/null @@ -1,226 +0,0 @@ -# k3s 方案 A:仅静态 SPA。API 由浏览器按域名直连(prod: api.ideship.com / test: api.test.ideship.com)。 -server { - listen 80; - server_name _; - - gzip on; - gzip_vary on; - gzip_min_length 1024; - gzip_types text/plain text/css text/xml text/javascript application/javascript application/json application/xml image/svg+xml; - - root /usr/share/nginx/html; - - location = / { - try_files $uri @spa; - } - - location = /9610.html { - try_files /9610.html =404; - add_header Cache-Control "no-cache, no-store, must-revalidate"; - } - - location = /site { - return 301 /; - } - location = /site/ { - return 301 /; - } - location = /site/index.html { - return 301 /; - } - - location ^~ /site/ { - try_files $uri =404; - expires 7d; - add_header Cache-Control "public"; - access_log off; - } - - location = /favicon.ico { - try_files /favicon.png =404; - expires 7d; - add_header Cache-Control "public"; - access_log off; - } - - location = /favicon.png { - try_files /favicon.png =404; - expires 7d; - add_header Cache-Control "public"; - access_log off; - } - - location = /login { - try_files $uri @spa; - } - location = /login/ { - try_files $uri @spa; - } - - rewrite ^/admin/?$ /index.html last; - - location = /admin { - try_files $uri @spa; - } - location = /admin/ { - try_files $uri @spa; - } - - location ~ ^/api/(upstreamInterface|upstreamInterfaceConfig|upstreamInterfaceConfigEdit|downstreamInterface(|/newPort)|appList(|/createApplication)|requestLog|callStatistics|apiDebugger|apiTest|openApi)(/|$) { - try_files $uri @spa; - } - - location ^~ /xhs/startService { try_files $uri @spa; } - location ^~ /xhs/creation { try_files $uri @spa; } - location ^~ /xhs/publishHistory { try_files $uri @spa; } - location ^~ /xhs/skills { try_files $uri @spa; } - location ^~ /xhs/humanSettings { try_files $uri @spa; } - location ^~ /xhs/agentPair { try_files $uri @spa; } - location = /xhs/analysis { try_files $uri @spa; } - location = /xhs/analysis/ { try_files $uri @spa; } - - location ^~ /wechat/register { try_files $uri @spa; } - location ^~ /wechat/bind { try_files $uri @spa; } - location ^~ /h5 { try_files $uri @spa; } - location ^~ /track/query { try_files $uri @spa; } - location ^~ /operation/inboundScan { try_files $uri @spa; } - location ^~ /operation/outboundScan { try_files $uri @spa; } - location ^~ /operation/printLabel { try_files $uri @spa; } - location ^~ /operation/parcelArrival { try_files $uri @spa; } - location ^~ /operation/fulfillPack { try_files $uri @spa; } - - # 订单前端路由(^~ 须比下方 /order/ API 正则代理更优先) - location ^~ /order/priceCheck { try_files $uri @spa; } - location ^~ /order/placeOrder { try_files $uri @spa; } - location ^~ /order/myOrders { try_files $uri @spa; } - location ^~ /order/fulfillOrders { try_files $uri @spa; } - location ^~ /order/waybillManage { try_files $uri @spa; } - location ^~ /order/pickupOrder { try_files $uri @spa; } - location ^~ /order/customs9610Order { try_files $uri @spa; } - location ^~ /order/batchJobs { try_files $uri @spa; } - location ^~ /order/templateMapping { try_files $uri @spa; } - location ^~ /order/platformStore { try_files $uri @spa; } - location ^~ /order/platformStoreOrder { try_files $uri @spa; } - - # 知识库前端路由(^~ 须比下方 /kb/ API 代理更优先) - location ^~ /kb/category { try_files $uri @spa; } - location ^~ /kb/article { try_files $uri @spa; } - - location = /warehouse/parcel { try_files $uri @spa; } - location = /warehouse/inventory { try_files $uri @spa; } - location = /warehouse/sku { try_files $uri @spa; } - location = /warehouse/binLocation { try_files $uri @spa; } - location ^~ /warehouse/inboundNotice { try_files $uri @spa; } - location ^~ /Plugin/ { try_files $uri @spa; } - - location ^~ /channel/salesProducts { try_files $uri @spa; } - location ^~ /channel/serviceChannels { try_files $uri @spa; } - location ^~ /channel/priceArea { try_files $uri @spa; } - location ^~ /channel/blacklist { try_files $uri @spa; } - location ^~ /channel/remoteZones { try_files $uri @spa; } - location ^~ /channel/weightRules { try_files $uri @spa; } - location ^~ /channel/weightRounding { try_files $uri @spa; } - location ^~ /channel/autoTrigger { try_files $uri @spa; } - location ^~ /channel/issueTemplate { try_files $uri @spa; } - location ^~ /channel/labelDesign { try_files $uri @spa; } - - # 官网线路目录。这条不是前端路由;落到下方 SPA 会返回 index.html,浏览器看不到 JSON。 - # 仅这一条转到对应环境的 Admin API(test → api.test.ideship.com,其余 → api.ideship.com)。 - location = /channel/sales-product/portal/catalog { - set $oms_api_upstream api.ideship.com; - if ($host ~* "(^test\.ideship\.com$|\.test\.ideship\.com$)") { - set $oms_api_upstream api.test.ideship.com; - } - resolver 1.1.1.1 8.8.8.8 ipv6=off valid=300s; - resolver_timeout 5s; - proxy_http_version 1.1; - proxy_ssl_server_name on; - proxy_set_header Host $oms_api_upstream; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_pass https://$oms_api_upstream/admin/channel/sales-product/portal/catalog$is_args$args; - proxy_connect_timeout 10s; - proxy_read_timeout 30s; - } - - # 营销前端路由(^~ 须比下方 /marketing/ API 正则代理更优先) - location ^~ /marketing/memberLevel { try_files $uri @spa; } - location ^~ /marketing/pointsRule { try_files $uri @spa; } - location ^~ /marketing/pointsFlow { try_files $uri @spa; } - location ^~ /marketing/couponTemplate { try_files $uri @spa; } - location ^~ /marketing/couponGrant { try_files $uri @spa; } - location ^~ /marketing/commissionRule { try_files $uri @spa; } - location ^~ /marketing/commissionDetail { try_files $uri @spa; } - location ^~ /marketing/commissionWithdraw { try_files $uri @spa; } - location ^~ /marketing/referralTree { try_files $uri @spa; } - - # 面单/截图同源代理:/__cos__// → http:/// - # cos.* 自定义域 https 证书常仍是 *.myqcloud.com;上游继续走 http。 - # COS 签名 q-sign-time 含 ";":前端须编成 %3B,否则查询串被按 ";" 拆参 → Request has expired。 - location ~ ^/__cos__/([^/]+)(/.*)$ { - set $cos_host $1; - set $cos_uri $2; - resolver 1.1.1.1 8.8.8.8 ipv6=off valid=300s; - resolver_timeout 5s; - proxy_http_version 1.1; - proxy_set_header Host $cos_host; - proxy_set_header Authorization ""; - proxy_set_header Cookie ""; - proxy_set_header X-Real-IP ""; - proxy_pass http://$cos_host$cos_uri$is_args$args; - proxy_connect_timeout 10s; - proxy_read_timeout 120s; - proxy_buffering off; - } - - # 系统管理前端路由(^~ 须比下方 /system/ API 正则代理更优先) - location = /system/dict { try_files $uri @spa; } - location = /system/warehouseAddress { try_files $uri @spa; } - location = /system/announcement { try_files $uri @spa; } - location = /system/country { try_files $uri @spa; } - location ^~ /system/users { try_files $uri @spa; } - location ^~ /system/roles { try_files $uri @spa; } - location ^~ /system/menus { try_files $uri @spa; } - location ^~ /system/organizations { try_files $uri @spa; } - location ^~ /system/batchEmail { try_files $uri @spa; } - location ^~ /system/otherConfig { try_files $uri @spa; } - location ^~ /system/aiAssistant { try_files $uri @spa; } - location ^~ /system/aiSkills { try_files $uri @spa; } - location ^~ /system/mcpTools { try_files $uri @spa; } - location ^~ /system/dataPerm/ { try_files $uri @spa; } - - # SPA 回退:try_files $uri @spa 统一带 no-cache,避免部署后浏览器缓存旧 index.html - location @spa { - add_header Cache-Control "no-cache, no-store, must-revalidate" always; - add_header Pragma "no-cache" always; - add_header Expires "0" always; - try_files /index.html =404; - } - - location = /index.html { - add_header Cache-Control "no-cache, no-store, must-revalidate" always; - add_header Pragma "no-cache" always; - add_header Expires "0" always; - try_files /index.html =404; - } - - # 微信 JS 安全域名校验:原样返回 txt,禁止 SPA 回落 index.html - location ~* ^/MP_verify_[A-Za-z0-9]+\.txt$ { - default_type text/plain; - charset off; - add_header Cache-Control "no-store"; - try_files $uri =404; - } - - location ~* \.(?:js|css)$ { - try_files $uri =404; - expires 1y; - add_header Cache-Control "public, immutable"; - access_log off; - } - - location / { - try_files $uri @spa; - } -} diff --git a/src/api/mcp/index.ts b/src/api/mcp/index.ts index 459ce64..b3c72c4 100644 --- a/src/api/mcp/index.ts +++ b/src/api/mcp/index.ts @@ -112,7 +112,7 @@ export async function chatAiAgent(payload: { toolKeys?: string[]; maxToolRounds?: number; }): Promise { - // 走 /ai/agent/chat:线上已有;OMS nginx / Vite 须反代 /ai/(见 nginx-*.conf) + // 走 /ai/agent/chat:开发 Vite 反代 /ai/;生产浏览器直连 api.* 域名 // 后端另注册 /mcp/agent/chat,部署后可再切 const res = await axiosInstance.post(mcpUrl('/ai/agent/chat'), payload); const x = assertBusinessOk(res.data) as any; diff --git a/src/api/oms/common.ts b/src/api/oms/common.ts index 0367c20..139cc52 100644 --- a/src/api/oms/common.ts +++ b/src/api/oms/common.ts @@ -106,8 +106,7 @@ export async function omsPost(path: string, body?: Record) /** * COS 签名参数里的 `q-sign-time=start;end` 含分号。 - * 线上 nginx / 部分网关会把 `;` 当查询分隔符,拆成 `q-sign-time=start&end=…`, - * COS 随即返回 AccessDenied / Request has expired;本地 Vite 代理通常不拆,故本地正常。 + * 部分代理会把 `;` 当查询分隔符拆坏签名;本地 Vite `/__cos__` 转发前编成 `%3B`。 */ export function encodeCosSignedQuery(search: string): string { if (!search) return '' @@ -115,10 +114,11 @@ export function encodeCosSignedQuery(search: string): string { } /** - * COS 绝对地址 → 同源 `/__cos__//...`(开发 Vite / 生产 nginx 反代)。 - * cos.ideship.com 等自定义域名 https 不可用,直链 http 又会在 https 页面触发混合内容。 + * COS 绝对地址 → 同源 `/__cos__//...`(仅开发 Vite 反代)。 + * 生产已弃用站内 nginx,无 `/__cos__`;预览走 file-storage/stream,勿改写为该前缀(会落到 SPA)。 */ function toCosSameOriginProxyUrl(absoluteUrl: string): string | null { + if (!import.meta.env.DEV) return null try { const u = new URL(absoluteUrl) const host = u.hostname.toLowerCase() diff --git a/src/components/PrintTemplate/OrderPrintModal.vue b/src/components/PrintTemplate/OrderPrintModal.vue index c9126ef..35d1c82 100644 --- a/src/components/PrintTemplate/OrderPrintModal.vue +++ b/src/components/PrintTemplate/OrderPrintModal.vue @@ -101,7 +101,7 @@ import { import { useHiprintClientState } from '@/utils/hiprint/hiprintClientState'; import { resolveOmsAssetUrl } from '@/api/oms/common'; import PrintOptionsForm from '@/components/PrintTemplate/PrintOptionsForm.vue'; -import { fetchPrintAssetBytes, fetchPrintPreviewBlob } from '@/utils/printPreview'; +import { fetchPrintAssetBytes, openPrintAssetInNewTab } from '@/utils/printPreview'; import { buildHiprintBlobPrintExtras, HIPRINT_PAPER_PRESET_OPTIONS, @@ -258,34 +258,15 @@ function toAbsoluteUrl(url: string) { return `${window.location.origin}/${resolved}`; } -/** - * 查看面单:经 file-storage/stream(或 /__cos__)拉字节再开 blob。 - * 勿把带 `q-sign-time=a;b` 的 COS 链直接丢给浏览器——线上反代/跳转常把 `;` 拆成 `&`, - * COS 会回 AccessDenied/Request has expired(本地 Vite 不拆,故本地正常)。 - */ +/** 查看面单:与附件预览同一套 stream → blob,避免直开 COS 证书/签名问题 */ async function openItemInNative(item: PrintItem) { if (!item?.url) { Message.warning('当前无可打开的打印链接'); return; } - // 同步开空白页,避免 await 后被 Chrome 拦截弹窗 - const win = window.open('about:blank', '_blank'); - if (!win) { - Message.warning('请允许浏览器弹窗以查看面单'); - return; - } try { - win.document.title = '面单加载中…'; - const blob = await fetchPrintPreviewBlob(item.url); - const blobUrl = URL.createObjectURL(blob); - win.location.replace(blobUrl); - window.setTimeout(() => URL.revokeObjectURL(blobUrl), 60_000); + await openPrintAssetInNewTab(item.url, { loadingTitle: '面单加载中…' }); } catch (e: unknown) { - try { - win.close(); - } catch { - // ignore - } const msg = e instanceof Error ? e.message : String(e || ''); Message.warning(msg || '面单打开失败'); } diff --git a/src/utils/printPreview.ts b/src/utils/printPreview.ts index 373d977..d758a37 100644 --- a/src/utils/printPreview.ts +++ b/src/utils/printPreview.ts @@ -47,14 +47,14 @@ function isCosAbsoluteUrl(raw: string): boolean { } /** - * 把 COS 绝对地址改写为同源 `/__cos__//...` 代理路径。 - * 开发由 Vite 代理;生产由 nginx-k3s `/__cos__/` 反代(避免浏览器直打 COS 撞 CORS)。 + * 把 COS 绝对地址改写为同源 `/__cos__//...`(仅开发 Vite)。 + * 生产无站内 nginx/`__cos__`,返回 null,由 file-storage/stream 拉取。 */ export function toCosProxyUrl(url: string): string | null { + if (!import.meta.env.DEV) return null try { const u = new URL(url) if (!isCosHost(u.host)) return null - // 分号必须 %3B,否则线上反代会拆坏签名时间窗(见 encodeCosSignedQuery) return `${COS_PROXY_PREFIX}/${u.host}${u.pathname}${encodeCosSignedQuery(u.search)}` } catch { return null @@ -307,8 +307,8 @@ async function fetchBlobViaSameOriginPath(path: string): Promise { } /** - * 仅经同源 `/__cos__` 代理拉取 COS(开发 Vite / 生产 nginx)。 - * 禁止浏览器直连 cos.*:私有桶未配 CORS 时必失败。 + * 仅经同源 `/__cos__` 代理拉取 COS(仅开发 Vite)。 + * 禁止浏览器直连 cos.*:私有桶未配 CORS / 自定义域证书常不匹配。 */ async function fetchBlobViaCosProxy(rawUrl: string): Promise { const proxied = toCosProxyUrl(rawUrl) @@ -373,15 +373,6 @@ async function fetchPreviewBlob(rawUrl: string): Promise { const streamed = await fetchViaFileStorageStream(raw, errors) if (streamed) return streamed - // 同源 /__cos__ 代理(生产 nginx;开发上面已试过则跳过) - if (isCos && !import.meta.env.DEV) { - try { - return await fetchBlobViaCosProxy(raw) - } catch (e: unknown) { - errors.push(extractOmsRequestError(e) || 'COS 同源代理下载失败') - } - } - // 3) 同源 /uploads 路径 const sameOriginPath = toSameOriginPath(raw) if (sameOriginPath.startsWith('/')) { @@ -407,8 +398,6 @@ export async function fetchPrintPreviewBlob(rawUrl: string): Promise { export function resolvePrintPreviewPdfSource(rawUrl: string): string | null { const raw = String(rawUrl || '').trim() if (!raw || !isPrintPreviewPdf(raw)) return null - // 生产勿把 /__cos__ 交给前端直链:nginx 未配时会落到 SPA HTML - if (!import.meta.env.DEV) return null return toCosProxyUrl(raw) } @@ -442,3 +431,75 @@ export async function fetchPrintAssetBytes(rawUrl: string): Promise } return new Uint8Array(await blob.arrayBuffer()) } + +/** + * 新标签页预览面单/附件。 + * - 开发 + COS:同步打开 Vite `/__cos__/…`(`;`→`%3B`)。 + * - 生产:无站内反代,一律 file-storage/stream → blob(避免直开 cos 证书告警)。 + */ +export async function openPrintAssetInNewTab( + rawUrl: string, + options?: { loadingTitle?: string }, +): Promise { + const raw = String(rawUrl || '').trim() + if (!raw) { + throw new Error('预览地址为空') + } + + // 仅 DEV 有 /__cos__;生产打开该路径会落到 SPA + if (import.meta.env.DEV && /^https?:\/\//i.test(raw) && isCosAbsoluteUrl(raw)) { + const proxied = toCosProxyUrl(raw) + if (proxied) { + const win = window.open(proxied, '_blank', 'noopener,noreferrer') + if (!win) { + throw new Error('请允许浏览器弹窗以预览文件') + } + return + } + } + + // 同步开页,避免 await 后被当成非用户手势拦截;失败时在页内写错误,不直接 close + const win = window.open('about:blank', '_blank') + if (!win) { + throw new Error('请允许浏览器弹窗以预览文件') + } + const title = options?.loadingTitle || '加载中…' + try { + win.document.open() + win.document.write( + `${escapeHtml(title)}` + + `${escapeHtml(title)}…`, + ) + win.document.close() + } catch { + // 个别环境写 document 失败时仍继续拉 blob + } + + try { + const blob = await fetchPreviewBlob(raw) + const blobUrl = URL.createObjectURL(blob) + win.location.href = blobUrl + window.setTimeout(() => URL.revokeObjectURL(blobUrl), 120_000) + } catch (e) { + const msg = e instanceof Error ? e.message : String(e || '预览失败') + try { + win.document.open() + win.document.write( + `预览失败` + + `${escapeHtml(msg)}`, + ) + win.document.close() + } catch { + // ignore + } + throw e + } +} + +function escapeHtml(text: string): string { + return String(text) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') +} diff --git a/src/views/order/components/OrderAttachmentsTab.vue b/src/views/order/components/OrderAttachmentsTab.vue index c534d3a..a39864c 100644 --- a/src/views/order/components/OrderAttachmentsTab.vue +++ b/src/views/order/components/OrderAttachmentsTab.vue @@ -41,7 +41,7 @@ {{ file.fileName || file.fileUrl }} @@ -53,7 +53,7 @@ {{ $t('order.myOrders.drawer.attachments.preview') }} @@ -91,7 +91,8 @@ import { uploadOrderFiles, type OrderFileItem, } from '@/api/order'; -import { resolveOmsAssetUrl } from '@/api/oms/common'; +import { extractOmsRequestError } from '@/api/oms/common'; +import { openPrintAssetInNewTab } from '@/utils/printPreview'; const FILE_TYPE_DEFS = [ { @@ -166,26 +167,24 @@ const visibleSections = computed(() => { })); }); -/** 与打印弹窗「查看面单」一致:同步打开可访问 URL(勿先 await 再开 blob,Chrome 会当搜索词) */ -function toAbsoluteUrl(path?: string) { - const resolved = resolveOmsAssetUrl(path) || String(path || '').trim(); - if (!resolved) return ''; - if (/^https?:\/\//i.test(resolved) || resolved.startsWith('blob:') || resolved.startsWith('data:')) { - return resolved; - } - if (resolved.startsWith('/')) return `${window.location.origin}${resolved}`; - return `${window.location.origin}/${resolved}`; -} - -function openFile(file: OrderFileItem) { - const url = toAbsoluteUrl(file.fileUrl); - if (!url) { +/** 与打印弹窗「查看面单」一致:开发可走 /__cos__,生产 stream → blob,不直开 cos 域名 */ +async function openFile(file: OrderFileItem) { + const raw = String(file?.fileUrl || '').trim(); + if (!raw) { Message.warning(t('order.myOrders.drawer.attachments.previewFailed')); return; } - const win = window.open(url, '_blank', 'noopener,noreferrer'); - if (!win) { - Message.warning(t('order.myOrders.drawer.attachments.previewBlocked')); + try { + await openPrintAssetInNewTab(raw, { + loadingTitle: t('order.myOrders.drawer.attachments.preview'), + }); + } catch (e: unknown) { + const msg = extractOmsRequestError(e) || (e instanceof Error ? e.message : ''); + if (/弹窗|popup|blocked/i.test(msg)) { + Message.warning(t('order.myOrders.drawer.attachments.previewBlocked')); + return; + } + Message.warning(msg || t('order.myOrders.drawer.attachments.previewFailed')); } } diff --git a/vite.config.ts b/vite.config.ts index 744c327..747ed73 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -276,7 +276,7 @@ export default defineConfig(({ mode }) => { changeOrigin: true, secure: false, }, - // 开发环境面单/截图:同源代理 COS(自定义域名 https 不可用,上游走 http) + // 仅开发:同源代理 COS(生产无 /__cos__,走 file-storage/stream) '/__cos__': { target: 'http://cos.ideship.com', changeOrigin: true, @@ -382,7 +382,7 @@ export default defineConfig(({ mode }) => { changeOrigin: true, secure: false, }, - // MCP 工具 / AI Agent(须与 nginx /mcp/ 反代一致) + // MCP 工具 / AI Agent(开发 Vite 反代;生产直连 api.*) '/mcp': { target: omsAdminTarget, changeOrigin: true, -- 2.45.2 From 95918100cfa6cd7b4e1ea7f18b7c04889482f8cf Mon Sep 17 00:00:00 2001 From: zzh <1368735060@qq.com> Date: Fri, 23 Oct 2026 13:44:58 +0800 Subject: [PATCH 3/6] =?UTF-8?q?fix:=20=E9=9D=A2=E5=8D=95=E9=A2=84=E8=A7=88?= =?UTF-8?q?=E4=BC=98=E5=85=88=20access-url=EF=BC=8C=E8=AE=BE=E8=AE=A1?= =?UTF-8?q?=E5=99=A8=E8=A1=A5=E7=AE=B1=E5=AD=90=E6=80=BB=E6=95=B0=20mock?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 生产预览先换新鲜签名直开,失败再 stream→blob;标签设计 mock 对齐 boxCount。 Co-authored-by: Cursor --- docs/前端页面设计规范.md | 2 +- src/utils/printPreview.ts | 73 +++++++++++++------ .../labelDesign/LabelDesignerPanel.vue | 1 + 3 files changed, 52 insertions(+), 24 deletions(-) diff --git a/docs/前端页面设计规范.md b/docs/前端页面设计规范.md index 0c6752e..92b6bdd 100644 --- a/docs/前端页面设计规范.md +++ b/docs/前端页面设计规范.md @@ -572,7 +572,7 @@ const { t } = useI18n(); 1. **前后端分域名**(方案 A):站点如 `ideship.com` / `test.ideship.com`,API 如 `api.ideship.com` / `api.test.ideship.com`。浏览器直连 API,前端容器不做 Admin 反代,也无 `/__cos__` 反代。 2. SPA 回退由 `serve -s`(或等价静态服务器)处理;刷新 `/order/batchJobs` 等路径应回 `index.html`,不依赖 nginx `try_files`。 3. 官网 **`/`** 为 Vue 路由(`src/views/site`);静态资源在 **`/site/`**;**`/9610.html`** 为 `public/9610.html`。客户登录 **`/login`**,管理端 **`/admin`**。 -4. 面单/附件预览:开发环境可用 Vite `/__cos__`;**生产**经鉴权 `file-storage/stream` → blob,勿直开 `https://cos.*`(自定义域证书常不匹配)。 +4. 面单/附件预览:开发可用 Vite `/__cos__`;生产优先 `file-storage/access-url` 新鲜签名直开,失败再 `stream` → blob(勿依赖过期库内签名 URL)。 5. 本地 `pnpm dev` 不受 Ingress 限制;`/` 即为官网。 --- diff --git a/src/utils/printPreview.ts b/src/utils/printPreview.ts index d758a37..ba1e423 100644 --- a/src/utils/printPreview.ts +++ b/src/utils/printPreview.ts @@ -1,5 +1,5 @@ import { axiosInstance } from '@/api/rbac' -import { normalizeFileStorageKey, streamFileBlob } from '@/api/fileStorage' +import { getFileAccessUrl, normalizeFileStorageKey, streamFileBlob } from '@/api/fileStorage' import { encodeCosSignedQuery, extractOmsRequestError, @@ -432,10 +432,49 @@ export async function fetchPrintAssetBytes(rawUrl: string): Promise return new Uint8Array(await blob.arrayBuffer()) } +/** + * 经 /file-storage/access-url 换新鲜可直开地址(重新签名)。 + * 返回非 http(s)(如 Local 降级 key)时视为失败,由调用方走 stream→blob。 + */ +async function resolveFreshAccessUrl(rawUrl: string): Promise { + const keys = await buildPrintStreamKeyCandidates(rawUrl) + if (!keys.length) return null + + // 优先 object key,再试完整签名 URL(后端会剥签重签) + const ordered = [ + ...keys.filter((k) => !/^https?:\/\//i.test(k)), + ...keys.filter((k) => /^https?:\/\//i.test(k)), + ] + + for (const key of ordered) { + try { + const { url } = await getFileAccessUrl(key) + const trimmed = String(url || '').trim() + if (/^https?:\/\//i.test(trimmed)) return trimmed + } catch { + // 试下一个候选 + } + } + return null +} + +function writePreviewStatus(win: Window, title: string, bodyHtml: string, color = '#666') { + try { + win.document.open() + win.document.write( + `${escapeHtml(title)}` + + `${bodyHtml}`, + ) + win.document.close() + } catch { + // 跨域或 document 不可写时忽略 + } +} + /** * 新标签页预览面单/附件。 * - 开发 + COS:同步打开 Vite `/__cos__/…`(`;`→`%3B`)。 - * - 生产:无站内反代,一律 file-storage/stream → blob(避免直开 cos 证书告警)。 + * - 其余:access-url 新鲜签名直开;失败再 file-storage/stream → blob。 */ export async function openPrintAssetInNewTab( rawUrl: string, @@ -458,40 +497,28 @@ export async function openPrintAssetInNewTab( } } - // 同步开页,避免 await 后被当成非用户手势拦截;失败时在页内写错误,不直接 close + // 同步开页,避免 await 后被当成非用户手势拦截 const win = window.open('about:blank', '_blank') if (!win) { throw new Error('请允许浏览器弹窗以预览文件') } const title = options?.loadingTitle || '加载中…' - try { - win.document.open() - win.document.write( - `${escapeHtml(title)}` + - `${escapeHtml(title)}…`, - ) - win.document.close() - } catch { - // 个别环境写 document 失败时仍继续拉 blob - } + writePreviewStatus(win, title, `${escapeHtml(title)}…`) try { + const accessUrl = await resolveFreshAccessUrl(raw) + if (accessUrl) { + win.location.href = accessUrl + return + } + const blob = await fetchPreviewBlob(raw) const blobUrl = URL.createObjectURL(blob) win.location.href = blobUrl window.setTimeout(() => URL.revokeObjectURL(blobUrl), 120_000) } catch (e) { const msg = e instanceof Error ? e.message : String(e || '预览失败') - try { - win.document.open() - win.document.write( - `预览失败` + - `${escapeHtml(msg)}`, - ) - win.document.close() - } catch { - // ignore - } + writePreviewStatus(win, '预览失败', escapeHtml(msg), '#c00') throw e } } diff --git a/src/views/channel/labelDesign/LabelDesignerPanel.vue b/src/views/channel/labelDesign/LabelDesignerPanel.vue index 8321764..dcb551f 100644 --- a/src/views/channel/labelDesign/LabelDesignerPanel.vue +++ b/src/views/channel/labelDesign/LabelDesignerPanel.vue @@ -111,6 +111,7 @@ const MOCK_BY_FIELD: Record = { markReference: 'SF1234567890', markBoxNo: 'BOX-001', boxSeqNo: '1', + boxCount: '2', barcode: 'BOX-001', boxIndexText: '1/2', skuSummary: 'SKU001 T-Shirt×2\nSKU002 Pants×1', -- 2.45.2 From 07b71ca565ab5c87c22d9c305ae0e9cd2d48355c Mon Sep 17 00:00:00 2001 From: zzh <1368735060@qq.com> Date: Sat, 24 Oct 2026 13:33:00 +0800 Subject: [PATCH 4/6] =?UTF-8?q?feat:=20=E5=AE=98=E7=BD=91=E7=BA=B8?= =?UTF-8?q?=E9=9D=A2=E5=AF=B9=E8=AF=9D=E4=B8=8E=E7=99=BB=E5=BD=95=E7=BF=BB?= =?UTF-8?q?=E9=A1=B5=E8=BF=87=E6=B8=A1=EF=BC=8C=E6=B3=A2=E6=B5=AA=E6=9B=B4?= =?UTF-8?q?=E7=A8=B3=EF=BC=8C=E5=B9=B6=E5=90=8C=E6=AD=A5=E9=9D=A2=E5=8D=95?= =?UTF-8?q?=E6=89=93=E5=8D=B0=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 欢迎区/侧栏/聊天槽统一暖纸拟物;跟进改仓储代发;波浪降扰动关视差减少眩晕。 Co-authored-by: Cursor --- src/App.vue | 268 +++++- src/components/HandsontablePro/index.vue | 141 ++- .../PrintTemplate/OrderPrintModal.vue | 244 +++++- src/router/guard/userLoginInfo.ts | 3 + src/utils/hiprint/dispatchPrintUrl.ts | 15 +- src/utils/hiprint/hiprintElectronSocket.ts | 130 ++- src/utils/hiprint/hiprintPrintOptions.ts | 22 + src/utils/page-transition-flag.ts | 34 + .../login/components/LoginPortalSwitch.vue | 15 + src/views/login/index.vue | 7 +- src/views/order/waybillManage/index.vue | 335 +++++--- src/views/site/SiteAdvisorShell.vue | 167 ++-- src/views/site/SiteHeader.vue | 60 +- src/views/site/SiteRoutesPanel.vue | 72 +- src/views/site/index.vue | 60 +- src/views/site/lib/site-urls.ts | 105 ++- src/views/site/lib/site-welcome.ts | 53 +- src/views/site/styles/ai-advisor.css | 800 +++++++++++------- 18 files changed, 1869 insertions(+), 662 deletions(-) diff --git a/src/App.vue b/src/App.vue index 9d2fb68..2d6db0f 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,21 +1,216 @@ diff --git a/src/views/site/lib/site-urls.ts b/src/views/site/lib/site-urls.ts index 81db988..6f66492 100644 --- a/src/views/site/lib/site-urls.ts +++ b/src/views/site/lib/site-urls.ts @@ -1,18 +1,93 @@ -export function officialAppHomeUrl() { +import type { Router } from 'vue-router'; +import { armOfficialLoginFlip } from '@/utils/page-transition-flag'; + +function preferMobilePortal() { const ua = navigator.userAgent || ''; - const mobile = /MicroMessenger|Android|iPhone|iPod|iPad|Mobile/i.test(ua); - return new URL(mobile ? '/h5' : '/home/workplace', window.location.origin).toString(); + return /MicroMessenger|Android|iPhone|iPod|iPad|Mobile/i.test(ua); +} + +export function officialAppHomeUrl() { + return new URL( + preferMobilePortal() ? '/h5' : '/home/workplace', + window.location.origin, + ).toString(); +} + +export function officialLoginLocation(dest?: string) { + const path = preferMobilePortal() ? '/h5/login' : '/login'; + const query: Record = { from: 'official-site' }; + if (dest) query.dest = dest; + return { path, query }; } export function officialLoginUrl(dest?: string) { - const ua = navigator.userAgent || ''; - const mobile = /MicroMessenger|Android|iPhone|iPod|iPad|Mobile/i.test(ua); - const url = new URL(mobile ? '/h5/login' : '/login', window.location.origin); - url.searchParams.set('from', 'official-site'); - if (dest) url.searchParams.set('dest', dest); + const { path, query } = officialLoginLocation(dest); + const url = new URL(path, window.location.origin); + Object.entries(query).forEach(([key, value]) => { + url.searchParams.set(key, value); + }); return url.toString(); } +/** 同域 SPA 进登录;普通左键走翻页,修饰键仍允许新标签打开 */ +export function goOfficialLogin( + router: Router, + dest?: string, + event?: MouseEvent, +) { + if (event) { + if (event.defaultPrevented) return false; + if ( + event.button !== 0 || + event.metaKey || + event.ctrlKey || + event.shiftKey || + event.altKey + ) { + return false; + } + event.preventDefault(); + } + + const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; + if (!reduceMotion) { + armOfficialLoginFlip(); + } + + // 先确保登录分包就绪,再开翻页,避免顶栏进度条干等 + void (async () => { + try { + if (preferMobilePortal()) { + await import('@/views/h5/customer-center/login/index.vue'); + } else { + await import('@/views/login/index.vue'); + } + } catch { + /* ignore:仍继续跳转 */ + } + await router.push(officialLoginLocation(dest)); + })(); + return true; +} + +/** 从已有 login URL 解析 dest 后 SPA 跳转 */ +export function goOfficialLoginFromHref( + router: Router, + href: string, + event?: MouseEvent, +) { + try { + const url = new URL(href, window.location.origin); + if (url.origin !== window.location.origin) return false; + if (url.pathname !== '/login' && url.pathname !== '/h5/login') return false; + if (url.searchParams.get('from') !== 'official-site') return false; + const dest = url.searchParams.get('dest') || undefined; + return goOfficialLogin(router, dest, event); + } catch { + return false; + } +} + export function warmupOfficialLogin() { const warmed = document.documentElement.dataset.omsLoginWarm; if (warmed) return; @@ -28,16 +103,22 @@ export function warmupOfficialLogin() { prefetch.rel = 'prefetch'; prefetch.href = url; document.head.appendChild(prefetch); + // 预拉取登录分包,翻页进入时少等 chunk + void import('@/views/login/index.vue'); } catch { /* ignore */ } } export function officialTrackLoginUrl(number?: string) { - const ua = navigator.userAgent || ''; - const mobile = /MicroMessenger|Android|iPhone|iPod|iPad|Mobile/i.test(ua); - const url = new URL(mobile ? '/h5/login' : '/login', window.location.origin); - url.searchParams.set('redirect', mobile ? '/h5/orders' : '/home/workplace'); + const url = new URL( + preferMobilePortal() ? '/h5/login' : '/login', + window.location.origin, + ); + url.searchParams.set( + 'redirect', + preferMobilePortal() ? '/h5/orders' : '/home/workplace', + ); if (number) url.searchParams.set('track', number); return url.toString(); } diff --git a/src/views/site/lib/site-welcome.ts b/src/views/site/lib/site-welcome.ts index dbcfc27..3fe5bbb 100644 --- a/src/views/site/lib/site-welcome.ts +++ b/src/views/site/lib/site-welcome.ts @@ -4,7 +4,6 @@ export const WELCOME_COPY = { titleShort: 'IDE. Logistics advisor', titleRest: ' Your cross-border logistics advisor', titleRestShort: ' Logistics advisor', - sub: 'US, EU and Southeast Asia parcels. List rates after you register. One advisor through to delivery.', placeholder: 'e.g. 5 kg general cargo to the US, how many days?', sendLabel: 'Ask', human: 'Talk to an advisor', @@ -18,6 +17,8 @@ export const WELCOME_COPY = { statsAria: 'Service highlights', composeLead: 'Where are you shipping? Pick a destination, or ask below.', destAria: 'Pick a destination', + groupFulfill: 'Shipping', + groupGrowth: 'Benefits', partnerTag: 'Partners', partnerAria: 'Connected platforms and carriers', trustIcp: '粤ICP备2025463970号-2', @@ -29,7 +30,6 @@ export const WELCOME_COPY = { titleShort: 'IDE.跨境物流顾问', titleRest: '您身边的跨境物流顾问', titleRestShort: '跨境物流顾问', - sub: '中小卖家也能按线路看一口价。不砍价、不拉大群,一个人从报价跟到签收。', placeholder: '例如:发美国普货,5kg,要几天', sendLabel: '提问', human: '联系顾问', @@ -43,6 +43,8 @@ export const WELCOME_COPY = { statsAria: '服务数据', composeLead: '发到哪?点目的地看线路,或直接提问。', destAria: '选择目的地', + groupFulfill: '履约', + groupGrowth: '权益', partnerTag: '合作伙伴', partnerAria: '对接平台与服务商', trustIcp: '粤ICP备2025463970号-2', @@ -97,6 +99,7 @@ export const SITE_INTENTS = [ { id: 'track', icon: 'track', + followup: false, en: { title: 'Where is my parcel?', hint: 'Sign in to see live scans from pickup to delivery', @@ -108,6 +111,20 @@ export const SITE_INTENTS = [ label: '这票货从揽收到派送卡在哪', }, }, + { + id: 'warehouse', + icon: 'track', + en: { + title: 'Warehouse fulfillment', + hint: 'Inbound, pick & pack — ship when orders land', + label: 'I need warehouse dropshipping', + }, + zh: { + title: '仓储代发', + hint: '入仓拣货打包,订单来了就发出去', + label: '我要仓储一件代发', + }, + }, { id: 'hard', icon: 'hard', @@ -207,43 +224,49 @@ export const HOME_DOORS = [ id: 'hard', icon: 'hard', intentId: 'hard' as const, - zh: { title: '带电、别人不接也能问', hint: '先帮你看出得去的线路,再谈税。' }, - en: { title: 'Battery or declined cargo', hint: 'We find a lane that can leave, then the tax path.' }, + primary: true, + zh: { title: '敏感难发', hint: '先帮你看出得去的线路,再谈税。' }, + en: { title: 'Sensitive cargo', hint: 'We find a lane that can leave, then the tax path.' }, }, { - id: 'track', + id: 'warehouse', icon: 'track', - intentId: 'track' as const, - zh: { title: '货到哪了', hint: '登录后看揽收到派送的真实节点。' }, - en: { title: 'Where is my parcel?', hint: 'Sign in to see scans from pickup to delivery.' }, + intentId: 'warehouse' as const, + primary: true, + zh: { title: '仓储代发', hint: '入仓拣货打包,订单来了就发出去。' }, + en: { title: 'Warehouse fulfillment', hint: 'Inbound, pick & pack — ship when orders land.' }, }, { id: 'clearance', icon: 'clearance', intentId: 'compliance' as const, - zh: { title: '出口报关 / 退税', hint: '正式报关、退税,或没有专票怎么出。' }, - en: { title: 'Export paperwork', hint: 'Declaration, VAT refund, or shipping without invoices.' }, + primary: true, + zh: { title: '报关退税', hint: '正式报关、退税,或没有专票怎么出。' }, + en: { title: 'Customs & VAT', hint: 'Declaration, refund, or shipping without invoices.' }, }, { id: 'member', icon: 'growth', intentId: 'member' as const, - zh: { title: '会员成长体系', hint: '近 12 个月发货积分定级,基础运费最高 95 折。' }, - en: { title: 'Member growth', hint: 'Rolling 12-month shipping points — up to 95% on base freight.' }, + primary: false, + zh: { title: '会员成长', hint: '近 12 个月发货积分定级,基础运费最高 95 折。' }, + en: { title: 'Member tiers', hint: '12-month shipping points — up to 95% on base freight.' }, }, { id: 'invite', icon: 'coin', intentId: 'invite' as const, + primary: false, zh: { title: '邀新分成', hint: '好友派送成功,你拿运费余额 + 20 元券。' }, - en: { title: 'Invite & earn', hint: 'When friends deliver, you get freight credit + a ¥20 coupon.' }, + en: { title: 'Invite & earn', hint: 'Friends deliver → freight credit + ¥20 coupon.' }, }, { id: 'newcomer', icon: 'coupon', intentId: 'newcomer' as const, - zh: { title: '新人注册福利', hint: '注册送券:首单、备货、第二单都有。' }, - en: { title: 'New-seller coupons', hint: 'Sign up for first-ship, stock-up, and second-ship coupons.' }, + primary: false, + zh: { title: '新人福利', hint: '注册送券:首单、备货、第二单都有。' }, + en: { title: 'New-seller deals', hint: 'Coupons for first ship, stock-up, and second ship.' }, }, ] as const; diff --git a/src/views/site/styles/ai-advisor.css b/src/views/site/styles/ai-advisor.css index 5350591..a9dedc2 100644 --- a/src/views/site/styles/ai-advisor.css +++ b/src/views/site/styles/ai-advisor.css @@ -55,7 +55,7 @@ --ai-routes-panel-w: 1080px; --ai-acrylic-bg: rgba(255, 255, 255, 0.32); --ai-acrylic-bg-primary: color-mix(in srgb, var(--ai-gold-soft) 42%, rgba(255, 255, 255, 0.38)); - /* Ocean glass — cool tint aligned with GradientWaves */ + /* Soft fills — cool tint aligned with GradientWaves */ --ai-glass-fill: linear-gradient( 165deg, color-mix(in srgb, var(--ai-sky) 10%, rgba(255, 255, 255, 0.46)) 0%, @@ -83,6 +83,21 @@ color-mix(in srgb, var(--ai-border) 55%, transparent) 82%, transparent 100% ); + /* 厚纸拟物(欢迎页 / 对话 / 侧栏共用) */ + --ai-dock-radius: 28px; + --ai-dock-radius-inner: 22px; + --ai-chip-radius: 999px; + --ai-paper: #fffcf8; + --ai-paper-edge: color-mix(in srgb, var(--ai-border) 78%, #c4b8a6); + --ai-paper-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.9), + 0 8px 6px -6px rgba(40, 32, 20, 0.1), + 0 18px 32px -16px rgba(40, 32, 20, 0.16); + --ai-paper-shadow-lift: + inset 0 1px 0 rgba(255, 255, 255, 0.95), + 0 10px 8px -6px rgba(40, 32, 20, 0.12), + 0 22px 40px -14px rgba(2, 62, 138, 0.12); + --ai-trust-bar-h: 56px; } body.command-home { @@ -270,9 +285,10 @@ body.command-home #navbar .site-nav-dock a { body.command-home .site-nav-dock__slider.is-ready { transition: - left 0.42s cubic-bezier(0.32, 0.72, 0, 1), - width 0.42s cubic-bezier(0.32, 0.72, 0, 1), - opacity 0.2s ease; + left 0.4s cubic-bezier(0.22, 1, 0.36, 1), + width 0.4s cubic-bezier(0.22, 1, 0.36, 1), + opacity 0.22s ease; + will-change: left, width; } body.command-home #navbar li.site-nav-dock:hover > a, @@ -302,7 +318,9 @@ body.command-home #navbar .site-nav-dock a { white-space: nowrap; background: transparent; box-shadow: none; - transition: color 0.22s ease; + transition: + color 0.28s var(--ai-ease, cubic-bezier(0.22, 1, 0.36, 1)), + transform 0.28s var(--ai-ease, cubic-bezier(0.22, 1, 0.36, 1)); } body.command-home #navbar .site-nav-dock a::after { @@ -313,7 +331,7 @@ body.command-home #navbar .site-nav-dock a { color: var(--ai-text); border-color: transparent; background: transparent; - transform: none; + transform: translateY(-0.5px); } body.command-home #navbar .site-nav-dock a.active, @@ -349,7 +367,7 @@ body.command-home #navbar .site-nav-dock a { } body.command-home #navbar .site-nav-dock a.is-active .site-nav-dock__icon { - animation: h5-dock-icon-pop 0.42s cubic-bezier(0.22, 1.35, 0.36, 1); + animation: h5-dock-icon-pop 0.36s var(--ai-ease, cubic-bezier(0.22, 1, 0.36, 1)); } body.command-home .site-nav-dock__label { @@ -512,13 +530,12 @@ body.command-home #navbar .site-nav-dock a { @keyframes h5-dock-icon-pop { 0% { - transform: scale(0.88); - } - 70% { - transform: scale(1.08); + transform: scale(0.94); + opacity: 0.72; } 100% { transform: scale(1); + opacity: 1; } } @@ -683,8 +700,8 @@ body.command-home #ai-advisor-shell { background: radial-gradient(ellipse 78% 52% at 50% -8%, rgba(255, 255, 255, 0.92), transparent 58%), radial-gradient(ellipse 48% 42% at 8% 36%, rgba(255, 255, 255, 0.42), transparent 68%), - radial-gradient(ellipse 44% 40% at 92% 48%, rgba(18, 24, 32, 0.035), transparent 70%), - radial-gradient(ellipse 70% 48% at 50% 108%, rgba(18, 24, 32, 0.045), transparent 60%), + radial-gradient(ellipse 44% 40% at 92% 48%, rgba(18, 24, 32, 0.03), transparent 70%), + radial-gradient(ellipse 70% 48% at 50% 108%, rgba(92, 122, 154, 0.04), transparent 60%), var(--ai-bg); } @@ -698,7 +715,7 @@ body.command-home #ai-advisor-shell { radial-gradient(ellipse 46% 38% at 28% 24%, rgba(255, 255, 255, 0.55), transparent 70%), radial-gradient(ellipse 42% 36% at 74% 32%, rgba(255, 255, 255, 0.35), transparent 72%), radial-gradient(ellipse 50% 42% at 58% 78%, rgba(255, 255, 255, 0.22), transparent 74%), - radial-gradient(ellipse 40% 34% at 18% 72%, rgba(18, 24, 32, 0.035), transparent 70%); + radial-gradient(ellipse 40% 34% at 18% 72%, rgba(18, 24, 32, 0.03), transparent 70%); opacity: 0.9; } @@ -1057,13 +1074,11 @@ body.command-home .ai-stage-map--eu { .ai-msg-row--assistant .ai-msg-bubble { position: relative; isolation: isolate; - border: 1px solid var(--ai-border); - background: var(--ai-glass-panel); - backdrop-filter: blur(18px) saturate(175%); - -webkit-backdrop-filter: blur(18px) saturate(175%); - box-shadow: - inset 0 1px 0 rgba(255, 255, 255, 0.98), - 0 0 0 1px var(--ai-glass-hairline); + border: 1px solid var(--ai-paper-edge); + background: var(--ai-paper); + backdrop-filter: none; + -webkit-backdrop-filter: none; + box-shadow: var(--ai-paper-shadow); } .ai-composer { @@ -1072,12 +1087,12 @@ body.command-home .ai-stage-map--eu { overflow: hidden; min-height: 88px; padding: 16px 16px 16px 24px; - border-radius: 20px; - border: 1px solid var(--ai-glass-border); - background: var(--ai-glass-fill); - backdrop-filter: var(--ai-glass-blur); - -webkit-backdrop-filter: var(--ai-glass-blur); - box-shadow: var(--ai-glass-shadow); + border-radius: var(--ai-dock-radius); + border: 1px solid var(--ai-paper-edge); + background: var(--ai-paper); + backdrop-filter: none; + -webkit-backdrop-filter: none; + box-shadow: var(--ai-paper-shadow); transition: border-color 0.2s var(--ai-ease), background 0.2s var(--ai-ease), @@ -1085,34 +1100,17 @@ body.command-home .ai-stage-map--eu { } .ai-composer::before { - content: ''; - position: absolute; - inset: 0; - z-index: 0; - border-radius: inherit; - pointer-events: none; - background: linear-gradient(180deg, rgba(255, 255, 255, 0.5) 0%, transparent 44%); - opacity: 0.7; - mix-blend-mode: soft-light; + display: none; } .ai-composer:focus-within { - border-color: rgba(255, 255, 255, 0.94); - background: var(--ai-glass-fill-strong); + border-color: color-mix(in srgb, var(--ai-action) 22%, var(--ai-paper-edge)); + background: #fffefb; + box-shadow: var(--ai-paper-shadow-lift); } .ai-msg-row--assistant .ai-msg-bubble:not(:has(.ai-card))::before { - content: ''; - position: absolute; - inset: 0; - z-index: 0; - border-radius: inherit; - pointer-events: none; - background: - linear-gradient(180deg, rgba(255, 255, 255, 0.82) 0%, rgba(255, 255, 255, 0) 40%), - radial-gradient(92% 72% at 10% -8%, rgba(255, 255, 255, 0.95), transparent 58%); - mix-blend-mode: screen; - opacity: 0.9; + display: none; } .ai-composer > .ai-composer-form, @@ -1141,7 +1139,7 @@ body.command-home .ai-stage-map--eu { content: none; } -/* 工具栏 / 快捷芯片 — 极简磨砂玻璃(与意图卡片、导航坞一致) */ +/* 工具栏 / 快捷芯片 — 暖纸面(与首页目的地芯片一致) */ .ai-sidebar-toggle, .ai-new-chat-ghost, .ai-new-chat-btn, @@ -1149,15 +1147,18 @@ body.command-home .ai-stage-map--eu { .ai-history-item { position: relative; isolation: isolate; - border: 1px solid rgba(255, 255, 255, 0.72); - background: var(--ai-acrylic-bg); - backdrop-filter: blur(28px) saturate(160%); - -webkit-backdrop-filter: blur(28px) saturate(160%); - box-shadow: none; + border: 1px solid var(--ai-paper-edge); + background: linear-gradient(180deg, #fffefb 0%, var(--ai-bg-soft) 100%); + backdrop-filter: none; + -webkit-backdrop-filter: none; + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.9), + 0 3px 8px rgba(40, 32, 20, 0.06); transition: border-color 0.2s var(--ai-ease), background 0.2s var(--ai-ease), - color 0.2s var(--ai-ease); + color 0.2s var(--ai-ease), + box-shadow 0.2s var(--ai-ease); } .ai-sidebar-toggle::before, @@ -1242,8 +1243,11 @@ body.command-home .ai-stage-map--eu { .ai-sidebar-toggle:hover, .ai-new-chat-ghost:hover { color: var(--ai-action); - border-color: rgba(255, 255, 255, 0.92); - background: rgba(255, 255, 255, 0.44); + border-color: color-mix(in srgb, var(--ai-action) 24%, var(--ai-paper-edge)); + background: linear-gradient(180deg, #ffffff 0%, #fff8ef 100%); + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.95), + 0 6px 14px rgba(40, 32, 20, 0.08); } .ai-sidebar-toggle:hover i, @@ -1253,20 +1257,23 @@ body.command-home .ai-stage-map--eu { .ai-history-sidebar { position: fixed; - top: var(--ai-header-h); - left: 0; - bottom: 0; + top: calc(var(--ai-header-h) + 10px); + left: 12px; + bottom: calc(var(--ai-trust-bar-h) + 10px); z-index: 20; - width: min(280px, 86vw); + width: min(268px, calc(86vw - 24px)); display: flex; flex-direction: column; - padding: 16px 12px 20px; - border-right: 1px solid rgba(255, 255, 255, 0.55); - background: color-mix(in srgb, var(--ai-bg) 90%, var(--ai-surface)); - backdrop-filter: blur(28px) saturate(160%); - -webkit-backdrop-filter: blur(28px) saturate(160%); - box-shadow: none; - transform: translateX(-105%); + padding: 16px 12px 16px; + border-radius: var(--ai-dock-radius); + border: 1px solid color-mix(in srgb, var(--ai-paper-edge) 65%, transparent); + background: color-mix(in srgb, var(--ai-paper) 72%, transparent); + backdrop-filter: none; + -webkit-backdrop-filter: none; + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.55), + 0 10px 28px -12px rgba(40, 32, 20, 0.12); + transform: translateX(calc(-100% - 16px)); transition: transform 0.28s var(--ai-ease); } @@ -1861,17 +1868,64 @@ body.command-home .ai-stage-map--eu { display: flex; flex-wrap: wrap; align-items: center; - justify-content: space-between; gap: 8px 12px; } .ai-routes-meta { margin: 0; + flex-shrink: 0; font-size: 0.76rem; font-weight: 650; color: var(--ai-text-3); } +.ai-routes-pager { + display: inline-flex; + flex-shrink: 0; + align-items: center; + gap: 4px; +} + +.ai-routes-pager__btn { + display: inline-flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + margin: 0; + padding: 0; + border-radius: 8px; + border: 1px solid var(--ai-border-soft); + background: color-mix(in srgb, #fff 78%, var(--ai-bg)); + color: var(--ai-text-2); + font-size: 1rem; + font-weight: 600; + line-height: 1; + cursor: pointer; +} + +.ai-routes-pager__btn:hover:not(:disabled) { + color: var(--ai-action); + border-color: color-mix(in srgb, var(--ai-action) 35%, #fff); + background: color-mix(in srgb, var(--ai-action) 8%, #fff); +} + +.ai-routes-pager__btn:disabled { + opacity: 0.4; + cursor: not-allowed; +} + +.ai-routes-pager__label { + min-width: 3.2em; + padding: 0 4px; + text-align: center; + font-size: 0.76rem; + font-weight: 700; + color: var(--ai-text-2); + font-variant-numeric: tabular-nums; + white-space: nowrap; +} + .ai-routes-sticky { margin-left: auto; min-height: 32px; @@ -1932,11 +1986,13 @@ body.command-home .ai-stage-map--eu { display: flex; flex-direction: column; gap: 8px; + min-width: 0; min-height: 0; padding: 14px 16px; border-radius: 16px; border: 1px solid var(--ai-border-soft); background: color-mix(in srgb, #fff 78%, var(--ai-bg)); + overflow: hidden; } .ai-route-card__head { @@ -2028,13 +2084,6 @@ body.command-home .ai-stage-map--eu { font-weight: 800; } -.ai-route-card__cargo { - margin: 0; - font-size: 0.76rem; - line-height: 1.35; - color: var(--ai-text-3); -} - .ai-route-card__desc { margin: 0; font-size: 0.8rem; @@ -2066,16 +2115,20 @@ body.command-home .ai-stage-map--eu { .ai-route-card__actions { display: flex; - flex-wrap: wrap; + flex-wrap: nowrap; align-items: center; justify-content: flex-end; - gap: 8px; + gap: 6px; + width: 100%; + min-width: 0; margin-top: auto; padding-top: 4px; + box-sizing: border-box; } .ai-route-card__ask { display: inline-flex; + flex-shrink: 0; align-items: center; justify-content: center; width: 28px; @@ -2120,7 +2173,25 @@ body.command-home .ai-stage-map--eu { filter: brightness(1.06); } -.ai-route-card__reg, +.ai-route-card__reg { + display: inline-flex; + flex-shrink: 0; + align-items: center; + justify-content: center; + min-height: 28px; + max-width: calc(100% - 34px); + padding: 0 4px; + border: 0; + background: none; + color: var(--ai-text-3); + font-size: 0.78rem; + font-weight: 600; + line-height: 1.2; + white-space: nowrap; + text-decoration: none; + cursor: pointer; +} + .ai-routes-ghost { display: inline-flex; align-items: center; @@ -2177,29 +2248,6 @@ body.command-home .ai-stage-map--eu { margin-top: 4px; } -.ai-routes-more { - grid-column: 1 / -1; - display: flex; - justify-content: center; - padding: 4px 0 8px; -} - -.ai-routes-more__btn { - min-height: 36px; - padding: 0 18px; - border-radius: 999px; - border: 1px solid color-mix(in srgb, var(--ai-action) 30%, #fff); - background: color-mix(in srgb, var(--ai-action) 8%, #fff); - color: var(--ai-action); - font-size: 0.84rem; - font-weight: 700; - cursor: pointer; -} - -.ai-routes-more__btn:hover { - background: color-mix(in srgb, var(--ai-action) 14%, #fff); -} - .ai-routes-skeleton { width: min(100%, 420px); height: 72px; @@ -2257,9 +2305,12 @@ body.command-home .ai-stage-map--eu { } .ai-new-chat-btn:hover { - border-color: rgba(255, 255, 255, 0.92); - background: rgba(255, 255, 255, 0.44); + border-color: color-mix(in srgb, var(--ai-action) 24%, var(--ai-paper-edge)); + background: linear-gradient(180deg, #ffffff 0%, #fff8ef 100%); color: var(--ai-action); + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.95), + 0 6px 14px rgba(40, 32, 20, 0.08); } .ai-new-chat-btn:hover i { @@ -2301,8 +2352,11 @@ body.command-home .ai-stage-map--eu { .ai-history-item:hover, .ai-history-item.is-active { - border-color: rgba(255, 255, 255, 0.92); - background: rgba(255, 255, 255, 0.44); + border-color: color-mix(in srgb, var(--ai-action) 24%, var(--ai-paper-edge)); + background: linear-gradient(180deg, #ffffff 0%, #fff8ef 100%); + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.95), + 0 6px 14px rgba(40, 32, 20, 0.08); } .ai-history-item.is-active .ai-history-item__title { @@ -2447,11 +2501,11 @@ body.command-home #navbar .site-nav-avatar:hover { z-index: 2; display: flex; flex-direction: column; - gap: 16px; + gap: 28px; width: 100%; max-width: var(--ai-content-max); margin-inline: auto; - margin-top: -48px; + margin-top: -36px; border: none; border-radius: 0; background: transparent; @@ -2466,7 +2520,7 @@ body.command-home #navbar .site-nav-avatar:hover { z-index: 1; width: 100%; margin: 0; - padding: 20px 22px 18px; + padding: 8px 8px 4px; border: none; border-radius: 0; background: transparent; @@ -2482,7 +2536,7 @@ body.command-home #navbar .site-nav-avatar:hover { .ai-welcome-title { width: auto; max-width: 100%; - margin: 0 auto 14px; + margin: 0 auto 0; font-size: clamp(1.55rem, 2.2vw + 0.85rem, 2.65rem); font-weight: 800; letter-spacing: -0.03em; @@ -2524,159 +2578,211 @@ body.command-home #navbar .site-nav-avatar:hover { color: var(--ai-sky); } -.ai-welcome__foot { +.ai-welcome__meta { display: flex; align-items: center; - justify-content: space-between; - gap: 16px 24px; margin-top: 22px; padding-top: 18px; - border-top: none; - background-image: var(--ai-hairline); - background-repeat: no-repeat; - background-position: top center; - background-size: 100% 1px; + border-top: 1px solid var(--ai-border); } .ai-welcome__command { - padding: 0 4px 4px; + padding: 0 2px 8px; border-top: none; background: transparent; + display: flex; + flex-direction: column; + gap: 26px; +} + +/* 提问区:纯色纸面,圆角对齐 dock */ +.ai-welcome-compose-panel { + position: relative; + margin: 0; + padding: 20px 20px 18px; + border-radius: var(--ai-dock-radius); + border: 1px solid var(--ai-paper-edge); + background: var(--ai-paper); + box-shadow: var(--ai-paper-shadow); +} + +.ai-welcome-compose-panel .ai-welcome-cta-link { + position: absolute; + top: 18px; + right: 20px; + z-index: 1; +} + +.ai-welcome-compose-panel .ai-welcome-compose__lead { + padding-right: 5.5em; +} + +.ai-welcome-cta-link { + flex-shrink: 0; + padding: 0; + border: none; + background: none; + color: var(--ai-action); + font-size: 0.84rem; + font-weight: 600; + letter-spacing: 0.01em; + text-decoration: none; + white-space: nowrap; + transition: color 0.18s var(--ai-ease); +} + +.ai-welcome-cta-link:hover, +.ai-welcome-cta-link:focus { + color: var(--ai-sky-hover); + text-decoration: underline; + text-underline-offset: 3px; +} + +.ai-intent-section { + display: flex; + flex-direction: column; + gap: 20px; +} + +.ai-intent-group__label { + margin: 0 0 10px 2px; + font-size: 0.75rem; + font-weight: 700; + letter-spacing: 0.06em; + color: var(--ai-text-3); +} + +.ai-intent-grid--primary { + grid-template-columns: repeat(3, minmax(0, 1fr)); +} + +.ai-intent-grid--growth { + grid-template-columns: repeat(3, minmax(0, 1fr)); } .ai-welcome__command .ai-intent-grid { - gap: 10px 14px; - margin-top: 4px; - padding-top: 18px; - border-top: none; - background-image: var(--ai-hairline); - background-repeat: no-repeat; - background-position: top center; - background-size: 100% 1px; + gap: 12px 14px; + margin: 0; + padding: 0; + background: none; } -/* 首页意图卡片 — 半透明玻璃,折射全页明暗(无额外色块) */ +/* 首页意图卡:厚纸拟物,主路径略更厚实 */ .ai-welcome__command .ai-intent-door { overflow: hidden; - border: 1px solid var(--ai-glass-border); - border-radius: 20px; + border: 1px solid var(--ai-paper-edge); + border-radius: var(--ai-dock-radius); padding: 14px 14px; - background: var(--ai-glass-fill); - backdrop-filter: var(--ai-glass-blur); - -webkit-backdrop-filter: var(--ai-glass-blur); - box-shadow: var(--ai-glass-shadow); + background: + linear-gradient(165deg, #fffefb 0%, var(--ai-paper) 55%, #f6f0e7 100%); + backdrop-filter: none; + -webkit-backdrop-filter: none; + box-shadow: var(--ai-paper-shadow); transition: - border-color 0.22s var(--ai-ease), - background 0.22s var(--ai-ease), - box-shadow 0.22s var(--ai-ease), - transform 0.22s var(--ai-ease); + border-color 0.2s var(--ai-ease), + background 0.2s var(--ai-ease), + box-shadow 0.2s var(--ai-ease), + transform 0.2s var(--ai-ease); } .ai-welcome__command .ai-intent-door::before { display: block; - opacity: 0.55; - mix-blend-mode: soft-light; - background: - linear-gradient( - 180deg, - color-mix(in srgb, var(--ai-sky) 14%, rgba(255, 255, 255, 0.55)) 0%, - rgba(255, 255, 255, 0) 42% - ), - radial-gradient( - 92% 72% at 10% -8%, - rgba(255, 255, 255, 0.7), - transparent 58% - ); + opacity: 1; + mix-blend-mode: normal; + animation: none; + background: linear-gradient( + 180deg, + rgba(255, 255, 255, 0.65) 0%, + rgba(255, 255, 255, 0) 36% + ); + pointer-events: none; } .ai-welcome__command .ai-intent-door::after { - display: block; - opacity: 0.42; - filter: blur(16px); - background: radial-gradient( - circle at 36% 36%, - color-mix(in srgb, var(--ai-sky) 22%, rgba(255, 255, 255, 0.65)), - rgba(255, 255, 255, 0.08) 46%, - transparent 72% - ); + display: none; } .ai-welcome__command .ai-intent-door__icon { display: inline-flex; align-items: center; justify-content: center; - width: 48px; - height: 48px; + width: 40px; + height: 40px; + flex-shrink: 0; border: none; background: transparent; box-shadow: none; } +.ai-welcome__command .ai-intent-door__icon .ai-icon--illus { + width: 40px; + height: 40px; +} + .ai-welcome__command .ai-intent-door.is-primary { position: relative; - padding: 18px 16px 18px 18px; - background: var(--ai-glass-fill-strong); - border-color: color-mix(in srgb, var(--ai-sky) 22%, rgba(255, 255, 255, 0.82)); - box-shadow: - inset 0 1px 0 rgba(255, 255, 255, 0.9), - inset 0 -1px 0 color-mix(in srgb, var(--ai-sky) 14%, transparent), - 0 14px 34px rgba(2, 62, 138, 0.08), - 0 0 0 1px rgba(2, 62, 138, 0.05); + padding: 16px 15px; + border-radius: var(--ai-dock-radius); + background: + linear-gradient(165deg, #ffffff 0%, var(--ai-paper) 48%, #f3ebe0 100%); + border-color: color-mix(in srgb, var(--ai-action) 14%, var(--ai-paper-edge)); + box-shadow: var(--ai-paper-shadow-lift); } .ai-welcome__command .ai-intent-door.is-primary .ai-intent-door__icon { - width: 52px; - height: 52px; + width: 44px; + height: 44px; background: transparent; border: none; box-shadow: none; } .ai-welcome__command .ai-intent-door.is-primary .ai-icon--illus { - width: 48px; - height: 48px; -} - -.ai-welcome__command .ai-intent-door.is-primary .ai-intent-door__title { - color: var(--ai-action); + width: 42px; + height: 42px; } .ai-welcome__command .ai-intent-door:hover { - border-color: color-mix(in srgb, var(--ai-sky) 28%, rgba(255, 255, 255, 0.88)); - background: - linear-gradient( - 165deg, - color-mix(in srgb, var(--ai-sky) 14%, rgba(255, 255, 255, 0.52)) 0%, - rgba(255, 255, 255, 0.22) 50%, - color-mix(in srgb, var(--ai-action) 10%, rgba(255, 255, 255, 0.34)) 100% - ); - box-shadow: - inset 0 1px 0 rgba(255, 255, 255, 0.92), - 0 14px 32px rgba(2, 62, 138, 0.08), - 0 0 0 1px rgba(2, 62, 138, 0.05); - transform: translateY(-1px); + border-color: color-mix(in srgb, var(--ai-action) 22%, var(--ai-paper-edge)); + box-shadow: var(--ai-paper-shadow-lift); + transform: translateY(-2px); } .ai-welcome__command .ai-intent-door.is-primary:hover { - background: - linear-gradient( - 145deg, - color-mix(in srgb, var(--ai-sky) 16%, rgba(255, 255, 255, 0.6)) 0%, - color-mix(in srgb, var(--ai-action) 10%, rgba(255, 255, 255, 0.32)) 100% - ); - border-color: color-mix(in srgb, var(--ai-sky) 30%, rgba(255, 255, 255, 0.9)); + border-color: color-mix(in srgb, var(--ai-action) 28%, var(--ai-paper-edge)); box-shadow: - inset 0 1px 0 rgba(255, 255, 255, 0.94), - 0 16px 36px rgba(2, 62, 138, 0.09), - 0 0 0 1px rgba(2, 62, 138, 0.055); + inset 0 1px 0 rgba(255, 255, 255, 0.95), + 0 12px 10px -6px rgba(40, 32, 20, 0.14), + 0 26px 44px -12px rgba(2, 62, 138, 0.14); } .ai-welcome__command .ai-intent-door__title { + font-size: 0.94rem; font-weight: 700; - letter-spacing: -0.01em; + letter-spacing: -0.015em; + line-height: 1.28; + color: var(--ai-action); +} + +.ai-welcome__command .ai-intent-door.is-primary .ai-intent-door__title { + font-size: 0.98rem; + font-weight: 800; + color: var(--ai-action); } .ai-welcome__command .ai-intent-door__hint { + font-size: 0.76rem; + line-height: 1.4; + color: var(--ai-text-3); + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + line-clamp: 2; + overflow: hidden; +} + +.ai-welcome__command .ai-intent-door.is-primary .ai-intent-door__hint { + font-size: 0.78rem; color: var(--ai-text-2); } @@ -2686,19 +2792,19 @@ body.command-home #navbar .site-nav-avatar:hover { display: flex; justify-content: center; width: 100%; - margin: -8px 0 -12px; + margin: -4px 0 -18px; pointer-events: none; } .ai-welcome-plane img { position: relative; z-index: 2; - width: min(680px, 58vw); + width: min(520px, 46vw); height: auto; display: block; - filter: drop-shadow(0 16px 36px rgba(var(--ai-sky-rgb), 0.13)); + filter: drop-shadow(0 14px 30px rgba(var(--ai-sky-rgb), 0.12)); transform-origin: 50% 60%; - animation: aiPlaneIn 2.4s cubic-bezier(0.22, 1, 0.36, 1) both; + animation: aiPlaneIn 1.8s cubic-bezier(0.22, 1, 0.36, 1) both; } @keyframes aiPlaneIn { @@ -2712,45 +2818,36 @@ body.command-home #navbar .site-nav-avatar:hover { } } -.ai-welcome__actions { - display: flex; - flex-direction: column; - align-items: flex-end; - gap: 8px; - flex-shrink: 0; +@media (prefers-reduced-motion: reduce) { + .ai-welcome-plane img { + animation: none; + } } -body.command-home a.ai-welcome-cta, -body.command-home a.ai-welcome-cta:visited, -body.command-home a.ai-welcome-cta:hover, -body.command-home a.ai-welcome-cta:focus { - display: inline-flex; - align-items: center; - justify-content: center; - flex-shrink: 0; - min-height: 40px; - padding: 0 22px; +.ai-welcome__meta .ai-welcome-stats--compact { + flex: 1; + min-width: 0; margin: 0; - border-radius: 999px; - background: var(--ai-action); - color: #fff; - font-size: 0.88rem; - font-weight: 700; - letter-spacing: 0.02em; - text-decoration: none; - box-shadow: 0 8px 22px rgba(var(--ai-action-rgb), 0.22); - transition: - background 0.2s var(--ai-ease), - transform 0.15s var(--ai-ease), - box-shadow 0.2s var(--ai-ease); + padding: 0; + background: none; } -body.command-home a.ai-welcome-cta:hover, -body.command-home a.ai-welcome-cta:focus { - background: var(--ai-action-hover); - color: #fff; - transform: translateY(-1px); - box-shadow: 0 10px 26px rgba(var(--ai-action-rgb), 0.28); +.ai-welcome-stats--compact .ai-welcome-stats__item { + padding: 0 10px; + gap: 3px; +} + +.ai-welcome-stats--compact .ai-welcome-stats__value { + font-size: clamp(0.92rem, 0.45vw + 0.75rem, 1.12rem); +} + +.ai-welcome-stats--compact .ai-welcome-stats__label { + font-size: 0.64rem; +} + +.ai-welcome-stats--compact .ai-welcome-stats__item + .ai-welcome-stats__item::before { + height: 26px; + background: color-mix(in srgb, var(--ai-action) 16%, var(--ai-border)); } .ai-welcome-human { @@ -2911,9 +3008,10 @@ body.command-home a.ai-welcome-cta:focus { left: 0; top: 50%; width: 1px; - height: 26px; + height: 28px; transform: translateY(-50%); - background: color-mix(in srgb, var(--ai-border) 85%, transparent); + /* 与统计数字海军蓝同族,略抬对比 */ + background: color-mix(in srgb, var(--ai-action) 22%, var(--ai-border)); } .ai-welcome-stats__value { @@ -3340,7 +3438,7 @@ body.command-home a.ai-welcome-cta:focus { .ai-msg-bubble { max-width: min(680px, 90%); padding: 12px 16px; - border-radius: 16px; + border-radius: var(--ai-dock-radius-inner); font-size: 0.94rem; line-height: 1.6; } @@ -3402,18 +3500,18 @@ body.command-home a.ai-welcome-cta:focus { 40% { opacity: 1; transform: scale(1); } } -/* Cards inside assistant replies — 极简磨砂玻璃 */ +/* Cards inside assistant replies — 厚纸面 */ .ai-card { position: relative; isolation: isolate; overflow: hidden; padding: 18px 18px 16px; - border-radius: 20px; - border: 1px solid rgba(255, 255, 255, 0.72); - background: var(--ai-acrylic-bg); - backdrop-filter: blur(28px) saturate(160%); - -webkit-backdrop-filter: blur(28px) saturate(160%); - box-shadow: none; + border-radius: var(--ai-dock-radius-inner); + border: 1px solid var(--ai-paper-edge); + background: var(--ai-paper); + backdrop-filter: none; + -webkit-backdrop-filter: none; + box-shadow: var(--ai-paper-shadow); } .ai-card::before, @@ -3471,8 +3569,8 @@ body.command-home a.ai-welcome-cta:focus { padding: 10px 12px; border-radius: var(--ai-radius-sm); text-align: left; - border: 1px solid rgba(255, 255, 255, 0.55); - background: transparent; + border: 1px solid var(--ai-paper-edge); + background: color-mix(in srgb, var(--ai-bg-soft) 70%, #fff); box-shadow: none; } @@ -3603,9 +3701,12 @@ body.command-home a.ai-welcome-cta:focus { } .ai-followup-chip:hover { - border-color: rgba(255, 255, 255, 0.92); - background: rgba(255, 255, 255, 0.44); + border-color: color-mix(in srgb, var(--ai-action) 24%, var(--ai-paper-edge)); + background: linear-gradient(180deg, #ffffff 0%, #fff8ef 100%); color: var(--ai-action); + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.95), + 0 6px 14px rgba(40, 32, 20, 0.08); } .ai-followup-chip:disabled { @@ -3617,6 +3718,7 @@ body.command-home a.ai-welcome-cta:focus { color: #fff; border-color: var(--ai-action); background: var(--ai-action); + box-shadow: 0 6px 14px color-mix(in srgb, var(--ai-action) 22%, transparent); } .ai-followup-chip--routes:hover { @@ -3625,14 +3727,9 @@ body.command-home a.ai-welcome-cta:focus { background: var(--ai-action); } -.ai-welcome-compose { - margin: 0; - padding: 2px 2px 4px; -} - .ai-welcome-compose__lead { - margin: 0 0 10px; - padding: 0 4px; + margin: 0 0 12px; + padding: 0; font-size: 0.9rem; font-weight: 600; letter-spacing: 0; @@ -3645,37 +3742,44 @@ body.command-home a.ai-welcome-cta:focus { display: flex; flex-wrap: wrap; gap: 8px; - margin: 0 0 8px; - padding: 0 2px; + margin: 0 0 14px; + padding: 0; } .ai-home-dest { min-height: 34px; - padding: 0 14px; - border-radius: 999px; - border: 1px solid var(--ai-glass-border); - background: color-mix(in srgb, var(--ai-sky) 8%, rgba(255, 255, 255, 0.38)); + padding: 0 16px; + border-radius: var(--ai-chip-radius, 999px); + border: 1px solid var(--ai-paper-edge, var(--ai-border)); + background: linear-gradient(180deg, #fffefb 0%, var(--ai-bg-soft) 100%); box-shadow: - inset 0 1px 0 rgba(255, 255, 255, 0.7), - 0 4px 12px rgba(2, 62, 138, 0.04); + inset 0 1px 0 rgba(255, 255, 255, 0.9), + 0 3px 8px rgba(40, 32, 20, 0.06); color: var(--ai-text-2); font-size: 0.86rem; - font-weight: 650; + font-weight: 600; cursor: pointer; transition: color 0.18s var(--ai-ease), border-color 0.18s var(--ai-ease), background 0.18s var(--ai-ease), - box-shadow 0.18s var(--ai-ease); + box-shadow 0.18s var(--ai-ease), + transform 0.15s var(--ai-ease); } .ai-home-dest:hover { color: var(--ai-action); - border-color: color-mix(in srgb, var(--ai-sky) 32%, rgba(255, 255, 255, 0.78)); - background: color-mix(in srgb, var(--ai-sky) 12%, rgba(255, 255, 255, 0.55)); + border-color: color-mix(in srgb, var(--ai-action) 24%, var(--ai-paper-edge, var(--ai-border))); + background: linear-gradient(180deg, #ffffff 0%, #fff8ef 100%); box-shadow: - inset 0 1px 0 rgba(255, 255, 255, 0.85), - 0 6px 16px rgba(2, 62, 138, 0.07); + inset 0 1px 0 rgba(255, 255, 255, 0.95), + 0 6px 14px rgba(40, 32, 20, 0.08); + transform: translateY(-1px); +} + +.ai-home-dest:active { + transform: translateY(0); + box-shadow: inset 0 1px 2px rgba(90, 70, 40, 0.08); } .ai-home-price-hint { @@ -3686,24 +3790,56 @@ body.command-home a.ai-welcome-cta:focus { color: var(--ai-text-3); } -.ai-composer-wrap.is-welcome .ai-composer { - min-height: 72px; - padding: 12px 14px 12px 18px; - border-radius: 16px; - border-color: var(--ai-glass-border); - background: var(--ai-glass-fill); - backdrop-filter: var(--ai-glass-blur); - -webkit-backdrop-filter: var(--ai-glass-blur); - box-shadow: var(--ai-glass-shadow); +/* 首页提问槽:浅灰内凹(挂在真实存在的 slot 上) */ +.ai-composer-slot--welcome .ai-composer { + min-height: 56px; + padding: 12px 12px 12px 18px; + border-radius: 26px; + border: none !important; + outline: none; + background: #e8ecf1 !important; + backdrop-filter: none !important; + -webkit-backdrop-filter: none !important; + box-shadow: inset 0 3px 10px rgba(15, 23, 42, 0.14) !important; + transition: + background 0.18s var(--ai-ease), + box-shadow 0.18s var(--ai-ease); } -.ai-composer-wrap.is-welcome .ai-composer::before { - opacity: 0.55; - background: linear-gradient( - 180deg, - color-mix(in srgb, var(--ai-sky) 12%, rgba(255, 255, 255, 0.5)) 0%, - rgba(255, 255, 255, 0) 40% - ); +.ai-composer-slot--welcome .ai-composer::before, +.ai-composer-slot--welcome .ai-composer::after { + display: none !important; +} + +.ai-composer-slot--welcome .ai-composer:focus-within { + border: none !important; + outline: none; + background: #dde3ea !important; + box-shadow: inset 0 4px 12px rgba(15, 23, 42, 0.16) !important; +} + +.ai-composer-slot--welcome .ai-composer-input { + color: var(--ai-text); +} + +.ai-composer-slot--welcome .ai-composer-input::placeholder { + color: var(--ai-text-3); +} + +.ai-composer-slot--welcome .ai-composer-send { + width: 40px; + height: 40px; + font-size: 1.1rem; + box-shadow: none; +} + +.ai-composer-slot--welcome .ai-composer-send i { + font-size: 1rem; +} + +.ai-composer-slot--welcome .ai-composer-send.is-empty { + background: #cfd6de; + color: #7a8490; } .ai-composer-dock { @@ -3806,7 +3942,7 @@ body.command-home a.ai-welcome-cta:focus { } .ai-composer-wrap.is-docked .ai-composer { - box-shadow: var(--ai-glass-shadow); + box-shadow: var(--ai-paper-shadow); } .ai-composer__caustic, @@ -4094,36 +4230,59 @@ body.command-home a.ai-welcome-cta:focus { .ai-welcome__command .ai-intent-door.is-primary { padding: 16px 14px; - border-radius: 20px; + border-radius: var(--ai-dock-radius, 28px); } .ai-welcome__command .ai-intent-door { - border-radius: 20px; + border-radius: var(--ai-dock-radius, 28px); } .ai-welcome-plane img { - width: min(520px, 78vw); + width: min(420px, 52vw); + } + + .ai-intent-grid--primary, + .ai-intent-grid--growth { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .ai-intent-grid--primary .ai-intent-door:last-child { + grid-column: 1 / -1; } } @media (max-width: 780px) { - .ai-welcome__foot { - flex-direction: column; - align-items: stretch; - gap: 12px; - } - - .ai-welcome-stats { + .ai-welcome__meta { justify-content: center; } - .ai-welcome__actions { - align-items: center; - width: 100%; + .ai-welcome-stats--compact { + justify-content: center; } - .ai-welcome-cta { - width: 100%; + .ai-welcome-compose-panel .ai-welcome-compose__lead { + padding-right: 0; + padding-top: 4px; + } + + .ai-welcome-compose-panel .ai-welcome-cta-link { + top: 14px; + right: 16px; + font-size: 0.8rem; + } + + .ai-intent-grid--growth { + display: flex; + gap: 10px; + overflow-x: auto; + padding-bottom: 4px; + scroll-snap-type: x proximity; + -webkit-overflow-scrolling: touch; + } + + .ai-intent-grid--growth .ai-intent-door { + flex: 0 0 min(78vw, 260px); + scroll-snap-align: start; } } @@ -4177,17 +4336,24 @@ body.command-home a.ai-welcome-cta:focus { height: min(68%, 160px); } - .ai-welcome__foot { - flex-direction: column; - align-items: stretch; - } - - .ai-welcome-stats-block { - width: 100%; + .ai-welcome-plane { + display: none; } .ai-welcome__stack { - margin-top: -20px; + margin-top: 4px; + } + + .ai-welcome-stats--compact .ai-welcome-stats__item:nth-child(n + 3) { + display: none; + } + + .ai-intent-grid--primary { + grid-template-columns: 1fr; + } + + .ai-intent-grid--primary .ai-intent-door:last-child { + grid-column: auto; } .ai-partner-marquee { @@ -4397,7 +4563,7 @@ body.command-home .ai-welcome-plane img { } body.command-home .ai-history-sidebar { - background: #fff; + background: color-mix(in srgb, var(--ai-paper) 72%, transparent); } body.command-home .ai-history-backdrop { -- 2.45.2 From fbf1064ab87cb1e65bdba74a461bc27f83280e91 Mon Sep 17 00:00:00 2001 From: zzh <1368735060@qq.com> Date: Sat, 24 Oct 2026 17:03:18 +0800 Subject: [PATCH 5/6] =?UTF-8?q?style:=20=E7=BB=9F=E4=B8=80=E5=8E=8B?= =?UTF-8?q?=E6=84=9F=E5=8D=A1=E7=89=87=E9=98=B4=E5=BD=B1=E5=B9=B6=E7=B2=BE?= =?UTF-8?q?=E7=AE=80=E5=85=85=E5=80=BC/=E7=9B=91=E6=8E=A7=E9=A1=B5?= =?UTF-8?q?=E4=BA=A4=E4=BA=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 对齐登录、工作台、H5、官网与充值页的轻压 inset;监控改胶囊 Tab 减闪;充值页去步骤条并收窄布局。 Co-authored-by: Cursor --- src/components/SlidingTabs/index.vue | 111 ++- src/locale/en-US.ts | 16 +- src/locale/zh-CN.ts | 16 +- src/views/h5/admin-center/login/index.vue | 37 +- src/views/h5/admin-center/monitor/index.vue | 19 +- src/views/h5/admin-center/styles/tokens.less | 12 +- src/views/h5/admin-center/workplace/index.vue | 21 +- .../h5/customer-center/assistant/index.vue | 22 +- src/views/h5/customer-center/login/index.vue | 41 +- src/views/h5/customer-center/price/index.vue | 12 +- src/views/home/monitor/index.vue | 466 ++++----- .../workplace/components/AdminOpsQueue.vue | 38 +- .../workplace/components/AdminStatsPanel.vue | 15 + .../workplace/components/AdminWorkplace.vue | 115 ++- .../components/CustomerWorkplace.vue | 82 +- .../home/workplace/components/banner.vue | 31 +- .../home/workplace/components/data-panel.vue | 13 +- .../components/marketing-shortcuts.vue | 18 +- .../workplace/components/quick-operation.vue | 69 +- .../login/components/LoginCareBanner.vue | 160 +--- .../login/components/pwd-login/index.vue | 194 +--- src/views/login/index.vue | 50 +- src/views/site/GradientWaves.vue | 14 +- src/views/site/SiteAdvisorShell.vue | 32 +- src/views/site/SitePartnerMarquee.vue | 3 +- src/views/site/styles/ai-advisor.css | 538 ++++++----- src/views/site/styles/main.css | 2 +- src/views/user/recharge/deposit.vue | 895 +++++++++++------- src/views/warehouse/inboundNotice/index.vue | 201 +++- 29 files changed, 1931 insertions(+), 1312 deletions(-) diff --git a/src/components/SlidingTabs/index.vue b/src/components/SlidingTabs/index.vue index a2b80df..02100da 100644 --- a/src/components/SlidingTabs/index.vue +++ b/src/components/SlidingTabs/index.vue @@ -2,9 +2,14 @@