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)
158 lines
2.9 KiB
Vue
158 lines
2.9 KiB
Vue
<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>
|