快递应付:左侧增加快递报价入口并打开四个页面 #309

Merged
limeng merged 1 commits from feature/limeng-courier-quote-sidebar into main 2026-10-12 13:26:23 +08:00
5 changed files with 85 additions and 48 deletions

View File

@ -360,6 +360,13 @@ class Pages extends Controller
abort_if(empty($pageConfig), 404);
if ($section === 'courier-payables' && $page === 'courier-quote') {
abort_if(empty($pageConfig['children']), 403, '当前账号没有可访问的快递报价页面。');
abort_if($tab !== null, 404);
return view('business.pages.show', $this->viewData($sectionConfig, $pageConfig));
}
if (! empty($pageConfig['children']) && empty($pageConfig['tabs'])) {
return $this->redirectToPage($sectionConfig, $pageConfig['children'][0]);
}

View File

@ -119,6 +119,10 @@ class ShowInAdmin
$order = ($sectionIndex + 1) * 10;
if (empty($pages) || in_array($section['slug'], self::WORKSPACE_SECTION_SLUGS, true)) {
$quoteDirectory = $section['slug'] === 'courier-payables'
? collect($pages)->firstWhere('slug', 'courier-quote')
: null;
$quoteSlugs = array_merge(['courier-quote'], array_column($quoteDirectory['children'] ?? [], 'slug'));
$menu->route(
'business.pages.show',
$section['title'],
@ -126,10 +130,26 @@ class ShowInAdmin
$order,
[
'icon' => $section['icon'],
'active' => fn () => request()->route('section') === $section['slug'],
'active' => fn () => request()->route('section') === $section['slug']
&& (! $quoteDirectory || ! in_array(request()->route('page'), $quoteSlugs, true)),
]
);
if (! empty($quoteDirectory['children'])) {
$menu->route(
'business.pages.show',
$quoteDirectory['title'],
['section' => $section['slug'], 'page' => $quoteDirectory['slug']],
$order + 1,
[
'icon' => '',
'class' => 'ltr:ml-8 rtl:mr-8',
'active' => fn () => request()->route('section') === $section['slug']
&& in_array(request()->route('page'), $quoteSlugs, true),
]
);
}
continue;
}

View File

