feat: 接入企业微信客服,并同步工作台与下单页改动
官网/H5 支持企微客服跳转;用户资料新增 WecomKfUrl;工作台与下单相关体验一并更新。 Co-authored-by: Cursor <cursoragent@cursor.com>
@ -2312,9 +2312,74 @@ body.command-home .ai-stage-map--eu {
|
||||
|
||||
/* Composer + trust strip */
|
||||
.ai-composer-wrap {
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.ai-new-chat-tip {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
bottom: calc(100% + 10px);
|
||||
z-index: 6;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
max-width: min(92%, 360px);
|
||||
padding: 8px 14px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.78);
|
||||
background: rgba(5, 79, 116, 0.92);
|
||||
color: #fff;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.3;
|
||||
letter-spacing: 0.01em;
|
||||
white-space: nowrap;
|
||||
pointer-events: none;
|
||||
transform: translate(-50%, 6px);
|
||||
opacity: 0;
|
||||
box-shadow: 0 10px 28px rgba(5, 79, 116, 0.22);
|
||||
transition:
|
||||
opacity 0.28s var(--ai-ease),
|
||||
transform 0.28s var(--ai-ease);
|
||||
}
|
||||
|
||||
.ai-new-chat-tip::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 100%;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
margin-left: -5px;
|
||||
background: rgba(5, 79, 116, 0.92);
|
||||
border-right: 1px solid rgba(255, 255, 255, 0.5);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.5);
|
||||
transform: rotate(45deg) translateY(-4px);
|
||||
}
|
||||
|
||||
.ai-new-chat-tip[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.ai-new-chat-tip.is-visible {
|
||||
opacity: 1;
|
||||
transform: translate(-50%, 0);
|
||||
}
|
||||
|
||||
.ai-composer-wrap.has-new-chat-tip .ai-composer {
|
||||
animation: ai-composer-tip-pulse 1.1s var(--ai-ease) 2;
|
||||
}
|
||||
|
||||
@keyframes ai-composer-tip-pulse {
|
||||
0%,
|
||||
100% {
|
||||
box-shadow: none;
|
||||
}
|
||||
45% {
|
||||
box-shadow: 0 0 0 3px rgba(5, 79, 116, 0.18);
|
||||
}
|
||||
}
|
||||
|
||||
.ai-composer-wrap.is-welcome {
|
||||
padding: 0;
|
||||
border-top: none;
|
||||
@ -2444,16 +2509,22 @@ body.command-home .ai-stage-map--eu {
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.ai-composer__sheen,
|
||||
.ai-composer__caustic,
|
||||
.ai-intent-door::after {
|
||||
.ai-intent-door::after,
|
||||
.ai-composer-wrap.has-new-chat-tip .ai-composer {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
.ai-new-chat-tip {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
||||
.ai-trust-bar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px 18px;
|
||||
margin-top: 4px;
|
||||
padding: 14px 4px 16px;
|
||||
border-top: 1px solid color-mix(in srgb, var(--ai-border) 55%, transparent);
|
||||
@ -2461,23 +2532,6 @@ body.command-home .ai-stage-map--eu {
|
||||
font-size: 0.72rem;
|
||||
}
|
||||
|
||||
.ai-trust-bar__contacts {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px 22px;
|
||||
}
|
||||
|
||||
.ai-trust-bar__meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px 18px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.ai-trust-bar a,
|
||||
.ai-trust-wechat {
|
||||
display: inline-flex;
|
||||
@ -2526,19 +2580,19 @@ body.command-home .ai-stage-map--eu {
|
||||
|
||||
.ai-trust-address {
|
||||
display: inline-flex;
|
||||
align-items: flex-start;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
margin: 0;
|
||||
max-width: min(100%, 560px);
|
||||
color: var(--ai-text-3);
|
||||
font-size: 0.72rem;
|
||||
line-height: 1.45;
|
||||
text-align: left;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.ai-trust-address i {
|
||||
margin-top: 2px;
|
||||
margin-top: 0;
|
||||
font-size: 0.88rem;
|
||||
line-height: 1;
|
||||
color: var(--ai-action);
|
||||
@ -2722,15 +2776,8 @@ body.command-home .ai-stage-map--eu {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.ai-trust-bar__contacts {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.ai-trust-bar__meta {
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
.ai-trust-bar {
|
||||
gap: 8px 14px;
|
||||
}
|
||||
|
||||
.ai-trust-address {
|
||||
@ -2738,6 +2785,13 @@ body.command-home .ai-stage-map--eu {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.ai-new-chat-tip {
|
||||
max-width: min(94vw, 320px);
|
||||
white-space: normal;
|
||||
text-align: center;
|
||||
border-radius: 14px;
|
||||
}
|
||||
|
||||
.ai-composer {
|
||||
min-height: 76px;
|
||||
padding: 12px 12px 12px 18px;
|
||||
@ -2754,3 +2808,123 @@ body.command-home .ai-stage-map--eu {
|
||||
height: 46px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 企业微信客服 — PC 扫码浮层 */
|
||||
.ide-kf-modal {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 10050;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.ide-kf-modal__backdrop {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(12, 18, 28, 0.55);
|
||||
backdrop-filter: blur(4px);
|
||||
-webkit-backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
.ide-kf-modal__panel {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: min(100%, 360px);
|
||||
padding: 28px 24px 22px;
|
||||
border-radius: 16px;
|
||||
border: 1px solid color-mix(in srgb, var(--ai-border) 80%, transparent);
|
||||
background: var(--ai-surface, #fff);
|
||||
box-shadow: 0 24px 64px rgba(12, 18, 28, 0.22);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.ide-kf-modal__close {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background: transparent;
|
||||
color: var(--ai-text-3, #6b7280);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ide-kf-modal__close:hover {
|
||||
color: var(--ai-text, #111);
|
||||
background: color-mix(in srgb, var(--ai-border) 40%, transparent);
|
||||
}
|
||||
|
||||
.ide-kf-modal__kicker {
|
||||
margin: 0 0 6px;
|
||||
font-size: 0.75rem;
|
||||
letter-spacing: 0.04em;
|
||||
color: var(--ai-text-3, #6b7280);
|
||||
}
|
||||
|
||||
.ide-kf-modal__title {
|
||||
margin: 0 0 8px;
|
||||
font-size: 1.15rem;
|
||||
font-weight: 650;
|
||||
color: var(--ai-text, #111);
|
||||
}
|
||||
|
||||
.ide-kf-modal__hint {
|
||||
margin: 0 0 18px;
|
||||
font-size: 0.84rem;
|
||||
line-height: 1.5;
|
||||
color: var(--ai-text-2, #4b5563);
|
||||
}
|
||||
|
||||
.ide-kf-modal__qr-wrap {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
margin: 0 auto 16px;
|
||||
padding: 12px;
|
||||
width: fit-content;
|
||||
border-radius: 12px;
|
||||
background: #fff;
|
||||
border: 1px solid color-mix(in srgb, var(--ai-border) 70%, transparent);
|
||||
}
|
||||
|
||||
.ide-kf-modal__qr {
|
||||
display: block;
|
||||
width: 220px;
|
||||
height: 220px;
|
||||
}
|
||||
|
||||
.ide-kf-modal__qr-fallback {
|
||||
margin: 0;
|
||||
max-width: 220px;
|
||||
font-size: 0.8rem;
|
||||
line-height: 1.45;
|
||||
color: var(--ai-text-2, #4b5563);
|
||||
}
|
||||
|
||||
.ide-kf-modal__open {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 36px;
|
||||
padding: 0 14px;
|
||||
border-radius: 8px;
|
||||
font-size: 0.84rem;
|
||||
font-weight: 600;
|
||||
color: var(--ai-action, #1d7281);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.ide-kf-modal__open:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
button.ai-card-link {
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@ -10,6 +10,8 @@
|
||||
const PHONE = '18123959245';
|
||||
const EMAIL = 'bd01@1st56.com';
|
||||
const WECHAT = 'ydgjzzh';
|
||||
// 企业微信管理后台 → 微信客服 → 在微信外接入 → 复制客服链接(勿带尾部空格)
|
||||
const WECOM_KF_URL = 'https://work.weixin.qq.com/kfid/kfcae09c206f6c7045a';
|
||||
const ICP = '粤ICP备2025463970号-2';
|
||||
const ADDRESS_ZH = '深圳宝安区福永怀德翠岗5区36栋';
|
||||
const ADDRESS_EN = 'Bldg 36, Cuigang 5, Fuyong, Bao’an, Shenzhen';
|
||||
@ -33,8 +35,8 @@
|
||||
|
||||
const COPY = {
|
||||
en: {
|
||||
welcomeTitle: 'IDE, a digital one-stop cross-border logistics provider',
|
||||
welcomeTitleShort: 'IDE · Cross-border logistics',
|
||||
welcomeTitle: 'IDE, your cross-border logistics advisor',
|
||||
welcomeTitleShort: 'IDE · Your logistics advisor',
|
||||
welcomeSubLead: 'Parcels, express, dropship, and 9610 clearance — ',
|
||||
welcomeSubAccent: 'built for SMB cross-border sellers',
|
||||
welcomeSubTail: '.',
|
||||
@ -44,11 +46,12 @@
|
||||
you: 'You',
|
||||
historyTitle: 'Chat history',
|
||||
newChat: 'New chat',
|
||||
newChatHint: 'New chat started — ask here',
|
||||
emptyHistory: 'No conversations yet',
|
||||
fallback: 'Quotes are in the customer system after you register. You can also talk to an advisor.',
|
||||
trustPhone: PHONE,
|
||||
trustAddress: ADDRESS_EN,
|
||||
trustWechat: 'WeChat ' + WECHAT,
|
||||
trustWechat: 'WeChat support',
|
||||
trustEmail: EMAIL,
|
||||
trustIcp: ICP,
|
||||
guide9610Title: 'Ship first, tax second',
|
||||
@ -64,8 +67,8 @@
|
||||
composeLead: 'Ask about a lane, or pick a shortcut below',
|
||||
},
|
||||
zh: {
|
||||
welcomeTitle: 'IDE,全球数字化一站式跨境物流服务商',
|
||||
welcomeTitleShort: 'IDE · 一站式跨境物流',
|
||||
welcomeTitle: 'IDE,您身边的跨境物流顾问',
|
||||
welcomeTitleShort: 'IDE · 跨境物流顾问',
|
||||
welcomeSubLead: '专线小包、国际快递、一件代发与 9610 报关,',
|
||||
welcomeSubAccent: '专为中小跨境卖家而生',
|
||||
welcomeSubTail: '。',
|
||||
@ -75,11 +78,12 @@
|
||||
you: '你',
|
||||
historyTitle: '历史记录',
|
||||
newChat: '新会话',
|
||||
newChatHint: '已开启新会话,在这里提问',
|
||||
emptyHistory: '暂无会话',
|
||||
fallback: '官网不提供询价。完成注册登记后,即可在客户系统里看明码价。也可以先联系顾问。',
|
||||
trustPhone: PHONE,
|
||||
trustAddress: ADDRESS_ZH,
|
||||
trustWechat: '微信 ' + WECHAT,
|
||||
trustWechat: '微信客服',
|
||||
trustEmail: EMAIL,
|
||||
trustIcp: ICP,
|
||||
guide9610Title: '先发货,再办税',
|
||||
@ -365,6 +369,7 @@
|
||||
let historyListEl;
|
||||
let globeController = null;
|
||||
let coverageOpen = false;
|
||||
let newChatTipTimer = null;
|
||||
|
||||
const MAP_REGION_EU = new Set(['eu'].concat(COVER_EU.map((item) => item.mapId)));
|
||||
const MAP_REGION_AM = new Set(['us', 'mx', 'ca']);
|
||||
@ -696,7 +701,8 @@
|
||||
const rows = el('div', 'ai-card__rows');
|
||||
[
|
||||
[zh ? '电话' : 'Phone', PHONE],
|
||||
[zh ? '微信' : 'WeChat', WECHAT],
|
||||
[zh ? '微信客服' : 'WeCom', zh ? '扫码 / 手机直达' : 'Scan or tap to chat'],
|
||||
[zh ? '微信号' : 'WeChat ID', WECHAT],
|
||||
[zh ? '邮箱' : 'Email', EMAIL],
|
||||
[zh ? '地址' : 'Address', zh ? ADDRESS_ZH : ADDRESS_EN],
|
||||
].forEach(([label, value]) => {
|
||||
@ -710,7 +716,9 @@
|
||||
el(
|
||||
'p',
|
||||
'ai-card__body',
|
||||
zh ? '要询价请先注册登记,或直接打电话、加微信。' : 'Register first to quote, or call / WeChat us directly.'
|
||||
zh
|
||||
? '点「在线咨询」用微信对接顾问。询价请先注册登记。'
|
||||
: 'Tap Live chat to reach an advisor on WeChat. Register first if you need a quote.'
|
||||
)
|
||||
);
|
||||
wrap.appendChild(contactActions());
|
||||
@ -817,15 +825,90 @@
|
||||
return actions;
|
||||
}
|
||||
|
||||
function isWeChatEnv() {
|
||||
return /MicroMessenger/i.test(navigator.userAgent);
|
||||
}
|
||||
|
||||
function isMobileEnv() {
|
||||
return /Android|iPhone|iPad|iPod|Mobile/i.test(navigator.userAgent);
|
||||
}
|
||||
|
||||
function closeKfQrModal() {
|
||||
document.getElementById('ide-kf-modal')?.remove();
|
||||
}
|
||||
|
||||
function showKfQrModal(url) {
|
||||
closeKfQrModal();
|
||||
const zh = lang === 'zh';
|
||||
const modal = document.createElement('div');
|
||||
modal.id = 'ide-kf-modal';
|
||||
modal.className = 'ide-kf-modal';
|
||||
modal.setAttribute('role', 'dialog');
|
||||
modal.setAttribute('aria-modal', 'true');
|
||||
modal.setAttribute('aria-label', zh ? '微信扫码咨询' : 'Scan to chat on WeChat');
|
||||
const qrSrc =
|
||||
'https://api.qrserver.com/v1/create-qr-code/?size=220x220&margin=8&data=' +
|
||||
encodeURIComponent(url);
|
||||
modal.innerHTML = `
|
||||
<div class="ide-kf-modal__backdrop" data-kf-close></div>
|
||||
<div class="ide-kf-modal__panel">
|
||||
<button type="button" class="ide-kf-modal__close" data-kf-close aria-label="${zh ? '关闭' : 'Close'}">
|
||||
<i class="bi bi-x-lg" aria-hidden="true"></i>
|
||||
</button>
|
||||
<p class="ide-kf-modal__kicker">${zh ? '企业微信客服' : 'WeCom support'}</p>
|
||||
<h3 class="ide-kf-modal__title">${zh ? '微信扫码咨询' : 'Scan with WeChat'}</h3>
|
||||
<p class="ide-kf-modal__hint">${zh ? '打开手机微信,扫一扫下方二维码即可对话顾问' : 'Open WeChat on your phone and scan the code to chat'}</p>
|
||||
<div class="ide-kf-modal__qr-wrap">
|
||||
<img class="ide-kf-modal__qr" src="${qrSrc}" width="220" height="220" alt="WeCom QR">
|
||||
<p class="ide-kf-modal__qr-fallback" hidden>${zh ? '二维码加载失败,请点下方打开客服页后用微信扫码' : 'QR failed to load — open the support page and scan there'}</p>
|
||||
</div>
|
||||
<a class="ide-kf-modal__open" href="${url}" target="_blank" rel="noopener noreferrer">${zh ? '在新窗口打开客服页' : 'Open support page'}</a>
|
||||
</div>
|
||||
`;
|
||||
const qrImg = modal.querySelector('.ide-kf-modal__qr');
|
||||
const qrFallback = modal.querySelector('.ide-kf-modal__qr-fallback');
|
||||
qrImg?.addEventListener('error', () => {
|
||||
qrImg.hidden = true;
|
||||
if (qrFallback) qrFallback.hidden = false;
|
||||
});
|
||||
modal.addEventListener('click', (e) => {
|
||||
if (e.target.closest('[data-kf-close]')) closeKfQrModal();
|
||||
});
|
||||
document.addEventListener(
|
||||
'keydown',
|
||||
function onEsc(e) {
|
||||
if (e.key === 'Escape') {
|
||||
closeKfQrModal();
|
||||
document.removeEventListener('keydown', onEsc);
|
||||
}
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
document.body.appendChild(modal);
|
||||
}
|
||||
|
||||
/** 手机 / 微信内:直跳唤起会话;PC:弹二维码浮层 */
|
||||
function openWecomKf(e) {
|
||||
if (e) e.preventDefault();
|
||||
const url = WECOM_KF_URL;
|
||||
if (isWeChatEnv() || isMobileEnv()) {
|
||||
window.location.href = url;
|
||||
return;
|
||||
}
|
||||
showKfQrModal(url);
|
||||
}
|
||||
|
||||
function contactActions() {
|
||||
const actions = el('div', 'ai-card__actions');
|
||||
const tel = el('a', 'ai-card-link is-primary', PHONE);
|
||||
const kf = el('button', 'ai-card-link is-primary', lang === 'zh' ? '在线咨询' : 'Live chat');
|
||||
kf.type = 'button';
|
||||
kf.addEventListener('click', openWecomKf);
|
||||
const tel = el('a', 'ai-card-link', PHONE);
|
||||
tel.href = `tel:${PHONE}`;
|
||||
const mail = el('a', 'ai-card-link', EMAIL);
|
||||
mail.href = `mailto:${EMAIL}`;
|
||||
const wx = el('span', 'ai-card-link', (lang === 'zh' ? '微信 ' : 'WeChat ') + WECHAT);
|
||||
actions.appendChild(kf);
|
||||
actions.appendChild(tel);
|
||||
actions.appendChild(wx);
|
||||
actions.appendChild(mail);
|
||||
return actions;
|
||||
}
|
||||
@ -899,6 +982,9 @@
|
||||
<div class="ai-followup" role="toolbar"></div>
|
||||
<div class="ai-composer-slot--chat"></div>
|
||||
<div class="ai-composer-wrap">
|
||||
<div class="ai-new-chat-tip" hidden role="status" aria-live="polite">
|
||||
<span class="ai-new-chat-tip__text"></span>
|
||||
</div>
|
||||
<div class="ai-composer">
|
||||
<span class="ai-composer__caustic" aria-hidden="true"></span>
|
||||
<span class="ai-composer__sheen" aria-hidden="true"></span>
|
||||
@ -909,31 +995,27 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="ai-trust-bar">
|
||||
<div class="ai-trust-bar__contacts">
|
||||
<a class="ai-trust-phone" href="tel:${PHONE}">
|
||||
<i class="bi bi-telephone" aria-hidden="true"></i>
|
||||
<span class="ai-trust-label"></span>
|
||||
</a>
|
||||
<button type="button" class="ai-trust-wechat" title="${WECHAT}">
|
||||
<i class="bi bi-wechat" aria-hidden="true"></i>
|
||||
<span class="ai-trust-label"></span>
|
||||
</button>
|
||||
<a class="ai-trust-email" href="mailto:${EMAIL}">
|
||||
<i class="bi bi-envelope" aria-hidden="true"></i>
|
||||
<span class="ai-trust-label"></span>
|
||||
</a>
|
||||
<a class="ai-trust-bar__human" href="#contact"></a>
|
||||
</div>
|
||||
<div class="ai-trust-bar__meta">
|
||||
<p class="ai-trust-address">
|
||||
<i class="bi bi-geo-alt" aria-hidden="true"></i>
|
||||
<span class="ai-trust-label"></span>
|
||||
</p>
|
||||
<a class="ai-trust-icp" href="https://beian.miit.gov.cn" target="_blank" rel="noopener">
|
||||
<i class="bi bi-shield-check" aria-hidden="true"></i>
|
||||
<span class="ai-trust-label"></span>
|
||||
</a>
|
||||
</div>
|
||||
<a class="ai-trust-phone" href="tel:${PHONE}">
|
||||
<i class="bi bi-telephone" aria-hidden="true"></i>
|
||||
<span class="ai-trust-label"></span>
|
||||
</a>
|
||||
<button type="button" class="ai-trust-wechat" title="${WECHAT}">
|
||||
<i class="bi bi-wechat" aria-hidden="true"></i>
|
||||
<span class="ai-trust-label"></span>
|
||||
</button>
|
||||
<a class="ai-trust-email" href="mailto:${EMAIL}">
|
||||
<i class="bi bi-envelope" aria-hidden="true"></i>
|
||||
<span class="ai-trust-label"></span>
|
||||
</a>
|
||||
<a class="ai-trust-bar__human" href="#contact"></a>
|
||||
<p class="ai-trust-address">
|
||||
<i class="bi bi-geo-alt" aria-hidden="true"></i>
|
||||
<span class="ai-trust-label"></span>
|
||||
</p>
|
||||
<a class="ai-trust-icp" href="https://beian.miit.gov.cn" target="_blank" rel="noopener">
|
||||
<i class="bi bi-shield-check" aria-hidden="true"></i>
|
||||
<span class="ai-trust-label"></span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -987,19 +1069,7 @@
|
||||
sendUserMessage(card.dataset.guideAsk, { intentId: card.dataset.guideIntent || 'hard' });
|
||||
});
|
||||
|
||||
shell.querySelector('.ai-trust-wechat').addEventListener('click', async () => {
|
||||
const btn = shell.querySelector('.ai-trust-wechat');
|
||||
const label = btn.querySelector('.ai-trust-label');
|
||||
try {
|
||||
await navigator.clipboard.writeText(WECHAT);
|
||||
if (label) label.textContent = lang === 'zh' ? '微信已复制' : 'WeChat copied';
|
||||
} catch {
|
||||
if (label) label.textContent = WECHAT;
|
||||
}
|
||||
window.setTimeout(() => {
|
||||
if (label) label.textContent = t('trustWechat');
|
||||
}, 1600);
|
||||
});
|
||||
shell.querySelector('.ai-trust-wechat').addEventListener('click', openWecomKf);
|
||||
|
||||
shell.querySelector('.ai-composer-form').addEventListener('submit', (e) => {
|
||||
e.preventDefault();
|
||||
@ -1014,6 +1084,7 @@
|
||||
});
|
||||
|
||||
inputEl.addEventListener('input', () => {
|
||||
hideNewChatTip();
|
||||
inputEl.style.height = 'auto';
|
||||
inputEl.style.height = Math.min(inputEl.scrollHeight, 160) + 'px';
|
||||
syncComposerSendState();
|
||||
@ -1344,6 +1415,7 @@
|
||||
closeHistorySidebar();
|
||||
return;
|
||||
}
|
||||
hideNewChatTip();
|
||||
syncActiveThreadMessages();
|
||||
activeThreadId = threadId;
|
||||
applyThreadMessages(getActiveThread());
|
||||
@ -1355,13 +1427,45 @@
|
||||
inputEl.focus();
|
||||
}
|
||||
|
||||
function hideNewChatTip() {
|
||||
const tip = shell?.querySelector('.ai-new-chat-tip');
|
||||
const wrap = shell?.querySelector('.ai-composer-wrap');
|
||||
if (newChatTipTimer) {
|
||||
clearTimeout(newChatTipTimer);
|
||||
newChatTipTimer = null;
|
||||
}
|
||||
if (tip) {
|
||||
tip.classList.remove('is-visible');
|
||||
tip.hidden = true;
|
||||
}
|
||||
wrap?.classList.remove('has-new-chat-tip');
|
||||
}
|
||||
|
||||
function showNewChatTip() {
|
||||
const tip = shell?.querySelector('.ai-new-chat-tip');
|
||||
const wrap = shell?.querySelector('.ai-composer-wrap');
|
||||
const textEl = tip?.querySelector('.ai-new-chat-tip__text');
|
||||
if (!tip || !wrap || !textEl) return;
|
||||
if (newChatTipTimer) clearTimeout(newChatTipTimer);
|
||||
textEl.textContent = t('newChatHint');
|
||||
tip.hidden = false;
|
||||
wrap.classList.add('has-new-chat-tip');
|
||||
// Restart enter animation if already visible
|
||||
tip.classList.remove('is-visible');
|
||||
void tip.offsetWidth;
|
||||
tip.classList.add('is-visible');
|
||||
newChatTipTimer = setTimeout(() => hideNewChatTip(), 4200);
|
||||
}
|
||||
|
||||
function startNewChat() {
|
||||
if (isBusy) return;
|
||||
syncActiveThreadMessages();
|
||||
const current = getActiveThread();
|
||||
if (current && current.messages.length === 0) {
|
||||
closeHistorySidebar();
|
||||
updateViewMode();
|
||||
inputEl.focus();
|
||||
showNewChatTip();
|
||||
return;
|
||||
}
|
||||
createThread(true);
|
||||
@ -1371,6 +1475,7 @@
|
||||
closeHistorySidebar();
|
||||
setNavActive('home');
|
||||
inputEl.focus();
|
||||
showNewChatTip();
|
||||
}
|
||||
|
||||
function toggleHistorySidebar() {
|
||||
@ -1462,6 +1567,8 @@
|
||||
shell.querySelector('.ai-history-sidebar__title').textContent = t('historyTitle');
|
||||
shell.querySelector('.ai-new-chat-btn__label').textContent = t('newChat');
|
||||
shell.querySelector('.ai-new-chat-ghost__label').textContent = t('newChat');
|
||||
const tipText = shell.querySelector('.ai-new-chat-tip__text');
|
||||
if (tipText) tipText.textContent = t('newChatHint');
|
||||
applyWelcomeTitle();
|
||||
renderWelcomeSub();
|
||||
const heroCta = shell.querySelector('.ai-welcome-cta');
|
||||
@ -1597,7 +1704,9 @@
|
||||
if (intentId === 'human') {
|
||||
flow = null;
|
||||
return {
|
||||
text: zh ? '直接打给我们或加微信。询价请先注册登记。' : 'Call or WeChat us. Register first if you need a quote.',
|
||||
text: zh
|
||||
? '点「在线咨询」用微信对接顾问,也可打电话。询价请先注册登记。'
|
||||
: 'Tap Live chat for WeChat support, or call us. Register first if you need a quote.',
|
||||
card: { type: 'contact' },
|
||||
};
|
||||
}
|
||||
@ -1793,6 +1902,7 @@
|
||||
}
|
||||
|
||||
function sendUserMessage(text, extra) {
|
||||
hideNewChatTip();
|
||||
if (!text || isBusy || !shell) return;
|
||||
|
||||
appendMessageRow('user', text, null, true);
|
||||
@ -1895,6 +2005,8 @@
|
||||
e.preventDefault();
|
||||
openNavTopic('contact');
|
||||
});
|
||||
|
||||
window.IDEOpenWecomKf = openWecomKf;
|
||||
}
|
||||
|
||||
function init() {
|
||||
@ -1913,7 +2025,10 @@
|
||||
});
|
||||
|
||||
window.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') closeDrawers();
|
||||
if (e.key === 'Escape') {
|
||||
closeKfQrModal();
|
||||
closeDrawers();
|
||||
}
|
||||
});
|
||||
|
||||
const bootTopic = (location.hash || '').replace('#', '');
|
||||
@ -1933,6 +2048,7 @@
|
||||
if (!isHomePage()) window.location.href = '/';
|
||||
},
|
||||
close: () => closeDrawers(),
|
||||
openWecomKf,
|
||||
};
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
|
||||
@ -115,8 +115,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
const languageStorageKey = 'ide-language';
|
||||
const pageTitleMap = {
|
||||
'IDE | Global Digital One-Stop Cross-Border Logistics': 'IDE | 全球数字化一站式跨境物流服务商',
|
||||
'IDE|Immediate Delivery Express — Cross-Border Logistics': 'IDE | 全球数字化一站式跨境物流服务商',
|
||||
'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 - 关于我们',
|
||||
@ -394,7 +395,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
'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 | Global one-stop cross-border logistics solution provider': 'IDE - 跨境物流,全球即递'
|
||||
,'IDE - Cross-border Logistics, Delivered Globally': 'IDE - 跨境物流,全球即递'
|
||||
,'Explore Services': '了解服务'
|
||||
,'Quote Response': '报价响应'
|
||||
,'Global Routes': '全球线路'
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
<meta charset="utf-8">
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport">
|
||||
|
||||
<title>IDE | 全球数字化一站式跨境物流服务商</title>
|
||||
<title>IDE - 跨境物流,全球即递</title>
|
||||
<meta content="IDE 面向中小卖家的跨境物流。全球数字化一站式跨境物流服务商。注册登记后即可询价,一口价不砍,专属顾问跟到签收。" name="description">
|
||||
<meta content="IDE, 跨境物流, 中小卖家, 9610, 疑难单, 出口退税, 欧美专线, 东南亚专线" name="keywords">
|
||||
|
||||
@ -20,7 +20,7 @@
|
||||
|
||||
<!-- Template Main CSS File -->
|
||||
<link href="assets/css/main.css" rel="stylesheet">
|
||||
<link href="assets/css/ai-advisor.css?v=20260824an" rel="stylesheet">
|
||||
<link href="assets/css/ai-advisor.css?v=20260901a" rel="stylesheet">
|
||||
|
||||
</head>
|
||||
|
||||
@ -85,7 +85,7 @@
|
||||
<!-- 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=20260824an"></script>
|
||||
<script src="assets/js/ai-advisor.js?v=20260901a"></script>
|
||||
|
||||
</body>
|
||||
|
||||
|
||||
@ -98,6 +98,7 @@ export interface CustomerServiceAdvisorDto {
|
||||
name?: string
|
||||
wechatId?: string
|
||||
wechatQrCode?: string
|
||||
wecomKfUrl?: string
|
||||
}
|
||||
|
||||
export const CustomerService = {
|
||||
|
||||
@ -1720,7 +1720,8 @@ export async function deleteOrderTemplateCustom(templateId: number) {
|
||||
export async function postOrderUpload(
|
||||
file: File,
|
||||
templateId: number,
|
||||
senderAddressId?: number
|
||||
senderAddressId?: number,
|
||||
customerId?: number
|
||||
): Promise<{ jobId: number; async: boolean }> {
|
||||
const form = new FormData()
|
||||
form.append('file', file)
|
||||
@ -1728,6 +1729,9 @@ export async function postOrderUpload(
|
||||
if (senderAddressId != null && senderAddressId > 0) {
|
||||
form.append('senderAddressId', String(senderAddressId))
|
||||
}
|
||||
if (customerId != null && customerId > 0) {
|
||||
form.append('customerId', String(customerId))
|
||||
}
|
||||
const res = await axiosInstance.post(orderUrl('/order/upload'), form)
|
||||
assertBusinessOk(res.data)
|
||||
const d = unwrapApiData(res.data)
|
||||
|
||||
12
src/assets/icons/workplace/order-bind.svg
Normal file
@ -0,0 +1,12 @@
|
||||
<svg width="54" height="54" viewBox="0 0 54 54" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<ellipse cx="27" cy="44" rx="13" ry="3" fill="#FF7D00" opacity="0.28"/>
|
||||
<path d="M18.5 22a6.5 6.5 0 0 1 6.5-6.5h3.2a2 2 0 1 1 0 4H25a2.5 2.5 0 1 0 0 5h4.5a6.5 6.5 0 1 1 0 13H26.3a2 2 0 1 1 0-4H29.5a2.5 2.5 0 1 0 0-5H25a6.5 6.5 0 0 1-6.5-6.5z" fill="url(#bind)"/>
|
||||
<circle cx="38.5" cy="33.5" r="7.5" fill="#F53F3F"/>
|
||||
<path d="M38.5 29.8v4.4M36.3 34.2h4.4" stroke="#fff" stroke-width="2" stroke-linecap="round"/>
|
||||
<defs>
|
||||
<linearGradient id="bind" x1="16" y1="15" x2="40" y2="40" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FFB65D"/>
|
||||
<stop offset="1" stop-color="#FF7D00"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 738 B |
12
src/assets/icons/workplace/order-dispatch.svg
Normal file
@ -0,0 +1,12 @@
|
||||
<svg width="54" height="54" viewBox="0 0 54 54" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<ellipse cx="27" cy="44" rx="12" ry="3" fill="#7DA2FF" opacity="0.28"/>
|
||||
<path d="M14 20.5l26.2 8.2c1.1.3 1.1 1.9 0 2.2L28.5 34.5 25 40.8c-.5.9-1.9.6-2-.4l-1.2-9.2L14 20.5z" fill="url(#dp)"/>
|
||||
<path d="M14 20.5l10.8 18.7 1.2-9.2L36.5 27 14 20.5z" fill="#165DFF" opacity="0.35"/>
|
||||
<path d="M25 31.5l11.5-4.5" stroke="#fff" stroke-width="1.8" stroke-linecap="round" opacity="0.85"/>
|
||||
<defs>
|
||||
<linearGradient id="dp" x1="14" y1="20" x2="40" y2="40" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#7DA2FF"/>
|
||||
<stop offset="1" stop-color="#165DFF"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 703 B |
14
src/assets/icons/workplace/order-inbound.svg
Normal file
@ -0,0 +1,14 @@
|
||||
<svg width="54" height="54" viewBox="0 0 54 54" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<ellipse cx="27" cy="44" rx="13" ry="3" fill="#14C9C9" opacity="0.28"/>
|
||||
<path d="M13 24.5l14-8.5 14 8.5V38c0 1.1-.9 2-2 2H15c-1.1 0-2-.9-2-2V24.5z" fill="url(#ib)"/>
|
||||
<path d="M13 24.5l14 8.5 14-8.5" stroke="#fff" stroke-width="1.6" stroke-linejoin="round" opacity="0.55"/>
|
||||
<path d="M24 28.5h6v11.5h-6V28.5z" fill="#E8FFFB"/>
|
||||
<path d="M27 16v8.5" stroke="#0FC6C2" stroke-width="2.4" stroke-linecap="round"/>
|
||||
<path d="M23.2 21.5L27 25.3l3.8-3.8" stroke="#0FC6C2" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<defs>
|
||||
<linearGradient id="ib" x1="13" y1="16" x2="41" y2="40" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#37D4CF"/>
|
||||
<stop offset="1" stop-color="#0FC6C2"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 858 B |
13
src/assets/icons/workplace/order-payment.svg
Normal file
@ -0,0 +1,13 @@
|
||||
<svg width="54" height="54" viewBox="0 0 54 54" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<ellipse cx="27" cy="44" rx="13" ry="3" fill="#FF9A2E" opacity="0.28"/>
|
||||
<path d="M16 24.5c0-1.4 1.1-2.5 2.5-2.5H28c2.5 0 4.5 2 4.5 4.5v1.2h3.2c1.6 0 2.9 1.4 2.7 3l-.8 7.2c-.2 1.5-1.4 2.6-2.9 2.6H18.5c-1.4 0-2.5-1.1-2.5-2.5V24.5z" fill="url(#pay)"/>
|
||||
<path d="M19 22v-1.2c0-2.1 1.7-3.8 3.8-3.8h5.4c2.1 0 3.8 1.7 3.8 3.8V22" stroke="#FF7D00" stroke-width="2.2" stroke-linecap="round"/>
|
||||
<circle cx="27" cy="33" r="3.2" fill="#FFF7E8"/>
|
||||
<path d="M27 31.2v3.6M25.6 32.4h2.8" stroke="#FF7D00" stroke-width="1.4" stroke-linecap="round"/>
|
||||
<defs>
|
||||
<linearGradient id="pay" x1="16" y1="17" x2="38" y2="40" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FFB65D"/>
|
||||
<stop offset="1" stop-color="#FF7D00"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 858 B |
13
src/assets/icons/workplace/order-pick.svg
Normal file
@ -0,0 +1,13 @@
|
||||
<svg width="54" height="54" viewBox="0 0 54 54" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<ellipse cx="27" cy="44" rx="13" ry="3" fill="#FF9A2E" opacity="0.28"/>
|
||||
<path d="M15 20.5h16c1.1 0 2 .9 2 2v16c0 1.1-.9 2-2 2H15c-1.1 0-2-.9-2-2v-16c0-1.1.9-2 2-2z" fill="url(#pk)"/>
|
||||
<path d="M17.5 24h11M17.5 28.5h11M17.5 33h7.5" stroke="#fff" stroke-width="1.8" stroke-linecap="round" opacity="0.85"/>
|
||||
<path d="M33 24.5l8.5 3.2c.7.3 1.1 1 1.1 1.8v6.2c0 .9-.6 1.7-1.5 1.9l-7.2 1.8c-.6.2-1.2-.3-1.2-.9V25.4c0-.6.5-1 1.1-.9z" fill="#FF7D00"/>
|
||||
<path d="M36.2 29.2l2.6 1.5-2.6 1.5v-3z" fill="#FFF7E8"/>
|
||||
<defs>
|
||||
<linearGradient id="pk" x1="13" y1="20" x2="33" y2="40" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FFB65D"/>
|
||||
<stop offset="1" stop-color="#FF7D00"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 826 B |
12
src/assets/icons/workplace/order-pickup.svg
Normal file
@ -0,0 +1,12 @@
|
||||
<svg width="54" height="54" viewBox="0 0 54 54" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<ellipse cx="27" cy="44" rx="13" ry="3" fill="#FF9A2E" opacity="0.28"/>
|
||||
<path d="M16 21.5l10-5.5 10 5.5v12L26 39 16 33.5v-12z" fill="url(#pu)"/>
|
||||
<path d="M26 16v23M16 21.5l10 5.5 10-5.5" stroke="#fff" stroke-width="1.5" stroke-linejoin="round" opacity="0.5"/>
|
||||
<path d="M33.5 28.5c2.4 1.2 4 3.7 4 6.5v1.2c1.5.5 2.5 1.9 2.5 3.5 0 2.1-1.7 3.8-3.8 3.8h-9.2c-1.9 0-3.5-1.6-3.5-3.5 0-1.4.8-2.6 2-3.2v-.8c0-2.2 1.3-4.1 3.2-5 .6-.3 1.3.2 1.3.9v3.6l3.5-2.2v-4.3c0-.7.7-1.2 1.4-.9z" fill="#FF7D00"/>
|
||||
<defs>
|
||||
<linearGradient id="pu" x1="16" y1="16" x2="36" y2="39" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FFB65D"/>
|
||||
<stop offset="1" stop-color="#FF7D00"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 815 B |
17
src/assets/icons/workplace/order-transit.svg
Normal file
@ -0,0 +1,17 @@
|
||||
<svg width="54" height="54" viewBox="0 0 54 54" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<ellipse cx="27" cy="44" rx="13" ry="3" fill="#4080FF" opacity="0.28"/>
|
||||
<path d="M11 28h17.5l4-7c.4-.7 1.1-1.1 1.9-1.1H40c1.1 0 2 .9 2 2v12.5c0 .8-.7 1.5-1.5 1.5H11V28z" fill="url(#tr)"/>
|
||||
<path d="M11 22h15v18H13c-1.1 0-2-.9-2-2V22z" fill="#165DFF"/>
|
||||
<rect x="14" y="25.5" width="6.5" height="4" rx="1" fill="#E8F3FF"/>
|
||||
<circle cx="17.5" cy="39.5" r="3.4" fill="#1D2129"/>
|
||||
<circle cx="17.5" cy="39.5" r="1.5" fill="#C9CDD4"/>
|
||||
<circle cx="35.5" cy="39.5" r="3.4" fill="#1D2129"/>
|
||||
<circle cx="35.5" cy="39.5" r="1.5" fill="#C9CDD4"/>
|
||||
<path d="M6 24.5h4M5 28h3.5M6.5 31.5h3" stroke="#4080FF" stroke-width="1.8" stroke-linecap="round" opacity="0.85"/>
|
||||
<defs>
|
||||
<linearGradient id="tr" x1="11" y1="19" x2="42" y2="40" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#7DA2FF"/>
|
||||
<stop offset="1" stop-color="#165DFF"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 983 B |
@ -252,8 +252,16 @@
|
||||
{{ t('nanaAssistant.copyWechat') }}
|
||||
</button>
|
||||
</div>
|
||||
<button type="button" class="ai-advisor-drawer__kf" @click="openWecomKf(advisor.wecomKfUrl)">
|
||||
{{ t('nanaAssistant.wecomKf') }}
|
||||
</button>
|
||||
</template>
|
||||
<p v-else class="ai-advisor-drawer__empty">{{ t('nanaAssistant.noAdvisor') }}</p>
|
||||
<div v-else class="ai-advisor-drawer__empty-wrap">
|
||||
<p class="ai-advisor-drawer__empty">{{ t('nanaAssistant.noAdvisor') }}</p>
|
||||
<button type="button" class="ai-advisor-drawer__kf" @click="openWecomKf()">
|
||||
{{ t('nanaAssistant.wecomKf') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
@ -303,6 +311,7 @@ import {
|
||||
type McpCapabilityId,
|
||||
} from '@/composables/assistantMcpGuide';
|
||||
import { isMobileUserAgent, isWeChatBrowser } from '@/utils/device';
|
||||
import { openWecomKf } from '@/utils/wecomKf';
|
||||
import {
|
||||
providerModelTags,
|
||||
} from '@/views/system/aiAssistant/providerCatalog';
|
||||
@ -690,6 +699,16 @@ async function loadAdvisor() {
|
||||
}
|
||||
|
||||
function openAdvisorView() {
|
||||
// H5 / 微信 / 手机:优先企业微信客服(即时会话);顾问个人链接优先
|
||||
if (route.path.startsWith('/h5') || isWeChatBrowser() || isMobileUserAgent()) {
|
||||
void (async () => {
|
||||
if (!advisorLoaded.value) {
|
||||
await loadAdvisor();
|
||||
}
|
||||
openWecomKf(advisor.value.wecomKfUrl);
|
||||
})();
|
||||
return;
|
||||
}
|
||||
advisorDrawerOpen.value = true;
|
||||
if (!advisorLoaded.value) {
|
||||
void loadAdvisor();
|
||||
|
||||
@ -1520,6 +1520,35 @@
|
||||
background: rgba(45, 140, 140, 0.06);
|
||||
}
|
||||
|
||||
.ai-advisor-drawer__kf {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
min-height: 40px;
|
||||
margin-top: 16px;
|
||||
padding: 0 14px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
background: var(--app-primary-color, #2d8c8c);
|
||||
color: #fff;
|
||||
font-size: 0.88rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ai-advisor-drawer__kf:hover {
|
||||
filter: brightness(1.05);
|
||||
}
|
||||
|
||||
.ai-advisor-drawer__empty-wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ai-advisor-drawer__empty {
|
||||
font-size: 0.88rem;
|
||||
color: var(--kc-secondary, #6a75b8);
|
||||
|
||||
@ -179,6 +179,15 @@
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item :label="t('system.user.form.wecomKfUrl')" field="wecomKfUrl">
|
||||
<a-input
|
||||
v-model="formState.wecomKfUrl"
|
||||
:placeholder="t('system.user.form.wecomKfUrlPlaceholder')"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item :label="t('system.user.form.wechatQrCode')" field="wechatQrFiles">
|
||||
<a-upload
|
||||
|
||||
@ -16,6 +16,10 @@
|
||||
<a-input v-model="wechatId" :placeholder="t('system.user.form.wechatIdPlaceholder')" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item :label="t('system.user.form.wecomKfUrl')" field="wecomKfUrl">
|
||||
<a-input v-model="wecomKfUrl" :placeholder="t('system.user.form.wecomKfUrlPlaceholder')" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item :label="t('system.user.form.wechatQrCode')" field="wechatQrFiles">
|
||||
<a-upload
|
||||
v-model:file-list="wechatQrFiles"
|
||||
@ -55,6 +59,7 @@ withDefaults(
|
||||
const phone = defineModel<string>('phone', { default: '' })
|
||||
const employeeNo = defineModel<string>('employeeNo', { default: '' })
|
||||
const wechatId = defineModel<string>('wechatId', { default: '' })
|
||||
const wecomKfUrl = defineModel<string>('wecomKfUrl', { default: '' })
|
||||
const wechatQrFiles = defineModel<any[]>('wechatQrFiles', { default: () => [] })
|
||||
|
||||
async function handleWechatCustomRequest(options: any) {
|
||||
|
||||
@ -466,6 +466,7 @@ export default {
|
||||
'user.senderAddress.table.senderName': 'Contact',
|
||||
'user.senderAddress.table.phone': 'Phone',
|
||||
'user.senderAddress.table.destCountry': 'Country',
|
||||
'user.senderAddress.table.country': 'Country',
|
||||
'user.senderAddress.table.province': 'Province',
|
||||
'user.senderAddress.table.city': 'City',
|
||||
'user.senderAddress.table.address': 'Address',
|
||||
@ -477,6 +478,7 @@ export default {
|
||||
'user.senderAddress.form.phone': 'Phone',
|
||||
'user.senderAddress.form.company': 'Company',
|
||||
'user.senderAddress.form.destCountryCode': 'Country Code',
|
||||
'user.senderAddress.form.countryCode': 'Country Code',
|
||||
'user.senderAddress.form.province': 'Province',
|
||||
'user.senderAddress.form.city': 'City',
|
||||
'user.senderAddress.form.zipCode': 'Zip Code',
|
||||
@ -1256,7 +1258,8 @@ export default {
|
||||
'nanaAssistant.mcp.field.product': 'Sales product code/name',
|
||||
'nanaAssistant.mcp.field.cargoName': 'Cargo name',
|
||||
'nanaAssistant.mcp.field.customsOrderNo': 'Waybill No.',
|
||||
'nanaAssistant.human': 'Talk to a human advisor →',
|
||||
'nanaAssistant.human': 'Talk to online advisor',
|
||||
'nanaAssistant.wecomKf': 'Chat on WeCom',
|
||||
'nanaAssistant.you': 'You',
|
||||
'nanaAssistant.backToChat': 'Back to chat',
|
||||
'nanaAssistant.quickSmall': 'Small or difficult orders?',
|
||||
@ -1265,7 +1268,7 @@ export default {
|
||||
'nanaAssistant.quickTrack': 'Track my shipment',
|
||||
'nanaAssistant.quickVisibility': 'Real-time tracking?',
|
||||
'nanaAssistant.quickAfterSales': 'Something went wrong?',
|
||||
'nanaAssistant.quickHuman': 'Talk to human advisor',
|
||||
'nanaAssistant.quickHuman': 'Talk to online advisor',
|
||||
'nanaAssistant.replySmall': 'Yes. IDE focuses on SMB sellers and shipments others decline — special routes, complex cargo, and low-volume orders are in scope. Tell me origin, destination, and cargo type to get started.',
|
||||
'nanaAssistant.replyPrice': 'For a quote, destination destCountry + actual weight (kg) is enough — e.g. US 0.5kg. Postal code/dims help; origin city is not required.',
|
||||
'nanaAssistant.replyQuoteSpeed': 'With destCountry + weight we can return an indicative quote right away; final billing follows inbound weight and product rules.',
|
||||
@ -2050,6 +2053,7 @@ export default {
|
||||
'order.priceCheck.table.viewGuide': 'Channel guide',
|
||||
'order.priceCheck.table.viewGuideHint': 'Click to view route guide',
|
||||
'order.priceCheck.table.customsOrderNow': 'Place order',
|
||||
'order.priceCheck.table.orderNow': 'Place order',
|
||||
'order.priceCheck.table.askAi': 'Ask AI',
|
||||
'order.priceCheck.table.askAiCanShip': 'Can this ship?',
|
||||
'order.priceCheck.table.askAiCheaper': 'Anything cheaper?',
|
||||
@ -2150,10 +2154,14 @@ export default {
|
||||
'order.placeOrder.template.updatedAt': 'Updated: {time}',
|
||||
|
||||
'order.placeOrder.upload.dropText': 'Drag file here, or click to upload',
|
||||
'order.placeOrder.upload.tip1': 'Download the template: red headers are required; use dropdowns where provided.',
|
||||
'order.placeOrder.upload.tip2': 'Up to 2000 orders per upload. Pick a default sender below — do not put sender info in Excel.',
|
||||
'order.placeOrder.batch.senderLabel': 'Default sender address',
|
||||
'order.placeOrder.batch.templateTip': 'Simple (recommended) and Standard (extended fields) templates available. Red headers are required; pick sender below.',
|
||||
'order.placeOrder.batch.senderPlaceholder': 'Select a sender address',
|
||||
'order.placeOrder.selectCustomer': 'Select customer',
|
||||
'order.placeOrder.customerLabel': 'Customer',
|
||||
'order.placeOrder.selectCustomerRequired': 'Select a customer first',
|
||||
'order.placeOrder.batch.templateTip': 'Red headers are required. Up to 2000 orders.',
|
||||
'order.placeOrder.batch.addressCardTitle': 'Customer & sender',
|
||||
'order.placeOrder.batch.addressTip': 'Choose sender here, not in Excel.',
|
||||
'order.placeOrder.batch.block.noSender': 'Please select a default sender address',
|
||||
'order.placeOrder.batchOrder': 'Batch order',
|
||||
'order.placeOrder.fromPriceCheck.filled':
|
||||
@ -2185,7 +2193,7 @@ export default {
|
||||
'order.placeOrder.batch.stepUpload': 'Upload Excel',
|
||||
'order.placeOrder.batch.stepSubmit': 'Submit',
|
||||
'order.placeOrder.batch.guideHint': 'Click a step to jump · pick template on the right, then upload & submit',
|
||||
'order.placeOrder.batch.pickTemplateHint': 'Select a template from the library on the right',
|
||||
'order.placeOrder.batch.pickTemplateHint': 'Select a template',
|
||||
'order.placeOrder.batch.block.noTemplate': 'Please select an order template on the right first',
|
||||
'order.placeOrder.batch.block.templateDraft': 'Finish mapping this custom template before submitting',
|
||||
'order.placeOrder.batch.block.noFile': 'Please upload an Excel file first',
|
||||
@ -2222,7 +2230,6 @@ export default {
|
||||
'order.placeOrder.single.senderEmpty': 'No sender address yet. Add one in Address Management first.',
|
||||
'order.placeOrder.single.senderRequired': 'Please select a sender address',
|
||||
'order.placeOrder.single.senderPickHint': 'Select sender address first',
|
||||
'order.placeOrder.single.senderClear': 'Clear sender',
|
||||
'order.placeOrder.single.senderManage': 'Manage addresses',
|
||||
'order.placeOrder.single.senderEditHint': 'Click to change sender',
|
||||
'order.placeOrder.single.senderLoadFailed': 'Failed to load sender addresses',
|
||||
@ -2241,6 +2248,8 @@ export default {
|
||||
'order.placeOrder.contact.blockTitle': 'Complete your contact info',
|
||||
'order.placeOrder.contact.blockDesc': 'Add phone and email to continue placing orders',
|
||||
'order.placeOrder.contact.blockAction': 'Complete now',
|
||||
'order.placeOrder.contact.adminCustomerIncomplete':
|
||||
'The selected customer is missing phone or email. Complete their profile before placing the order.',
|
||||
'order.placeOrder.single.viewCustomerGuide': 'View customer guide',
|
||||
'order.placeOrder.single.declaredCurrencyHint': 'Default is USD; enter declared prices in that currency',
|
||||
'order.placeOrder.single.exportCustomsPlaceholder': 'Optional (defaults to simplified declaration)',
|
||||
@ -2760,6 +2769,8 @@ export default {
|
||||
'system.user.form.employeeNoPlaceholder': 'Auto-generated',
|
||||
'system.user.form.wechatId': 'WeChat ID',
|
||||
'system.user.form.wechatIdPlaceholder': 'Enter WeChat ID',
|
||||
'system.user.form.wecomKfUrl': 'WeCom support link',
|
||||
'system.user.form.wecomKfUrlPlaceholder': 'e.g. https://work.weixin.qq.com/kfid/...',
|
||||
'system.user.form.wechatQrCode': 'WeChat QR Code',
|
||||
'system.user.form.wechatQrUpload': 'Upload QR code',
|
||||
'system.user.table.employeeNo': 'Employee No.',
|
||||
|
||||
@ -467,6 +467,7 @@ export default {
|
||||
'user.senderAddress.table.senderName': '联系人',
|
||||
'user.senderAddress.table.phone': '手机',
|
||||
'user.senderAddress.table.destCountry': '国家',
|
||||
'user.senderAddress.table.country': '国家',
|
||||
'user.senderAddress.table.province': '省/州',
|
||||
'user.senderAddress.table.city': '城市',
|
||||
'user.senderAddress.table.address': '地址',
|
||||
@ -478,6 +479,7 @@ export default {
|
||||
'user.senderAddress.form.phone': '手机',
|
||||
'user.senderAddress.form.company': '公司',
|
||||
'user.senderAddress.form.destCountryCode': '国家二字码',
|
||||
'user.senderAddress.form.countryCode': '国家二字码',
|
||||
'user.senderAddress.form.province': '省/州',
|
||||
'user.senderAddress.form.city': '城市',
|
||||
'user.senderAddress.form.zipCode': '邮编',
|
||||
@ -1071,7 +1073,8 @@ export default {
|
||||
'nanaAssistant.mcp.field.product': '销售产品代码/名称',
|
||||
'nanaAssistant.mcp.field.cargoName': '客户品名',
|
||||
'nanaAssistant.mcp.field.customsOrderNo': '运单号',
|
||||
'nanaAssistant.human': '联系人工顾问 →',
|
||||
'nanaAssistant.human': '联系在线顾问',
|
||||
'nanaAssistant.wecomKf': '企业微信在线咨询',
|
||||
'nanaAssistant.you': '你',
|
||||
'nanaAssistant.backToChat': '返回对话',
|
||||
'nanaAssistant.quickSmall': '小批量 / 疑难单能发吗?',
|
||||
@ -1080,7 +1083,7 @@ export default {
|
||||
'nanaAssistant.quickTrack': '查物流单号',
|
||||
'nanaAssistant.quickVisibility': '能实时追踪吗?',
|
||||
'nanaAssistant.quickAfterSales': '出问题了怎么办?',
|
||||
'nanaAssistant.quickHuman': '联系人工顾问',
|
||||
'nanaAssistant.quickHuman': '联系在线顾问',
|
||||
'nanaAssistant.replySmall': '可以。IDE 专注中小卖家和别人不接的单 — 特殊线路、复杂货型、小批量都在服务范围内。告诉我起运地、目的地和货型即可开始。',
|
||||
'nanaAssistant.replyPrice': '查价只需目的国和实重(kg)。例如:美国 0.5kg。有邮编/尺寸会更准;不需要起运地。',
|
||||
'nanaAssistant.replyQuoteSpeed': '提交目的国和重量后可即时试算参考价;正式出账以入仓重与产品规则为准。',
|
||||
@ -1969,6 +1972,7 @@ export default {
|
||||
'order.priceCheck.table.viewGuide': '渠道说明',
|
||||
'order.priceCheck.table.viewGuideHint': '点击查看线路说明',
|
||||
'order.priceCheck.table.customsOrderNow': '立刻下单',
|
||||
'order.priceCheck.table.orderNow': '立刻下单',
|
||||
'order.priceCheck.table.askAi': '问 AI',
|
||||
'order.priceCheck.table.askAiCanShip': '这个能不能发?',
|
||||
'order.priceCheck.table.askAiCheaper': '有没有更便宜?',
|
||||
@ -2070,10 +2074,14 @@ export default {
|
||||
'order.placeOrder.template.updatedAt': '更新: {time}',
|
||||
|
||||
'order.placeOrder.upload.dropText': '将文件拖到此处,或点击上传',
|
||||
'order.placeOrder.upload.tip1': '请下载模板填写:红色表头为必填,带下拉的列请点选编码。',
|
||||
'order.placeOrder.upload.tip2': '单次最多 2000 单;发件人请在下方选择,无需写入 Excel。',
|
||||
'order.placeOrder.batch.senderLabel': '默认发件地址',
|
||||
'order.placeOrder.batch.templateTip': '提供精简版(日常推荐)与标准版(含扩展字段);红色表头必填,发件人在下方选择。',
|
||||
'order.placeOrder.batch.senderPlaceholder': '请选择发件人地址',
|
||||
'order.placeOrder.selectCustomer': '请选择客户',
|
||||
'order.placeOrder.customerLabel': '客户',
|
||||
'order.placeOrder.selectCustomerRequired': '请先选择客户',
|
||||
'order.placeOrder.batch.templateTip': '红色表头必填,单次最多 2000 单。',
|
||||
'order.placeOrder.batch.addressCardTitle': '客户地址',
|
||||
'order.placeOrder.batch.addressTip': '发件人在此选择,不必写入 Excel。',
|
||||
'order.placeOrder.batch.block.noSender': '请选择默认发件地址',
|
||||
'order.placeOrder.batchOrder': '批量下单',
|
||||
'order.placeOrder.fromPriceCheck.filled': '已带入查价所选渠道与包裹信息,请继续完善收件人',
|
||||
@ -2104,7 +2112,7 @@ export default {
|
||||
'order.placeOrder.batch.stepUpload': '上传 Excel',
|
||||
'order.placeOrder.batch.stepSubmit': '提交下单',
|
||||
'order.placeOrder.batch.guideHint': '点步骤可定位 · 右侧选模板,左侧上传后提交',
|
||||
'order.placeOrder.batch.pickTemplateHint': '请在右侧模板库点击选择模板',
|
||||
'order.placeOrder.batch.pickTemplateHint': '请选择模板',
|
||||
'order.placeOrder.batch.block.noTemplate': '请先在右侧选择下单模板',
|
||||
'order.placeOrder.batch.block.templateDraft': '当前自定义模板待编辑完成映射后才能提交',
|
||||
'order.placeOrder.batch.block.noFile': '请先上传 Excel 文件',
|
||||
@ -2141,7 +2149,6 @@ export default {
|
||||
'order.placeOrder.single.senderEmpty': '暂无发件人地址,请先在地址管理中新增',
|
||||
'order.placeOrder.single.senderRequired': '请选择发件人地址',
|
||||
'order.placeOrder.single.senderPickHint': '请先选择发件地址',
|
||||
'order.placeOrder.single.senderClear': '清空发件人',
|
||||
'order.placeOrder.single.senderManage': '去地址管理',
|
||||
'order.placeOrder.single.senderEditHint': '点击可更换发件人',
|
||||
'order.placeOrder.single.senderLoadFailed': '加载发件地址失败',
|
||||
@ -2160,6 +2167,8 @@ export default {
|
||||
'order.placeOrder.contact.blockTitle': '请先完善联系方式',
|
||||
'order.placeOrder.contact.blockDesc': '补全手机号和邮箱后,即可继续下单',
|
||||
'order.placeOrder.contact.blockAction': '去完善',
|
||||
'order.placeOrder.contact.adminCustomerIncomplete':
|
||||
'所选客户尚未完善手机号和邮箱,请先在客户资料中补全后再下单',
|
||||
'order.placeOrder.single.viewCustomerGuide': '查看对客说明',
|
||||
'order.placeOrder.single.declaredCurrencyHint': '默认美元,请按币种填写申报价格',
|
||||
'order.placeOrder.single.exportCustomsPlaceholder': '请选择(可不选,默认简易申报)',
|
||||
@ -2684,6 +2693,8 @@ export default {
|
||||
'system.user.form.employeeNoPlaceholder': '自动生成',
|
||||
'system.user.form.wechatId': '微信号',
|
||||
'system.user.form.wechatIdPlaceholder': '请输入微信号',
|
||||
'system.user.form.wecomKfUrl': '企业微信接入链接',
|
||||
'system.user.form.wecomKfUrlPlaceholder': '如 https://work.weixin.qq.com/kfid/...',
|
||||
'system.user.form.wechatQrCode': '微信二维码',
|
||||
'system.user.form.wechatQrUpload': '上传二维码',
|
||||
'system.user.table.employeeNo': '工号',
|
||||
|
||||
@ -3,6 +3,8 @@ export type UserProfileExt = {
|
||||
employeeNo?: string
|
||||
wechatId?: string
|
||||
wechatQrCode?: string
|
||||
/** 企业微信客服接入链接(微信外接入) */
|
||||
wecomKfUrl?: string
|
||||
}
|
||||
|
||||
export type UserFormExtState = UserProfileExt & {
|
||||
@ -32,6 +34,7 @@ export function pickUserProfileExt(
|
||||
employeeNo: pickStr(user, 'employeeNo', 'EmployeeNo'),
|
||||
wechatId: pickStr(user, 'wechatId', 'WechatId'),
|
||||
wechatQrCode: pickStr(user, 'wechatQrCode', 'WechatQrCode'),
|
||||
wecomKfUrl: pickStr(user, 'wecomKfUrl', 'WecomKfUrl'),
|
||||
}
|
||||
}
|
||||
|
||||
@ -45,5 +48,6 @@ export function toUserProfilePayload(state: UserFormExtState): UserProfileExt {
|
||||
employeeNo: state.employeeNo?.trim() || undefined,
|
||||
wechatId: state.wechatId?.trim() || undefined,
|
||||
wechatQrCode: (fromFile ? String(fromFile) : state.wechatQrCode?.trim()) || undefined,
|
||||
wecomKfUrl: state.wecomKfUrl?.trim() || undefined,
|
||||
}
|
||||
}
|
||||
|
||||
39
src/utils/wecomKf.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import { isMobileUserAgent, isWeChatBrowser } from '@/utils/device';
|
||||
|
||||
/**
|
||||
* 企业微信客服(微信外接入链接)
|
||||
* 管理后台 → 微信客服 → 在微信外接入 → 复制
|
||||
* 可用 VITE_WECOM_KF_URL 覆盖;顾问资料里的 wecomKfUrl 优先
|
||||
*/
|
||||
export const WECOM_KF_URL = (
|
||||
(import.meta.env.VITE_WECOM_KF_URL as string | undefined) ||
|
||||
'https://work.weixin.qq.com/kfid/kfcae09c206f6c7045a'
|
||||
).trim();
|
||||
|
||||
/**
|
||||
* 微信内 / 手机:直跳唤起客服会话
|
||||
* PC:打开企业微信客服页(自带扫码)
|
||||
* @param url 可选,优先使用顾问个人接入链接
|
||||
*/
|
||||
export function openWecomKf(url?: string | null): void {
|
||||
const target = (url?.trim() || WECOM_KF_URL).trim();
|
||||
if (!target) return;
|
||||
|
||||
if (isWeChatBrowser() || isMobileUserAgent()) {
|
||||
window.location.href = target;
|
||||
return;
|
||||
}
|
||||
|
||||
const w = 480;
|
||||
const h = 720;
|
||||
const left = Math.max(0, Math.round((window.screen.width - w) / 2));
|
||||
const top = Math.max(0, Math.round((window.screen.height - h) / 2));
|
||||
const popup = window.open(
|
||||
target,
|
||||
'ide_wecom_kf',
|
||||
`width=${w},height=${h},left=${left},top=${top},noopener,noreferrer`,
|
||||
);
|
||||
if (!popup) {
|
||||
window.location.href = target;
|
||||
}
|
||||
}
|
||||
@ -530,6 +530,7 @@ export default {
|
||||
'h5.me.placeOrderDesc': 'Ship in a few taps',
|
||||
'h5.me.section.services': 'Services',
|
||||
'h5.me.section.account': 'Account',
|
||||
'h5.me.onlineKf': 'Dedicated advisor',
|
||||
'h5.me.section.journey': 'Shipping guide',
|
||||
|
||||
'h5.storeOrders.title': 'Store orders',
|
||||
|
||||
@ -526,6 +526,7 @@ export default {
|
||||
'h5.me.placeOrderDesc': '填好收件,一键寄出',
|
||||
'h5.me.section.services': '常用服务',
|
||||
'h5.me.section.account': '账户设置',
|
||||
'h5.me.onlineKf': '专属顾问',
|
||||
'h5.me.section.journey': '发货指引',
|
||||
|
||||
'h5.storeOrders.title': '店铺订单',
|
||||
|
||||
@ -134,6 +134,13 @@
|
||||
<icon-right />
|
||||
</span>
|
||||
</button>
|
||||
<button type="button" class="h5-me-list__item" @click="openOnlineKf">
|
||||
<span class="h5-me-list__left">
|
||||
<icon-customer-service class="h5-me-list__icon" />
|
||||
{{ t('h5.me.onlineKf') }}
|
||||
</span>
|
||||
<icon-right />
|
||||
</button>
|
||||
<button type="button" class="h5-me-list__item" @click="bindWechat">
|
||||
<span class="h5-me-list__left">
|
||||
<icon-wechat class="h5-me-list__icon" />
|
||||
@ -207,6 +214,7 @@ import {
|
||||
IconLocation,
|
||||
IconLanguage,
|
||||
IconWechat,
|
||||
IconCustomerService,
|
||||
} from '@arco-design/web-vue/es/icon';
|
||||
import type { CustomerDto } from '@/api/customer';
|
||||
import {
|
||||
@ -220,6 +228,7 @@ import { isLogin } from '@/utils/auth';
|
||||
import { resolveAvatarUrl } from '@/utils/avatar';
|
||||
import { getMapIdFromAccessToken } from '@/utils/jwt';
|
||||
import { h5Navigate, isWeChatBrowser } from '@/utils/device';
|
||||
import { openWecomKf } from '@/utils/wecomKf';
|
||||
import { redirectToWeChatOAuth } from '@/utils/wechatOAuth';
|
||||
import { useUserStore } from '@/store';
|
||||
import { useRbacUserStore } from '@/store/modules/rbac-user';
|
||||
@ -543,6 +552,10 @@ function bindWechat() {
|
||||
redirectToWeChatOAuth('bind');
|
||||
}
|
||||
|
||||
function openOnlineKf() {
|
||||
openWecomKf();
|
||||
}
|
||||
|
||||
function handleLogout() {
|
||||
Modal.confirm({
|
||||
title: t('h5.me.logout'),
|
||||
|
||||
@ -20,7 +20,7 @@
|
||||
<p class="aw-trend__empty-desc">{{ t('workplace.admin.businessTrendEmptyDesc') }}</p>
|
||||
</div>
|
||||
<div v-else class="aw-trend__chart">
|
||||
<Chart width="100%" height="320px" :options="chartOption" />
|
||||
<Chart width="100%" height="280px" :options="chartOption" />
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
@ -95,12 +95,12 @@ const chartOption = computed(() => {
|
||||
grid: {
|
||||
left: 8,
|
||||
right: 8,
|
||||
top: 12,
|
||||
bottom: 28,
|
||||
top: 28,
|
||||
bottom: 8,
|
||||
containLabel: true,
|
||||
},
|
||||
legend: {
|
||||
bottom: 0,
|
||||
top: 0,
|
||||
left: 'center',
|
||||
icon: 'circle',
|
||||
itemWidth: 8,
|
||||
@ -238,9 +238,15 @@ onMounted(() => {
|
||||
.aw-trend {
|
||||
--aw-ink: #1a2e38;
|
||||
--aw-soft: rgba(26, 46, 56, 0.48);
|
||||
--aw-chart-h: 320px;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
max-height: 100%;
|
||||
padding: 12px 14px 14px;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.aw-trend__head {
|
||||
@ -250,6 +256,7 @@ onMounted(() => {
|
||||
gap: 12px;
|
||||
margin-bottom: 8px;
|
||||
flex-wrap: wrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.aw-trend__title {
|
||||
@ -259,11 +266,18 @@ onMounted(() => {
|
||||
color: var(--aw-ink);
|
||||
}
|
||||
|
||||
.aw-trend__loading,
|
||||
.aw-trend__empty {
|
||||
flex: 1 1 0%;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.aw-trend__loading {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: var(--aw-chart-h);
|
||||
min-height: 280px;
|
||||
}
|
||||
|
||||
.aw-trend__empty {
|
||||
@ -271,7 +285,7 @@ onMounted(() => {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: var(--aw-chart-h);
|
||||
min-height: 280px;
|
||||
padding: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
@ -291,7 +305,9 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.aw-trend__chart {
|
||||
flex: 0 0 auto;
|
||||
width: 100%;
|
||||
height: var(--aw-chart-h);
|
||||
height: 280px;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -74,7 +74,7 @@ import {
|
||||
type AdminOpsMetricKey,
|
||||
} from '@/utils/workplace-care'
|
||||
import WorldTime from './world-time.vue'
|
||||
import iconPendingOnline from '@/assets/icons/workplace/order-pending-online.svg?url'
|
||||
import iconPayment from '@/assets/icons/workplace/order-payment.svg?url'
|
||||
import iconPendingShip from '@/assets/icons/workplace/order-pending-ship.svg?url'
|
||||
import iconProblematic from '@/assets/icons/workplace/order-problematic.svg?url'
|
||||
import iconGrowth from '@/assets/icons/workplace/order-growth.svg?url'
|
||||
@ -110,7 +110,7 @@ const METRIC_META: Record<
|
||||
pendingPayment: {
|
||||
titleKey: 'workplace.admin.ops.pendingPayment',
|
||||
hintKey: 'workplace.stat.clickHint',
|
||||
icon: iconPendingOnline,
|
||||
icon: iconPayment,
|
||||
tone: 'warning',
|
||||
alert: true,
|
||||
path: WAYBILL,
|
||||
|
||||
@ -89,7 +89,7 @@ const items = computed(() =>
|
||||
|
||||
.stat-grid--inner {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
@ -105,12 +105,6 @@ const items = computed(() =>
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
@media (max-width: 1280px) {
|
||||
.stat-grid--inner {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.stat-grid--inner {
|
||||
grid-template-columns: 1fr;
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
<header class="aw-hero aw-hero--compact">
|
||||
<Banner compact />
|
||||
</header>
|
||||
<CareTodayPanel variant="strip" />
|
||||
<AdminOpsQueue />
|
||||
</section>
|
||||
|
||||
@ -19,14 +20,10 @@
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<section class="aw-panel aw-panel--care" aria-label="today-care">
|
||||
<CareTodayPanel variant="hero" />
|
||||
<section class="aw-panel aw-panel--trend" aria-label="business-trend">
|
||||
<AdminBusinessTrend />
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<section class="aw-panel aw-panel--trend" aria-label="business-trend">
|
||||
<AdminBusinessTrend />
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<aside class="admin-right">
|
||||
@ -163,7 +160,7 @@ function go(path: string, query?: Record<string, string>) {
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 100%;
|
||||
max-height: 100%;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@ -194,12 +191,12 @@ function go(path: string, query?: Record<string, string>) {
|
||||
}
|
||||
|
||||
.admin-left__body {
|
||||
flex: 1 1 auto;
|
||||
flex: 1 1 0%;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
overflow-y: auto;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.admin-right {
|
||||
@ -233,7 +230,8 @@ function go(path: string, query?: Record<string, string>) {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
||||
gap: 12px;
|
||||
align-items: start;
|
||||
align-items: stretch;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.aw-panel--fulfill,
|
||||
@ -243,35 +241,24 @@ function go(path: string, query?: Record<string, string>) {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.aw-panel--pickup {
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
.aw-panel--trend {
|
||||
flex: 0 0 auto;
|
||||
flex: 1 1 0%;
|
||||
min-height: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
margin-top: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
:deep(.aw-trend) {
|
||||
flex: 1 1 0%;
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.aw-panel--care {
|
||||
flex: 0 0 auto;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
|
||||
:deep(.care-today) {
|
||||
width: 100%;
|
||||
min-height: 148px;
|
||||
box-sizing: border-box;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
border-radius: var(--aw-radius);
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
:deep(.care-today.is-hero.is-collapsed) {
|
||||
min-height: 88px;
|
||||
}
|
||||
.aw-panel--hero :deep(.care-today.is-strip) {
|
||||
margin: 2px 0 8px;
|
||||
}
|
||||
|
||||
.aw-panel--links {
|
||||
@ -289,37 +276,10 @@ function go(path: string, query?: Record<string, string>) {
|
||||
flex-direction: column;
|
||||
padding: 0;
|
||||
|
||||
:deep(.announcement-card.arco-card) {
|
||||
:deep(.announcement-panel) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
background: transparent;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
:deep(.arco-card-header),
|
||||
:deep(.arco-card-body) {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
:deep(.arco-card-header) {
|
||||
flex-shrink: 0;
|
||||
border-bottom: 1px solid color-mix(in srgb, var(--app-acrylic-border) 55%, transparent);
|
||||
}
|
||||
|
||||
:deep(.arco-card-body) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
:deep(.arco-card-header .arco-card-header-title) {
|
||||
color: var(--aw-ink);
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
@ -469,7 +429,14 @@ function go(path: string, query?: Record<string, string>) {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.aw-panel--trend,
|
||||
.aw-panel--trend {
|
||||
flex: none;
|
||||
min-height: 340px;
|
||||
height: auto;
|
||||
max-height: none;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.aw-panel--announce {
|
||||
margin-top: 0;
|
||||
}
|
||||
@ -478,10 +445,6 @@ function go(path: string, query?: Record<string, string>) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.aw-panel--pickup {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.admin-right {
|
||||
min-height: 360px;
|
||||
}
|
||||
|
||||
@ -3,93 +3,149 @@
|
||||
v-if="show"
|
||||
class="care-today"
|
||||
:class="{
|
||||
'is-collapsed': collapsed,
|
||||
'is-collapsed': !isStrip && collapsed,
|
||||
'is-pressure': pressure,
|
||||
'is-clocked-out': clockedOut,
|
||||
'is-hero': variant === 'hero',
|
||||
'is-strip': isStrip,
|
||||
}"
|
||||
>
|
||||
<span class="care-today__blob care-today__blob--a" aria-hidden="true" />
|
||||
<span class="care-today__blob care-today__blob--b" aria-hidden="true" />
|
||||
|
||||
<div class="care-today__head">
|
||||
<div class="care-today__titles">
|
||||
<p class="care-today__eyebrow">{{ t('workplace.care.title') }}</p>
|
||||
<p v-if="!collapsed" class="care-today__date">{{ dateLabel }}</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="care-today__toggle"
|
||||
:aria-expanded="!collapsed"
|
||||
:aria-label="collapsed ? t('workplace.care.expand') : t('workplace.care.collapse')"
|
||||
@click="toggleCollapsed"
|
||||
>
|
||||
{{ collapsed ? t('workplace.care.expand') : t('workplace.care.collapse') }}
|
||||
</button>
|
||||
</div>
|
||||
<!-- strip:问候下轻提示,单行/矮条 -->
|
||||
<template v-if="isStrip">
|
||||
<div class="care-today__strip">
|
||||
<div class="care-today__strip-main">
|
||||
<span class="care-today__strip-date">{{ dateLabel }}</span>
|
||||
<p class="care-today__strip-text">
|
||||
<span class="care-today__strip-headline">{{ headline }}</span>
|
||||
<span class="care-today__strip-note">{{ note }}</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<template v-if="collapsed">
|
||||
<p class="care-today__collapsed-line">{{ headline }}</p>
|
||||
<div class="care-today__strip-aside">
|
||||
<a-spin v-if="loading" :size="14" />
|
||||
<template v-else>
|
||||
<button
|
||||
v-if="primaryGuide && !clockedOut"
|
||||
type="button"
|
||||
class="care-action care-action--strip"
|
||||
@click="goPrimary"
|
||||
>
|
||||
<span class="care-action__label">
|
||||
<template v-if="primaryIsHandoff">
|
||||
{{ t('workplace.care.handoff', { count: primaryGuide.count }) }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ t(`workplace.care.pending.${primaryGuide.kind}`) }}
|
||||
<em>{{ primaryGuide.count }}</em>
|
||||
</template>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
v-if="clockOutEnabled && !clockedOut"
|
||||
type="button"
|
||||
class="care-clockout care-clockout--strip"
|
||||
@click="clockOutVisible = true"
|
||||
>
|
||||
{{ t('workplace.care.clockOut') }}
|
||||
</button>
|
||||
<p
|
||||
v-else-if="clockOutEnabled && clockedOut"
|
||||
class="care-today__clocked care-today__clocked--strip"
|
||||
>
|
||||
{{ t('workplace.care.clockedOut') }}
|
||||
<button type="button" class="care-today__undo" @click="undoClockOut">
|
||||
{{ t('workplace.care.clockOutUndo') }}
|
||||
</button>
|
||||
</p>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div v-if="rhythmChips.length" class="care-chips">
|
||||
<span v-for="chip in rhythmChips" :key="chip" class="care-chip">{{ chip }}</span>
|
||||
</div>
|
||||
|
||||
<p class="care-today__headline">{{ headline }}</p>
|
||||
<p class="care-today__note">{{ note }}</p>
|
||||
|
||||
<div v-if="loading" class="care-today__loading">
|
||||
<a-spin :size="16" />
|
||||
</div>
|
||||
|
||||
<div v-else class="care-today__actions">
|
||||
<button
|
||||
v-if="primaryGuide && !clockedOut"
|
||||
type="button"
|
||||
class="care-action care-action--primary"
|
||||
@click="goPrimary"
|
||||
>
|
||||
<span class="care-action__label">
|
||||
<template v-if="primaryIsHandoff">
|
||||
{{ t('workplace.care.handoff', { count: primaryGuide.count }) }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ t(`workplace.care.pending.${primaryGuide.kind}`) }}
|
||||
<em>{{ primaryGuide.count }}</em>
|
||||
</template>
|
||||
</span>
|
||||
<span class="care-action__hint">
|
||||
{{
|
||||
primaryIsHandoff
|
||||
? t('workplace.care.handoffHint')
|
||||
: t('workplace.care.action.pendingHint')
|
||||
}}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<p v-else-if="!primaryGuide && !clockedOut" class="care-today__calm">
|
||||
{{ t('workplace.care.calm') }}
|
||||
</p>
|
||||
|
||||
<div class="care-today__footer">
|
||||
<button
|
||||
v-if="clockOutEnabled && !clockedOut"
|
||||
type="button"
|
||||
class="care-clockout"
|
||||
@click="clockOutVisible = true"
|
||||
>
|
||||
{{ t('workplace.care.clockOut') }}
|
||||
</button>
|
||||
<p v-else-if="clockOutEnabled && clockedOut" class="care-today__clocked">
|
||||
{{ t('workplace.care.clockedOut') }}
|
||||
<button type="button" class="care-today__undo" @click="undoClockOut">
|
||||
{{ t('workplace.care.clockOutUndo') }}
|
||||
</button>
|
||||
</p>
|
||||
<div class="care-today__head">
|
||||
<div class="care-today__titles">
|
||||
<p class="care-today__eyebrow">{{ t('workplace.care.title') }}</p>
|
||||
<p v-if="!collapsed" class="care-today__date">{{ dateLabel }}</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="care-today__toggle"
|
||||
:aria-expanded="!collapsed"
|
||||
:aria-label="collapsed ? t('workplace.care.expand') : t('workplace.care.collapse')"
|
||||
@click="toggleCollapsed"
|
||||
>
|
||||
{{ collapsed ? t('workplace.care.expand') : t('workplace.care.collapse') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<template v-if="collapsed">
|
||||
<p class="care-today__collapsed-line">{{ headline }}</p>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div v-if="rhythmChips.length" class="care-chips">
|
||||
<span v-for="chip in rhythmChips" :key="chip" class="care-chip">{{ chip }}</span>
|
||||
</div>
|
||||
|
||||
<p class="care-today__headline">{{ headline }}</p>
|
||||
<p class="care-today__note">{{ note }}</p>
|
||||
|
||||
<div v-if="loading" class="care-today__loading">
|
||||
<a-spin :size="16" />
|
||||
</div>
|
||||
|
||||
<div v-else class="care-today__actions">
|
||||
<button
|
||||
v-if="primaryGuide && !clockedOut"
|
||||
type="button"
|
||||
class="care-action care-action--primary"
|
||||
@click="goPrimary"
|
||||
>
|
||||
<span class="care-action__label">
|
||||
<template v-if="primaryIsHandoff">
|
||||
{{ t('workplace.care.handoff', { count: primaryGuide.count }) }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ t(`workplace.care.pending.${primaryGuide.kind}`) }}
|
||||
<em>{{ primaryGuide.count }}</em>
|
||||
</template>
|
||||
</span>
|
||||
<span class="care-action__hint">
|
||||
{{
|
||||
primaryIsHandoff
|
||||
? t('workplace.care.handoffHint')
|
||||
: t('workplace.care.action.pendingHint')
|
||||
}}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<p v-else-if="!primaryGuide && !clockedOut" class="care-today__calm">
|
||||
{{ t('workplace.care.calm') }}
|
||||
</p>
|
||||
|
||||
<div class="care-today__footer">
|
||||
<button
|
||||
v-if="clockOutEnabled && !clockedOut"
|
||||
type="button"
|
||||
class="care-clockout"
|
||||
@click="clockOutVisible = true"
|
||||
>
|
||||
{{ t('workplace.care.clockOut') }}
|
||||
</button>
|
||||
<p v-else-if="clockOutEnabled && clockedOut" class="care-today__clocked">
|
||||
{{ t('workplace.care.clockedOut') }}
|
||||
<button type="button" class="care-today__undo" @click="undoClockOut">
|
||||
{{ t('workplace.care.clockOutUndo') }}
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<a-modal
|
||||
@ -144,8 +200,8 @@ defineOptions({ name: 'CareTodayPanel' })
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
/** hero:员工工作台主视觉,默认展开 */
|
||||
variant?: 'default' | 'hero'
|
||||
/** hero:主视觉大块;strip:问候下轻条;default:独立面板 */
|
||||
variant?: 'default' | 'hero' | 'strip'
|
||||
}>(),
|
||||
{ variant: 'default' },
|
||||
)
|
||||
@ -176,6 +232,8 @@ const clockedOut = ref(false)
|
||||
const clockOutVisible = ref(false)
|
||||
const overdueThreshold = careSettings.overdueCareThreshold
|
||||
|
||||
const isStrip = computed(() => props.variant === 'strip')
|
||||
|
||||
const orderListPath = computed(() =>
|
||||
rbacUserStore.isCustomerPortal() ? '/order/myOrders' : '/order/waybillManage',
|
||||
)
|
||||
@ -372,10 +430,12 @@ async function load() {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
try {
|
||||
collapsed.value = localStorage.getItem(CARE_PANEL_COLLAPSED_KEY) === '1'
|
||||
} catch {
|
||||
collapsed.value = false
|
||||
if (!isStrip.value) {
|
||||
try {
|
||||
collapsed.value = localStorage.getItem(CARE_PANEL_COLLAPSED_KEY) === '1'
|
||||
} catch {
|
||||
collapsed.value = false
|
||||
}
|
||||
}
|
||||
void load()
|
||||
})
|
||||
@ -445,6 +505,16 @@ onMounted(() => {
|
||||
color: var(--care-ink);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-strip {
|
||||
padding: 0;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
backdrop-filter: none;
|
||||
-webkit-backdrop-filter: none;
|
||||
overflow: visible;
|
||||
}
|
||||
}
|
||||
|
||||
.care-today__blob {
|
||||
@ -459,11 +529,65 @@ onMounted(() => {
|
||||
.care-today__loading,
|
||||
.care-today__calm,
|
||||
.care-today__footer,
|
||||
.care-chips {
|
||||
.care-chips,
|
||||
.care-today__strip {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.care-today__strip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
min-height: 36px;
|
||||
padding: 4px 0 2px;
|
||||
}
|
||||
|
||||
.care-today__strip-main {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.care-today__strip-date {
|
||||
flex-shrink: 0;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--care-muted);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.care-today__strip-text {
|
||||
margin: 0;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: baseline;
|
||||
gap: 6px 10px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.care-today__strip-headline {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: var(--care-ink);
|
||||
}
|
||||
|
||||
.care-today__strip-note {
|
||||
font-size: 12px;
|
||||
color: var(--care-muted);
|
||||
}
|
||||
|
||||
.care-today__strip-aside {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.care-chips {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@ -585,6 +709,19 @@ onMounted(() => {
|
||||
border-color: color-mix(in srgb, var(--app-primary-color) 18%, var(--app-acrylic-border));
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&--strip {
|
||||
width: auto;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 4px 10px;
|
||||
border-radius: 8px;
|
||||
min-height: 28px;
|
||||
|
||||
.care-action__label {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.care-action__label {
|
||||
@ -632,6 +769,14 @@ onMounted(() => {
|
||||
&:hover {
|
||||
border-color: rgba(193, 122, 82, 0.5);
|
||||
}
|
||||
|
||||
&--strip {
|
||||
width: auto;
|
||||
height: 28px;
|
||||
padding: 0 10px;
|
||||
font-size: 12px;
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.care-today__clocked {
|
||||
@ -642,6 +787,11 @@ onMounted(() => {
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
|
||||
&--strip {
|
||||
justify-content: flex-end;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.care-today__undo {
|
||||
@ -668,6 +818,18 @@ onMounted(() => {
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.care-today__strip {
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.care-today__strip-aside {
|
||||
width: 100%;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.care-action {
|
||||
transition: none;
|
||||
|
||||
@ -158,23 +158,10 @@ defineOptions({ name: 'CustomerWorkplace' })
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
:deep(.announcement-card.arco-card) {
|
||||
:deep(.announcement-panel) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
:deep(.arco-card-header) {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
:deep(.arco-card-body) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,32 +1,31 @@
|
||||
<template>
|
||||
<a-card
|
||||
class="general-card announcement-card"
|
||||
:title="panelTitle"
|
||||
:header-style="{ paddingBottom: '0' }"
|
||||
:body-style="{ padding: '12px 16px 16px' }"
|
||||
>
|
||||
<div v-if="loading" class="announcement-state">
|
||||
<a-spin />
|
||||
<div class="announcement-panel">
|
||||
<p class="announcement-panel__title">{{ panelTitle }}</p>
|
||||
|
||||
<div class="announcement-panel__body">
|
||||
<div v-if="loading" class="announcement-state">
|
||||
<a-spin />
|
||||
</div>
|
||||
<div v-else-if="list.length === 0" class="announcement-state announcement-state--empty">
|
||||
<p class="announcement-state__title">{{ emptyTitle }}</p>
|
||||
<p class="announcement-state__desc">{{ emptyDesc }}</p>
|
||||
</div>
|
||||
<ul v-else class="announcement-list">
|
||||
<li
|
||||
v-for="item in list"
|
||||
:key="String(item.id)"
|
||||
class="announcement-item"
|
||||
@click="openDetail(item)"
|
||||
>
|
||||
<div class="announcement-item__main">
|
||||
<span v-if="isNew(item)" class="announcement-item__dot" />
|
||||
<span class="announcement-item__title">{{ displaySubject(item) }}</span>
|
||||
</div>
|
||||
<span class="announcement-item__time">{{ formatTime(item.createTime) }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div v-else-if="list.length === 0" class="announcement-state announcement-state--empty">
|
||||
<p class="announcement-state__title">{{ emptyTitle }}</p>
|
||||
<p class="announcement-state__desc">{{ emptyDesc }}</p>
|
||||
</div>
|
||||
<ul v-else class="announcement-list">
|
||||
<li
|
||||
v-for="item in list"
|
||||
:key="String(item.id)"
|
||||
class="announcement-item"
|
||||
@click="openDetail(item)"
|
||||
>
|
||||
<div class="announcement-item__main">
|
||||
<span v-if="isNew(item)" class="announcement-item__dot" />
|
||||
<span class="announcement-item__title">{{ displaySubject(item) }}</span>
|
||||
</div>
|
||||
<span class="announcement-item__time">{{ formatTime(item.createTime) }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</a-card>
|
||||
</div>
|
||||
|
||||
<a-modal
|
||||
v-model:visible="detailVisible"
|
||||
@ -192,6 +191,32 @@
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.announcement-panel {
|
||||
--aw-ink: #1a2e38;
|
||||
--aw-soft: rgba(26, 46, 56, 0.48);
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
box-sizing: border-box;
|
||||
padding: 14px 14px 16px;
|
||||
}
|
||||
|
||||
.announcement-panel__title {
|
||||
margin: 0 0 10px;
|
||||
flex-shrink: 0;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: var(--aw-ink);
|
||||
}
|
||||
|
||||
.announcement-panel__body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.announcement-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@ -203,19 +228,21 @@
|
||||
}
|
||||
|
||||
.announcement-state--empty {
|
||||
color: var(--color-text-3);
|
||||
color: var(--aw-soft);
|
||||
}
|
||||
|
||||
.announcement-state__title {
|
||||
margin: 0 0 4px;
|
||||
color: var(--color-text-2);
|
||||
color: var(--aw-ink);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.announcement-state__desc {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
color: var(--aw-soft);
|
||||
}
|
||||
|
||||
.announcement-list {
|
||||
@ -229,18 +256,23 @@
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 8px 6px;
|
||||
margin: 0 -6px;
|
||||
border-radius: 6px;
|
||||
padding: 10px 12px;
|
||||
margin: 0 0 8px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--app-acrylic-border);
|
||||
background: var(--app-acrylic-bg);
|
||||
backdrop-filter: var(--app-acrylic-blur);
|
||||
-webkit-backdrop-filter: var(--app-acrylic-blur);
|
||||
cursor: pointer;
|
||||
transition: background-color 0.15s;
|
||||
transition: border-color 0.2s ease, background 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-fill-2);
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
& + & {
|
||||
border-top: 1px solid var(--color-border-1);
|
||||
&:hover {
|
||||
border-color: var(--app-acrylic-border-strong);
|
||||
background: var(--app-acrylic-hover-bg);
|
||||
}
|
||||
|
||||
&__main {
|
||||
@ -261,8 +293,9 @@
|
||||
}
|
||||
|
||||
&__title {
|
||||
color: var(--color-text-1);
|
||||
color: var(--aw-ink);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
line-height: 1.45;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
@ -272,7 +305,7 @@
|
||||
|
||||
&__time {
|
||||
flex-shrink: 0;
|
||||
color: var(--color-text-3);
|
||||
color: var(--aw-soft);
|
||||
font-size: 12px;
|
||||
line-height: 1.45;
|
||||
white-space: nowrap;
|
||||
|
||||
@ -5,11 +5,14 @@ import {
|
||||
type FulfillWorkplaceStats,
|
||||
type PickupWorkplaceStats,
|
||||
} from '@/api/workplace'
|
||||
import iconStorePending from '@/assets/icons/workplace/order-store-pending.svg?url'
|
||||
import iconPendingShip from '@/assets/icons/workplace/order-pending-ship.svg?url'
|
||||
import iconPendingOnline from '@/assets/icons/workplace/order-pending-online.svg?url'
|
||||
import iconBind from '@/assets/icons/workplace/order-bind.svg?url'
|
||||
import iconTransit from '@/assets/icons/workplace/order-transit.svg?url'
|
||||
import iconPick from '@/assets/icons/workplace/order-pick.svg?url'
|
||||
import iconShipped from '@/assets/icons/workplace/order-shipped.svg?url'
|
||||
import iconNew from '@/assets/icons/workplace/order-new.svg?url'
|
||||
import iconPayment from '@/assets/icons/workplace/order-payment.svg?url'
|
||||
import iconDispatch from '@/assets/icons/workplace/order-dispatch.svg?url'
|
||||
import iconPickup from '@/assets/icons/workplace/order-pickup.svg?url'
|
||||
import iconInbound from '@/assets/icons/workplace/order-inbound.svg?url'
|
||||
|
||||
export type WorkplaceStatTone = 'neutral' | 'primary' | 'warning' | 'danger' | 'success'
|
||||
|
||||
@ -72,7 +75,7 @@ export function useWorkplaceFulfillPickupStats() {
|
||||
key: 'pendingBind',
|
||||
titleKey: 'workplace.admin.fulfill.pendingBind',
|
||||
value: fulfill.value.pendingBind,
|
||||
icon: iconStorePending,
|
||||
icon: iconBind,
|
||||
tone: 'warning',
|
||||
alert: true,
|
||||
path: FULFILL_PATH,
|
||||
@ -82,7 +85,7 @@ export function useWorkplaceFulfillPickupStats() {
|
||||
key: 'domesticInTransit',
|
||||
titleKey: 'workplace.admin.fulfill.domesticInTransit',
|
||||
value: fulfill.value.domesticInTransit,
|
||||
icon: iconPendingOnline,
|
||||
icon: iconTransit,
|
||||
tone: 'neutral',
|
||||
path: FULFILL_PATH,
|
||||
query: { progress: 'domesticInTransit' },
|
||||
@ -91,7 +94,7 @@ export function useWorkplaceFulfillPickupStats() {
|
||||
key: 'arrivedPendingPick',
|
||||
titleKey: 'workplace.admin.fulfill.arrivedPendingPick',
|
||||
value: fulfill.value.arrivedPendingPick,
|
||||
icon: iconPendingShip,
|
||||
icon: iconPick,
|
||||
tone: 'warning',
|
||||
alert: true,
|
||||
path: FULFILL_PATH,
|
||||
@ -113,7 +116,7 @@ export function useWorkplaceFulfillPickupStats() {
|
||||
key: 'pendingPayment',
|
||||
titleKey: 'workplace.admin.pickup.pendingPayment',
|
||||
value: pickup.value.pendingPayment,
|
||||
icon: iconPendingOnline,
|
||||
icon: iconPayment,
|
||||
tone: 'warning',
|
||||
alert: true,
|
||||
path: PICKUP_PATH,
|
||||
@ -123,7 +126,7 @@ export function useWorkplaceFulfillPickupStats() {
|
||||
key: 'pendingSchedule',
|
||||
titleKey: 'workplace.admin.pickup.pendingSchedule',
|
||||
value: pickup.value.pendingSchedule,
|
||||
icon: iconNew,
|
||||
icon: iconDispatch,
|
||||
tone: 'neutral',
|
||||
path: PICKUP_PATH,
|
||||
query: { status: 'draft' },
|
||||
@ -132,7 +135,7 @@ export function useWorkplaceFulfillPickupStats() {
|
||||
key: 'pendingPickup',
|
||||
titleKey: 'workplace.admin.pickup.pendingPickup',
|
||||
value: pickup.value.pendingPickup,
|
||||
icon: iconPendingShip,
|
||||
icon: iconPickup,
|
||||
tone: 'warning',
|
||||
alert: true,
|
||||
path: PICKUP_PATH,
|
||||
@ -142,7 +145,7 @@ export function useWorkplaceFulfillPickupStats() {
|
||||
key: 'pendingInbound',
|
||||
titleKey: 'workplace.admin.pickup.pendingInbound',
|
||||
value: pickup.value.pendingInbound,
|
||||
icon: iconShipped,
|
||||
icon: iconInbound,
|
||||
tone: 'primary',
|
||||
path: PICKUP_PATH,
|
||||
query: { status: 'picked' },
|
||||
|
||||
@ -213,7 +213,7 @@ export default {
|
||||
'workplace.admin.tip.peak': 'Peak season: accuracy and safety first.',
|
||||
'workplace.admin.tip.late': 'It’s late — ease up when you can.',
|
||||
'workplace.admin.linksTitle': 'Quick links',
|
||||
'workplace.admin.opsTitle': 'Today’s queue',
|
||||
'workplace.admin.opsTitle': 'Waybills',
|
||||
'workplace.admin.ops.pendingPayment': 'Pending payment',
|
||||
'workplace.admin.ops.pendingShip': 'Pending ship',
|
||||
'workplace.admin.ops.exception': 'Exceptions',
|
||||
|
||||
@ -210,7 +210,7 @@ export default {
|
||||
'workplace.admin.tip.peak': '旺季优先保准确与安全。',
|
||||
'workplace.admin.tip.late': '夜深了,能缓就缓一缓。',
|
||||
'workplace.admin.linksTitle': '常用入口',
|
||||
'workplace.admin.opsTitle': '今日待办',
|
||||
'workplace.admin.opsTitle': '运单',
|
||||
'workplace.admin.ops.pendingPayment': '待支付',
|
||||
'workplace.admin.ops.pendingShip': '待发货',
|
||||
'workplace.admin.ops.exception': '异常跟进',
|
||||
|
||||
@ -10,6 +10,7 @@ import { buildWarehouseSkuSelectPayload, normalizeListRow, pickField, pickSkuEng
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean
|
||||
customerId?: number
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
@ -66,6 +67,7 @@ async function loadList() {
|
||||
keyword: keyword.value.trim() || undefined,
|
||||
page: currentPage.value,
|
||||
pageSize: pageSize.value,
|
||||
...(props.customerId && props.customerId > 0 ? { customerId: props.customerId } : {}),
|
||||
})
|
||||
tableRows.value = (items || []).map((x) => mapRow(x as Record<string, unknown>))
|
||||
totalCount.value = Number(total ?? tableRows.value.length)
|
||||
|
||||
@ -34,6 +34,7 @@ import SelectDeclarationSkuModal from './components/SelectDeclarationSkuModal.vu
|
||||
import { mapWarehouseSkuToDeclarationFields, normalizeListRow, pickField } from '@/utils/warehouseListMapper';
|
||||
import { normalizeCargoAttributeCode } from '@/utils/cargoAttribute';
|
||||
import { useRbacUserStore } from '@/store/modules/rbac-user';
|
||||
import { useWarehouseAdminCustomer } from '@/composables/useWarehouseAdminCustomer';
|
||||
import {
|
||||
checkCustomerContactRequired,
|
||||
isCustomerContactRequiredError,
|
||||
@ -56,6 +57,18 @@ import SelectDeclarationSkuModal from './components/SelectDeclarationSkuModal.vu
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const rbacUserStore = useRbacUserStore();
|
||||
const {
|
||||
isAdmin,
|
||||
customerId: adminCustomerId,
|
||||
customerOptions,
|
||||
customerLoading,
|
||||
withCustomerId,
|
||||
ensureMutationCustomerId,
|
||||
resolveSelectedCustomerId,
|
||||
} = useWarehouseAdminCustomer();
|
||||
const adminCustomerSelected = computed(
|
||||
() => !isAdmin.value || resolveSelectedCustomerId() != null,
|
||||
);
|
||||
const contactVisible = ref(false);
|
||||
const contactProfile = ref<CustomerProfileCompleteness | null>(null);
|
||||
const contactBlocked = ref(false);
|
||||
@ -903,6 +916,7 @@ import SelectDeclarationSkuModal from './components/SelectDeclarationSkuModal.vu
|
||||
|
||||
const canSubmitBatch = computed(
|
||||
() =>
|
||||
adminCustomerSelected.value &&
|
||||
Boolean(pendingUploadFile.value) &&
|
||||
batchAgreementChecked.value &&
|
||||
selectedTemplate.value != null &&
|
||||
@ -912,6 +926,7 @@ import SelectDeclarationSkuModal from './components/SelectDeclarationSkuModal.vu
|
||||
|
||||
const batchSubmitBlockReason = computed(() => {
|
||||
if (canSubmitBatch.value) return '';
|
||||
if (!adminCustomerSelected.value) return t('order.placeOrder.selectCustomerRequired');
|
||||
if (selectedTemplate.value == null) return t('order.placeOrder.batch.block.noTemplate');
|
||||
if (!selectedTemplateReady.value) return t('order.placeOrder.batch.block.templateDraft');
|
||||
if (!selectedSenderAddressId.value) return t('order.placeOrder.batch.block.noSender');
|
||||
@ -1230,6 +1245,7 @@ import SelectDeclarationSkuModal from './components/SelectDeclarationSkuModal.vu
|
||||
|
||||
const canSubmitSingle = computed(
|
||||
() =>
|
||||
adminCustomerSelected.value &&
|
||||
singleAgreementChecked.value &&
|
||||
isSingleDeclarationComplete() &&
|
||||
isSingleRecipientComplete() &&
|
||||
@ -1240,6 +1256,7 @@ import SelectDeclarationSkuModal from './components/SelectDeclarationSkuModal.vu
|
||||
|
||||
const singleSubmitBlockReason = computed(() => {
|
||||
if (canSubmitSingle.value) return '';
|
||||
if (!adminCustomerSelected.value) return t('order.placeOrder.selectCustomerRequired');
|
||||
const err = getSingleFormValidationError();
|
||||
if (err) return err.message;
|
||||
if (!singleAgreementChecked.value) return t('order.placeOrder.agreement.required');
|
||||
@ -1347,6 +1364,7 @@ import SelectDeclarationSkuModal from './components/SelectDeclarationSkuModal.vu
|
||||
}
|
||||
|
||||
async function handleBatchOrderClick() {
|
||||
if (isAdmin.value && ensureMutationCustomerId() == null) return;
|
||||
if (!canSubmitBatch.value) {
|
||||
Message.warning(batchSubmitBlockReason.value || t('order.placeOrder.batch.block.generic'));
|
||||
return;
|
||||
@ -1356,6 +1374,7 @@ import SelectDeclarationSkuModal from './components/SelectDeclarationSkuModal.vu
|
||||
}
|
||||
|
||||
async function handleSingleOrderClick() {
|
||||
if (isAdmin.value && ensureMutationCustomerId() == null) return;
|
||||
if (!canSubmitSingle.value) {
|
||||
singleShowFieldErrors.value = true;
|
||||
const err = getSingleFormValidationError();
|
||||
@ -1502,13 +1521,22 @@ import SelectDeclarationSkuModal from './components/SelectDeclarationSkuModal.vu
|
||||
}
|
||||
|
||||
async function loadSenderAddressOptions({ applyDefault = true } = {}) {
|
||||
if (isAdmin.value && resolveSelectedCustomerId() == null) {
|
||||
senderAddressOptions.value = [];
|
||||
if (applyDefault) {
|
||||
applySenderAddressById(undefined, { silentEmpty: true });
|
||||
}
|
||||
return;
|
||||
}
|
||||
senderAddressLoading.value = true;
|
||||
try {
|
||||
const res = await getSenderAddressList({
|
||||
page: 1,
|
||||
pageSize: 200,
|
||||
addressType: 'sender',
|
||||
});
|
||||
const res = await getSenderAddressList(
|
||||
withCustomerId({
|
||||
page: 1,
|
||||
pageSize: 200,
|
||||
addressType: 'sender',
|
||||
}),
|
||||
);
|
||||
senderAddressOptions.value = (res.items || [])
|
||||
.map((item: Record<string, unknown>) => mapSenderAddressOption(item))
|
||||
.filter((x): x is SenderAddressOption => !!x);
|
||||
@ -1532,18 +1560,16 @@ import SelectDeclarationSkuModal from './components/SelectDeclarationSkuModal.vu
|
||||
}
|
||||
|
||||
async function openSenderModal() {
|
||||
if (isAdmin.value && ensureMutationCustomerId() == null) return;
|
||||
senderModalVisible.value = true;
|
||||
await loadSenderAddressOptions({ applyDefault: !isSingleSenderFilled() });
|
||||
}
|
||||
|
||||
function handleSenderAddressSelect(id: number | undefined) {
|
||||
if (id == null) return;
|
||||
applySenderAddressById(id);
|
||||
}
|
||||
|
||||
function clearSelectedSender() {
|
||||
applySenderAddressById(undefined);
|
||||
}
|
||||
|
||||
function goToSenderAddressManage() {
|
||||
router.push({ path: '/user/senderAddress' });
|
||||
}
|
||||
@ -2106,6 +2132,7 @@ import SelectDeclarationSkuModal from './components/SelectDeclarationSkuModal.vu
|
||||
loadReceiverAiPrefs();
|
||||
void Promise.all([loadReceiverAiProviders(), loadReceiverAiSkills()]);
|
||||
void (async () => {
|
||||
if (isAdmin.value) return;
|
||||
const { needContact, profile } = await checkCustomerContactRequired();
|
||||
if (needContact) {
|
||||
contactBlocked.value = true;
|
||||
@ -2175,6 +2202,7 @@ import SelectDeclarationSkuModal from './components/SelectDeclarationSkuModal.vu
|
||||
uploadFile,
|
||||
selectedTemplate.value,
|
||||
selectedSenderAddressId.value,
|
||||
resolveSelectedCustomerId(),
|
||||
);
|
||||
clearUploadedFile();
|
||||
if (uploadResult.jobId > 0) {
|
||||
@ -2448,6 +2476,10 @@ import SelectDeclarationSkuModal from './components/SelectDeclarationSkuModal.vu
|
||||
}
|
||||
const inbound = String(singleForm.inboundMethod || 'selfShip').trim() || 'selfShip';
|
||||
payload.inboundMethod = inbound;
|
||||
const mutationCustomerId = resolveSelectedCustomerId();
|
||||
if (mutationCustomerId != null) {
|
||||
payload.customerId = mutationCustomerId;
|
||||
}
|
||||
if (inbound === 'doorPickup') {
|
||||
payload.senderAddressId = parseNumberValue(singleForm.senderAddressId);
|
||||
payload.expectPickupTime = formTrim(singleForm.expectPickupTime) || undefined;
|
||||
@ -2510,6 +2542,7 @@ import SelectDeclarationSkuModal from './components/SelectDeclarationSkuModal.vu
|
||||
}
|
||||
|
||||
function openDeclarationSkuModal() {
|
||||
if (isAdmin.value && ensureMutationCustomerId() == null) return;
|
||||
declarationSkuModalVisible.value = true;
|
||||
}
|
||||
|
||||
@ -2788,6 +2821,7 @@ import SelectDeclarationSkuModal from './components/SelectDeclarationSkuModal.vu
|
||||
async function handleCreateFailSaveAsDraft() {
|
||||
const info = createFailInfo.value;
|
||||
if (!info || info.draftSaved) return;
|
||||
if (isAdmin.value && ensureMutationCustomerId() == null) return;
|
||||
|
||||
createFailSavingDraft.value = true;
|
||||
try {
|
||||
@ -2903,6 +2937,7 @@ import SelectDeclarationSkuModal from './components/SelectDeclarationSkuModal.vu
|
||||
}
|
||||
|
||||
async function handleSingleOrderSubmit() {
|
||||
if (isAdmin.value && ensureMutationCustomerId() == null) return;
|
||||
const validationError = getSingleFormValidationError();
|
||||
if (validationError) {
|
||||
singleShowFieldErrors.value = true;
|
||||
@ -2938,9 +2973,13 @@ import SelectDeclarationSkuModal from './components/SelectDeclarationSkuModal.vu
|
||||
});
|
||||
} catch (e: any) {
|
||||
if (isCustomerContactRequiredError(e)) {
|
||||
contactBlocked.value = true;
|
||||
contactVisible.value = true;
|
||||
Message.warning(t('h5.contact.requiredToast'));
|
||||
if (isAdmin.value) {
|
||||
Message.warning(t('order.placeOrder.contact.adminCustomerIncomplete'));
|
||||
} else {
|
||||
contactBlocked.value = true;
|
||||
contactVisible.value = true;
|
||||
Message.warning(t('h5.contact.requiredToast'));
|
||||
}
|
||||
} else if (
|
||||
String(e?.message || '').includes('发件人姓名不能为空') ||
|
||||
/sender\s*name/i.test(String(e?.message || ''))
|
||||
@ -2982,6 +3021,12 @@ import SelectDeclarationSkuModal from './components/SelectDeclarationSkuModal.vu
|
||||
}
|
||||
});
|
||||
|
||||
watch(adminCustomerId, () => {
|
||||
if (!isAdmin.value) return;
|
||||
applySenderAddressById(undefined);
|
||||
void loadSenderAddressOptions({ applyDefault: true });
|
||||
});
|
||||
|
||||
function handleSingleFlowClick(stepKey: string) {
|
||||
if (stepKey === 'sender') {
|
||||
void openSenderModal();
|
||||
@ -3077,6 +3122,17 @@ import SelectDeclarationSkuModal from './components/SelectDeclarationSkuModal.vu
|
||||
</template>
|
||||
</div>
|
||||
<div class="toolbar-right single-toolbar-right">
|
||||
<a-select
|
||||
v-if="isAdmin"
|
||||
v-model="adminCustomerId"
|
||||
allow-search
|
||||
allow-clear
|
||||
size="small"
|
||||
:placeholder="t('order.placeOrder.selectCustomer')"
|
||||
:loading="customerLoading"
|
||||
:options="customerOptions"
|
||||
class="place-order-customer-select"
|
||||
/>
|
||||
<a-button type="secondary" size="small" @click="handleGenerateSampleOrder">
|
||||
{{ t('order.placeOrder.single.sampleOrder') }}
|
||||
</a-button>
|
||||
@ -3732,47 +3788,62 @@ import SelectDeclarationSkuModal from './components/SelectDeclarationSkuModal.vu
|
||||
|
||||
<div class="upload-card-main">
|
||||
<div class="upload-card-main__left">
|
||||
<div class="template-selector">
|
||||
<label class="selector-label">{{ t('order.placeOrder.template.label') }}</label>
|
||||
<a-select
|
||||
v-model="selectedTemplate"
|
||||
class="template-select"
|
||||
allow-search
|
||||
:placeholder="t('order.placeOrder.batch.pickTemplateHint')"
|
||||
:options="templateOptions"
|
||||
:loading="templatePanelLoading"
|
||||
/>
|
||||
<div
|
||||
v-if="selectedTemplateRow?.type === 'custom' && selectedTemplateRow.mappingStatus !== 'ready'"
|
||||
class="template-draft-tip"
|
||||
>
|
||||
<a-tag size="small" color="orangered">待编辑</a-tag>
|
||||
<span>{{ t('order.placeOrder.batch.block.templateDraft') }}</span>
|
||||
<div class="upload-subcard">
|
||||
<div class="upload-subcard__title">{{ t('order.placeOrder.template.label') }}</div>
|
||||
<div class="template-selector">
|
||||
<a-select
|
||||
v-model="selectedTemplate"
|
||||
class="template-select"
|
||||
allow-search
|
||||
:placeholder="t('order.placeOrder.batch.pickTemplateHint')"
|
||||
:options="templateOptions"
|
||||
:loading="templatePanelLoading"
|
||||
/>
|
||||
<div
|
||||
v-if="selectedTemplateRow?.type === 'custom' && selectedTemplateRow.mappingStatus !== 'ready'"
|
||||
class="template-draft-tip"
|
||||
>
|
||||
<a-tag size="small" color="orangered">待编辑</a-tag>
|
||||
<span>{{ t('order.placeOrder.batch.block.templateDraft') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="batch-sender-block">
|
||||
<label class="selector-label">{{ t('order.placeOrder.batch.senderLabel') }}</label>
|
||||
<a-select
|
||||
:model-value="selectedSenderAddressId"
|
||||
allow-search
|
||||
class="template-select"
|
||||
:loading="senderAddressLoading"
|
||||
:placeholder="t('order.placeOrder.single.senderSelectPlaceholder')"
|
||||
:options="senderSelectOptions"
|
||||
@change="handleSenderAddressSelect"
|
||||
/>
|
||||
<p v-if="selectedSenderSummary" class="batch-sender-summary">{{ selectedSenderSummary }}</p>
|
||||
<p v-else-if="!senderAddressLoading && !senderAddressOptions.length" class="batch-sender-empty">
|
||||
{{ t('order.placeOrder.single.senderEmpty') }}
|
||||
<a-link @click="goToSenderAddressManage">{{ t('order.placeOrder.single.senderManage') }}</a-link>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="upload-left-tips">
|
||||
<p>{{ t('order.placeOrder.upload.tip1') }}</p>
|
||||
<p>{{ t('order.placeOrder.batch.templateTip') }}</p>
|
||||
<p>{{ t('order.placeOrder.upload.tip2') }}</p>
|
||||
<div class="upload-subcard upload-subcard--fill">
|
||||
<div class="upload-subcard__title">{{ t('order.placeOrder.batch.addressCardTitle') }}</div>
|
||||
<div class="batch-sender-block">
|
||||
<template v-if="isAdmin">
|
||||
<label class="selector-label">{{ t('order.placeOrder.customerLabel') }}</label>
|
||||
<a-select
|
||||
v-model="adminCustomerId"
|
||||
allow-search
|
||||
allow-clear
|
||||
class="template-select"
|
||||
:placeholder="t('order.placeOrder.selectCustomer')"
|
||||
:loading="customerLoading"
|
||||
:options="customerOptions"
|
||||
/>
|
||||
</template>
|
||||
<label class="selector-label">{{ t('order.placeOrder.batch.senderLabel') }}</label>
|
||||
<a-select
|
||||
:model-value="selectedSenderAddressId"
|
||||
allow-search
|
||||
class="template-select"
|
||||
:loading="senderAddressLoading"
|
||||
:placeholder="t('order.placeOrder.batch.senderPlaceholder')"
|
||||
:options="senderSelectOptions"
|
||||
@change="handleSenderAddressSelect"
|
||||
/>
|
||||
<p v-if="selectedSenderSummary" class="batch-sender-summary">{{ selectedSenderSummary }}</p>
|
||||
</div>
|
||||
<div class="upload-subcard__bottom">
|
||||
<p class="upload-left-tips">{{ t('order.placeOrder.batch.addressTip') }}</p>
|
||||
<div class="upload-subcard__footer">
|
||||
<a-button class="app-btn-toolbar" @click="goToSenderAddressManage">
|
||||
{{ t('order.placeOrder.single.senderManage') }}
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -3795,7 +3866,7 @@ import SelectDeclarationSkuModal from './components/SelectDeclarationSkuModal.vu
|
||||
<div class="upload-file-hint">{{ t('order.placeOrder.upload.fileReady') }}</div>
|
||||
</div>
|
||||
<div v-if="!pendingUploadFile" class="upload-inline-tips">
|
||||
<p class="upload-inline-tip primary">{{ t('order.placeOrder.batch.block.noFile') }}</p>
|
||||
<p class="upload-inline-tip">{{ t('order.placeOrder.batch.templateTip') }}</p>
|
||||
</div>
|
||||
<div v-if="pendingUploadFile" class="upload-file-actions" @click.stop>
|
||||
<a-button size="small" @click="triggerFilePicker">
|
||||
@ -4084,10 +4155,8 @@ import SelectDeclarationSkuModal from './components/SelectDeclarationSkuModal.vu
|
||||
:title="t('order.placeOrder.single.senderModalTitle')"
|
||||
:mask-closable="true"
|
||||
:esc-to-close="true"
|
||||
width="560px"
|
||||
width="600px"
|
||||
unmount-on-close
|
||||
:ok-text="t('order.placeOrder.single.senderModalOk')"
|
||||
@ok="senderModalVisible = false"
|
||||
>
|
||||
<a-spin :loading="senderAddressLoading" class="sender-modal-spin">
|
||||
<div class="sender-modal-body">
|
||||
@ -4096,7 +4165,6 @@ import SelectDeclarationSkuModal from './components/SelectDeclarationSkuModal.vu
|
||||
<a-select
|
||||
:model-value="selectedSenderAddressId"
|
||||
allow-search
|
||||
allow-clear
|
||||
:placeholder="t('order.placeOrder.single.senderSelectPlaceholder')"
|
||||
:options="senderSelectOptions"
|
||||
@change="handleSenderAddressSelect"
|
||||
@ -4124,17 +4192,27 @@ import SelectDeclarationSkuModal from './components/SelectDeclarationSkuModal.vu
|
||||
v-else-if="!senderAddressOptions.length"
|
||||
:description="t('order.placeOrder.single.senderEmpty')"
|
||||
/>
|
||||
|
||||
<div class="sender-modal-actions">
|
||||
<a-button size="small" type="text" status="danger" :disabled="!selectedSenderSummary" @click="clearSelectedSender">
|
||||
{{ t('order.placeOrder.single.senderClear') }}
|
||||
</div>
|
||||
</a-spin>
|
||||
<template #footer>
|
||||
<div class="sender-modal-footer">
|
||||
<a-button class="app-btn-toolbar" @click="goToSenderAddressManage">
|
||||
{{ t('order.placeOrder.single.senderManage') }}
|
||||
</a-button>
|
||||
<div class="sender-modal-footer__right">
|
||||
<a-button class="app-btn-toolbar" @click="senderModalVisible = false">
|
||||
{{ t('order.placeOrder.template.cancel') }}
|
||||
</a-button>
|
||||
<a-button size="small" type="text" @click="goToSenderAddressManage">
|
||||
{{ t('order.placeOrder.single.senderManage') }}
|
||||
<a-button
|
||||
type="primary"
|
||||
:disabled="!selectedSenderAddressId"
|
||||
@click="senderModalVisible = false"
|
||||
>
|
||||
{{ t('order.placeOrder.single.senderModalOk') }}
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</a-spin>
|
||||
</template>
|
||||
</a-modal>
|
||||
|
||||
<a-modal
|
||||
@ -4160,6 +4238,7 @@ import SelectDeclarationSkuModal from './components/SelectDeclarationSkuModal.vu
|
||||
|
||||
<SelectDeclarationSkuModal
|
||||
v-model:visible="declarationSkuModalVisible"
|
||||
:customer-id="resolveSelectedCustomerId()"
|
||||
@select="handleDeclarationSkuSelect"
|
||||
/>
|
||||
|
||||
@ -4280,8 +4359,14 @@ import SelectDeclarationSkuModal from './components/SelectDeclarationSkuModal.vu
|
||||
}
|
||||
|
||||
.single-toolbar-right {
|
||||
flex-wrap: wrap;
|
||||
flex-wrap: nowrap;
|
||||
flex-shrink: 0;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.place-order-customer-select {
|
||||
width: 180px;
|
||||
}
|
||||
|
||||
.single-tips {
|
||||
@ -4837,11 +4922,45 @@ import SelectDeclarationSkuModal from './components/SelectDeclarationSkuModal.vu
|
||||
|
||||
.upload-card-main__left {
|
||||
gap: 12px;
|
||||
justify-content: flex-start;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.upload-subcard {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
padding: 14px;
|
||||
border-radius: 10px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--color-border-2);
|
||||
background: var(--color-fill-2);
|
||||
justify-content: flex-start;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.upload-subcard--fill {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.upload-subcard__title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-1);
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
.upload-subcard__bottom {
|
||||
margin-top: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.upload-subcard__footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.upload-card-main__right {
|
||||
@ -4893,17 +5012,10 @@ import SelectDeclarationSkuModal from './components/SelectDeclarationSkuModal.vu
|
||||
.batch-sender-summary {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
line-height: 1.45;
|
||||
color: var(--color-text-2);
|
||||
}
|
||||
|
||||
.batch-sender-empty {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
color: var(--color-text-3);
|
||||
}
|
||||
|
||||
.template-draft-tip {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
@ -4914,20 +5026,10 @@ import SelectDeclarationSkuModal from './components/SelectDeclarationSkuModal.vu
|
||||
}
|
||||
|
||||
.upload-left-tips {
|
||||
margin-top: auto;
|
||||
padding-top: 12px;
|
||||
border-top: 1px dashed var(--color-border-2);
|
||||
|
||||
p {
|
||||
margin: 0 0 4px;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
color: var(--color-text-2);
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
color: var(--color-text-3);
|
||||
}
|
||||
|
||||
.upload-area {
|
||||
@ -5693,6 +5795,10 @@ import SelectDeclarationSkuModal from './components/SelectDeclarationSkuModal.vu
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
|
||||
:deep(.arco-form-item) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.sender-preview {
|
||||
@ -5724,10 +5830,18 @@ import SelectDeclarationSkuModal from './components/SelectDeclarationSkuModal.vu
|
||||
}
|
||||
}
|
||||
|
||||
.sender-modal-actions {
|
||||
.sender-modal-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sender-modal-footer__right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.single-form-footer {
|
||||
|
||||
@ -379,6 +379,7 @@ const userFormState = reactive<UserCreateDto & UserFormExtState & { organization
|
||||
employeeNo: '',
|
||||
wechatId: '',
|
||||
wechatQrCode: '',
|
||||
wecomKfUrl: '',
|
||||
wechatQrFiles: [],
|
||||
})
|
||||
|
||||
@ -689,6 +690,7 @@ async function handleAddUser() {
|
||||
employeeNo,
|
||||
wechatId: '',
|
||||
wechatQrCode: '',
|
||||
wecomKfUrl: '',
|
||||
wechatQrFiles: [],
|
||||
})
|
||||
applyEmployeeDto(employeeFormState, null, '')
|
||||
@ -738,6 +740,7 @@ async function handleEditUser(record: UserDto) {
|
||||
employeeNo: profile.employeeNo || '',
|
||||
wechatId: profile.wechatId || '',
|
||||
wechatQrCode: profile.wechatQrCode || '',
|
||||
wecomKfUrl: profile.wecomKfUrl || '',
|
||||
wechatQrFiles: urlToUploadFileList(profile.wechatQrCode, 'user-wx'),
|
||||
})
|
||||
await loadEmployeeForm(record.id || '', userDetail.name || '')
|
||||
@ -754,6 +757,7 @@ async function handleEditUser(record: UserDto) {
|
||||
employeeNo: profile.employeeNo || '',
|
||||
wechatId: profile.wechatId || '',
|
||||
wechatQrCode: profile.wechatQrCode || '',
|
||||
wecomKfUrl: profile.wecomKfUrl || '',
|
||||
wechatQrFiles: urlToUploadFileList(profile.wechatQrCode, 'user-wx'),
|
||||
})
|
||||
await loadEmployeeForm(record.id || '', record.name || '')
|
||||
|
||||
@ -247,6 +247,7 @@ const formState = reactive<UserCreateDto & UserFormExtState & { id?: string; org
|
||||
employeeNo: '',
|
||||
wechatId: '',
|
||||
wechatQrCode: '',
|
||||
wecomKfUrl: '',
|
||||
wechatQrFiles: [],
|
||||
})
|
||||
|
||||
@ -342,6 +343,7 @@ async function handleAdd() {
|
||||
employeeNo,
|
||||
wechatId: '',
|
||||
wechatQrCode: '',
|
||||
wecomKfUrl: '',
|
||||
wechatQrFiles: [],
|
||||
})
|
||||
applyEmployeeDto(
|
||||
@ -373,6 +375,7 @@ async function handleEdit(record: UserDto) {
|
||||
employeeNo: profile.employeeNo || '',
|
||||
wechatId: profile.wechatId || '',
|
||||
wechatQrCode: profile.wechatQrCode || '',
|
||||
wecomKfUrl: profile.wecomKfUrl || '',
|
||||
wechatQrFiles: urlToUploadFileList(profile.wechatQrCode, 'user-wx'),
|
||||
})
|
||||
await loadEmployeeForm(record.id || '', userDetail.name || '')
|
||||
@ -389,6 +392,7 @@ async function handleEdit(record: UserDto) {
|
||||
employeeNo: profile.employeeNo || '',
|
||||
wechatId: profile.wechatId || '',
|
||||
wechatQrCode: profile.wechatQrCode || '',
|
||||
wecomKfUrl: profile.wecomKfUrl || '',
|
||||
wechatQrFiles: urlToUploadFileList(profile.wechatQrCode, 'user-wx'),
|
||||
})
|
||||
await loadEmployeeForm(record.id || '', record.name || '')
|
||||
|
||||