优化入仓码查询入口与卡片交互
This commit is contained in:
+73
-37
@@ -41,6 +41,8 @@ let historyRequestVersion = 0;
|
||||
let followLatest = true;
|
||||
let voiceInput = null;
|
||||
let customerLogin = null;
|
||||
let orderLookup = null;
|
||||
let lookupRequestVersion = 0;
|
||||
let customerRecovery = null;
|
||||
let creatingConv = null;
|
||||
let sendPreparing = false;
|
||||
@@ -234,6 +236,11 @@ const I18N = {
|
||||
customerUsernamePlaceholder: "请输入账号", customerPasswordPlaceholder: "请输入密码",
|
||||
customerSigningIn: "登录中…", customerLoginFailed: "登录失败,请稍后重试。",
|
||||
customerLogoutFailed: "退出失败,请重试。",
|
||||
lookupTitle: "查入仓码", lookupOrderTitle: "查询订单", lookupPlaceholder: "请输入入仓码",
|
||||
lookupOrderPlaceholder: "请输入订单号", lookupSubmit: "查询轨迹", lookupSending: "查询中…",
|
||||
lookupInvalid: "请输入完整的入仓码或订单号。", lookupFailed: "查询未发送成功,请重试。",
|
||||
lookupBusy: "请等待当前操作完成后再查询。", lookupAIOnly: "请先返回 AI 问答,再查询订单轨迹。",
|
||||
lookupAction: "查询或复制",
|
||||
newChat: "新建对话",
|
||||
clearAll: "清空全部历史",
|
||||
history: "历史对话",
|
||||
@@ -345,6 +352,11 @@ const I18N = {
|
||||
customerUsernamePlaceholder: "Enter username", customerPasswordPlaceholder: "Enter password",
|
||||
customerSigningIn: "Signing in…", customerLoginFailed: "Could not sign in. Please try again.",
|
||||
customerLogoutFailed: "Could not sign out. Please try again.",
|
||||
lookupTitle: "Track by inbound code", lookupOrderTitle: "Look up an order", lookupPlaceholder: "Enter an inbound code",
|
||||
lookupOrderPlaceholder: "Enter an order number", lookupSubmit: "Track shipment", lookupSending: "Sending…",
|
||||
lookupInvalid: "Enter a complete inbound code or order number.", lookupFailed: "Could not send the lookup. Please retry.",
|
||||
lookupBusy: "Wait for the current action to finish before looking up an order.", lookupAIOnly: "Return to AI chat before looking up an order.",
|
||||
lookupAction: "Look up or copy",
|
||||
newChat: "New Chat",
|
||||
clearAll: "Clear All History",
|
||||
history: "History",
|
||||
@@ -462,6 +474,10 @@ function applyLanguage(lang){
|
||||
LANG = lang === "en" ? "en" : "zh";
|
||||
const dict = I18N[LANG];
|
||||
if(customerLogin) customerLogin.render();
|
||||
if(orderLookup) orderLookup.render();
|
||||
document.querySelectorAll('.order-reference').forEach(function(button){
|
||||
button.setAttribute('aria-label',t('lookupAction')+' '+t(button.dataset.referenceLabel)+' '+button.textContent);
|
||||
});
|
||||
document.documentElement.lang = LANG === "en" ? "en" : "zh-CN";
|
||||
document.querySelectorAll("[data-i18n]").forEach(function(element){
|
||||
const value = dict[element.dataset.i18n];
|
||||
@@ -617,7 +633,7 @@ function welcomeHTML(){
|
||||
const h = t("hotList");
|
||||
const kinds = ["track", "quote", "issue", "human"];
|
||||
const cards = q.map(function(question, index){
|
||||
const action = index === 3 ? 'data-human-support' : `data-question="${escHtml(question)}"`;
|
||||
const action = index === 3 ? 'data-human-support' : index === 0 ? 'data-order-lookup' : `data-question="${escHtml(question)}"`;
|
||||
return `<button type="button" class="suggest-item" ${action}><span class="suggest-icon">${welcomeIcon(kinds[index])}</span><span class="suggest-copy"><strong>${question}</strong></span><span class="suggest-arrow" aria-hidden="true">↗</span></button>`;
|
||||
}).join("");
|
||||
return `
|
||||
@@ -859,6 +875,7 @@ function ensureConv(){
|
||||
function resetConversation(id){
|
||||
flushDraft();
|
||||
emojiPicker.close();
|
||||
if(orderLookup) orderLookup.cancel();
|
||||
if(voiceInput) voiceInput.cancel();
|
||||
if(cancelActive){ cancelActive(); cancelActive = null; }
|
||||
if(sseInstance){ sseInstance.close(); sseInstance = null; }
|
||||
@@ -941,7 +958,16 @@ function fillTrackCard(el, card){
|
||||
no.className = "track-no";
|
||||
const inbound = isOrder && (card.fields || []).find(field=>field && field.key === 'inbound' && field.value);
|
||||
const numberLabel = inbound ? 'order_inbound' : isOrder ? 'orderNo' : 'trackingNo';
|
||||
no.innerHTML = '<span data-i18n="'+numberLabel+'">' + t(numberLabel) + "</span>" + escHtml(inbound ? inbound.value : card.no);
|
||||
no.innerHTML = '<span data-i18n="'+numberLabel+'">' + t(numberLabel) + "</span>";
|
||||
if(isOrder){
|
||||
const reference=document.createElement('button');
|
||||
const value=String(inbound ? inbound.value : card.no), version=conversationVersion;
|
||||
reference.type='button';reference.className='order-reference';reference.textContent=value;
|
||||
reference.dataset.referenceLabel=numberLabel;
|
||||
reference.setAttribute('aria-label',t('lookupAction')+' '+t(numberLabel)+' '+value);
|
||||
reference.onclick=function(){if(version === conversationVersion) return showOrderLookup(value,inbound ? 'inbound' : 'order',reference);};
|
||||
no.appendChild(reference);
|
||||
}else no.innerHTML += escHtml(card.no);
|
||||
const st = document.createElement("div");
|
||||
st.className = "track-status";
|
||||
st.textContent = card.status;
|
||||
@@ -1121,12 +1147,33 @@ window.askNewTopic = function(words){
|
||||
return doSend();
|
||||
}
|
||||
|
||||
window.sendOrder = function(){
|
||||
const no = prompt("请输入运单号:");
|
||||
if(no && no.trim()){
|
||||
inputDom.value = "我的运单号是 " + no.trim() + ",帮我查一下轨迹";
|
||||
doSend();
|
||||
}
|
||||
window.sendOrder = function(){ return showOrderLookup(); };
|
||||
|
||||
function lookupBlocked(){
|
||||
if(window.humanSid || humanRequestToken || humanClosing || humanSendingToken) return t('lookupAIOnly');
|
||||
if(openingConv !== null) return t(openingConv === 'failed' ? 'historyLoadFailed' : 'historyLoading');
|
||||
if(uploadPending || sendPreparing || window.__busy) return t('lookupBusy');
|
||||
return '';
|
||||
}
|
||||
|
||||
function sameCustomerContext(version,user){
|
||||
return version === conversationVersion || (customerRecovery &&
|
||||
customerRecovery.from === version && customerRecovery.to === conversationVersion &&
|
||||
(!user || user.id === customerRecovery.customerId));
|
||||
}
|
||||
|
||||
async function showOrderLookup(reference,kind,source){
|
||||
const blocked=lookupBlocked();
|
||||
if(blocked){setComposerStatus(blocked,true);return;}
|
||||
const version=conversationVersion;
|
||||
const requestVersion=++lookupRequestVersion;
|
||||
function isCurrent(user){return requestVersion === lookupRequestVersion && sameCustomerContext(version,user);}
|
||||
return customerLogin.require(function(user){
|
||||
if(!isCurrent(user)) return;
|
||||
const blocked=lookupBlocked();
|
||||
if(blocked){setComposerStatus(blocked,true);return;}
|
||||
orderLookup.open(reference,kind,source);
|
||||
},isCurrent);
|
||||
}
|
||||
|
||||
function updateHumanMode(){
|
||||
@@ -1438,9 +1485,7 @@ function attachCustomerLogin(bubble, question){
|
||||
const version = conversationVersion;
|
||||
const convId = currentConvId;
|
||||
function sameContext(user){
|
||||
return version === conversationVersion || (customerRecovery &&
|
||||
customerRecovery.from === version && customerRecovery.to === conversationVersion &&
|
||||
(!user || user.id === customerRecovery.customerId));
|
||||
return sameCustomerContext(version,user);
|
||||
}
|
||||
button.onclick = function(){
|
||||
if(version !== conversationVersion) return;
|
||||
@@ -1498,27 +1543,7 @@ function attachActions(bubble, rawText, question, showRetry){
|
||||
copyBtn.textContent = t(copied ? 'copied' : 'copyFailed');
|
||||
setTimeout(()=>{ copyBtn.textContent = t("copy"); }, copied ? 1200 : 3000);
|
||||
};
|
||||
if(navigator.clipboard && navigator.clipboard.writeText){
|
||||
return navigator.clipboard.writeText(plain).then(()=>done(true)).catch(fallback);
|
||||
}else{
|
||||
return fallback();
|
||||
}
|
||||
function fallback(){
|
||||
const ta = document.createElement("textarea");
|
||||
ta.value = plain;
|
||||
ta.readOnly = true;
|
||||
ta.style.position = "fixed";
|
||||
ta.style.opacity = "0";
|
||||
const previousFocus = document.activeElement;
|
||||
document.body.appendChild(ta);
|
||||
let copied = false;
|
||||
try{ ta.focus({preventScroll:true}); ta.select(); copied = Boolean(document.execCommand("copy")); }catch(e){}
|
||||
finally{
|
||||
ta.remove();
|
||||
if(previousFocus && previousFocus.isConnected) previousFocus.focus({preventScroll:true});
|
||||
}
|
||||
done(copied);
|
||||
}
|
||||
return SupportUI.copyText(plain).then(done);
|
||||
};
|
||||
bar.appendChild(copyBtn);
|
||||
|
||||
@@ -1604,7 +1629,8 @@ function renderFollowups(bubble){
|
||||
bubble.appendChild(wrap);
|
||||
}
|
||||
|
||||
async function doSend(retryText){
|
||||
async function doSend(retryText, showQuestion){
|
||||
if(showQuestion && lookupBlocked()) return false;
|
||||
if(voiceInput && voiceInput.busy()){ setComposerStatus(t('voiceWait')); return false; }
|
||||
const isRetry = typeof retryText === "string";
|
||||
const sendWord = (isRetry ? retryText : inputDom.value).trim();
|
||||
@@ -1640,9 +1666,9 @@ async function doSend(retryText){
|
||||
}finally{
|
||||
if(version === conversationVersion) sendPreparing = false;
|
||||
}
|
||||
if(version !== conversationVersion || humanRequestToken) return;
|
||||
if(version !== conversationVersion || humanRequestToken || (showQuestion && window.humanSid)) return;
|
||||
if(!isRetry || showQuestion) addMsg(fullContent, true);
|
||||
if(!isRetry){
|
||||
addMsg(fullContent, true);
|
||||
if(inputDom.value === draft){ inputDom.value = ""; autoGrow(); }
|
||||
if(attachmentVersion === uploadVersion) clearAttachment();
|
||||
focusComposer();
|
||||
@@ -1939,6 +1965,7 @@ function syncChatViewport(){
|
||||
chatContainer.style.top = '';
|
||||
}
|
||||
if(customerLogin) customerLogin.viewport(chatContainer.style.height, chatContainer.style.top);
|
||||
if(orderLookup) orderLookup.viewport(chatContainer.style.height, chatContainer.style.top);
|
||||
chatContainer.classList.toggle('is-composing', narrow && document.activeElement === inputDom);
|
||||
autoGrow();
|
||||
if(!chatDom.querySelector('.ai-welcome-card')) scrollChatToEnd();
|
||||
@@ -2016,7 +2043,7 @@ voiceInput = ChatVoice.create({
|
||||
}
|
||||
});
|
||||
customerLogin = CustomerLogin.create({fetch:fetch, text:t,
|
||||
onOpen:function(){ if(voiceInput) voiceInput.cancel(); sideOpen=false; renderSidebar(); syncChatViewport(); },
|
||||
onOpen:function(){ if(voiceInput) voiceInput.cancel(); if(isNarrow()){sideOpen=false;renderSidebar();} syncChatViewport(); },
|
||||
onChange:function(user,previous){
|
||||
if(previous === undefined) return;
|
||||
if(previous && (!user || user.id !== previous.id)){
|
||||
@@ -2034,16 +2061,25 @@ customerLogin = CustomerLogin.create({fetch:fetch, text:t,
|
||||
});
|
||||
customerLogin.viewport(chatContainer.style.height,chatContainer.style.top);
|
||||
customerLogin.refresh();
|
||||
orderLookup = OrderLookup.create({text:t,canQuery:lookupBlocked,
|
||||
onOpen:function(){emojiPicker.close();if(voiceInput) voiceInput.cancel();if(isNarrow()){sideOpen=false;renderSidebar();}syncChatViewport();},
|
||||
query:function(reference,kind){return doSend((kind === 'order' ? '查询订单 ' : '查询入仓码 ')+reference,true);}
|
||||
});
|
||||
orderLookup.viewport(chatContainer.style.height,chatContainer.style.top);
|
||||
window.addEventListener('resize', function(){
|
||||
if(!isNarrow()) voiceInput.setMode(false);
|
||||
});
|
||||
|
||||
document.querySelectorAll('[data-question-index]').forEach(function(button){
|
||||
button.addEventListener('click', function(){
|
||||
window.askNewTopic(t('quick')[Number(button.dataset.questionIndex)]);
|
||||
const index=Number(button.dataset.questionIndex);
|
||||
if(index === 0) return showOrderLookup(undefined,undefined,button);
|
||||
return window.askNewTopic(t('quick')[index]);
|
||||
});
|
||||
});
|
||||
chatDom.addEventListener('click', function(event){
|
||||
const lookup=event.target.closest('button[data-order-lookup]');
|
||||
if(lookup){showOrderLookup(undefined,undefined,lookup);return;}
|
||||
const question = event.target.closest('button[data-question]');
|
||||
if(question) window.askNewTopic(question.dataset.question);
|
||||
const human = event.target.closest('button[data-human-support]');
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
// One lookup form for shortcuts and saved order cards; no draft or chat ownership changes.
|
||||
var OrderLookup = {
|
||||
create(options){
|
||||
const overlay=document.getElementById('order-lookup');
|
||||
const form=document.getElementById('order-lookup-form');
|
||||
const input=document.getElementById('order-reference');
|
||||
const label=document.getElementById('order-reference-label');
|
||||
const title=document.getElementById('order-lookup-title');
|
||||
const submit=document.getElementById('order-lookup-submit');
|
||||
const cancel=document.getElementById('order-lookup-cancel');
|
||||
const copy=document.getElementById('order-reference-copy');
|
||||
const error=document.getElementById('order-lookup-error');
|
||||
let kind='inbound', opener=null, busy=false, copying=false, copyState='', version=0, timer=null;
|
||||
overlay.hidden=true;
|
||||
function valid(value){
|
||||
return /^[A-Za-z0-9][A-Za-z0-9_-]{5,63}$/.test(value) && (value.match(/[0-9]/g) || []).length >= 3;
|
||||
}
|
||||
function status(message){ error.textContent=message || ''; error.hidden=!message; }
|
||||
function render(){
|
||||
title.textContent=options.text(kind === 'inbound' ? 'lookupTitle' : 'lookupOrderTitle');
|
||||
label.textContent=options.text(kind === 'inbound' ? 'order_inbound' : 'orderNo');
|
||||
input.placeholder=options.text(kind === 'inbound' ? 'lookupPlaceholder' : 'lookupOrderPlaceholder');
|
||||
submit.textContent=options.text(busy ? 'lookupSending' : 'lookupSubmit');
|
||||
cancel.textContent=options.text('customerCancel');
|
||||
copy.textContent=options.text(copyState || 'copy');
|
||||
submit.disabled=busy; cancel.disabled=busy; input.disabled=busy;
|
||||
copy.disabled=busy || copying || !valid(input.value.trim());
|
||||
}
|
||||
function clearTimer(){ if(timer !== null) clearTimeout(timer); timer=null; }
|
||||
function close(){
|
||||
version+=1;clearTimer();overlay.hidden=true;
|
||||
busy=false;copying=false;copyState='';input.value='';status('');render();
|
||||
if(opener && opener.isConnected) opener.focus({preventScroll:true});
|
||||
opener=null;
|
||||
}
|
||||
function open(reference, referenceKind, source){
|
||||
version+=1;clearTimer();kind=referenceKind === 'order' ? 'order' : 'inbound';
|
||||
busy=false;copying=false;copyState='';
|
||||
opener=source || document.activeElement;input.value=reference || '';status('');render();overlay.hidden=false;
|
||||
if(options.onOpen) options.onOpen();
|
||||
input.focus({preventScroll:true});
|
||||
}
|
||||
input.addEventListener('input',function(){ clearTimer();copyState='';status('');render(); });
|
||||
copy.onclick=async function(){
|
||||
const reference=input.value.trim(), token=version;
|
||||
if(busy || copying || !valid(reference)) return;
|
||||
copying=true;copyState='';status('');render();
|
||||
const copied=await SupportUI.copyText(reference);
|
||||
if(token !== version) return;
|
||||
copying=false;
|
||||
if(input.value.trim() === reference){
|
||||
copyState=copied ? 'copied' : 'copyFailed';
|
||||
if(!copied) status(options.text('copyFailed'));
|
||||
timer=setTimeout(function(){copyState='';timer=null;render();},copied ? 1200 : 3000);
|
||||
}
|
||||
render();
|
||||
};
|
||||
form.addEventListener('submit',async function(event){
|
||||
event.preventDefault();
|
||||
if(busy) return;
|
||||
const reference=input.value.trim(), token=version;
|
||||
if(!valid(reference)){status(options.text('lookupInvalid'));input.focus({preventScroll:true});return;}
|
||||
const blocked=options.canQuery();
|
||||
if(blocked){status(blocked);return;}
|
||||
busy=true;status('');render();
|
||||
try{
|
||||
const accepted=await options.query(reference,kind);
|
||||
if(token !== version) return;
|
||||
if(accepted) close();else status(options.text('lookupFailed'));
|
||||
}catch(error){if(token === version) status(options.text('lookupFailed'));}
|
||||
finally{if(token === version){busy=false;render();}}
|
||||
});
|
||||
cancel.onclick=function(){if(!busy) close();};
|
||||
overlay.addEventListener('click',function(event){if(event.target === overlay && !busy) close();});
|
||||
overlay.addEventListener('keydown',function(event){
|
||||
if(event.key === 'Escape' && !busy){event.preventDefault();close();return;}
|
||||
if(event.key === 'Tab'){
|
||||
const controls=[input,copy,cancel,submit].filter(control=>!control.disabled);
|
||||
event.preventDefault();
|
||||
if(controls.length){
|
||||
const index=controls.indexOf(document.activeElement);
|
||||
controls[(index+(event.shiftKey ? -1 : 1)+controls.length)%controls.length].focus({preventScroll:true});
|
||||
}
|
||||
}
|
||||
});
|
||||
render();
|
||||
return {open:open,cancel:close,render:render,viewport:function(height,top){
|
||||
overlay.style.height=height || '';overlay.style.top=top || '';overlay.style.bottom=height ? 'auto' : '';
|
||||
}};
|
||||
}
|
||||
};
|
||||
@@ -1,5 +1,26 @@
|
||||
// Shared controls for the customer page and service workspace.
|
||||
var SupportUI = {
|
||||
async copyText(text){
|
||||
try{
|
||||
if(navigator.clipboard && navigator.clipboard.writeText){
|
||||
await navigator.clipboard.writeText(text);
|
||||
return true;
|
||||
}
|
||||
}catch(error){}
|
||||
const field = document.createElement('textarea');
|
||||
field.value=text; field.readOnly=true;
|
||||
field.style.position='fixed'; field.style.opacity='0';
|
||||
const previous=document.activeElement;
|
||||
document.body.appendChild(field);
|
||||
try{
|
||||
field.focus({preventScroll:true}); field.select();
|
||||
return Boolean(document.execCommand('copy'));
|
||||
}catch(error){ return false; }
|
||||
finally{
|
||||
field.remove();
|
||||
if(previous && previous.isConnected) previous.focus({preventScroll:true});
|
||||
}
|
||||
},
|
||||
initAttachments(container, label){
|
||||
container.addEventListener('error', function(event){
|
||||
const image = event.target;
|
||||
|
||||
Reference in New Issue
Block a user