后台管理重塑 + 主题 / 皮肤双轴 + 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)
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
import { site } from '../site'
|
||||
import { publicApi } from '../api'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import ThemeSwitcher from './ThemeSwitcher.vue'
|
||||
|
||||
const tags = ref([])
|
||||
|
||||
@@ -39,8 +40,6 @@ onMounted(async () => {
|
||||
</RouterLink>
|
||||
</nav>
|
||||
|
||||
<a class="subscribe" href="/rss.xml">订阅 RSS</a>
|
||||
|
||||
<div v-if="tags.length" class="tagbox">
|
||||
<p class="eyebrow">常读标签</p>
|
||||
<div class="tags">
|
||||
@@ -49,6 +48,11 @@ onMounted(async () => {
|
||||
</RouterLink>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="theme-box">
|
||||
<p class="eyebrow">主题</p>
|
||||
<ThemeSwitcher variant="inline" />
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</template>
|
||||
@@ -130,21 +134,6 @@ onMounted(async () => {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.subscribe {
|
||||
align-self: flex-start;
|
||||
padding: 7px 20px;
|
||||
border-radius: 999px;
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
border: 1px solid var(--accent);
|
||||
}
|
||||
|
||||
.subscribe:hover {
|
||||
background: #356f88;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.tagbox {
|
||||
border-top: 1px solid var(--line);
|
||||
padding-top: 14px;
|
||||
@@ -156,4 +145,13 @@ onMounted(async () => {
|
||||
gap: 6px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.theme-box {
|
||||
border-top: 1px solid var(--line);
|
||||
padding-top: 14px;
|
||||
}
|
||||
|
||||
.theme-box .theme-switcher {
|
||||
margin-top: 8px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -71,7 +71,7 @@ const initial = computed(() => (site.author_name || 'O').trim().slice(0, 1).toUp
|
||||
display: grid;
|
||||
grid-template-columns: 40px minmax(0, 1fr);
|
||||
gap: 12px;
|
||||
padding: 18px 16px;
|
||||
padding: 14px 16px;
|
||||
margin: 0 -16px;
|
||||
border-bottom: 1px solid var(--line-soft);
|
||||
transition: background 0.15s ease;
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { site } from '../site'
|
||||
|
||||
// 循环顺序:auto → light → dark → auto
|
||||
const order = ['auto', 'light', 'dark']
|
||||
const meta = {
|
||||
auto: { icon: '◐', label: '跟随系统', hint: '跟随操作系统的明暗偏好' },
|
||||
light: { icon: '☀', label: '浅色', hint: '始终使用浅色' },
|
||||
dark: { icon: '☾', label: '深色', hint: '始终使用深色' }
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
// floating: 全局右下角浮动(仅 mobile 用)
|
||||
// inline: 嵌入父容器,跟左栏布局一起
|
||||
variant: {
|
||||
type: String,
|
||||
default: 'inline',
|
||||
validator: (v) => ['floating', 'inline'].includes(v)
|
||||
}
|
||||
})
|
||||
|
||||
const current = () => meta[site.themeMode] || meta.auto
|
||||
const nextMode = () => {
|
||||
const i = order.indexOf(site.themeMode)
|
||||
return order[(i + 1) % order.length]
|
||||
}
|
||||
|
||||
function cycle() {
|
||||
site.themeMode = nextMode()
|
||||
}
|
||||
|
||||
const classes = computed(() => ['theme-switcher', `theme-switcher--${props.variant}`])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
:class="classes"
|
||||
type="button"
|
||||
:title="`${current().label} · 点击切换到 ${meta[nextMode()].label}`"
|
||||
:aria-label="`主题:${current().label}`"
|
||||
@click="cycle"
|
||||
>
|
||||
<span class="ic">{{ current().icon }}</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.theme-switcher {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid var(--admin-line);
|
||||
border-radius: 999px;
|
||||
background: var(--admin-card);
|
||||
color: var(--admin-ink-soft);
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
line-height: 1;
|
||||
padding: 0;
|
||||
transition: color 0.15s ease, background-color 0.15s ease, border-color 0.15s ease;
|
||||
}
|
||||
|
||||
.theme-switcher:hover {
|
||||
background: var(--admin-paper-sunken);
|
||||
color: var(--admin-ink);
|
||||
border-color: var(--admin-accent-line);
|
||||
}
|
||||
|
||||
.theme-switcher .ic {
|
||||
font-size: 19px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* ---------- inline 形态:嵌入左栏底部 ---------- */
|
||||
|
||||
.theme-switcher--inline {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
/* ---------- floating 形态:右下角浮动 ---------- */
|
||||
|
||||
.theme-switcher--floating {
|
||||
position: fixed;
|
||||
right: 18px;
|
||||
bottom: 18px;
|
||||
z-index: 100;
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
backdrop-filter: blur(6px);
|
||||
box-shadow: var(--admin-shadow);
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.theme-switcher--floating {
|
||||
right: 12px;
|
||||
bottom: 12px;
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -29,7 +29,7 @@ import { site } from '../site'
|
||||
top: 0;
|
||||
z-index: 20;
|
||||
padding: 10px 18px;
|
||||
background: rgba(250, 247, 241, 0.94);
|
||||
background-color: var(--paper);
|
||||
backdrop-filter: blur(6px);
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user