style(console): 配色收敛为「简约白 / 高级灰」两套中性灰阶主题
原配色是深色 + 品牌青绿(#63e2b7)+ naive-ui 默认绿主色 —— 颜色种类多。 改为两套**只用中性灰阶**的主题,侧栏底部一行切换,选择存 localStorage: 简约白 页底 #f4f4f5 / 侧栏 #fafafa / 卡片 #ffffff 高级灰 页底 #1d1f22 / 侧栏 #181a1d / 卡片 #24262a 几处刻意的选择: 1. 白主题页底**不是纯白**。卡片本身是白的,两者同色就只能靠 1px 分线撑层次, 整页发「糊」;压一档浅灰后层次为「页底 < 侧栏 < 卡片」,且仍是白为主。 2. 主色位(naive-ui 的 primary)改**中性灰** —— 主按钮、开关激活态、进度条、 勾选框全吃这一个值。原来它们是绿色/青绿,正是「配色不简单」的主因。 3. 语义色(成功/警告/错误/信息)**降饱和而非去掉**。这是运维工具, 「运行中 / 出错」必须一眼可辨,全部灰化会导致误读。故它们只出现在徽章与 告警这类**小面积**上,底色观感仍是「白 + 灰」。 4. **色值只定义一份**:src/theme/index.ts 的 Palette 既生成 naive-ui 的 themeOverrides,又由 App.vue 运行期注入成 --na-* CSS 变量。 若「TS 调色板」与「CSS 写死色值」各存一份,换主题时会**一半新一半旧、 不报错、类型也抓不到** —— 正是本轮要消灭的失效模式。 为此清掉散在 8 个文件里的 17 处写死色值(日志三流着色、代码块底色、 节点选中边框、历史悬停…),复扫后自定义样式里硬编码色为 0。 5. 主题**只存 localStorage**,不落 Rust 侧 settings:那套 Settings 是给 「仅服务模式」(无窗口、常驻托盘)也生效的偏好用的;主题是纯图形偏好, 没有窗口时毫无意义 —— 为一件纯前端的事去动 Rust 命令面与契约守卫不划算。 6. index.html 内联一小段脚本做**首帧防闪**(否则深色用户每次启动闪一帧白)。 此处**刻意重复 2 个底色值**,已在 index.html 与 theme/index.ts 两处注明 「改底色时同步」—— 因为在它执行之前没有任何模块可依赖; 除此之外全应用再无第二份色值。 验证:vue-tsc 0 错 | 契约守卫 6/6 | 单测 280/280 | .app 重打通过(92.22 MiB)| 两套主题均以真实采集数据回填渲染核对 —— 截图脚本顺带读回 --na-bg 与 body 背景, 确认主题真的切过去了,而不是「看起来像」。
This commit is contained in:
1 parent
591e44721a
commit
4e6eb32929
14 files changed
+719
-29
No files matched your search
@@ -10,6 +10,9 @@
|
||||
> 主题:**控制台「去命令行化」** —— 双击应用即用,启停/重启/更新/日志/托盘常驻/开机自启全部在界面里完成。
|
||||
> 目标原文:「不再打开命令行窗口,运行应用之后,有启动、停止、重启、检查更新等功能,
|
||||
> 有日志功能,可以后台运行和开机自启等等功能」。
|
||||
>
|
||||
> 末段另含两轮纯界面调整:**一律不使用图标**(emoji 在部分渲染器里退化成豆腐块)、
|
||||
> **配色收敛为「简约白 / 高级灰」两套中性灰阶主题**。
|
||||
|
||||
### 新增 —— 本机被控端托管(`apps/console/src-tauri/src/agent.rs`,约 560 行)
|
||||
|
||||
@@ -109,6 +112,28 @@
|
||||
> 它们是**程序输出内容**,会原样出现在「服务日志」页里,但不是界面元素;
|
||||
> ③ 控件的结构性部件(下拉箭头、折叠箭头、输入框清除钮、表格排序箭头)。
|
||||
|
||||
### 变更 —— 配色收敛为两套中性灰阶主题
|
||||
|
||||
- **两套可切换主题:`简约白`(浅)与 `高级灰`(深)**,侧栏底部一行切换,选择存 `localStorage`。
|
||||
两套都**只用中性灰阶**:去掉原先的青色品牌字(`#63e2b7`)与 naive-ui 默认的绿色主色,
|
||||
主按钮 / 开关激活态 / 进度条 / 勾选框统一改吃「主色位 = 中性灰」这一个值。
|
||||
- 白主题的**页底刻意不是 `#ffffff`**(`#f4f4f5`):卡片本身是白的,两者同色就只能靠 1px 分线撑层次,
|
||||
整页发「糊」;压一档浅灰后层次为「页底 < 侧栏 < 卡片」,且仍是白为主。
|
||||
- **语义色不是去掉,而是降饱和**(灰绿 / 灰琥珀 / 灰红 / 灰蓝)。
|
||||
这是运维工具,`运行中 / 出错` 必须一眼可辨,全部灰化会导致误读;故它们只出现在徽章与告警这类小面积上,
|
||||
底色观感仍是「白 + 灰」。严重程度在关闭图标后本就更依赖 `type` 的底色/边框。
|
||||
- **色值只有一份定义**(`src/theme/index.ts` 的 `Palette`):既生成 naive-ui 的 `themeOverrides`,
|
||||
又由 `App.vue` 在运行期注入成 `--na-*` CSS 变量供自定义样式使用。
|
||||
原因是若「TS 调色板」与「CSS 写死的色值」各存一份,换主题时会**一半新一半旧且不报错、类型也抓不到**
|
||||
—— 这正是本轮要消灭的失效模式。
|
||||
- **清掉 17 处写死的色值**(散在 8 个文件:日志三流着色、代码块/预览底色、节点选中边框、历史悬停等),
|
||||
全部改为 `var(--na-*)`。复扫后自定义样式里的硬编码色为 **0**。
|
||||
- **主题偏好只存 `localStorage`,不落 Rust 侧 settings**:那是给「仅服务模式」(无窗口、常驻托盘)
|
||||
也要生效的偏好用的,而主题是纯图形偏好 —— 没有窗口时毫无意义。为一件纯前端的事去动 Rust 命令面与契约守卫不划算。
|
||||
- **首帧防闪**:`index.html` 内联一段极小脚本,在 Vue 接管前先把上次主题的底色刷上,
|
||||
否则「高级灰」用户每次启动都会看到一帧刺眼的白。此处**刻意重复了 2 个底色值**(真值在 `theme/index.ts`),
|
||||
因为在此之前没有任何模块可依赖;已在两处都注明「改底色时同步」。
|
||||
|
||||
### 说明 —— 本轮**刻意不升版本号**
|
||||
|
||||
本轮新增的全部是**控制台**功能,**被控端能力集合未变**(未增删任何 `system.*` / `fs.*` / `app.*` 能力)。
|
||||
|
||||
@@ -4,6 +4,21 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>nodeagent 控制台</title>
|
||||
<script>
|
||||
// 首帧防闪:Vue 接管之前浏览器已经会画一次背景,若不先刷上——
|
||||
// 「高级灰」用户每次启动都会看到一帧刺眼的白。
|
||||
//
|
||||
// 此处只重复**两个**底色值(真值在 src/theme/index.ts),因为在此之前
|
||||
// 没有任何模块可依赖;其余所有颜色仍由 App.vue 运行期注入的 --na-* 负责。
|
||||
// 改调色板底色时记得同步这两行。
|
||||
try {
|
||||
var isGray = localStorage.getItem("nodeagent.console.theme") === "gray";
|
||||
document.documentElement.style.background = isGray ? "#1d1f22" : "#f4f4f5";
|
||||
document.documentElement.style.colorScheme = isGray ? "dark" : "light";
|
||||
} catch (e) {
|
||||
/* localStorage 不可用(隐私模式)时走默认底色即可 */
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
@@ -1,17 +1,40 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
NConfigProvider,
|
||||
NMessageProvider,
|
||||
NDialogProvider,
|
||||
darkTheme,
|
||||
zhCN,
|
||||
dateZhCN,
|
||||
} from "naive-ui";
|
||||
import { watchEffect } from "vue";
|
||||
import { NConfigProvider, NMessageProvider, NDialogProvider, zhCN, dateZhCN } from "naive-ui";
|
||||
import AppShell from "./components/AppShell.vue";
|
||||
import { cssVars } from "./theme";
|
||||
import { useThemeStore } from "./stores/theme";
|
||||
|
||||
const theme = useThemeStore();
|
||||
|
||||
/**
|
||||
* 把调色板写成 `--na-*` CSS 变量挂到根元素。
|
||||
*
|
||||
* 色值只在 `theme/index.ts` 定义一份,此处在运行期注入给自定义样式用 ——
|
||||
* 免得「TS 调色板」与「CSS 硬编码」各写一份后静默漂移(换主题时一半新一半旧,
|
||||
* 不报错、类型也抓不到)。
|
||||
*
|
||||
* 同时设 `color-scheme`:不设的话滚动条、`<input>` 等原生控件会一直是浅色,
|
||||
* 在「高级灰」下白得刺眼。
|
||||
*/
|
||||
watchEffect(() => {
|
||||
const { palette, name } = theme.preset;
|
||||
const root = document.documentElement;
|
||||
for (const [k, v] of Object.entries(cssVars(palette))) {
|
||||
root.style.setProperty(k, v);
|
||||
}
|
||||
root.style.colorScheme = palette.scheme;
|
||||
root.dataset.theme = name;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-config-provider :theme="darkTheme" :locale="zhCN" :date-locale="dateZhCN">
|
||||
<n-config-provider
|
||||
:theme="theme.preset.base"
|
||||
:theme-overrides="theme.preset.overrides"
|
||||
:locale="zhCN"
|
||||
:date-locale="dateZhCN"
|
||||
>
|
||||
<n-message-provider>
|
||||
<n-dialog-provider>
|
||||
<app-shell />
|
||||
|
||||
@@ -12,16 +12,25 @@ import {
|
||||
NSpace,
|
||||
NText,
|
||||
NTooltip,
|
||||
NRadioGroup,
|
||||
NRadioButton,
|
||||
type MenuOption,
|
||||
} from "naive-ui";
|
||||
import { navRoutes, type NavMeta } from "../router";
|
||||
import { useConnStore } from "../stores/conn";
|
||||
import { useAgentStore } from "../stores/agent";
|
||||
import { useThemeStore } from "../stores/theme";
|
||||
import { THEME_LIST } from "../theme";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const conn = useConnStore();
|
||||
const agent = useAgentStore();
|
||||
const theme = useThemeStore();
|
||||
const themeName = computed({
|
||||
get: () => theme.name,
|
||||
set: (v) => theme.set(v),
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
conn.bind();
|
||||
@@ -68,6 +77,15 @@ async function quickToggle() {
|
||||
</div>
|
||||
<n-menu :options="menuOptions" :value="activeKey" @update:value="onMenu" />
|
||||
<div class="sider-foot">
|
||||
<div class="foot-label">主题</div>
|
||||
<n-radio-group v-model:value="themeName" size="small" class="theme-switch">
|
||||
<n-radio-button v-for="t in THEME_LIST" :key="t.name" :value="t.name">
|
||||
{{ t.label }}
|
||||
</n-radio-button>
|
||||
</n-radio-group>
|
||||
|
||||
<div class="foot-sep"></div>
|
||||
|
||||
<div class="foot-row">
|
||||
<n-text depth="3" style="font-size: 12px">本机被控端</n-text>
|
||||
<n-tag :type="agent.meta.type" size="tiny" round>{{ agent.meta.label }}</n-tag>
|
||||
@@ -112,8 +130,12 @@ async function quickToggle() {
|
||||
</template>
|
||||
|
||||
<style>
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
/* 配色只走 `--na-*` 变量(值在 src/theme/index.ts 单点定义,App.vue 运行期注入),
|
||||
此处不留任何写死的色值 —— 否则换主题时会一半新一半旧,且不报错。
|
||||
`color-scheme` 也由 App.vue 按主题设,这里不重复声明。 */
|
||||
html,
|
||||
body {
|
||||
background: var(--na-bg);
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
@@ -124,12 +146,12 @@ body {
|
||||
.brand-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #63e2b7;
|
||||
color: var(--na-brand);
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
.brand-sub {
|
||||
font-size: 12px;
|
||||
opacity: 0.6;
|
||||
color: var(--na-text-3);
|
||||
margin-top: 2px;
|
||||
}
|
||||
.topbar {
|
||||
@@ -139,7 +161,7 @@ body {
|
||||
justify-content: space-between;
|
||||
padding: 0 16px;
|
||||
}
|
||||
/* 侧栏做成纵向弹性布局,才能把「本机服务」快捷块压到底部 */
|
||||
/* 侧栏做成纵向弹性布局,才能把底部快捷块压到底 */
|
||||
:deep(.n-layout-sider-scroll-container) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -147,7 +169,31 @@ body {
|
||||
.sider-foot {
|
||||
margin-top: auto;
|
||||
padding: 12px 14px 16px;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.09);
|
||||
border-top: 1px solid var(--na-border);
|
||||
}
|
||||
.foot-label {
|
||||
font-size: 11px;
|
||||
color: var(--na-text-3);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.foot-sep {
|
||||
height: 1px;
|
||||
background: var(--na-border);
|
||||
margin: 12px 0;
|
||||
}
|
||||
/* 两个主题按钮各占一半宽(侧栏只有 200px,默认内边距会挤出去) */
|
||||
.theme-switch {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
}
|
||||
.theme-switch :deep(.n-radio-button) {
|
||||
flex: 1;
|
||||
}
|
||||
.theme-switch :deep(.n-radio-button__label) {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
padding: 0 2px;
|
||||
}
|
||||
.foot-row {
|
||||
display: flex;
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { computed, ref, watch } from "vue";
|
||||
import {
|
||||
THEMES,
|
||||
THEME_STORAGE_KEY,
|
||||
normalizeThemeName,
|
||||
type ThemeName,
|
||||
} from "../theme";
|
||||
|
||||
/**
|
||||
* 主题选择。
|
||||
*
|
||||
* 只存 **localStorage**,不落 Rust 侧 settings:那是「仅服务模式」(无窗口、
|
||||
* 常驻托盘)也要生效的偏好,而主题是纯图形偏好 —— 没有窗口时毫无意义。
|
||||
* 放 localStorage 可避免为一件纯前端的事去动 Rust 命令面与契约守卫。
|
||||
*/
|
||||
export const useThemeStore = defineStore("theme", () => {
|
||||
const name = ref<ThemeName>(read());
|
||||
|
||||
const preset = computed(() => THEMES[name.value]);
|
||||
const isWhite = computed(() => name.value === "white");
|
||||
|
||||
function read(): ThemeName {
|
||||
try {
|
||||
return normalizeThemeName(localStorage.getItem(THEME_STORAGE_KEY));
|
||||
} catch {
|
||||
// 隐私模式等场景下 localStorage 可能不可用 —— 不该因此启动失败
|
||||
return "white";
|
||||
}
|
||||
}
|
||||
|
||||
function set(next: ThemeName) {
|
||||
name.value = normalizeThemeName(next);
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
set(name.value === "white" ? "gray" : "white");
|
||||
}
|
||||
|
||||
watch(
|
||||
name,
|
||||
(v) => {
|
||||
try {
|
||||
localStorage.setItem(THEME_STORAGE_KEY, v);
|
||||
} catch {
|
||||
/* 忽略:存不下只影响下次启动的默认值 */
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
return { name, preset, isWhite, set, toggle };
|
||||
});
|
||||
@@ -0,0 +1,478 @@
|
||||
import {
|
||||
darkTheme,
|
||||
lightTheme,
|
||||
type GlobalTheme,
|
||||
type GlobalThemeOverrides,
|
||||
} from "naive-ui";
|
||||
|
||||
/**
|
||||
* 控制台只有两套主题,且都**只用中性灰阶** —— 不含彩色品牌色。
|
||||
*
|
||||
* 「简约白」= 白底 + 浅灰分线,靠留白与层次而不是颜色区分区域。
|
||||
* 「高级灰」= 低饱和冷灰底 + 浅灰字,同样只用灰阶。
|
||||
*
|
||||
* 之所以真的把两套都做出来、而不是只留一套浅色:深色在长时间盯日志/终端时
|
||||
* 更省眼(运维场景常态),浅色在白天与投屏时更清楚。两套共用同一份灰阶逻辑
|
||||
* (只是明暗反转),故维护成本不翻倍。
|
||||
*/
|
||||
export type ThemeName = "white" | "gray";
|
||||
|
||||
export const THEME_STORAGE_KEY = "nodeagent.console.theme";
|
||||
|
||||
/**
|
||||
* 调色板。**这是全应用唯一一份色值定义**:
|
||||
* 既喂给 naive-ui 的 themeOverrides,也由 App.vue 注入成 `--na-*` CSS 变量
|
||||
* 供自定义样式使用。两处各写一份必然漂移(本项目吃过这类亏),故只此一处。
|
||||
*/
|
||||
export interface Palette {
|
||||
/** 页面底色 */
|
||||
bg: string;
|
||||
/** 侧栏底色(与 bg 拉开一档层次,而不是靠边框) */
|
||||
sider: string;
|
||||
/** 卡片 / 浮层底色 */
|
||||
card: string;
|
||||
/** 内嵌区:日志框、代码块、表头 */
|
||||
sunken: string;
|
||||
border: string;
|
||||
borderStrong: string;
|
||||
text1: string;
|
||||
text2: string;
|
||||
text3: string;
|
||||
hover: string;
|
||||
/** 主色位(naive-ui 的 primary)—— 刻意用中性色,不用彩色 */
|
||||
accent: string;
|
||||
accentHover: string;
|
||||
accentPressed: string;
|
||||
/** 主色底之上的文字色 */
|
||||
accentText: string;
|
||||
/** 品牌字(侧栏站点标题),不再用青绿 */
|
||||
brand: string;
|
||||
/** 语义色:统一降饱和,保证「白 + 灰」的底色观感 */
|
||||
ok: string;
|
||||
warn: string;
|
||||
err: string;
|
||||
info: string;
|
||||
/** 语义色的浅底(用于 Tag / 告警块) */
|
||||
okSoft: string;
|
||||
warnSoft: string;
|
||||
errSoft: string;
|
||||
infoSoft: string;
|
||||
tooltipBg: string;
|
||||
tooltipText: string;
|
||||
scrollbar: string;
|
||||
scrollbarHover: string;
|
||||
shadow: string;
|
||||
/** 原生控件(滚动条、表单控件)的明暗 */
|
||||
scheme: "light" | "dark";
|
||||
}
|
||||
|
||||
/** 简约白:白为主,灰为辅。层次靠灰阶(页底 < 侧栏 < 卡片),不靠颜色。 */
|
||||
const WHITE: Palette = {
|
||||
// 页底刻意不是 #ffffff:卡片也是白的,两者同色就只能靠 1px 分线撑层次,
|
||||
// 整页会发"糊"。压一档浅灰后卡片自然浮起来,且仍是「白」为主。
|
||||
bg: "#f4f4f5",
|
||||
sider: "#fafafa",
|
||||
card: "#ffffff",
|
||||
sunken: "#f5f5f6",
|
||||
border: "#e8e8ea",
|
||||
borderStrong: "#d6d6da",
|
||||
text1: "#1a1a1c",
|
||||
text2: "#55555c",
|
||||
text3: "#8e8e97",
|
||||
hover: "#eeeeef",
|
||||
accent: "#1f1f23",
|
||||
accentHover: "#33333a",
|
||||
accentPressed: "#0d0d10",
|
||||
accentText: "#ffffff",
|
||||
brand: "#1a1a1c",
|
||||
ok: "#4f8768",
|
||||
warn: "#9c7c39",
|
||||
err: "#b4635c",
|
||||
info: "#5b789a",
|
||||
okSoft: "#eef4f0",
|
||||
warnSoft: "#f7f3e9",
|
||||
errSoft: "#f8efee",
|
||||
infoSoft: "#eef2f7",
|
||||
tooltipBg: "#2b2b30",
|
||||
tooltipText: "#f2f2f3",
|
||||
scrollbar: "#d6d6da",
|
||||
scrollbarHover: "#bcbcc2",
|
||||
shadow: "0 1px 2px rgba(16, 16, 20, 0.05)",
|
||||
scheme: "light",
|
||||
};
|
||||
|
||||
/** 高级灰:雾霾冷灰调。低饱和、低对比,但保证正文可读。 */
|
||||
const GRAY: Palette = {
|
||||
bg: "#1d1f22",
|
||||
sider: "#181a1d",
|
||||
card: "#24262a",
|
||||
sunken: "#1a1c1f",
|
||||
border: "#32353a",
|
||||
borderStrong: "#41454c",
|
||||
text1: "#dcdee2",
|
||||
text2: "#a2a6ae",
|
||||
text3: "#71757c",
|
||||
hover: "#2b2e33",
|
||||
accent: "#dcdee2",
|
||||
accentHover: "#eef0f2",
|
||||
accentPressed: "#c6c9ce",
|
||||
accentText: "#1d1f22",
|
||||
brand: "#e6e8eb",
|
||||
ok: "#83a891",
|
||||
warn: "#b99a5c",
|
||||
err: "#cc8b82",
|
||||
info: "#8098b5",
|
||||
okSoft: "#262f2a",
|
||||
warnSoft: "#2f2c22",
|
||||
errSoft: "#312625",
|
||||
infoSoft: "#252b33",
|
||||
tooltipBg: "#3a3e44",
|
||||
tooltipText: "#e8eaed",
|
||||
scrollbar: "#3a3e44",
|
||||
scrollbarHover: "#4a4f56",
|
||||
shadow: "0 1px 2px rgba(0, 0, 0, 0.35)",
|
||||
scheme: "dark",
|
||||
};
|
||||
|
||||
/**
|
||||
* 调色板 → `--na-*` CSS 变量。
|
||||
*
|
||||
* 名字写死而不是自动 kebab 化:自动转换一旦改字段名,CSS 侧会**静默**失配
|
||||
* (变量没人定义 → 颜色落回继承值,不报错、类型也抓不到)。写死则改字段时
|
||||
* 至少能在这里一眼看到。
|
||||
*/
|
||||
export function cssVars(p: Palette): Record<string, string> {
|
||||
return {
|
||||
"--na-bg": p.bg,
|
||||
"--na-sider": p.sider,
|
||||
"--na-card": p.card,
|
||||
"--na-sunken": p.sunken,
|
||||
"--na-border": p.border,
|
||||
"--na-border-strong": p.borderStrong,
|
||||
"--na-text-1": p.text1,
|
||||
"--na-text-2": p.text2,
|
||||
"--na-text-3": p.text3,
|
||||
"--na-hover": p.hover,
|
||||
"--na-accent": p.accent,
|
||||
"--na-accent-text": p.accentText,
|
||||
"--na-brand": p.brand,
|
||||
"--na-ok": p.ok,
|
||||
"--na-warn": p.warn,
|
||||
"--na-err": p.err,
|
||||
"--na-info": p.info,
|
||||
"--na-tooltip-bg": p.tooltipBg,
|
||||
"--na-tooltip-text": p.tooltipText,
|
||||
"--na-scrollbar": p.scrollbar,
|
||||
"--na-scrollbar-hover": p.scrollbarHover,
|
||||
"--na-shadow": p.shadow,
|
||||
};
|
||||
}
|
||||
|
||||
function overridesOf(p: Palette): GlobalThemeOverrides {
|
||||
return {
|
||||
common: {
|
||||
// —— 主色:中性灰。naive-ui 默认主色是绿色,留着它整套配色立刻"不简单"。
|
||||
// 主按钮、开关激活态、进度条、勾选框都吃这一个值。
|
||||
primaryColor: p.accent,
|
||||
primaryColorHover: p.accentHover,
|
||||
primaryColorPressed: p.accentPressed,
|
||||
primaryColorSuppl: p.accentHover,
|
||||
|
||||
// —— 语义色降饱和(仍保留:运维工具里"运行中/出错"必须一眼可辨,
|
||||
// 全部灰化会导致误读;故只在徽章与告警这类小面积上出现)
|
||||
successColor: p.ok,
|
||||
successColorHover: p.ok,
|
||||
successColorPressed: p.ok,
|
||||
successColorSuppl: p.ok,
|
||||
warningColor: p.warn,
|
||||
warningColorHover: p.warn,
|
||||
warningColorPressed: p.warn,
|
||||
warningColorSuppl: p.warn,
|
||||
errorColor: p.err,
|
||||
errorColorHover: p.err,
|
||||
errorColorPressed: p.err,
|
||||
errorColorSuppl: p.err,
|
||||
infoColor: p.info,
|
||||
infoColorHover: p.info,
|
||||
infoColorPressed: p.info,
|
||||
infoColorSuppl: p.info,
|
||||
|
||||
textColorBase: p.text1,
|
||||
textColor1: p.text1,
|
||||
textColor2: p.text2,
|
||||
textColor3: p.text3,
|
||||
placeholderColor: p.text3,
|
||||
placeholderColorDisabled: p.text3,
|
||||
iconColor: p.text3,
|
||||
iconColorHover: p.text2,
|
||||
|
||||
dividerColor: p.border,
|
||||
borderColor: p.border,
|
||||
closeIconColor: p.text3,
|
||||
|
||||
bodyColor: p.bg,
|
||||
cardColor: p.card,
|
||||
modalColor: p.card,
|
||||
popoverColor: p.card,
|
||||
tableColor: p.card,
|
||||
inputColor: p.sunken,
|
||||
inputColorDisabled: p.sunken,
|
||||
tagColor: p.sunken,
|
||||
actionColor: p.sunken,
|
||||
avatarColor: p.sunken,
|
||||
codeColor: p.sunken,
|
||||
tabColor: p.sunken,
|
||||
tableHeaderColor: p.sunken,
|
||||
hoverColor: p.hover,
|
||||
tableColorHover: p.hover,
|
||||
tableColorStriped: p.sunken,
|
||||
pressedColor: p.hover,
|
||||
progressRailColor: p.sunken,
|
||||
railColor: p.sunken,
|
||||
scrollbarColor: p.scrollbar,
|
||||
scrollbarColorHover: p.scrollbarHover,
|
||||
|
||||
borderRadius: "6px",
|
||||
borderRadiusSmall: "4px",
|
||||
},
|
||||
|
||||
Layout: {
|
||||
color: p.bg,
|
||||
siderColor: p.sider,
|
||||
headerColor: p.bg,
|
||||
siderBorderColor: p.border,
|
||||
headerBorderColor: p.border,
|
||||
textColor: p.text1,
|
||||
},
|
||||
|
||||
Menu: {
|
||||
color: p.sider,
|
||||
itemTextColor: p.text2,
|
||||
itemTextColorHover: p.text1,
|
||||
itemTextColorActive: p.text1,
|
||||
itemTextColorActiveHover: p.text1,
|
||||
itemTextColorChildActive: p.text1,
|
||||
itemColorHover: p.hover,
|
||||
itemColorActive: p.hover,
|
||||
itemColorActiveHover: p.hover,
|
||||
itemColorActiveCollapsed: p.hover,
|
||||
itemIconColor: p.text3,
|
||||
itemIconColorHover: p.text2,
|
||||
itemIconColorActive: p.text1,
|
||||
groupTextColor: p.text3,
|
||||
arrowColor: p.text3,
|
||||
borderColorHorizontal: p.border,
|
||||
borderRadius: "6px",
|
||||
},
|
||||
|
||||
Card: {
|
||||
color: p.card,
|
||||
colorModal: p.card,
|
||||
colorPopover: p.card,
|
||||
colorEmbedded: p.sunken,
|
||||
textColor: p.text1,
|
||||
titleTextColor: p.text1,
|
||||
borderColor: p.border,
|
||||
actionColor: p.sunken,
|
||||
titleFontWeight: "600",
|
||||
boxShadow: p.shadow,
|
||||
},
|
||||
|
||||
DataTable: {
|
||||
thColor: p.sunken,
|
||||
thColorHover: p.hover,
|
||||
tdColor: p.card,
|
||||
tdColorHover: p.hover,
|
||||
tdColorStriped: p.sunken,
|
||||
borderColor: p.border,
|
||||
thTextColor: p.text2,
|
||||
tdTextColor: p.text2,
|
||||
thFontWeight: "600",
|
||||
loadingColor: p.accent,
|
||||
},
|
||||
|
||||
Input: {
|
||||
color: p.card,
|
||||
colorFocus: p.card,
|
||||
colorDisabled: p.sunken,
|
||||
textColor: p.text1,
|
||||
textColorDisabled: p.text3,
|
||||
placeholderColor: p.text3,
|
||||
border: `1px solid ${p.border}`,
|
||||
borderHover: `1px solid ${p.borderStrong}`,
|
||||
borderFocus: `1px solid ${p.accent}`,
|
||||
borderDisabled: `1px solid ${p.border}`,
|
||||
boxShadowFocus: `0 0 0 2px ${p.hover}`,
|
||||
caretColor: p.accent,
|
||||
iconColor: p.text3,
|
||||
clearColor: p.text3,
|
||||
clearColorHover: p.text2,
|
||||
},
|
||||
|
||||
Alert: {
|
||||
color: p.sunken,
|
||||
border: `1px solid ${p.border}`,
|
||||
titleTextColor: p.text1,
|
||||
contentTextColor: p.text2,
|
||||
iconColor: p.text3,
|
||||
borderInfo: `1px solid ${p.info}`,
|
||||
colorInfo: p.infoSoft,
|
||||
titleTextColorInfo: p.text1,
|
||||
contentTextColorInfo: p.text2,
|
||||
iconColorInfo: p.info,
|
||||
borderSuccess: `1px solid ${p.ok}`,
|
||||
colorSuccess: p.okSoft,
|
||||
titleTextColorSuccess: p.text1,
|
||||
contentTextColorSuccess: p.text2,
|
||||
iconColorSuccess: p.ok,
|
||||
borderWarning: `1px solid ${p.warn}`,
|
||||
colorWarning: p.warnSoft,
|
||||
titleTextColorWarning: p.text1,
|
||||
contentTextColorWarning: p.text2,
|
||||
iconColorWarning: p.warn,
|
||||
borderError: `1px solid ${p.err}`,
|
||||
colorError: p.errSoft,
|
||||
titleTextColorError: p.text1,
|
||||
contentTextColorError: p.text2,
|
||||
iconColorError: p.err,
|
||||
},
|
||||
|
||||
Tag: {
|
||||
color: p.sunken,
|
||||
textColor: p.text2,
|
||||
border: "1px solid transparent",
|
||||
colorBordered: p.sunken,
|
||||
borderRadius: "5px",
|
||||
colorPrimary: p.accent,
|
||||
textColorPrimary: p.accentText,
|
||||
borderPrimary: "1px solid transparent",
|
||||
colorInfo: p.infoSoft,
|
||||
textColorInfo: p.info,
|
||||
borderInfo: `1px solid ${p.info}`,
|
||||
colorSuccess: p.okSoft,
|
||||
textColorSuccess: p.ok,
|
||||
borderSuccess: `1px solid ${p.ok}`,
|
||||
colorWarning: p.warnSoft,
|
||||
textColorWarning: p.warn,
|
||||
borderWarning: `1px solid ${p.warn}`,
|
||||
colorError: p.errSoft,
|
||||
textColorError: p.err,
|
||||
borderError: `1px solid ${p.err}`,
|
||||
},
|
||||
|
||||
Divider: {
|
||||
color: p.border,
|
||||
textColor: p.text3,
|
||||
},
|
||||
|
||||
Progress: {
|
||||
railColor: p.sunken,
|
||||
fillColor: p.accent,
|
||||
fillColorSuccess: p.ok,
|
||||
fillColorWarning: p.warn,
|
||||
fillColorError: p.err,
|
||||
fillColorInfo: p.info,
|
||||
textColorLineInner: p.accentText,
|
||||
textColorLineOuter: p.text2,
|
||||
},
|
||||
|
||||
Switch: {
|
||||
railColor: p.borderStrong,
|
||||
railColorActive: p.accent,
|
||||
buttonColor: p.card,
|
||||
iconColor: p.text3,
|
||||
loadingColor: p.text3,
|
||||
},
|
||||
|
||||
Statistic: {
|
||||
labelTextColor: p.text3,
|
||||
valueTextColor: p.text1,
|
||||
valuePrefixTextColor: p.text2,
|
||||
valueSuffixTextColor: p.text2,
|
||||
},
|
||||
|
||||
Empty: {
|
||||
textColor: p.text3,
|
||||
iconColor: p.text3,
|
||||
extraTextColor: p.text3,
|
||||
},
|
||||
|
||||
Spin: {
|
||||
color: p.accent,
|
||||
textColor: p.text2,
|
||||
},
|
||||
|
||||
Popover: {
|
||||
color: p.card,
|
||||
textColor: p.text1,
|
||||
dividerColor: p.border,
|
||||
boxShadow: "0 4px 16px rgba(0, 0, 0, 0.12)",
|
||||
},
|
||||
|
||||
Tooltip: {
|
||||
color: p.tooltipBg,
|
||||
textColor: p.tooltipText,
|
||||
},
|
||||
|
||||
Modal: {
|
||||
color: p.card,
|
||||
textColor: p.text1,
|
||||
},
|
||||
|
||||
List: {
|
||||
color: p.card,
|
||||
textColor: p.text1,
|
||||
borderColor: p.border,
|
||||
},
|
||||
|
||||
Collapse: {
|
||||
titleTextColor: p.text1,
|
||||
textColor: p.text2,
|
||||
dividerColor: p.border,
|
||||
},
|
||||
|
||||
Checkbox: {
|
||||
labelTextColor: p.text1,
|
||||
color: p.card,
|
||||
border: `1px solid ${p.borderStrong}`,
|
||||
borderChecked: `1px solid ${p.accent}`,
|
||||
colorChecked: p.accent,
|
||||
checkMarkColor: p.accentText,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export interface ThemePreset {
|
||||
name: ThemeName;
|
||||
label: string;
|
||||
desc: string;
|
||||
base: GlobalTheme;
|
||||
palette: Palette;
|
||||
overrides: GlobalThemeOverrides;
|
||||
}
|
||||
|
||||
export const THEMES: Record<ThemeName, ThemePreset> = {
|
||||
white: {
|
||||
name: "white",
|
||||
label: "简约白",
|
||||
desc: "白底浅灰分线",
|
||||
base: lightTheme,
|
||||
palette: WHITE,
|
||||
overrides: overridesOf(WHITE),
|
||||
},
|
||||
gray: {
|
||||
name: "gray",
|
||||
label: "高级灰",
|
||||
desc: "雾霾冷灰低对比",
|
||||
base: darkTheme,
|
||||
palette: GRAY,
|
||||
overrides: overridesOf(GRAY),
|
||||
},
|
||||
};
|
||||
|
||||
export const THEME_LIST: ThemePreset[] = [THEMES.white, THEMES.gray];
|
||||
|
||||
/** 越界 / 脏数据一律落回默认主题,避免启动即白屏。 */
|
||||
export function normalizeThemeName(v: unknown): ThemeName {
|
||||
return v === "gray" ? "gray" : "white";
|
||||
}
|
||||
@@ -228,7 +228,7 @@ onMounted(() => {
|
||||
margin: 12px 0 0;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
background: var(--na-sunken);
|
||||
font-family: ui-monospace, Menlo, monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
|
||||
@@ -415,9 +415,9 @@ function openRelease() {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.log-line.stderr .txt {
|
||||
color: #e88080;
|
||||
color: var(--na-err);
|
||||
}
|
||||
.log-line.system .txt {
|
||||
color: #63e2b7;
|
||||
color: var(--na-info);
|
||||
}
|
||||
</style>
|
||||
@@ -137,7 +137,7 @@ function streamType(s: string) {
|
||||
height: calc(100vh - 300px);
|
||||
min-height: 320px;
|
||||
overflow: auto;
|
||||
background: rgba(0, 0, 0, 0.18);
|
||||
background: var(--na-sunken);
|
||||
border-radius: 6px;
|
||||
padding: 10px 12px;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
@@ -160,10 +160,10 @@ function streamType(s: string) {
|
||||
margin-top: 1px;
|
||||
}
|
||||
.txt.stderr {
|
||||
color: #e88080;
|
||||
color: var(--na-err);
|
||||
}
|
||||
.txt.system {
|
||||
color: #63e2b7;
|
||||
color: var(--na-info);
|
||||
}
|
||||
.empty {
|
||||
opacity: 0.5;
|
||||
|
||||
@@ -195,11 +195,11 @@ const capGroups = computed(() => {
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.09);
|
||||
border: 1px solid var(--na-border);
|
||||
border-radius: 8px;
|
||||
}
|
||||
.node.active {
|
||||
border-color: #63e2b7;
|
||||
border-color: var(--na-accent);
|
||||
}
|
||||
.node-name {
|
||||
font-size: 14px;
|
||||
@@ -219,7 +219,7 @@ const capGroups = computed(() => {
|
||||
font-size: 12px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
background: var(--na-sunken);
|
||||
}
|
||||
.logs {
|
||||
font-family: ui-monospace, Menlo, monospace;
|
||||
|
||||
@@ -102,7 +102,7 @@ onUnmounted(stop);
|
||||
type="line"
|
||||
:percentage="Math.round(status?.cpu_pct ?? 0)"
|
||||
:height="10"
|
||||
:color="(status?.cpu_pct ?? 0) > 80 ? '#e88080' : '#63e2b7'"
|
||||
:color="(status?.cpu_pct ?? 0) > 80 ? 'var(--na-err)' : 'var(--na-ok)'"
|
||||
/>
|
||||
</div>
|
||||
<div class="gauge">
|
||||
@@ -113,7 +113,7 @@ onUnmounted(stop);
|
||||
type="line"
|
||||
:percentage="Math.round(status?.memory_pct ?? 0)"
|
||||
:height="10"
|
||||
:color="(status?.memory_pct ?? 0) > 85 ? '#e88080' : '#63e2b7'"
|
||||
:color="(status?.memory_pct ?? 0) > 85 ? 'var(--na-err)' : 'var(--na-ok)'"
|
||||
/>
|
||||
</div>
|
||||
<n-statistic
|
||||
@@ -138,7 +138,7 @@ onUnmounted(stop);
|
||||
type="line"
|
||||
:percentage="Math.round(d.used_pct ?? 0)"
|
||||
:height="8"
|
||||
:color="(d.used_pct ?? 0) > 90 ? '#e88080' : '#63e2b7'"
|
||||
:color="(d.used_pct ?? 0) > 90 ? 'var(--na-err)' : 'var(--na-ok)'"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -175,7 +175,7 @@ onMounted(() => {
|
||||
margin: 12px 0 0;
|
||||
padding: 10px;
|
||||
border-radius: 8px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
background: var(--na-sunken);
|
||||
font-size: 12px;
|
||||
max-height: 200px;
|
||||
overflow: auto;
|
||||
|
||||
@@ -161,6 +161,6 @@ function reuse(c: string) {
|
||||
opacity: 0.85;
|
||||
}
|
||||
.hist:hover code {
|
||||
color: #63e2b7;
|
||||
color: var(--na-accent);
|
||||
}
|
||||
</style>
|
||||
@@ -572,6 +572,56 @@ node 去跑打包内的 sidecar,并走一次真实 JSON-RPC 握手 —— 源
|
||||
> 该测试**已注入故障实测会红**(改 `agent_start` → `agent_startX`、`na-agent-state` → `na-agent-statee`
|
||||
> 当场 2 条失败),非空转守卫。
|
||||
|
||||
## 界面观感约定(2026-10-11)
|
||||
|
||||
两轮纯界面调整,方向都是「少即是多」。理由记下来,免得日后被当成"没做完"再补回去。
|
||||
|
||||
### 一、一律不使用图标
|
||||
|
||||
触发原因是一次真实的渲染事故:侧栏的 emoji 图标在非系统字体栈的渲染器里退化成**豆腐块**。
|
||||
试过三法,分别无效或更糟 —— 给图标指定 `Apple Color Emoji`、全局字体栈加 `!important`、
|
||||
乃至 `font-variant-emoji: emoji`(最后一招把 ASCII 数字也拆成了 emoji 呈现)。
|
||||
「哪套字体栈一定带 emoji」跨平台并不成立 —— **与其赌字形,不如不用**。
|
||||
|
||||
去掉的范围比"看得见的图标"宽得多(下面这三类都算):
|
||||
|
||||
| 类别 | 处数 | 关法 |
|
||||
|---|---|---|
|
||||
| 侧栏 emoji | 9 | 路由表 `meta.icon` 字段**整体删除**,`NavMeta` 接口同步收窄 |
|
||||
| `n-empty` 空态**插画** | **16** | `:show-icon="false"` —— 插画比图标更「图形」,散在 9 个页面,**最易漏** |
|
||||
| `n-alert` 类型图标 | 7 | naive-ui 的 `showIcon` **默认就是 `true`**,不显式关它就一直都在 |
|
||||
| `n-popconfirm` 询问图标 | 3 | 同上 |
|
||||
| 文案里的符号 | 1 | 终端页「⚠ 高权限操作,请谨慎」保留纯文字 |
|
||||
|
||||
顺带清掉 `public/{vite,tauri}.svg` 与 `src/assets/vue.svg`:`public/` 下的文件会被**整体复制进产物**,
|
||||
此前等于把 Vite / Tauri 的 logo 一并发了出去(且 `index.html` 的 favicon 正指向它)。
|
||||
`index.html` 的 `<title>` 还是模板默认的「Tauri + Vue + Typescript App」,也一并改齐
|
||||
(原生窗口标题取自 `tauri.conf.json`,故此前只是 dev 态标签页不一致)。
|
||||
|
||||
**刻意未动**:① 托盘图标 —— 菜单栏没图标就点不到,关窗后应用无法寻回,属**功能必需**;
|
||||
② 被控端 CLI 自己打印的文本标记(`⚠️`/`ℹ️`/`✅`)—— 是**程序输出内容**,会原样出现在「服务日志」页里,
|
||||
但不是界面元素;③ 控件的结构性部件(下拉/折叠箭头、输入框清除钮、表格排序箭头)。
|
||||
|
||||
### 二、配色收敛为两套中性灰阶主题
|
||||
|
||||
`简约白`(浅)/`高级灰`(深),侧栏底部一行切换,选择存 `localStorage`。
|
||||
|
||||
| 决定 | 理由 |
|
||||
|---|---|
|
||||
| **主色位改中性灰** | naive-ui 默认主色是**绿色**、原设计的品牌字是**青绿 `#63e2b7`** —— 都是"颜色",与「简单些」相悖。主按钮/开关激活/进度条/勾选框统一吃这一个值 |
|
||||
| 白主题**页底不是纯白**(`#f4f4f5`) | 卡片本身是白的,两者同色就只能靠 1px 分线撑层次,整页发「糊」。压一档浅灰后层次为「页底 < 侧栏 < 卡片」,且仍是白为主 |
|
||||
| 语义色**降饱和**而非去掉 | 这是运维工具,「运行中 / 出错」必须一眼可辨,全部灰化会导致误读。故它们只出现在徽章与告警这类**小面积**上,底色观感仍是「白 + 灰」 |
|
||||
| **色值只定义一份** | `src/theme/index.ts` 的 `Palette` 同时喂 naive-ui 的 `themeOverrides` 与 `--na-*` CSS 变量(`App.vue` 运行期注入)。两处各写一份,换主题时会**一半新一半旧且不报错、类型也抓不到** |
|
||||
| 主题**只存 `localStorage`**,不落 Rust settings | Rust 那套 `Settings` 是给「仅服务模式」(无窗口、常驻托盘)也要生效的偏好用的;主题是纯图形偏好,**没有窗口时毫无意义**。为一件纯前端的事去动 Rust 命令面与契约守卫不划算 |
|
||||
|
||||
清掉散在 **8 个文件**里的 **17 处写死色值**(日志三流着色、代码块/预览底色、节点选中边框、历史悬停…),
|
||||
全部改为 `var(--na-*)`。复扫后自定义样式里的硬编码色为 **0**。
|
||||
|
||||
**首帧防闪**:`index.html` 内联一小段脚本,在 Vue 接管前先把上次主题的底色刷上
|
||||
(否则「高级灰」用户每次启动都会看到一帧刺眼的白)。
|
||||
此处**刻意重复了 2 个底色值**,已在 `index.html` 与 `theme/index.ts` 两处注明「改底色时同步」——
|
||||
因为在它执行之前没有任何模块可依赖。除此之外全应用再无第二份色值。
|
||||
|
||||
### 本轮验证记录
|
||||
|
||||
| 条目 | 结果 |
|
||||
|
||||
Reference in new issue
Block a user