供应商应付子目录移入页面横向导航 #167

Merged
renshen merged 3 commits from feature/renshen-supplier-page-navigation into main 2026-09-12 13:51:30 +08:00
4 changed files with 121 additions and 1 deletions

View File

@ -108,7 +108,7 @@ class ShowInAdmin
: $this->sortByTitleLength($section['pages'] ?? []);
$order = ($sectionIndex + 1) * 10;
if (empty($pages)) {
if (empty($pages) || $section['slug'] === 'supplier-payables') {
$menu->route(
'business.pages.show',
$section['title'],

View File

@ -546,6 +546,7 @@
'business-page--marketing-account-balance' => $isMarketingAccountBalancePage,
])
@if ($isMarketingAccountBillsPage) data-marketing-account-bills-workspace @endif
@if ($section['slug'] === 'supplier-payables') data-supplier-workspace @endif
>
@if ($isMarketingAccountBillsPage)
<p class="sr-only" role="status" aria-live="polite" data-marketing-account-bills-status></p>
@ -570,6 +571,10 @@
<div class="biz-alert biz-alert-error" role="alert">{{ $upstreamError }}</div>
@endif
@if ($section['slug'] === 'supplier-payables')
@include('business.pages.supplier-payables.navigation')
@endif
@if ($tabs->isNotEmpty())
<nav class="business-tab-bar" aria-label="{{ $page['title'] }}">
@foreach ($tabs as $tab)

View File

@ -0,0 +1,78 @@
@push('body_end')
<script>
(() => {
const selector = '[data-supplier-workspace]';
const initial = document.querySelector(selector);
if (!initial) return;
const cache = new Map();
const loaded = new Set([...document.scripts].map(script => script.src));
let current = location.href;
let pending = false;
const heading = document.querySelector('#main-body h1');
const remember = () => cache.set(current, {
node: document.querySelector(selector),
title: document.title,
heading: heading ? heading.innerHTML : '',
});
remember();
async function navigate(href, push = true) {
if (pending) return;
const previous = document.querySelector(selector);
pending = true;
previous.setAttribute('aria-busy', 'true');
const status = previous.querySelector('[data-supplier-navigation-status]');
status.textContent = '正在加载…';
try {
let next = cache.get(href);
let scripts = [];
if (!next) {
const response = await fetch(href, {credentials: 'same-origin', headers: {Accept: 'text/html'}});
if (!response.ok) throw new Error('HTTP ' + response.status);
const doc = new DOMParser().parseFromString(await response.text(), 'text/html');
const node = doc.querySelector(selector);
if (!node) throw new Error('页面不可用或登录已过期');
scripts = [...doc.querySelectorAll('script[src]')].map(s => s.src)
.filter(src => new URL(src).origin === location.origin && new URL(src).pathname.startsWith('/public/js/ecommerce-finance/') && !loaded.has(src));
next = {node: document.importNode(node, true), title: doc.title, heading: doc.querySelector('#main-body h1')?.innerHTML || ''};
}
remember();
previous.replaceWith(next.node);
// Keep each mounted Vue page alive while switching, preserving its filters and pagination.
for (const src of scripts) {
await new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = src;
script.onload = () => { loaded.add(src); resolve(); };
script.onerror = () => { script.remove(); reject(new Error('页面脚本加载失败')); };
document.body.appendChild(script);
});
}
cache.set(href, next);
current = href;
document.title = next.title;
if (heading) heading.innerHTML = next.heading;
next.node.querySelector('[data-supplier-navigation-status]').textContent = '';
if (push && location.href !== href) history.pushState({}, '', href);
next.node.querySelector('.supplier-page-navigation [aria-current]')?.focus({preventScroll: true});
} catch (error) {
const visible = document.querySelector(selector);
if (visible !== previous) visible.replaceWith(previous);
status.textContent = '加载失败,原页面已保留。请重新点击目录重试。';
} finally {
previous.removeAttribute('aria-busy');
pending = false;
}
}
document.addEventListener('click', event => {
const link = event.target.closest('.supplier-page-navigation a');
if (!link || event.button !== 0 || event.ctrlKey || event.metaKey || event.shiftKey || event.altKey) return;
event.preventDefault();
navigate(link.href);
});
window.addEventListener('popstate', () => {
if (location.pathname.includes('/workspace/supplier-payables/')) navigate(location.href, false);
});
})();
</script>
@endpush

View File

@ -0,0 +1,37 @@
@php
$supplierPages = $section['pages'] ?? [];
$supplierPages = array_merge(
array_filter($supplierPages, fn ($item) => $item['slug'] === 'supplier-payables-report'),
array_filter($supplierPages, fn ($item) => $item['slug'] !== 'supplier-payables-report'),
);
$supplierCurrent = $page['slug'] ?? null;
@endphp
<p role="status" aria-live="polite" data-supplier-navigation-status></p>
<nav class="business-tab-bar supplier-page-navigation" aria-label="供应商应付页面">
@foreach ($supplierPages as $supplierPage)
@php
$children = $supplierPage['children'] ?? [];
$selected = $supplierCurrent === $supplierPage['slug'] || in_array($supplierCurrent, array_column($children, 'slug'), true);
@endphp
<a href="{{ route('business.pages.show', ['section' => 'supplier-payables', 'page' => $children[0]['slug'] ?? $supplierPage['slug']]) }}"
@class(['business-tab-link', 'is-active' => $selected])
@if ($selected) aria-current="page" @endif>{{ $supplierPage['title'] }}</a>
@endforeach
</nav>
@foreach ($supplierPages as $supplierPage)
@if (in_array($supplierCurrent, array_column($supplierPage['children'] ?? [], 'slug'), true))
<nav class="business-tab-bar supplier-page-navigation" aria-label="{{ $supplierPage['title'] }}">
@foreach ($supplierPage['children'] as $child)
<a href="{{ route('business.pages.show', ['section' => 'supplier-payables', 'page' => $child['slug']]) }}"
@class(['business-tab-link', 'is-active' => $supplierCurrent === $child['slug']])
@if ($supplierCurrent === $child['slug']) aria-current="page" @endif>{{ $child['title'] }}</a>
@endforeach
</nav>
@endif
@endforeach
<style>
.supplier-page-navigation { display:flex; flex-wrap:wrap; gap:4px 16px; margin-bottom:16px; }
.supplier-page-navigation .business-tab-link { flex:0 0 auto; padding:10px 0; font-size:0.75rem; color:var(--vip-content-muted, #595959); }
.supplier-page-navigation .business-tab-link.is-active { color:var(--vip-content-accent-dark, #856632); }
</style>
@include('business.pages.supplier-payables.local-navigation')