mach-cms的前台,用vue写的
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

<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.isAdmin" 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: 52px;
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 - 52px - 80px);
max-width: 960px;
}
.app-main--admin {
max-width: none;
margin: 0;
padding: 0;
min-height: calc(100vh - 52px);
}
.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>