20261008-ide1.0发布 #1
@ -20,6 +20,7 @@ alwaysApply: true
|
||||
|
||||
### 组件
|
||||
- 按钮 32px 高,border-radius 4px,间距 8px
|
||||
- **禁止** `a-button type="outline"`(Arco 默认描边与主题不搭);主操作用 `primary` / `app-btn-primary-soft`,查询用 `app-btn-search`,次要用默认按钮或抽屉内软按钮
|
||||
- 优先 FormPro、TablePro、ModalPro、CustomList、HandsontablePro
|
||||
|
||||
### 表单
|
||||
|
||||
@ -1,11 +1,13 @@
|
||||
# =============================================================================
|
||||
# 本地开发默认 → 生产环境(Vite 代理到 api.ideship.com,无浏览器 CORS)
|
||||
# 切测试:pnpm 用 .env.nanafa 构建,或临时改回 api.nanafa.com
|
||||
# 本地开发 → ideship 测试环境(禁止本地直连生产)
|
||||
# Vite 代理:https://api.test.ideship.com/{admin|itfx|track}
|
||||
# 站点对照:https://test.ideship.com
|
||||
# =============================================================================
|
||||
VITE_DEPLOY_TARGET=ideship
|
||||
VITE_OMS_ADMIN_URL=https://api.ideship.com/admin
|
||||
VITE_API_ORIGIN=https://api.ideship.com
|
||||
|
||||
VITE_OMS_ADMIN_URL=https://api.test.ideship.com/admin
|
||||
VITE_API_ORIGIN=https://api.test.ideship.com
|
||||
VITE_API_ADMIN_URL=https://api.test.ideship.com/admin
|
||||
VITE_API_ASSET_ORIGIN=https://api.test.ideship.com/admin
|
||||
VITE_WECOM_KF_URL=https://work.weixin.qq.com/kfid/kfcae09c206f6c7045a
|
||||
|
||||
VITE_ZHIPU_API_KEY=85bbbef6bafd4ca383eef04189decd07.lKVQHcLGtYE8oUn5
|
||||
|
||||
@ -1,9 +0,0 @@
|
||||
# =============================================================================
|
||||
# 本地开发直连生产 API(ideship)— pnpm dev:ideship
|
||||
# 浏览器仍请求 localhost,由 Vite 代理到 api.ideship.com(避免 CORS)
|
||||
# =============================================================================
|
||||
VITE_DEPLOY_TARGET=ideship
|
||||
VITE_OMS_ADMIN_URL=https://api.ideship.com/admin
|
||||
VITE_API_ORIGIN=https://api.ideship.com
|
||||
|
||||
VITE_WECOM_KF_URL=https://work.weixin.qq.com/kfid/kfcae09c206f6c7045a
|
||||
12
.env.ideship-test
Normal file
@ -0,0 +1,12 @@
|
||||
# =============================================================================
|
||||
# ideship 测试环境 — pnpm dev:test / pnpm build:test
|
||||
# API:https://api.test.ideship.com/{admin|itfx|track}
|
||||
# 站点:https://test.ideship.com
|
||||
# =============================================================================
|
||||
VITE_DEPLOY_TARGET=ideship
|
||||
VITE_API_ORIGIN=https://api.test.ideship.com
|
||||
VITE_API_ADMIN_URL=https://api.test.ideship.com/admin
|
||||
VITE_API_ASSET_ORIGIN=https://api.test.ideship.com/admin
|
||||
VITE_OMS_ADMIN_URL=https://api.test.ideship.com/admin
|
||||
|
||||
VITE_WECOM_KF_URL=https://work.weixin.qq.com/kfid/kfcae09c206f6c7045a
|
||||
@ -1,7 +1,8 @@
|
||||
# =============================================================================
|
||||
# 生产环境(ideship.com)— vite build / vite build --mode production
|
||||
# 生产构建专用(pnpm build:prod)— 请勿用于本地开发直连
|
||||
# 三套 API:https://api.ideship.com/{admin|itfx|track}
|
||||
# 前端站点:https://ideship.com
|
||||
# 本地开发请用 .env.development → api.test.ideship.com
|
||||
# =============================================================================
|
||||
VITE_DEPLOY_TARGET=ideship
|
||||
VITE_API_ORIGIN=https://api.ideship.com
|
||||
@ -9,5 +10,4 @@ VITE_API_ADMIN_URL=https://api.ideship.com/admin
|
||||
VITE_API_ASSET_ORIGIN=https://api.ideship.com/admin
|
||||
VITE_OMS_ADMIN_URL=https://api.ideship.com/admin
|
||||
|
||||
# 企业微信客服
|
||||
VITE_WECOM_KF_URL=https://work.weixin.qq.com/kfid/kfcae09c206f6c7045a
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
# 复制到 itfx-front 仓库 .gitea/workflows/deploy.yml
|
||||
# 前置: kaniko-git + kaniko-docker-config;act_runner(k3s-host) 只负责 kubectl
|
||||
# 构建在集群 Kaniko Job(role=build),不跑宿主机 Docker
|
||||
# push master → prod;push test → test
|
||||
# 构建始终用 prod kubeconfig(Kaniko);仅 deploy 切集群
|
||||
# 前端镜像:test 分支须传 FRONT_BUILD_SCRIPT=build:test,否则会打进正式 API 域名
|
||||
name: Build and Deploy Front
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master, main]
|
||||
workflow_dispatch:
|
||||
branches: [master, test]
|
||||
|
||||
env:
|
||||
IMAGE_NAME: itfx-front
|
||||
@ -20,13 +20,51 @@ jobs:
|
||||
- name: Build and deploy
|
||||
run: |
|
||||
set -euxo pipefail
|
||||
export KUBECONFIG=/etc/rancher/k3s/k3s.yaml
|
||||
TAG="${{ gitea.sha }}"
|
||||
IMG="${{ env.TCR_REGISTRY }}/${{ env.TCR_NAMESPACE }}/${{ env.IMAGE_NAME }}:${TAG}"
|
||||
REF="${{ gitea.ref_name }}"
|
||||
case "$REF" in
|
||||
test) TARGET=test ;;
|
||||
master) TARGET=prod ;;
|
||||
*) echo "unsupported branch: $REF"; exit 1 ;;
|
||||
esac
|
||||
|
||||
SHA="${{ gitea.sha }}"
|
||||
REPO="${{ gitea.repository }}"
|
||||
REPO="${REPO:-${{ github.repository }}}"
|
||||
test -n "$REPO" && [[ "$REPO" == */* ]]
|
||||
echo "REPO=$REPO TAG=$TAG host=$(hostname)"
|
||||
|
||||
case "$TARGET" in
|
||||
test) TAG="test-${SHA}" ;;
|
||||
*) TAG="${SHA}" ;;
|
||||
esac
|
||||
|
||||
IMG="${{ env.TCR_REGISTRY }}/${{ env.TCR_NAMESPACE }}/${{ env.IMAGE_NAME }}:${TAG}"
|
||||
echo "TARGET=$TARGET REF=$REF REPO=$REPO TAG=$TAG host=$(hostname)"
|
||||
|
||||
case "$TARGET" in
|
||||
test) export FRONT_BUILD_SCRIPT=build:test ;;
|
||||
*) export FRONT_BUILD_SCRIPT=build ;;
|
||||
esac
|
||||
echo "FRONT_BUILD_SCRIPT=$FRONT_BUILD_SCRIPT"
|
||||
|
||||
export KUBECONFIG=/etc/rancher/k3s/k3s.yaml
|
||||
unset NAMESPACE
|
||||
cd /data/k3s
|
||||
./scripts/ci-publish-front.sh "$REPO" "${{ env.IMAGE_NAME }}" "$TAG" "$TAG"
|
||||
./scripts/ci-publish-front.sh "$REPO" "${{ env.IMAGE_NAME }}" "$TAG" "$SHA"
|
||||
|
||||
case "$TARGET" in
|
||||
test)
|
||||
export KUBECONFIG=/etc/rancher/k3s/k3s-test.yaml
|
||||
export NAMESPACE=test
|
||||
export MANIFEST_ROOT=/data/k3s-test
|
||||
;;
|
||||
*)
|
||||
export KUBECONFIG=/etc/rancher/k3s/k3s.yaml
|
||||
export NAMESPACE=prod
|
||||
export MANIFEST_ROOT=/data/k3s
|
||||
;;
|
||||
esac
|
||||
if [[ ! -f "$KUBECONFIG" ]]; then
|
||||
echo "FAIL: missing KUBECONFIG=$KUBECONFIG host=$(hostname) uid=$(id -u)"
|
||||
ls -la /etc/rancher/k3s/ /root/.kube/ 2>/dev/null || true
|
||||
exit 1
|
||||
fi
|
||||
./scripts/ci-deploy-front.sh "$IMG"
|
||||
|
||||
@ -1,7 +1,12 @@
|
||||
# k3s / Kaniko:静态 SPA(方案 A)。API 直连 api.ideship.com,镜像内 nginx 不做反代
|
||||
# 基础镜像走 DaoCloud,避免 Kaniko 直连 docker.io 超时
|
||||
# k3s / Kaniko:静态 SPA(方案 A)。API 由浏览器按站点域名直连
|
||||
# test.ideship.com → api.test.ideship.com;其余 → api.ideship.com
|
||||
# FRONT_BUILD_SCRIPT:
|
||||
# prod 默认 build(.env.production → api.ideship.com)
|
||||
# test 传 build:test(.env.ideship-test → api.test.ideship.com)
|
||||
# 即使误用 production 构建,运行时也会按 hostname 纠正到测试 API。
|
||||
FROM docker.m.daocloud.io/library/node:20-alpine AS build
|
||||
WORKDIR /app
|
||||
ARG FRONT_BUILD_SCRIPT=build
|
||||
RUN npm config set registry https://registry.npmmirror.com \
|
||||
&& npm install -g pnpm@9.15.9
|
||||
COPY package.json pnpm-lock.yaml ./
|
||||
@ -9,7 +14,7 @@ COPY package.json pnpm-lock.yaml ./
|
||||
RUN pnpm config set registry https://registry.npmmirror.com \
|
||||
&& pnpm install --frozen-lockfile --ignore-scripts
|
||||
COPY . .
|
||||
RUN pnpm run build
|
||||
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/
|
||||
|
||||
@ -1 +0,0 @@
|
||||
|
||||
@ -1 +0,0 @@
|
||||
open_basedir=/www/wwwroot/ide.com/:/tmp/
|
||||
@ -1 +0,0 @@
|
||||
8lcz4Vt-jvor1dPfnDFrwmwxckn-uTCvfI5MydyGRSY.rzK5Pv1nT7agf6I1GalcCEnwH8jhYjKMla4EwvPHZU4
|
||||
@ -1,14 +0,0 @@
|
||||
<html>
|
||||
<head><title>404 Not Found</title></head>
|
||||
<body>
|
||||
<center><h1>404 Not Found</h1></center>
|
||||
<hr><center>nginx</center>
|
||||
<script src="assets/js/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 686 KiB |
@ -1,885 +0,0 @@
|
||||
/**
|
||||
* Template Name: Logis - v1.0.1
|
||||
* Template URL: https://bootstrapmade.com/logis-bootstrap-logistics-website-template/
|
||||
* Author: BootstrapMade.com
|
||||
* License: https://bootstrapmade.com/license/
|
||||
*/
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
"use strict";
|
||||
|
||||
const pageBackgroundStorageKey = 'app-page-background-color';
|
||||
const themeColorStorageKey = 'app-theme-color';
|
||||
const DEFAULT_THEME_COLOR = '#054f74';
|
||||
|
||||
function hexToRgb(hex) {
|
||||
const normalized = hex.replace(/^#/, '');
|
||||
if (!/^[a-f\d]{6}$/i.test(normalized)) return null;
|
||||
return {
|
||||
r: parseInt(normalized.slice(0, 2), 16),
|
||||
g: parseInt(normalized.slice(2, 4), 16),
|
||||
b: parseInt(normalized.slice(4, 6), 16),
|
||||
};
|
||||
}
|
||||
|
||||
function darkenHex(hex, ratio) {
|
||||
const rgb = hexToRgb(hex);
|
||||
if (!rgb) return hex;
|
||||
const toHex = (value) => Math.round(value).toString(16).padStart(2, '0');
|
||||
return (
|
||||
'#' +
|
||||
toHex(rgb.r * (1 - ratio)) +
|
||||
toHex(rgb.g * (1 - ratio)) +
|
||||
toHex(rgb.b * (1 - ratio))
|
||||
);
|
||||
}
|
||||
|
||||
function lightenHex(hex, mixWhite) {
|
||||
const rgb = hexToRgb(hex);
|
||||
if (!rgb) return hex;
|
||||
const mix = typeof mixWhite === 'number' ? mixWhite : 0.92;
|
||||
const toHex = (value) => Math.round(value).toString(16).padStart(2, '0');
|
||||
return (
|
||||
'#' +
|
||||
toHex(rgb.r * (1 - mix) + 255 * mix) +
|
||||
toHex(rgb.g * (1 - mix) + 255 * mix) +
|
||||
toHex(rgb.b * (1 - mix) + 255 * mix)
|
||||
);
|
||||
}
|
||||
|
||||
function applyStoredThemeColor() {
|
||||
let color = '';
|
||||
try {
|
||||
color = localStorage.getItem(themeColorStorageKey) || '';
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
if (!color) color = DEFAULT_THEME_COLOR;
|
||||
if (!color.startsWith('#')) color = `#${color}`;
|
||||
|
||||
const root = document.documentElement;
|
||||
const hover = darkenHex(color, 0.05);
|
||||
const soft = lightenHex(color, 0.9);
|
||||
const rgb = hexToRgb(color);
|
||||
const brandTokens = ['--kc-brand', '--kc-primary', '--kc-tertiary', '--color-primary', '--ai-action'];
|
||||
brandTokens.forEach((token) => root.style.setProperty(token, color));
|
||||
root.style.setProperty('--kc-brand-hover', hover);
|
||||
root.style.setProperty('--ai-action-hover', hover);
|
||||
root.style.setProperty('--kc-brand-soft', soft);
|
||||
root.style.setProperty('--ai-primary-soft', soft);
|
||||
if (rgb) {
|
||||
const rgbStr = `${rgb.r}, ${rgb.g}, ${rgb.b}`;
|
||||
['--kc-brand-rgb', '--kc-primary-rgb', '--kc-tertiary-rgb', '--ai-action-rgb', '--ai-navy-rgb'].forEach(
|
||||
(token) => root.style.setProperty(token, rgbStr),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function softenHex(hex, mixWhite) {
|
||||
const normalized = hex.replace(/^#/, '');
|
||||
if (!/^[a-f\d]{6}$/i.test(normalized)) return hex;
|
||||
const r = parseInt(normalized.slice(0, 2), 16);
|
||||
const g = parseInt(normalized.slice(2, 4), 16);
|
||||
const b = parseInt(normalized.slice(4, 6), 16);
|
||||
const mix = typeof mixWhite === 'number' ? mixWhite : 0.55;
|
||||
const toHex = (value) => Math.round(value).toString(16).padStart(2, '0');
|
||||
return (
|
||||
'#' +
|
||||
toHex(r * (1 - mix) + 255 * mix) +
|
||||
toHex(g * (1 - mix) + 255 * mix) +
|
||||
toHex(b * (1 - mix) + 255 * mix)
|
||||
);
|
||||
}
|
||||
|
||||
function applyStoredPageBackground() {
|
||||
let color = '';
|
||||
try {
|
||||
color = localStorage.getItem(pageBackgroundStorageKey) || '';
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
if (!color) return;
|
||||
if (!color.startsWith('#')) color = `#${color}`;
|
||||
const root = document.documentElement;
|
||||
const warmPageBgs = new Set(['#f6f1e7', '#f5f4f1', '#faf9f7']);
|
||||
const soft = softenHex(color, warmPageBgs.has(color.toLowerCase()) ? 0.42 : 0.55);
|
||||
root.style.setProperty('--ai-bg', color);
|
||||
root.style.setProperty('--ai-canvas', color);
|
||||
root.style.setProperty('--ai-bg-soft', soft);
|
||||
root.style.setProperty('--ai-header-bg', `color-mix(in srgb, ${color} 94%, transparent)`);
|
||||
root.style.setProperty('--kc-page-bg', color);
|
||||
root.style.setProperty('--kc-neutral', soft);
|
||||
}
|
||||
|
||||
applyStoredThemeColor();
|
||||
applyStoredPageBackground();
|
||||
|
||||
const languageStorageKey = 'ide-language';
|
||||
const pageTitleMap = {
|
||||
'IDE - Cross-border Logistics, Delivered Globally': 'IDE - 跨境物流,全球即递',
|
||||
'IDE | Global Digital One-Stop Cross-Border Logistics': 'IDE - 跨境物流,全球即递',
|
||||
'IDE|Immediate Delivery Express — Cross-Border Logistics': 'IDE - 跨境物流,全球即递',
|
||||
'Faster & Simpler': '更快 · 更简单',
|
||||
'|Faster & Simpler': '更快 · 更简单',
|
||||
'IDE - About': 'IDE - 关于我们',
|
||||
'IDE - Services': 'IDE - 服务',
|
||||
'IDE - Service Details': 'IDE - 服务详情',
|
||||
'IDE - Pricing': 'IDE - 价格',
|
||||
'IDE - Contact': 'IDE - 联系我们',
|
||||
'IDE - Get a Quote': 'IDE - 获取报价',
|
||||
'IDE - Sample Inner Page': 'IDE - 示例内页',
|
||||
'IDE - Company Profile': 'IDE - 公司介绍',
|
||||
'404 Not Found': '404 页面未找到'
|
||||
};
|
||||
|
||||
const i18nTextNodes = new WeakMap();
|
||||
|
||||
const textMap = {
|
||||
'Home': '首页',
|
||||
'Routes': '线路',
|
||||
'About': '关于我们',
|
||||
'About us': '关于我们',
|
||||
'About Us': '关于我们',
|
||||
'Services': '服务',
|
||||
'Our Services': '我们的服务',
|
||||
'Pricing': '价格',
|
||||
'Contact': '联系我们',
|
||||
'Contact CN': '联系我们',
|
||||
'Login': '登录',
|
||||
'Ship now': '去发货',
|
||||
'Enter': '进入系统',
|
||||
'Sign In / Register': '注册/登录',
|
||||
'Learn More': '了解更多',
|
||||
'Search': '搜索',
|
||||
'Track': '查询物流',
|
||||
'Enter tracking number': '请输入跟踪单号',
|
||||
'Your Name': '你的姓名',
|
||||
'Your Email': '你的邮箱',
|
||||
'Subject': '主题',
|
||||
'Message': '留言内容',
|
||||
'Name': '姓名',
|
||||
'Email': '邮箱',
|
||||
'Phone': '电话',
|
||||
'City of Departure': '始发城市',
|
||||
'Delivery City': '目的城市',
|
||||
'Total Weight (kg)': '总重量 (kg)',
|
||||
'Dimensions (cm)': '尺寸 (cm)',
|
||||
'ZIP code or CitY': '邮编或城市',
|
||||
'Call To Action': '立即行动',
|
||||
'Frequently Asked Questions': '常见问题',
|
||||
'Useful Links': '常用链接',
|
||||
'Terms of service': '服务条款',
|
||||
'Privacy policy': '隐私政策',
|
||||
'Service Details': '服务详情',
|
||||
'Sample Inner Page': '示例内页',
|
||||
'Company Profile': '公司介绍',
|
||||
'[Template] Sample Inner Page': '[模板] 示例内页',
|
||||
'Our Team': '我们的团队',
|
||||
'Horizontal Pricing': '横向价格',
|
||||
'Your Personal Details': '您的个人信息',
|
||||
'Send Message': '发送消息',
|
||||
'Your quote request has been sent successfully. Thank you!': '您的询价请求已成功发送,感谢您!',
|
||||
'Your message has been sent. Thank you!': '您的消息已发送,感谢您!',
|
||||
'Loading': '加载中',
|
||||
'Location:': '地址:',
|
||||
'Phone:': '电话:',
|
||||
'Call:': '电话:',
|
||||
'Email:': '邮箱:',
|
||||
'Address:': '地址:',
|
||||
'Get a Quote': '获取报价',
|
||||
'Get a quote': '获取报价',
|
||||
'Copyright © 2020.SZJYH All rights reserved.': '版权 © 2020.SZJYH 保留所有权利。',
|
||||
'About Us': '关于我们',
|
||||
'Dolor iure expedita id fuga asperiores qui sunt consequatur minima. Quidem voluptas deleniti. Sit quia molestiae quia quas qui magnam itaque veritatis dolores. Corrupti totam ut eius incidunt reiciendis veritatis asperiores placeat.': '我们坚持高效、可靠、透明的服务理念,为客户提供稳定的跨境物流解决方案。',
|
||||
'Ullamco laboris nisi ut aliquip consequat': '高效履约与稳定交付',
|
||||
'Magni facilis facilis repellendus cum excepturi quaerat praesentium libre trade': '覆盖多场景物流需求,保障运输过程可追踪、可管理。',
|
||||
'Magnam soluta odio exercitationem reprehenderi': '专业运营团队支持',
|
||||
'Quo totam dolorum at pariatur aut distinctio dolorum laudantium illo direna pasata redi': '从下单到签收全流程跟进,及时响应客户问题。',
|
||||
'Voluptatem et qui exercitationem': '持续优化时效与成本',
|
||||
'Et velit et eos maiores est tempora et quos dolorem autem tempora incidunt maxime veniam': '通过线路与仓配优化,帮助客户获得更优履约体验。',
|
||||
'Our Services': '我们的服务',
|
||||
'Storage': '仓储',
|
||||
'Logistics': '物流',
|
||||
'Cargo': '货运',
|
||||
'Trucking': '卡车运输',
|
||||
'Packaging': '包装',
|
||||
'Warehousing': '仓储',
|
||||
'Cumque eos in qui numquam. Aut aspernatur perferendis sed atque quia voluptas quisquam repellendus temporibus itaqueofficiis odit': '提供灵活仓储与库存管理方案,支持多渠道订单履约。',
|
||||
'Asperiores provident dolor accusamus pariatur dolore nam id audantium ut et iure incidunt molestiae dolor ipsam ducimus occaecati nisi': '整合头程、干线与末端配送,打造一体化物流服务。',
|
||||
'Dicta quam similique quia architecto eos nisi aut ratione aut ipsum reiciendis sit doloremque oluptatem aut et molestiae ut et nihil': '针对不同货型与业务场景,定制高性价比运输方案。',
|
||||
'Illo consequuntur quisquam delectus praesentium modi dignissimos facere vel cum onsequuntur maiores beatae consequatur magni voluptates': '提供标准化与定制化包装服务,降低运输破损风险。',
|
||||
'Quas assumenda non occaecati molestiae. In aut earum sed natus eatae in vero. Ab modi quisquam aut nostrum unde et qui est non quo nulla': '多仓协同与库存可视化,提升发货效率与周转能力。',
|
||||
'Call To Action': '立即行动',
|
||||
'Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.': '立即联系我们获取专属物流方案,我们将根据您的业务规模与线路需求快速报价并提供落地建议。',
|
||||
'Frequently Asked Questions': '常见问题',
|
||||
'Non consectetur a erat nam at lectus urna duis?': '你们支持哪些物流线路?',
|
||||
'Feugiat pretium nibh ipsum consequat. Tempus iaculis urna id volutpat lacus laoreet non curabitur gravida. Venenatis lectus magna fringilla urna porttitor rhoncus dolor purus non.': '我们支持多国跨境线路,可根据目的地、时效和成本为您推荐合适渠道。',
|
||||
'Feugiat scelerisque varius morbi enim nunc faucibus a pellentesque?': '是否支持订单追踪?',
|
||||
'Dolor sit amet consectetur adipiscing elit pellentesque habitant morbi. Id interdum velit laoreet id donec ultrices. Fringilla phasellus faucibus scelerisque eleifend donec pretium. Est pellentesque elit ullamcorper dignissim. Mauris ultrices eros in cursus turpis massa tincidunt dui.': '支持全流程节点追踪与状态更新,您可随时查看运输进展。',
|
||||
'Dolor sit amet consectetur adipiscing elit pellentesque habitant morbi?': '如何获取报价?',
|
||||
'Eleifend mi in nulla posuere sollicitudin aliquam ultrices sagittis orci. Faucibus pulvinar elementum integer enim. Sem nulla pharetra diam sit amet nisl suscipit. Rutrum tellus pellentesque eu tincidunt. Lectus urna duis convallis convallis tellus. Urna molestie at elementum eu facilisis sed odio morbi quis': '可在报价页面提交需求,我们会根据货量、线路、时效快速回复。',
|
||||
'Ac odio tempor orci dapibus. Aliquam eleifend mi in nulla?': '支持企业定制方案吗?',
|
||||
'Tempus quam pellentesque nec nam aliquam sem et tortor consequat?': '售后如何对接?',
|
||||
'Molestie a iaculis at erat pellentesque adipiscing commodo. Dignissim suspendisse in est ante in. Nunc vel risus commodo viverra maecenas accumsan. Sit amet nisl suscipit adipiscing bibendum est. Purus gravida quis blandit turpis cursus in': '支持企业级定制服务,包含对账、异常处理与专属客服协同。',
|
||||
'Useful Links': '常用链接',
|
||||
'Our Services': '我们的服务',
|
||||
'Web Design': '网站设计',
|
||||
'Web Development': '网站开发',
|
||||
'Product Management': '产品管理',
|
||||
'Marketing': '市场推广',
|
||||
'Graphic Design': '平面设计',
|
||||
'IDE|Faster & Simpler': 'IDE|更快 · 更简单',
|
||||
'Faster & Simpler': '更快 · 更简单',
|
||||
'|Faster & Simpler': '更快 · 更简单',
|
||||
'We provide end-to-end international logistics for cross-border sellers and brands. From first mile to final delivery, IDE makes shipping faster and simpler.': '我们为跨境卖家与品牌提供端到端国际物流服务,从揽收到妥投全链路可控。IDE物流,让发货更快更简单。',
|
||||
'IDE focuses on international logistics operations with stable timelines, clear tracking, and transparent pricing to make global fulfillment faster and simpler.': 'IDE专注国际物流运营,以稳定时效、清晰轨迹和透明报价,帮助全球履约更快更简单。',
|
||||
'Reliable global delivery network': '稳定可靠的全球配送网络',
|
||||
'Integrated routes and carriers help us keep lead times short and delivery quality consistent.': '通过整合线路与承运资源,我们在缩短时效的同时保持交付稳定性。',
|
||||
'Real-time tracking and exception handling': '全程追踪与异常处理',
|
||||
'Every shipment is visible from pickup to delivery, with proactive alerts for delays or issues.': '每一票货物从揽收到签收全程可视,延误与异常可主动预警。',
|
||||
'Dedicated support for cross-border teams': '跨境团队专属支持',
|
||||
'Our logistics specialists respond quickly so your operations stay smooth and simple.': '物流专家快速响应,保障你的运营流程顺畅且简单。',
|
||||
'Built for international fulfillment at scale, with standard processes and reliable execution.': '面向国际履约规模化场景,提供标准化流程与稳定执行。',
|
||||
'Flexible shipping channels for parcels, pallets, and B2B cargo.': '支持包裹、托盘与 B2B 货运的灵活运输通道。',
|
||||
'Standardized workflows that reduce risk and improve delivery speed.': '标准化操作流程,降低风险并提升交付速度。',
|
||||
'Multi-warehouse inventory and order orchestration for international operations.': '面向国际业务的多仓库存与订单协同。',
|
||||
'Line-haul and last-mile coordination across regions with stable performance.': '跨区域干线与末端协同,保障稳定履约表现。',
|
||||
'Cost-efficient cargo plans tailored by route, weight, and service level.': '按线路、重量与服务等级定制更高性价比的货运方案。',
|
||||
'Protective packaging options designed for international transport.': '针对国际运输场景提供防护型包装方案。',
|
||||
'Smart warehousing and fast dispatch to keep global orders moving.': '智能仓储与快速出库,保障全球订单持续流转。',
|
||||
'Need a global logistics partner? Talk to IDE and launch a faster, simpler shipping plan for your business.': '需要国际物流合作伙伴?联系IDE,为你的业务启动更快更简单的发货方案。',
|
||||
'A logistics system built for speed, visibility, and simplicity.': '为速度、可视化与简化而打造的物流体系。',
|
||||
'From pickup to customs and final delivery, we connect every step with operational control.': '从揽收、清关到末端妥投,每个环节都在运营管控之中。',
|
||||
'Standard operating procedures ensure stable service quality.': '标准作业流程保障服务质量稳定。',
|
||||
'Route optimization improves both transit time and cost.': '线路优化同时改善时效与成本。',
|
||||
'Exception workflows reduce delays and customer complaints.': '异常处理流程降低延误与客诉。',
|
||||
'Optimized for cross-border growth': '为跨境增长场景优化',
|
||||
'Whether DTC or B2B, we provide consistent logistics capacity and service-level execution.': '无论 DTC 还是 B2B,我们都能提供持续稳定的履约能力与服务水位。',
|
||||
'Data-driven operations and reporting': '数据驱动的运营与报表',
|
||||
'Operational dashboards help you monitor timeliness, costs, and fulfillment performance.': '通过运营看板实时监控时效、成本与履约表现。',
|
||||
'Scale your logistics without adding operational complexity.': '在不增加运营复杂度的情况下扩展物流规模。',
|
||||
'Secure, compliant, and dependable international shipping.': '安全、合规、可靠的国际运输服务。',
|
||||
'Which countries and routes do you support?': '支持哪些国家与物流线路?',
|
||||
'We support major cross-border routes and can recommend channels based on transit time and cost targets.': '我们覆盖主流跨境线路,并可根据时效与成本目标推荐最合适渠道。',
|
||||
'Can I track every shipment in real time?': '是否支持全程实时追踪?',
|
||||
'Yes. You get milestone updates from pickup to delivery, with clear status visibility for your team.': '支持。你可以从揽收到签收获取关键节点更新,团队可清晰查看状态。',
|
||||
'How do I get a shipping quote?': '如何获取运输报价?',
|
||||
'Share shipment details in the contact form and we will return a route-specific quote quickly.': '在联系表单提交货物信息后,我们会快速返回线路化报价。',
|
||||
'Do you provide enterprise-level custom solutions?': '是否提供企业级定制方案?',
|
||||
'How is after-sales support handled?': '售后支持如何对接?',
|
||||
'Yes. We provide tailored SOPs, billing reconciliation, exception handling, and dedicated support teams.': '支持。我们提供定制 SOP、账单对账、异常处理与专属客服团队。',
|
||||
'Faster and simpler logistics execution designed for growing international businesses.': '为成长型国际业务打造更快更简单的物流执行体系。',
|
||||
'We align capacity, route planning, and exception management so your team can scale operations without process chaos.': '我们协同运力、线路规划与异常处理,让团队在规模增长时依然保持流程清晰。',
|
||||
'International Logistics Expertise': '国际物流专业能力',
|
||||
'We combine route planning, customs coordination, and fulfillment execution to keep cross-border delivery reliable and easy to manage.': '我们将线路规划、清关协同与履约执行整合在一起,让跨境交付稳定且易管理。',
|
||||
'Service details built around faster and simpler global shipping': '围绕“更快更简单”打造的全球发运服务细节',
|
||||
'IDE delivers practical international logistics with clear operating standards, stable timelines, and full shipment visibility.': 'IDE提供务实的国际物流服务,以清晰标准、稳定时效和全程可视化保障履约。',
|
||||
'Route-specific transport plans for cost and speed balance.': '按线路定制运输方案,平衡成本与时效。',
|
||||
'Milestone tracking with proactive exception alerts.': '关键节点跟踪并主动预警异常。',
|
||||
'Dedicated support from booking to final delivery.': '从订舱到妥投提供专属支持。',
|
||||
'Our teams coordinate warehouse, line-haul, customs, and last-mile partners to reduce delays and improve customer delivery experience.': '我们的团队协同仓储、干线、清关与末端伙伴,减少延误并提升客户签收体验。',
|
||||
'If your business needs a dependable cross-border logistics partner, we can design and run a workflow that keeps shipping faster and simpler every day.': '如果你的业务需要可靠的跨境物流伙伴,我们可为你设计并运营一套每天都更快更简单的发运流程。',
|
||||
'IDE is your international logistics partner for cross-border fulfillment, helping teams ship worldwide with faster execution and simpler operations.': 'IDE是全球数字化一站式跨境物流解决方案服务商,依托集团成熟物流产品和渠道,聚焦行业忽视的中小卖家与疑难订单赛道。以数字化智能系统、全流程顾问专属服务,解决中小跨境卖家发货烦琐、疑难单无人承接、售后无保障、行业资金风险高的普遍痛点。'
|
||||
,'IDE is a global digital one-stop cross-border logistics solution provider. Built on the group\'s mature logistics products and channels, we focus on underserved SMB sellers and difficult-order segments. With digital intelligent systems and full-process dedicated advisory services, we solve common pain points: cumbersome shipping, difficult orders with no takers, lack of after-sales protection, and high industry financial risk.': 'IDE是全球数字化一站式跨境物流解决方案服务商,依托集团成熟物流产品和渠道,聚焦行业忽视的中小卖家与疑难订单赛道。以数字化智能系统、全流程顾问专属服务,解决中小跨境卖家发货烦琐、疑难单无人承接、售后无保障、行业资金风险高的普遍痛点。',
|
||||
'Facere distinctio molestiae nisi fugit tenetur repellat non praesentium nesciunt optio quis sit odio nemo quisquam. eius quos reiciendis eum vel eum voluptatem eum maiores eaque id optio ullam occaecati odio est possimus vel reprehenderit': '我们提供稳定高效的国际物流服务,帮助卖家与企业在全球范围内更快完成履约。',
|
||||
'Clients': '客户',
|
||||
'Projects': '项目',
|
||||
'Support': '支持',
|
||||
'Workers': '员工',
|
||||
'Staff': '员工',
|
||||
'Delivered': '签收',
|
||||
'Customers': '客户',
|
||||
'Orders': '订单',
|
||||
'Hours Of Support': '支持时长',
|
||||
'Free Plan': '免费方案',
|
||||
'Business Plan': '商业方案',
|
||||
'Developer Plan': '开发者方案',
|
||||
'Buy Now': '立即购买',
|
||||
'Consult Now': '立即咨询',
|
||||
'Customs Clearance': '清关',
|
||||
'Warehousing': '仓储',
|
||||
'International Logistics': '国际物流',
|
||||
'Formal and express customs declaration support': '一般贸易与快件清关申报支持',
|
||||
'Document review and compliance check': '单证预审与合规校验',
|
||||
'Customs status updates and exception follow-up': '清关状态更新与异常跟进',
|
||||
'Inbound, shelving, and inventory control': '入库、上架与库存管控',
|
||||
'Order picking, packing, and same-day dispatch': '拣货、打包与当日出库',
|
||||
'Multi-platform order integration and reconciliation': '多平台订单对接与账务对账',
|
||||
'Air, sea, and truck route options': '空运、海运与卡航多线路可选',
|
||||
'End-to-end tracking from pickup to delivery': '从揽收到签收的全程追踪',
|
||||
'SLA-based operations and after-sales support': '基于 SLA 的运营保障与售后支持',
|
||||
'/ month': '/ 月',
|
||||
'Quam adipiscing vitae proin': '基础功能支持',
|
||||
'Nec feugiat nisl pretium': '标准服务保障',
|
||||
'Nulla at volutpat diam uteera': '多渠道履约能力',
|
||||
'Global route coverage': '全球线路覆盖',
|
||||
'Customs support and compliance': '清关支持与合规保障',
|
||||
'Real-time shipment visibility': '运输轨迹实时可视',
|
||||
'Pharetra massa massa ultricies': '高级扩展能力',
|
||||
'Massa ultricies mi quis hendrerit': '专属团队支持',
|
||||
'Core Services': '核心服务',
|
||||
'Transport Network': '运输网络',
|
||||
'Fulfillment Assurance': '履约保障',
|
||||
'Voluptatum deleniti atque corrupti quos dolores et quas molestias excepturi sint occaecati cupiditate non provident': '我们通过标准化流程与可视化系统,确保每一票货物稳定交付。',
|
||||
'Minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat tarad limino ata': '多渠道承运资源整合,兼顾时效、成本与稳定性。',
|
||||
'Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur': '以客户体验为中心,持续优化运输与履约流程。',
|
||||
'Voluptatem dignissimos provident quasi corporis voluptates sit assumenda.': '我们通过技术与运营协同,持续提升物流服务品质。',
|
||||
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.': '提供从仓储到配送的一站式解决方案,帮助业务稳步增长。',
|
||||
'Ullamco laboris nisi ut aliquip ex ea commodo consequat.': '标准化操作流程,保障稳定履约。',
|
||||
'Duis aute irure dolor in reprehenderit in voluptate velit.': '智能路由规划,优化整体成本。',
|
||||
'Ullam est qui quos consequatur eos accusamus.': '异常可追踪可处理,交付更可靠。',
|
||||
'Corporis temporibus maiores provident': '覆盖多场景运输能力',
|
||||
'Ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum': '无论是 B2B 还是电商订单,我们都可提供稳定履约能力与售后支持。',
|
||||
'Sunt consequatur ad ut est nulla consectetur reiciendis animi voluptas': '以数据驱动的运营优化',
|
||||
'Cupiditate placeat cupiditate placeat est ipsam culpa. Delectus quia minima quod. Sunt saepe odit aut quia voluptatem hic voluptas dolor doloremque.': '通过节点数据分析持续优化时效、库存与配送效率。',
|
||||
'Facilis ut et voluptatem aperiam. Autem soluta ad fugiat.': '支持业务规模增长过程中的灵活扩展。',
|
||||
'Quas et necessitatibus eaque impedit ipsum animi consequatur incidunt in': '稳定、安全、可扩展的服务体系',
|
||||
'You can duplicate this sample page and create any number of inner pages you like!': '您可以复制该示例页面并创建任意数量的内页。',
|
||||
'Our Team': '我们的团队',
|
||||
'Walter White': 'Walter White',
|
||||
'Sarah Jhinson': 'Sarah Jhinson',
|
||||
'William Anderson': 'William Anderson',
|
||||
'Saul Goodman': 'Saul Goodman',
|
||||
'Sara Wilsson': 'Sara Wilsson',
|
||||
'Jena Karlis': 'Jena Karlis',
|
||||
'Matt Brandon': 'Matt Brandon',
|
||||
'John Larson': 'John Larson',
|
||||
'Liam Chen': '陈亮',
|
||||
'Sophie Lin': '林苏菲',
|
||||
'David Wong': '王大卫',
|
||||
'Amy Zhao': '赵安妮',
|
||||
'Kevin Xu': '徐凯文',
|
||||
'Ceo & Founder': '首席执行官 / 创始人',
|
||||
'Designer': '设计师',
|
||||
'Store Owner': '店主',
|
||||
'Freelancer': '自由职业者',
|
||||
'Entrepreneur': '企业家',
|
||||
'Cross-border E-commerce Seller': '跨境电商卖家',
|
||||
'Brand Operations Director': '品牌运营总监',
|
||||
'B2B Trading Manager': 'B2B 贸易经理',
|
||||
'Warehouse Operations Lead': '仓储运营负责人',
|
||||
'International Business Owner': '国际业务负责人',
|
||||
'A108 Adam Street, New York, NY 535022': 'A108 Adam Street, New York, NY 535022',
|
||||
'404 Not Found': '404 页面未找到',
|
||||
'nginx': 'nginx',
|
||||
'We provide transparent pricing, stable transit timelines, and responsive support to help cross-border businesses ship with confidence.': '我们提供透明报价、稳定时效和快速响应支持,帮助跨境业务更安心地发货。',
|
||||
'Our operations team standardizes every fulfillment step to improve predictability and customer satisfaction.': '我们的运营团队将履约环节标准化,提升交付可预测性与客户满意度。',
|
||||
'Start Shipping': '开始发货',
|
||||
'Products': '产品',
|
||||
'Start Shipping with IDE': '开始用 IDE 发货',
|
||||
'View full pricing →': '查看完整价格 →',
|
||||
'Global routes with predictable transit times': '全球线路覆盖,时效可预测',
|
||||
'Real-time tracking with proactive exception control': '全程追踪并主动控制异常',
|
||||
'Dedicated account support and response SLA': '专属客户支持与响应 SLA',
|
||||
'Our specialists respond fast with SLA-backed support so operations stay smooth and simple.': '我们的专家团队提供 SLA 保障的快速响应,让运营始终更顺畅更简单。',
|
||||
'Built for high-volume international fulfillment with stable operations and measurable service quality.': '面向大规模国际履约,提供稳定运营与可量化服务质量。',
|
||||
'Flexible channels for parcels, pallets, and B2B cargo with optimized cost-time balance.': '支持包裹、托盘与 B2B 货运,并优化时效与成本平衡。',
|
||||
'Standard operating procedures that reduce delivery risk and improve on-time performance.': '标准作业流程降低交付风险并提升准时率。',
|
||||
'Need a reliable international logistics partner? Get a route-specific plan with pricing, transit time, and SLA commitment within 24 hours.': '需要可靠的国际物流伙伴?24 小时内获取含报价、时效与 SLA 承诺的线路方案。',
|
||||
'A commercial logistics system built for speed, visibility, and margin control.': '为速度、可视化与利润控制打造的商业物流体系。',
|
||||
'From pickup to customs clearance and final delivery, every milestone is managed with operational controls.': '从揽收到清关再到妥投,每个里程碑都在运营管控下执行。',
|
||||
'Optimized for cross-border growth and profitability': '为跨境增长与利润提升优化',
|
||||
'Faster and simpler fulfillment designed to support growing international revenue.': '更快更简单的履约体系,支持国际业务持续增长。',
|
||||
'We align capacity, routing, and exception management so your team can scale volume without losing control of service levels.': '我们协同运力、路由与异常管理,让团队扩量时仍能稳住服务水位。',
|
||||
'Data-driven operations, billing, and KPI reporting': '数据驱动的运营、对账与 KPI 报表',
|
||||
'Operational dashboards track on-time rate, logistics cost, and fulfillment KPIs in one view.': '运营看板一屏跟踪准时率、物流成本与履约 KPI。',
|
||||
'Secure, compliant, and claim-ready international shipping': '安全、合规、可理赔的国际运输服务',
|
||||
'How fast can I get a shipping quote?': '最快多久可以拿到报价?',
|
||||
'Share shipment details and we return a route-specific quote with transit-time options quickly.': '提交货物信息后,我们会快速返回含多种时效选项的线路报价。',
|
||||
'Do you support enterprise-level custom workflows?': '是否支持企业级定制流程?',
|
||||
'How are claims and after-sales issues handled?': '理赔与售后问题如何处理?',
|
||||
'Yes. We provide tailored SOPs, billing reconciliation, claims handling, and dedicated support teams.': '支持。我们提供定制 SOP、账单对账、理赔处理与专属支持团队。',
|
||||
'Route-specific transport plans balancing cost, speed, and service reliability.': '按线路定制运输方案,平衡成本、时效与服务稳定性。',
|
||||
'Milestone tracking with proactive delay and exception alerts.': '关键节点跟踪并主动预警延误与异常。',
|
||||
'Dedicated support from booking to delivery with clear escalation paths.': '从订舱到妥投提供专属支持,并配备清晰升级路径。',
|
||||
'Bldg. 36, Zone 5, Cuigang, Huaide, Fuyong, Baoan District, Shenzhen': '深圳宝安区福永怀德翠岗5区36栋',
|
||||
'Copyright 2026 Immediate Delivery Express (Shenzhen) Co, Ltd. All Rights Reserved.': 'Copyright 2026 Immediate Delivery Express (Shenzhen) Co, Ltd. All Rights Reserved.',
|
||||
'IDE logo': 'IDE标志',
|
||||
'Immediate Delivery Express': 'Immediate Delivery Express'
|
||||
,'IDE is a global digital one-stop cross-border logistics provider for SMB sellers and difficult orders — with smart systems and dedicated advisors at every step.': '体量不大、线路偏门?IDE 照样发。全球订单一站搞定,专属顾问从报价跟到签收,疑难单也有人管。'
|
||||
,'IDE | Global one-stop cross-border logistics solution provider': 'IDE - 跨境物流,全球即递'
|
||||
,'IDE - Cross-border Logistics, Delivered Globally': 'IDE - 跨境物流,全球即递'
|
||||
,'Explore Services': '了解服务'
|
||||
,'Quote Response': '报价响应'
|
||||
,'Global Routes': '全球线路'
|
||||
,'Active Clients': '活跃客户'
|
||||
,'Why IDE': '为什么选择 IDE'
|
||||
,'Three reasons cross-border teams choose Immediate Delivery Express.': '跨境团队选择 Immediate Delivery Express 的三大理由。'
|
||||
,'Built for SMB sellers and difficult orders that others overlook.': '专为行业忽视的中小卖家与疑难订单而生。'
|
||||
,'Immediate': '即时'
|
||||
,'Integrated': '一体化'
|
||||
,'Intelligent': '智能'
|
||||
,'Digital': '数字化'
|
||||
,'Dedicated': '专属顾问'
|
||||
,'Specialized': '疑难专精'
|
||||
,'Predictable transit times with SLA-backed operations.': 'SLA 保障运营,时效可预期。'
|
||||
,'Route-specific quotes within 24 hours.': '24 小时内返回线路化报价。'
|
||||
,'Smart systems streamline shipping for growing cross-border sellers.': '智能系统简化中小跨境卖家的发货流程。'
|
||||
,'Less manual work, more visibility, faster execution.': '减少繁琐操作,提升可视化,加快履约执行。'
|
||||
,'Full-process advisors from quote to after-sales support.': '从报价到售后,全流程顾问专属服务。'
|
||||
,'One dedicated team — not a ticket queue.': '专属团队对接,不是工单排队。'
|
||||
,'We take on difficult orders that others won\'t accept.': '承接其他渠道不愿接的疑难订单。'
|
||||
,'Complex cargo, special routes, and edge cases welcome.': '复杂货型、特殊线路与边缘场景均可承接。'
|
||||
,'Warehousing, customs, and line-haul in one flow.': '仓储、清关与干线一体化协同。'
|
||||
,'IDE OMS + WMS connected end-to-end.': 'IDE OMS + WMS 端到端打通。'
|
||||
,'Full shipment visibility from pickup to delivery.': '从揽收到签收全程可视。'
|
||||
,'Proactive alerts before exceptions escalate.': '异常升级前主动预警。'
|
||||
,'We provide transparent pricing, stable transit timelines, and responsive support to help SMB cross-border sellers ship with confidence.': '我们提供透明报价、稳定时效和快速响应支持,帮助中小跨境卖家更安心地发货。'
|
||||
,'Everything you need to ship globally — built around IDE systems.': '全球发运所需的一切——围绕 IDE 系统构建。'
|
||||
,'Inbound, inventory control, picking, packing, and same-day dispatch for global orders.': '入库、库存管控、拣货打包与全球订单当日出库。'
|
||||
,'Air, sea, and truck routes with end-to-end tracking and SLA-based operations.': '空运、海运与卡航线路,全程追踪与 SLA 运营保障。'
|
||||
,'Formal and express declaration, compliance review, and status follow-up.': '一般贸易与快件申报、合规预审与状态跟进。'
|
||||
,'View all services →': '查看全部服务 →'
|
||||
,'Tracking visibility is excellent. Our team handles exceptions faster with clearer operational ownership — and dedicated advisors keep everything on track.': '轨迹可视化非常出色。团队处理异常更快、责任更清晰——专属顾问全程跟进保障。'
|
||||
,'Ready to ship with IDE?': '准备好用 IDE 发货了吗?'
|
||||
,'Get a route-specific plan with pricing, transit time, and SLA commitment within 24 hours.': '24 小时内获取含报价、时效与 SLA 承诺的线路方案。'
|
||||
,'Share shipment details and we return a route-specific quote with transit-time options within 24 hours.': '提交货物信息后,24 小时内返回含多种时效选项的线路报价。'
|
||||
,'CEO & Founder · Cross-border Brand': '首席执行官 / 创始人 · 跨境品牌'
|
||||
,'Operations Lead · E-commerce Seller': '运营负责人 · 电商卖家'
|
||||
,'Hong Kong': '香港'
|
||||
,'Shenzhen': '深圳'
|
||||
,'Los Angeles': '洛杉矶'
|
||||
,'London': '伦敦'
|
||||
,'Singapore': '新加坡'
|
||||
,'Tokyo': '东京'
|
||||
,'Frankfurt': '法兰克福'
|
||||
,'Sydney': '悉尼'
|
||||
,'View Pricing': '查看价格'
|
||||
,'Get Started': '快速开始'
|
||||
,'How It Works': '合作流程'
|
||||
,'3 steps — quote to delivery.': '三步 — 从报价到签收'
|
||||
,'Others said no?': '别人不接?'
|
||||
,'We say yes': '我们接'
|
||||
,'Route · Cargo · Time': '线路 · 货型 · 时效'
|
||||
,'24h quote': '24h 报价'
|
||||
,'Price · Transit': '价格 · 时效'
|
||||
,'1 advisor assigned': '分配专属顾问'
|
||||
,'1 advisor': '1 位顾问'
|
||||
,'Pickup → Delivered': '揽收 → 签收'
|
||||
,'After-sales backup': '售后保障'
|
||||
,'Share your shipment': '提交货物信息'
|
||||
,'Get a quote →': '获取报价 →'
|
||||
,'Start shipping →': '开始发货 →'
|
||||
,'Get your plan in 24h': '24 小时拿方案'
|
||||
,'See pricing →': '查看价格 →'
|
||||
,'Ship with support': '顾问全程跟单'
|
||||
,'View services →': '查看服务 →'
|
||||
,'Why Sellers Choose IDE': '卖家为什么选择 IDE'
|
||||
,'3 questions — 3 answers you\'ll remember.': '三个问题 — 三个让你记住的答案'
|
||||
,'Will you take my order?': '我的单你们接吗?'
|
||||
,'YES': '能'
|
||||
,'Small volume': '小批量'
|
||||
,'Tough cargo': '疑难货'
|
||||
,'Special routes': '特殊线路'
|
||||
,'Who do I talk to?': '出了问题找谁?'
|
||||
,'Quote → After-sales': '报价 → 售后'
|
||||
,'Not a ticket queue': '不是工单排队'
|
||||
,'Can I track everything?': '物流能全程查到吗?'
|
||||
,'Live milestones': '节点实时'
|
||||
,'Alert before delay': '延误先预警'
|
||||
,'One team — three pillars.': '一个团队 — 三大支柱'
|
||||
,'Same-day dispatch': '当日出库'
|
||||
,'Pick · Pack · Ship': '拣 · 包 · 发'
|
||||
,'Air · Sea · Truck': '空 · 海 · 卡'
|
||||
,'Door-to-door track': '门到门追踪'
|
||||
,'Fast clearance': '快速清关'
|
||||
,'Compliant · On time': '合规 · 准时'
|
||||
,'Ask about your shipment →': '咨询我的货物 →'
|
||||
,'YES — we take them': '能 — 我们接'
|
||||
,'Small · Tough · Special': '小批 · 疑难 · 特殊'
|
||||
,'Within 24 hours': '24 小时内'
|
||||
,'Price + transit options': '价格 + 时效方案'
|
||||
,'Global routes': '全球线路'
|
||||
,'Best channel matched': '最优渠道匹配'
|
||||
,'Full visibility': '全程可视'
|
||||
,'Your advisor handles it': '顾问帮你处理'
|
||||
,'Claims · Exceptions': '理赔 · 异常'
|
||||
,'Hard shipment?': '疑难货?'
|
||||
,'Free advisor': '免费顾问'
|
||||
,'Route · Price · Transit': '线路 · 价格 · 时效'
|
||||
,'One-stop fulfillment': '一站式履约'
|
||||
,'Stable SLA': '稳定 SLA'
|
||||
,'Cost · Speed balanced': '成本 · 时效平衡'
|
||||
,'On-time delivery': '准时送达'
|
||||
,'Risk controlled': '风险可控'
|
||||
,'Multi-warehouse': '多仓协同'
|
||||
,'Global inventory': '全球库存'
|
||||
,'Line-haul + last mile': '干线 + 末端'
|
||||
,'Stable performance': '稳定履约'
|
||||
,'Route optimized': '线路优化'
|
||||
,'By weight & SLA': '按重量 · SLA'
|
||||
,'Regional fleet': '区域车队'
|
||||
,'Flexible capacity': '弹性运力'
|
||||
,'Export-grade pack': '出口级包装'
|
||||
,'Damage protected': '防损保护'
|
||||
,'Smart WMS': '智能 WMS'
|
||||
,'Docs reviewed': '单证预审'
|
||||
,'Status tracked': '状态跟踪'
|
||||
,'Multi-platform sync': '多平台同步'
|
||||
,'SLA backed': 'SLA 保障'
|
||||
,'Call Us': '电话咨询'
|
||||
,'Get Help': '快速帮助'
|
||||
,'Track Shipment': '查询物流'
|
||||
,'Cross-border shipping for SMB sellers and difficult orders — dedicated advisors, full tracking, and real after-sales support.': '面向中小卖家与疑难订单的跨境物流——专属顾问、全程追踪、真实售后保障。'
|
||||
,'FAQ': '常见问题'
|
||||
,'Do you accept small-volume or difficult orders?': '体量小或疑难订单能接吗?'
|
||||
,'What if something goes wrong after shipping?': '发货后出问题怎么办?'
|
||||
};
|
||||
|
||||
const reverseTextMap = {};
|
||||
Object.entries(textMap).forEach(([englishText, chineseText]) => {
|
||||
if (chineseText && !reverseTextMap[chineseText]) {
|
||||
reverseTextMap[chineseText] = englishText;
|
||||
}
|
||||
});
|
||||
|
||||
function normalizeI18nText(text) {
|
||||
return (text || '')
|
||||
.replace(/[\u2018\u2019\u2032]/g, "'")
|
||||
.replace(/[\u201C\u201D\u2033]/g, '"')
|
||||
.replace(/\u00A0/g, ' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
}
|
||||
|
||||
function resolveEnglishKey(rawText) {
|
||||
const normalizedText = normalizeI18nText(rawText || '');
|
||||
if (!normalizedText) {
|
||||
return '';
|
||||
}
|
||||
if (textMap[normalizedText]) {
|
||||
return normalizedText;
|
||||
}
|
||||
return reverseTextMap[normalizedText] || '';
|
||||
}
|
||||
|
||||
function tagI18nElement(element) {
|
||||
if (!element || element.hasAttribute('data-i18n-key')) {
|
||||
return;
|
||||
}
|
||||
if (element.closest('.site-nav-dock')) {
|
||||
return;
|
||||
}
|
||||
const key = resolveEnglishKey(element.textContent || '');
|
||||
if (key) {
|
||||
element.setAttribute('data-i18n-key', key);
|
||||
}
|
||||
}
|
||||
|
||||
function registerI18nTargets() {
|
||||
document.querySelectorAll('body *').forEach((element) => {
|
||||
if (element.children.length !== 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
tagI18nElement(element);
|
||||
});
|
||||
|
||||
[
|
||||
'.hero-desc',
|
||||
'.hero-stat__label',
|
||||
'.btn-hero-primary',
|
||||
'.btn-hero-secondary',
|
||||
'.section-header > span',
|
||||
'.section-header > h2',
|
||||
'.section-header > p',
|
||||
'.how-step h3',
|
||||
'.why-ide-card h3',
|
||||
'.how-step-link',
|
||||
'.pricing-more-link',
|
||||
'.call-to-action h3',
|
||||
'.cta-btn',
|
||||
'.faq-question',
|
||||
'.page-header h2',
|
||||
'.page-header p',
|
||||
'.featured-services .title',
|
||||
'.services .service-card h3 a',
|
||||
'.services .card:not(.service-card) h3 a',
|
||||
'.pricing .pricing-item h3',
|
||||
'.horizontal-pricing .pricing-item h3',
|
||||
'.footer-info > p',
|
||||
'.footer-links h4',
|
||||
'.footer-contact h4',
|
||||
'#navbar ul > li:not(.site-nav-dock) > a:not(#language-toggle-btn)',
|
||||
].forEach((selector) => {
|
||||
document.querySelectorAll(selector).forEach(tagI18nElement);
|
||||
});
|
||||
|
||||
document.querySelectorAll('[placeholder]').forEach((element) => {
|
||||
const placeholder = element.getAttribute('placeholder');
|
||||
const placeholderKey = resolveEnglishKey(placeholder || '');
|
||||
if (placeholderKey) {
|
||||
element.setAttribute('data-i18n-placeholder-key', placeholderKey);
|
||||
}
|
||||
});
|
||||
|
||||
document.querySelectorAll('[alt],[title],[aria-label],[value]').forEach((element) => {
|
||||
['alt', 'title', 'aria-label', 'value'].forEach((attr) => {
|
||||
const rawValue = element.getAttribute(attr);
|
||||
const key = resolveEnglishKey(rawValue || '');
|
||||
if (key) {
|
||||
element.setAttribute(`data-i18n-${attr}-key`, key);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const textNodeWalker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
|
||||
let textNode = textNodeWalker.nextNode();
|
||||
while (textNode) {
|
||||
const parent = textNode.parentElement;
|
||||
if (!parent || ['SCRIPT', 'STYLE', 'NOSCRIPT'].includes(parent.tagName)) {
|
||||
textNode = textNodeWalker.nextNode();
|
||||
continue;
|
||||
}
|
||||
|
||||
const key = resolveEnglishKey(textNode.textContent || '');
|
||||
if (key) {
|
||||
i18nTextNodes.set(textNode, key);
|
||||
}
|
||||
textNode = textNodeWalker.nextNode();
|
||||
}
|
||||
}
|
||||
|
||||
function renderLanguageToggle(isChinese) {
|
||||
const languageToggleLink = document.querySelector('#language-toggle-btn');
|
||||
if (!languageToggleLink) {
|
||||
return;
|
||||
}
|
||||
|
||||
languageToggleLink.classList.toggle('lang-is-zh', isChinese);
|
||||
languageToggleLink.classList.toggle('lang-is-en', !isChinese);
|
||||
languageToggleLink.innerHTML =
|
||||
'<i class="bi bi-translate language-toggle-icon" aria-hidden="true"></i>' +
|
||||
`<span class="language-toggle__next">${isChinese ? 'EN' : '中'}</span>`;
|
||||
languageToggleLink.setAttribute(
|
||||
'aria-label',
|
||||
isChinese ? 'Switch to English' : '切换到中文'
|
||||
);
|
||||
languageToggleLink.setAttribute('title', isChinese ? 'Switch to English' : '切换到中文');
|
||||
}
|
||||
|
||||
function applyLanguage(lang) {
|
||||
const isChinese = lang === 'zh';
|
||||
document.documentElement.lang = isChinese ? 'zh-CN' : 'en';
|
||||
|
||||
const currentTitle = document.title;
|
||||
if (isChinese && pageTitleMap[currentTitle]) {
|
||||
document.title = pageTitleMap[currentTitle];
|
||||
} else if (!isChinese) {
|
||||
const englishTitle = Object.keys(pageTitleMap).find((key) => pageTitleMap[key] === currentTitle);
|
||||
if (englishTitle) {
|
||||
document.title = englishTitle;
|
||||
}
|
||||
}
|
||||
|
||||
document.querySelectorAll('[data-i18n-key]').forEach((element) => {
|
||||
const key = element.getAttribute('data-i18n-key');
|
||||
if (!key) {
|
||||
return;
|
||||
}
|
||||
element.textContent = isChinese ? (textMap[key] || key) : key;
|
||||
});
|
||||
|
||||
document.querySelectorAll('[data-i18n-placeholder-key]').forEach((element) => {
|
||||
const key = element.getAttribute('data-i18n-placeholder-key');
|
||||
if (!key) {
|
||||
return;
|
||||
}
|
||||
element.setAttribute('placeholder', isChinese ? (textMap[key] || key) : key);
|
||||
});
|
||||
|
||||
['alt', 'title', 'aria-label', 'value'].forEach((attr) => {
|
||||
document.querySelectorAll(`[data-i18n-${attr}-key]`).forEach((element) => {
|
||||
const key = element.getAttribute(`data-i18n-${attr}-key`);
|
||||
if (!key) {
|
||||
return;
|
||||
}
|
||||
element.setAttribute(attr, isChinese ? (textMap[key] || key) : key);
|
||||
});
|
||||
});
|
||||
|
||||
const textNodeWalker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
|
||||
let textNode = textNodeWalker.nextNode();
|
||||
while (textNode) {
|
||||
const key = i18nTextNodes.get(textNode);
|
||||
if (key) {
|
||||
textNode.textContent = isChinese ? (textMap[key] || key) : key;
|
||||
}
|
||||
textNode = textNodeWalker.nextNode();
|
||||
}
|
||||
|
||||
renderLanguageToggle(isChinese);
|
||||
|
||||
if (window.IDEAdvisor) {
|
||||
window.IDEAdvisor.setLanguage(isChinese ? 'zh' : 'en');
|
||||
}
|
||||
|
||||
if (window.IDEOpenGraph && typeof window.IDEOpenGraph.refresh === 'function') {
|
||||
window.IDEOpenGraph.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
function resolveSavedLanguage() {
|
||||
const saved = localStorage.getItem(languageStorageKey);
|
||||
if (saved === 'zh' || saved === 'en') return saved;
|
||||
return 'zh';
|
||||
}
|
||||
|
||||
function initLanguageSwitch() {
|
||||
const dropDownMenu = Array.from(document.querySelectorAll('#navbar > ul > li.dropdown')).find((menuItem) => {
|
||||
const menuText = normalizeI18nText(menuItem.textContent || '');
|
||||
return menuText.includes('Drop Down');
|
||||
});
|
||||
if (dropDownMenu) {
|
||||
dropDownMenu.remove();
|
||||
}
|
||||
|
||||
registerI18nTargets();
|
||||
|
||||
const languageBtn = document.getElementById('language-toggle-btn');
|
||||
if (languageBtn && languageBtn.dataset.bound !== '1') {
|
||||
languageBtn.dataset.bound = '1';
|
||||
languageBtn.addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
const currentLang = resolveSavedLanguage();
|
||||
const nextLang = currentLang === 'en' ? 'zh' : 'en';
|
||||
localStorage.setItem(languageStorageKey, nextLang);
|
||||
applyLanguage(nextLang);
|
||||
});
|
||||
}
|
||||
|
||||
applyLanguage(resolveSavedLanguage());
|
||||
}
|
||||
|
||||
/**
|
||||
* Preloader
|
||||
*/
|
||||
const preloader = document.querySelector('#preloader');
|
||||
if (preloader) {
|
||||
window.addEventListener('load', () => {
|
||||
preloader.remove();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sticky header on scroll
|
||||
*/
|
||||
const selectHeader = document.querySelector('#header');
|
||||
if (selectHeader) {
|
||||
document.addEventListener('scroll', () => {
|
||||
window.scrollY > 100 ? selectHeader.classList.add('sticked') : selectHeader.classList.remove('sticked');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Scroll top button
|
||||
*/
|
||||
const scrollTop = document.querySelector('.scroll-top');
|
||||
if (scrollTop) {
|
||||
const togglescrollTop = function() {
|
||||
window.scrollY > 100 ? scrollTop.classList.add('active') : scrollTop.classList.remove('active');
|
||||
}
|
||||
window.addEventListener('load', togglescrollTop);
|
||||
document.addEventListener('scroll', togglescrollTop);
|
||||
scrollTop.addEventListener('click', window.scrollTo({
|
||||
top: 0,
|
||||
behavior: 'smooth'
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Mobile nav toggle
|
||||
*/
|
||||
const mobileNavShow = document.querySelector('.mobile-nav-show');
|
||||
const mobileNavHide = document.querySelector('.mobile-nav-hide');
|
||||
|
||||
document.querySelectorAll('.mobile-nav-toggle').forEach(el => {
|
||||
el.addEventListener('click', function(event) {
|
||||
event.preventDefault();
|
||||
mobileNavToogle();
|
||||
})
|
||||
});
|
||||
|
||||
function mobileNavToogle() {
|
||||
document.querySelector('body').classList.toggle('mobile-nav-active');
|
||||
mobileNavShow.classList.toggle('d-none');
|
||||
mobileNavHide.classList.toggle('d-none');
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide mobile nav on same-page/hash links
|
||||
*/
|
||||
document.querySelectorAll('#navbar a').forEach(navbarlink => {
|
||||
if (navbarlink.id === 'language-toggle-btn') return;
|
||||
const hash = navbarlink.hash;
|
||||
if (!hash || hash === '#') return;
|
||||
|
||||
let section = null;
|
||||
try {
|
||||
section = document.querySelector(hash);
|
||||
} catch (error) {
|
||||
return;
|
||||
}
|
||||
if (!section) return;
|
||||
|
||||
navbarlink.addEventListener('click', () => {
|
||||
if (document.querySelector('.mobile-nav-active')) {
|
||||
mobileNavToogle();
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
/**
|
||||
* Toggle mobile nav dropdowns
|
||||
*/
|
||||
const navDropdowns = document.querySelectorAll('.navbar .dropdown > a');
|
||||
|
||||
navDropdowns.forEach(el => {
|
||||
el.addEventListener('click', function(event) {
|
||||
if (document.querySelector('.mobile-nav-active')) {
|
||||
event.preventDefault();
|
||||
this.classList.toggle('active');
|
||||
this.nextElementSibling.classList.toggle('dropdown-active');
|
||||
|
||||
let dropDownIndicator = this.querySelector('.dropdown-indicator');
|
||||
dropDownIndicator.classList.toggle('bi-chevron-up');
|
||||
dropDownIndicator.classList.toggle('bi-chevron-down');
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
/**
|
||||
* Initiate PureCounter
|
||||
*/
|
||||
if (typeof PureCounter !== 'undefined' && document.querySelector('.purecounter')) {
|
||||
new PureCounter();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiate glightbox
|
||||
*/
|
||||
if (typeof GLightbox !== 'undefined' && document.querySelector('.glightbox')) {
|
||||
GLightbox({
|
||||
selector: '.glightbox'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Animation on scroll function and init
|
||||
*/
|
||||
function aos_init() {
|
||||
if (typeof AOS === 'undefined') return;
|
||||
AOS.init({
|
||||
duration: 1000,
|
||||
easing: 'ease-in-out',
|
||||
once: true,
|
||||
mirror: false
|
||||
});
|
||||
}
|
||||
window.addEventListener('load', () => {
|
||||
aos_init();
|
||||
});
|
||||
|
||||
initLanguageSwitch();
|
||||
|
||||
});
|
||||
@ -1,78 +0,0 @@
|
||||
/**
|
||||
* Open Graph:复用 <title> / meta[name=description],补齐绝对 og:url / og:image。
|
||||
* 爬虫可读 head 内静态 meta;本脚本在浏览器中按当前域名校正。
|
||||
*/
|
||||
(function () {
|
||||
var PROD_ORIGIN = 'https://ideship.com';
|
||||
var OG_IMAGE_PATH = '/site/assets/img/og-cover.png';
|
||||
var OG_IMAGE_REL = 'assets/img/og-cover.png';
|
||||
|
||||
function truncate(text, max) {
|
||||
var value = String(text || '')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
if (value.length <= max) return value;
|
||||
return value.slice(0, max);
|
||||
}
|
||||
|
||||
function ensureMeta(property, content) {
|
||||
if (!content) return;
|
||||
var node = document.querySelector('meta[property="' + property + '"]');
|
||||
if (!node) {
|
||||
node = document.createElement('meta');
|
||||
node.setAttribute('property', property);
|
||||
document.head.appendChild(node);
|
||||
}
|
||||
node.setAttribute('content', content);
|
||||
}
|
||||
|
||||
function resolveOrigin() {
|
||||
if (location.protocol === 'http:' || location.protocol === 'https:') {
|
||||
return location.origin;
|
||||
}
|
||||
return PROD_ORIGIN;
|
||||
}
|
||||
|
||||
function resolvePageUrl(origin) {
|
||||
if (location.protocol === 'file:') {
|
||||
return /9610\.html$/i.test(location.pathname || '')
|
||||
? PROD_ORIGIN + '/9610.html'
|
||||
: PROD_ORIGIN + '/';
|
||||
}
|
||||
var path = location.pathname || '/';
|
||||
if (/\/index\.html$/i.test(path)) {
|
||||
path = path.replace(/\/index\.html$/i, '/') || '/';
|
||||
}
|
||||
if (path === '/site' || path === '/site/' || path === '/site/index.html') {
|
||||
path = '/';
|
||||
}
|
||||
return origin + path + (location.search || '');
|
||||
}
|
||||
|
||||
function resolveImageUrl(origin) {
|
||||
if (origin === PROD_ORIGIN || /(^|\.)ideship\.com$/i.test(location.hostname || '')) {
|
||||
return origin + OG_IMAGE_PATH;
|
||||
}
|
||||
try {
|
||||
return new URL(OG_IMAGE_REL, location.href).href;
|
||||
} catch (e) {
|
||||
return origin + '/assets/img/og-cover.png';
|
||||
}
|
||||
}
|
||||
|
||||
function applyOpenGraph() {
|
||||
var title = truncate(document.title, 30);
|
||||
var descNode = document.querySelector('meta[name="description"]');
|
||||
var description = truncate(descNode && descNode.getAttribute('content'), 60);
|
||||
var origin = resolveOrigin();
|
||||
|
||||
ensureMeta('og:title', title);
|
||||
if (description) ensureMeta('og:description', description);
|
||||
ensureMeta('og:image', resolveImageUrl(origin));
|
||||
ensureMeta('og:url', resolvePageUrl(origin));
|
||||
ensureMeta('og:type', 'website');
|
||||
}
|
||||
|
||||
applyOpenGraph();
|
||||
window.IDEOpenGraph = { refresh: applyOpenGraph };
|
||||
})();
|
||||
1
IDE官网/assets/vendor/aos/aos.css
vendored
1
IDE官网/assets/vendor/aos/aos.js
vendored
1981
IDE官网/assets/vendor/bootstrap-icons/bootstrap-icons.css
vendored
80
IDE官网/assets/vendor/php-email-form/validate.js
vendored
@ -1,80 +0,0 @@
|
||||
/**
|
||||
* PHP Email Form Validation - v3.1
|
||||
* URL: https://bootstrapmade.com/php-email-form/
|
||||
* Author: BootstrapMade.com
|
||||
*/
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
let forms = document.querySelectorAll('.php-email-form');
|
||||
|
||||
forms.forEach(function (form) {
|
||||
form.addEventListener('submit', function (event) {
|
||||
event.preventDefault();
|
||||
|
||||
let thisForm = this;
|
||||
let action = thisForm.getAttribute('action');
|
||||
let recaptcha = thisForm.getAttribute('data-recaptcha-site-key');
|
||||
|
||||
if (!action) {
|
||||
displayError(thisForm, 'The form action property is not set!');
|
||||
return;
|
||||
}
|
||||
|
||||
thisForm.querySelector('.loading').classList.add('d-block');
|
||||
thisForm.querySelector('.error-message').classList.remove('d-block');
|
||||
thisForm.querySelector('.sent-message').classList.remove('d-block');
|
||||
|
||||
let formData = new FormData(thisForm);
|
||||
|
||||
if (recaptcha) {
|
||||
if (typeof grecaptcha !== 'undefined') {
|
||||
grecaptcha.ready(function () {
|
||||
grecaptcha.execute(recaptcha, { action: 'php_email_form_submit' })
|
||||
.then(function (token) {
|
||||
formData.set('recaptcha-response', token);
|
||||
php_email_form_submit(thisForm, action, formData);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
displayError(thisForm, 'The reCaptcha javascript API url is not loaded!');
|
||||
}
|
||||
} else {
|
||||
php_email_form_submit(thisForm, action, formData);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function php_email_form_submit(thisForm, action, formData) {
|
||||
fetch(action, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
||||
})
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
return response.text();
|
||||
}
|
||||
throw new Error(`${response.status} ${response.statusText} ${response.url}`);
|
||||
})
|
||||
.then(data => {
|
||||
thisForm.querySelector('.loading').classList.remove('d-block');
|
||||
if (data.trim() == 'OK') {
|
||||
thisForm.querySelector('.sent-message').classList.add('d-block');
|
||||
thisForm.reset();
|
||||
} else {
|
||||
throw new Error(data ? data : 'Form submission failed and no error message returned from: ' + action);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
displayError(thisForm, error);
|
||||
});
|
||||
}
|
||||
|
||||
function displayError(thisForm, error) {
|
||||
thisForm.querySelector('.loading').classList.remove('d-block');
|
||||
thisForm.querySelector('.error-message').innerHTML = error;
|
||||
thisForm.querySelector('.error-message').classList.add('d-block');
|
||||
}
|
||||
|
||||
})();
|
||||
@ -1,61 +0,0 @@
|
||||
# IDE 文案术语统一表
|
||||
|
||||
本表用于官网中英文切换与后续文案维护,优先使用以下标准词汇。
|
||||
|
||||
## 核心业务术语
|
||||
|
||||
- 物流: Logistics
|
||||
- 国际物流: International Logistics
|
||||
- 运输: Transportation
|
||||
- 货运: Cargo
|
||||
- 仓储: Warehousing
|
||||
- 履约: Fulfillment
|
||||
- 清关: Customs Clearance
|
||||
- 路线/线路: Route
|
||||
- 报价: Quote
|
||||
- 时效: Transit Time
|
||||
- 追踪/轨迹查询: Tracking
|
||||
|
||||
## 常用按钮与动作
|
||||
|
||||
- 开始发货: Start Shipping
|
||||
- 获取报价: Get a Quote
|
||||
- 查询物流: Track
|
||||
- 立即咨询: Consult Now
|
||||
- 了解更多: Learn More
|
||||
|
||||
## 服务能力表达
|
||||
|
||||
- 全程可视化: End-to-end Visibility
|
||||
- 异常处理: Exception Handling
|
||||
- 运营保障: Operational Assurance
|
||||
- 账单对账: Billing Reconciliation
|
||||
- 标准作业流程: Standard Operating Procedures (SOP)
|
||||
|
||||
## 品牌定位(官方)
|
||||
|
||||
**中文:**
|
||||
|
||||
IDE是全球数字化一站式跨境物流解决方案服务商,依托集团成熟物流产品和渠道,聚焦行业忽视的中小卖家与疑难订单赛道。以数字化智能系统、全流程顾问专属服务,解决中小跨境卖家发货烦琐、疑难单无人承接、售后无保障、行业资金风险高的普遍痛点。
|
||||
|
||||
**英文:**
|
||||
|
||||
IDE is a global digital one-stop cross-border logistics solution provider. Built on the group's mature logistics products and channels, we focus on underserved SMB sellers and difficult-order segments. With digital intelligent systems and full-process dedicated advisory services, we solve common pain points: cumbersome shipping, difficult orders with no takers, lack of after-sales protection, and high industry financial risk.
|
||||
|
||||
## 写作规范
|
||||
|
||||
- 英文大小写:
|
||||
- 导航、按钮使用 Title Case(如 `Get a Quote`)
|
||||
- 正文句子使用 sentence case
|
||||
- 中文风格:
|
||||
- 优先短句,避免重复修饰
|
||||
- 同一页面中“物流 / 国际物流 / 履约 / 运输”按语义严格区分,不混用
|
||||
- 品牌名:
|
||||
- 主品牌: IDE(页面主视觉必须突出 IDE)
|
||||
- 英文全称(小字副标): Immediate Delivery Express
|
||||
- 中文: IDE
|
||||
|
||||
## 备注
|
||||
|
||||
- 若页面原文偏营销,可在不改变术语的前提下调整语气。
|
||||
- 如新增术语,请先更新本表,再同步到 `assets/js/main.js` 的 `textMap`。
|
||||
@ -1,98 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport">
|
||||
|
||||
<title>IDE - 跨境物流,全球即递</title>
|
||||
<meta content="IDE 面向中小卖家的跨境物流。全球数字化一站式跨境物流服务商。注册登记后即可询价,一口价不砍,专属顾问跟到签收。" name="description">
|
||||
<meta content="IDE, 跨境物流, 中小卖家, 9610, 疑难单, 出口退税, 欧美专线, 东南亚专线" name="keywords">
|
||||
|
||||
<!-- Open Graph(标题/描述复用上方 title、description;图片为 478×478) -->
|
||||
<meta property="og:title" content="IDE - 跨境物流,全球即递">
|
||||
<meta property="og:description" content="IDE 面向中小卖家的跨境物流。全球数字化一站式服务,一口价不砍。">
|
||||
<meta property="og:image" content="https://ideship.com/site/assets/img/og-cover.png">
|
||||
<meta property="og:url" content="https://ideship.com/">
|
||||
<meta property="og:type" content="website">
|
||||
<script src="assets/js/og-meta.js"></script>
|
||||
|
||||
<!-- Favicons(logo1 已入库;favicon.png 由 postbuild 同步) -->
|
||||
<link href="assets/img/logo1.png" rel="icon" type="image/png">
|
||||
<link href="assets/img/apple-touch-icon.png" rel="apple-touch-icon">
|
||||
|
||||
<!-- Vendor CSS Files -->
|
||||
<link href="assets/vendor/bootstrap/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="assets/vendor/bootstrap-icons/bootstrap-icons.css" rel="stylesheet">
|
||||
<link href="assets/vendor/fontawesome-free/css/all.min.css" rel="stylesheet">
|
||||
|
||||
<!-- Template Main CSS File -->
|
||||
<link href="assets/css/main.css" rel="stylesheet">
|
||||
<link href="assets/css/ai-advisor.css?v=20260904g" rel="stylesheet">
|
||||
|
||||
</head>
|
||||
|
||||
<body class="command-home">
|
||||
|
||||
<!-- ======= Header ======= -->
|
||||
<header id="header" class="header d-flex align-items-center fixed-top">
|
||||
<div class="container-fluid container-xl d-flex align-items-center justify-content-between">
|
||||
|
||||
<a href="/" class="logo d-flex align-items-center">
|
||||
<img src="assets/img/logo1.png" alt="IDE logo">
|
||||
<span class="brand-lockup">
|
||||
<h1>IDE</h1>
|
||||
<small class="brand-fullname">Immediate Delivery Express</small>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<i class="mobile-nav-toggle mobile-nav-show bi bi-list"></i>
|
||||
<i class="mobile-nav-toggle mobile-nav-hide d-none bi bi-x"></i>
|
||||
<nav id="navbar" class="navbar">
|
||||
|
||||
<ul>
|
||||
<li class="site-nav-dock">
|
||||
<a href="/" class="active">
|
||||
<span data-i18n-key="Home">Home</span>
|
||||
</a>
|
||||
<a href="#routes">
|
||||
<span data-i18n-key="Routes">Routes</span>
|
||||
</a>
|
||||
<a href="#services">
|
||||
<span data-i18n-key="Services">Services</span>
|
||||
</a>
|
||||
<a href="#pricing">
|
||||
<span data-i18n-key="Pricing">Pricing</span>
|
||||
</a>
|
||||
<a href="#about">
|
||||
<span data-i18n-key="About">About</span>
|
||||
</a>
|
||||
<a href="#contact">
|
||||
<span data-i18n-key="Contact">Contact</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="site-nav-tools">
|
||||
<button type="button" id="language-toggle-btn" class="language-toggle-link" aria-label="Switch language">
|
||||
<i class="bi bi-translate language-toggle-icon" aria-hidden="true"></i>
|
||||
<span class="language-toggle__next">EN</span>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</nav><!-- .navbar -->
|
||||
|
||||
</div>
|
||||
</header><!-- End Header -->
|
||||
|
||||
<!-- Vendor JS Files -->
|
||||
<script src="assets/vendor/bootstrap/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="assets/vendor/d3/d3.min.js"></script>
|
||||
<script src="assets/vendor/topojson/topojson-client.min.js"></script>
|
||||
|
||||
<!-- Template Main JS File -->
|
||||
<script src="assets/js/main.js"></script>
|
||||
<script src="assets/js/stage-globe.js?v=20260824an"></script>
|
||||
<script src="assets/js/ai-advisor.js?v=20260904d"></script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@ -1,52 +0,0 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const root = path.join(__dirname, '..');
|
||||
const js = fs.readFileSync(path.join(root, 'assets/js/main.js'), 'utf8');
|
||||
const mapMatch = js.match(/const textMap = \{([\s\S]*?)\n \};/);
|
||||
const keys = new Set();
|
||||
const re = /^\s*,?'((?:\\'|[^'])*)':/gm;
|
||||
let m;
|
||||
while ((m = re.exec(mapMatch[1]))) {
|
||||
keys.add(m[1].replace(/\\'/g, "'"));
|
||||
}
|
||||
|
||||
const files = [];
|
||||
function walk(dir) {
|
||||
for (const f of fs.readdirSync(dir)) {
|
||||
const p = path.join(dir, f);
|
||||
if (fs.statSync(p).isDirectory()) {
|
||||
if (f === 'vendor' || f === 'node_modules' || f === 'scripts') continue;
|
||||
walk(p);
|
||||
} else if (f.endsWith('.html')) files.push(p);
|
||||
}
|
||||
}
|
||||
walk(root);
|
||||
|
||||
const missingKeys = new Set();
|
||||
const missingLeaf = new Set();
|
||||
|
||||
for (const file of files) {
|
||||
const html = fs.readFileSync(file, 'utf8');
|
||||
const i18nKeys = [...html.matchAll(/data-i18n-key="([^"]+)"/g)].map((x) => x[1]);
|
||||
for (const t of i18nKeys) {
|
||||
if (!keys.has(t)) missingKeys.add(t);
|
||||
}
|
||||
|
||||
const stripped = html
|
||||
.replace(/<script[\s\S]*?<\/script>/gi, '')
|
||||
.replace(/<style[\s\S]*?<\/style>/gi, '');
|
||||
const leaf = [...stripped.matchAll(/>([^<>]{2,140})</g)]
|
||||
.map((x) => x[1].replace(/\s+/g, ' ').trim())
|
||||
.filter((t) => /^[A-Za-z0-9]/.test(t) && !/^\d+$/.test(t));
|
||||
|
||||
for (const t of new Set(leaf)) {
|
||||
if (t.includes('http') || t.includes('assets/') || t.includes('©') || t.includes('@')) continue;
|
||||
if (!keys.has(t) && !i18nKeys.includes(t)) missingLeaf.add(t);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('=== data-i18n-key not in textMap ===');
|
||||
[...missingKeys].sort().forEach((k) => console.log(k));
|
||||
console.log('\n=== leaf text not in textMap (sample) ===');
|
||||
[...missingLeaf].sort().slice(0, 60).forEach((k) => console.log(k));
|
||||
BIN
docs/9610订单导入模板.xlsx
Normal file
@ -197,12 +197,17 @@ font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
|
||||
|
||||
### 5.1 按钮
|
||||
|
||||
- 高度:`32px`,`border-radius: 4px`
|
||||
- 高度:`32px`,`border-radius: 4px`(抽屉/卡片内软操作可用 `12px`,与亚克力面板一致)
|
||||
- 图标按钮:`32px × 32px` 正方形
|
||||
- **相邻按钮间距**:统一 `8px`
|
||||
- 使用 `a-space :size="8"` 或 `gap: 8px` / `gap: var(--app-button-gap)`
|
||||
- 适用于工具栏、表格操作列、弹窗底部按钮组
|
||||
- 主操作使用 `type="primary"`
|
||||
- **推荐用法**
|
||||
- 主操作:`type="primary"`,或工具栏新增用 `app-btn-primary-soft`
|
||||
- 查询:`app-btn-search`
|
||||
- 次要/取消:默认 `a-button`(无 type)或抽屉内与面板同风格的软按钮
|
||||
- 表格行内操作:优先图标按钮 / `type="text"`(见操作列规范)
|
||||
- **禁止**:Arco `type="outline"` 默认描边按钮(灰硬边框、白底,与主题亚克力/软主色体系完全不搭)。新页面与改版时一律不要使用;次要操作改用上文推荐样式。
|
||||
|
||||
### 5.2 输入类(Input / Select / Picker)
|
||||
|
||||
@ -541,7 +546,7 @@ const { t } = useI18n();
|
||||
- [ ] 白卡片与 **`.list-wrap`** 内边距勿重复叠加(推荐白卡片 `padding: 0` + 分区 padding)
|
||||
- [ ] **`.list-wrap`**:`padding: 16px`、`flex: 1; min-height: 0`;需测高时 `ref` 挂在本层
|
||||
- [ ] 工具栏使用 `table-toolbar`、`toolbar-left`、`toolbar-right`,工具栏 **`padding: 16px 16px 0`**
|
||||
- [ ] 按钮、输入框符合全局样式(高度、圆角)
|
||||
- [ ] 按钮、输入框符合全局样式(高度、圆角);未使用 `type="outline"` 描边按钮
|
||||
- [ ] 使用主题色变量,不硬编码 `#1d7281`
|
||||
- [ ] 弹窗 / 抽屉使用 ModalPro / DrawerPro + useModal
|
||||
- [ ] 表单使用 FormPro 或统一 label/span
|
||||
@ -562,7 +567,7 @@ const { t } = useI18n();
|
||||
|
||||
**约定:**
|
||||
|
||||
1. 官网静态页占用 **`/`** 与 **`/9610.html`**(`dist/site/index.html`);客户登录为 **`/login`**,管理端为 **`/admin`**。勿再把 `/login` 重定向到 `/`。
|
||||
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
|
||||
@ -574,7 +579,7 @@ location ^~ /order/batchJobs {
|
||||
|
||||
3. 修改后执行 `nginx -t && nginx -s reload`(或重建前端 Docker 镜像使 `nginx-docker.conf` 生效)。
|
||||
|
||||
4. 本地 Vite 开发不受此限制;**仅在部署环境**需遵守本节前缀规则。`pnpm dev` 时 `/` 会跳到 `/login`,官网页请直接打开 `IDE官网/`。
|
||||
4. 本地 Vite 开发不受此限制;**仅在部署环境**需遵守本节前缀规则。`pnpm dev` 时 `/` 即为官网。
|
||||
|
||||
当前订单模块已配置的前端路径示例:`/order/priceCheck`、`/order/placeOrder`、`/order/myOrders`、`/order/batchJobs`。
|
||||
|
||||
|
||||
44
index.html
@ -15,8 +15,13 @@
|
||||
path === '/track/query' ||
|
||||
path.indexOf('/track/query/') === 0;
|
||||
|
||||
/* 手机/微信访问客户登录:首屏前直达 H5。生产环境 / 是官网,不会走到这份 HTML */
|
||||
if (path === '/' || path === '/login') {
|
||||
if (path === '/' || path === '') {
|
||||
document.documentElement.classList.remove('app-booting');
|
||||
document.documentElement.classList.add('app-boot-site');
|
||||
}
|
||||
|
||||
/* 手机/微信访问客户登录:首屏前直达 H5。官网 `/` 走 Vue 站点,不要跳到 H5。 */
|
||||
if (path === '/login') {
|
||||
try {
|
||||
var forcePc =
|
||||
sessionStorage.getItem('oms_force_desktop') === '1' ||
|
||||
@ -51,14 +56,24 @@
|
||||
|
||||
var isH5 = isH5Path;
|
||||
var isEn = false;
|
||||
var pageBg = '#f0f3f4';
|
||||
try {
|
||||
var locale =
|
||||
localStorage.getItem('arco-locale') || localStorage.getItem('locale') || '';
|
||||
isEn = /^en/i.test(locale);
|
||||
var storedBg = localStorage.getItem('app-page-background-color');
|
||||
if (storedBg && /^#?[0-9a-fA-F]{6}$/.test(storedBg)) {
|
||||
pageBg = storedBg.charAt(0) === '#' ? storedBg : '#' + storedBg;
|
||||
}
|
||||
} catch (e) {}
|
||||
var root = document.documentElement;
|
||||
if (isH5) root.classList.add('app-boot-h5');
|
||||
if (isEn) root.classList.add('app-boot-en');
|
||||
/* 加载页与 OMS/官网共用页面背景配置,首屏前写入避免闪色 */
|
||||
root.style.setProperty('--app-page-bg', pageBg);
|
||||
root.style.setProperty('--kc-page-bg', pageBg);
|
||||
root.style.setProperty('--ai-bg', pageBg);
|
||||
root.style.setProperty('--ai-canvas', pageBg);
|
||||
try {
|
||||
document.title = isH5
|
||||
? isEn
|
||||
@ -66,6 +81,14 @@
|
||||
: 'IDE客户中心'
|
||||
: 'IDE I Faster & Simpler';
|
||||
} catch (e2) {}
|
||||
if (path === '/login') {
|
||||
var plane = document.createElement('link');
|
||||
plane.rel = 'preload';
|
||||
plane.as = 'image';
|
||||
plane.href = '/login-assets/hero-plane.webp';
|
||||
plane.type = 'image/webp';
|
||||
document.head.appendChild(plane);
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<link rel="icon" type="image/png" href="/favicon.png" />
|
||||
@ -87,7 +110,10 @@
|
||||
* 常驻 stable 会在右侧留出约 15px 空缝,贯穿 fixed 顶栏。
|
||||
*/
|
||||
html {
|
||||
background: #f5f4f1;
|
||||
background: var(--app-page-bg, #f0f3f4);
|
||||
}
|
||||
html.app-boot-site #app-boot {
|
||||
display: none !important;
|
||||
}
|
||||
html.app-booting {
|
||||
scrollbar-gutter: stable;
|
||||
@ -102,7 +128,7 @@
|
||||
#app {
|
||||
margin: 0;
|
||||
min-height: 100%;
|
||||
background: #f5f4f1;
|
||||
background: var(--app-page-bg, #f0f3f4);
|
||||
}
|
||||
#app-boot {
|
||||
position: fixed;
|
||||
@ -117,7 +143,7 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #f5f4f1;
|
||||
background: var(--app-page-bg, #f0f3f4);
|
||||
/*
|
||||
* 必须与 global.less 首屏后状态一致:
|
||||
* Vue 加载后 html 会带上 antialiased,若 boot 首帧没有,中文会突然变细像「字号缩小」。
|
||||
@ -256,9 +282,9 @@
|
||||
#app-boot.is-timeout .boot-sub {
|
||||
color: #86909c;
|
||||
}
|
||||
/* H5:样式挂在 html.app-boot-h5 上,首屏即正确,不再二次改字号/Logo */
|
||||
/* H5:样式挂在 html.app-boot-h5 上,首屏即正确,不再二次改字号/Logo;背景随页面背景配置 */
|
||||
html.app-boot-h5 {
|
||||
background: #f5f6f8;
|
||||
background: var(--app-page-bg, #f0f3f4);
|
||||
}
|
||||
html.app-boot-h5.app-booting {
|
||||
scrollbar-gutter: stable;
|
||||
@ -266,10 +292,10 @@
|
||||
html.app-boot-h5,
|
||||
html.app-boot-h5 body,
|
||||
html.app-boot-h5 #app {
|
||||
background: #f5f6f8;
|
||||
background: var(--app-page-bg, #f0f3f4);
|
||||
}
|
||||
html.app-boot-h5 #app-boot {
|
||||
background: #f5f6f8;
|
||||
background: var(--app-page-bg, #f0f3f4);
|
||||
padding: env(safe-area-inset-top, 0) 24px env(safe-area-inset-bottom, 0);
|
||||
}
|
||||
html.app-boot-h5 #app-boot .boot-panel {
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
# 3. HTTPS:证书部署到服务器(勿提交 .key 到 git)
|
||||
# /etc/nginx/ssl/oms.nanafa.com.pem
|
||||
# /etc/nginx/ssl/oms.nanafa.com.key
|
||||
# 4. 官网在 /(dist/site/index.html);客户登录 /login;管理端 /admin
|
||||
# 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})
|
||||
@ -43,9 +43,9 @@ server {
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
|
||||
# ============ 官网首页(静态站,不进 Vue SPA)============
|
||||
# ============ 官网首页(Vue SPA)============
|
||||
location = / {
|
||||
try_files /site/index.html =404;
|
||||
try_files /index.html =200;
|
||||
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
||||
add_header Pragma "no-cache";
|
||||
add_header Expires "0";
|
||||
@ -106,7 +106,7 @@ server {
|
||||
}
|
||||
|
||||
# ============ API 前端路由(必须先于 /api/ 代理)============
|
||||
location ~ ^/api/(upstreamInterface|upstreamInterfaceConfig|upstreamInterfaceConfigEdit|downstreamInterface(|/newPort)|appList(|/createApplication)|requestLog|callStatistics|apiDebugger|apiTest)(/|$) {
|
||||
location ~ ^/api/(upstreamInterface|upstreamInterfaceConfig|upstreamInterfaceConfigEdit|downstreamInterface(|/newPort)|appList(|/createApplication)|requestLog|callStatistics|apiDebugger|apiTest|openApi)(/|$) {
|
||||
try_files /index.html =200;
|
||||
}
|
||||
|
||||
|
||||
@ -11,7 +11,7 @@ server {
|
||||
root /usr/share/nginx/html;
|
||||
|
||||
location = / {
|
||||
try_files /site/index.html =404;
|
||||
try_files /index.html =200;
|
||||
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
||||
add_header Pragma "no-cache";
|
||||
add_header Expires "0";
|
||||
@ -69,7 +69,7 @@ server {
|
||||
try_files /index.html =200;
|
||||
}
|
||||
|
||||
location ~ ^/api/(upstreamInterface|upstreamInterfaceConfig|upstreamInterfaceConfigEdit|downstreamInterface(|/newPort)|appList(|/createApplication)|requestLog|callStatistics|apiDebugger|apiTest)(/|$) {
|
||||
location ~ ^/api/(upstreamInterface|upstreamInterfaceConfig|upstreamInterfaceConfigEdit|downstreamInterface(|/newPort)|appList(|/createApplication)|requestLog|callStatistics|apiDebugger|apiTest|openApi)(/|$) {
|
||||
try_files /index.html =200;
|
||||
}
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
# k3s 方案 A:仅静态 SPA。API 由浏览器直连 api.ideship.com(本文件不做反代)。
|
||||
# k3s 方案 A:仅静态 SPA。API 由浏览器按域名直连(prod: api.ideship.com / test: api.test.ideship.com)。
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
@ -11,11 +11,7 @@ server {
|
||||
root /usr/share/nginx/html;
|
||||
|
||||
location = / {
|
||||
# 官网优先;缺失时回退 SPA,避免 readiness 探针 404
|
||||
try_files /site/index.html /index.html =404;
|
||||
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
||||
add_header Pragma "no-cache";
|
||||
add_header Expires "0";
|
||||
try_files $uri @spa;
|
||||
}
|
||||
|
||||
location = /9610.html {
|
||||
@ -70,7 +66,7 @@ server {
|
||||
try_files $uri @spa;
|
||||
}
|
||||
|
||||
location ~ ^/api/(upstreamInterface|upstreamInterfaceConfig|upstreamInterfaceConfigEdit|downstreamInterface(|/newPort)|appList(|/createApplication)|requestLog|callStatistics|apiDebugger|apiTest)(/|$) {
|
||||
location ~ ^/api/(upstreamInterface|upstreamInterfaceConfig|upstreamInterfaceConfigEdit|downstreamInterface(|/newPort)|appList(|/createApplication)|requestLog|callStatistics|apiDebugger|apiTest|openApi)(/|$) {
|
||||
try_files $uri @spa;
|
||||
}
|
||||
|
||||
@ -128,6 +124,26 @@ server {
|
||||
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; }
|
||||
|
||||
@ -11,11 +11,12 @@
|
||||
"preinstall": "node -e \"if (!process.env.npm_config_user_agent || !process.env.npm_config_user_agent.includes('pnpm')) { console.error('Please use pnpm to install dependencies.'); process.exit(1); }\"",
|
||||
"generate:order-template": "node scripts/generate-order-template.js",
|
||||
"dev": "vite",
|
||||
"dev:test": "vite --mode nanafa",
|
||||
"dev:ideship": "vite --mode ideship",
|
||||
"dev:test": "vite --mode ideship-test",
|
||||
"dev:nanafa": "vite --mode nanafa",
|
||||
"build": "cross-env BROWSERSLIST_IGNORE_OLD_DATA=1 node --max-old-space-size=4096 node_modules/vite/bin/vite.js build",
|
||||
"build:prod": "cross-env BROWSERSLIST_IGNORE_OLD_DATA=1 node --max-old-space-size=4096 node_modules/vite/bin/vite.js build --mode production",
|
||||
"build:test": "cross-env BROWSERSLIST_IGNORE_OLD_DATA=1 node --max-old-space-size=4096 node_modules/vite/bin/vite.js build --mode nanafa",
|
||||
"build:test": "cross-env BROWSERSLIST_IGNORE_OLD_DATA=1 node --max-old-space-size=4096 node_modules/vite/bin/vite.js build --mode ideship-test",
|
||||
"build:nanafa": "cross-env BROWSERSLIST_IGNORE_OLD_DATA=1 node --max-old-space-size=4096 node_modules/vite/bin/vite.js build --mode nanafa",
|
||||
"postbuild": "node scripts/postbuild-cleanup-dist.js",
|
||||
"report": "cross-env REPORT=true pnpm run build",
|
||||
"preview": "npm run build && vite preview --host",
|
||||
|
||||
@ -7,10 +7,9 @@
|
||||
<meta name="description" content="IDE 9610跨境电商出口指南:报关、退税与专线物流怎么选,一看就懂。">
|
||||
<meta property="og:title" content="9610跨境电商出口方案选择指南 | IDE国际物流">
|
||||
<meta property="og:description" content="IDE 9610跨境电商出口指南:报关、退税与专线物流怎么选,一看就懂。">
|
||||
<meta property="og:image" content="https://ideship.com/site/assets/img/og-cover.png">
|
||||
<meta property="og:image" content="https://ideship.com/site/img/og-cover.png">
|
||||
<meta property="og:url" content="https://ideship.com/9610.html">
|
||||
<meta property="og:type" content="website">
|
||||
<script src="assets/js/og-meta.js"></script>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
@ -552,9 +551,7 @@
|
||||
var btn = document.getElementById('site-login-btn');
|
||||
if (!btn) return;
|
||||
var path = (location.pathname || '').replace(/\\/g, '/');
|
||||
var port = location.port;
|
||||
var standalone = location.protocol === 'file:' || /\/IDE官网(\/|$)/.test(path) ||
|
||||
(port && ['80', '443', '5173', '4173'].indexOf(port) < 0);
|
||||
var standalone = location.protocol === 'file:';
|
||||
var origin = standalone
|
||||
? (location.protocol === 'file:' ? 'http:' : location.protocol) + '//' + (location.hostname || '127.0.0.1') + ':5173'
|
||||
: location.origin;
|
||||
@ -622,7 +619,7 @@
|
||||
<span class="sol-badge">合规推荐</span>
|
||||
</div>
|
||||
<ul class="sol-list">
|
||||
<li>报关方式:简易 / 汇总均可</li>
|
||||
<li>报关方式:汇总申报(正式报关单)</li>
|
||||
<li>HS编码:前4位即可</li>
|
||||
<li>核心凭证:综服平台免税登记记录</li>
|
||||
<li>操作平台:单一窗口 + 综服平台</li>
|
||||
@ -679,7 +676,7 @@
|
||||
<tr>
|
||||
<td>报关方式</td>
|
||||
<td class="col-refund">汇总申报</td>
|
||||
<td class="col-exempt">简易 / 汇总均可</td>
|
||||
<td class="col-exempt">汇总申报(正式报关单)</td>
|
||||
<td style="color:#5a6472;">统一简易申报</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 17 KiB |
BIN
public/login-assets/hero-truck.png
Normal file
|
After Width: | Height: | Size: 158 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
BIN
public/site/img/hero-plane-3d.webp
Normal file
|
After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
BIN
public/site/img/partners/1688.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
1
public/site/img/partners/aliexpress.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>AliExpress</title><path fill="#E62E04" d="M5.166 9.096a.022.022 0 0 0-.022.021c0 .396-.32.717-.713.717a.021.021 0 0 0-.021.022c0 .012.01.021.021.021.394 0 .713.322.713.718 0 .012.01.021.022.021.011 0 .021-.01.021-.021A.717.717 0 0 1 5.9 9.88a.021.021 0 0 0 0-.043.716.716 0 0 1-.713-.718v-.002a.021.021 0 0 0-.006-.015.022.022 0 0 0-.015-.006zm-3.693.526L0 13.462h.48l.355-.922h1.782l.354.922h.481L1.98 9.622zm2.264.002v3.838h.491V9.624zm2.375 0v3.838h2.413v-.502H6.613v-1.19H8.19v-.477H6.613v-1.166h1.773v-.502zm-4.386.592l.698 1.82H1.028zm14.689.402a1.466 1.466 0 0 0-.966.366V10.7h-.491v2.763h.49c.002-.477 0-.955.002-1.433a.969.969 0 0 1 .965-.918zm4.18.007c-.053 0-.105.003-.158.01-.315.031-.606.175-.753.377a.689.689 0 0 0-.14.465c.007.2.066.357.233.496.184.147.42.2.657.259.311.067.426.095.546.186.08.07.133.127.136.27 0 .25-.221.372-.42.41a.89.89 0 0 1-.894-.344l-.371.288c.33.382.777.505 1.09.5.54-.01.891-.217 1.029-.534.066-.153.063-.309.063-.38a.677.677 0 0 0-.267-.545c-.228-.177-.583-.228-.636-.242-.437-.078-.658-.196-.697-.341-.043-.192.102-.35.297-.411a.76.76 0 0 1 .857.277l.367-.247a1.166 1.166 0 0 0-.939-.494zm2.387 0c-.052 0-.105.003-.157.01-.316.031-.607.175-.753.377a.689.689 0 0 0-.14.465c.006.2.065.357.233.496.183.147.42.2.657.259.31.067.426.095.545.186.081.07.134.127.136.27.001.25-.221.372-.42.41a.89.89 0 0 1-.894-.344l-.371.288c.33.382.777.505 1.09.5.541-.01.891-.217 1.03-.534.065-.153.062-.309.062-.38a.677.677 0 0 0-.267-.545c-.227-.177-.583-.228-.636-.242-.437-.078-.658-.196-.696-.341-.043-.192.101-.35.297-.411a.76.76 0 0 1 .857.277l.367-.247a1.167 1.167 0 0 0-.94-.494zm-9.84.002a1.461 1.461 0 0 0-1.42 1.117 1.305 1.305 0 0 0-.041.327v2.833h.491v-1.813c.17.18.487.42.96.454a1.447 1.447 0 0 0 1.208-.627 1.457 1.457 0 0 0-1.199-2.292zm4.804 0a1.448 1.448 0 0 0-1.288 2.08c.255.53.811.87 1.412.833a1.452 1.452 0 0 0 1.012-.51l-.363-.291a.968.968 0 0 1-1.106.273 1.01 1.01 0 0 1-.602-.69h2.239l.002-.427a1.295 1.295 0 0 0-1.306-1.268zm-9.2.08l1.062 1.377-1.062 1.378h.581l.779-1.01.778 1.01h.581l-1.062-1.378 1.062-1.378h-.581l-.778 1.01-.779-1.01zm-3.825.015v2.74h.49v-2.74zm8.233.37a.96.96 0 0 1 .95.993.963.963 0 0 1-.863.998.962.962 0 0 1-1.034-.739c-.074-.382 0-.746.307-1.019a.959.959 0 0 1 .64-.233zm4.79.015a.823.823 0 0 1 .819.755h-1.76a.964.964 0 0 1 .94-.755z"/></svg>
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 916 B After Width: | Height: | Size: 916 B |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
1
public/site/img/partners/icbu.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>ICBU</title><path fill="#FF6A00" d="M14.391 16.22c-.963.044-.865-.459-.302-1.234 1.32-1.768 3.82-4.236 3.906-5.982.151-2.283-2.143-3.026-4.501-3.004-1.645.022-3.344.492-4.501.906C5 8.315 2.489 10.576.909 13.076-.768 15.554-.216 17.923 3.322 18c2.716-.109 4.48-.862 6.32-1.802.01 0-5.086 1.453-6.958.383l-.008-.002c-.193-.11-.404-.264-.457-.683-.012-.885 1.46-1.802 2.283-2.097v-1.533a5.374 5.374 0 0 0 1.955.366 5.378 5.378 0 0 0 3.472-1.265c.037.13.056.278.044.447h.371c.048-.394-.172-.706-.172-.706-.333-.529-.915-.52-.915-.52s.315.137.529.466a4.953 4.953 0 0 1-4.665.932l1.21-1.2-.336-.874c2.435-.852 4.48-1.507 7.812-2.085l-.746-.624.389-.24c2.01.568 3.325.985 3.253 2.051a2.672 2.672 0 0 1-.202.611c-.584 1.158-2.326 3.09-3.029 3.898-.465.535-.92 1.06-1.245 1.562-.335.503-.54.971-.551 1.42.043 3.504 10.334-1.64 12.324-3.003-2.943 1.266-6.113 2.489-9.609 2.718Z"/></svg>
|
||||
|
After Width: | Height: | Size: 955 B |
1
public/site/img/partners/jd.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" role="img"><title>JD</title><rect width="100" height="100" rx="22" fill="#E1251B"/><g transform="translate(4 8) scale(.9)"><path fill="#EDEDED" d="M89.6,41.1c0.7-0.4,1.2-0.9,1.9-1.3c0.7-1.9,1.5-3.7,2.2-5.6c1.1-2.8,1.5-5.8,1.4-8.8c-0.1-2.5-1.2-4.4-3.2-5.9c-1.5-1.1-3.1-1.8-4.8-2.4c-4.2-1.4-8.6-1.7-13.1-1.8c-4-0.1-8,0.1-11.9,0.7c-2.9,0.4-5.8,0.8-8.7,1.5c-2.8,0.7-5.5,1.6-8.2,2.6c-4.6,1.6-9.1,3.5-13.3,5.9c-3.1,1.7-6,3.9-8.2,6.7c-0.4,0.5-0.6,0.5-1.2,0.2c-1.7-1-3.5-2-5.3-2.9c-2.1-1-4-0.3-4.7,1.9c-0.4,1.3-0.8,2.7-1.2,4c-0.8,2.9-1.8,5.7-3.4,8.3c-1.2,1.9-2.1,4-2.6,6.3c-0.8,3.1,2.5,5.7,4.9,4.8c1.2-0.5,2.5-0.8,3.6-1.3c1.9-0.8,3.7-2,5.1-3.5c1.6-1.7,2.3-3.8,2.3-6c0-1.3-0.1-2.5-0.2-3.8c-0.1-0.7-0.2-1.4-0.3-2c0-0.1,0.1-0.4,0.2-0.4c0.4,0,1,0.6,1.1,1c0.2,0.7,0.4,1.5,0.7,2.2c1.1,2.6,3,4.6,5,6.4c4,3.6,8.5,6.5,13.4,8.7c4.5,2,9.1,3.6,13.9,4.4c0.3,0.2,1.4,0.8,0.5,2.3c-0.1,0.1-0.2,0.3-0.3,0.4c-1.3,2.2-2.6,4.5-4,6.7c-0.4,0.7-0.8,1.6-1.4,2.1c-1.1,1-3.8,0.5-4.6-0.8c-0.4-0.6-0.5-1.4-0.7-2.1c0-0.1-0.1-0.2-0.2-0.3c-0.1,0.1-0.1,0.1-0.2,0.2c-0.2,1.5-0.3,2.9,0.7,4.1c0.7,0.9,1.7,1.4,2.8,1.7c0.4,0.1,0.4,0.2,0.2,0.6c-0.9,1.5-1.8,3.1-2.7,4.6c-0.7,1.2-1.4,2.3-2,3.5c-0.1,0.1-0.1,0.4,0,0.5c0.5,0.2,1.7-0.1,2.1-0.5c1.3-1.4,2.5-2.7,3.8-4.1c1.1-1.2,2.1-2.4,3.6-3.1c0.9-0.5,1-0.4,1,0.6c-0.1,2.2-0.2,4.3-0.3,6.5c0,0.5,0.1,0.7,0.6,0.6c0.4,0,0.7,0,1.1,0c0.4,0,0.6-0.1,0.7-0.5c0.5-1.6,0.9-3.2,1.5-4.7c0.4-1.1,1.1-2.1,1.8-3.1c0.7-0.9,1.8-0.8,2.5,0.1c0.3,0.4,0.6,0.9,0.7,1.4c0.5,1.7,0.9,3.4,1.4,5.1c0.1,0.6,0.1,1.3,0.5,1.6c0.4,0.3,1.1,0.1,1.7,0.1c0.4,0,0.5-0.1,0.5-0.5c0-3.5,0-7,0-10.5c0-3.1,0-6.3,0-9.4c0-0.6,0.2-1.3,0.4-1.8c0.3-0.6,0.8-1.4,1.1-1.5c0.2-0.1,0.4-0.1,0.6-0.2c4.5-1.4,8.7-3.5,12.3-6.5c3.6-3,6.6-6.5,9.1-10.5L89.6,41.1z"/><path fill="#AA201D" d="M66.2,63.6c0-0.6,0.2-1.1,0.4-1.6c0.3-0.6,0.8-1.4,1.1-1.5c-3.7,1.1-7.5,1.2-11.3,0.8c-0.5-0.1-1-0.1-1.5-0.2c0.3,0.2,1.4,0.8,0.5,2.3c-0.1,0.1-0.2,0.3-0.3,0.4c0.4,0.1,0.9,0.1,1.3,0.2C59.7,64.3,63,64.3,66.2,63.6z"/><path fill="#302F2D" d="M89.7,43.3c-3.2,1.7-6.7,2.4-10.3,2.6c-5.7,0.4-11.2-0.2-16.5-2.5c-3.5-1.5-6.4-3.7-9-6.4c-0.3-0.3-0.6-0.7-0.8-1.1c-0.4-0.7-0.4-1.3,0.1-1.6c0.4-0.3,1.1-0.1,1.6,0.4c2.8,3.1,6,5.7,10,7.1c6.3,2.2,12.7,2.7,19.2,1.2c2-0.4,3.9-1,5.7-2c0.7-0.4,1.2-0.9,1.9-1.3C90.8,41,90.3,42.1,89.7,43.3z"/><path fill="#302F2D" d="M77.5,22.8c-0.4-0.1-0.7-0.3-1-0.5c-0.9-0.6-1-1.7-0.3-2.5c0.3-0.3,0.7-0.6,1.1-0.8c4-1.9,8-2,12-0.4c0.4,0.2,0.7,0.4,1,0.7c0.4,0.4,0.4,0.9,0.2,1.5c-0.4,1.2-1.5,1.5-2.4,1.9c-1.2,0.4-3.2,1-5.4,0.9C82.3,23.6,79.2,23.4,77.5,22.8z"/><path fill="#302F2D" d="M54.9,28.8c-1.5,0-2.6-1.1-2.6-2.4c0-1.4,1.1-2.3,2.7-2.3c1.4,0,2.5,1,2.5,2.3C57.4,27.8,56.3,28.8,54.9,28.8z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
1
public/site/img/partners/taobao.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Taobao</title><path fill="#FF5000" d="M21.3099 9.9008c.5285 0 .9584.4284.9584.9589 0 .5276-.4299.958-.9584.958-.5276 0-.9585-.4304-.9585-.958 0-.5305.4309-.959.9585-.959zm2.3899 3.0462h-10.408v-.9595h4.15V9.7591h-2.8869v-.768h2.8868v-.9234h-2.508v.2034h-1.6418V5.3733h1.6418v.3497c.4945-.0607 1.463-.1814 2.5175-.2956v-.8257h1.8522v.6408c.9249-.0807 1.753-.1312 2.211-.1112 1.489.0716 2.4449.2816 2.485 1.273.0356.989-1.4244 1.9043-1.4244 1.9043l-.4509-.4338v.1929h-2.8116v.9233h3.229v.768h-3.229v2.2285h4.3873v.9595zM21.5259 7.299l-.0115-.0115s1.3722-.7595.3427-1.272c-.8633-.43-5.5346.3056-6.9234.6257v.6578h6.5922zM1.8822 6.4166c.5515 0 .9985-.449.9985-1 0-.5531-.447-.9995-.9985-.9995a.9984.9984 0 00-1.001.9995c0 .551.4463 1 1.001 1zm3.4094-.8596c.252-.4364.3717-.7195.3717-.7195l-1.466-.4123S3.6068 6.3546 2.5527 7.253c0 0 1.0195.5897 1.0095.5732a9.6444 9.6444 0 00.782-.8793c.2345-.1017.4585-.198.6794-.2876-.2715.487-.7094 1.219-1.1478 1.6809l.6178.5385s.4198-.4033.8792-.8907h.5246v.8993H3.8557v.7204h2.0416v1.7235c-.025 0-.0521 0-.0782-.002-.224-.0106-.5751-.0476-.7124-.265-.1678-.2636-.044-.7496-.0346-1.0457H3.6608l-.0496.026s-.517 2.3142 1.489 2.2621c1.8793.0521 2.9544-.523 3.4725-.9178l.2064.7645 1.1583-.4825-.785-1.9183-.941.292.1764.6574c-.2415.1809-.518.3157-.8187.4134V9.6076h1.995v-.7204h-1.995v-.8993h2.003v-.72h-3.557c.2565-.3111.4589-.5982.5095-.78L5.9058 6.32c2.6603-.9519 4.1408-.7886 4.1278.773v4.1128s.1568 1.4124-1.461 1.3107l-.8757-.188-.207.8307s3.7822 1.0812 4.0913-1.8246c.3096-2.9058-.0767-4.7576-.0767-4.7576s-.3451-2.6824-6.213-1.02zM.0582 12.1534l1.5867.9905c1.0967-2.3813 1.0265-2.0657 1.302-2.92.2832-.8737.3453-1.54-.1362-2.023-.6172-.6197-.6844-.6773-1.6017-1.3582L.5487 7.8562l1.2164.7576s.8141.4153.4274 1.1903c-.3617.7375-2.1343 2.3493-2.1343 2.3493zm19.94 6.8484s-.0186.523-.6704.523c-.5892 0-.6363-.4113-.6363-.4113-.2485.2996-.6207.4549-1.0696.4549-.7606 0-1.2961-.5135-1.2961-1.2896 0-.786.5576-1.28 1.3983-1.28.3832 0 .725.1462.9258.4037.0066-.0706.0136-.1362.0136-.1968 0-.5622-.3092-.8127-1.01-.8127-.3382 0-.6659.0486-1.0136.1408.1107-.219.1924-.3793.2971-.4574.1202-.0972.4294-.1408.9419-.1408 1.269 0 1.742.4259 1.742 1.4279v1.2024c0 .3161.033.4359.3772.4359zm-1.3272-.745c0-.4815-.2555-.747-.6378-.747-.4043 0-.6668.2825-.6668.7595 0 .467.2775.7615.6533.7615.3882 0 .6513-.275.6513-.774zm5.2706-.501c0 1.1528-.6924 1.8271-1.7786 1.8271-1.0957 0-1.7766-.6743-1.7766-1.8271 0-1.1584.6809-1.8252 1.7766-1.8252 1.0862 0 1.7786.6719 1.7786 1.8252zm-1.0867 0c0-.8693-.237-1.2997-.692-1.2997-.4734 0-.6938.4304-.6938 1.2997 0 .8662.2204 1.299.6939 1.299.467 0 .6919-.4328.6919-1.299zm-7.1313-.0441c0 1.1703-.6534 1.8567-1.5767 1.8567-.4233 0-.8056-.1633-1.0466-.47 0 0-.1082.4264-.6618.4264-.6884 0-.6644-.523-.6644-.523.3868.0165.3758-.2145.3758-.436v-2.8862c0-.3552-.0742-.4795-.4269-.4865.0136-.1072.0777-.5376.6789-.5376.8156 0 .7615.9149.7615.9149v.786c.229-.268.5516-.3942.9905-.3942.9624 0 1.5697.6447 1.5697 1.7495zm-1.0937.0806c0-.8983-.2716-1.3557-.7616-1.3557-.4393 0-.74.3968-.74 1.1047v.3602c0 .7205.3152 1.1168.7631 1.1168.477 0 .7385-.4134.7385-1.226zm-3.2425-.0365c0 1.1528-.688 1.8271-1.7806 1.8271-1.0937 0-1.7726-.6743-1.7726-1.8271 0-1.1584.6789-1.8252 1.7726-1.8252 1.0927 0 1.7806.6719 1.7806 1.8252zm-1.0832 0c0-.8693-.236-1.2997-.6974-1.2997-.467 0-.6874.4304-.6874 1.2997 0 .8662.2205 1.299.6874 1.299.469 0 .6974-.4328.6974-1.299zm-5.9895-2.716c-.268.0782-.7901.0972-1.5516.0972-.925 0-1.5391-.039-1.8543-.039-.5195 0-.7254.1342-.9088.6592.3006-.1017.6729-.1087 1.1353-.1087.3512 0 .4108.0431.4108.2941V18.846c0 .2821.1178.6789.7255.6789.7104 0 .8346-.526.8346-.526-.3552-.007-.4248-.1313-.4248-.4865V15.952c0-.2675.1012-.2795.471-.2795.1252 0 .2094.007.2605.007.545 0 .6883-.1047.9018-.6398zm3.1303 3.962s-.018.523-.6664.523c-.556 0-.6403-.4113-.6403-.4113-.248.2996-.6207.4549-1.0656.4549-.7655 0-1.2996-.5135-1.2996-1.2896 0-.786.557-1.28 1.3993-1.28.3843 0 .7274.1462.9258.4037.0036-.0706.0106-.1362.0106-.1968 0-.5622-.3051-.8127-1.007-.8127-.3382 0-.6669.0486-1.015.1408.1131-.219.1923-.3793.2945-.4574.1293-.0972.4329-.1408.9469-.1408 1.265 0 1.746.4259 1.746 1.4279v1.2024c0 .3161.0276.4359.3708.4359zm-1.3262-.745c0-.4815-.2555-.747-.6378-.747-.4013 0-.6668.2825-.6668.7595 0 .467.279.7615.6513.7615.3893 0 .6533-.275.6533-.774z"/></svg>
|
||||
|
After Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 358 B After Width: | Height: | Size: 358 B |
|
Before Width: | Height: | Size: 722 B After Width: | Height: | Size: 722 B |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.0 KiB |
@ -4,83 +4,15 @@ import { fileURLToPath } from 'node:url';
|
||||
|
||||
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const dist = path.join(root, 'dist');
|
||||
const siteSrc = path.join(root, 'IDE官网');
|
||||
const legacyLoginDir = path.join(dist, 'login');
|
||||
const siteDir = path.join(dist, 'site');
|
||||
|
||||
/** Avoid fs.cpSync merge into dist/assets (conflicts with Vite + can crash on Windows). */
|
||||
function copyDirSync(src, dest, options = {}) {
|
||||
const { skipDirNames = new Set() } = options;
|
||||
fs.mkdirSync(dest, { recursive: true });
|
||||
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
||||
if (entry.isDirectory() && skipDirNames.has(entry.name)) continue;
|
||||
const srcPath = path.join(src, entry.name);
|
||||
const destPath = path.join(dest, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
copyDirSync(srcPath, destPath, options);
|
||||
continue;
|
||||
}
|
||||
if (entry.isFile()) {
|
||||
fs.copyFileSync(srcPath, destPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 生产环境首页在 /,静态资源在 /site/assets/;勿用 base href(会把地址栏带到 /site/) */
|
||||
function rewriteSiteAssetPaths(content) {
|
||||
return content
|
||||
.replace(/((?:href|src)=["'])assets\//g, '$1/site/assets/')
|
||||
.replace(/(["'])assets\//g, '$1/site/assets/');
|
||||
}
|
||||
|
||||
if (fs.existsSync(legacyLoginDir)) {
|
||||
fs.rmSync(legacyLoginDir, { recursive: true, force: true });
|
||||
console.log('[postbuild] removed dist/login (conflicts with SPA route /login)');
|
||||
}
|
||||
|
||||
function copyOfficialSite() {
|
||||
if (!fs.existsSync(siteSrc) || !fs.existsSync(dist)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (fs.existsSync(siteDir)) {
|
||||
fs.rmSync(siteDir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
copyDirSync(siteSrc, siteDir, {
|
||||
skipDirNames: new Set(['.well-known']),
|
||||
});
|
||||
|
||||
for (const rel of ['index.html', '404.html', '9610.html']) {
|
||||
const filePath = path.join(siteDir, rel);
|
||||
if (!fs.existsSync(filePath)) continue;
|
||||
const html = fs.readFileSync(filePath, 'utf8');
|
||||
fs.writeFileSync(filePath, rewriteSiteAssetPaths(html), 'utf8');
|
||||
}
|
||||
|
||||
const jsDir = path.join(siteDir, 'assets', 'js');
|
||||
if (fs.existsSync(jsDir)) {
|
||||
for (const name of fs.readdirSync(jsDir)) {
|
||||
if (!name.endsWith('.js')) continue;
|
||||
const filePath = path.join(jsDir, name);
|
||||
const js = fs.readFileSync(filePath, 'utf8');
|
||||
if (!js.includes('assets/')) continue;
|
||||
fs.writeFileSync(filePath, rewriteSiteAssetPaths(js), 'utf8');
|
||||
}
|
||||
}
|
||||
|
||||
const src9610 = path.join(siteSrc, '9610.html');
|
||||
if (fs.existsSync(src9610)) {
|
||||
fs.copyFileSync(src9610, path.join(dist, '9610.html'));
|
||||
}
|
||||
|
||||
const faviconSrc = path.join(siteDir, 'assets', 'img', 'logo1.png');
|
||||
if (fs.existsSync(faviconSrc)) {
|
||||
fs.copyFileSync(faviconSrc, path.join(siteDir, 'assets', 'img', 'favicon.png'));
|
||||
const faviconSrc = path.join(dist, 'site', 'img', 'logo1.png');
|
||||
if (fs.existsSync(faviconSrc)) {
|
||||
fs.copyFileSync(faviconSrc, path.join(dist, 'favicon.png'));
|
||||
}
|
||||
|
||||
console.log('[postbuild] copied official site to dist/site (assets under /site/assets)');
|
||||
console.log('[postbuild] copied favicon.png from site logo');
|
||||
}
|
||||
|
||||
copyOfficialSite();
|
||||
|
||||
@ -19,6 +19,7 @@
|
||||
|
||||
function isStandalonePortalPath(path: string) {
|
||||
return (
|
||||
path === '/' ||
|
||||
path.startsWith('/h5') ||
|
||||
path.startsWith('/wechat/') ||
|
||||
path === '/track/query' ||
|
||||
|
||||
14
src/api/TrackPull/typings.d.ts
vendored
@ -90,6 +90,8 @@ declare namespace API {
|
||||
app?: string;
|
||||
/** 应用code */
|
||||
appKey?: string;
|
||||
/** 订单类型 */
|
||||
orderType?: number;
|
||||
/** 轨迹发生ISO时间参数名 */
|
||||
trackIsoTimeName?: string;
|
||||
/** 拉取轨迹发生时间 */
|
||||
@ -108,6 +110,14 @@ declare namespace API {
|
||||
orderPath?: string;
|
||||
/** 轨迹明细路径 */
|
||||
trackDetailsPath?: string;
|
||||
/** 开始轨迹代码 */
|
||||
startTrackCode?: string;
|
||||
/** 结束轨迹代码 */
|
||||
endTrackCode?: string;
|
||||
/** 开始前静默时间 */
|
||||
silenceBeforeStart?: number;
|
||||
/** 结束后静默时间 */
|
||||
silenceAfterEnd?: number;
|
||||
};
|
||||
|
||||
type GetTrackPullConfigPageOutputVO = {
|
||||
@ -116,10 +126,14 @@ declare namespace API {
|
||||
id?: number;
|
||||
/** 服务商名称 */
|
||||
serviceProvider?: string;
|
||||
/** 服务商code */
|
||||
serviceProviderCode?: string;
|
||||
/** 应用 */
|
||||
app?: string;
|
||||
/** 应用 */
|
||||
appKey?: string;
|
||||
/** 订单类型 */
|
||||
orderType?: number;
|
||||
};
|
||||
|
||||
type GetTrackPullConfigPageOutputVOPagedResult = {
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { axiosInstance } from '@/api/rbac'
|
||||
import { assertBusinessOk, omsUrl, parseListPayload, unwrapData } from '@/api/oms/common'
|
||||
import * as XLSX from 'xlsx'
|
||||
|
||||
function sanitizePayload(body?: Record<string, any>) {
|
||||
if (!body || typeof body !== 'object') return body
|
||||
@ -116,49 +117,144 @@ export async function postForewarningServiceKpiAreaGetKpiAreaInfos(
|
||||
export type KpiAreaInfoImportResult = {
|
||||
successCount: number
|
||||
failCount: number
|
||||
skipCount: number
|
||||
failures: Array<{ rowNumber: number; reason: string }>
|
||||
}
|
||||
|
||||
/** GET /channel/kpi-area/info/import-template */
|
||||
export async function downloadKpiAreaInfoImportTemplate(
|
||||
export type KpiAreaInfoExcelRow = {
|
||||
国家代码: string
|
||||
匹配维度: string
|
||||
匹配方式: string
|
||||
'州/省': string
|
||||
城市: string
|
||||
起始邮编: string
|
||||
截止邮编: string
|
||||
}
|
||||
|
||||
const KPI_AREA_ENUM_ROWS = [
|
||||
{ 枚举类型: '匹配维度', 编码: 'country', 名称: '仅国家' },
|
||||
{ 枚举类型: '匹配维度', 编码: 'zip', 名称: '邮编' },
|
||||
{ 枚举类型: '匹配维度', 编码: 'state', 名称: '州/省' },
|
||||
{ 枚举类型: '匹配维度', 编码: 'city', 名称: '城市' },
|
||||
{ 枚举类型: '匹配方式', 编码: 'prefix', 名称: '前缀' },
|
||||
{ 枚举类型: '匹配方式', 编码: 'exact', 名称: '精确' },
|
||||
{ 枚举类型: '匹配方式', 编码: 'range', 名称: '区间' },
|
||||
{ 枚举类型: '匹配方式', 编码: 'all', 名称: '不限' },
|
||||
]
|
||||
|
||||
const KPI_AREA_TEMPLATE_SAMPLE_ROWS: KpiAreaInfoExcelRow[] = [
|
||||
{
|
||||
国家代码: 'US',
|
||||
匹配维度: 'zip',
|
||||
匹配方式: 'range',
|
||||
'州/省': '',
|
||||
城市: '',
|
||||
起始邮编: '90000',
|
||||
截止邮编: '90199',
|
||||
},
|
||||
{
|
||||
国家代码: 'US',
|
||||
匹配维度: 'zip',
|
||||
匹配方式: 'prefix',
|
||||
'州/省': '',
|
||||
城市: '',
|
||||
起始邮编: '90',
|
||||
截止邮编: '',
|
||||
},
|
||||
{
|
||||
国家代码: 'US',
|
||||
匹配维度: 'state',
|
||||
匹配方式: 'exact',
|
||||
'州/省': 'CA',
|
||||
城市: '',
|
||||
起始邮编: '',
|
||||
截止邮编: '',
|
||||
},
|
||||
]
|
||||
|
||||
function writeKpiAreaInfoWorkbook(rows: KpiAreaInfoExcelRow[], fileName: string) {
|
||||
const dataRows =
|
||||
rows.length > 0
|
||||
? rows
|
||||
: [
|
||||
{
|
||||
国家代码: '',
|
||||
匹配维度: '',
|
||||
匹配方式: '',
|
||||
'州/省': '',
|
||||
城市: '',
|
||||
起始邮编: '',
|
||||
截止邮编: '',
|
||||
},
|
||||
]
|
||||
const wb = XLSX.utils.book_new()
|
||||
const templateSheet = XLSX.utils.json_to_sheet(dataRows)
|
||||
templateSheet['!cols'] = [
|
||||
{ wch: 12 },
|
||||
{ wch: 12 },
|
||||
{ wch: 12 },
|
||||
{ wch: 12 },
|
||||
{ wch: 12 },
|
||||
{ wch: 12 },
|
||||
{ wch: 12 },
|
||||
]
|
||||
const enumSheet = XLSX.utils.json_to_sheet(KPI_AREA_ENUM_ROWS)
|
||||
enumSheet['!cols'] = [{ wch: 12 }, { wch: 12 }, { wch: 12 }]
|
||||
XLSX.utils.book_append_sheet(wb, templateSheet, '导入模板')
|
||||
XLSX.utils.book_append_sheet(wb, enumSheet, '枚举值')
|
||||
XLSX.writeFile(wb, fileName.endsWith('.xlsx') ? fileName : `${fileName}.xlsx`)
|
||||
}
|
||||
|
||||
/** 前端生成导入模板(Sheet1 导入模板 / Sheet2 枚举值) */
|
||||
export function downloadKpiAreaInfoImportTemplate(
|
||||
areaType: 'blacklist' | 'remote' | 'price',
|
||||
fileName?: string
|
||||
) {
|
||||
const res = await axiosInstance.get(omsUrl('/channel/kpi-area/info/import-template'), {
|
||||
params: { areaType },
|
||||
responseType: 'blob',
|
||||
})
|
||||
const blob = res.data as Blob
|
||||
const ct = (res.headers['content-type'] || '').toString()
|
||||
if (ct.includes('application/json')) {
|
||||
const text = await blob.text()
|
||||
const json = JSON.parse(text)
|
||||
throw new Error(json.msg ?? json.message ?? json.Message ?? '下载失败')
|
||||
}
|
||||
const defaultNames: Record<string, string> = {
|
||||
blacklist: '黑名单分区明细导入模板.xlsx',
|
||||
remote: '偏远分区明细导入模板.xlsx',
|
||||
price: '价格分区明细导入模板.xlsx',
|
||||
}
|
||||
const name = fileName || defaultNames[areaType] || '分区明细导入模板.xlsx'
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = name.endsWith('.xlsx') ? name : `${name}.xlsx`
|
||||
a.style.display = 'none'
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
document.body.removeChild(a)
|
||||
URL.revokeObjectURL(url)
|
||||
writeKpiAreaInfoWorkbook(
|
||||
KPI_AREA_TEMPLATE_SAMPLE_ROWS,
|
||||
fileName || defaultNames[areaType] || '分区明细导入模板.xlsx'
|
||||
)
|
||||
}
|
||||
|
||||
export function exportKpiAreaInfoExcel(rows: KpiAreaInfoExcelRow[], fileName: string) {
|
||||
writeKpiAreaInfoWorkbook(rows, fileName)
|
||||
}
|
||||
|
||||
export function mapKpiAreaInfoToExcelRow(item: Record<string, unknown>): KpiAreaInfoExcelRow {
|
||||
const pick = (...keys: string[]) => {
|
||||
for (const key of keys) {
|
||||
const value = item[key]
|
||||
if (value != null && String(value).trim()) return String(value).trim()
|
||||
}
|
||||
return ''
|
||||
}
|
||||
return {
|
||||
国家代码: pick('countryCode', 'CountryCode'),
|
||||
匹配维度: pick('matchDim', 'MatchDim'),
|
||||
匹配方式: pick('matchMode', 'MatchMode'),
|
||||
'州/省': pick('province', 'Province'),
|
||||
城市: pick('city', 'City'),
|
||||
起始邮编: pick('leftPostcode', 'leftPostCode', 'LeftPostcode', 'LeftPostCode'),
|
||||
截止邮编: pick('rightPostcode', 'rightPostCode', 'RightPostcode', 'RightPostCode'),
|
||||
}
|
||||
}
|
||||
|
||||
export type KpiAreaInfoImportMode = 'incremental' | 'full'
|
||||
|
||||
/** POST /channel/kpi-area/info/import multipart */
|
||||
export async function importKpiAreaInfo(
|
||||
areaId: number,
|
||||
file: File
|
||||
file: File,
|
||||
importMode: KpiAreaInfoImportMode = 'incremental'
|
||||
): Promise<KpiAreaInfoImportResult> {
|
||||
const form = new FormData()
|
||||
form.append('areaId', String(areaId))
|
||||
form.append('importMode', importMode)
|
||||
form.append('file', file)
|
||||
const res = await axiosInstance.post(omsUrl('/channel/kpi-area/info/import'), form)
|
||||
assertBusinessOk(res.data)
|
||||
@ -170,6 +266,25 @@ export async function importKpiAreaInfo(
|
||||
return {
|
||||
successCount: Number(d.successCount ?? d.SuccessCount ?? 0),
|
||||
failCount: Number(d.failCount ?? d.FailCount ?? 0),
|
||||
skipCount: Number(d.skipCount ?? d.SkipCount ?? 0),
|
||||
failures,
|
||||
}
|
||||
}
|
||||
|
||||
/** 分区包版本列表(不含正文) */
|
||||
export async function getKpiAreaPublishList(areaId: number | string) {
|
||||
const res = await axiosInstance.get(omsUrl('/channel/kpi-area/publish/list'), {
|
||||
params: { areaId },
|
||||
})
|
||||
const data = unwrapData<any>(res.data)
|
||||
const items = Array.isArray(data) ? data : data?.items ?? data?.Items ?? []
|
||||
return items as Array<Record<string, any>>
|
||||
}
|
||||
|
||||
/** 单条分区包(含正文) */
|
||||
export async function getKpiAreaPublishRecord(id: number | string) {
|
||||
const res = await axiosInstance.get(omsUrl('/channel/kpi-area/publish/get'), {
|
||||
params: { id },
|
||||
})
|
||||
return unwrapData<Record<string, any>>(res.data) ?? res.data?.data ?? res.data
|
||||
}
|
||||
|
||||
@ -62,6 +62,11 @@ export async function updateSalesProduct(body: Record<string, any> & { id: numbe
|
||||
assertBusinessOk(res.data)
|
||||
}
|
||||
|
||||
export async function updateSalesProductCode(body: { id: number | string; productCode: string }) {
|
||||
const res = await axiosInstance.post(omsUrl('/channel/sales-product/product-code/update'), body)
|
||||
assertBusinessOk(res.data)
|
||||
}
|
||||
|
||||
export async function deleteSalesProduct(id: number | string) {
|
||||
const res = await axiosInstance.post(omsUrl('/channel/sales-product/delete'), { id })
|
||||
assertBusinessOk(res.data)
|
||||
@ -76,15 +81,29 @@ export async function saveSalesProductCustomerGuide(body: {
|
||||
assertBusinessOk(res.data)
|
||||
}
|
||||
|
||||
/** 卖价 */
|
||||
export async function getSalesProductPriceList(salesProductId: number | string) {
|
||||
/** 卖价列表;列表页请传 includeDetails:false,避免拉回整张价格矩阵导致卡顿 */
|
||||
export async function getSalesProductPriceList(
|
||||
salesProductId: number | string,
|
||||
opts?: { includeDetails?: boolean },
|
||||
) {
|
||||
const res = await axiosInstance.get(omsUrl('/channel/sales-product/price/list'), {
|
||||
params: { salesProductId },
|
||||
params: {
|
||||
salesProductId,
|
||||
includeDetails: opts?.includeDetails ?? true,
|
||||
},
|
||||
})
|
||||
const { items } = parseListPayload(res.data)
|
||||
return items ?? []
|
||||
}
|
||||
|
||||
/** 单条卖价(含 factors/details),编辑弹窗按需加载 */
|
||||
export async function getSalesProductPrice(id: number | string) {
|
||||
const res = await axiosInstance.get(omsUrl('/channel/sales-product/price/get'), {
|
||||
params: { id },
|
||||
})
|
||||
return unwrapData<any>(res.data) ?? res.data?.data ?? res.data
|
||||
}
|
||||
|
||||
export async function createSalesProductPrice(body: Record<string, any>) {
|
||||
const res = await axiosInstance.post(omsUrl('/channel/sales-product/price/create'), body)
|
||||
assertBusinessOk(res.data)
|
||||
@ -145,6 +164,8 @@ export async function getSalesProductPublishPriceList(params: {
|
||||
salesProductId: number | string
|
||||
page?: number
|
||||
pageSize?: number
|
||||
/** false 时后端不返回快照正文,用于版本头展示 */
|
||||
includeContent?: boolean
|
||||
}) {
|
||||
const res = await axiosInstance.get(omsUrl('/channel/sales-product/publish/price/list'), { params })
|
||||
return parseListPayload(res.data)
|
||||
@ -154,11 +175,21 @@ export async function getSalesProductPublishRulesList(params: {
|
||||
salesProductId: number | string
|
||||
page?: number
|
||||
pageSize?: number
|
||||
/** false 时只返回版本头,不带快照正文 */
|
||||
includeContent?: boolean
|
||||
}) {
|
||||
const res = await axiosInstance.get(omsUrl('/channel/sales-product/publish/rules/list'), { params })
|
||||
return parseListPayload(res.data)
|
||||
}
|
||||
|
||||
/** 单条发布记录(含快照正文) */
|
||||
export async function getSalesProductPublishRecord(id: number | string) {
|
||||
const res = await axiosInstance.get(omsUrl('/channel/sales-product/publish/get'), {
|
||||
params: { id },
|
||||
})
|
||||
return unwrapData<any>(res.data) ?? res.data?.data ?? res.data
|
||||
}
|
||||
|
||||
/** 预览即将发布的快照(不落库),用于发布弹窗与发布记录核对 */
|
||||
export async function getSalesProductPublishPreview(params: {
|
||||
salesProductId: number | string
|
||||
@ -168,6 +199,24 @@ export async function getSalesProductPublishPreview(params: {
|
||||
return unwrapData<Record<string, unknown>>(res.data)
|
||||
}
|
||||
|
||||
/** 发布同步态(轻量,不含快照正文) */
|
||||
export async function getSalesProductPublishSyncStatus(params: {
|
||||
salesProductId: number | string
|
||||
operatorType: 'rules' | 'price' | string
|
||||
}) {
|
||||
const res = await axiosInstance.get(omsUrl('/channel/sales-product/publish/sync-status'), { params })
|
||||
return unwrapData<{
|
||||
pending?: boolean
|
||||
Pending?: boolean
|
||||
onlineVersion?: string | null
|
||||
OnlineVersion?: string | null
|
||||
onlinePublishTime?: string | null
|
||||
OnlinePublishTime?: string | null
|
||||
pendingContentHash?: string | null
|
||||
onlineContentHash?: string | null
|
||||
}>(res.data)
|
||||
}
|
||||
|
||||
export async function publishSalesProduct(body: {
|
||||
salesProductId: number | string
|
||||
operatorType: string
|
||||
|
||||
@ -1,7 +1,22 @@
|
||||
import { axiosInstance } from '@/api/rbac'
|
||||
import { assertBusinessOk, omsUrl, parseListPayload } from '@/api/oms/common'
|
||||
import { assertBusinessOk, omsUrl, parseListPayload, unwrapData } from '@/api/oms/common'
|
||||
import { ENABLE_STATUS, isEnableStatusEnabled, toEnableStatus } from '@/utils/enableStatus'
|
||||
|
||||
/** 渠道下拉:全部名称,不分页 */
|
||||
export async function getServiceChannelOptions() {
|
||||
const res = await axiosInstance.get(omsUrl('/channel/service-channel/options'))
|
||||
const body = res.data
|
||||
if (Array.isArray(body)) return body
|
||||
assertBusinessOk(body)
|
||||
const raw = body?.data ?? body?.Data ?? body?.result ?? body?.Result ?? body
|
||||
if (Array.isArray(raw)) return raw
|
||||
if (raw && typeof raw === 'object') {
|
||||
const items = (raw as any).items ?? (raw as any).Items ?? (raw as any).list ?? (raw as any).List
|
||||
if (Array.isArray(items)) return items
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
export async function getServiceChannelList(params: {
|
||||
page?: number
|
||||
pageSize?: number
|
||||
@ -181,15 +196,30 @@ export async function deleteServiceChannelGroup(id: number | string) {
|
||||
assertBusinessOk(res.data)
|
||||
}
|
||||
|
||||
export async function getServiceChannelPriceList(serviceChannelId: number | string) {
|
||||
/** 买价列表;列表页请传 includeDetails:false,避免拉回整张价格矩阵导致卡顿 */
|
||||
export async function getServiceChannelPriceList(
|
||||
serviceChannelId: number | string,
|
||||
opts?: { includeDetails?: boolean },
|
||||
) {
|
||||
const res = await axiosInstance.get(omsUrl('/channel/service-channel/price/list'), {
|
||||
params: { serviceChannelId },
|
||||
params: {
|
||||
serviceChannelId,
|
||||
includeDetails: opts?.includeDetails ?? true,
|
||||
},
|
||||
})
|
||||
// 与 parseListPayload 一致:兼容 data 为数组、{ items }、{ list } 等
|
||||
const { items } = parseListPayload(res.data)
|
||||
return items
|
||||
}
|
||||
|
||||
/** 单条买价(含 factors/details),编辑弹窗按需加载 */
|
||||
export async function getServiceChannelPrice(id: number | string) {
|
||||
const res = await axiosInstance.get(omsUrl('/channel/service-channel/price/get'), {
|
||||
params: { id },
|
||||
})
|
||||
return unwrapData<any>(res.data) ?? res.data?.data ?? res.data
|
||||
}
|
||||
|
||||
export async function createServiceChannelPrice(body: Record<string, any>) {
|
||||
const res = await axiosInstance.post(omsUrl('/channel/service-channel/price/create'), body)
|
||||
assertBusinessOk(res.data)
|
||||
@ -249,6 +279,26 @@ export async function getServiceChannelPublishPreview(params: {
|
||||
return (body?.data ?? body?.Data ?? body) as Record<string, unknown>
|
||||
}
|
||||
|
||||
/** 发布同步态(轻量,不含快照正文),与产品卖价对称 */
|
||||
export async function getServiceChannelPublishSyncStatus(params: {
|
||||
serviceChannelId: number | string
|
||||
operatorType: 'rules' | 'price' | string
|
||||
}) {
|
||||
const res = await axiosInstance.get(omsUrl('/channel/service-channel/publish/sync-status'), { params })
|
||||
const body = res.data
|
||||
assertBusinessOk(body)
|
||||
return (body?.data ?? body?.Data ?? body) as {
|
||||
pending?: boolean
|
||||
Pending?: boolean
|
||||
onlineVersion?: string | null
|
||||
OnlineVersion?: string | null
|
||||
onlinePublishTime?: string | null
|
||||
OnlinePublishTime?: string | null
|
||||
pendingContentHash?: string | null
|
||||
onlineContentHash?: string | null
|
||||
}
|
||||
}
|
||||
|
||||
export async function publishServiceChannel(body: {
|
||||
serviceChannelId: number | string
|
||||
operatorType: string
|
||||
|
||||
35
src/api/customer/exportQualification.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { axiosInstance } from '@/api/rbac'
|
||||
import { assertBusinessOk, omsUrl, parseListPayload, unwrapData } from '@/api/oms/common'
|
||||
|
||||
function postList(path: string, body: Record<string, unknown>) {
|
||||
return axiosInstance.post(omsUrl(path), body).then((res) => parseListPayload(res.data))
|
||||
}
|
||||
|
||||
function postOk(path: string, body: Record<string, unknown>) {
|
||||
return axiosInstance.post(omsUrl(path), body).then((res) => {
|
||||
assertBusinessOk(res.data)
|
||||
return unwrapData(res.data)
|
||||
})
|
||||
}
|
||||
|
||||
function getData(path: string, params?: Record<string, unknown>) {
|
||||
return axiosInstance.get(omsUrl(path), { params }).then((res) => unwrapData(res.data))
|
||||
}
|
||||
|
||||
export const getExportQualificationList = (body: Record<string, unknown>) =>
|
||||
postList('/customer/export-qualification/list', body)
|
||||
|
||||
export const getExportQualificationDetail = (id: number) =>
|
||||
getData('/customer/export-qualification/detail', { id })
|
||||
|
||||
export const createExportQualification = (body: Record<string, unknown>) =>
|
||||
postOk('/customer/export-qualification/create', body)
|
||||
|
||||
export const updateExportQualification = (body: Record<string, unknown>) =>
|
||||
postOk('/customer/export-qualification/update', body)
|
||||
|
||||
export const deleteExportQualification = (id: number) =>
|
||||
postOk('/customer/export-qualification/delete', { id })
|
||||
|
||||
export const setDefaultExportQualification = (id: number) =>
|
||||
postOk('/customer/export-qualification/set-default', { id })
|
||||
@ -101,6 +101,21 @@ export interface CustomerServiceAdvisorDto {
|
||||
wecomKfUrl?: string
|
||||
}
|
||||
|
||||
/** 客户 OpenAPI 机机凭证 */
|
||||
export interface CustomerOpenApiCredentialDto {
|
||||
id?: number
|
||||
customerId?: number
|
||||
appKey?: string
|
||||
appSecret?: string
|
||||
enabled?: boolean
|
||||
hasCredential?: boolean
|
||||
secretRevealed?: boolean
|
||||
lastUsedAt?: string | null
|
||||
lastUsedIp?: string | null
|
||||
createTime?: string | null
|
||||
updateTime?: string | null
|
||||
}
|
||||
|
||||
export const CustomerService = {
|
||||
async getList(params: CustomerListParams): Promise<CustomerPagedResult> {
|
||||
const res = await axiosInstance.get(omsUrl('/customer/list'), { params })
|
||||
@ -238,4 +253,64 @@ export const CustomerService = {
|
||||
}
|
||||
return `${window.location.origin}/?${params.toString()}`
|
||||
},
|
||||
|
||||
async getOpenApiCredential(): Promise<CustomerOpenApiCredentialDto> {
|
||||
const r = await axiosInstance.get(omsUrl('/customer/openapi-credential'))
|
||||
return unwrapData<CustomerOpenApiCredentialDto>(r.data)
|
||||
},
|
||||
|
||||
async ensureOpenApiCredential(): Promise<CustomerOpenApiCredentialDto> {
|
||||
const r = await axiosInstance.post(omsUrl('/customer/openapi-credential/ensure'))
|
||||
return unwrapData<CustomerOpenApiCredentialDto>(r.data)
|
||||
},
|
||||
|
||||
async resetOpenApiSecret(): Promise<CustomerOpenApiCredentialDto> {
|
||||
const r = await axiosInstance.post(omsUrl('/customer/openapi-credential/reset-secret'))
|
||||
return unwrapData<CustomerOpenApiCredentialDto>(r.data)
|
||||
},
|
||||
|
||||
async setOpenApiEnabled(enabled: boolean): Promise<CustomerOpenApiCredentialDto> {
|
||||
const r = await axiosInstance.post(omsUrl('/customer/openapi-credential/set-enabled'), {
|
||||
enabled,
|
||||
})
|
||||
return unwrapData<CustomerOpenApiCredentialDto>(r.data)
|
||||
},
|
||||
|
||||
async adminGetOpenApiCredential(
|
||||
customerId: string | number,
|
||||
): Promise<CustomerOpenApiCredentialDto> {
|
||||
const r = await axiosInstance.get(omsUrl('/customer/openapi-credential/admin/get'), {
|
||||
params: { customerId: Number(customerId) },
|
||||
})
|
||||
return unwrapData<CustomerOpenApiCredentialDto>(r.data)
|
||||
},
|
||||
|
||||
async adminEnsureOpenApiCredential(
|
||||
customerId: string | number,
|
||||
): Promise<CustomerOpenApiCredentialDto> {
|
||||
const r = await axiosInstance.post(omsUrl('/customer/openapi-credential/admin/ensure'), {
|
||||
customerId: Number(customerId),
|
||||
})
|
||||
return unwrapData<CustomerOpenApiCredentialDto>(r.data)
|
||||
},
|
||||
|
||||
async adminResetOpenApiSecret(
|
||||
customerId: string | number,
|
||||
): Promise<CustomerOpenApiCredentialDto> {
|
||||
const r = await axiosInstance.post(omsUrl('/customer/openapi-credential/admin/reset-secret'), {
|
||||
customerId: Number(customerId),
|
||||
})
|
||||
return unwrapData<CustomerOpenApiCredentialDto>(r.data)
|
||||
},
|
||||
|
||||
async adminSetOpenApiEnabled(
|
||||
customerId: string | number,
|
||||
enabled: boolean,
|
||||
): Promise<CustomerOpenApiCredentialDto> {
|
||||
const r = await axiosInstance.post(omsUrl('/customer/openapi-credential/admin/set-enabled'), {
|
||||
customerId: Number(customerId),
|
||||
enabled,
|
||||
})
|
||||
return unwrapData<CustomerOpenApiCredentialDto>(r.data)
|
||||
},
|
||||
}
|
||||
|
||||
@ -65,6 +65,7 @@ type CacheEntry = { key: string; data: CustomerDto; at: number }
|
||||
|
||||
const CACHE_TTL_MS = 60_000
|
||||
let memoryCache: CacheEntry | null = null
|
||||
const inflight = new Map<string, Promise<CustomerDto | null>>()
|
||||
|
||||
function cacheKey(options: { mapId: number | null; account?: string }) {
|
||||
return `${options.mapId ?? ''}|${(options.account || '').trim()}`
|
||||
@ -169,9 +170,19 @@ export async function resolveCurrentCustomer(options: {
|
||||
if (cached) return cached
|
||||
}
|
||||
|
||||
const data = await findCustomerFromList(options)
|
||||
const pending = inflight.get(key)
|
||||
if (pending) return pending
|
||||
|
||||
const task = findCustomerFromList(options)
|
||||
.then((data) => {
|
||||
if (data) writeCache(key, data)
|
||||
return data
|
||||
})
|
||||
.finally(() => {
|
||||
inflight.delete(key)
|
||||
})
|
||||
inflight.set(key, task)
|
||||
return task
|
||||
}
|
||||
|
||||
/** 根据 JWT mapId / 登录账号解析 t_customer 整数主键(非 RBAC user GUID) */
|
||||
|
||||
@ -1267,6 +1267,7 @@ export interface PriceCheckRequest {
|
||||
/** 仓库主键(多选;为空则遍历全部仓库) */
|
||||
warehouseIds?: number[]
|
||||
destinationProvince?: string
|
||||
destinationCity?: string
|
||||
postalCode?: string
|
||||
/** 选用的优惠券实例 Id */
|
||||
couponInstanceId?: number
|
||||
@ -2175,11 +2176,16 @@ export interface OrderAllocationTrace {
|
||||
selectedChannelName?: string
|
||||
failReason?: string
|
||||
traceTime?: string
|
||||
poolCount?: number
|
||||
conditionPassCount?: number
|
||||
candidateCount?: number
|
||||
candidates: OrderAllocationCandidate[]
|
||||
skipped: Array<{
|
||||
serviceChannelId?: number
|
||||
channelCode?: string
|
||||
channelName?: string
|
||||
stage?: string
|
||||
matchCondition?: string
|
||||
reason: string
|
||||
}>
|
||||
}
|
||||
@ -2222,11 +2228,16 @@ export async function getOrderAllocationTrace(params: {
|
||||
selectedChannelName: raw?.selectedChannelName ?? raw?.SelectedChannelName,
|
||||
failReason: raw?.failReason ?? raw?.FailReason,
|
||||
traceTime: raw?.traceTime ?? raw?.TraceTime,
|
||||
poolCount: Number(raw?.poolCount ?? raw?.PoolCount ?? 0),
|
||||
conditionPassCount: Number(raw?.conditionPassCount ?? raw?.ConditionPassCount ?? 0),
|
||||
candidateCount: Number(raw?.candidateCount ?? raw?.CandidateCount ?? 0),
|
||||
candidates: (raw?.candidates ?? raw?.Candidates ?? []).map(mapCandidate),
|
||||
skipped: (raw?.skipped ?? raw?.Skipped ?? []).map((s: any) => ({
|
||||
serviceChannelId: s.serviceChannelId ?? s.ServiceChannelId,
|
||||
channelCode: s.channelCode ?? s.ChannelCode,
|
||||
channelName: s.channelName ?? s.ChannelName,
|
||||
stage: s.stage ?? s.Stage,
|
||||
matchCondition: s.matchCondition ?? s.MatchCondition,
|
||||
reason: String(s.reason ?? s.Reason ?? ''),
|
||||
})),
|
||||
}
|
||||
|
||||
@ -78,3 +78,69 @@ export const markPickupPicked = (id: number | string) =>
|
||||
|
||||
export const markPickupInbound = (id: number | string) =>
|
||||
postOk('/order/pickup-order/mark-inbound', { id })
|
||||
|
||||
/** POST /order/pickup-order/downstream-forward → 从揽收单表组装 createOrderOne 并转发 */
|
||||
export async function postPickupOrderDownstreamForward(id: number | string) {
|
||||
const data = await postOk('/order/pickup-order/downstream-forward', { id })
|
||||
const d = (data ?? {}) as Record<string, any>
|
||||
return {
|
||||
success: d.success ?? d.Success,
|
||||
supplierCode: d.supplierCode ?? d.SupplierCode,
|
||||
url: d.url ?? d.Url,
|
||||
requestJson: d.requestJson ?? d.RequestJson,
|
||||
statusCode: d.statusCode ?? d.StatusCode,
|
||||
responseBody: d.responseBody ?? d.ResponseBody,
|
||||
downstreamServiceNo: d.downstreamServiceNo ?? d.DownstreamServiceNo,
|
||||
trackNo: d.trackNo ?? d.TrackNo,
|
||||
}
|
||||
}
|
||||
|
||||
export type PickupBigBagTrackStepResult = {
|
||||
skipped?: boolean
|
||||
skipReason?: string
|
||||
success?: boolean
|
||||
url?: string
|
||||
requestJson?: string
|
||||
statusCode?: number
|
||||
responseBody?: string
|
||||
}
|
||||
|
||||
export type PickupBigBagTrackSyncResult = {
|
||||
pickupNo?: string
|
||||
success?: boolean
|
||||
failStep?: string
|
||||
message?: string
|
||||
upsertBigBag?: PickupBigBagTrackStepResult
|
||||
link?: PickupBigBagTrackStepResult
|
||||
trackUpsert?: PickupBigBagTrackStepResult
|
||||
}
|
||||
|
||||
function mapTrackStep(raw: any | undefined): PickupBigBagTrackStepResult | undefined {
|
||||
if (!raw) return undefined
|
||||
return {
|
||||
skipped: raw.skipped ?? raw.Skipped,
|
||||
skipReason: raw.skipReason ?? raw.SkipReason,
|
||||
success: raw.success ?? raw.Success,
|
||||
url: raw.url ?? raw.Url,
|
||||
requestJson: raw.requestJson ?? raw.RequestJson,
|
||||
statusCode: raw.statusCode ?? raw.StatusCode,
|
||||
responseBody: raw.responseBody ?? raw.ResponseBody,
|
||||
}
|
||||
}
|
||||
|
||||
/** POST /order/pickup-order/track-upsert-big-bag → UpsertBigBag + Link + Track A0201 */
|
||||
export async function postPickupOrderTrackUpsertBigBag(
|
||||
id: number | string,
|
||||
): Promise<PickupBigBagTrackSyncResult> {
|
||||
const data = await postOk('/order/pickup-order/track-upsert-big-bag', { id })
|
||||
const d = (data ?? {}) as Record<string, any>
|
||||
return {
|
||||
pickupNo: d.pickupNo ?? d.PickupNo,
|
||||
success: d.success ?? d.Success,
|
||||
failStep: d.failStep ?? d.FailStep,
|
||||
message: d.message ?? d.Message,
|
||||
upsertBigBag: mapTrackStep(d.upsertBigBag ?? d.UpsertBigBag),
|
||||
link: mapTrackStep(d.link ?? d.Link),
|
||||
trackUpsert: mapTrackStep(d.trackUpsert ?? d.TrackUpsert),
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,7 +6,7 @@ import { useRbacUserStore } from '@/store/modules/rbac-user'
|
||||
import { resolvePostLogoutLogin } from '@/utils/login-portal'
|
||||
|
||||
// 创建自定义 axios 实例
|
||||
// baseURL 为空;生产 URL 由 OpenAPI.BASE / omsUrl 拼到 api.ideship.com
|
||||
// baseURL 为空;远端由 OpenAPI.BASE / omsUrl 按 env 拼接(本地走 Vite 代理到测试环境)
|
||||
export const axiosInstance = axios.create({
|
||||
baseURL: '', // 始终为空,URL 由 OpenAPI.BASE + path 构建
|
||||
timeout: 30000,
|
||||
|
||||
@ -7,6 +7,10 @@ function internalUserListUrl() {
|
||||
return resolveAdminApiPath('/rbac/user/internal-list')
|
||||
}
|
||||
|
||||
function resetPasswordUrl() {
|
||||
return resolveAdminApiPath('/rbac/user/reset-password')
|
||||
}
|
||||
|
||||
/** 内部用户列表(排除纯客户账号),响应结构与 UserDtoPagedResultDto 兼容 */
|
||||
export async function getInternalUserList(params: {
|
||||
keyword?: string
|
||||
@ -18,4 +22,9 @@ export async function getInternalUserList(params: {
|
||||
return res.data
|
||||
}
|
||||
|
||||
/** 管理员重置用户密码(无需旧密码) */
|
||||
export async function resetUserPassword(id: string, newPassword: string): Promise<void> {
|
||||
await axiosInstance.post(resetPasswordUrl(), { newPassword }, { params: { id } })
|
||||
}
|
||||
|
||||
export type { UserDto }
|
||||
|
||||
@ -58,6 +58,9 @@ export const TRACK_SERVICE_CONFIG_CODE = 'track_service'
|
||||
/** API 中心(接口通 / itfx)网关 */
|
||||
export const API_CENTER_CONFIG_CODE = 'api_center'
|
||||
|
||||
/** 国内揽收开放接口(历史配置;鉴权在 itfx,OMS 不再校验 AppKey) */
|
||||
export const PICKUP_OPEN_API_CONFIG_CODE = 'pickup_open_api'
|
||||
|
||||
/** 微信公众号 */
|
||||
export const WECHAT_MP_CONFIG_CODE = 'wechat_mp'
|
||||
|
||||
|
||||
17
src/assets/icons/h5/route-package.svg
Normal file
@ -0,0 +1,17 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 36" fill="none">
|
||||
<g transform="translate(11.2 0.4) scale(0.025)" fill="currentColor">
|
||||
<path
|
||||
d="M1024 708.7c0 36.4-20.5 69.6-53.1 85.9 0 0-261.1 130.3-374.9 186.8-23.9 11.9-52-5.5-52-32.3V456.4c0-20.3 12.8-38.4 31.8-45.2l137.4-49.1c2.6-0.9 5.3 1 5.3 3.8v197.2c0 5.9 6.3 9.8 11.6 7.2l43.6-21.8c5.4-2.7 8.8-8.3 8.8-14.3V342.9c0-3.4 2.1-6.4 5.3-7.5l203.8-72.8c15.6-5.6 32.1 6 32.1 22.6 0.3 105.9 0.3 423.5 0.3 423.5zM480 456.2v490.4c0 27.2-29.1 44.6-53 31.7L50.6 776.2C19.5 759.5 0 727 0 691.6c0 0 0.1-304.3 0.3-408 0-16.6 16.5-28.2 32.1-22.5L448.3 411c19 6.9 31.7 24.9 31.7 45.2zM978.9 199.2l-207.9 74.3c-3.7 1.3-7.8 1.2-11.4-0.2L300.2 86.9c-3.4-1.4-3.3-6.2 0.1-7.5L478.4 12.6c10.9-4.1 22.3-6.1 33.7-6.1 11.4 0 22.7 2 33.6 6l415.9 155.2c7.2 2.7 13.9 6.2 20.1 10.3 8.1 5.5 6.4 17.9-2.8 21.2zM655.6 299.9c6.8 2.8 6.6 12.5-0.3 14.9L544.4 354.4c-21 7.5-43.9 7.5-64.9-0.1l-432-155.8c-9.4-3.4-10.8-16.1-2.4-21.4 5.4-3.3 11.1-6.2 17.2-8.5L196.4 118.4c3.8-1.4 7.9-1.4 11.6 0.2l447.6 181.3z"
|
||||
/>
|
||||
</g>
|
||||
<!-- ?? -->
|
||||
<path d="M2 32.6h3v2.4H2z" fill="currentColor" opacity="0.16" />
|
||||
<path d="M6.6 32.6h3.2v2.4H6.6z" fill="currentColor" opacity="0.3" />
|
||||
<path d="M11.4 32.6h3.4v2.4H11.4z" fill="currentColor" opacity="0.44" />
|
||||
<!-- ?? + ?????? -->
|
||||
<path
|
||||
d="M42.6 32.6 40.8 29.6 47.8 32.6H48v2.4H16v-2.4h26.6z"
|
||||
fill="currentColor"
|
||||
opacity="0.55"
|
||||
/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
9
src/assets/icons/h5/tab-me.svg
Normal file
@ -0,0 +1,9 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||
<circle cx="12" cy="8" r="3.2" stroke="currentColor" stroke-width="1.8" />
|
||||
<path
|
||||
d="M5.5 20.2c.9-3.6 3.4-5.4 6.5-5.4s5.6 1.8 6.5 5.4"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.8"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 303 B |
@ -1,12 +1,17 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||
<path
|
||||
d="M4.5 8.2L12 4l7.5 4.2v8.6L12 21l-7.5-4.2V8.2z"
|
||||
<rect
|
||||
x="5"
|
||||
y="3"
|
||||
width="14"
|
||||
height="18"
|
||||
rx="2.2"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.8"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path d="M4.5 8.2L12 12.4l7.5-4.2" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round" />
|
||||
<path d="M12 12.4V21" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" />
|
||||
<path d="M9.2 6.5l2.8 1.6 2.8-1.6" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M12 8.1v4" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" />
|
||||
<path
|
||||
d="M8.5 9h7M8.5 13h7M8.5 17h4.5"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.8"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 637 B After Width: | Height: | Size: 332 B |
20
src/assets/icons/h5/tab-service.svg
Normal file
@ -0,0 +1,20 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||
<path
|
||||
d="M4 14.5V12a8 8 0 0 1 16 0v2.5"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.8"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
<path
|
||||
d="M4 14h2.8v5.2H4A1.6 1.6 0 0 1 2.4 17.6v-2A1.6 1.6 0 0 1 4 14z"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.8"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M20 14h-2.8v5.2H20a1.6 1.6 0 0 0 1.6-1.6v-2A1.6 1.6 0 0 0 20 14z"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.8"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 530 B |
@ -5,7 +5,7 @@
|
||||
--app-primary-color-light: #eef3f6;
|
||||
--app-navbar-bg: #054f74;
|
||||
--app-sidebar-bg: #054f74;
|
||||
--app-page-bg: #f5f4f1;
|
||||
--app-page-bg: #f0f3f4;
|
||||
--app-card-bg: #ffffff;
|
||||
--app-card-border: #e5e6eb;
|
||||
--app-elevated-bg: #ffffff;
|
||||
|
||||
1
src/components.d.ts
vendored
@ -35,6 +35,7 @@ declare module 'vue' {
|
||||
JsonEditorPro: typeof import('./components/JsonEditorPro/index.vue')['default']
|
||||
MarkdownDocPanel: typeof import('./components/MarkdownDocPanel/index.vue')['default']
|
||||
MarkdownViewer: typeof import('./components/MarkdownViewer/index.vue')['default']
|
||||
MemberLevelBadge: typeof import('./components/MemberLevelBadge/index.vue')['default']
|
||||
MenuIconPicker: typeof import('./components/MenuIconPicker/index.vue')['default']
|
||||
ModalForm: typeof import('./components/ModalForm/index.vue')['default']
|
||||
ModalPro: typeof import('./components/ModalPro/index.vue')['default']
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, computed, nextTick, watch, h } from 'vue';
|
||||
import { ref, reactive, computed, nextTick, watch, h, defineAsyncComponent } from 'vue';
|
||||
import type { PropType } from 'vue';
|
||||
import { IconDelete } from '@arco-design/web-vue/es/icon';
|
||||
import { Message, Modal } from '@arco-design/web-vue';
|
||||
@ -14,7 +14,10 @@
|
||||
import { getInterfaceGetInterfaceById } from '@/api/InterfaceServices/interface';
|
||||
import { getMethodText } from '@/views/api/upstreamInterface/utils';
|
||||
import { useTabRowCounts } from '@/utils/useTabRowCounts';
|
||||
import RequestLog from '@/views/api/requestLog/index.vue';
|
||||
import { prodApiOrigin } from '@/utils/rewriteBrowserApiPath';
|
||||
|
||||
/** 请求日志页含 Handsontable,仅打开「查看报文」时再加载 */
|
||||
const RequestLog = defineAsyncComponent(() => import('@/views/api/requestLog/index.vue'));
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
@ -1336,21 +1339,29 @@
|
||||
|
||||
/**
|
||||
* 开发态:把已知外域 URL 改写到 Vite 代理,避免浏览器 CORS。
|
||||
* - api.nanafa.com → /nanafa/...
|
||||
* - api.nanafa.com / api(.test).ideship.com → /nanafa/...
|
||||
* - 旧 itfx IP → /api/...
|
||||
* - lsp.globex.cn → /external/...
|
||||
* 生产:nanafa 旧址改写到 api.ideship.com(方案 A)
|
||||
* 生产/测试构建:nanafa 旧址改写到当前环境 API 根(方案 A)
|
||||
*/
|
||||
function rewriteDebuggerUrlForProxy(rawUrl: string): string {
|
||||
const url = String(rawUrl || '').trim();
|
||||
if (!url) return url;
|
||||
const apiOrigin = prodApiOrigin();
|
||||
|
||||
// http(s)://api.nanafa.com/...
|
||||
if (/^https?:\/\/api\.nanafa\.com(?::\d+)?(\/|$)/i.test(url)) {
|
||||
// http(s)://api.nanafa.com/... 或 api(.test).ideship.com/...
|
||||
if (/^https?:\/\/api\.(?:nanafa|ideship)\.com(?::\d+)?(\/|$)/i.test(url)
|
||||
|| /^https?:\/\/api\.test\.ideship\.com(?::\d+)?(\/|$)/i.test(url)) {
|
||||
if (import.meta.env.DEV) {
|
||||
return url.replace(/^https?:\/\/api\.nanafa\.com(?::\d+)?/i, '/nanafa');
|
||||
return url
|
||||
.replace(/^https?:\/\/api\.nanafa\.com(?::\d+)?/i, '/nanafa')
|
||||
.replace(/^https?:\/\/api\.test\.ideship\.com(?::\d+)?/i, '/nanafa')
|
||||
.replace(/^https?:\/\/api\.ideship\.com(?::\d+)?/i, '/nanafa');
|
||||
}
|
||||
return url.replace(/^https?:\/\/api\.nanafa\.com(?::\d+)?/i, 'https://api.ideship.com');
|
||||
return url
|
||||
.replace(/^https?:\/\/api\.nanafa\.com(?::\d+)?/i, apiOrigin)
|
||||
.replace(/^https?:\/\/api\.test\.ideship\.com(?::\d+)?/i, apiOrigin)
|
||||
.replace(/^https?:\/\/api\.ideship\.com(?::\d+)?/i, apiOrigin);
|
||||
}
|
||||
|
||||
// 旧 itfx 直连 IP
|
||||
@ -1358,7 +1369,7 @@
|
||||
if (import.meta.env.DEV) {
|
||||
return url.replace('http://43.139.210.35:8888', '/api');
|
||||
}
|
||||
return url.replace('http://43.139.210.35:8888', 'https://api.ideship.com/itfx');
|
||||
return url.replace('http://43.139.210.35:8888', `${apiOrigin}/itfx`);
|
||||
}
|
||||
|
||||
// 示例外部服务商
|
||||
|
||||
@ -482,6 +482,14 @@
|
||||
</a-dropdown>
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
v-else-if="getNodeData(key)?.isLeaf !== true && slots['branch-node-extra']"
|
||||
class="node-actions-cell branch-node-extra-cell"
|
||||
>
|
||||
<span class="branch-node-extra" @click.stop>
|
||||
<slot name="branch-node-extra" :node-data="getNodeData(key)" :node-key="key"></slot>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- 第二行始终占位,保证单行/双行卡片高度一致(以两行为准) -->
|
||||
<tr class="node-second-row">
|
||||
@ -582,19 +590,21 @@
|
||||
.custom-left-menu {
|
||||
--clm-bg: #ffffff;
|
||||
--clm-text: #1a1c20;
|
||||
--clm-muted: #8a8f98;
|
||||
--clm-faint: #b0b4ba;
|
||||
--clm-muted: #6b7280;
|
||||
--clm-faint: #9ca3af;
|
||||
--clm-hover: rgba(15, 16, 17, 0.04);
|
||||
--clm-selected: var(--app-primary-color-light);
|
||||
--clm-selected-text: var(--app-primary-color);
|
||||
--clm-selected-border: color-mix(in srgb, var(--app-primary-color) 32%, transparent);
|
||||
--clm-border: rgba(15, 16, 17, 0.08);
|
||||
--clm-selected-border: color-mix(in srgb, var(--app-primary-color) 40%, transparent);
|
||||
--clm-border: rgba(15, 16, 17, 0.12);
|
||||
--clm-radius: 8px;
|
||||
/* 列表卡片统一高度 */
|
||||
--clm-node-title-line-height: 18px;
|
||||
--clm-node-second-line-height: 16px;
|
||||
--clm-node-padding-y: 5px;
|
||||
--clm-node-height: 60px;
|
||||
/* 菜单项垂直间距 */
|
||||
--clm-node-gap: 8px;
|
||||
|
||||
position: relative;
|
||||
display: flex;
|
||||
@ -761,6 +771,16 @@
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
:deep(.branch-node-extra .arco-icon) {
|
||||
display: inline-block !important;
|
||||
width: 14px !important;
|
||||
height: 14px !important;
|
||||
min-width: 14px !important;
|
||||
flex: none !important;
|
||||
font-size: 14px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
:deep(.arco-tree-node-wrapper),
|
||||
:deep(.arco-tree-node-selected),
|
||||
:deep(.arco-tree-node-selected > .arco-tree-node-title) {
|
||||
@ -836,54 +856,58 @@
|
||||
:deep(.arco-tree-node-line),
|
||||
:deep(.arco-tree-node-line-top),
|
||||
:deep(.arco-tree-node-line-bottom) {
|
||||
border-color: rgba(15, 16, 17, 0.08) !important;
|
||||
border-color: var(--clm-border) !important;
|
||||
}
|
||||
|
||||
/*
|
||||
* 层级树(组织等)节点本身就是可选内容,不能套用「分组头」透明边框/淡字样式。
|
||||
*/
|
||||
.tree-node {
|
||||
margin: 2px 0;
|
||||
padding: var(--clm-node-padding-y) 8px;
|
||||
margin: calc(var(--clm-node-gap) / 2) 0;
|
||||
padding: 8px 10px;
|
||||
border-radius: var(--clm-radius);
|
||||
border: 1px solid rgba(15, 16, 17, 0.06);
|
||||
background: transparent;
|
||||
border: 1px solid var(--clm-border);
|
||||
background: var(--color-fill-1, #f7f8fa);
|
||||
box-sizing: border-box;
|
||||
height: var(--clm-node-height);
|
||||
min-height: var(--clm-node-height);
|
||||
max-height: var(--clm-node-height);
|
||||
overflow: hidden;
|
||||
height: auto;
|
||||
min-height: 44px;
|
||||
max-height: none;
|
||||
overflow: visible;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
|
||||
&.tree-node--with-code {
|
||||
min-height: var(--clm-node-height);
|
||||
&.tree-node--branch,
|
||||
&.tree-node--leaf {
|
||||
background: var(--color-fill-1, #f7f8fa);
|
||||
border-color: var(--clm-border);
|
||||
}
|
||||
|
||||
&.tree-node--branch {
|
||||
height: auto;
|
||||
min-height: 34px;
|
||||
max-height: none;
|
||||
margin: 8px 0 2px;
|
||||
padding: 6px 8px;
|
||||
border-color: transparent;
|
||||
background: transparent;
|
||||
overflow: visible;
|
||||
|
||||
.node-second-row-spacer-cell {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.node-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--clm-text);
|
||||
}
|
||||
|
||||
&.tree-node--leaf {
|
||||
background: var(--color-fill-1);
|
||||
.node-subtitle {
|
||||
color: var(--clm-muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.node-subtitle-cell {
|
||||
padding-top: 2px;
|
||||
padding-left: 11px;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: var(--clm-hover);
|
||||
border-color: rgba(15, 16, 17, 0.1);
|
||||
}
|
||||
|
||||
&.tree-node--branch:hover {
|
||||
background: transparent;
|
||||
border-color: transparent;
|
||||
border-color: rgba(15, 16, 17, 0.18);
|
||||
}
|
||||
|
||||
&.selected {
|
||||
@ -894,39 +918,29 @@
|
||||
|
||||
.node-title {
|
||||
color: var(--clm-selected-text) !important;
|
||||
font-weight: 560;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.node-subtitle {
|
||||
color: var(--clm-muted) !important;
|
||||
}
|
||||
|
||||
&.tree-node--branch {
|
||||
background: transparent !important;
|
||||
border-color: transparent !important;
|
||||
|
||||
.node-title {
|
||||
color: var(--clm-muted) !important;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.node-title::before {
|
||||
background: var(--app-primary-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tree-node--depth-1.tree-node--branch .node-title {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.02em;
|
||||
color: var(--clm-muted);
|
||||
.tree-node--depth-1 .node-title {
|
||||
font-size: 13px;
|
||||
font-weight: 650;
|
||||
color: var(--clm-text);
|
||||
}
|
||||
|
||||
.tree-node--depth-2 .node-title {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--clm-text);
|
||||
}
|
||||
|
||||
.tree-node--depth-3,
|
||||
@ -934,25 +948,26 @@
|
||||
.tree-node--depth-5,
|
||||
.tree-node--depth-6 {
|
||||
.node-title {
|
||||
font-size: 11px;
|
||||
font-weight: 450;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--clm-text);
|
||||
}
|
||||
}
|
||||
|
||||
.tree-node--branch .node-title::before {
|
||||
.tree-node .node-title::before {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
width: 3px;
|
||||
height: 12px;
|
||||
margin-right: 8px;
|
||||
border-radius: 2px;
|
||||
background: color-mix(in srgb, var(--app-primary-color) 55%, var(--clm-faint));
|
||||
background: color-mix(in srgb, var(--app-primary-color) 70%, var(--clm-muted));
|
||||
vertical-align: -2px;
|
||||
}
|
||||
|
||||
.node-subtitle {
|
||||
color: var(--clm-muted);
|
||||
font-size: 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1080,7 +1095,7 @@
|
||||
box-sizing: border-box;
|
||||
border: 1px solid rgba(15, 16, 17, 0.06);
|
||||
padding: var(--clm-node-padding-y) 8px;
|
||||
margin: 2px 0;
|
||||
margin: calc(var(--clm-node-gap) / 2) 0;
|
||||
border-radius: var(--clm-radius);
|
||||
transition: background-color 0.12s ease, border-color 0.12s ease;
|
||||
cursor: pointer;
|
||||
@ -1102,7 +1117,7 @@
|
||||
height: auto;
|
||||
min-height: 34px;
|
||||
max-height: none;
|
||||
margin: 10px 0 4px;
|
||||
margin: calc(var(--clm-node-gap) + 4px) 0 calc(var(--clm-node-gap) / 2);
|
||||
padding: 6px 8px;
|
||||
border-color: transparent;
|
||||
background: transparent;
|
||||
@ -1166,10 +1181,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* 实体叶子:保持卡片感 */
|
||||
/* 实体叶子:固定高度,内容块垂直居中避免贴底 */
|
||||
&.tree-node--leaf {
|
||||
background: var(--color-fill-1);
|
||||
border-color: rgba(15, 16, 17, 0.07);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@ -1232,7 +1248,11 @@
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
table-layout: fixed;
|
||||
height: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.node-second-row > td {
|
||||
padding-top: 2px;
|
||||
}
|
||||
|
||||
.node-content-cell {
|
||||
@ -1288,6 +1308,31 @@
|
||||
}
|
||||
}
|
||||
|
||||
.branch-node-extra-cell {
|
||||
width: 28px;
|
||||
|
||||
.branch-node-extra {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
|
||||
:deep(.arco-btn) {
|
||||
width: 22px !important;
|
||||
min-width: 22px !important;
|
||||
height: 22px !important;
|
||||
min-height: 22px !important;
|
||||
padding: 0 !important;
|
||||
color: var(--clm-muted);
|
||||
border-radius: 5px;
|
||||
|
||||
&:hover {
|
||||
color: var(--clm-text);
|
||||
background: var(--clm-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.node-second-row-extra-cell {
|
||||
text-align: right;
|
||||
padding: 2px 0 0;
|
||||
|
||||
@ -1824,8 +1824,14 @@
|
||||
if (!hotInstance) return;
|
||||
const editor = hotInstance.getActiveEditor() as any;
|
||||
if (!editor) return;
|
||||
// TextEditor:只有传入 string 才会用该初值;数字会被忽略并回退 originalValue。
|
||||
// 显式传入 String(cell) 避免 originalValue 未就绪时点进编辑被清空。
|
||||
const raw = hotInstance.getDataAtCell(r, c);
|
||||
const initial = raw === null || raw === undefined ? '' : String(raw);
|
||||
if (!editor.isOpened?.()) {
|
||||
editor.beginEditing?.();
|
||||
editor.beginEditing?.(initial);
|
||||
} else if (editor.TEXTAREA && (editor.TEXTAREA.value == null || editor.TEXTAREA.value === '')) {
|
||||
editor.setValue?.(initial);
|
||||
}
|
||||
// 下拉/自动完成:点开即展开选项,避免用户不知道还能点箭头
|
||||
const meta = hotInstance.getCellMeta(r, c);
|
||||
@ -1833,8 +1839,12 @@
|
||||
if (t === 'dropdown' || t === 'autocomplete') {
|
||||
window.setTimeout(() => {
|
||||
try {
|
||||
if (typeof editor.open === 'function') editor.open();
|
||||
else if (typeof editor.queryChoices === 'function') editor.queryChoices(editor.TEXTAREA?.value ?? '');
|
||||
if (editor.TEXTAREA && !editor.TEXTAREA.value && initial) {
|
||||
editor.setValue?.(initial);
|
||||
}
|
||||
// 用空 query 拉全量选项,避免「当前值与 source 不完全一致」时列表被滤空
|
||||
if (typeof editor.queryChoices === 'function') editor.queryChoices('');
|
||||
else if (typeof editor.open === 'function') editor.open();
|
||||
} catch (_) {
|
||||
/* ignore */
|
||||
}
|
||||
@ -2406,8 +2416,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 监听数据变化
|
||||
watch(() => props.data, () => {
|
||||
function refreshHandsontableData() {
|
||||
if (ignoreDataWatch) return;
|
||||
// 编辑中勿 loadData,否则输入框/下拉会被立刻拆掉
|
||||
if (hotInstance && props.readOnly === false) {
|
||||
@ -2422,7 +2431,29 @@
|
||||
void initHandsontableWhenReady({ finalize: false });
|
||||
});
|
||||
}
|
||||
}, { deep: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据刷新策略:
|
||||
* - 可编辑表:afterChange 会写回行对象;若 deep watch,上千行一改一格就全表 loadData → 卡死。
|
||||
* 因此只在数组引用/长度变化时刷新。
|
||||
* - 只读表:仍 deep watch,兼容外部原地改字段后的展示刷新。
|
||||
*/
|
||||
watch(
|
||||
() => [props.data, props.data?.length] as const,
|
||||
() => {
|
||||
refreshHandsontableData();
|
||||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.data,
|
||||
() => {
|
||||
if (props.readOnly === false) return;
|
||||
refreshHandsontableData();
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
// 监听分页变化
|
||||
watch([() => props.currentPage, () => props.pageSize], () => {
|
||||
@ -2442,10 +2473,26 @@
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
// 列定义变化时,重新对齐顺序并重建表格
|
||||
function columnsStructuralKey(cols: ColumnDef[]): string {
|
||||
return (cols || [])
|
||||
.map((c) =>
|
||||
[
|
||||
c.key,
|
||||
c.label,
|
||||
c.type || '',
|
||||
c.readOnly ? 1 : 0,
|
||||
c.width || '',
|
||||
Array.isArray(c.source) ? c.source.join('\u0001') : '',
|
||||
].join('\u0002'),
|
||||
)
|
||||
.join('\u0003');
|
||||
}
|
||||
|
||||
// 列定义变化时,仅在结构真正变化时重建(避免 computed 每次新对象引用就整表销毁)
|
||||
watch(
|
||||
() => props.columns,
|
||||
() => {
|
||||
() => columnsStructuralKey(props.columns),
|
||||
(next, prev) => {
|
||||
if (next === prev) return;
|
||||
internalColumnOrder.value = normalizeColumnOrder(props.columns, internalColumnOrder.value);
|
||||
if (hotInstance) {
|
||||
nextTick(() => {
|
||||
@ -2454,7 +2501,6 @@
|
||||
});
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
// 窗口大小变化处理函数
|
||||
@ -2502,12 +2548,15 @@
|
||||
|
||||
// 容器尺寸变化时同步表格高度,使列表高度与容器一致
|
||||
let resizeObserver: ResizeObserver | null = null;
|
||||
let lastObservedTableHeight = 0;
|
||||
let resizeRaf = 0;
|
||||
const setupResizeObserver = () => {
|
||||
if (!containerRef.value) return;
|
||||
if (resizeObserver) {
|
||||
resizeObserver.disconnect();
|
||||
resizeObserver = null;
|
||||
}
|
||||
lastObservedTableHeight = 0;
|
||||
resizeObserver = new ResizeObserver((entries) => {
|
||||
const entry = entries[0];
|
||||
if (!entry || !hotInstance) return;
|
||||
@ -2515,9 +2564,14 @@
|
||||
// 高度以本组件根容器实测为准;勿用外层 props.height 覆盖,否则与 flex 内真实高度不一致,stretchH 会把列压满导致横向条消失
|
||||
if (h <= 0) return;
|
||||
const fh = Math.max(Math.floor(h - customScrollbarSize), 120);
|
||||
hotInstance.updateSettings({ height: fh });
|
||||
nextTick(() => {
|
||||
hotInstance?.refreshDimensions();
|
||||
// 高度几乎不变时跳过,避免 updateSettings→再触发 RO 的死循环把页面卡死
|
||||
if (Math.abs(fh - lastObservedTableHeight) < 2) return;
|
||||
lastObservedTableHeight = fh;
|
||||
if (resizeRaf) cancelAnimationFrame(resizeRaf);
|
||||
resizeRaf = requestAnimationFrame(() => {
|
||||
resizeRaf = 0;
|
||||
if (!hotInstance) return;
|
||||
hotInstance.updateSettings({ height: fh }, false);
|
||||
ensureHorizontalScrollbarVisible();
|
||||
syncHorizontalScrollbarMetrics();
|
||||
syncVerticalScrollbarMetrics();
|
||||
@ -3382,7 +3436,8 @@
|
||||
.autocompleteEditor.handsontable.listbox,
|
||||
.ht-theme-classic .handsontable.listbox,
|
||||
.ht-theme-classic-dark .handsontable.listbox {
|
||||
z-index: 5600 !important;
|
||||
/* 高于 Arco Modal(约 1001+),避免弹窗内下拉被挡住 */
|
||||
z-index: 11000 !important;
|
||||
border: 1px solid var(--color-border-2, #e5e6eb) !important;
|
||||
border-radius: 6px !important;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12) !important;
|
||||
|
||||
134
src/components/MemberLevelBadge/index.vue
Normal file
@ -0,0 +1,134 @@
|
||||
<template>
|
||||
<span
|
||||
v-if="displayName"
|
||||
class="member-level-badge"
|
||||
:class="[`is-${size}`, tone ? `is-${tone.key}` : 'is-fallback']"
|
||||
:style="badgeStyle"
|
||||
:title="displayName"
|
||||
>
|
||||
<img
|
||||
v-if="resolvedIconUrl"
|
||||
class="member-level-badge__img"
|
||||
:src="resolvedIconUrl"
|
||||
alt=""
|
||||
draggable="false"
|
||||
/>
|
||||
<span
|
||||
v-else-if="tone"
|
||||
class="member-level-badge__glyph"
|
||||
aria-hidden="true"
|
||||
v-html="glyphHtml"
|
||||
/>
|
||||
<span v-else class="member-level-badge__glyph member-level-badge__glyph--text" aria-hidden="true">
|
||||
{{ displayName.slice(0, 1) }}
|
||||
</span>
|
||||
<span class="member-level-badge__name">{{ displayName }}</span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue'
|
||||
import {
|
||||
getMemberLevelTone,
|
||||
renderMemberLevelGlyphSvg,
|
||||
} from '@/utils/memberLevelIcon'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
levelCode?: string | null
|
||||
levelName?: string | null
|
||||
iconUrl?: string | null
|
||||
size?: 'sm' | 'md'
|
||||
}>(),
|
||||
{
|
||||
levelCode: '',
|
||||
levelName: '',
|
||||
iconUrl: '',
|
||||
size: 'sm',
|
||||
},
|
||||
)
|
||||
|
||||
const displayName = computed(() => String(props.levelName || '').trim())
|
||||
const tone = computed(() => getMemberLevelTone(props.levelCode, props.levelName))
|
||||
const resolvedIconUrl = computed(() => String(props.iconUrl || '').trim())
|
||||
const glyphHtml = computed(() =>
|
||||
tone.value ? renderMemberLevelGlyphSvg(tone.value.key, props.size === 'md' ? 16 : 14) : '',
|
||||
)
|
||||
const badgeStyle = computed(() => {
|
||||
if (!tone.value) return undefined
|
||||
return {
|
||||
'--mlb-color': tone.value.color,
|
||||
'--mlb-bg': tone.value.bg,
|
||||
'--mlb-border': tone.value.border,
|
||||
} as Record<string, string>
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.member-level-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
max-width: 100%;
|
||||
min-height: 22px;
|
||||
padding: 2px 8px 2px 6px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--mlb-border, var(--color-border-2));
|
||||
background: var(--mlb-bg, var(--color-fill-2));
|
||||
color: var(--mlb-color, var(--app-primary-color));
|
||||
vertical-align: middle;
|
||||
line-height: 1.2;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.member-level-badge.is-md {
|
||||
min-height: 26px;
|
||||
gap: 6px;
|
||||
padding: 3px 10px 3px 7px;
|
||||
}
|
||||
|
||||
.member-level-badge__img,
|
||||
.member-level-badge__glyph {
|
||||
flex-shrink: 0;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.member-level-badge.is-md .member-level-badge__img,
|
||||
.member-level-badge.is-md .member-level-badge__glyph {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.member-level-badge__img {
|
||||
object-fit: contain;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.member-level-badge__glyph :deep(svg) {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.member-level-badge__glyph--text {
|
||||
font-size: 10px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.member-level-badge__name {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.member-level-badge.is-md .member-level-badge__name {
|
||||
font-size: 13px;
|
||||
}
|
||||
</style>
|
||||
@ -121,14 +121,15 @@
|
||||
</div>
|
||||
|
||||
<div ref="mainScrollRef" class="ai-app-main">
|
||||
<section v-show="showWelcome" class="ai-welcome">
|
||||
<section v-show="showWelcome && !waybillContext" class="ai-welcome">
|
||||
<div class="ai-welcome__hero">
|
||||
<NanaAvatar class="ai-welcome__avatar" size="md" />
|
||||
<h2>{{ t('nanaAssistant.welcomeTitle') }}</h2>
|
||||
<NanaAvatar class="ai-welcome__avatar" size="sm" />
|
||||
<div class="ai-welcome__copy">
|
||||
<h2>{{ welcomeHeadline }}</h2>
|
||||
<p>{{ t('nanaAssistant.welcomeSub') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="ai-quick-section-label">{{ t('nanaAssistant.mcp.sectionLabel') }}</p>
|
||||
<div class="ai-mcp-grid">
|
||||
<button
|
||||
v-for="cap in mcpCapabilities"
|
||||
@ -138,8 +139,53 @@
|
||||
:disabled="chatLoading"
|
||||
@click="startMcpCapability(cap.id)"
|
||||
>
|
||||
<span class="ai-mcp-card__icon" aria-hidden="true">
|
||||
<component :is="mcpCardIcon(cap.id)" />
|
||||
</span>
|
||||
<span class="ai-mcp-card__body">
|
||||
<span class="ai-mcp-card__title">{{ t(cap.titleKey) }}</span>
|
||||
<span class="ai-mcp-card__desc">{{ t(cap.descKey) }}</span>
|
||||
<span class="ai-mcp-card__desc">{{ t(cap.exampleKey) }}</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-show="showWelcome && waybillContext" class="ai-welcome ai-welcome--waybill">
|
||||
<div class="ai-welcome__hero">
|
||||
<NanaAvatar class="ai-welcome__avatar" size="sm" />
|
||||
<div class="ai-welcome__copy">
|
||||
<h2>{{ welcomeHeadline }}</h2>
|
||||
<p>{{ t('nanaAssistant.waybillWelcomeSub', { no: waybillDisplayNo }) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ai-mcp-grid ai-mcp-grid--waybill">
|
||||
<button
|
||||
type="button"
|
||||
class="ai-mcp-card"
|
||||
:disabled="chatLoading"
|
||||
@click="askWaybillTrack"
|
||||
>
|
||||
<span class="ai-mcp-card__icon" aria-hidden="true">
|
||||
<component :is="mcpCardIcon('track')" />
|
||||
</span>
|
||||
<span class="ai-mcp-card__body">
|
||||
<span class="ai-mcp-card__title">{{ t('nanaAssistant.waybillAskTrack') }}</span>
|
||||
<span class="ai-mcp-card__desc">{{ waybillDisplayNo }}</span>
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="ai-mcp-card"
|
||||
:disabled="chatLoading"
|
||||
@click="startMcpCapability('price')"
|
||||
>
|
||||
<span class="ai-mcp-card__icon" aria-hidden="true">
|
||||
<component :is="mcpCardIcon('price')" />
|
||||
</span>
|
||||
<span class="ai-mcp-card__body">
|
||||
<span class="ai-mcp-card__title">{{ t('nanaAssistant.waybillAskFee') }}</span>
|
||||
<span class="ai-mcp-card__desc">{{ t('nanaAssistant.mcp.priceExample') }}</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
@ -196,7 +242,7 @@
|
||||
:aria-label="t('nanaAssistant.send')"
|
||||
:disabled="!canSend"
|
||||
>
|
||||
<icon-arrow-up />
|
||||
<icon-send />
|
||||
</button>
|
||||
</form>
|
||||
<div class="ai-composer-footer">
|
||||
@ -268,12 +314,12 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||
import { computed, nextTick, onMounted, onUnmounted, ref, watch, type Component } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import {
|
||||
IconArrowUp,
|
||||
IconSend,
|
||||
IconClockCircle,
|
||||
IconCustomerService,
|
||||
IconDelete,
|
||||
@ -284,6 +330,7 @@ import {
|
||||
IconMessage,
|
||||
IconPlus,
|
||||
IconPublic,
|
||||
IconSafe,
|
||||
IconStorage,
|
||||
IconUser,
|
||||
IconWifi,
|
||||
@ -303,6 +350,8 @@ import {
|
||||
parseWaybillContextFromQuery,
|
||||
type WaybillContext,
|
||||
} from '@/composables/useAskAdvisor';
|
||||
import IconTabCalculator from '@/assets/icons/h5/tab-calculator.svg';
|
||||
import IconTabPackage from '@/assets/icons/h5/tab-package.svg';
|
||||
import {
|
||||
MCP_CAPABILITY_GUIDES,
|
||||
detectMissingFields,
|
||||
@ -377,6 +426,16 @@ const guidedSession = ref(false);
|
||||
const threads = ref<Thread[]>([]);
|
||||
|
||||
const mcpCapabilities = MCP_CAPABILITY_GUIDES;
|
||||
|
||||
const mcpCardIcons: Record<McpCapabilityId, Component> = {
|
||||
price: IconTabCalculator,
|
||||
guide: IconSafe,
|
||||
track: IconTabPackage,
|
||||
};
|
||||
|
||||
function mcpCardIcon(id: McpCapabilityId) {
|
||||
return mcpCardIcons[id];
|
||||
}
|
||||
const activeThreadId = ref<string | null>(null);
|
||||
const messages = ref<ChatMessage[]>([]);
|
||||
const mainScrollRef = ref<HTMLElement>();
|
||||
@ -445,7 +504,20 @@ const isChatting = computed(
|
||||
() => messages.value.some((m) => m.role === 'user') || guidedSession.value,
|
||||
);
|
||||
|
||||
const showWelcome = computed(() => !isChatting.value && !waybillContext.value);
|
||||
const showWelcome = computed(() => !isChatting.value);
|
||||
|
||||
const welcomeGreet = computed(() => {
|
||||
const hour = new Date().getHours();
|
||||
if (hour < 6) return t('nanaAssistant.greet.night');
|
||||
if (hour < 12) return t('nanaAssistant.greet.morning');
|
||||
if (hour < 14) return t('nanaAssistant.greet.noon');
|
||||
if (hour < 18) return t('nanaAssistant.greet.afternoon');
|
||||
return t('nanaAssistant.greet.evening');
|
||||
});
|
||||
|
||||
const welcomeHeadline = computed(() =>
|
||||
t('nanaAssistant.welcomeHello', { greet: welcomeGreet.value }),
|
||||
);
|
||||
|
||||
const canSend = computed(() => Boolean(inputText.value.trim() && !chatLoading.value));
|
||||
|
||||
@ -876,7 +948,7 @@ async function startMcpCapability(id: McpCapabilityId) {
|
||||
messages.value.push({ role: 'assistant', content: coach });
|
||||
guidedSession.value = true;
|
||||
saveThreads();
|
||||
inputText.value = '';
|
||||
inputText.value = t(cap.exampleKey);
|
||||
await nextTick();
|
||||
resizeInput();
|
||||
scrollToBottom();
|
||||
|
||||
@ -691,12 +691,40 @@
|
||||
}
|
||||
|
||||
.ai-welcome__hero {
|
||||
text-align: center;
|
||||
padding: 24px 12px 28px;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
text-align: left;
|
||||
padding: 12px 4px 16px;
|
||||
}
|
||||
|
||||
.ai-welcome__avatar {
|
||||
margin: 0 auto 16px;
|
||||
.ai-welcome__avatar.nana-fab-wrap,
|
||||
.ai-welcome__avatar.nana-fab-wrap--sm {
|
||||
flex-shrink: 0;
|
||||
margin: 0;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
.ai-welcome__copy {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.ai-welcome__hero h2 {
|
||||
margin: 0 0 4px;
|
||||
font-size: clamp(1.15rem, 3.6vw, 1.4rem);
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.02em;
|
||||
color: var(--kc-primary, #1b2a6b);
|
||||
}
|
||||
|
||||
.ai-welcome__hero p {
|
||||
margin: 0;
|
||||
max-width: none;
|
||||
font-size: 0.88rem;
|
||||
line-height: 1.5;
|
||||
color: var(--color-text-3, #86909c);
|
||||
}
|
||||
|
||||
.ai-avatar {
|
||||
@ -713,22 +741,6 @@
|
||||
box-shadow: 0 8px 24px rgba(27, 42, 107, 0.2);
|
||||
}
|
||||
|
||||
.ai-welcome__hero h2 {
|
||||
margin: 0 0 8px;
|
||||
font-size: clamp(1.35rem, 4vw, 1.65rem);
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.02em;
|
||||
color: var(--kc-primary, #1b2a6b);
|
||||
}
|
||||
|
||||
.ai-welcome__hero p {
|
||||
margin: 0 auto;
|
||||
max-width: 420px;
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.55;
|
||||
color: var(--kc-secondary, #6a75b8);
|
||||
}
|
||||
|
||||
.ai-quick-section-label {
|
||||
margin: 0 0 12px;
|
||||
padding: 0 4px;
|
||||
@ -748,35 +760,39 @@
|
||||
|
||||
.ai-mcp-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.ai-mcp-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.ai-mcp-grid--waybill {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
.ai-mcp-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 4px;
|
||||
padding: 14px 12px;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 10px;
|
||||
min-height: 92px;
|
||||
padding: 16px 12px;
|
||||
text-align: left;
|
||||
border-radius: 14px;
|
||||
border: none;
|
||||
background: color-mix(in srgb, var(--app-primary-color, #007979) 8%, var(--app-card-bg));
|
||||
box-shadow: 0 2px 8px rgba(15, 23, 42, 0.04);
|
||||
border-radius: 12px;
|
||||
border: 1px solid color-mix(in srgb, var(--color-text-1, #1d2129) 10%, transparent);
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
cursor: pointer;
|
||||
transition: transform 0.15s ease, box-shadow 0.15s ease;
|
||||
transition: opacity 0.15s ease;
|
||||
}
|
||||
|
||||
.ai-mcp-card:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 6px 16px color-mix(in srgb, var(--app-primary-color, #007979) 16%, transparent);
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.ai-mcp-card:active:not(:disabled) {
|
||||
opacity: 0.72;
|
||||
}
|
||||
|
||||
.ai-mcp-card:disabled {
|
||||
@ -785,6 +801,31 @@
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.ai-mcp-card__icon {
|
||||
flex: 0 0 28px;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--app-primary-color, #007979);
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.ai-mcp-card__icon svg,
|
||||
.ai-mcp-card__icon .arco-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.ai-mcp-card__body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 2px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.ai-mcp-card__title {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
@ -799,7 +840,7 @@
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.ai-mcp-grid {
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@ -1199,7 +1240,7 @@
|
||||
}
|
||||
|
||||
.ai-welcome__hero {
|
||||
padding: 16px 0 20px;
|
||||
padding: 8px 0 12px;
|
||||
}
|
||||
|
||||
.ai-avatar {
|
||||
|
||||
@ -158,7 +158,13 @@
|
||||
class="publish-diff-synced-panel"
|
||||
>
|
||||
<slot name="synced">
|
||||
<pre class="diff-pre synced-preview">{{ modifiedJson }}</pre>
|
||||
<pre
|
||||
v-if="(modifiedJson?.length || 0) <= 120000"
|
||||
class="diff-pre synced-preview"
|
||||
>{{ modifiedJson }}</pre>
|
||||
<p v-else class="raw-json-placeholder">
|
||||
快照较大(约 {{ Math.round((modifiedJson?.length || 0) / 1024) }} KB),已省略全文预览以免卡顿。
|
||||
</p>
|
||||
</slot>
|
||||
</div>
|
||||
|
||||
@ -234,7 +240,8 @@
|
||||
@toggle="onRawToggle"
|
||||
>
|
||||
<summary>{{ rawSummaryText }}</summary>
|
||||
<div class="diff-boxes">
|
||||
<!-- 大快照仅在展开后挂载,避免未展开就把 MB 级文本塞进 DOM 卡死 -->
|
||||
<div v-if="rawDetailsOpen" class="diff-boxes">
|
||||
<div class="diff-box">
|
||||
<div class="diff-box-title">{{ originalTitle }}</div>
|
||||
<pre class="diff-pre">{{ originalJson || '{}' }}</pre>
|
||||
@ -244,6 +251,7 @@
|
||||
<pre class="diff-pre">{{ modifiedJson || '{}' }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
<p v-else class="raw-json-placeholder">展开后加载原始 JSON…</p>
|
||||
</details>
|
||||
|
||||
<div v-else-if="rawMode === 'always'" class="diff-boxes">
|
||||
@ -471,6 +479,15 @@
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.raw-json-placeholder {
|
||||
margin: 8px 0 0;
|
||||
padding: 10px 12px;
|
||||
border-radius: 6px;
|
||||
background: var(--color-fill-1, #f7f8fa);
|
||||
color: var(--color-text-3);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.raw-json-details {
|
||||
border: 1px solid var(--color-border-2);
|
||||
border-radius: 6px;
|
||||
|
||||
@ -257,10 +257,11 @@ const modelTab = computed({
|
||||
.shell-tabs {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
justify-content: flex-start;
|
||||
gap: 12px;
|
||||
padding: 10px 24px;
|
||||
padding: 12px 24px;
|
||||
border-bottom: 1px solid var(--color-border-2);
|
||||
background: var(--color-fill-1);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
|
||||
@ -1,58 +1,47 @@
|
||||
<template>
|
||||
<a-drawer
|
||||
:visible="visible"
|
||||
:width="640"
|
||||
:width="drawerWidth"
|
||||
unmount-on-close
|
||||
:esc-to-close="true"
|
||||
:mask-closable="!confirmLoading"
|
||||
class="user-edit-drawer"
|
||||
:header="false"
|
||||
:footer="false"
|
||||
class="user-edit-drawer"
|
||||
:drawer-style="drawerShellStyle"
|
||||
:body-style="drawerBodyStyle"
|
||||
@update:visible="onVisibleChange"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<div class="ue-drawer">
|
||||
<header class="ue-drawer__header">
|
||||
<div class="ue-drawer__title-row">
|
||||
<div>
|
||||
<p class="ue-drawer__eyebrow">{{ t('system.user.drawer.eyebrow') }}</p>
|
||||
<h3 class="ue-drawer__title">
|
||||
<header class="ue-drawer__bar">
|
||||
<div class="ue-drawer__bar-main">
|
||||
<h2 class="ue-drawer__title">
|
||||
{{ isEdit ? t('system.user.modal.edit') : t('system.user.modal.add') }}
|
||||
</h3>
|
||||
</div>
|
||||
<button type="button" class="ue-drawer__close" @click="handleCancel">
|
||||
<icon-close />
|
||||
</h2>
|
||||
<span class="ue-drawer__chip">{{ t('system.user.drawer.eyebrow') }}</span>
|
||||
<button
|
||||
v-if="isEdit && canResetPassword"
|
||||
type="button"
|
||||
class="ue-drawer__action"
|
||||
:disabled="resetPwdLoading || confirmLoading"
|
||||
@click="openResetPasswordModal"
|
||||
>
|
||||
{{ t('system.user.actions.resetPassword') }}
|
||||
</button>
|
||||
</div>
|
||||
<p class="ue-drawer__desc">{{ t('system.user.drawer.desc') }}</p>
|
||||
<button
|
||||
type="button"
|
||||
class="ue-drawer__close"
|
||||
:aria-label="t('common.cancel')"
|
||||
:disabled="confirmLoading"
|
||||
@click="handleCancel"
|
||||
>
|
||||
<icon-close />
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<div class="ue-drawer__body">
|
||||
<section class="ue-preview">
|
||||
<div class="ue-preview__avatar">
|
||||
<img v-if="avatarUrl" :src="avatarUrl" alt="" />
|
||||
<span v-else>{{ previewInitial }}</span>
|
||||
</div>
|
||||
<div class="ue-preview__meta">
|
||||
<h4 class="ue-preview__name">
|
||||
{{ formState.name || t('system.user.drawer.nameFallback') }}
|
||||
</h4>
|
||||
<p class="ue-preview__account">
|
||||
{{ formState.account || t('system.user.drawer.accountFallback') }}
|
||||
<template v-if="formState.employeeNo"> · {{ formState.employeeNo }}</template>
|
||||
</p>
|
||||
<div class="ue-preview__tags">
|
||||
<span v-if="formState.phone" class="ue-preview__tag">{{ formState.phone }}</span>
|
||||
<span v-if="orgDisplayName" class="ue-preview__tag">{{ orgDisplayName }}</span>
|
||||
<span
|
||||
v-if="employeeFormState.employmentStatus"
|
||||
class="ue-preview__tag ue-preview__tag--accent"
|
||||
>
|
||||
{{ employmentStatusText }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<a-form
|
||||
ref="formRef"
|
||||
class="ue-form"
|
||||
@ -62,15 +51,13 @@
|
||||
:label-col-props="{ span: 24 }"
|
||||
:wrapper-col-props="{ span: 24 }"
|
||||
>
|
||||
<section class="ue-section">
|
||||
<div class="ue-section__head">
|
||||
<span class="ue-section__index">01</span>
|
||||
<div>
|
||||
<h5 class="ue-section__title">{{ t('system.user.drawer.section.account') }}</h5>
|
||||
<p class="ue-section__hint">{{ t('system.user.drawer.section.accountHint') }}</p>
|
||||
<section class="ue-card">
|
||||
<div class="ue-card__head">
|
||||
<h4 class="ue-card__title">{{ t('system.user.drawer.section.account') }}</h4>
|
||||
<span class="ue-card__hint">{{ t('system.user.drawer.section.accountHint') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<a-row :gutter="16">
|
||||
<div class="ue-card__body">
|
||||
<a-row :gutter="[16, 0]">
|
||||
<a-col :span="12">
|
||||
<a-form-item :label="t('system.user.form.name')" field="name">
|
||||
<a-input
|
||||
@ -158,17 +145,16 @@
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="ue-section">
|
||||
<div class="ue-section__head">
|
||||
<span class="ue-section__index">02</span>
|
||||
<div>
|
||||
<h5 class="ue-section__title">{{ t('system.user.drawer.section.contact') }}</h5>
|
||||
<p class="ue-section__hint">{{ t('system.user.drawer.section.contactHint') }}</p>
|
||||
<section class="ue-card">
|
||||
<div class="ue-card__head">
|
||||
<h4 class="ue-card__title">{{ t('system.user.drawer.section.contact') }}</h4>
|
||||
<span class="ue-card__hint">{{ t('system.user.drawer.section.contactHint') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<a-row :gutter="16">
|
||||
<div class="ue-card__body">
|
||||
<a-row :gutter="[16, 0]">
|
||||
<a-col :span="12">
|
||||
<a-form-item :label="t('system.user.form.phone')" field="phone">
|
||||
<a-input
|
||||
@ -224,17 +210,17 @@
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="ue-section">
|
||||
<div class="ue-section__head">
|
||||
<span class="ue-section__index">03</span>
|
||||
<div>
|
||||
<h5 class="ue-section__title">{{ t('system.employee.section') }}</h5>
|
||||
<p class="ue-section__hint">{{ t('system.user.drawer.section.employeeHint') }}</p>
|
||||
</div>
|
||||
<section class="ue-card">
|
||||
<div class="ue-card__head">
|
||||
<h4 class="ue-card__title">{{ t('system.employee.section') }}</h4>
|
||||
<span class="ue-card__hint">{{ t('system.user.drawer.section.employeeHint') }}</span>
|
||||
</div>
|
||||
<div class="ue-card__body">
|
||||
<EmployeeProfileFields :model-value="employeeFormState" hide-title dense />
|
||||
</div>
|
||||
</section>
|
||||
</a-form>
|
||||
</div>
|
||||
@ -248,19 +234,53 @@
|
||||
</a-button>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<a-modal
|
||||
v-model:visible="resetPwdVisible"
|
||||
:title="t('system.user.modal.resetPasswordTitle')"
|
||||
:ok-loading="resetPwdLoading"
|
||||
unmount-on-close
|
||||
@ok="handleResetPasswordSubmit"
|
||||
@cancel="closeResetPasswordModal"
|
||||
>
|
||||
<p class="ue-reset-tip">
|
||||
{{
|
||||
t('system.user.modal.resetPasswordContentSingle', {
|
||||
account: formState.account || t('system.user.drawer.accountFallback'),
|
||||
})
|
||||
}}
|
||||
</p>
|
||||
<a-form layout="vertical">
|
||||
<a-form-item :label="t('system.user.form.newPassword')" required>
|
||||
<a-input-password
|
||||
v-model="resetPwdForm.newPassword"
|
||||
:placeholder="t('system.user.form.newPasswordPlaceholder')"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item :label="t('system.user.form.confirmPassword')" required>
|
||||
<a-input-password
|
||||
v-model="resetPwdForm.confirmPassword"
|
||||
:placeholder="t('system.user.form.confirmPasswordPlaceholder')"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</a-drawer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import { IconClose, IconPlus } from '@arco-design/web-vue/es/icon'
|
||||
import EmployeeProfileFields from '@/components/EmployeeProfileFields/index.vue'
|
||||
import { resetUserPassword } from '@/api/rbac/internal-user'
|
||||
import type { EmployeeFormState } from '@/types/employee-profile'
|
||||
import type { UserFormExtState } from '@/types/rbac-user-profile'
|
||||
import { resolveAvatarUrl } from '@/utils/avatar'
|
||||
import { buildAdvisorInviteRegisterUrl } from '@/utils/inviteLink'
|
||||
import { hasPermission } from '@/utils/rbac-permission'
|
||||
import { $baseMessage } from '@/utils'
|
||||
import { readFileAsDataUrl } from '@/utils/image-data-url'
|
||||
|
||||
@ -279,6 +299,7 @@ const props = withDefaults(
|
||||
visible: boolean
|
||||
confirmLoading?: boolean
|
||||
isEdit?: boolean
|
||||
userId?: string
|
||||
formState: UserEditFormState
|
||||
employeeFormState: EmployeeFormState
|
||||
organizationTreeData?: any[]
|
||||
@ -289,6 +310,7 @@ const props = withDefaults(
|
||||
{
|
||||
confirmLoading: false,
|
||||
isEdit: false,
|
||||
userId: '',
|
||||
organizationTreeData: () => [],
|
||||
organizationLoading: false,
|
||||
roleOptions: () => [],
|
||||
@ -304,44 +326,34 @@ const emit = defineEmits<{
|
||||
|
||||
const { t } = useI18n()
|
||||
const formRef = ref()
|
||||
|
||||
const avatarUrl = computed(() => {
|
||||
const raw = props.formState.avatar?.trim()
|
||||
return raw ? resolveAvatarUrl(raw) : ''
|
||||
const resetPwdVisible = ref(false)
|
||||
const resetPwdLoading = ref(false)
|
||||
const resetPwdForm = reactive({
|
||||
newPassword: '',
|
||||
confirmPassword: '',
|
||||
})
|
||||
|
||||
const previewInitial = computed(() => {
|
||||
const name = props.formState.name?.trim()
|
||||
if (name) return name.slice(0, 1)
|
||||
const account = props.formState.account?.trim()
|
||||
return account ? account.slice(0, 1).toUpperCase() : 'U'
|
||||
const canResetPassword = computed(
|
||||
() => hasPermission('Rbac.Users.ResetPassword') || hasPermission('Rbac.Users.Update'),
|
||||
)
|
||||
|
||||
const drawerWidth = computed(() => {
|
||||
if (typeof window !== 'undefined' && window.innerWidth < 768) return '100%'
|
||||
return 640
|
||||
})
|
||||
|
||||
const orgDisplayName = computed(() => {
|
||||
const id = props.formState.organizationId
|
||||
if (!id) return ''
|
||||
const find = (nodes: any[]): string => {
|
||||
for (const node of nodes || []) {
|
||||
if (node.value === id || node.key === id || node.id === id) {
|
||||
return String(node.label || node.title || node.name || '')
|
||||
}
|
||||
const child = find(node.children || [])
|
||||
if (child) return child
|
||||
}
|
||||
return ''
|
||||
}
|
||||
return find(props.organizationTreeData || [])
|
||||
})
|
||||
const drawerShellStyle = {
|
||||
borderRadius: '16px 0 0 16px',
|
||||
overflow: 'hidden',
|
||||
boxShadow: '-12px 0 40px rgba(15, 23, 42, 0.12)',
|
||||
}
|
||||
|
||||
const employmentStatusText = computed(() => {
|
||||
const map: Record<number, string> = {
|
||||
1: t('system.employee.status.probation'),
|
||||
2: t('system.employee.status.regular'),
|
||||
3: t('system.employee.status.suspended'),
|
||||
4: t('system.employee.status.resigned'),
|
||||
}
|
||||
return map[props.employeeFormState.employmentStatus] || ''
|
||||
})
|
||||
const drawerBodyStyle = {
|
||||
padding: '0',
|
||||
background: 'var(--app-page-bg)',
|
||||
height: '100%',
|
||||
overflow: 'hidden',
|
||||
}
|
||||
|
||||
const advisorInviteUrl = computed(() =>
|
||||
buildAdvisorInviteRegisterUrl(String(props.formState.account || '')),
|
||||
@ -358,6 +370,55 @@ async function copyAdvisorInviteLink() {
|
||||
}
|
||||
}
|
||||
|
||||
function openResetPasswordModal() {
|
||||
if (!props.userId) {
|
||||
Message.warning(t('system.user.message.resetPasswordFailed'))
|
||||
return
|
||||
}
|
||||
resetPwdForm.newPassword = ''
|
||||
resetPwdForm.confirmPassword = ''
|
||||
resetPwdVisible.value = true
|
||||
}
|
||||
|
||||
function closeResetPasswordModal() {
|
||||
resetPwdVisible.value = false
|
||||
resetPwdForm.newPassword = ''
|
||||
resetPwdForm.confirmPassword = ''
|
||||
}
|
||||
|
||||
async function handleResetPasswordSubmit() {
|
||||
const newPassword = resetPwdForm.newPassword.trim()
|
||||
const confirmPassword = resetPwdForm.confirmPassword.trim()
|
||||
if (!newPassword) {
|
||||
Message.warning(t('system.user.rules.password.required'))
|
||||
return Promise.reject()
|
||||
}
|
||||
if (newPassword.length < 6) {
|
||||
Message.warning(t('system.user.rules.password.minLength'))
|
||||
return Promise.reject()
|
||||
}
|
||||
if (newPassword !== confirmPassword) {
|
||||
Message.warning(t('system.user.rules.password.confirmMismatch'))
|
||||
return Promise.reject()
|
||||
}
|
||||
if (!props.userId) {
|
||||
Message.warning(t('system.user.message.resetPasswordFailed'))
|
||||
return Promise.reject()
|
||||
}
|
||||
|
||||
resetPwdLoading.value = true
|
||||
try {
|
||||
await resetUserPassword(props.userId, newPassword)
|
||||
Message.success(t('system.user.message.resetPasswordSuccess'))
|
||||
closeResetPasswordModal()
|
||||
} catch (e: any) {
|
||||
Message.error(e?.message || t('system.user.message.resetPasswordFailed'))
|
||||
return Promise.reject(e)
|
||||
} finally {
|
||||
resetPwdLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function onVisibleChange(val: boolean) {
|
||||
emit('update:visible', val)
|
||||
}
|
||||
@ -408,77 +469,105 @@ defineExpose({
|
||||
|
||||
<style scoped lang="less">
|
||||
.ue-drawer {
|
||||
--ue-ink: #1a2e38;
|
||||
--ue-muted: rgba(26, 46, 56, 0.68);
|
||||
--ue-soft: rgba(26, 46, 56, 0.48);
|
||||
--ue-radius: 16px;
|
||||
--ue-edge: var(--app-acrylic-border);
|
||||
--ue-panel-bg: var(--app-acrylic-bg);
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
margin: -16px;
|
||||
background: var(--app-page-bg, #f0f3f4);
|
||||
background: var(--app-page-bg);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.ue-drawer__header {
|
||||
flex-shrink: 0;
|
||||
padding: 18px 20px 14px;
|
||||
border-bottom: 1px solid var(--ue-edge);
|
||||
background: var(--ue-panel-bg);
|
||||
backdrop-filter: var(--app-acrylic-blur);
|
||||
-webkit-backdrop-filter: var(--app-acrylic-blur);
|
||||
}
|
||||
|
||||
.ue-drawer__title-row {
|
||||
.ue-drawer__bar {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
flex-shrink: 0;
|
||||
margin: 12px 12px 0;
|
||||
padding: 12px 14px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.78);
|
||||
border-radius: 14px;
|
||||
background: linear-gradient(155deg, rgba(255, 255, 255, 0.92) 0%, rgba(255, 255, 255, 0.72) 100%);
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.9),
|
||||
0 4px 14px rgba(0, 121, 121, 0.05);
|
||||
}
|
||||
|
||||
.ue-drawer__eyebrow {
|
||||
margin: 0 0 4px;
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.04em;
|
||||
color: var(--app-primary-color);
|
||||
font-weight: 600;
|
||||
.ue-drawer__bar-main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.ue-drawer__title {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
line-height: 1.35;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: var(--ue-ink);
|
||||
color: var(--color-text-1);
|
||||
line-height: 1.3;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.ue-drawer__desc {
|
||||
margin: 8px 0 0;
|
||||
font-size: 13px;
|
||||
line-height: 1.55;
|
||||
color: var(--ue-muted);
|
||||
.ue-drawer__chip {
|
||||
flex-shrink: 0;
|
||||
height: 24px;
|
||||
padding: 0 8px;
|
||||
border-radius: 999px;
|
||||
background: color-mix(in srgb, var(--app-primary-color) 10%, var(--app-card-bg));
|
||||
color: var(--app-primary-color);
|
||||
font-size: 12px;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.ue-drawer__action {
|
||||
flex-shrink: 0;
|
||||
height: 24px;
|
||||
padding: 0 10px;
|
||||
border: 1px solid color-mix(in srgb, var(--app-primary-color) 28%, var(--color-border-2));
|
||||
border-radius: 999px;
|
||||
background: transparent;
|
||||
color: var(--app-primary-color);
|
||||
font-size: 12px;
|
||||
line-height: 22px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s, border-color 0.2s;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: color-mix(in srgb, var(--app-primary-color) 8%, var(--app-card-bg));
|
||||
border-color: var(--app-primary-color);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.ue-drawer__close {
|
||||
flex-shrink: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: 1px solid var(--ue-edge);
|
||||
border-radius: 12px;
|
||||
background: var(--ue-panel-bg);
|
||||
color: var(--ue-muted);
|
||||
border: 1px solid rgba(255, 255, 255, 0.8);
|
||||
border-radius: 10px;
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
color: var(--color-text-2);
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease, color 0.2s ease, border-color 0.2s ease;
|
||||
transition: color 0.2s, background 0.2s, border-color 0.2s;
|
||||
|
||||
&:hover {
|
||||
border-color: color-mix(in srgb, var(--app-primary-color) 22%, var(--ue-edge));
|
||||
background: var(--app-acrylic-hover-bg);
|
||||
&:hover:not(:disabled) {
|
||||
color: var(--app-primary-color);
|
||||
border-color: color-mix(in srgb, var(--app-primary-color) 30%, var(--app-card-bg));
|
||||
background: color-mix(in srgb, var(--app-primary-color) 8%, var(--app-card-bg));
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
@ -486,130 +575,46 @@ defineExpose({
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
padding: 16px 16px 10px;
|
||||
padding: 10px 12px 16px;
|
||||
}
|
||||
|
||||
.ue-preview {
|
||||
display: flex;
|
||||
gap: 14px;
|
||||
align-items: center;
|
||||
margin-bottom: 14px;
|
||||
padding: 14px 16px;
|
||||
border-radius: var(--ue-radius);
|
||||
border: 1px solid var(--ue-edge);
|
||||
background: var(--ue-panel-bg);
|
||||
backdrop-filter: var(--app-acrylic-blur);
|
||||
-webkit-backdrop-filter: var(--app-acrylic-blur);
|
||||
box-shadow: none;
|
||||
color: var(--ue-ink);
|
||||
}
|
||||
|
||||
.ue-preview__avatar {
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
border-radius: 14px;
|
||||
overflow: hidden;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
border: 1px solid var(--ue-edge);
|
||||
background: color-mix(in srgb, var(--app-primary-color) 12%, #fff);
|
||||
color: var(--app-primary-color);
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.ue-preview__name {
|
||||
margin: 0;
|
||||
font-size: 17px;
|
||||
font-weight: 700;
|
||||
line-height: 1.3;
|
||||
color: var(--ue-ink);
|
||||
}
|
||||
|
||||
.ue-preview__account {
|
||||
margin: 4px 0 0;
|
||||
font-size: 12px;
|
||||
color: var(--ue-soft);
|
||||
}
|
||||
|
||||
.ue-preview__tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.ue-preview__tag {
|
||||
display: inline-flex;
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
font-size: 11px;
|
||||
border: 1px solid var(--ue-edge);
|
||||
background: color-mix(in srgb, #fff 55%, transparent);
|
||||
color: var(--ue-muted);
|
||||
|
||||
&--accent {
|
||||
border-color: color-mix(in srgb, var(--app-primary-color) 22%, var(--ue-edge));
|
||||
background: color-mix(in srgb, var(--app-primary-color) 10%, #fff);
|
||||
color: var(--app-primary-color);
|
||||
}
|
||||
}
|
||||
|
||||
.ue-section {
|
||||
margin-bottom: 14px;
|
||||
padding: 14px 14px 6px;
|
||||
border-radius: var(--ue-radius);
|
||||
border: 1px solid var(--ue-edge);
|
||||
background: var(--ue-panel-bg);
|
||||
backdrop-filter: var(--app-acrylic-blur);
|
||||
-webkit-backdrop-filter: var(--app-acrylic-blur);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.ue-section__head {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: flex-start;
|
||||
.ue-card {
|
||||
margin-bottom: 12px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.78);
|
||||
border-radius: 14px;
|
||||
background: var(--app-card-bg);
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.9),
|
||||
0 4px 14px rgba(0, 121, 121, 0.04);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ue-section__index {
|
||||
flex-shrink: 0;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 12px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: var(--app-primary-color);
|
||||
border: 1px solid color-mix(in srgb, var(--app-primary-color) 18%, var(--ue-edge));
|
||||
background: color-mix(in srgb, var(--app-primary-color) 10%, #fff);
|
||||
.ue-card__head {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
padding: 14px 16px 0;
|
||||
}
|
||||
|
||||
.ue-section__title {
|
||||
.ue-card__title {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: var(--ue-ink);
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
|
||||
.ue-section__hint {
|
||||
margin: 2px 0 0;
|
||||
.ue-card__hint {
|
||||
font-size: 12px;
|
||||
color: var(--ue-soft);
|
||||
color: var(--color-text-3);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.ue-card__body {
|
||||
padding: 12px 16px 4px;
|
||||
}
|
||||
|
||||
.ue-form {
|
||||
:deep(.arco-form-item) {
|
||||
margin-bottom: 14px;
|
||||
@ -618,7 +623,7 @@ defineExpose({
|
||||
:deep(.arco-form-item-label) {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--ue-ink);
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
|
||||
:deep(.arco-input-wrapper),
|
||||
@ -626,10 +631,9 @@ defineExpose({
|
||||
:deep(.arco-select-view-multiple),
|
||||
:deep(.arco-textarea-wrapper),
|
||||
:deep(.arco-input-number),
|
||||
:deep(.arco-picker) {
|
||||
border-radius: 12px;
|
||||
border-color: color-mix(in srgb, var(--ue-edge) 70%, #d0d5dd);
|
||||
background: color-mix(in srgb, #fff 72%, transparent);
|
||||
:deep(.arco-picker),
|
||||
:deep(.arco-tree-select-view) {
|
||||
border-radius: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
@ -657,43 +661,43 @@ defineExpose({
|
||||
|
||||
.ue-invite-copy {
|
||||
flex-shrink: 0;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.ue-invite-hint {
|
||||
color: var(--ue-soft);
|
||||
color: var(--color-text-3);
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.ue-reset-tip {
|
||||
margin: 0 0 12px;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
color: var(--color-text-2);
|
||||
}
|
||||
|
||||
.ue-drawer__footer {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
padding: 14px 16px 16px;
|
||||
border-top: 1px solid var(--ue-edge);
|
||||
background: var(--ue-panel-bg);
|
||||
backdrop-filter: var(--app-acrylic-blur);
|
||||
-webkit-backdrop-filter: var(--app-acrylic-blur);
|
||||
margin: 0 12px 12px;
|
||||
padding: 12px 14px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.78);
|
||||
border-radius: 14px;
|
||||
background: linear-gradient(155deg, rgba(255, 255, 255, 0.92) 0%, rgba(255, 255, 255, 0.72) 100%);
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.9),
|
||||
0 4px 14px rgba(0, 121, 121, 0.05);
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="less">
|
||||
.user-edit-drawer {
|
||||
.arco-drawer-header {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.arco-drawer-body {
|
||||
padding: 16px;
|
||||
padding: 0 !important;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background: var(--app-page-bg, #f0f3f4);
|
||||
}
|
||||
|
||||
.arco-drawer {
|
||||
background: var(--app-page-bg, #f0f3f4);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,14 +1,14 @@
|
||||
<template>
|
||||
<div id="top-nav" class="top-nav" :class="{ tabNavEnable: tabNavEnable }">
|
||||
<div class="top-nav-header">
|
||||
<!-- Logo 区域 -->
|
||||
<div class="top-nav-logo">
|
||||
<!-- Logo 区域:点击跳转官网首页 -->
|
||||
<a class="top-nav-logo" href="/" :title="t('brand.fullName')" @click.prevent="goOfficialSite">
|
||||
<img :src="navLogo" :alt="t('brand.name')" />
|
||||
<div class="top-nav-logo__brand">
|
||||
<span class="top-nav-logo__name">{{ t('brand.name') }}</span>
|
||||
<span class="top-nav-logo__full">{{ t('brand.fullName') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<!-- 顶部水平菜单:磁吸滑块 + 极速 hover 下拉 -->
|
||||
<div
|
||||
@ -175,6 +175,9 @@
|
||||
<a-doption @click="() => onBalanceMenuClick({ key: 'accountFlow' })">
|
||||
{{ t('nav.balance.accountFlow') }}
|
||||
</a-doption>
|
||||
<a-doption :disabled="balanceRefreshing" @click="onRefreshBalance">
|
||||
{{ t('nav.balance.refresh') }}
|
||||
</a-doption>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</div>
|
||||
@ -206,6 +209,23 @@
|
||||
</a-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="top-nav-right-item top-nav-right-item--icon">
|
||||
<a-tooltip position="bottom" :mouse-enter-delay="0" :mouse-leave-delay="0">
|
||||
<template #content>
|
||||
<span>{{ t('nav.printControl.download') }}</span>
|
||||
</template>
|
||||
<a
|
||||
class="print-connection-trigger print-control-download"
|
||||
:href="PRINT_CONTROL_DOWNLOAD_URL"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
:aria-label="t('nav.printControl.download')"
|
||||
>
|
||||
<icon-download class="print-connection-icon" />
|
||||
</a>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="top-nav-right-item top-nav-right-item--warehouse">
|
||||
<ContactService variant="navbar" />
|
||||
</div>
|
||||
@ -246,7 +266,7 @@ import RightTopUser from './RightTopUser.vue';
|
||||
import RightTopMessage from './RightTopMessage.vue';
|
||||
import SelectLang from './SelectLang.vue';
|
||||
import ContactService from '@/components/ContactService/index.vue';
|
||||
import { IconPrinter } from '@arco-design/web-vue/es/icon';
|
||||
import { IconPrinter, IconDownload } from '@arco-design/web-vue/es/icon';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import { openWindow, regexUrl } from '@/utils';
|
||||
import { MenuService, AuthService } from '@/api/rbac';
|
||||
@ -264,7 +284,7 @@ import { canAccessCustomerPortalRoute, isAdminPortalOnlyPath, isCustomerPortalOn
|
||||
import { resolveCurrentCustomer } from '@/api/customer/resolve';
|
||||
import { CUSTOMER_BALANCE_CHANGED_EVENT } from '@/api/customer/accountFlow';
|
||||
import { resolvePortalDrawerPanel } from '@/views/user/portal-drawer/types';
|
||||
import { openPortalSideDrawer } from '@/views/user/portal-drawer/usePortalSideDrawer';
|
||||
import { PRINT_CONTROL_DOWNLOAD_URL } from '@/constants/printControl';
|
||||
|
||||
const { t } = useI18n();
|
||||
const route = useRoute();
|
||||
@ -278,6 +298,11 @@ const topNavEnable = computed(() => true); // 启用顶部导航
|
||||
const tabNavEnable = computed(() => appStore.tabBar);
|
||||
const printConnectionState = useHiprintClientState();
|
||||
|
||||
/** 顶栏 Logo → 官网首页(开发环境即 http://localhost:5173/) */
|
||||
function goOfficialSite() {
|
||||
window.location.href = '/';
|
||||
}
|
||||
|
||||
const menuItemEls = new Map<string, HTMLElement>();
|
||||
const indicatorReady = ref(false);
|
||||
const indicatorVisible = ref(false);
|
||||
@ -287,6 +312,8 @@ const indicatorStyle = ref<Record<string, string>>({
|
||||
});
|
||||
/** 磁吸:当前指针悬停的一级菜单;空则吸回路由选中项 */
|
||||
const hoverMenuKey = ref('');
|
||||
/** 跳转中目标一级(路由尚未变时避免滑块先吸回旧项再弹到新项) */
|
||||
const pendingNavTopKey = ref('');
|
||||
/** 下拉进入/离开延迟(ms):偏快,横向扫菜单时几乎无感 */
|
||||
const SUBMENU_ENTER_DELAY = 20;
|
||||
const SUBMENU_LEAVE_DELAY = 120;
|
||||
@ -316,6 +343,7 @@ function setMenuItemRef(key: string, el: Element | ComponentPublicInstance | nul
|
||||
function resolveIndicatorKey() {
|
||||
if (hoverMenuKey.value) return hoverMenuKey.value;
|
||||
if (openedSubmenuKey.value) return openedSubmenuKey.value;
|
||||
if (pendingNavTopKey.value) return pendingNavTopKey.value;
|
||||
return isOverflowActive.value ? MORE_MENU_KEY : belongTopMenu.value;
|
||||
}
|
||||
|
||||
@ -365,6 +393,7 @@ function onMenuItemPointerEnter(key: string) {
|
||||
}
|
||||
|
||||
function onMenuBarPointerLeave(e: PointerEvent) {
|
||||
if (navPaintLock.value) return;
|
||||
const next = e.relatedTarget as Node | null;
|
||||
// 指针进入二级浮层时保持当前一级高亮,不吸回
|
||||
if (
|
||||
@ -378,6 +407,7 @@ function onMenuBarPointerLeave(e: PointerEvent) {
|
||||
hoverMenuKey.value = '';
|
||||
moveIndicatorToKey(
|
||||
openedSubmenuKey.value ||
|
||||
pendingNavTopKey.value ||
|
||||
(isOverflowActive.value ? MORE_MENU_KEY : belongTopMenu.value),
|
||||
);
|
||||
}
|
||||
@ -397,7 +427,14 @@ async function onTopMenuClick(e: MouseEvent, item: any) {
|
||||
e.preventDefault();
|
||||
const children = filterPage(item.children);
|
||||
if (children.length > 0) {
|
||||
paintNavThenGo(children[0]);
|
||||
// 有二级时只展开面板,不抢先进第一项(否则再点目标页等于连续加载两次)
|
||||
if (openedSubmenuKey.value === MORE_MENU_KEY) {
|
||||
return;
|
||||
}
|
||||
const key = menuKey(item);
|
||||
openedSubmenuKey.value = key;
|
||||
hoverMenuKey.value = key;
|
||||
moveIndicatorToKey(key);
|
||||
return;
|
||||
}
|
||||
paintNavThenGo(item);
|
||||
@ -751,6 +788,19 @@ function onSubmenuPopupVisibleChange(key: string, visible: boolean) {
|
||||
openedSubmenuKey.value = key;
|
||||
hoverMenuKey.value = key;
|
||||
moveIndicatorToKey(key);
|
||||
// 展开一级时预取二级路由 chunk,缩短点击后白屏
|
||||
if (key === MORE_MENU_KEY) {
|
||||
overflowTopMenus.value.forEach((item) => {
|
||||
filterPage(item.children).forEach((child) => prefetchMenuRoute(child));
|
||||
});
|
||||
} else {
|
||||
const parent =
|
||||
fittedTopMenus.value.find((m) => menuKey(m) === key) ||
|
||||
overflowTopMenus.value.find((m) => menuKey(m) === key);
|
||||
if (parent) {
|
||||
filterPage(parent.children).forEach((child) => prefetchMenuRoute(child));
|
||||
}
|
||||
}
|
||||
nextTick(() => {
|
||||
requestAnimationFrame(() => {
|
||||
const el = document.querySelector(
|
||||
@ -1038,15 +1088,44 @@ function previewMenuNavigation(item: any) {
|
||||
}
|
||||
}
|
||||
|
||||
/** 二级项所属一级(溢出则落到「更多」) */
|
||||
function findTopMenuKeyForItem(item: any): string {
|
||||
const targetKey = menuKey(item);
|
||||
const targetPath = String(item?.path || item?.route || '');
|
||||
if (!targetKey && !targetPath) return '';
|
||||
|
||||
for (const menu of visibleTopMenus.value) {
|
||||
const topKey = menuKey(menu);
|
||||
if (topKey && topKey === targetKey) {
|
||||
return overflowTopMenus.value.some((m) => menuKey(m) === topKey)
|
||||
? MORE_MENU_KEY
|
||||
: topKey;
|
||||
}
|
||||
for (const child of filterPage(menu.children) || []) {
|
||||
const childPath = String(child.path || child.route || '');
|
||||
const childMatched =
|
||||
(targetKey && menuKey(child) === targetKey) ||
|
||||
(targetPath && childPath === targetPath);
|
||||
if (!childMatched) continue;
|
||||
return overflowTopMenus.value.some((m) => menuKey(m) === topKey)
|
||||
? MORE_MENU_KEY
|
||||
: topKey;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/** 先关弹层、亮标签,下一帧再走路由,避免页面重渲染把顶栏一起卡住 */
|
||||
let navGoingTo = '';
|
||||
function paintNavThenGo(item: any) {
|
||||
const rawPath = String(item?.path || item?.route || '');
|
||||
if (navGoingTo && navGoingTo === rawPath) return;
|
||||
navGoingTo = rawPath;
|
||||
// 先锁再关弹层,避免 leave/关闭回调把滑块吸回旧一级
|
||||
navPaintLock.value = true;
|
||||
pendingNavTopKey.value = findTopMenuKeyForItem(item);
|
||||
openedSubmenuKey.value = '';
|
||||
hoverMenuKey.value = '';
|
||||
navPaintLock.value = true;
|
||||
if (navPaintUnlockTimer) {
|
||||
window.clearTimeout(navPaintUnlockTimer);
|
||||
}
|
||||
@ -1057,8 +1136,17 @@ function paintNavThenGo(item: any) {
|
||||
goto(item);
|
||||
navPaintUnlockTimer = window.setTimeout(() => {
|
||||
navPaintLock.value = false;
|
||||
// 路由已切到目标时清掉 pending;否则留到 belongTopMenu 对齐再清
|
||||
if (
|
||||
!pendingNavTopKey.value ||
|
||||
pendingNavTopKey.value === belongTopMenu.value ||
|
||||
(pendingNavTopKey.value === MORE_MENU_KEY && isOverflowActive.value)
|
||||
) {
|
||||
pendingNavTopKey.value = '';
|
||||
}
|
||||
navPaintUnlockTimer = 0;
|
||||
navGoingTo = '';
|
||||
updateActiveIndicator();
|
||||
}, 280);
|
||||
});
|
||||
});
|
||||
@ -1072,6 +1160,8 @@ const gotoFromSubmenu = (item: any) => {
|
||||
const showBalanceNav = computed(() => rbacUserStore.isCustomerPortal());
|
||||
|
||||
const customerBalance = ref<number | null>(null);
|
||||
const balanceRefreshing = ref(false);
|
||||
let balanceInflight: Promise<void> | null = null;
|
||||
|
||||
const balanceDisplay = computed(() => {
|
||||
const raw = customerBalance.value;
|
||||
@ -1082,15 +1172,15 @@ const balanceDisplay = computed(() => {
|
||||
return Number.isFinite(parsed) ? parsed.toFixed(2) : '0.00';
|
||||
});
|
||||
|
||||
async function loadCustomerBalance() {
|
||||
async function fetchCustomerBalance() {
|
||||
if (!rbacUserStore.isCustomerPortal() || !rbacUserStore.isLoggedIn) {
|
||||
customerBalance.value = null;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const mapId = getMapIdFromAccessToken(rbacUserStore.accessToken);
|
||||
const account = rbacUserStore.userInfo?.account?.trim() || '';
|
||||
const data = await resolveCurrentCustomer({ mapId, account });
|
||||
// 跳过 session 缓存:F5 也要拿到当前余额,不能沿用登录时写入的旧值
|
||||
const data = await resolveCurrentCustomer({ mapId, account, force: true });
|
||||
const raw = data?.accountBalance ?? data?.balance;
|
||||
if (raw == null || raw === '') {
|
||||
customerBalance.value = 0;
|
||||
@ -1098,9 +1188,30 @@ async function loadCustomerBalance() {
|
||||
}
|
||||
const parsed = Number(raw);
|
||||
customerBalance.value = Number.isFinite(parsed) ? parsed : 0;
|
||||
}
|
||||
|
||||
function loadCustomerBalance() {
|
||||
if (!balanceInflight) {
|
||||
balanceInflight = fetchCustomerBalance().finally(() => {
|
||||
balanceInflight = null;
|
||||
});
|
||||
}
|
||||
return balanceInflight;
|
||||
}
|
||||
|
||||
async function onRefreshBalance() {
|
||||
if (balanceRefreshing.value) return;
|
||||
balanceRefreshing.value = true;
|
||||
const previous = customerBalance.value;
|
||||
try {
|
||||
await loadCustomerBalance();
|
||||
Message.success(t('nav.balance.refreshed'));
|
||||
} catch (error) {
|
||||
console.error('Failed to load customer balance', error);
|
||||
customerBalance.value = 0;
|
||||
customerBalance.value = previous;
|
||||
Message.error(t('nav.balance.refreshFailed'));
|
||||
} finally {
|
||||
balanceRefreshing.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1119,18 +1230,30 @@ const onBalanceMenuClick = (event: any) => {
|
||||
watch(
|
||||
() => [rbacUserStore.isLoggedIn, rbacUserStore.loginPortalType, rbacUserStore.accessToken] as const,
|
||||
() => {
|
||||
void loadCustomerBalance();
|
||||
void loadCustomerBalance().catch((error) => {
|
||||
console.error('Failed to load customer balance', error);
|
||||
if (customerBalance.value == null) customerBalance.value = 0;
|
||||
});
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
function handleCustomerBalanceChanged() {
|
||||
void loadCustomerBalance();
|
||||
void loadCustomerBalance().catch((error) => {
|
||||
console.error('Failed to load customer balance', error);
|
||||
});
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [belongTopMenu.value, menuDataReady.value, visibleTopMenus.value.length] as const,
|
||||
async () => {
|
||||
if (
|
||||
pendingNavTopKey.value &&
|
||||
(pendingNavTopKey.value === belongTopMenu.value ||
|
||||
(pendingNavTopKey.value === MORE_MENU_KEY && isOverflowActive.value))
|
||||
) {
|
||||
pendingNavTopKey.value = '';
|
||||
}
|
||||
await nextTick();
|
||||
await recalcMenuOverflow();
|
||||
},
|
||||
@ -1193,6 +1316,9 @@ onUnmounted(() => {
|
||||
gap: 10px;
|
||||
padding: 0 16px;
|
||||
flex-shrink: 0;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
|
||||
img {
|
||||
height: 36px;
|
||||
@ -1272,7 +1398,7 @@ onUnmounted(() => {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: inline-block;
|
||||
padding: 6px 20px;
|
||||
padding: 6px 16px;
|
||||
height: auto;
|
||||
line-height: 1.5;
|
||||
text-decoration: none;
|
||||
@ -1284,7 +1410,7 @@ onUnmounted(() => {
|
||||
color 0.1s ease,
|
||||
opacity 0.1s ease,
|
||||
transform 0.1s cubic-bezier(0.22, 1, 0.36, 1);
|
||||
margin: 0 16px;
|
||||
margin: 0 10px;
|
||||
cursor: pointer;
|
||||
background-color: transparent;
|
||||
|
||||
|
||||
@ -3,11 +3,12 @@
|
||||
*
|
||||
* | 目标 | 前端 | API |
|
||||
* |------|------|-----|
|
||||
* | ideship(生产) | https://ideship.com | https://api.ideship.com/{admin,itfx,track} |
|
||||
* | nanafa(测试) | https://oms.nanafa.com | http://api.nanafa.com/{admin,itfx,track} |
|
||||
* | ideship(生产构建) | https://ideship.com | https://api.ideship.com/{admin,itfx,track} |
|
||||
* | ideship 测试(本地默认) | https://test.ideship.com | https://api.test.ideship.com/{admin,itfx,track} |
|
||||
* | nanafa(旧测试) | https://oms.nanafa.com | http://api.nanafa.com/{admin,itfx,track} |
|
||||
*
|
||||
* 本地:pnpm dev(生产 API) / pnpm dev:test(测试 API)
|
||||
* 构建:pnpm build(生产) / pnpm build:test(测试)
|
||||
* 本地:pnpm dev / pnpm dev:test → 仅测试 API(禁止直连生产)
|
||||
* 构建:pnpm build:prod(生产) / pnpm build:test(测试)
|
||||
*/
|
||||
export type DeployTarget = 'ideship' | 'nanafa'
|
||||
|
||||
@ -28,10 +29,19 @@ export const DEPLOY_TARGETS = {
|
||||
},
|
||||
} as const
|
||||
|
||||
/** 本地开发 / 测试构建默认的 ideship 测试网关(与 DEPLOY_TARGETS.ideship 生产区分) */
|
||||
export const IDESHIP_TEST_API = {
|
||||
siteOrigin: 'https://test.ideship.com',
|
||||
apiOrigin: 'https://api.test.ideship.com',
|
||||
adminBase: 'https://api.test.ideship.com/admin',
|
||||
itfxBase: 'https://api.test.ideship.com/itfx',
|
||||
trackBase: 'https://api.test.ideship.com/track',
|
||||
} as const
|
||||
|
||||
export function resolveDeployTarget(): DeployTarget {
|
||||
const raw = String(import.meta.env.VITE_DEPLOY_TARGET ?? '').trim().toLowerCase()
|
||||
if (raw === 'nanafa') return 'nanafa'
|
||||
if (raw === 'ideship') return 'ideship'
|
||||
// 未显式配置时:生产构建默认 ideship,开发默认 nanafa
|
||||
// 未显式配置:仅生产构建默认 ideship;开发默认走测试(由 .env.development 注入 URL)
|
||||
return import.meta.env.PROD ? 'ideship' : 'nanafa'
|
||||
}
|
||||
|
||||