diff --git a/static/js/app.js b/static/js/app.js index 611af08..f02578a 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -4476,6 +4476,11 @@ function loadPage(page) { }); if (typeof loadSuppliers === 'function') loadSuppliers(1); } + // 国内订单可能已被其他人确认;切回缓存页面时重新查询,保留当前筛选和页码。 + if (page === 'orders_domestic' && typeof domesticLoadOrders === 'function') { + invalidateOrderColumnCache('domestic'); + domesticLoadOrders(domesticOrderPage || 1); + } // 先销毁原有 Chart 实例(canvas detach 后 chart 可能状态错乱) if (typeof Chart !== 'undefined') { const canvases = cached.querySelectorAll('canvas'); @@ -16160,22 +16165,36 @@ function renderPendingConfirmModal(modal, item, bomMaterials, onConfirm) { } function _refreshLinkedOrderLists() { - // 只重新请求当前已挂载的订单表,沿用各自现有的筛选、表头筛选和页码状态。 + // 未挂载的页面容器仍可能保留下单前的数据,切回时必须重新加载。 + ['orders_domestic', 'orders_overseas', 'orders_packaging', 'purchase_ready'].forEach(function(page) { + var container = window._pageCache && window._pageCache[page]; + if (container && !container.isConnected) { + delete window._pageCache[page]; + if (window._pageDataCache) delete window._pageDataCache[page]; + } + }); + ['domestic', 'overseas', 'packaging', 'backup'].forEach(function(type) { + invalidateOrderColumnCache(type); + }); + // 当前已挂载的订单表保留筛选和页码,立即重新请求。 if (document.getElementById('domesticOrdersTbody') && typeof domesticLoadOrders === 'function') domesticLoadOrders(domesticOrderPage || 1); if (document.getElementById('overseasOrdersTbody') && typeof overseasLoadOrders === 'function') overseasLoadOrders(overseasOrderPage || 1); if (document.getElementById('packagingOrdersTbody') && typeof packagingLoadOrders === 'function') packagingLoadOrders(packagingOrderPage || 1); + if (document.getElementById('orderTbody') && typeof loadBackupList === 'function') loadBackupList(false); } -function showConfirmRedirect(hasProduct, hasPackaging) { +function showConfirmRedirect(hasProduct, hasPackaging, productPage) { var links = []; if (hasProduct) { - links.push({ label: '📦 国内/海外订单管理', page: '/orders/domestic' }); + var overseas = productPage === 'orders_overseas'; + links.push({ label: overseas ? '📦 海外订单管理' : '📦 国内订单管理', + page: overseas ? '/orders/overseas' : '/orders/domestic' }); } if (hasPackaging) { links.push({ label: '📦 包材订单管理', page: '/orders/packaging' }); } // 下单备份始终存在(已标记为ordered的记录) - links.push({ label: '📋 下单备份管理', page: '/purchase' }); + links.push({ label: '📋 下单备份管理', page: '/purchase/ready' }); if (links.length === 0) return; @@ -16231,7 +16250,7 @@ function pendingBatchConfirm() { if (res.code === 0) { _refreshLinkedOrderLists(); showCenteredAlert('操作成功', '✅ 批量下单成功,共 ' + checkedCodes.length + ' 个', function() { - showConfirmRedirect(res.has_product_order, res.has_packaging_order); + showConfirmRedirect(res.has_product_order, res.has_packaging_order, res.redirect); }); loadPendingList(); } else { @@ -16323,7 +16342,7 @@ function pendingConfirmOrder(productCode) { _refreshLinkedOrderLists(); showCenteredAlert('操作成功', '✅ 下单成功', { zIndex: 100000, - onConfirm: function() { showConfirmRedirect(res.has_product_order, res.has_packaging_order); } + onConfirm: function() { showConfirmRedirect(res.has_product_order, res.has_packaging_order, res.redirect); } }); loadPendingList(); } else { diff --git a/tests/test_order_confirmation_navigation.js b/tests/test_order_confirmation_navigation.js new file mode 100644 index 0000000..9856cee --- /dev/null +++ b/tests/test_order_confirmation_navigation.js @@ -0,0 +1,110 @@ +/* Run with node: actual page functions in a minimal browser context. */ +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); +const vm = require('node:vm'); +const source = fs.readFileSync(path.join(__dirname, '../static/js/app.js'), 'utf8'); + +function functionSource(start, end) { + const offset = source.indexOf(start); + assert.ok(offset >= 0, start); + const finish = source.indexOf(end, offset); + assert.ok(finish > offset, end); + return source.slice(offset, finish); +} + +function context() { + const calls = []; + const overlays = []; + const mainContent = { + querySelectorAll: () => [], + appendChild: element => { element.isConnected = true; } + }; + const elements = { mainContent }; + const environment = { + window: { _pageCache: {}, _pageDataCache: {}, _pageReloaders: {} }, + document: { + getElementById: id => elements[id] || null, + querySelectorAll: () => [], + querySelector: () => null, + createElement: () => ({ style: {}, innerHTML: '' }), + body: { appendChild: element => overlays.push(element) } + }, + domesticOrderPage: 3, + overseasOrderPage: 2, + packagingOrderPage: 4, + domesticLoadOrders: page => calls.push(['domestic', page]), + overseasLoadOrders: page => calls.push(['overseas', page]), + packagingLoadOrders: page => calls.push(['packaging', page]), + loadBackupList: reset => calls.push(['backup', reset]), + invalidateOrderColumnCache: type => calls.push(['invalidate', type]), + requestAnimationFrame: callback => callback(), + setTimeout: callback => callback(), + }; + vm.createContext(environment); + vm.runInContext(functionSource('function _refreshLinkedOrderLists()', '// 防重复提交锁'), environment); + vm.runInContext(functionSource('function loadPage(page)', '// ============ SPA 导航系统'), environment); + return { environment, calls, overlays, elements }; +} + +// Returning to a previously visited domestic order page queries current records, +// even when the order was confirmed in another user's browser. +{ + const { environment: env, calls } = context(); + const cached = { style: {}, isConnected: false, querySelectorAll: () => [] }; + env.window._pageCache.orders_domestic = cached; + env.loadPage('orders_domestic'); + assert.equal(cached.isConnected, true); + assert.deepEqual(calls, [['invalidate', 'domestic'], ['domestic', 3]]); +} + +// Confirming an order while its destination pages are detached removes their +// old containers/data, while unrelated pages remain intact. +{ + const { environment: env, calls } = context(); + const pages = ['orders_domestic', 'orders_overseas', 'orders_packaging', 'purchase_ready']; + pages.forEach(page => { + env.window._pageCache[page] = { isConnected: false }; + env.window._pageDataCache[page] = { old: true }; + }); + const unrelated = { isConnected: false }; + env.window._pageCache.planning = unrelated; + env._refreshLinkedOrderLists(); + pages.forEach(page => { + assert.equal(env.window._pageCache[page], undefined); + assert.equal(env.window._pageDataCache[page], undefined); + }); + assert.equal(env.window._pageCache.planning, unrelated); + assert.deepEqual(calls, ['domestic', 'overseas', 'packaging', 'backup'].map(type => ['invalidate', type])); +} + +// A currently mounted list stays mounted and refreshes with its current page. +{ + const { environment: env, elements, calls } = context(); + const current = { isConnected: true }; + env.window._pageCache.orders_domestic = current; + elements.domesticOrdersTbody = {}; + env._refreshLinkedOrderLists(); + assert.equal(env.window._pageCache.orders_domestic, current); + assert.deepEqual(calls.at(-1), ['domestic', 3]); +} + +// Finished-product navigation follows the server's route and the backup link +// actually opens the backup menu rather than returning to pending orders. +for (const [route, url] of [['orders_domestic', '/orders/domestic'], ['orders_overseas', '/orders/overseas']]) { + const { environment: env, overlays } = context(); + env.showConfirmRedirect(true, false, route); + const html = overlays[0].innerHTML; + assert.ok(html.includes(`href="${url}"`)); + assert.ok(html.includes('href="/purchase/ready"')); + assert.ok(!html.includes('href="/purchase"')); +} + +{ + const { environment: env, overlays } = context(); + env.showConfirmRedirect(false, true, null); + assert.ok(overlays[0].innerHTML.includes('href="/orders/packaging"')); + assert.ok(!overlays[0].innerHTML.includes('href="/orders/domestic"')); +} + +console.log('Order navigation regression tests passed (6 scenarios).');