fix: 修复新版前端夜间模式
This commit is contained in:
@@ -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:对齐新版前端主体布局并修复全局拖拽默认行为
|
## 2026-05-15:对齐新版前端主体布局并修复全局拖拽默认行为
|
||||||
|
|
||||||
- 新版 Vue 前端主体内容继续按旧版 `frontend_old/` 的工作区布局对齐:上传页保留旧版上传区尺寸和文件列表结构,数据库页恢复左侧表列表 + 右侧数据区,设置页恢复左右列,脚本页恢复编辑器容器和底部状态栏。
|
- 新版 Vue 前端主体内容继续按旧版 `frontend_old/` 的工作区布局对齐:上传页保留旧版上传区尺寸和文件列表结构,数据库页恢复左侧表列表 + 右侧数据区,设置页恢复左右列,脚本页恢复编辑器容器和底部状态栏。
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<n-config-provider>
|
<n-config-provider :theme="naiveTheme">
|
||||||
<n-message-provider>
|
<n-message-provider>
|
||||||
<n-dialog-provider>
|
<n-dialog-provider>
|
||||||
<AppShell />
|
<AppShell />
|
||||||
@@ -9,6 +9,11 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import AppShell from './AppShell.vue';
|
import { computed } from 'vue';
|
||||||
</script>
|
import { darkTheme } from 'naive-ui';
|
||||||
|
|
||||||
|
import AppShell from './AppShell.vue';
|
||||||
|
import { themeName } from './composables/theme';
|
||||||
|
|
||||||
|
const naiveTheme = computed(() => (themeName.value === 'dark' ? darkTheme : undefined));
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ import { apiPost, clearToken, getToken, setToken, setUnauthorizedHandler } from
|
|||||||
import type { LoginResponse } from './types';
|
import type { LoginResponse } from './types';
|
||||||
import LoginView from './components/LoginView.vue';
|
import LoginView from './components/LoginView.vue';
|
||||||
import { pageHeader, resetPageHeader, resolveHeaderValue, type PageHeaderAction } from './composables/pageHeader';
|
import { pageHeader, resetPageHeader, resolveHeaderValue, type PageHeaderAction } from './composables/pageHeader';
|
||||||
|
import { toggleAppTheme } from './composables/theme';
|
||||||
|
|
||||||
const message = useMessage();
|
const message = useMessage();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
@@ -105,7 +106,6 @@ const router = useRouter();
|
|||||||
const token = ref(getToken());
|
const token = ref(getToken());
|
||||||
const loginLoading = ref(false);
|
const loginLoading = ref(false);
|
||||||
const sidebarCollapsed = ref(localStorage.getItem('sidebarCollapsed') === 'true');
|
const sidebarCollapsed = ref(localStorage.getItem('sidebarCollapsed') === 'true');
|
||||||
const theme = ref(localStorage.getItem('theme') || 'light');
|
|
||||||
const menuKeys = ['workflow', 'history', 'database', 'script', 'settings'] as const;
|
const menuKeys = ['workflow', 'history', 'database', 'script', 'settings'] as const;
|
||||||
type MenuKey = (typeof menuKeys)[number];
|
type MenuKey = (typeof menuKeys)[number];
|
||||||
|
|
||||||
@@ -126,8 +126,6 @@ const currentTitle = computed(() => {
|
|||||||
});
|
});
|
||||||
const pageSubtitle = computed(() => resolveHeaderValue(pageHeader.subtitle || '', ''));
|
const pageSubtitle = computed(() => resolveHeaderValue(pageHeader.subtitle || '', ''));
|
||||||
|
|
||||||
document.documentElement.setAttribute('data-theme', theme.value);
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
window.addEventListener('dragover', preventWindowFileDrop, { capture: true });
|
window.addEventListener('dragover', preventWindowFileDrop, { capture: true });
|
||||||
window.addEventListener('drop', preventWindowFileDrop, { capture: true });
|
window.addEventListener('drop', preventWindowFileDrop, { capture: true });
|
||||||
@@ -173,9 +171,7 @@ function toggleSidebar() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function toggleTheme() {
|
function toggleTheme() {
|
||||||
theme.value = theme.value === 'dark' ? 'light' : 'dark';
|
toggleAppTheme();
|
||||||
localStorage.setItem('theme', theme.value);
|
|
||||||
document.documentElement.setAttribute('data-theme', theme.value);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function restartService() {
|
async function restartService() {
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
export type AppTheme = 'light' | 'dark';
|
||||||
|
|
||||||
|
const THEME_KEY = 'theme';
|
||||||
|
|
||||||
|
export const themeName = ref<AppTheme>(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.
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -210,6 +210,11 @@ select {
|
|||||||
transition: all var(--td-transition);
|
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 {
|
.app-menu .n-menu-item-content:hover {
|
||||||
color: var(--td-text-color-primary);
|
color: var(--td-text-color-primary);
|
||||||
background: var(--td-bg-color-container-hover);
|
background: var(--td-bg-color-container-hover);
|
||||||
@@ -371,8 +376,14 @@ select {
|
|||||||
border-bottom: 1px solid var(--td-border-color-light);
|
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 {
|
.work-card > .n-card__content {
|
||||||
padding: 16px 20px;
|
padding: 16px 20px;
|
||||||
|
color: var(--td-text-color-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.work-card > .n-card__footer {
|
.work-card > .n-card__footer {
|
||||||
@@ -381,6 +392,11 @@ select {
|
|||||||
border-top: 1px solid var(--td-border-color-light);
|
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 {
|
.login-page {
|
||||||
display: grid;
|
display: grid;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
|
|||||||
Reference in New Issue
Block a user