diff --git a/docs/project_context.md b/docs/project_context.md
index 8ed5dce..c269215 100644
--- a/docs/project_context.md
+++ b/docs/project_context.md
@@ -1,5 +1,13 @@
# 项目上下文记录
+## 2026-05-15:修复新版前端夜间模式组件颜色
+
+- 新增 `frontend/src/composables/theme.ts` 作为前端共享主题状态,统一读写 `localStorage.theme` 并同步 `document.documentElement[data-theme]`。
+- `App.vue` 的 `n-config-provider` 现在会在夜间模式下使用 Naive UI `darkTheme`,修复 `n-card`、`n-input`、`n-form`、`n-menu`、`n-button` 等组件仍按亮色主题渲染的问题。
+- `AppShell.vue` 的主题切换改为调用共享主题逻辑,避免 AppShell 和 Naive Provider 各自维护主题状态。
+- `styles.css` 补充工作卡片、卡片标题、表单标签、菜单图标和菜单文字颜色兜底,防止局部组件样式覆盖暗色文本。
+- 已执行 `npm run build`,构建通过,仅有 Monaco/Vite 大 chunk 体积警告;已用 headless Chrome 打开新版 `/settings` 并预置 `theme=dark` 验证:页面、卡片标题、表单标签、输入框文字、字段映射标题和顶部按钮均为暗色主题可读颜色。
+
## 2026-05-15:对齐新版前端主体布局并修复全局拖拽默认行为
- 新版 Vue 前端主体内容继续按旧版 `frontend_old/` 的工作区布局对齐:上传页保留旧版上传区尺寸和文件列表结构,数据库页恢复左侧表列表 + 右侧数据区,设置页恢复左右列,脚本页恢复编辑器容器和底部状态栏。
diff --git a/frontend/src/App.vue b/frontend/src/App.vue
index 0dec5ba..552dd99 100644
--- a/frontend/src/App.vue
+++ b/frontend/src/App.vue
@@ -1,5 +1,5 @@
-
+
@@ -9,6 +9,11 @@
+import { computed } from 'vue';
+import { darkTheme } from 'naive-ui';
+import AppShell from './AppShell.vue';
+import { themeName } from './composables/theme';
+
+const naiveTheme = computed(() => (themeName.value === 'dark' ? darkTheme : undefined));
+
diff --git a/frontend/src/AppShell.vue b/frontend/src/AppShell.vue
index 4f1bd53..7583381 100644
--- a/frontend/src/AppShell.vue
+++ b/frontend/src/AppShell.vue
@@ -98,6 +98,7 @@ import { apiPost, clearToken, getToken, setToken, setUnauthorizedHandler } from
import type { LoginResponse } from './types';
import LoginView from './components/LoginView.vue';
import { pageHeader, resetPageHeader, resolveHeaderValue, type PageHeaderAction } from './composables/pageHeader';
+import { toggleAppTheme } from './composables/theme';
const message = useMessage();
const route = useRoute();
@@ -105,7 +106,6 @@ const router = useRouter();
const token = ref(getToken());
const loginLoading = ref(false);
const sidebarCollapsed = ref(localStorage.getItem('sidebarCollapsed') === 'true');
-const theme = ref(localStorage.getItem('theme') || 'light');
const menuKeys = ['workflow', 'history', 'database', 'script', 'settings'] as const;
type MenuKey = (typeof menuKeys)[number];
@@ -126,8 +126,6 @@ const currentTitle = computed(() => {
});
const pageSubtitle = computed(() => resolveHeaderValue(pageHeader.subtitle || '', ''));
-document.documentElement.setAttribute('data-theme', theme.value);
-
onMounted(() => {
window.addEventListener('dragover', preventWindowFileDrop, { capture: true });
window.addEventListener('drop', preventWindowFileDrop, { capture: true });
@@ -173,9 +171,7 @@ function toggleSidebar() {
}
function toggleTheme() {
- theme.value = theme.value === 'dark' ? 'light' : 'dark';
- localStorage.setItem('theme', theme.value);
- document.documentElement.setAttribute('data-theme', theme.value);
+ toggleAppTheme();
}
async function restartService() {
diff --git a/frontend/src/composables/theme.ts b/frontend/src/composables/theme.ts
new file mode 100644
index 0000000..6b8dfab
--- /dev/null
+++ b/frontend/src/composables/theme.ts
@@ -0,0 +1,40 @@
+import { ref } from 'vue';
+
+export type AppTheme = 'light' | 'dark';
+
+const THEME_KEY = 'theme';
+
+export const themeName = ref(readStoredTheme());
+
+applyTheme(themeName.value);
+
+export function setAppTheme(nextTheme: AppTheme) {
+ themeName.value = nextTheme;
+ applyTheme(nextTheme);
+}
+
+export function toggleAppTheme() {
+ setAppTheme(themeName.value === 'dark' ? 'light' : 'dark');
+}
+
+function readStoredTheme(): AppTheme {
+ try {
+ const stored = typeof localStorage === 'undefined' ? null : localStorage.getItem(THEME_KEY);
+ return stored === 'dark' ? 'dark' : 'light';
+ } catch {
+ return 'light';
+ }
+}
+
+function applyTheme(nextTheme: AppTheme) {
+ if (typeof document !== 'undefined') {
+ document.documentElement.setAttribute('data-theme', nextTheme);
+ }
+ try {
+ if (typeof localStorage !== 'undefined') {
+ localStorage.setItem(THEME_KEY, nextTheme);
+ }
+ } catch {
+ // Ignore storage failures so theme switching never blocks the UI.
+ }
+}
diff --git a/frontend/src/styles.css b/frontend/src/styles.css
index b97391d..00c70b0 100644
--- a/frontend/src/styles.css
+++ b/frontend/src/styles.css
@@ -210,6 +210,11 @@ select {
transition: all var(--td-transition);
}
+.app-menu .n-menu-item-content-header,
+.app-menu .n-menu-item-content__icon {
+ color: inherit;
+}
+
.app-menu .n-menu-item-content:hover {
color: var(--td-text-color-primary);
background: var(--td-bg-color-container-hover);
@@ -371,8 +376,14 @@ select {
border-bottom: 1px solid var(--td-border-color-light);
}
+.work-card > .n-card-header .n-card-header__main,
+.work-card > .n-card-header .n-card-header__extra {
+ color: var(--td-text-color-primary);
+}
+
.work-card > .n-card__content {
padding: 16px 20px;
+ color: var(--td-text-color-primary);
}
.work-card > .n-card__footer {
@@ -381,6 +392,11 @@ select {
border-top: 1px solid var(--td-border-color-light);
}
+.work-card .n-form-item-label,
+.work-card .n-form-item-label__text {
+ color: var(--td-text-color-secondary);
+}
+
.login-page {
display: grid;
min-height: 100vh;