You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
271 lines
17 KiB
271 lines
17 KiB
<!doctype html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>{% block title %}HW{% endblock %}</title>
|
|
<script>
|
|
(function () {
|
|
const storedTheme = window.localStorage.getItem('hw-theme');
|
|
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
const isDark = storedTheme ? storedTheme === 'dark' : prefersDark;
|
|
document.documentElement.classList.toggle('dark', isDark);
|
|
document.documentElement.style.colorScheme = isDark ? 'dark' : 'light';
|
|
}());
|
|
</script>
|
|
<script>
|
|
(function () {
|
|
const storedSidebar = window.localStorage.getItem('hw-sidebar-collapsed');
|
|
const isDesktop = window.matchMedia('(min-width: 1024px)').matches;
|
|
const isCollapsed = isDesktop ? storedSidebar === 'true' : true;
|
|
document.documentElement.classList.toggle('sidebar-collapsed', isCollapsed);
|
|
}());
|
|
</script>
|
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/main.css') }}">
|
|
</head>
|
|
<body class="min-h-screen bg-neutral-50 font-sans text-sm text-neutral-800 antialiased dark:bg-neutral-900 dark:text-neutral-100">
|
|
{% set flash_classes = {
|
|
'success': 'border-green-400 bg-green-50 text-green-800 dark:bg-green-950 dark:text-green-200',
|
|
'warning': 'border-accent-400 bg-accent-50 text-accent-700 dark:bg-accent-950/50 dark:text-accent-200',
|
|
'error': 'border-brand-400 bg-brand-50 text-brand-800 dark:bg-brand-950/50 dark:text-brand-200',
|
|
'info': 'border-blue-400 bg-blue-50 text-blue-800 dark:bg-blue-950/50 dark:text-blue-200'
|
|
} %}
|
|
|
|
{% if current_account %}
|
|
<div id="app-shell" class="min-h-screen bg-neutral-50 dark:bg-neutral-900 lg:flex">
|
|
<div class="sidebar-backdrop fixed inset-0 z-40 bg-neutral-950/40 lg:hidden" data-sidebar-overlay></div>
|
|
<aside id="app-sidebar" class="sidebar-shell fixed inset-y-0 left-0 z-50 flex min-h-screen flex-col overflow-x-hidden border-r border-neutral-200 bg-white dark:border-neutral-800 dark:bg-neutral-950 lg:sticky lg:top-0">
|
|
<div class="sidebar-brand-bar flex items-center justify-between gap-3 border-b border-neutral-200 px-3 dark:border-neutral-800">
|
|
<div class="flex min-w-0 flex-1 items-center gap-3">
|
|
<a href="{{ url_for('main.index') if current_account.role in ['admin', 'editor'] else url_for('quick_entry.index') }}" class="inline-flex h-10 w-10 shrink-0 items-center justify-center rounded-2xl border border-brand-200 bg-brand-50 text-sm font-bold tracking-[0.22em] text-brand-600 dark:border-brand-500/30 dark:bg-brand-500/10 dark:text-brand-300">HW</a>
|
|
</div>
|
|
<button type="button" class="btn-ghost btn-sm shrink-0" data-sidebar-toggle aria-controls="app-sidebar" aria-expanded="true" aria-label="切换侧边栏">
|
|
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
|
|
<nav class="flex flex-col gap-1 px-2 py-2 lg:flex-1 lg:overflow-y-auto lg:px-2.5 lg:py-3">
|
|
{% if current_account.role in ['admin', 'editor'] %}
|
|
<a
|
|
href="{{ url_for('main.index') }}"
|
|
class="sidebar-nav-link flex items-center gap-2 rounded-lg px-3 py-2 text-sm font-medium transition {% if request.endpoint and request.endpoint.startswith('main.') %}bg-brand-50 text-brand-700 dark:bg-brand-950/60 dark:text-brand-300{% else %}text-neutral-600 hover:bg-neutral-100 hover:text-neutral-800 dark:text-neutral-400 dark:hover:bg-neutral-800 dark:hover:text-neutral-200{% endif %}"
|
|
><span class="sidebar-nav-icon" aria-hidden="true">⌂</span><span class="sidebar-label">管理首页</span></a>
|
|
{% endif %}
|
|
|
|
<a
|
|
href="{{ url_for('quick_entry.index') }}"
|
|
class="sidebar-nav-link flex items-center gap-2 rounded-lg px-3 py-2 text-sm font-medium transition {% if request.endpoint and request.endpoint.startswith('quick_entry.') %}bg-brand-50 text-brand-700 dark:bg-brand-950/60 dark:text-brand-300{% else %}text-neutral-600 hover:bg-neutral-100 hover:text-neutral-800 dark:text-neutral-400 dark:hover:bg-neutral-800 dark:hover:text-neutral-200{% endif %}"
|
|
><span class="sidebar-nav-icon" aria-hidden="true">💰</span><span class="sidebar-label">快速录入</span></a>
|
|
|
|
{% if current_account.role == 'admin' %}
|
|
{% set is_accounts_page = request.endpoint and request.endpoint in ['admin.accounts', 'admin.create_account', 'admin.edit_account', 'admin.reset_account_password'] %}
|
|
<a
|
|
href="{{ url_for('admin.accounts') }}"
|
|
class="sidebar-nav-link flex items-center gap-2 rounded-lg px-3 py-2 text-sm font-medium transition {% if is_accounts_page %}bg-brand-50 text-brand-700 dark:bg-brand-950/60 dark:text-brand-300{% else %}text-neutral-600 hover:bg-neutral-100 hover:text-neutral-800 dark:text-neutral-400 dark:hover:bg-neutral-800 dark:hover:text-neutral-200{% endif %}"
|
|
><span class="sidebar-nav-icon" aria-hidden="true">⚙</span><span class="sidebar-label">账号管理</span></a>
|
|
<a
|
|
href="{{ url_for('admin.audit_logs') }}"
|
|
class="sidebar-nav-link flex items-center gap-2 rounded-lg px-3 py-2 text-sm font-medium transition {% if request.endpoint and request.endpoint.startswith('admin.audit_log') %}bg-brand-50 text-brand-700 dark:bg-brand-950/60 dark:text-brand-300{% else %}text-neutral-600 hover:bg-neutral-100 hover:text-neutral-800 dark:text-neutral-400 dark:hover:bg-neutral-800 dark:hover:text-neutral-200{% endif %}"
|
|
><span class="sidebar-nav-icon" aria-hidden="true">☰</span><span class="sidebar-label">审计日志</span></a>
|
|
<a
|
|
href="{{ url_for('admin.share_links') }}"
|
|
class="sidebar-nav-link flex items-center gap-2 rounded-lg px-3 py-2 text-sm font-medium transition {% if request.endpoint and request.endpoint.startswith('admin.share_link') %}bg-brand-50 text-brand-700 dark:bg-brand-950/60 dark:text-brand-300{% else %}text-neutral-600 hover:bg-neutral-100 hover:text-neutral-800 dark:text-neutral-400 dark:hover:bg-neutral-800 dark:hover:text-neutral-200{% endif %}"
|
|
><span class="sidebar-nav-icon" aria-hidden="true">⌁</span><span class="sidebar-label">分享链接</span></a>
|
|
{% endif %}
|
|
</nav>
|
|
|
|
</aside>
|
|
|
|
<div class="flex-1">
|
|
<header class="workspace-header">
|
|
<div class="flex items-center justify-between gap-3 px-2 py-2 sm:px-3 lg:px-4">
|
|
<div class="flex min-w-0 items-center gap-2">
|
|
<button type="button" class="btn-ghost btn-sm shrink-0 lg:hidden" data-sidebar-toggle aria-controls="app-sidebar" aria-expanded="false" aria-label="打开导航菜单">
|
|
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
<div class="relative" data-user-menu>
|
|
<button
|
|
type="button"
|
|
id="user-menu-toggle"
|
|
class="workspace-user-avatar h-10 w-10 border border-neutral-200 bg-white text-sm text-neutral-700 shadow-sm transition hover:border-neutral-300 hover:bg-neutral-50 focus:outline-none focus:ring-2 focus:ring-neutral-300 dark:border-neutral-700 dark:bg-neutral-900 dark:text-neutral-100 dark:hover:border-neutral-600 dark:hover:bg-neutral-800 dark:focus:ring-neutral-600"
|
|
aria-haspopup="menu"
|
|
aria-expanded="false"
|
|
aria-controls="user-menu-panel"
|
|
aria-label="打开账户菜单"
|
|
>
|
|
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M15 19a4 4 0 00-8 0"></path>
|
|
<circle cx="11" cy="9" r="3.2" stroke-width="1.8"></circle>
|
|
</svg>
|
|
</button>
|
|
<div
|
|
id="user-menu-panel"
|
|
class="absolute right-0 top-full z-40 mt-2 hidden w-56 rounded-2xl border border-neutral-200 bg-white p-2 shadow-lg shadow-neutral-950/10 dark:border-neutral-700 dark:bg-neutral-900"
|
|
role="menu"
|
|
aria-labelledby="user-menu-toggle"
|
|
>
|
|
<div class="rounded-xl border border-neutral-200 bg-neutral-50 px-3 py-2 dark:border-neutral-700 dark:bg-neutral-800/80">
|
|
<p class="truncate text-sm font-medium text-neutral-800 dark:text-neutral-100">{{ current_account.display_name or current_account.username }}</p>
|
|
<p class="truncate text-xs text-neutral-500 dark:text-neutral-400">{{ current_account.username }} · {{ '快速录入' if current_account.role == 'quick_editor' else current_account.role }}</p>
|
|
</div>
|
|
<button type="button" id="theme-toggle" class="mt-2 flex w-full items-center justify-between rounded-xl px-3 py-2 text-left text-sm font-medium text-neutral-700 transition hover:bg-neutral-100 hover:text-neutral-900 focus:outline-none focus:ring-2 focus:ring-neutral-300 dark:text-neutral-200 dark:hover:bg-neutral-800 dark:hover:text-neutral-50 dark:focus:ring-neutral-600" role="menuitem">
|
|
<span>切换主题</span>
|
|
<span class="inline-flex h-8 w-8 items-center justify-center rounded-full bg-neutral-100 text-neutral-500 dark:bg-neutral-800 dark:text-neutral-300" aria-hidden="true">
|
|
<svg class="h-4 w-4 dark:hidden" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"></path>
|
|
</svg>
|
|
<svg class="hidden h-4 w-4 dark:block" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"></path>
|
|
</svg>
|
|
</span>
|
|
</button>
|
|
<form class="mt-1" action="{{ url_for('auth.logout') }}" method="post">
|
|
<button type="submit" class="flex w-full items-center justify-between rounded-xl px-3 py-2 text-left text-sm font-medium text-neutral-700 transition hover:bg-neutral-100 hover:text-neutral-900 focus:outline-none focus:ring-2 focus:ring-neutral-300 dark:text-neutral-200 dark:hover:bg-neutral-800 dark:hover:text-neutral-50 dark:focus:ring-neutral-600" role="menuitem" aria-label="退出登录">
|
|
<span>退出登录</span>
|
|
<span class="inline-flex h-8 w-8 items-center justify-center rounded-full bg-neutral-100 text-neutral-500 dark:bg-neutral-800 dark:text-neutral-300" aria-hidden="true">↗</span>
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
{% else %}
|
|
<div class="mx-auto flex min-h-screen max-w-lg items-start justify-center px-4 py-6 sm:items-center sm:px-6 sm:py-10 lg:px-8">
|
|
<div class="w-full max-w-md">
|
|
<header class="mb-5 text-center sm:mb-6">
|
|
<div class="mx-auto flex h-16 w-16 items-center justify-center rounded-3xl border border-brand-200 bg-brand-50 text-2xl font-bold tracking-[0.18em] text-brand-600 dark:border-brand-500/30 dark:bg-brand-500/10 dark:text-brand-300">HW</div>
|
|
</header>
|
|
{% endif %}
|
|
|
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
|
{% if messages %}
|
|
<section id="global-flash-region" class="space-y-2 {% if current_account %}px-2 pt-2 sm:px-3 lg:px-4 lg:pt-3{% else %}mb-4{% endif %}">
|
|
{% for category, message in messages %}
|
|
<div class="rounded-lg border-l-4 px-4 py-3 text-sm {{ flash_classes.get(category, 'border-neutral-400 bg-neutral-50 text-neutral-700') }}">
|
|
<strong class="capitalize">{{ category }}</strong>:{{ message }}
|
|
</div>
|
|
{% endfor %}
|
|
</section>
|
|
{% endif %}
|
|
{% endwith %}
|
|
|
|
<main class="{% if current_account %}px-2 py-3 sm:px-3 lg:px-4 lg:py-4{% else %}pb-6{% endif %}">
|
|
<div class="{% if current_account %}mx-auto max-w-full{% endif %}">
|
|
{% block content %}{% endblock %}
|
|
</div>
|
|
</main>
|
|
|
|
{% if current_account %}
|
|
</div>
|
|
</div>
|
|
{% else %}
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
<script>
|
|
(function () {
|
|
const appShell = document.getElementById('app-shell');
|
|
const themeToggles = document.querySelectorAll('#theme-toggle');
|
|
const sidebarToggles = document.querySelectorAll('[data-sidebar-toggle]');
|
|
const sidebarOverlay = document.querySelector('[data-sidebar-overlay]');
|
|
const sidebarAccountActions = document.querySelector('.sidebar-account-actions');
|
|
const userMenu = document.querySelector('[data-user-menu]');
|
|
const userMenuToggle = document.getElementById('user-menu-toggle');
|
|
const userMenuPanel = document.getElementById('user-menu-panel');
|
|
const desktopMedia = window.matchMedia('(min-width: 1024px)');
|
|
|
|
function setUserMenuOpen(isOpen) {
|
|
if (!(userMenuToggle instanceof HTMLElement) || !(userMenuPanel instanceof HTMLElement)) {
|
|
return;
|
|
}
|
|
|
|
userMenuToggle.setAttribute('aria-expanded', String(isOpen));
|
|
userMenuPanel.classList.toggle('hidden', !isOpen);
|
|
}
|
|
|
|
function applySidebarState() {
|
|
const isDesktop = desktopMedia.matches;
|
|
const isCollapsed = document.documentElement.classList.contains('sidebar-collapsed');
|
|
appShell?.classList.toggle('sidebar-open', !isDesktop && !isCollapsed);
|
|
if (sidebarAccountActions instanceof HTMLElement) {
|
|
sidebarAccountActions.hidden = isDesktop && isCollapsed;
|
|
}
|
|
sidebarToggles.forEach(function (toggle) {
|
|
toggle.setAttribute('aria-expanded', String(isDesktop ? !isCollapsed : !isCollapsed));
|
|
});
|
|
}
|
|
|
|
function setSidebarCollapsed(isCollapsed, options) {
|
|
const persist = options?.persist ?? true;
|
|
document.documentElement.classList.toggle('sidebar-collapsed', isCollapsed);
|
|
if (persist) {
|
|
window.localStorage.setItem('hw-sidebar-collapsed', isCollapsed ? 'true' : 'false');
|
|
}
|
|
applySidebarState();
|
|
}
|
|
|
|
sidebarToggles.forEach(function (toggle) {
|
|
toggle.addEventListener('click', function () {
|
|
if (desktopMedia.matches) {
|
|
setSidebarCollapsed(!document.documentElement.classList.contains('sidebar-collapsed'));
|
|
return;
|
|
}
|
|
|
|
const isOpen = appShell?.classList.contains('sidebar-open') ?? false;
|
|
setSidebarCollapsed(isOpen, { persist: false });
|
|
});
|
|
});
|
|
|
|
sidebarOverlay?.addEventListener('click', function () {
|
|
if (!desktopMedia.matches) {
|
|
setSidebarCollapsed(true, { persist: false });
|
|
}
|
|
});
|
|
|
|
desktopMedia.addEventListener('change', function (event) {
|
|
if (event.matches) {
|
|
setSidebarCollapsed(window.localStorage.getItem('hw-sidebar-collapsed') === 'true', { persist: false });
|
|
return;
|
|
}
|
|
|
|
setSidebarCollapsed(true, { persist: false });
|
|
});
|
|
|
|
applySidebarState();
|
|
|
|
userMenuToggle?.addEventListener('click', function () {
|
|
const isOpen = userMenuToggle.getAttribute('aria-expanded') === 'true';
|
|
setUserMenuOpen(!isOpen);
|
|
});
|
|
|
|
document.addEventListener('click', function (event) {
|
|
if (!(userMenu instanceof HTMLElement)) {
|
|
return;
|
|
}
|
|
|
|
if (!userMenu.contains(event.target)) {
|
|
setUserMenuOpen(false);
|
|
}
|
|
});
|
|
|
|
if (themeToggles.length === 0) {
|
|
return;
|
|
}
|
|
|
|
themeToggles.forEach(function (themeToggle) {
|
|
themeToggle.addEventListener('click', function () {
|
|
const isDark = document.documentElement.classList.toggle('dark');
|
|
window.localStorage.setItem('hw-theme', isDark ? 'dark' : 'light');
|
|
document.documentElement.style.colorScheme = isDark ? 'dark' : 'light';
|
|
setUserMenuOpen(false);
|
|
});
|
|
});
|
|
}());
|
|
</script>
|
|
</body>
|
|
</html>
|
|
|