renshen b22eb61541 侧边栏:搜索框右侧增加常用网址入口
- 搜索框右侧新增书签按钮,点开显示 config/sidebar_links.php 中配置的网址,新标签页打开
- 未配置时给出去哪里加的提示
- 补充导航测试
2026-10-19 11:13:49 +08:00

428 lines
21 KiB
PHP

@props(['companies'])
<x-loading.menu />
<div class="container flex items-center py-3 mb-4 border-b-2 xl:hidden">
<span class="material-icons text-black js-hamburger-menu">menu</span>
<div class="flex items-center m-auto">
<img src="{{ asset('public/img/akaunting-logo-gold.svg') }}" class="w-8 m-auto" alt="Akaunting" />
<span class="ltr:ml-2 rtl:mr-2">{{ Str::limit(setting('company.name'), 22) }}</span>
</div>
@can('create-banking-transactions')
<x-dropdown id="dropdown-mobile-actions">
<x-slot name="trigger">
<span class="material-icons pointer-events-none">more_horiz</span>
</x-slot>
<x-dropdown.link href="{{ route('transactions.create', ['type' => 'income']) }}">
{{ trans('general.title.new', ['type' => trans_choice('general.incomes', 1)]) }}
</x-dropdown.link>
<x-dropdown.link href="{{ route('transactions.create', ['type' => 'expense']) }}" kind="primary">
{{ trans('general.title.new', ['type' => trans_choice('general.expenses', 1)]) }}
</x-dropdown.link>
</x-dropdown>
@endcan
</div>
@stack('menu_start')
<div
x-data="{ }"
x-init="() => {
const loadEvent = 'onpagehide' in window ? 'pageshow' : 'load';
window.addEventListener(loadEvent, () => {
$refs.realMenu.classList.remove('hidden');
});
}"
x-ref="realMenu"
class="w-70 h-screen flex hidden fixed top-0 js-menu z-20 xl:z-10 transition-all ltr:-left-80 rtl:-right-80 xl:ltr:left-0 xl:rtl:right-0"
>
<div class="finance-theme-rail w-14 py-7 px-1 bg-lilac-900 z-10 menu-scroll overflow-y-auto overflow-x-hidden">
<div
data-tooltip-target="tooltip-profile"
data-tooltip-placement="{{ language()->direction() === 'rtl' ? 'left' : 'right' }}"
class="flex flex-col items-center justify-center mb-5 cursor-pointer menu-button"
data-menu="profile-menu"
>
<span id="menu-profile-icon-cancel" name="account_circle" class="material-icons-outlined w-8 h-8 flex items-center justify-center text-purple text-2xl hidden pointer-events-none">
account_circle
</span>
@if (setting('default.use_gravatar', '0') == '1')
<img src="{{ user()->picture }}" class="w-8 h-8 m-auto rounded-full text-transparent" alt="{{ user()->name }}" title="{{ user()->name }}">
@elseif (is_object(user()->picture))
<img src="{{ route('uploads.get', user()->picture->id) }}" class="w-8 h-8 m-auto rounded-full text-transparent" alt="{{ user()->name }}" title="{{ user()->name }}">
@else
<span id="menu-profile-icon" name="account_circle" class="material-icons-outlined text-purple w-8 h-8 flex items-center justify-center text-center text-2xl pointer-events-none" title="{{ user()->name }}" aria-label="{{ user()->name }}">
account_circle
</span>
@endif
</div>
<div id="tooltip-profile" class="inline-block absolute z-20 py-1 px-2 text-sm font-medium rounded-lg bg-white text-gray-900 w-auto border border-gray-200 shadow-sm whitespace-nowrap opacity-0 invisible">
{{ trans('auth.profile') }}
<div class="absolute w-2 h-2 before:absolute before:w-2 before:h-2 before:bg-white before:border-gray-200 before:transform before:rotate-45 before:border ltr:-left-1 rtl:-right-1 before:border-t-0 ltr:before:border-r-0 rtl:before:border-l-0 border-gray-200" data-popper-arrow></div>
</div>
<div class="group flex flex-col items-center justify-center menu-toggle-buttons">
@can('read-notifications')
<x-tooltip id="tooltip-notifications" placement="{{ language()->direction() === 'rtl' ? 'left' : 'right' }}" message="{{ trans_choice('general.notifications', 2) }}">
<button type="button"
@class([
'flex items-center menu-button justify-center w-8 h-8 mb-2.5 relative cursor-pointer js-menu-toggles outline-none',
'animate-vibrate' => $notification_count,
])
data-menu="notifications-menu"
>
<span id="menu-notification-icon" name="notifications" class="material-icons-outlined text-purple text-2xl pointer-events-none">notifications</span>
@if ($notification_count)
<span data-notification-count class="w-2 h-2 absolute top-2 ltr:right-2 rtl:left-2 inline-flex items-center justify-center p-2.5 text-xs text-white font-bold leading-none transform translate-x-1/2 -translate-y-1/2 bg-orange rounded-full">
{{ $notification_count }}
</span>
@endif
</button>
</x-tooltip>
@endcan
<x-tooltip id="tooltip-search" placement="{{ language()->direction() === 'rtl' ? 'left' : 'right' }}" message="{{ trans('general.search') }}">
<button type="button" class="flex items-center menu-button justify-center w-8 h-8 mb-2.5 relative cursor-pointer outline-none">
<span id="menu-search-icon" name="search" class="material-icons-outlined text-purple text-2xl pointer-events-none">search</span>
</button>
</x-tooltip>
<x-tooltip id="tooltip-new" placement="{{ language()->direction() === 'rtl' ? 'left' : 'right' }}" message="{{ trans('general.new') }}">
<button type="button" class="add-item menu-button flex items-center justify-center w-8 h-8 mb-2.5 cursor-pointer js-menu-toggles" data-menu="add-new-menu">
<span id="menu-neww-icon" name="add_circle_outline" class="material-icons-outlined text-purple text-2xl pointer-events-none">add_circle_outline</span>
</button>
</x-tooltip>
@if (\App\Utilities\SystemAccess::canOpenSettings())
<x-tooltip id="tooltip-settings" placement="{{ language()->direction() === 'rtl' ? 'left' : 'right' }}" message="系统设置">
<button type="button" class="settings-item menu-button flex items-center justify-center w-8 h-8 mb-2.5 cursor-pointer js-menu-toggles" data-menu="settings-menu">
<span id="menu-settings-icon" name="settings" class="material-icons-outlined text-purple text-2xl pointer-events-none">settings</span>
</button>
</x-tooltip>
@endif
@unless (config('app.offline'))
<x-tooltip id="tooltip-support" placement="{{ language()->direction() === 'rtl' ? 'left' : 'right' }}" message="{{ trans('general.help') }}">
<x-link href="{{ url(trans('header.support_link')) }}" target="_blank" class="flex items-center justify-center w-8 h-8 mb-2.5 cursor-pointer js-menu-toggles" override="class">
<span id="menu-support-icon" class="material-icons-outlined text-purple text-2xl pointer-events-none">support</span>
</x-link>
</x-tooltip>
@endunless
</div>
<livewire:menu.favorites />
</div>
@stack('main_menu_start')
<nav class="finance-theme-sidebar menu-list js-main-menu" id="sidenav-main">
@stack('main_menu_company_start')
<div class="relative mb-5">
<a href="{{ route('dashboard', ['company_id' => company_id()]) }}" class="flex items-center" title="返回首页">
<div class="w-8 h-8 flex items-center justify-center">
<img src="{{ asset('public/img/akaunting-logo-gold.svg') }}" class="w-6 h-6" alt="Akaunting" />
</div>
<div class="flex flex-col ltr:ml-2 rtl:mr-2">
<span class="w-28 ltr:text-left rtl:text-right block text-base truncate">
税务
</span>
<span class="text-xs text-gray-400">财务运营管理</span>
</div>
</a>
</div>
@stack('main_menu_admin_start')
@php
$prototypeGroups = \App\Utilities\SidebarMenu::groups(user());
@endphp
<style>
.prototype-tag-filter { margin: 0 0 1rem; }
.prototype-tag-filter input {
width: 100%;
height: 2rem;
padding: 0 0.6rem;
border: 1px solid rgba(240, 228, 191, 0.25);
border-radius: 0.375rem;
background: rgba(240, 228, 191, 0.06);
color: var(--vip-sidebar-gold, #F0E4BF);
font: inherit;
font-size: 0.75rem;
outline: none;
}
.prototype-tag-filter input::placeholder { color: rgba(240, 228, 191, 0.45); }
.prototype-tag-filter input:focus { border-color: var(--vip-sidebar-gold, #F0E4BF); background: rgba(240, 228, 191, 0.12); }
.prototype-tag-filter__row { display: flex; align-items: center; gap: 0.375rem; }
.prototype-tag-filter__row input { flex: 1 1 auto; width: auto; min-width: 0; }
.sidebar-links__toggle {
display: inline-flex;
height: 2rem;
flex: 0 0 auto;
align-items: center;
gap: 0.125rem;
padding: 0 0.4rem;
border: 1px solid rgba(240, 228, 191, 0.25);
border-radius: 0.375rem;
background: rgba(240, 228, 191, 0.06);
color: var(--vip-sidebar-gold, #F0E4BF);
font: inherit;
font-size: 0.6875rem;
cursor: pointer;
}
.sidebar-links__toggle:hover,
.sidebar-links__toggle[aria-expanded="true"] { border-color: rgba(240, 228, 191, 0.45); background: rgba(240, 228, 191, 0.14); }
.sidebar-links__toggle:focus-visible { outline: 2px solid var(--vip-sidebar-gold, #F0E4BF); outline-offset: 2px; }
.sidebar-links__toggle .material-icons-outlined { font-size: 1rem; }
.sidebar-links__panel { margin: 0.375rem 0 0; padding: 0.25rem; border: 1px solid rgba(240, 228, 191, 0.18); border-radius: 0.375rem; background: rgba(5, 5, 6, 0.4); max-height: 15rem; overflow-y: auto; }
.sidebar-links__item { display: flex; align-items: center; gap: 0.5rem; padding: 0.4rem 0.5rem; border-radius: 0.25rem; color: var(--vip-sidebar-gold-soft, #DDD0AA); font-size: 0.75rem; text-decoration: none; }
.sidebar-links__item:hover { background: rgba(240, 228, 191, 0.08); color: var(--vip-sidebar-gold, #F0E4BF); }
.sidebar-links__item .material-icons-outlined { font-size: 0.875rem; }
.sidebar-links__empty { padding: 0.4rem 0.5rem; color: rgba(240, 228, 191, 0.55); font-size: 0.6875rem; line-height: 1.4; }
.prototype-tag-empty { padding: 0.35rem 0.75rem; color: rgba(240, 228, 191, 0.5); font-size: 0.75rem; }
.prototype-tag-navigation { margin: 0 -0.25rem 1.25rem; }
.prototype-tag-group { margin-bottom: 1.15rem; }
.prototype-tag-group-title { padding: 0 0.75rem 0.45rem; color: var(--vip-sidebar-gold-soft, #DDD0AA); font-size: 0.6875rem; line-height: 1rem; }
.prototype-tag-link { position: relative; display: flex; min-height: 2.4rem; align-items: center; gap: 0.65rem; margin: 0.125rem 0; padding: 0.55rem 0.75rem; border-radius: 0.25rem; color: var(--vip-sidebar-gold-soft, #DDD0AA); font-size: 0.75rem; line-height: 1rem; }
.prototype-tag-link:hover { color: var(--vip-sidebar-gold, #F0E4BF); background: rgba(240, 228, 191, 0.08); }
.prototype-tag-link.is-active { color: var(--vip-rail-gold, #F2E7C5); background: var(--vip-sidebar-active, #2A2927); }
.prototype-tag-link:focus-visible { outline: 2px solid var(--vip-sidebar-gold, #F0E4BF); outline-offset: 2px; }
.prototype-tag-link .material-icons-outlined { width: 1.25rem; flex: 0 0 1.25rem; font-size: 1rem; text-align: center; }
.prototype-tag-link > span:nth-child(2) { flex: 1; white-space: nowrap; }
</style>
<div class="prototype-tag-filter">
@php
$sidebarLinks = collect(config('sidebar_links', []))
->filter(fn ($link) => is_array($link) && ! empty($link['url']))
->values();
@endphp
<div class="prototype-tag-filter__row">
<input type="search" id="prototype-menu-filter" placeholder="搜索菜单,如:快递应付" autocomplete="off" aria-label="搜索菜单">
<button type="button" class="sidebar-links__toggle" data-sidebar-links-toggle aria-expanded="false" aria-controls="sidebar-links-panel" title="常用网址">
<span class="material-icons-outlined" aria-hidden="true">bookmarks</span>
@if ($sidebarLinks->isNotEmpty())
<span>{{ $sidebarLinks->count() }}</span>
@endif
</button>
</div>
<div class="sidebar-links__panel" id="sidebar-links-panel" data-sidebar-links hidden>
@forelse ($sidebarLinks as $link)
<a class="sidebar-links__item" href="{{ $link['url'] }}" target="_blank" rel="noopener noreferrer">
<span class="material-icons-outlined" aria-hidden="true">open_in_new</span>
<span>{{ $link['title'] ?? $link['url'] }}</span>
</a>
@empty
<p class="sidebar-links__empty">还没有配置网址,把网址发给我就能加进来。</p>
@endforelse
</div>
<script>
(() => {
const toggle = document.querySelector('[data-sidebar-links-toggle]');
const panel = document.querySelector('[data-sidebar-links]');
if (!toggle || !panel) {
return;
}
toggle.addEventListener('click', () => {
const open = panel.hidden;
panel.hidden = !open;
toggle.setAttribute('aria-expanded', open ? 'true' : 'false');
});
})();
</script>
</div>
<div class="prototype-tag-navigation" aria-label="税务功能导航">
@foreach ($prototypeGroups as $group)
<section class="prototype-tag-group">
<div class="prototype-tag-group-title">{{ $group['title'] }}</div>
@foreach ($group['items'] as $navItem)
<a
href="{{ route('business.pages.show', ['section' => $navItem['section'], 'page' => $navItem['page']]) }}"
@class(['prototype-tag-link', 'is-active' => request()->route('section') === $navItem['section'] && request()->route('page') === $navItem['page']])
@if (request()->route('section') === $navItem['section'] && request()->route('page') === $navItem['page']) aria-current="page" @endif
>
<span class="material-icons-outlined" aria-hidden="true">{{ $navItem['icon'] }}</span>
<span>{{ $navItem['title'] }}</span>
</a>
@endforeach
</section>
@endforeach
<div class="prototype-tag-empty" data-menu-filter-empty hidden>没有匹配的菜单</div>
</div>
<div class="main-menu transform">
{!! menu('admin') !!}
</div>
@stack('main_menu_admin_end')
</nav>
@stack('main_menu_end')
@stack('profile_menu_start')
<div class="finance-theme-sidebar profile-menu user-menu menu-list fixed h-full ltr:-left-80 rtl:-right-80">
@stack('profile_menu_avatar_start')
<div class="flex h-12.5">
@if (setting('default.use_gravatar', '0') == '1')
<img src="{{ user()->picture }}" class="w-8 h-8 rounded-full" alt="{{ user()->name }}" title="{{ user()->name }}">
@elseif (is_object(user()->picture))
<img src="{{ route('uploads.get', user()->picture->id) }}" class="w-8 h-8 rounded-full" alt="{{ user()->name }}" title="{{ user()->name }}">
@else
<span name="account_circle" class="material-icons-outlined w-8 h-8 flex items-center justify-center text-purple text-2xl pointer-events-none" title="{{ user()->name }}" aria-label="{{ user()->name }}">account_circle</span>
@endif
@stack('navbar_profile_welcome')
<div class="flex flex-col text-black ltr:ml-2 rtl:mr-2">
<span class="text-xs">{{ trans('general.welcome') }}</span>
{{ user()->name }}
</div>
</div>
@stack('profile_menu_profile_start')
<livewire:menu.profile />
@stack('profile_menu_profile_end')
</div>
@stack('profile_menu_end')
@can('read-notifications')
@stack('notifications_menu_start')
<div class="finance-theme-sidebar notifications-menu user-menu menu-list fixed h-full ltr:-left-80 rtl:-right-80">
@stack('notifications_menu_title_start')
<div class="flex items-center mb-3">
<span name="notifications" class="material-icons-outlined w-8 h-8 flex items-center justify-center text-purple text-2xl pointer-events-none">notifications</span>
<div class="text-black ltr:ml-1 rtl:mr-1">
{{ trans_choice('general.your_notifications', 2) }}
</div>
</div>
@stack('notifications_menu_notifications_start')
<livewire:menu.notifications />
@stack('notifications_menu_notifications_end')
</div>
@stack('notifications_menu_end')
@endcan
@stack('settings_menu_start')
<div class="finance-theme-sidebar settings-menu user-menu menu-list fixed h-full overflow-y-unset ltr:-left-80 rtl:-right-80">
@stack('settings_menu_title_start')
<div class="flex items-center mb-3">
<span name="settings" class="material-icons-outlined w-8 h-8 flex items-center justify-center text-purple text-2xl pointer-events-none">settings</span>
<div class="text-black ltr:ml-1 rtl:mr-1">
系统设置
</div>
</div>
@stack('settings_menu_settings_start')
<livewire:menu.settings />
@stack('settings_menu_settings_end')
</div>
@stack('settings_menu_end')
@stack('add_new_menu_start')
<div class="finance-theme-sidebar add-new-menu user-menu menu-list fixed h-full ltr:-left-80 rtl:-right-80">
@stack('add_new_menu_title_start')
<div class="flex items-center mb-3">
<span name="add_circle_outline" class="material-icons-outlined w-8 h-8 flex items-center justify-center text-purple text-2xl pointer-events-none">add_circle_outline</span>
<div class="text-black ltr:ml-1 rtl:mr-1">
{{ trans('general.new_more') }}
</div>
</div>
@stack('add_new_menu_add_new_start')
<livewire:menu.neww />
@stack('add_new_menu_add_new_end')
</div>
@stack('add_new_menu_end')
<button type="button" class="finance-theme-toggle toggle-button absolute ltr:-right-2 rtl:-left-2 top-8 cursor-pointer transition-opacity ease-in-out z-50">
<span class="material-icons text-lg text-purple transform ltr:rotate-90 rtl:-rotate-90 pointer-events-none">expand_circle_down</span>
</button>
<span data-menu-close id="menu-cancel" class="finance-theme-toggle material-icons absolute ltr:-right-2 rtl:-left-1.5 transition-all top-8 text-lg text-purple cursor-pointer z-10 hidden">cancel</span>
<div class="fixed w-full h-full invisible lg:hidden js-menu-background" style="background-color: rgba(0, 0, 0, 0.5); z-index: -1;"></div>
</div>
@stack('menu_end')
<script>
// 侧边栏菜单搜索:按关键字过滤菜单项,方便快速找到页面。
(() => {
const input = document.getElementById('prototype-menu-filter');
if (!input) {
return;
}
const nav = document.querySelector('.prototype-tag-navigation');
if (!nav) {
return;
}
const groups = Array.from(nav.querySelectorAll('.prototype-tag-group'));
const empty = nav.querySelector('[data-menu-filter-empty]');
const apply = () => {
const keyword = input.value.trim().toLowerCase();
let total = 0;
groups.forEach((group) => {
let visible = 0;
group.querySelectorAll('.prototype-tag-link').forEach((link) => {
const match = keyword === '' || link.textContent.trim().toLowerCase().includes(keyword);
link.style.display = match ? '' : 'none';
if (match) {
visible += 1;
}
});
group.style.display = visible > 0 ? '' : 'none';
total += visible;
});
if (empty) {
empty.hidden = total > 0;
}
};
input.addEventListener('input', apply);
input.addEventListener('keydown', (event) => {
if (event.key === 'Escape') {
input.value = '';
apply();
}
});
})();
</script>