feat: 设置页官方化改造并升级 1.0.8
- 设置页改用 Halo 官方 VPageHeader / VCard / VDescription / VTag 组件 - 精简 variables.css 为设置页实际使用的变量 - 同步 README 更新日志与本地说明文档
This commit is contained in:
@@ -128,6 +128,11 @@ pnpm build # 生产构建
|
||||
|
||||
## 更新日志
|
||||
|
||||
### v1.0.8
|
||||
|
||||
- 设置页改用 Halo 官方 VPageHeader / VCard / VDescription / VTag 组件,与官方页面风格统一
|
||||
- 精简插件自身 CSS 变量,仅保留设置页所需变量
|
||||
|
||||
### v1.0.7
|
||||
|
||||
- 移除设置页方向键切换、roving tabindex 与 ARIA 单选组语义,保留原生按钮点击切换
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
# 改进交接单:设置页官方化改造(2026-08-08)
|
||||
|
||||
> 审查窗口调研产出,**交付开发窗口执行**;审查窗口不修改代码。
|
||||
> 调研来源:Halo 官方仓库 `halo-dev/halo`(commit `815292f`)+ 本地代码 + 线上 `blog.liuhangyv.top/console` 实证(已登录)。
|
||||
> 本文档为**唯一交付文件**(已合并此前两份调研文档)。
|
||||
|
||||
## 〇、需求与结论总览
|
||||
|
||||
| # | 需求 | 结论 |
|
||||
| --- | --- | --- |
|
||||
| 1 | 侧边栏「深色模式」入口状态反馈 | **不做改动**(依据见第一节) |
|
||||
| 2 | 设置页顶部缺官方风格标题栏 | **要改**:使用官方 `VPageHeader`(第二节) |
|
||||
| 3 | 尽量用官方组件实现 | 组件全景与复用方案见第三、四节 |
|
||||
|
||||
## 一、侧边栏「深色模式」入口状态反馈(结论:保持现状,不动)
|
||||
|
||||
### 1.1 现象
|
||||
|
||||
点击「深色模式」后界面切换为深色,但侧边栏「深色模式」入口(菜单项)文案没有随之变化;用户曾疑问是否应显示"白天模式 / 深色模式 / 跟随系统模式"。
|
||||
|
||||
### 1.2 调研结论(证据)
|
||||
|
||||
1. **Halo 菜单是静态导航**:`use-route-menu-generator.ts` 一次性从 `router.getRoutes()` 按 `meta.menu` 生成菜单,无业务状态联动。
|
||||
2. **菜单名是静态字符串**:`RoutesMenu.tsx` 中 `title={t(item.name, item.name)}`,官方机制不支持动态文案;所有官方/插件菜单项均为固定名称。
|
||||
3. **激活高亮已生效**:`active={route.matched.includes(item.path)}`;线上实证进入 `/console/dark-mode-settings` 后,菜单项 `class="active menu-item-title"`。
|
||||
4. **平台惯例**:窗口内改设置、侧边栏入口不变化,是 Halo 全站一致行为(菜单是入口,不是状态开关)。
|
||||
|
||||
### 1.3 结论
|
||||
|
||||
- 动态菜单文案不可直接实现(需 DOM hack,不推荐)。
|
||||
- 保持静态「深色模式」菜单名,激活高亮现状即符合平台标准。
|
||||
- 当前模式信息继续由设置页内「当前生效」展示(第四节中会优化展示形式)。
|
||||
|
||||
## 二、设置页顶部标题栏(需改造)
|
||||
|
||||
### 2.1 现象
|
||||
|
||||
`/console/dark-mode-settings` 顶部为自定义 `h1 + p`;官方页面(`/console/theme`、`/console/plugins`)顶部有统一白底标题栏。线上实证:设置页 `document.querySelectorAll('.page-header').length === 0`。
|
||||
|
||||
### 2.2 官方 `VPageHeader` 组件(`@halo-dev/components` 已导出)
|
||||
|
||||
结构(源码 `ui/packages/components/src/components/header/PageHeader.vue`):
|
||||
|
||||
```html
|
||||
<div class="page-header">
|
||||
<h2 class="page-header__title">
|
||||
<slot name="icon" />
|
||||
<span class="page-header__title-text">{{ title }}</span>
|
||||
</h2>
|
||||
<div class="page-header__actions"><slot name="actions" /></div>
|
||||
</div>
|
||||
```
|
||||
|
||||
官方用法(`PluginList.vue`):
|
||||
|
||||
```html
|
||||
<VPageHeader :title="…">
|
||||
<template #icon><IconPlug /></template>
|
||||
<template #actions>…按钮…</template>
|
||||
</VPageHeader>
|
||||
<div class="m-0 md:m-4"><VCard>…</VCard></div>
|
||||
```
|
||||
|
||||
## 三、官方组件库全景(21 类)与插件对照
|
||||
|
||||
### 3.1 组件清单
|
||||
|
||||
| 目录 | 导出名 | 用途 | 设置页可用 |
|
||||
| --- | --- | --- | --- |
|
||||
| header | `VPageHeader` | 页面标题栏 | ✅ |
|
||||
| card | `VCard`(title / bodyClass;slot header) | 内容卡片 | ✅ |
|
||||
| description | `VDescription` / `VDescriptionItem`(label / content / verticalCenter) | 键值展示 | ✅ |
|
||||
| tag | `VTag`(theme / rounded;slot leftIcon) | 标签/徽标 | ✅ |
|
||||
| status | `VStatusDot`(state / text / animate) | 状态点 | ✅ |
|
||||
| button | `VButton`(type / size / block / ghost / loading / route;slot icon) | 按钮 | ✅ |
|
||||
| space | `VSpace` | 间距布局 | ✅ |
|
||||
| alert | `VAlert` | 提示条 | 可选 |
|
||||
| switch | `VSwitch` | 开关 | 未来扩展 |
|
||||
| menu | `VMenu` / `VMenuItem` / `VMenuLabel` | 菜单 | — |
|
||||
| entity | `VEntity` / `VEntityContainer` / `VEntityField` | 列表实体 | — |
|
||||
| modal / dialog | `VModal` / `VDialog` | 模态/确认 | — |
|
||||
| dropdown | `VDropdownItem` / `VDropdownDivider` | 下拉 | — |
|
||||
| tabs | `VTabs` / `VTabItem` / `VTabbar` | 标签页 | — |
|
||||
| pagination / empty / loading | `VPagination` / `VEmpty` / `VLoading` | 分页/空态/加载 | — |
|
||||
| avatar | `VAvatar` / `VAvatarGroup` | 头像 | — |
|
||||
| toast / tooltip | `toast` 函数 / `vTooltip` 指令(非 V 组件) | 提示 | 可选 |
|
||||
|
||||
> **注意:官方组件库没有 radio / radio group 单选组件** —— 三个模式选项保持语义化 `<button>` 是合理方案。
|
||||
|
||||
### 3.2 插件现状 vs 官方组件
|
||||
|
||||
| 插件现状 | 建议 |
|
||||
| --- | --- |
|
||||
| 标题:自定义 `.dark-mode-settings__header`(h1+p) | **`VPageHeader`** |
|
||||
| 卡片:`.dark-mode-settings__card` | **`VCard`** |
|
||||
| 「当前生效」:自定义 div + strong | **`VDescription` + `VDescriptionItem`**,模式值用 **`VTag`** / `VStatusDot` |
|
||||
| 三个模式选项:自定义 `<button>` + `is-active` | 保持 `<button>`(无官方单选组件) |
|
||||
| 图标:`IconPalette` / ri 图标 | 已是官方体系 ✅ |
|
||||
|
||||
## 四、设置页改造方案(用官方组件实现)
|
||||
|
||||
`ui/src/views/SettingsView.vue` 重构骨架:
|
||||
|
||||
```html
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
IconPalette,
|
||||
VCard,
|
||||
VDescription,
|
||||
VDescriptionItem,
|
||||
VPageHeader,
|
||||
VTag,
|
||||
} from '@halo-dev/components'
|
||||
// …现有 useDarkMode / currentEffectiveMode / modeOptions 逻辑保留…
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<VPageHeader title="深色模式设置">
|
||||
<template #icon><IconPalette /></template>
|
||||
<!-- 可选:<template #actions><VButton size="sm" @click="…">…</VButton></template> -->
|
||||
</VPageHeader>
|
||||
|
||||
<div class="m-0 md:m-4">
|
||||
<VCard :body-class="['!p-0']">
|
||||
<div class="p-4">
|
||||
<VDescription>
|
||||
<VDescriptionItem label="当前生效">
|
||||
<VTag>{{ currentEffectiveMode }}</VTag>
|
||||
</VDescriptionItem>
|
||||
</VDescription>
|
||||
|
||||
<div class="dark-mode-settings__options">
|
||||
<!-- 三个模式按钮保持现有实现(含 is-active 高亮) -->
|
||||
</div>
|
||||
</div>
|
||||
</VCard>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
要点:
|
||||
- 外层 `m-0 md:m-4` + `VCard` 是官方列表页通行布局(同 `PluginList.vue`)。
|
||||
- 顶部标题栏白底在深色模式下由 Dark Reader 自动转换,无需额外适配。
|
||||
- 三个模式选项维持 `<button>`(官方无单选组件),保留 `is-active` 选中态。
|
||||
- 样式清理:移除 `.dark-mode-settings__header` / `__title` / `__desc` 相关 CSS;卡片/选项样式按需精简。
|
||||
|
||||
## 五、涉及文件与验证
|
||||
|
||||
| 文件 | 改动 |
|
||||
| --- | --- |
|
||||
| `ui/src/views/SettingsView.vue` | 用 `VPageHeader` / `VCard` / `VDescription` / `VTag` 重构 |
|
||||
| `ui/src/styles/variables.css` | 可精简(自定义 header/card 样式移除后) |
|
||||
| `README.md` / `CLAUDE.md` / `AGENTS.md` | 可选:同步设置页结构描述 |
|
||||
|
||||
验证:
|
||||
|
||||
```bash
|
||||
cd ui
|
||||
pnpm build && pnpm type-check && pnpm prettier && pnpm lint && pnpm test:unit
|
||||
cd .. && gradlew build
|
||||
```
|
||||
|
||||
部署后线上确认:
|
||||
- `/console/dark-mode-settings` 顶部出现与官方一致的 `.page-header` 标题栏
|
||||
- 侧边栏「深色模式」菜单项进入页面后保持高亮(现状回归确认,未改动)
|
||||
- 深色模式下标题栏/卡片与整体协调
|
||||
|
||||
## 六、注意事项
|
||||
|
||||
- 问题 1(侧边栏入口)**明确不做改动**。
|
||||
- 官方组件无单选组件,三选项保持 `<button>`,勿强行套用不存在的组件。
|
||||
- 官方组件样式依赖 Halo Console 的 UnoCSS/主题变量,插件内直接使用即可。
|
||||
- i18n(可选):插件文案目前硬编码中文,未来可接入 Halo `locales` 国际化,非本次必做。
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
version=1.0.7
|
||||
version=1.0.8
|
||||
org.gradle.jvmargs=-Xmx2g -Dfile.encoding=UTF-8
|
||||
org.gradle.java.home=D:/settings/Language/Java/jdk-25.0.1
|
||||
|
||||
@@ -11,7 +11,7 @@ import run.halo.app.plugin.PluginContext;
|
||||
* <p>Only one main class extending {@link BasePlugin} is allowed per plugin.</p>
|
||||
*
|
||||
* @author LHY
|
||||
* @since 1.0.7
|
||||
* @since 1.0.8
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
|
||||
@@ -6,56 +6,14 @@
|
||||
/* 说明:data-halo-theme 为兼容性遗留标记,当前已无 CSS 消费方,保留给外部脚本与验证工具。 */
|
||||
/* ===== 浅色模式(Halo 默认,此处定义为显式回退) ===== */
|
||||
:root {
|
||||
--halo-bg-body: oklch(97% 0.005 250);
|
||||
--halo-bg-sidebar: oklch(100% 0 0);
|
||||
--halo-bg-content: oklch(97% 0.005 250);
|
||||
--halo-bg-card: oklch(100% 0 0);
|
||||
--halo-bg-input: oklch(100% 0 0);
|
||||
--halo-bg-hover: oklch(95% 0.01 250);
|
||||
--halo-bg-active: oklch(90% 0.02 160);
|
||||
--halo-bg-disabled: oklch(95% 0.005 250);
|
||||
--halo-bg-tooltip: oklch(20% 0.01 250);
|
||||
--halo-bg-modal: oklch(0% 0 0 / 60%);
|
||||
--halo-bg-dropdown: oklch(100% 0 0);
|
||||
|
||||
--halo-text-primary: oklch(20% 0.01 250);
|
||||
--halo-text-secondary: oklch(45% 0.01 250);
|
||||
--halo-text-tertiary: oklch(60% 0.01 250);
|
||||
--halo-text-link: oklch(45% 0.15 160);
|
||||
--halo-text-inverse: oklch(100% 0 0);
|
||||
|
||||
--halo-bg-hover: oklch(95% 0.01 250);
|
||||
|
||||
--halo-border-base: oklch(88% 0.01 250);
|
||||
--halo-border-light: oklch(93% 0.005 250);
|
||||
--halo-border-input: oklch(80% 0.01 250);
|
||||
--halo-border-focus: oklch(55% 0.15 160);
|
||||
|
||||
--halo-accent-primary: oklch(55% 0.14 160);
|
||||
--halo-accent-primary-hover: oklch(48% 0.15 160);
|
||||
--halo-accent-primary-text: oklch(100% 0 0);
|
||||
--halo-accent-danger: oklch(45% 0.18 25);
|
||||
--halo-accent-danger-hover: oklch(40% 0.19 25);
|
||||
--halo-accent-success: oklch(50% 0.16 150);
|
||||
--halo-accent-warning: oklch(60% 0.16 85);
|
||||
|
||||
--halo-shadow-sm: 0 1px 2px oklch(0% 0 0 / 6%);
|
||||
--halo-shadow-base: 0 2px 8px oklch(0% 0 0 / 10%);
|
||||
--halo-shadow-lg: 0 4px 16px oklch(0% 0 0 / 14%);
|
||||
|
||||
--halo-scrollbar-thumb: oklch(80% 0.01 250);
|
||||
--halo-scrollbar-track: oklch(95% 0.005 250);
|
||||
|
||||
--halo-search-bg: oklch(95% 0.01 250);
|
||||
--halo-search-text: oklch(45% 0.01 250);
|
||||
--halo-search-placeholder: oklch(60% 0.01 250);
|
||||
|
||||
--halo-menu-item-hover: oklch(93% 0.02 160);
|
||||
--halo-menu-item-active: oklch(88% 0.03 160);
|
||||
--halo-menu-group-title: oklch(55% 0.01 250);
|
||||
|
||||
--halo-table-header-bg: oklch(96% 0.005 250);
|
||||
--halo-table-row-hover: oklch(93% 0.015 250);
|
||||
--halo-table-border: oklch(88% 0.01 250);
|
||||
|
||||
--halo-tag-bg: oklch(90% 0.04 160);
|
||||
--halo-tag-text: oklch(40% 0.1 160);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import type { ThemeMode } from '../composables/useDarkMode'
|
||||
import { useDarkMode } from '../composables/useDarkMode'
|
||||
import {
|
||||
IconPalette,
|
||||
VCard,
|
||||
VDescription,
|
||||
VDescriptionItem,
|
||||
VPageHeader,
|
||||
VTag,
|
||||
} from '@halo-dev/components'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const { theme, isDark, setTheme } = useDarkMode()
|
||||
@@ -20,79 +28,47 @@ const modeOptions: { value: ThemeMode; label: string; description: string }[] =
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="dark-mode-settings">
|
||||
<div class="dark-mode-settings__header">
|
||||
<h1 class="dark-mode-settings__title">深色模式设置</h1>
|
||||
<p class="dark-mode-settings__desc">选择后台管理面板的显示模式</p>
|
||||
</div>
|
||||
<div>
|
||||
<VPageHeader title="深色模式设置">
|
||||
<template #icon>
|
||||
<IconPalette />
|
||||
</template>
|
||||
</VPageHeader>
|
||||
|
||||
<div class="dark-mode-settings__card">
|
||||
<div class="dark-mode-settings__current">
|
||||
当前生效:<strong>{{ currentEffectiveMode }}</strong>
|
||||
</div>
|
||||
<div class="m-0 md:m-4">
|
||||
<VCard :body-class="['!p-0']">
|
||||
<div class="p-4">
|
||||
<VDescription>
|
||||
<VDescriptionItem label="当前生效">
|
||||
<VTag>{{ currentEffectiveMode }}</VTag>
|
||||
</VDescriptionItem>
|
||||
</VDescription>
|
||||
|
||||
<div class="dark-mode-settings__options">
|
||||
<button
|
||||
v-for="option in modeOptions"
|
||||
:key="option.value"
|
||||
type="button"
|
||||
class="dark-mode-settings__option"
|
||||
:class="{ 'is-active': theme === option.value }"
|
||||
@click="setTheme(option.value)"
|
||||
>
|
||||
<div class="dark-mode-settings__option-label">{{ option.label }}</div>
|
||||
<div class="dark-mode-settings__option-desc">{{ option.description }}</div>
|
||||
</button>
|
||||
</div>
|
||||
<div class="dark-mode-settings__options">
|
||||
<button
|
||||
v-for="option in modeOptions"
|
||||
:key="option.value"
|
||||
type="button"
|
||||
class="dark-mode-settings__option"
|
||||
:class="{ 'is-active': theme === option.value }"
|
||||
@click="setTheme(option.value)"
|
||||
>
|
||||
<div class="dark-mode-settings__option-label">{{ option.label }}</div>
|
||||
<div class="dark-mode-settings__option-desc">{{ option.description }}</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</VCard>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.dark-mode-settings {
|
||||
max-width: 640px;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.dark-mode-settings__header {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.dark-mode-settings__title {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
color: var(--halo-text-primary);
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.dark-mode-settings__desc {
|
||||
font-size: 0.875rem;
|
||||
color: var(--halo-text-secondary);
|
||||
}
|
||||
|
||||
.dark-mode-settings__card {
|
||||
padding: 1.25rem;
|
||||
background-color: var(--halo-bg-card);
|
||||
border: 1px solid var(--halo-border-base);
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.dark-mode-settings__current {
|
||||
font-size: 0.875rem;
|
||||
color: var(--halo-text-secondary);
|
||||
margin-bottom: 1rem;
|
||||
padding-bottom: 1rem;
|
||||
border-bottom: 1px solid var(--halo-border-light);
|
||||
}
|
||||
|
||||
.dark-mode-settings__current strong {
|
||||
color: var(--halo-accent-primary);
|
||||
}
|
||||
|
||||
.dark-mode-settings__options {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.dark-mode-settings__option {
|
||||
|
||||
Reference in New Issue
Block a user