优化H5订单轨迹展示并修复视口变化和输入区高度
This commit is contained in:
+8
-2
@@ -155,6 +155,7 @@ body { padding: 20px; height: 100dvh; overflow: hidden; }
|
||||
.order-data-fields { display: grid; grid-template-columns: minmax(90px, auto) minmax(0, 1fr); gap: 7px 16px; margin: 14px 0; font-size: 13px; }
|
||||
.order-data-fields dt { color: var(--muted); }
|
||||
.order-data-fields dd { margin: 0; overflow-wrap: anywhere; }
|
||||
.order-details-toggle { display: none; }
|
||||
.order-history-empty { color: var(--muted); font-size: 11px; margin: 10px 0; }
|
||||
.order-history-title { margin: 18px 0 10px; font-size: 13px; font-weight: 600; }
|
||||
.order-tracking-status { margin: 0 0 12px; color: var(--brand-text); font-size: 13px; }
|
||||
@@ -236,7 +237,7 @@ body { padding: 20px; height: 100dvh; overflow: hidden; }
|
||||
.composer { width: calc(100% - 24px); padding-bottom: calc(10px + env(safe-area-inset-bottom)); }
|
||||
.hot-bar { flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none; gap: 5px; margin-bottom: 8px; }
|
||||
.hot-bar-label { display: none; }
|
||||
.hot-bar .hot-capsule { white-space: nowrap; flex-shrink: 0; }
|
||||
.hot-bar .hot-capsule { white-space: nowrap; flex-shrink: 0; min-height: 32px; padding: 6px 9px; font-size: 11px; }
|
||||
.input-area { gap: 7px; padding: 7px; }
|
||||
#send-button { min-width: 57px; padding: 0 12px; }
|
||||
#upload-btn { flex-basis: 36px; height: 42px; }
|
||||
@@ -249,6 +250,11 @@ body { padding: 20px; height: 100dvh; overflow: hidden; }
|
||||
.track-text { grid-template-columns: 1fr; gap: 2px; }
|
||||
.track-place { grid-column: 1; }
|
||||
.robot-message.track-card { padding: 16px; }
|
||||
.track-no { font-size: 15px; }
|
||||
.track-head { align-items: flex-start; gap: 10px; }
|
||||
.track-no { min-width: 0; flex: 1; font-size: 15px; }
|
||||
.order-details-toggle { display: block; width: 100%; min-height: 44px; margin: 0 0 12px; border: 1px solid var(--border); border-radius: 7px; background: var(--surface-soft); color: var(--brand-text); font-size: 12px; }
|
||||
.order-data-fields.is-collapsed { display: none; }
|
||||
.track-expand { min-height: 44px; }
|
||||
.msg-action-btn { min-height: 40px; }
|
||||
.to-bottom { right: 15px; }
|
||||
}
|
||||
|
||||
+40
-2
@@ -265,6 +265,8 @@ const I18N = {
|
||||
orderNo: "订单号",
|
||||
orderHistory: "订单业务状态历史",
|
||||
noOrderHistory: "未提供订单状态历史",
|
||||
moreOrderDetails: "展开订单信息",
|
||||
lessOrderDetails: "收起订单信息",
|
||||
logisticsHistory: "物流轨迹",
|
||||
noLogisticsHistory: "暂无已发生的物流节点",
|
||||
estimatedTracking: "预计节点",
|
||||
@@ -360,6 +362,8 @@ const I18N = {
|
||||
orderNo: "Order number",
|
||||
orderHistory: "Order workflow history",
|
||||
noOrderHistory: "No order status history provided",
|
||||
moreOrderDetails: "Show order details",
|
||||
lessOrderDetails: "Hide order details",
|
||||
logisticsHistory: "Shipment tracking",
|
||||
noLogisticsHistory: "No actual shipment events available",
|
||||
estimatedTracking: "Estimated milestones",
|
||||
@@ -871,7 +875,7 @@ function fillTrackCard(el, card){
|
||||
|
||||
if(isOrder){
|
||||
const fields = document.createElement('dl');
|
||||
fields.className = 'order-data-fields';
|
||||
fields.className = 'order-data-fields is-collapsed';
|
||||
(card.fields || []).forEach(function(field){
|
||||
const label = document.createElement('dt');
|
||||
label.dataset.i18n = 'order_'+field.key;
|
||||
@@ -880,7 +884,23 @@ function fillTrackCard(el, card){
|
||||
value.textContent = field.value;
|
||||
fields.append(label, value);
|
||||
});
|
||||
el.appendChild(fields);
|
||||
if((card.fields || []).length){
|
||||
const toggle = document.createElement('button');
|
||||
toggle.type = 'button';
|
||||
toggle.className = 'order-details-toggle';
|
||||
toggle.setAttribute('aria-expanded', 'false');
|
||||
toggle.dataset.i18n = 'moreOrderDetails';
|
||||
toggle.textContent = t('moreOrderDetails');
|
||||
toggle.onclick = function(){
|
||||
const expanded = toggle.getAttribute('aria-expanded') !== 'true';
|
||||
fields.classList.toggle('is-collapsed', !expanded);
|
||||
toggle.setAttribute('aria-expanded', String(expanded));
|
||||
toggle.dataset.i18n = expanded ? 'lessOrderDetails' : 'moreOrderDetails';
|
||||
toggle.textContent = t(toggle.dataset.i18n);
|
||||
};
|
||||
el.appendChild(toggle);
|
||||
el.appendChild(fields);
|
||||
}
|
||||
const title = document.createElement('p');
|
||||
title.className = 'order-history-title';
|
||||
title.dataset.i18n = isLogistics ? 'logisticsHistory' : 'orderHistory';
|
||||
@@ -1673,6 +1693,10 @@ sendBtn.addEventListener("click", function(){
|
||||
|
||||
// ===== 多行输入框:自动增高 =====
|
||||
function autoGrow(){
|
||||
if(!inputDom.value){
|
||||
inputDom.style.height = '';
|
||||
return;
|
||||
}
|
||||
inputDom.style.height = "auto";
|
||||
inputDom.style.height = Math.min(inputDom.scrollHeight, 140) + "px";
|
||||
}
|
||||
@@ -1703,6 +1727,8 @@ toBottomBtn.onclick = ()=>{
|
||||
if(window.ResizeObserver){
|
||||
new ResizeObserver(function(){
|
||||
document.querySelector('.main-chat').style.setProperty('--composer-height', document.querySelector('.composer').offsetHeight + 'px');
|
||||
if(!chatDom.querySelector('.ai-welcome-card')) scrollChatToEnd();
|
||||
updateToBottom();
|
||||
}).observe(document.querySelector('.composer'));
|
||||
}
|
||||
|
||||
@@ -1728,6 +1754,15 @@ function renderSidebar(){
|
||||
}
|
||||
renderSidebar();
|
||||
|
||||
function syncChatViewport(){
|
||||
const viewport = window.visualViewport;
|
||||
chatContainer.style.height = viewport && isNarrow() && viewport.scale === 1
|
||||
? Math.round(viewport.height) + 'px' : '';
|
||||
autoGrow();
|
||||
if(!chatDom.querySelector('.ai-welcome-card')) scrollChatToEnd();
|
||||
updateToBottom();
|
||||
}
|
||||
|
||||
toggleBtn.onclick = function(){
|
||||
sideOpen = !sideOpen;
|
||||
renderSidebar();
|
||||
@@ -1752,7 +1787,10 @@ window.addEventListener("resize", function(){
|
||||
wasNarrow = narrow;
|
||||
}
|
||||
renderSidebar();
|
||||
syncChatViewport();
|
||||
});
|
||||
if(window.visualViewport) window.visualViewport.addEventListener('resize', syncChatViewport);
|
||||
syncChatViewport();
|
||||
|
||||
document.querySelectorAll('[data-question-index]').forEach(function(button){
|
||||
button.addEventListener('click', function(){
|
||||
|
||||
@@ -6,12 +6,12 @@ const vm = require('node:vm');
|
||||
const flush = () => new Promise(setImmediate);
|
||||
|
||||
async function customerPage(pageOptions = {}){
|
||||
const elements = new Map(), listeners = {}, intervals = new Map(), timers = new Map(), calls = [], hooks = {};
|
||||
const elements = new Map(), listeners = {}, windowListeners = {}, viewportListeners = {}, intervals = new Map(), timers = new Map(), calls = [], hooks = {};
|
||||
let intervalId = 0, timerId = 0, nextConvId = 1;
|
||||
class Element {
|
||||
constructor(){
|
||||
this.children = []; this.attributes = {}; this.value = ''; this.style = {};
|
||||
this.className = ''; this.dataset = {}; this.scrollHeight = 300; this.clientHeight = 300;
|
||||
this.className = ''; this.dataset = {}; this.scrollHeight = 300; this.clientHeight = 300; this.scrollTop = 0;
|
||||
this.listeners = {};
|
||||
this.classList = {
|
||||
contains: name => this.className.split(' ').includes(name),
|
||||
@@ -66,7 +66,12 @@ async function customerPage(pageOptions = {}){
|
||||
FormData:class { append(){} },
|
||||
requestAnimationFrame:callback => callback(),
|
||||
localStorage:{getItem:key => (pageOptions.storage || {})[key] ?? null, setItem(){}},
|
||||
window:{innerWidth:1200, addEventListener(){}, matchMedia:() => ({matches:false}), prompt:() => pageOptions.offlineMessage || null},
|
||||
window:{innerWidth:pageOptions.width || 1200,
|
||||
addEventListener:(name,callback) => { (windowListeners[name] ||= []).push(callback); },
|
||||
matchMedia:query => ({matches:pageOptions.width ? pageOptions.width <= Number((query.match(/max-width:\s*(\d+)px/) || [])[1]) : false}),
|
||||
visualViewport:pageOptions.viewportHeight ? {height:pageOptions.viewportHeight,scale:1,
|
||||
addEventListener:(name,callback) => { (viewportListeners[name] ||= []).push(callback); }} : undefined,
|
||||
prompt:() => pageOptions.offlineMessage || null},
|
||||
setInterval: callback => { const id = ++intervalId; intervals.set(id, callback); return id; },
|
||||
clearInterval:id => intervals.delete(id),
|
||||
setTimeout:(callback, delay) => { const id=++timerId; timers.set(id,{callback,delay}); return id; },
|
||||
@@ -100,6 +105,16 @@ async function customerPage(pageOptions = {}){
|
||||
if(pageOptions.human !== false) await context.window.showHuman();
|
||||
await flush();
|
||||
return {context, element, calls, hooks, quickButtons,
|
||||
resize:async height => {
|
||||
if(context.window.visualViewport) context.window.visualViewport.height=height;
|
||||
(windowListeners.resize || []).forEach(callback => callback());
|
||||
await flush();
|
||||
},
|
||||
resizeViewport:async (height,scale=1) => {
|
||||
Object.assign(context.window.visualViewport,{height,scale});
|
||||
(viewportListeners.resize || []).forEach(callback => callback());
|
||||
await flush();
|
||||
},
|
||||
setState:value => { state=value; },
|
||||
runTimers:async delay => { for(const [id, timer] of Array.from(timers)) if(timer.delay === delay){ timers.delete(id); timer.callback(); } await flush(); },
|
||||
tick:async () => { Array.from(intervals.values()).forEach(callback => callback()); await flush(); },
|
||||
@@ -559,3 +574,57 @@ test('shipment cards separate forecasts from actual progress and retain both in
|
||||
assert.match(timelines[1].querySelector('.track-text').innerHTML, /<img/);
|
||||
assert.ok(log.querySelector('.msg-actions'));
|
||||
});
|
||||
|
||||
test('order details can expand without hiding the shipment timeline', async () => {
|
||||
const app = await customerPage({human:false,width:390});
|
||||
const card = {kind:'order',no:'ORDER2026001',status:'履约中',node_kind:'logistics',
|
||||
fields:[{key:'shipment',value:'LONG-SHIPMENT-20261030001'}],
|
||||
nodes:[{time:'2026-10-29 10:00',text:'海运途中',place:''}],summary:'订单概要'};
|
||||
app.hooks['/stream_chat'] = () => streamResponse(['data: '+JSON.stringify({card})+'\n\ndata: {"d":1}\n\n']);
|
||||
await app.context.window.fillInput('ORDER2026001');
|
||||
await flush();
|
||||
const log=app.element('chatDom'), fields=log.querySelector('.order-data-fields');
|
||||
const toggle=log.querySelector('.order-details-toggle');
|
||||
assert.equal(toggle.getAttribute('aria-expanded'),'false');
|
||||
assert.ok(fields.classList.contains('is-collapsed'));
|
||||
assert.match(log.querySelector('.track-timeline').textContent,/海运途中/);
|
||||
await app.click(toggle);
|
||||
assert.equal(toggle.getAttribute('aria-expanded'),'true');
|
||||
assert.equal(fields.classList.contains('is-collapsed'),false);
|
||||
assert.match(fields.textContent,/LONG-SHIPMENT-20261030001/);
|
||||
await app.click(toggle);
|
||||
assert.ok(fields.classList.contains('is-collapsed'));
|
||||
});
|
||||
|
||||
test('viewport changes keep latest replies visible and preserve an older reading position', async () => {
|
||||
const app=await customerPage({human:false,width:390,viewportHeight:844});
|
||||
await app.context.window.fillInput('查询物流');
|
||||
await flush();
|
||||
const log=app.element('chatDom');
|
||||
Object.assign(log,{scrollHeight:1500,clientHeight:600,scrollTop:900});
|
||||
log.listeners.scroll();
|
||||
log.clientHeight=260;
|
||||
await app.resize(500);
|
||||
assert.equal(app.element('.chat-container').style.height,'500px');
|
||||
assert.equal(log.scrollTop,log.scrollHeight);
|
||||
log.scrollTop=100;
|
||||
log.listeners.scroll();
|
||||
await app.resizeViewport(450);
|
||||
assert.equal(app.element('.chat-container').style.height,'450px');
|
||||
assert.equal(log.scrollTop,100);
|
||||
await app.resizeViewport(400,2);
|
||||
assert.equal(app.element('.chat-container').style.height,'');
|
||||
assert.equal(log.scrollTop,100);
|
||||
});
|
||||
|
||||
test('clearing a sent draft restores a single-row composer instead of sizing to its placeholder', async () => {
|
||||
const app=await customerPage({human:false,width:320});
|
||||
const input=app.element('input-text');
|
||||
input.value='多行内容\n第二行';
|
||||
input.listeners.input();
|
||||
assert.equal(input.style.height,'140px');
|
||||
await app.context.window.fillInput(input.value);
|
||||
await flush();
|
||||
assert.equal(input.value,'');
|
||||
assert.equal(input.style.height,'');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user