feat(快递应付): 四个报价相关模块归入快递报价子目录 #307
@ -49,7 +49,7 @@ class Navigation extends Controller
|
||||
$titles['sections'][$sectionSlug] = $sectionTitle;
|
||||
}
|
||||
|
||||
foreach ($section['pages'] ?? [] as $page) {
|
||||
foreach (collect($section['pages'] ?? [])->flatMap(fn (array $page) => [$page, ...($page['children'] ?? [])]) as $page) {
|
||||
$pageTitle = trim((string) ($pageTitles[$sectionSlug][$page['slug']] ?? ''));
|
||||
|
||||
if ($pageTitle !== '') {
|
||||
|
||||
@ -23,6 +23,13 @@ class BusinessNavigation
|
||||
$pageTitles[$sectionSlug][$page['slug']] ?? null,
|
||||
$page['title']
|
||||
);
|
||||
|
||||
foreach ($page['children'] ?? [] as $childIndex => $child) {
|
||||
$section['pages'][$pageIndex]['children'][$childIndex]['title'] = self::title(
|
||||
$pageTitles[$sectionSlug][$child['slug']] ?? null,
|
||||
$child['title']
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
unset($section);
|
||||
|
||||
@ -701,9 +701,23 @@ $navigation = [
|
||||
'title' => '快递应付',
|
||||
'icon' => 'local_shipping',
|
||||
'pages' => [
|
||||
['slug' => 'quote-management', 'title' => '报价管理'],
|
||||
['slug' => 'courier-information', 'title' => '快递资料'],
|
||||
['slug' => 'agreement-signing', 'title' => '协议签署'],
|
||||
[
|
||||
'slug' => 'courier-quote',
|
||||
'title' => '快递报价',
|
||||
'children' => [
|
||||
['slug' => 'quote-management', 'title' => '报价管理'],
|
||||
['slug' => 'courier-information', 'title' => '快递资料'],
|
||||
['slug' => 'agreement-signing', 'title' => '协议签署'],
|
||||
[
|
||||
'slug' => 'courier-surcharge',
|
||||
'title' => '附加费报价维护',
|
||||
'tabs' => [
|
||||
['slug' => 'quotes', 'title' => '附加费报价'],
|
||||
['slug' => 'settlement', 'title' => '附加费结算'],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
['slug' => 'freight-reconciliation', 'title' => '运费对账'],
|
||||
['slug' => 'shipping-data', 'title' => '发货数据'],
|
||||
['slug' => 'return-pickup-management', 'title' => '退货取件管理'],
|
||||
@ -712,14 +726,6 @@ $navigation = [
|
||||
['slug' => 'compensation-details', 'title' => '赔偿明细'],
|
||||
['slug' => 'advance-details', 'title' => '借支明细'],
|
||||
['slug' => 'other-adjustments', 'title' => '其他扣款补款'],
|
||||
[
|
||||
'slug' => 'courier-surcharge',
|
||||
'title' => '附加费报价维护',
|
||||
'tabs' => [
|
||||
['slug' => 'quotes', 'title' => '附加费报价'],
|
||||
['slug' => 'settlement', 'title' => '附加费结算'],
|
||||
],
|
||||
],
|
||||
['slug' => 'payable-cloud-warehouse-vehicle-fee', 'title' => '应付云仓车费'],
|
||||
['slug' => 'courier-payables-report', 'title' => '快递应付表'],
|
||||
['slug' => 'freight-difference', 'title' => '运费差'],
|
||||
|
||||
@ -5,10 +5,26 @@
|
||||
<div data-courier-final>
|
||||
<nav class="business-tab-bar courier-final-tabs" aria-label="快递应付子功能">
|
||||
@foreach ($courierPages as $item)
|
||||
<a class="business-tab-link" data-courier-tab="{{ $item['slug'] }}"
|
||||
href="{{ route('business.pages.show', ['section' => 'courier-payables', 'page' => $item['slug']]) }}">{{ $item['title'] }}</a>
|
||||
@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
|
||||
@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))
|
||||
@ -19,7 +35,12 @@
|
||||
<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; }
|
||||
[data-courier-frames] iframe[hidden] { display:none; }
|
||||
[data-courier-retry][hidden] { display:none!important; }
|
||||
@ -30,6 +51,9 @@
|
||||
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]');
|
||||
@ -38,6 +62,14 @@
|
||||
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');
|
||||
@ -93,8 +125,16 @@
|
||||
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 = () => select(links.find(link => link.dataset.courierTab === new URL(location.href).searchParams.get('view')) || links[0], false);
|
||||
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);
|
||||
};
|
||||
window.addEventListener('popstate', restore);
|
||||
restore();
|
||||
})();
|
||||
|
||||
@ -27,10 +27,10 @@
|
||||
|
||||
@if (! empty($section['pages']))
|
||||
<div class="sm:col-span-3 grid grid-cols-1 gap-4">
|
||||
@foreach ($section['pages'] as $page)
|
||||
@foreach (collect($section['pages'])->flatMap(fn ($page) => [$page, ...($page['children'] ?? [])]) as $page)
|
||||
@php $pageInput = "page_titles[{$section['slug']}][{$page['slug']}]"; @endphp
|
||||
<div>
|
||||
<x-form.label for="{{ $pageInput }}">二级功能</x-form.label>
|
||||
<x-form.label for="{{ $pageInput }}">功能名称</x-form.label>
|
||||
<input
|
||||
type="text"
|
||||
id="{{ $pageInput }}"
|
||||
|
||||
@ -5,6 +5,7 @@ namespace Tests\Feature\Business;
|
||||
use App\Models\Auth\Permission;
|
||||
use App\Support\QuoteManagementSnapshot;
|
||||
use App\Support\ReverseLogisticsQuoteSnapshot;
|
||||
use App\Utilities\BusinessNavigation;
|
||||
use Illuminate\Http\Exceptions\HttpResponseException;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
use Tests\Feature\FeatureTestCase;
|
||||
@ -164,7 +165,13 @@ class QuoteManagementPageTest extends FeatureTestCase
|
||||
->firstWhere('slug', 'courier-payables');
|
||||
|
||||
$this->assertIsArray($courierPayables);
|
||||
$this->assertContains('quote-management', array_column($courierPayables['pages'], 'slug'));
|
||||
$quoteDirectory = collect($courierPayables['pages'])->firstWhere('slug', 'courier-quote');
|
||||
$this->assertSame([
|
||||
'quote-management',
|
||||
'courier-information',
|
||||
'agreement-signing',
|
||||
'courier-surcharge',
|
||||
], array_column($quoteDirectory['children'], 'slug'));
|
||||
$this->assertNotContains('reverse-logistics-quotes', array_column($courierPayables['pages'], 'slug'));
|
||||
|
||||
$this->user->attachPermission(
|
||||
@ -194,6 +201,52 @@ class QuoteManagementPageTest extends FeatureTestCase
|
||||
->assertSee('data-reverse-rate-body', false);
|
||||
}
|
||||
|
||||
public function testCourierQuoteDirectoryKeepsPageLabelsAndExactPermissions(): void
|
||||
{
|
||||
setting()->set('navigation.titles', [
|
||||
'pages' => ['courier-payables' => ['quote-management' => '快递报价单']],
|
||||
]);
|
||||
setting()->save();
|
||||
|
||||
$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/quote-management',
|
||||
'courier-payables/courier-surcharge',
|
||||
]],
|
||||
]);
|
||||
setting()->save();
|
||||
|
||||
$navigation = BusinessNavigation::all();
|
||||
$section = collect($navigation)->firstWhere('slug', 'courier-payables');
|
||||
$directory = collect($section['pages'])->firstWhere('slug', 'courier-quote');
|
||||
$this->assertSame('快递报价单', collect($directory['children'])->firstWhere('slug', 'quote-management')['title']);
|
||||
|
||||
$this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail());
|
||||
$this->loginAs()
|
||||
->get(route('business.pages.show', ['section' => 'courier-payables']))
|
||||
->assertOk()
|
||||
->assertSee('data-courier-group="courier-quote"', false)
|
||||
->assertSeeText('快递报价单')
|
||||
->assertSee('data-courier-tab="courier-surcharge"', false)
|
||||
->assertDontSee('data-courier-tab="courier-information"', false)
|
||||
->assertDontSee('data-courier-tab="agreement-signing"', false);
|
||||
|
||||
try {
|
||||
$this->get(route('business.pages.show', ['section' => 'courier-payables', 'page' => 'courier-information']));
|
||||
$this->fail('未授权的快递资料页面不应访问。');
|
||||
} catch (HttpException|HttpResponseException $exception) {
|
||||
$status = method_exists($exception, 'getStatusCode')
|
||||
? $exception->getStatusCode()
|
||||
: $exception->getResponse()->getStatusCode();
|
||||
$this->assertSame(404, $status);
|
||||
}
|
||||
}
|
||||
|
||||
public function testQuoteManagementPageKeepsTheShellWhenThePrivateSnapshotIsMissing(): void
|
||||
{
|
||||
app()->instance(
|
||||
|
||||
@ -245,7 +245,9 @@ class NavigationTest extends FeatureTestCase
|
||||
$this->loginAs()
|
||||
->get(route('settings.navigation.manage'))
|
||||
->assertOk()
|
||||
->assertSeeText('侧边栏功能名称');
|
||||
->assertSeeText('侧边栏功能名称')
|
||||
->assertSee('name="page_titles[courier-payables][quote-management]"', false)
|
||||
->assertSee('name="page_titles[courier-payables][courier-surcharge]"', false);
|
||||
|
||||
$this->patch(route('settings.navigation.save'), [
|
||||
'section_titles' => [
|
||||
@ -255,6 +257,10 @@ class NavigationTest extends FeatureTestCase
|
||||
'company-management' => [
|
||||
'subject-operating-report' => '主体经营',
|
||||
],
|
||||
'courier-payables' => [
|
||||
'quote-management' => '快递报价单',
|
||||
'unknown-page' => '不应保存',
|
||||
],
|
||||
],
|
||||
])->assertOk();
|
||||
|
||||
@ -263,6 +269,10 @@ class NavigationTest extends FeatureTestCase
|
||||
|
||||
$this->assertSame('企业档案', $navigation[0]['title']);
|
||||
$this->assertSame('主体经营', $companyRegistration['title']);
|
||||
$courier = collect($navigation)->firstWhere('slug', 'courier-payables');
|
||||
$quotes = collect($courier['pages'])->firstWhere('slug', 'courier-quote');
|
||||
$this->assertSame('快递报价单', collect($quotes['children'])->firstWhere('slug', 'quote-management')['title']);
|
||||
$this->assertArrayNotHasKey('unknown-page', setting('navigation.titles')['pages']['courier-payables']);
|
||||
}
|
||||
|
||||
public function testOrderDataAndRefundAreConfiguredBetweenInvoiceAndReceivablesMenus()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user