- 服务端新增 database 模块(/api/database/*,仅 ADMIN):全库 JSON 快照(备份 / 恢复 / 导入导出 / 定时备份
共用同一格式,14 张表按拓扑序导出、恢复时关外键检查)、连接测试与目标库校验、一键 SQLite ⇄ MySQL 切换
(备份 → 导出 → 测试连接 → 校验空库 → 改 schema → 子进程 generate / tsc / db push → 写数据,全部成功才改写
.env,中途失败回滚 schema);XML 快照与 SQLite 数据文件均可下载;打包形态随包带上 prisma CLI 与 typescript
(prisma-client 生成物是 .ts,需再编进 dist/generated,安装包因之变大)
- Web 管理端新增「数据库管理」页(仅管理员),并加首次登录新手引导(el-tour,可跳过、可随时重看,版本号写
localStorage,改版后老用户会再收到一次)
- 客户端设置改版:侧边栏改为分组折叠菜单,设置拆成 6 个子路由
(/settings/{general,appearance,island,reminder,account,about}),新增深色模式(主题持久化到主进程配置,
主窗口底色跟主题走,避免"先白后黑"闪一下),新增「关于」页(应用信息 / 诊断信息)
- 客户端新增首启六步引导(OnboardingWelcome,状态存主进程配置 onboardingDone)
- 客户端主窗口初始尺寸与 ClassIsland 对齐(1242x582 / minHeight 540 / 居中)
- 版本号统一提升到 1.0.0:5 个 package.json、部署配置与镜像标签(.env.example / docker-compose / Dockerfile)、
服务端 /health 与首页、插件 manifest.yml 与 BridgeService.PluginVersion、文档里的安装包名
- 文档与验收同步:AGENTS.md 补第 37~51 条约定(深色主题实现边界、设置子路由、数据库管理三件事、侧栏与窗口
尺寸、引导在自动化里的判据等),README 补「数据库管理 / 新手引导 / 设置改版」三节;verify:e2e 增补 13 条
数据库管理用例,客户端冒烟与 Web UI 冒烟跟进子路由、深色主题、侧栏与引导
验证:pnpm lint 0 error;pnpm typecheck 全过;verify:e2e 186/186;verify:classisland-plugin 68/68;
build:desktop 通过(含产物凭据门禁);verify:desktop 99/102(3 项为上课时段下的"真实通知链路"用例,
客户端按产品规则暂存普通通知所致,见下);插件 dotnet 编译 0 错误。
注:verify:desktop 的「真实通知链路 / 标记已读」三项要求当前不在上课时段——本次运行时种子课表恰有
「周五 10:55-11:40 生物」正在进行,客户端正确地暂存了普通通知(AGENTS.md 第 18 条),属时间相关用例,
与本次改动无关。
490 lines
13 KiB
CSS
490 lines
13 KiB
CSS
/* ============================================================================
|
||
* 班级小助手客户端主题 —— ClassIsland 同款 Fluent 风格(浅色 / 深色双主题)
|
||
*
|
||
* 参考 ClassIsland(Windows 课件表应用)的观感:
|
||
* - WinUI / Fluent 设计语言:Mica 底 + 半透明"亚克力"卡片 + 1px 细腻描边
|
||
* - 强调色 #0F6CBD(悬停 #115EA3),选中项用强调色淡底 + 左侧强调条
|
||
* - 圆角:卡片 8px、控件 4~6px、胶囊 999px
|
||
* - 字体:Segoe UI Variable / 微软雅黑 UI,字号偏小、行距偏松
|
||
* - 阴影极轻(0 2px 4px rgba(0,0,0,.04)),层次主要靠描边与底色区分
|
||
*
|
||
* 主题机制:
|
||
* - 全部颜色走 --ch-* / --el-* 变量,:root 是浅色值;
|
||
* - 深色主题 = html 根元素挂 .dark 类(由 stores/ui.ts 按配置切换),
|
||
* Element Plus 的 dark/css-vars.css(main.ts 引入)提供 --el-* 深色默认,
|
||
* 下面的 html.dark 块覆盖品牌变量(特异性高于 :root,必定生效);
|
||
* - 组件里**不要写死颜色**,一律用变量——深色回归靠这条纪律守着。
|
||
* ========================================================================== */
|
||
|
||
:root {
|
||
/* Fluent 强调色 */
|
||
--ch-accent: #0f6cbd;
|
||
--ch-accent-hover: #115ea3;
|
||
--ch-accent-soft: rgba(15, 108, 189, 0.08);
|
||
--ch-accent-soft-strong: rgba(15, 108, 189, 0.14);
|
||
|
||
/* 层次底色(Mica / Layer) */
|
||
--ch-bg: #f3f3f3;
|
||
--ch-layer: rgba(255, 255, 255, 0.7);
|
||
--ch-layer-solid: #ffffff;
|
||
--ch-layer-alt: rgba(249, 249, 249, 0.5);
|
||
--ch-card-subtle: rgba(255, 255, 255, 0.5);
|
||
|
||
/* 描边(Fluent 用半透明黑,避免生硬的灰线) */
|
||
--ch-border: rgba(0, 0, 0, 0.06);
|
||
--ch-border-strong: rgba(0, 0, 0, 0.1);
|
||
--ch-divider: rgba(0, 0, 0, 0.05);
|
||
|
||
/* 文本 */
|
||
--ch-text: #1b1b1b;
|
||
--ch-text-secondary: #5c5c5c;
|
||
--ch-text-tertiary: #8a8a8a;
|
||
|
||
/* 交互态(悬停底色 / 侧栏与顶栏的亚克力底 / 滚动条) */
|
||
--ch-hover-soft: rgba(0, 0, 0, 0.04);
|
||
--ch-nav-bg: rgba(255, 255, 255, 0.65);
|
||
--ch-header-bg: rgba(255, 255, 255, 0.6);
|
||
--ch-scrollbar: rgba(0, 0, 0, 0.18);
|
||
--ch-scrollbar-hover: rgba(0, 0, 0, 0.3);
|
||
|
||
/* 圆角与阴影 */
|
||
--ch-radius-card: 8px;
|
||
--ch-radius-control: 4px;
|
||
--ch-shadow-flyout: 0 8px 16px rgba(0, 0, 0, 0.08);
|
||
--ch-shadow-card: 0 2px 4px rgba(0, 0, 0, 0.04);
|
||
|
||
/* 兼容旧变量名 */
|
||
--ch-sidebar: #ffffff;
|
||
|
||
/* Element Plus 变量对齐 Fluent */
|
||
--el-color-primary: #0f6cbd;
|
||
--el-color-primary-light-3: #4d94d1;
|
||
--el-color-primary-light-5: #7fb1de;
|
||
--el-color-primary-light-7: #b3d0ec;
|
||
--el-color-primary-light-8: #d0e2f4;
|
||
--el-color-primary-light-9: #e7f0f9;
|
||
--el-color-primary-dark-2: #115ea3;
|
||
--el-border-radius-base: 4px;
|
||
--el-border-radius-small: 3px;
|
||
--el-text-color-primary: #1b1b1b;
|
||
--el-text-color-regular: #3a3a3a;
|
||
--el-text-color-secondary: #5c5c5c;
|
||
--el-border-color: rgba(0, 0, 0, 0.1);
|
||
--el-border-color-light: rgba(0, 0, 0, 0.06);
|
||
--el-border-color-lighter: rgba(0, 0, 0, 0.05);
|
||
--el-fill-color-blank: transparent;
|
||
--el-font-size-base: 13px;
|
||
}
|
||
|
||
/* ---------------------------------------------------------------- 深色主题 */
|
||
|
||
html.dark {
|
||
--ch-accent: #5aabec;
|
||
--ch-accent-hover: #7cc0f4;
|
||
--ch-accent-soft: rgba(90, 171, 236, 0.16);
|
||
--ch-accent-soft-strong: rgba(90, 171, 236, 0.26);
|
||
|
||
--ch-bg: #161719;
|
||
--ch-layer: rgba(32, 33, 36, 0.72);
|
||
--ch-layer-solid: #1e1f23;
|
||
--ch-layer-alt: rgba(28, 29, 32, 0.5);
|
||
--ch-card-subtle: rgba(255, 255, 255, 0.04);
|
||
|
||
--ch-border: rgba(255, 255, 255, 0.08);
|
||
--ch-border-strong: rgba(255, 255, 255, 0.14);
|
||
--ch-divider: rgba(255, 255, 255, 0.06);
|
||
|
||
--ch-text: #e8e8ea;
|
||
--ch-text-secondary: #b0b1b5;
|
||
--ch-text-tertiary: #7e7f85;
|
||
|
||
--ch-hover-soft: rgba(255, 255, 255, 0.07);
|
||
--ch-nav-bg: rgba(28, 29, 32, 0.65);
|
||
--ch-header-bg: rgba(30, 31, 34, 0.6);
|
||
--ch-scrollbar: rgba(255, 255, 255, 0.22);
|
||
--ch-scrollbar-hover: rgba(255, 255, 255, 0.36);
|
||
|
||
--ch-shadow-flyout: 0 8px 16px rgba(0, 0, 0, 0.45);
|
||
--ch-shadow-card: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||
|
||
--ch-sidebar: #1e1f23;
|
||
|
||
/* Element Plus 深色下的品牌色对齐(dark/css-vars.css 的默认蓝偏亮,这里对齐品牌) */
|
||
--el-color-primary: #4a9eff;
|
||
--el-color-primary-light-3: #337fd6;
|
||
--el-color-primary-light-5: #2563ab;
|
||
--el-color-primary-light-7: #1b4a82;
|
||
--el-color-primary-light-8: #163c6b;
|
||
--el-color-primary-light-9: #122f55;
|
||
--el-color-primary-dark-2: #6cb4ff;
|
||
--el-text-color-primary: #e8e8ea;
|
||
--el-text-color-regular: #c9cacd;
|
||
--el-text-color-secondary: #9a9b9f;
|
||
--el-border-color: rgba(255, 255, 255, 0.12);
|
||
--el-border-color-light: rgba(255, 255, 255, 0.08);
|
||
--el-border-color-lighter: rgba(255, 255, 255, 0.06);
|
||
}
|
||
|
||
* {
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
html,
|
||
body,
|
||
#app {
|
||
height: 100%;
|
||
margin: 0;
|
||
padding: 0;
|
||
}
|
||
|
||
body {
|
||
font-family:
|
||
'Segoe UI Variable Text', 'Segoe UI', 'Microsoft YaHei UI', 'Microsoft YaHei', 'PingFang SC',
|
||
'Helvetica Neue', Arial, sans-serif;
|
||
font-size: 13px;
|
||
background: var(--ch-bg);
|
||
color: var(--ch-text);
|
||
/* 桌面端禁用整页选中,更接近原生应用体验 */
|
||
user-select: none;
|
||
-webkit-font-smoothing: antialiased;
|
||
}
|
||
|
||
input,
|
||
textarea {
|
||
user-select: text;
|
||
}
|
||
|
||
/* ---------------------------------------------------------------- 页面骨架 */
|
||
|
||
.page {
|
||
padding: 16px 20px 24px;
|
||
}
|
||
|
||
.page-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 12px;
|
||
flex-wrap: wrap;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.page-title {
|
||
margin: 0;
|
||
font-size: 20px;
|
||
font-weight: 600;
|
||
letter-spacing: 0.2px;
|
||
}
|
||
|
||
.page-subtitle {
|
||
margin: 4px 0 0;
|
||
font-size: 12.5px;
|
||
color: var(--ch-text-secondary);
|
||
}
|
||
|
||
.toolbar {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.mt-12 {
|
||
margin-top: 12px;
|
||
}
|
||
|
||
.mt-16 {
|
||
margin-top: 16px;
|
||
}
|
||
|
||
.text-muted {
|
||
color: var(--ch-text-tertiary);
|
||
}
|
||
|
||
.offline-banner {
|
||
margin: 0 0 12px;
|
||
}
|
||
|
||
/* ---------------------------------------------------------------- 卡片 / 分组 */
|
||
|
||
/* 亚克力卡片:半透明底 + 极细描边 + 极轻阴影(ClassIsland 的内容卡片观感) */
|
||
.el-card {
|
||
border-radius: var(--ch-radius-card) !important;
|
||
border: 1px solid var(--ch-border) !important;
|
||
background: var(--ch-layer) !important;
|
||
backdrop-filter: blur(20px) saturate(1.1);
|
||
box-shadow: var(--ch-shadow-card) !important;
|
||
transition:
|
||
border-color 0.15s ease,
|
||
box-shadow 0.15s ease;
|
||
}
|
||
|
||
.el-card:hover {
|
||
border-color: var(--ch-border-strong) !important;
|
||
}
|
||
|
||
.el-card__header {
|
||
padding: 12px 16px !important;
|
||
border-bottom: 1px solid var(--ch-divider) !important;
|
||
font-weight: 600;
|
||
font-size: 13.5px;
|
||
}
|
||
|
||
.el-card__body {
|
||
padding: 14px 16px !important;
|
||
}
|
||
|
||
/* ---------------------------------------------------------------- 控件 */
|
||
|
||
.el-button {
|
||
border-radius: var(--ch-radius-control) !important;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.el-button--default {
|
||
background: var(--ch-layer) !important;
|
||
border-color: var(--ch-border-strong) !important;
|
||
}
|
||
|
||
.el-button--default:hover {
|
||
background: var(--ch-hover-soft) !important;
|
||
}
|
||
|
||
.el-button--primary {
|
||
box-shadow: 0 1px 2px rgba(15, 108, 189, 0.2);
|
||
}
|
||
|
||
.el-input__wrapper,
|
||
.el-textarea__inner,
|
||
.el-select__wrapper {
|
||
border-radius: var(--ch-radius-control) !important;
|
||
background: var(--ch-layer) !important;
|
||
box-shadow: 0 0 0 1px var(--ch-border-strong) inset !important;
|
||
}
|
||
|
||
.el-input__wrapper.is-focus,
|
||
.el-select__wrapper.is-focused {
|
||
box-shadow: 0 0 0 1px var(--ch-accent) inset !important;
|
||
}
|
||
|
||
.el-tag {
|
||
border-radius: var(--ch-radius-control) !important;
|
||
}
|
||
|
||
.el-dialog,
|
||
.el-drawer {
|
||
border-radius: var(--ch-radius-card) !important;
|
||
background: var(--ch-layer-solid) !important;
|
||
}
|
||
|
||
.el-message-box {
|
||
border-radius: var(--ch-radius-card) !important;
|
||
}
|
||
|
||
.el-popper,
|
||
.el-dropdown__popper {
|
||
border-radius: var(--ch-radius-card) !important;
|
||
box-shadow: var(--ch-shadow-flyout) !important;
|
||
}
|
||
|
||
/* 关于页的折叠卡片:与 el-card 同一套观感(分割线代替卡片描边) */
|
||
.el-collapse {
|
||
--el-collapse-border-color: var(--ch-divider);
|
||
border: none !important;
|
||
}
|
||
|
||
.el-collapse-item__header {
|
||
background: var(--ch-layer) !important;
|
||
border: 1px solid var(--ch-border) !important;
|
||
border-radius: var(--ch-radius-card) !important;
|
||
padding: 0 16px;
|
||
margin-top: 8px;
|
||
font-weight: 600;
|
||
font-size: 13.5px;
|
||
}
|
||
|
||
.el-collapse-item__wrap {
|
||
background: transparent !important;
|
||
border-bottom: none !important;
|
||
}
|
||
|
||
.el-collapse-item__content {
|
||
padding: 12px 6px 4px !important;
|
||
color: var(--ch-text);
|
||
}
|
||
|
||
/* ---------------------------------------------------------------- 表格 */
|
||
|
||
.el-table {
|
||
--el-table-border-color: var(--ch-divider);
|
||
--el-table-header-bg-color: transparent;
|
||
--el-table-row-hover-bg-color: var(--ch-hover-soft);
|
||
background: transparent !important;
|
||
}
|
||
|
||
.el-table th.el-table__cell {
|
||
background: transparent !important;
|
||
font-weight: 600;
|
||
color: var(--ch-text-secondary);
|
||
}
|
||
|
||
.el-table td.el-table__cell,
|
||
.el-table th.el-table__cell {
|
||
border-bottom: 1px solid var(--ch-divider) !important;
|
||
}
|
||
|
||
.el-table__inner-wrapper::before {
|
||
display: none;
|
||
}
|
||
|
||
/* ---------------------------------------------------------------- 侧边导航(Fluent NavigationView 观感) */
|
||
|
||
.ch-nav {
|
||
background: var(--ch-nav-bg);
|
||
backdrop-filter: blur(24px) saturate(1.2);
|
||
border-right: 1px solid var(--ch-border);
|
||
}
|
||
|
||
.ch-nav .el-menu-item,
|
||
.ch-nav .el-sub-menu__title {
|
||
height: 40px;
|
||
line-height: 40px;
|
||
margin: 2px 8px;
|
||
border-radius: var(--ch-radius-control);
|
||
color: var(--ch-text);
|
||
font-weight: 500;
|
||
position: relative;
|
||
}
|
||
|
||
.ch-nav .el-menu-item:hover,
|
||
.ch-nav .el-sub-menu__title:hover {
|
||
background: var(--ch-hover-soft);
|
||
}
|
||
|
||
/* 分组标题:比普通菜单项轻一档(分组是"容器",不是目的地) */
|
||
.ch-nav .el-sub-menu__title {
|
||
font-weight: 600;
|
||
color: var(--ch-text-secondary);
|
||
}
|
||
|
||
.ch-nav .el-sub-menu .el-menu {
|
||
background: transparent;
|
||
}
|
||
|
||
/* 选中项:强调色淡底 + 左侧强调条(WinUI 经典样式) */
|
||
.ch-nav .el-menu-item.is-active {
|
||
background: var(--ch-accent-soft);
|
||
color: var(--ch-text);
|
||
}
|
||
|
||
.ch-nav .el-menu-item.is-active::before {
|
||
content: '';
|
||
position: absolute;
|
||
left: 0;
|
||
top: 50%;
|
||
transform: translateY(-50%);
|
||
width: 3px;
|
||
height: 16px;
|
||
border-radius: 2px;
|
||
background: var(--ch-accent);
|
||
}
|
||
|
||
/* 折叠态:图标居中(el-menu collapse 自带收窄,这里压掉分组缩进) */
|
||
.ch-nav .el-menu--collapse .el-menu-item,
|
||
.ch-nav .el-menu--collapse .el-sub-menu__title {
|
||
margin: 2px 8px;
|
||
padding: 0 !important;
|
||
}
|
||
|
||
/*
|
||
* 折叠态:把菜单文字**彻底不渲染**,只留图标槽。
|
||
*
|
||
* Element Plus 自己的隐藏方式是"把 span 的宽高压成 0 + overflow",一旦外层样式
|
||
* (缩进、margin、transition)稍有干扰,文字就会以 0 宽溢出的形式留在画面上 ——
|
||
* 表现为折叠后菜单里残留"半截字"。这里直接 display:none,从根上消除残影。
|
||
* 图标槽(.menu-icon-slot)必须保留(它同时承载未读红点)。
|
||
*/
|
||
.ch-nav .el-menu--collapse .el-menu-item > span:not(.menu-icon-slot),
|
||
.ch-nav .el-menu--collapse .el-sub-menu__title > span:not(.menu-icon-slot) {
|
||
display: none !important;
|
||
}
|
||
|
||
/* 折叠态:图标在 64px 栏内严格水平居中,且与顶部的汉堡按钮同一竖列(中心都在 32px) */
|
||
.ch-nav .el-menu--collapse .el-menu-item,
|
||
.ch-nav .el-menu--collapse .el-sub-menu__title {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
/*
|
||
* 折叠态点分组图标会弹出子菜单浮层 —— 它被 teleport 到 body 下,
|
||
* 不受 .ch-nav 作用域影响,所以这些规则写在全局;不写的话深色主题下会是浅色浮层。
|
||
*/
|
||
.el-menu--popup {
|
||
background: var(--ch-layer-solid);
|
||
border: 1px solid var(--ch-border);
|
||
border-radius: var(--ch-radius-card);
|
||
box-shadow: var(--ch-shadow-flyout);
|
||
}
|
||
|
||
.el-menu--popup .el-menu-item {
|
||
color: var(--ch-text);
|
||
}
|
||
|
||
.el-menu--popup .el-menu-item:hover {
|
||
background: var(--ch-hover-soft);
|
||
}
|
||
|
||
.el-menu--popup .el-menu-item.is-active {
|
||
background: var(--ch-accent-soft);
|
||
color: var(--ch-text);
|
||
}
|
||
|
||
/* ---------------------------------------------------------------- 课表时间轴 */
|
||
|
||
.timetable-cell {
|
||
min-height: 30px;
|
||
}
|
||
|
||
.timetable-course {
|
||
font-weight: 600;
|
||
}
|
||
|
||
.timetable-meta {
|
||
font-size: 12px;
|
||
color: var(--ch-text-tertiary);
|
||
}
|
||
|
||
.unread-dot {
|
||
display: inline-block;
|
||
width: 8px;
|
||
height: 8px;
|
||
border-radius: 50%;
|
||
background: #d13438;
|
||
margin-right: 6px;
|
||
}
|
||
|
||
/* ---------------------------------------------------------------- 滚动条 */
|
||
|
||
::-webkit-scrollbar {
|
||
width: 10px;
|
||
height: 10px;
|
||
}
|
||
|
||
::-webkit-scrollbar-thumb {
|
||
background: var(--ch-scrollbar);
|
||
border: 3px solid transparent;
|
||
background-clip: content-box;
|
||
border-radius: 8px;
|
||
}
|
||
|
||
::-webkit-scrollbar-thumb:hover {
|
||
background: var(--ch-scrollbar-hover);
|
||
background-clip: content-box;
|
||
}
|
||
|
||
::-webkit-scrollbar-track {
|
||
background: transparent;
|
||
}
|