@ -1,30 +1,17 @@
@php
$courierSection = collect(\App\Utilities\SidebarPermissions::navigationFor(user()))->firstWhere('slug', 'courier-payables');
$courierPages = $courierSection['pages'] ?? [];
$isQuoteWorkspace = ($page['slug'] ?? null) === 'courier-quote';
$courierPages = $isQuoteWorkspace
? ($page['children'] ?? [])
: array_values(array_filter($courierSection['pages'] ?? [], static fn (array $item) => $item['slug'] !== 'courier-quote'));
@endphp
<div data-courier-final>
<nav class="business-tab-bar courier-final-tabs" aria-label="快递应付子功能">
<div data-courier-final @if ($isQuoteWorkspace) data-courier-quote-workspace @endif>
<nav class="business-tab-bar courier-final-tabs" aria-label="{{ $isQuoteWorkspace ? $page['title'] : '快递应付子功能' }}">
@foreach ($courierPages as $item)
@if (!empty($item['children']))
<button type="button" class="business-tab-link" data-courier-group="{{ $item['slug'] }}"
aria-expanded="false" aria-controls="courier-group-{{ $item['slug'] }}">{{ $item['title'] }}</button>
@else
<a class="business-tab-link" data-courier-tab="{{ $item['slug'] }}"
href="{{ route('business.pages.show', ['section' => 'courier-payables', 'page' => $item['slug']]) }}">{{ $item['title'] }}</a>
@endif
<a class="business-tab-link" data-courier-tab="{{ $item['slug'] }}"
href="{{ route('business.pages.show', ['section' => 'courier-payables', 'page' => $item['slug']]) }}">{{ $item['title'] }}</a>
@endforeach
</nav>
@foreach ($courierPages as $item)
@if (!empty($item['children']))
<nav class="business-tab-bar courier-final-tabs courier-final-subtabs" id="courier-group-{{ $item['slug'] }}"
aria-label="{{ $item['title'] }}子功能" data-courier-subtabs="{{ $item['slug'] }}" hidden>
@foreach ($item['children'] as $child)
<a class="business-tab-link" data-courier-tab="{{ $child['slug'] }}" data-courier-parent="{{ $item['slug'] }}"
href="{{ route('business.pages.show', ['section' => 'courier-payables', 'page' => $child['slug']]) }}">{{ $child['title'] }}</a>
@endforeach
</nav>
@endif
@endforeach
<div role="status" aria-live="polite" data-courier-status></div>
<button type="button" class="business-tab-link" data-courier-retry hidden>重新加载</button>
@if (empty($courierPages))
@ -35,10 +22,7 @@
<style>
.courier-final-tabs { flex-wrap:wrap; gap:4px 16px; min-height:34px; padding:0 16px; margin-bottom:12px; }
.courier-final-tabs .business-tab-link { min-height:34px; font-size:12px; color:var(--vip-content-muted, #595959); }
.courier-final-tabs button.business-tab-link { border:0; background:transparent; padding:0; font-family:inherit; cursor:pointer; }
.courier-final-tabs .business-tab-link:focus-visible { outline:2px solid var(--vip-content-accent-dark, #856632); outline-offset:3px; }
.courier-final-subtabs[hidden] { display:none; }
.courier-final-subtabs { border:0; border-bottom:1px solid var(--vip-border, #D8D2C7); border-radius:0; box-shadow:none; }
.courier-final-tabs .is-active { color:var(--vip-content-accent-dark, #856632); }
@media (max-width:640px) { .courier-final-tabs { flex-wrap:nowrap; } }
[data-courier-frames] iframe { width:100%; height:calc(100vh - 260px); min-height:400px; border:0; }
@ -51,9 +35,6 @@
const root = document.querySelector('[data-courier-final]');
if (!root) return;
const links = [...root.querySelectorAll('[data-courier-tab]')];
const groups = [...root.querySelectorAll('[data-courier-group]')];
const submenus = [...root.querySelectorAll('[data-courier-subtabs]')];
const groupSelections = new Map();
const frames = new Map();
const status = root.querySelector('[data-courier-status]');
const retry = root.querySelector('[data-courier-retry]');
@ -62,14 +43,6 @@
if (!link) return;
selected = link;
retry.hidden = true;
const parent = link.dataset.courierParent;
if (parent) groupSelections.set(parent, link);
groups.forEach(group => {
const active = group.dataset.courierGroup === parent;
group.classList.toggle('is-active', active);
group.setAttribute('aria-expanded', String(active));
});
submenus.forEach(menu => menu.hidden = menu.dataset.courierSubtabs !== parent);
links.forEach(item => {
item.classList.toggle('is-active', item === link);
if (item === link) item.setAttribute('aria-current', 'page');
@ -125,15 +98,10 @@
if (event.button !== 0 || event.ctrlKey || event.metaKey || event.shiftKey || event.altKey) return;
event.preventDefault(); select(link);
}));
const groupTarget = group => group && (groupSelections.get(group.dataset.courierGroup)
|| links.find(link => link.dataset.courierParent === group.dataset.courierGroup));
groups.forEach(group => group.addEventListener('click', () => select(groupTarget(group))));
retry.addEventListener('click', () => select(selected, false, true));
const restore = () => {
const view = new URL(location.href).searchParams.get('view');
select(links.find(link => link.dataset.courierTab === view)
|| groupTarget(groups.find(group => group.dataset.courierGroup === view))
|| groupTarget(groups[0]) || links[0], false);
select(links.find(link => link.dataset.courierTab === view) || links[0], false);
};
window.addEventListener('popstate', restore);
restore();

View File

@ -647,7 +647,7 @@
@if (!empty($payablesFinal))
@include('business.pages.partials.final-dataset-table', ['payablesFinal' => $payablesFinal])
@elseif ($section['slug'] === 'courier-payables' && empty($page))
@elseif ($section['slug'] === 'courier-payables' && (empty($page) || $page['slug'] === 'courier-quote'))
@include('business.pages.courier-payables.final-workspace')
@elseif ($sectionView)
@include($sectionView)

View File

@ -201,6 +201,43 @@ class QuoteManagementPageTest extends FeatureTestCase
->assertSee('data-reverse-rate-body', false);
}
public function testCourierQuoteSidebarEntryOpensFourWorkspacePagesAndKeepsOtherPayablePages(): void
{
$subject = 'user_' . $this->user->id;
$this->user->roles()->sync([
\App\Models\Auth\Role::where('name', 'admin')->firstOrFail()->id,
]);
setting()->set('system.sidebar_permissions', [
'configured' => [$subject],
'subjects' => [$subject => ['courier-payables']],
]);
setting()->save();
$this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail());
$response = $this->loginAs()->get(route('business.pages.show', ['section' => 'courier-payables']))
->assertOk()
->assertSeeText('快递报价')
->assertSee(route('business.pages.show', ['section' => 'courier-payables', 'page' => 'courier-quote']), false)
->assertSee('data-courier-tab="freight-reconciliation"', false)
->assertSee('data-courier-tab="shipping-data"', false)
->assertDontSee('data-courier-group="courier-quote"', false);
foreach (['quote-management', 'courier-information', 'agreement-signing', 'courier-surcharge'] as $page) {
$response->assertDontSee('data-courier-tab="' . $page . '"', false);
}
$response = $this->get(route('business.pages.show', ['section' => 'courier-payables', 'page' => 'courier-quote']))
->assertOk()
->assertSee('data-courier-quote-workspace', false)
->assertDontSee('data-courier-tab="freight-reconciliation"', false)
->assertDontSee('data-courier-tab="shipping-data"', false);
foreach (['quote-management', 'courier-information', 'agreement-signing', 'courier-surcharge'] as $page) {
$response->assertSee(route('business.pages.show', ['section' => 'courier-payables', 'page' => $page]), false)
->assertSee('data-courier-tab="' . $page . '"', false);
}
}
public function testCourierQuoteDirectoryKeepsPageLabelsAndExactPermissions(): void
{
setting()->set('navigation.titles', [
@ -228,13 +265,18 @@ class QuoteManagementPageTest extends FeatureTestCase
$this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail());
$this->loginAs()
->get(route('business.pages.show', ['section' => 'courier-payables']))
->get(route('business.pages.show', ['section' => 'courier-payables', 'page' => 'courier-quote']))
->assertOk()
->assertSee('data-courier-group="courier-quote"', false)
->assertSeeText('快递报价')
->assertSeeText('快递报价单')
->assertSee('data-courier-tab="courier-surcharge"', false)
->assertDontSee('data-courier-tab="courier-information"', false)
->assertDontSee('data-courier-tab="agreement-signing"', false);
->assertSee(route('business.pages.show', ['section' => 'courier-payables', 'page' => 'quote-management']), false)
->assertSee(route('business.pages.show', ['section' => 'courier-payables', 'page' => 'courier-surcharge']), false)
->assertDontSee(route('business.pages.show', ['section' => 'courier-payables', 'page' => 'courier-information']), false)
->assertDontSee(route('business.pages.show', ['section' => 'courier-payables', 'page' => 'agreement-signing']), false)
->assertSee('data-courier-quote-workspace', false)
->assertDontSee('data-courier-group="courier-quote"', false)
->assertSee('data-courier-tab="quote-management"', false)
->assertSee('data-courier-tab="courier-surcharge"', false);
try {
$this->get(route('business.pages.show', ['section' => 'courier-payables', 'page' => 'courier-information']));