Files
ONE/frontend/src/components/LeftNav.vue
T
cjun a721e26e45 后台管理重塑 + 主题 / 皮肤双轴 + a11y 与编辑器增强
Backend
- Post 加 cover_url,Tag 加 color;老库 ALTER 兼容
- admin API: GET /api/admin/dashboard,POST /bulk,POST /tags/:id/merge,
  PUT /tags/:id 接 {name,color},Settings 加 light_skin_id(兼容老 theme_id)
- 白名单兜底(非法 light_skin_id 落 paper)
- 新增合并 / Dashboard / Theme 测试覆盖

Frontend
- AdminLayout 改为侧边栏布局;新增 Dashboard 总览页
- PostsView 卡片化预览 + 筛选 chip + 批量操作 + 排序
- EditorView 增强:封面图 / WYSIWYG↔MD 双模式 / 实时大纲 / 字数统计 /
  自动保存条 / ⌘S / unsaved 守卫(beforeunload + beforeRouteLeave)
- TagsView 加色板 + 合并到目标
- SettingsView 主题外观改为「亮色皮肤」三选一 + 「暗色皮肤」固定墨色说明
- 新增 CommandPalette(⌘K)+ ShortcutsPanel(?)
- api.js 接新接口;router.js 拆 /admin → /admin/posts

主题 / 皮肤双轴
- site.js: themeMode(light/dark/auto)+ light_skin_id;data-theme 前台,
  data-admin-theme 后台(仅跟随 mode 切明暗)
- styles.css: :root[data-theme] 四套皮肤 + :root[data-admin-theme=dark] 后台墨感
- 修 sticky head / ThemeSwitcher / 表单 / 批量条 等 token 引用
- 后台用独立 --admin-* 系列,与前台主题解耦
- 左栏底部放 inline 形态 ThemeSwitcher(图标按钮 36×36),mobile 保留浮窗

a11y
- 全局 :focus-visible 描边(前后台跟随 token)
- prefers-reduced-motion 全局降级
- color-scheme 随主题切
- meta theme-color 双套(light/dark)
- 命令面板 / 快捷键面板:role=dialog aria-modal aria-label + 搜索框 aria-label
- icon-only 按钮加 aria-label(tag × / 清除选择 / 移除封面等)
- 输入框 aria-label + spellcheck=false
- PostsView 筛选同步 URL(可分享、可刷新保留)
- img width/height + loading=lazy
- utils.js 用 Intl.DateTimeFormat(zh-CN / sv-SE)
2026-09-21 22:14:58 +08:00

158 lines
2.9 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup>
import { site } from '../site'
import { publicApi } from '../api'
import { onMounted, ref } from 'vue'
import ThemeSwitcher from './ThemeSwitcher.vue'
const tags = ref([])
onMounted(async () => {
try {
const data = await publicApi.tags()
tags.value = (data.tags || []).slice(0, 8)
} catch (e) {
tags.value = []
}
})
</script>
<template>
<aside class="rail-left">
<div class="inner">
<RouterLink to="/" class="brand">
<span class="mark">○</span>
<span class="name">{{ site.site_title || 'ONE' }}</span>
</RouterLink>
<p class="desc">{{ site.site_desc }}</p>
<nav class="nav">
<RouterLink to="/" class="item">
<span class="ico">◴</span><span>时间线</span>
</RouterLink>
<RouterLink to="/archive" class="item">
<span class="ico">▤</span><span>归档</span>
</RouterLink>
<RouterLink to="/tags" class="item">
<span class="ico">#</span><span>标签</span>
</RouterLink>
<RouterLink to="/about" class="item">
<span class="ico">◍</span><span>关于</span>
</RouterLink>
</nav>
<div v-if="tags.length" class="tagbox">
<p class="eyebrow">常读标签</p>
<div class="tags">
<RouterLink v-for="t in tags" :key="t.id" :to="`/tag/${t.slug}`" class="tag-chip">
{{ t.name }}
</RouterLink>
</div>
</div>
<div class="theme-box">
<p class="eyebrow">主题</p>
<ThemeSwitcher variant="inline" />
</div>
</div>
</aside>
</template>
<style scoped>
.rail-left {
position: sticky;
top: 0;
align-self: start;
padding: 28px 0 40px;
}
.inner {
display: flex;
flex-direction: column;
gap: 16px;
}
.brand {
display: flex;
align-items: center;
gap: 8px;
}
.mark {
color: var(--accent);
font-size: 18px;
}
.name {
font-family: var(--serif);
font-size: 21px;
letter-spacing: 0.02em;
}
.desc {
margin: -6px 0 0;
font-size: 13px;
line-height: 1.75;
color: var(--muted);
}
.nav {
display: flex;
flex-direction: column;
gap: 2px;
margin-top: 4px;
}
.item {
display: flex;
align-items: center;
gap: 10px;
padding: 7px 10px;
margin-left: -10px;
border-radius: 3px;
font-size: 15px;
color: var(--ink-soft);
}
.item:hover {
background: var(--paper-sunken);
color: var(--ink);
}
.item.router-link-exact-active {
color: var(--accent);
font-weight: 500;
}
.ico {
width: 16px;
text-align: center;
color: var(--faint);
font-size: 13px;
}
.item.router-link-exact-active .ico {
color: var(--accent);
}
.tagbox {
border-top: 1px solid var(--line);
padding-top: 14px;
}
.tags {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: 8px;
}
.theme-box {
border-top: 1px solid var(--line);
padding-top: 14px;
}
.theme-box .theme-switcher {
margin-top: 8px;
}
</style>