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.
287 lines
7.8 KiB
287 lines
7.8 KiB
<script setup lang="ts">
|
|
import { computed } from 'vue'
|
|
import { useRoute, useRouter } from 'vue-router'
|
|
import { useAuthStore } from '@/stores/auth'
|
|
import { useTheme } from '@/composables/useTheme'
|
|
import brandLogo from '@/assets/brand-logo.svg'
|
|
|
|
const auth = useAuthStore()
|
|
const router = useRouter()
|
|
const route = useRoute()
|
|
const theme = useTheme()
|
|
|
|
const pageTitle = computed(() => {
|
|
const t = route.meta.title as string | undefined
|
|
return t || ''
|
|
})
|
|
|
|
const isAdminRoute = computed(() => route.path.startsWith('/admin'))
|
|
|
|
async function handleLogout() {
|
|
try {
|
|
await auth.logout()
|
|
} catch (e) {
|
|
console.error(e)
|
|
}
|
|
router.push('/')
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div>
|
|
<header class="app-header">
|
|
<router-link to="/" class="brand" aria-label="Mach-CMS 首页">
|
|
<img :src="brandLogo" class="brand-logo" alt="Mach" />
|
|
</router-link>
|
|
<span v-if="pageTitle" class="header-page-title"><span class="header-title-sep">×</span>{{ pageTitle }}</span>
|
|
<nav class="nav-links">
|
|
<router-link to="/" title="首页">
|
|
<svg class="nav-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" />
|
|
<polyline points="9 22 9 12 15 12 15 22" />
|
|
</svg>
|
|
<span class="nav-text">首页</span>
|
|
</router-link>
|
|
<router-link to="/search" title="搜索">
|
|
<svg class="nav-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
<circle cx="11" cy="11" r="7" />
|
|
<line x1="21" y1="21" x2="16.65" y2="16.65" />
|
|
</svg>
|
|
<span class="nav-text">搜索</span>
|
|
</router-link>
|
|
<router-link v-if="auth.isLoggedIn" to="/admin" title="管理后台">
|
|
<svg class="nav-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
<rect x="3" y="3" width="7" height="7" />
|
|
<rect x="14" y="3" width="7" height="7" />
|
|
<rect x="14" y="14" width="7" height="7" />
|
|
<rect x="3" y="14" width="7" height="7" />
|
|
</svg>
|
|
<span class="nav-text">管理后台</span>
|
|
</router-link>
|
|
<router-link v-if="auth.isAdmin" to="/admin/comments" title="评论审核">
|
|
<svg class="nav-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
|
|
</svg>
|
|
<span class="nav-text">评论审核</span>
|
|
</router-link>
|
|
<span v-if="auth.username" class="username">{{ auth.username }}</span>
|
|
<a v-if="auth.isLoggedIn" href="#" title="退出" @click.prevent="handleLogout">
|
|
<svg class="nav-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" />
|
|
<polyline points="16 17 21 12 16 7" />
|
|
<line x1="21" y1="12" x2="9" y2="12" />
|
|
</svg>
|
|
<span class="nav-text">退出</span>
|
|
</a>
|
|
<router-link v-else to="/login" title="登录">
|
|
<svg class="nav-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
<path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4" />
|
|
<polyline points="10 17 15 12 10 7" />
|
|
<line x1="15" y1="12" x2="3" y2="12" />
|
|
</svg>
|
|
<span class="nav-text">登录</span>
|
|
</router-link>
|
|
</nav>
|
|
<button
|
|
class="theme-toggle"
|
|
type="button"
|
|
:title="theme.isDark ? '切至天青昼' : '切至黛墨夜'"
|
|
@click="theme.toggle()"
|
|
>{{ theme.isDark ? '昼' : '夜' }}</button>
|
|
</header>
|
|
<main class="app-main" :class="{ 'app-main--admin': isAdminRoute }">
|
|
<router-view v-slot="{ Component }">
|
|
<transition name="page" mode="out-in">
|
|
<component :is="Component" />
|
|
</transition>
|
|
</router-view>
|
|
</main>
|
|
<footer class="wave-decoration" aria-hidden="true">
|
|
<svg viewBox="0 0 1440 80" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg">
|
|
<path d="M0 40c240 40 480-40 720 0s480 40 720 0v40H0z" fill="currentColor"/>
|
|
</svg>
|
|
</footer>
|
|
</div>
|
|
</template>
|
|
|
|
<style>
|
|
* {
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
.app-header {
|
|
position: sticky;
|
|
top: 0;
|
|
z-index: 1000;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 20px;
|
|
padding: 0 24px;
|
|
height: 60px;
|
|
background-color: var(--ac-header-bg);
|
|
color: var(--ac-header-text);
|
|
border-bottom: 1px solid var(--ac-border);
|
|
transition: background-color var(--song-dur-2) var(--song-ease), color var(--song-dur-2) var(--song-ease);
|
|
}
|
|
|
|
.header-page-title {
|
|
font-family: var(--song-font-serif);
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
color: var(--ac-header-text);
|
|
}
|
|
|
|
.header-title-sep {
|
|
margin: 0 8px;
|
|
color: var(--ac-header-muted);
|
|
font-weight: 400;
|
|
}
|
|
|
|
.app-header .brand {
|
|
display: flex;
|
|
align-items: center;
|
|
height: 100%;
|
|
flex-shrink: 0;
|
|
text-decoration: none;
|
|
}
|
|
|
|
.brand-logo {
|
|
display: block;
|
|
width: auto;
|
|
height: 30px;
|
|
}
|
|
|
|
.nav-links {
|
|
display: flex;
|
|
align-items: center;
|
|
height: 100%;
|
|
gap: 6px;
|
|
margin-left: auto;
|
|
}
|
|
|
|
.nav-links a,
|
|
.nav-links .username {
|
|
color: var(--ac-header-muted);
|
|
text-decoration: none;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.nav-links a {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 5px;
|
|
height: 100%;
|
|
padding: 0 12px;
|
|
color: var(--ac-header-muted);
|
|
}
|
|
|
|
.nav-icon {
|
|
width: 16px;
|
|
height: 16px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.nav-text {
|
|
font-size: 14px;
|
|
}
|
|
|
|
.nav-links a:hover {
|
|
color: var(--ac-header-text);
|
|
}
|
|
|
|
.nav-links a.router-link-active {
|
|
color: var(--ac-header-text);
|
|
background: var(--ac-header-active-bg);
|
|
}
|
|
|
|
.nav-links .username {
|
|
margin-left: 12px;
|
|
}
|
|
|
|
.theme-toggle {
|
|
width: 34px;
|
|
height: 34px;
|
|
flex-shrink: 0;
|
|
border: 1px solid var(--ac-border);
|
|
border-radius: var(--ac-radius-m);
|
|
background: transparent;
|
|
color: var(--ac-header-muted);
|
|
font-family: var(--song-font-serif);
|
|
font-size: 15px;
|
|
letter-spacing: 0.1em;
|
|
cursor: pointer;
|
|
transition: color var(--song-dur-1) var(--song-ease), border-color var(--song-dur-1) var(--song-ease), background var(--song-dur-1) var(--song-ease);
|
|
}
|
|
|
|
.theme-toggle:hover {
|
|
color: var(--ac-header-text);
|
|
border-color: var(--ac-header-muted);
|
|
background: var(--ac-header-active-bg);
|
|
}
|
|
|
|
@media (max-width: 823px) {
|
|
.app-header {
|
|
padding: 0 10px;
|
|
gap: 8px;
|
|
}
|
|
|
|
.header-page-title {
|
|
display: none;
|
|
}
|
|
|
|
.nav-links {
|
|
gap: 0;
|
|
overflow-x: auto;
|
|
-webkit-overflow-scrolling: touch;
|
|
scrollbar-width: none;
|
|
}
|
|
|
|
.nav-links::-webkit-scrollbar {
|
|
display: none;
|
|
}
|
|
|
|
.nav-links a {
|
|
padding: 0 10px;
|
|
gap: 0;
|
|
min-width: 38px;
|
|
justify-content: center;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.nav-text,
|
|
.nav-links .username {
|
|
display: none;
|
|
}
|
|
}
|
|
|
|
.app-main {
|
|
margin: 24px auto;
|
|
padding: 0 16px;
|
|
min-height: calc(100vh - 60px - 80px);
|
|
max-width: 960px;
|
|
}
|
|
|
|
.app-main--admin {
|
|
max-width: none;
|
|
margin: 0;
|
|
padding: 0;
|
|
min-height: calc(100vh - 60px);
|
|
}
|
|
|
|
.wave-decoration {
|
|
position: fixed;
|
|
bottom: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 80px;
|
|
pointer-events: none;
|
|
z-index: -1;
|
|
color: var(--ac-primary);
|
|
opacity: 0.08;
|
|
}
|
|
|
|
.wave-decoration svg {
|
|
width: 100%;
|
|
height: 100%;
|
|
}
|
|
</style>
|