✨ Added theme switching
This commit is contained in:
@@ -109,11 +109,19 @@ export class AppearanceConfig {
|
|||||||
*/
|
*/
|
||||||
"language": LanguageType;
|
"language": LanguageType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编辑器主题
|
||||||
|
*/
|
||||||
|
"theme": ThemeType;
|
||||||
|
|
||||||
/** Creates a new AppearanceConfig instance. */
|
/** Creates a new AppearanceConfig instance. */
|
||||||
constructor($$source: Partial<AppearanceConfig> = {}) {
|
constructor($$source: Partial<AppearanceConfig> = {}) {
|
||||||
if (!("language" in $$source)) {
|
if (!("language" in $$source)) {
|
||||||
this["language"] = ("" as LanguageType);
|
this["language"] = ("" as LanguageType);
|
||||||
}
|
}
|
||||||
|
if (!("theme" in $$source)) {
|
||||||
|
this["theme"] = ("" as ThemeType);
|
||||||
|
}
|
||||||
|
|
||||||
Object.assign(this, $$source);
|
Object.assign(this, $$source);
|
||||||
}
|
}
|
||||||
@@ -513,6 +521,76 @@ export enum TabType {
|
|||||||
TabTypeTab = "tab",
|
TabTypeTab = "tab",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ThemeType 主题类型定义
|
||||||
|
*/
|
||||||
|
export enum ThemeType {
|
||||||
|
/**
|
||||||
|
* The Go zero value for the underlying type of the enum.
|
||||||
|
*/
|
||||||
|
$zero = "",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ThemeDefaultDark 默认深色主题
|
||||||
|
*/
|
||||||
|
ThemeDefaultDark = "default-dark",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ThemeDracula Dracula主题
|
||||||
|
*/
|
||||||
|
ThemeDracula = "dracula",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ThemeAura Aura主题
|
||||||
|
*/
|
||||||
|
ThemeAura = "aura",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ThemeGithubDark GitHub深色主题
|
||||||
|
*/
|
||||||
|
ThemeGithubDark = "github-dark",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ThemeGithubLight GitHub浅色主题
|
||||||
|
*/
|
||||||
|
ThemeGithubLight = "github-light",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ThemeMaterialDark Material深色主题
|
||||||
|
*/
|
||||||
|
ThemeMaterialDark = "material-dark",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ThemeMaterialLight Material浅色主题
|
||||||
|
*/
|
||||||
|
ThemeMaterialLight = "material-light",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ThemeSolarizedDark Solarized深色主题
|
||||||
|
*/
|
||||||
|
ThemeSolarizedDark = "solarized-dark",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ThemeSolarizedLight Solarized浅色主题
|
||||||
|
*/
|
||||||
|
ThemeSolarizedLight = "solarized-light",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ThemeTokyoNight Tokyo Night主题
|
||||||
|
*/
|
||||||
|
ThemeTokyoNight = "tokyo-night",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ThemeTokyoNightStorm Tokyo Night Storm主题
|
||||||
|
*/
|
||||||
|
ThemeTokyoNightStorm = "tokyo-night-storm",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ThemeTokyoNightDay Tokyo Night Day主题
|
||||||
|
*/
|
||||||
|
ThemeTokyoNightDay = "tokyo-night-day",
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* UpdatesConfig 更新设置配置
|
* UpdatesConfig 更新设置配置
|
||||||
*/
|
*/
|
||||||
|
189
frontend/src/composables/useEditorTheme.ts
Normal file
189
frontend/src/composables/useEditorTheme.ts
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
import { ref, computed, shallowRef } from 'vue';
|
||||||
|
import { Extension, Compartment } from '@codemirror/state';
|
||||||
|
import { EditorView } from '@codemirror/view';
|
||||||
|
import type { ThemeType } from '@/types';
|
||||||
|
|
||||||
|
// 主题加载器类型
|
||||||
|
type ThemeLoader = () => Promise<Extension>;
|
||||||
|
|
||||||
|
// 默认主题常量
|
||||||
|
const DEFAULT_THEME = 'default-dark' as ThemeType;
|
||||||
|
|
||||||
|
// 主题加载映射
|
||||||
|
const themeLoaderMap = new Map<string, ThemeLoader>();
|
||||||
|
|
||||||
|
// 初始化主题加载器
|
||||||
|
const initThemeLoaders = () => {
|
||||||
|
themeLoaderMap.set('default-dark', () => import('@/views/editor/theme/default-dark').then(m => m.defaultDark));
|
||||||
|
themeLoaderMap.set('dracula', () => import('@/views/editor/theme/dracula').then(m => m.dracula));
|
||||||
|
themeLoaderMap.set('aura', () => import('@/views/editor/theme/aura').then(m => m.aura));
|
||||||
|
themeLoaderMap.set('github-dark', () => import('@/views/editor/theme/github-dark').then(m => m.githubDark));
|
||||||
|
themeLoaderMap.set('github-light', () => import('@/views/editor/theme/github-light').then(m => m.githubLight));
|
||||||
|
themeLoaderMap.set('material-dark', () => import('@/views/editor/theme/material-dark').then(m => m.materialDark));
|
||||||
|
themeLoaderMap.set('material-light', () => import('@/views/editor/theme/material-light').then(m => m.materialLight));
|
||||||
|
themeLoaderMap.set('solarized-dark', () => import('@/views/editor/theme/solarized-dark').then(m => m.solarizedDark));
|
||||||
|
themeLoaderMap.set('solarized-light', () => import('@/views/editor/theme/solarized-light').then(m => m.solarizedLight));
|
||||||
|
themeLoaderMap.set('tokyo-night', () => import('@/views/editor/theme/tokyo-night').then(m => m.tokyoNight));
|
||||||
|
themeLoaderMap.set('tokyo-night-storm', () => import('@/views/editor/theme/tokyo-night-storm').then(m => m.tokyoNightStorm));
|
||||||
|
themeLoaderMap.set('tokyo-night-day', () => import('@/views/editor/theme/tokyo-night-day').then(m => m.tokyoNightDay));
|
||||||
|
};
|
||||||
|
|
||||||
|
// 延迟初始化
|
||||||
|
initThemeLoaders();
|
||||||
|
|
||||||
|
// 全局状态
|
||||||
|
const currentTheme = ref<ThemeType>(DEFAULT_THEME);
|
||||||
|
const themeCompartment = new Compartment();
|
||||||
|
const themeCache = new Map<ThemeType, Extension>();
|
||||||
|
const failedThemes = new Set<ThemeType>(); // 记录加载失败的主题
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编辑器主题管理
|
||||||
|
*/
|
||||||
|
export function useEditorTheme() {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 安全加载主题扩展
|
||||||
|
*/
|
||||||
|
const loadTheme = async (targetTheme: ThemeType): Promise<Extension> => {
|
||||||
|
// 1. 从缓存快速返回
|
||||||
|
const cached = themeCache.get(targetTheme);
|
||||||
|
if (cached) return cached;
|
||||||
|
|
||||||
|
// 2. 检查是否已知失败的主题,避免重复尝试
|
||||||
|
if (failedThemes.has(targetTheme) && targetTheme !== DEFAULT_THEME) {
|
||||||
|
console.info(`Theme ${targetTheme} is known to fail, attempting default theme directly`);
|
||||||
|
return attemptLoadTheme(DEFAULT_THEME).catch(() => [] as Extension);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 使用 try-catch 链和 nullish coalescing,替代递归
|
||||||
|
const result = await attemptLoadTheme(targetTheme)
|
||||||
|
.catch(async (error) => {
|
||||||
|
// 仅当目标主题不是默认主题时,才尝试默认主题
|
||||||
|
if (targetTheme !== DEFAULT_THEME) {
|
||||||
|
console.warn(`Theme ${targetTheme} failed, fallback to ${DEFAULT_THEME}:`, error);
|
||||||
|
return attemptLoadTheme(DEFAULT_THEME).catch((fallbackError) => {
|
||||||
|
console.error(`Fallback theme ${DEFAULT_THEME} also failed:`, fallbackError);
|
||||||
|
return [] as Extension; // 最终回退到空扩展
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// 如果默认主题也失败了,返回空扩展
|
||||||
|
console.error(`Default theme ${DEFAULT_THEME} failed:`, error);
|
||||||
|
return [] as Extension;
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 单纯的主题加载尝试 - 不处理回退逻辑
|
||||||
|
*/
|
||||||
|
const attemptLoadTheme = async (themeType: ThemeType): Promise<Extension> => {
|
||||||
|
// 获取加载器,使用 optional chaining 和 nullish coalescing
|
||||||
|
const loader = themeLoaderMap.get(themeType);
|
||||||
|
|
||||||
|
if (!loader) {
|
||||||
|
const error = new Error(`Theme loader not found: ${themeType}`);
|
||||||
|
failedThemes.add(themeType);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const extension = await loader();
|
||||||
|
|
||||||
|
// 缓存成功加载的主题
|
||||||
|
themeCache.set(themeType, extension);
|
||||||
|
// 从失败列表中移除(如果存在)
|
||||||
|
failedThemes.delete(themeType);
|
||||||
|
|
||||||
|
return extension;
|
||||||
|
} catch (error) {
|
||||||
|
// 记录失败的主题
|
||||||
|
failedThemes.add(themeType);
|
||||||
|
console.error(`Failed to load theme: ${themeType}`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建可配置的主题扩展
|
||||||
|
*/
|
||||||
|
const createThemeExtension = async (themeType: ThemeType): Promise<Extension> => {
|
||||||
|
const extension = await loadTheme(themeType);
|
||||||
|
currentTheme.value = themeType;
|
||||||
|
return themeCompartment.of(extension);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新编辑器主题 - 使用防抖和错误处理
|
||||||
|
*/
|
||||||
|
const updateTheme = async (view: EditorView, themeType: ThemeType): Promise<void> => {
|
||||||
|
// 使用可选链操作符检查 view
|
||||||
|
if (!view?.dispatch || themeType === currentTheme.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const extension = await loadTheme(themeType);
|
||||||
|
|
||||||
|
// 使用 try-catch 包装 dispatch,避免编辑器状态异常
|
||||||
|
try {
|
||||||
|
view.dispatch({
|
||||||
|
effects: themeCompartment.reconfigure(extension)
|
||||||
|
});
|
||||||
|
currentTheme.value = themeType;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to dispatch theme update:', error);
|
||||||
|
throw error; // 重新抛出,让调用者处理
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量预加载主题 - 使用 Promise.allSettled 确保部分失败不影响其他
|
||||||
|
*/
|
||||||
|
const preloadThemes = async (themes: ThemeType[]): Promise<PromiseSettledResult<Extension>[]> => {
|
||||||
|
const uniqueThemes = [...new Set(themes)]; // 去重
|
||||||
|
return Promise.allSettled(
|
||||||
|
uniqueThemes.map(theme => loadTheme(theme))
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重置主题系统状态
|
||||||
|
*/
|
||||||
|
const resetThemeSystem = (): void => {
|
||||||
|
themeCache.clear();
|
||||||
|
failedThemes.clear();
|
||||||
|
currentTheme.value = DEFAULT_THEME;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取主题系统状态信息
|
||||||
|
*/
|
||||||
|
const getThemeSystemInfo = () => ({
|
||||||
|
currentTheme: currentTheme.value,
|
||||||
|
cachedThemes: Array.from(themeCache.keys()),
|
||||||
|
failedThemes: Array.from(failedThemes),
|
||||||
|
availableThemes: Array.from(themeLoaderMap.keys()),
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
// 状态
|
||||||
|
currentTheme: computed(() => currentTheme.value),
|
||||||
|
|
||||||
|
// 核心方法
|
||||||
|
createThemeExtension,
|
||||||
|
updateTheme,
|
||||||
|
loadTheme,
|
||||||
|
|
||||||
|
// 批量操作
|
||||||
|
preloadThemes,
|
||||||
|
|
||||||
|
// 工具方法
|
||||||
|
resetThemeSystem,
|
||||||
|
getThemeSystemInfo,
|
||||||
|
|
||||||
|
// 缓存管理
|
||||||
|
clearCache: () => themeCache.clear(),
|
||||||
|
clearFailedThemes: () => failedThemes.clear(),
|
||||||
|
};
|
||||||
|
}
|
59
frontend/src/composables/useTheme.ts
Normal file
59
frontend/src/composables/useTheme.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import { useConfigStore } from '@/stores/configStore';
|
||||||
|
import { useEditorTheme } from './useEditorTheme';
|
||||||
|
import type { ThemeType } from '@/types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 主题管理 - 用于设置页面
|
||||||
|
*/
|
||||||
|
export function useTheme() {
|
||||||
|
const configStore = useConfigStore();
|
||||||
|
const { preloadThemes, getThemeSystemInfo } = useEditorTheme();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置主题 - 同时更新配置和预览
|
||||||
|
*/
|
||||||
|
const setTheme = async (themeType: ThemeType): Promise<void> => {
|
||||||
|
try {
|
||||||
|
// 更新配置存储(这会自动触发编辑器主题更新)
|
||||||
|
await configStore.setTheme(themeType);
|
||||||
|
|
||||||
|
console.info(`Theme switched to: ${themeType}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to set theme:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 预加载常用主题
|
||||||
|
*/
|
||||||
|
const preloadCommonThemes = async (): Promise<void> => {
|
||||||
|
const commonThemes: ThemeType[] = [
|
||||||
|
'default-dark' as ThemeType,
|
||||||
|
'dracula' as ThemeType,
|
||||||
|
'github-dark' as ThemeType,
|
||||||
|
'material-dark' as ThemeType
|
||||||
|
];
|
||||||
|
|
||||||
|
try {
|
||||||
|
await preloadThemes(commonThemes);
|
||||||
|
console.info('Common themes preloaded successfully');
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Some themes failed to preload:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取主题状态
|
||||||
|
*/
|
||||||
|
const getThemeStatus = () => ({
|
||||||
|
current: configStore.config.appearance.theme,
|
||||||
|
...getThemeSystemInfo()
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
setTheme,
|
||||||
|
preloadCommonThemes,
|
||||||
|
getThemeStatus,
|
||||||
|
};
|
||||||
|
}
|
@@ -29,7 +29,9 @@ export default {
|
|||||||
alwaysOnTopFailed: 'Failed to set window always on top',
|
alwaysOnTopFailed: 'Failed to set window always on top',
|
||||||
alwaysOnTopSuccess: 'Window always on top status updated',
|
alwaysOnTopSuccess: 'Window always on top status updated',
|
||||||
languageChanged: 'Language setting updated',
|
languageChanged: 'Language setting updated',
|
||||||
languageChangeFailed: 'Failed to update language setting'
|
languageChangeFailed: 'Failed to update language setting',
|
||||||
|
themeChanged: 'Theme setting updated',
|
||||||
|
themeChangeFailed: 'Failed to update theme setting'
|
||||||
},
|
},
|
||||||
languages: {
|
languages: {
|
||||||
'zh-CN': '简体中文',
|
'zh-CN': '简体中文',
|
||||||
@@ -97,6 +99,8 @@ export default {
|
|||||||
tabs: 'Tabs',
|
tabs: 'Tabs',
|
||||||
enableTabIndent: 'Enable Tab Indent',
|
enableTabIndent: 'Enable Tab Indent',
|
||||||
language: 'Interface Language',
|
language: 'Interface Language',
|
||||||
|
theme: 'Editor Theme',
|
||||||
|
themeDescription: 'Choose editor theme',
|
||||||
restartRequired: '(Restart required)',
|
restartRequired: '(Restart required)',
|
||||||
saveOptions: 'Save Options',
|
saveOptions: 'Save Options',
|
||||||
autoSaveDelay: 'Auto Save Delay (ms)',
|
autoSaveDelay: 'Auto Save Delay (ms)',
|
||||||
|
@@ -29,7 +29,9 @@ export default {
|
|||||||
alwaysOnTopFailed: '无法设置窗口置顶状态',
|
alwaysOnTopFailed: '无法设置窗口置顶状态',
|
||||||
alwaysOnTopSuccess: '窗口置顶状态已更新',
|
alwaysOnTopSuccess: '窗口置顶状态已更新',
|
||||||
languageChanged: '语言设置已更新',
|
languageChanged: '语言设置已更新',
|
||||||
languageChangeFailed: '语言设置更新失败'
|
languageChangeFailed: '语言设置更新失败',
|
||||||
|
themeChanged: '主题设置已更新',
|
||||||
|
themeChangeFailed: '主题设置更新失败'
|
||||||
},
|
},
|
||||||
languages: {
|
languages: {
|
||||||
'zh-CN': '简体中文',
|
'zh-CN': '简体中文',
|
||||||
@@ -97,6 +99,8 @@ export default {
|
|||||||
tabs: '制表符',
|
tabs: '制表符',
|
||||||
enableTabIndent: '启用 Tab 缩进',
|
enableTabIndent: '启用 Tab 缩进',
|
||||||
language: '界面语言',
|
language: '界面语言',
|
||||||
|
theme: '编辑器主题',
|
||||||
|
themeDescription: '选择编辑器主题',
|
||||||
restartRequired: '(需要重启)',
|
restartRequired: '(需要重启)',
|
||||||
saveOptions: '保存选项',
|
saveOptions: '保存选项',
|
||||||
autoSaveDelay: '自动保存延迟(毫秒)',
|
autoSaveDelay: '自动保存延迟(毫秒)',
|
||||||
|
@@ -7,7 +7,8 @@ import {
|
|||||||
EditingConfig,
|
EditingConfig,
|
||||||
GeneralConfig,
|
GeneralConfig,
|
||||||
LanguageType,
|
LanguageType,
|
||||||
TabType
|
TabType,
|
||||||
|
ThemeType
|
||||||
} from '@/../bindings/voidraft/internal/models';
|
} from '@/../bindings/voidraft/internal/models';
|
||||||
import {useI18n} from 'vue-i18n';
|
import {useI18n} from 'vue-i18n';
|
||||||
import {useErrorHandler} from '@/utils/errorHandler';
|
import {useErrorHandler} from '@/utils/errorHandler';
|
||||||
@@ -64,7 +65,8 @@ const EDITING_CONFIG_KEY_MAP: EditingConfigKeyMap = {
|
|||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
const APPEARANCE_CONFIG_KEY_MAP: AppearanceConfigKeyMap = {
|
const APPEARANCE_CONFIG_KEY_MAP: AppearanceConfigKeyMap = {
|
||||||
language: 'appearance.language'
|
language: 'appearance.language',
|
||||||
|
theme: 'appearance.theme'
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
// 配置限制
|
// 配置限制
|
||||||
@@ -113,7 +115,7 @@ const getBrowserLanguage = (): SupportedLocaleType => {
|
|||||||
const DEFAULT_CONFIG: AppConfig = {
|
const DEFAULT_CONFIG: AppConfig = {
|
||||||
general: {
|
general: {
|
||||||
alwaysOnTop: false,
|
alwaysOnTop: false,
|
||||||
dataPath: './data',
|
dataPath: '',
|
||||||
enableGlobalHotkey: false,
|
enableGlobalHotkey: false,
|
||||||
globalHotkey: {
|
globalHotkey: {
|
||||||
ctrl: false,
|
ctrl: false,
|
||||||
@@ -134,7 +136,8 @@ const DEFAULT_CONFIG: AppConfig = {
|
|||||||
autoSaveDelay: 5000
|
autoSaveDelay: 5000
|
||||||
},
|
},
|
||||||
appearance: {
|
appearance: {
|
||||||
language: LanguageType.LangZhCN
|
language: LanguageType.LangZhCN,
|
||||||
|
theme: 'default-dark' as ThemeType
|
||||||
},
|
},
|
||||||
keyBindings: {},
|
keyBindings: {},
|
||||||
updates: {},
|
updates: {},
|
||||||
@@ -296,6 +299,13 @@ export const useConfigStore = defineStore('config', () => {
|
|||||||
}, 'config.languageChangeFailed', 'config.languageChanged');
|
}, 'config.languageChangeFailed', 'config.languageChanged');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 主题设置方法
|
||||||
|
const setTheme = async (theme: ThemeType): Promise<void> => {
|
||||||
|
await safeCall(async () => {
|
||||||
|
await updateAppearanceConfig('theme', theme);
|
||||||
|
}, 'config.themeChangeFailed', 'config.themeChanged');
|
||||||
|
};
|
||||||
|
|
||||||
// 初始化语言设置
|
// 初始化语言设置
|
||||||
const initializeLanguage = async (): Promise<void> => {
|
const initializeLanguage = async (): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
@@ -358,6 +368,9 @@ export const useConfigStore = defineStore('config', () => {
|
|||||||
setLanguage,
|
setLanguage,
|
||||||
initializeLanguage,
|
initializeLanguage,
|
||||||
|
|
||||||
|
// 主题相关方法
|
||||||
|
setTheme,
|
||||||
|
|
||||||
// 字体大小操作
|
// 字体大小操作
|
||||||
...adjusters.fontSize,
|
...adjusters.fontSize,
|
||||||
increaseFontSize: adjusters.fontSize.increase,
|
increaseFontSize: adjusters.fontSize.increase,
|
||||||
|
4
frontend/src/types/index.ts
Normal file
4
frontend/src/types/index.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
// 统一类型导出
|
||||||
|
export type { ThemeType, LanguageType } from '@/../bindings/voidraft/internal/models';
|
||||||
|
export * from './theme';
|
||||||
|
export * from './editor';
|
204
frontend/src/types/theme.ts
Normal file
204
frontend/src/types/theme.ts
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
import type { ThemeType } from '@/../bindings/voidraft/internal/models';
|
||||||
|
|
||||||
|
// 主题配置信息
|
||||||
|
export interface ThemeInfo {
|
||||||
|
id: ThemeType;
|
||||||
|
name: string;
|
||||||
|
displayName: string;
|
||||||
|
isDark: boolean;
|
||||||
|
previewColors: {
|
||||||
|
background: string;
|
||||||
|
foreground: string;
|
||||||
|
keyword: string;
|
||||||
|
string: string;
|
||||||
|
function: string;
|
||||||
|
comment: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 可用主题列表
|
||||||
|
export const AVAILABLE_THEMES: ThemeInfo[] = [
|
||||||
|
{
|
||||||
|
id: 'default-dark' as ThemeType,
|
||||||
|
name: 'default-dark',
|
||||||
|
displayName: '深色默认',
|
||||||
|
isDark: true,
|
||||||
|
previewColors: {
|
||||||
|
background: '#252B37',
|
||||||
|
foreground: '#9BB586',
|
||||||
|
keyword: '#FF79C6',
|
||||||
|
string: '#F1FA8C',
|
||||||
|
function: '#50FA7B',
|
||||||
|
comment: '#6272A4'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'dracula' as ThemeType,
|
||||||
|
name: 'dracula',
|
||||||
|
displayName: 'Dracula',
|
||||||
|
isDark: true,
|
||||||
|
previewColors: {
|
||||||
|
background: '#282A36',
|
||||||
|
foreground: '#F8F8F2',
|
||||||
|
keyword: '#FF79C6',
|
||||||
|
string: '#F1FA8C',
|
||||||
|
function: '#50FA7B',
|
||||||
|
comment: '#6272A4'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'aura' as ThemeType,
|
||||||
|
name: 'aura',
|
||||||
|
displayName: 'Aura',
|
||||||
|
isDark: true,
|
||||||
|
previewColors: {
|
||||||
|
background: '#21202e',
|
||||||
|
foreground: '#edecee',
|
||||||
|
keyword: '#a277ff',
|
||||||
|
string: '#61ffca',
|
||||||
|
function: '#ffca85',
|
||||||
|
comment: '#6d6d6d'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'github-dark' as ThemeType,
|
||||||
|
name: 'github-dark',
|
||||||
|
displayName: 'GitHub 深色',
|
||||||
|
isDark: true,
|
||||||
|
previewColors: {
|
||||||
|
background: '#24292e',
|
||||||
|
foreground: '#d1d5da',
|
||||||
|
keyword: '#f97583',
|
||||||
|
string: '#9ecbff',
|
||||||
|
function: '#79b8ff',
|
||||||
|
comment: '#6a737d'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'github-light' as ThemeType,
|
||||||
|
name: 'github-light',
|
||||||
|
displayName: 'GitHub 浅色',
|
||||||
|
isDark: false,
|
||||||
|
previewColors: {
|
||||||
|
background: '#fff',
|
||||||
|
foreground: '#444d56',
|
||||||
|
keyword: '#d73a49',
|
||||||
|
string: '#032f62',
|
||||||
|
function: '#005cc5',
|
||||||
|
comment: '#6a737d'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'material-dark' as ThemeType,
|
||||||
|
name: 'material-dark',
|
||||||
|
displayName: 'Material 深色',
|
||||||
|
isDark: true,
|
||||||
|
previewColors: {
|
||||||
|
background: '#263238',
|
||||||
|
foreground: '#EEFFFF',
|
||||||
|
keyword: '#C792EA',
|
||||||
|
string: '#C3E88D',
|
||||||
|
function: '#82AAFF',
|
||||||
|
comment: '#546E7A'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'material-light' as ThemeType,
|
||||||
|
name: 'material-light',
|
||||||
|
displayName: 'Material 浅色',
|
||||||
|
isDark: false,
|
||||||
|
previewColors: {
|
||||||
|
background: '#FAFAFA',
|
||||||
|
foreground: '#90A4AE',
|
||||||
|
keyword: '#7C4DFF',
|
||||||
|
string: '#91B859',
|
||||||
|
function: '#6182B8',
|
||||||
|
comment: '#90A4AE'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'solarized-dark' as ThemeType,
|
||||||
|
name: 'solarized-dark',
|
||||||
|
displayName: 'Solarized 深色',
|
||||||
|
isDark: true,
|
||||||
|
previewColors: {
|
||||||
|
background: '#002B36',
|
||||||
|
foreground: '#93A1A1',
|
||||||
|
keyword: '#859900',
|
||||||
|
string: '#2AA198',
|
||||||
|
function: '#268BD2',
|
||||||
|
comment: '#586E75'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'solarized-light' as ThemeType,
|
||||||
|
name: 'solarized-light',
|
||||||
|
displayName: 'Solarized 浅色',
|
||||||
|
isDark: false,
|
||||||
|
previewColors: {
|
||||||
|
background: '#FDF6E3',
|
||||||
|
foreground: '#586E75',
|
||||||
|
keyword: '#859900',
|
||||||
|
string: '#2AA198',
|
||||||
|
function: '#268BD2',
|
||||||
|
comment: '#93A1A1'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'tokyo-night' as ThemeType,
|
||||||
|
name: 'tokyo-night',
|
||||||
|
displayName: 'Tokyo Night',
|
||||||
|
isDark: true,
|
||||||
|
previewColors: {
|
||||||
|
background: '#1a1b26',
|
||||||
|
foreground: '#787c99',
|
||||||
|
keyword: '#bb9af7',
|
||||||
|
string: '#9ece6a',
|
||||||
|
function: '#7aa2f7',
|
||||||
|
comment: '#444b6a'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'tokyo-night-storm' as ThemeType,
|
||||||
|
name: 'tokyo-night-storm',
|
||||||
|
displayName: 'Tokyo Night Storm',
|
||||||
|
isDark: true,
|
||||||
|
previewColors: {
|
||||||
|
background: '#24283b',
|
||||||
|
foreground: '#7982a9',
|
||||||
|
keyword: '#bb9af7',
|
||||||
|
string: '#9ece6a',
|
||||||
|
function: '#7aa2f7',
|
||||||
|
comment: '#565f89'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'tokyo-night-day' as ThemeType,
|
||||||
|
name: 'tokyo-night-day',
|
||||||
|
displayName: 'Tokyo Night Day',
|
||||||
|
isDark: false,
|
||||||
|
previewColors: {
|
||||||
|
background: '#e1e2e7',
|
||||||
|
foreground: '#6a6f8e',
|
||||||
|
keyword: '#9854f1',
|
||||||
|
string: '#587539',
|
||||||
|
function: '#2e7de9',
|
||||||
|
comment: '#9da3c2'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// 根据主题ID获取主题信息
|
||||||
|
export function getThemeInfo(themeId: ThemeType): ThemeInfo | undefined {
|
||||||
|
return AVAILABLE_THEMES.find(theme => theme.id === themeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取所有深色主题
|
||||||
|
export function getDarkThemes(): ThemeInfo[] {
|
||||||
|
return AVAILABLE_THEMES.filter(theme => theme.isDark);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取所有浅色主题
|
||||||
|
export function getLightThemes(): ThemeInfo[] {
|
||||||
|
return AVAILABLE_THEMES.filter(theme => !theme.isDark);
|
||||||
|
}
|
@@ -18,7 +18,9 @@ import {
|
|||||||
createFontExtensionFromBackend,
|
createFontExtensionFromBackend,
|
||||||
updateFontConfig,
|
updateFontConfig,
|
||||||
} from './extensions';
|
} from './extensions';
|
||||||
|
import { useEditorTheme } from '@/composables/useEditorTheme';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import type { ThemeType } from '@/types';
|
||||||
import { DocumentService } from '../../../bindings/voidraft/internal/services';
|
import { DocumentService } from '../../../bindings/voidraft/internal/services';
|
||||||
import Toolbar from '@/components/toolbar/Toolbar.vue';
|
import Toolbar from '@/components/toolbar/Toolbar.vue';
|
||||||
|
|
||||||
@@ -27,6 +29,7 @@ const configStore = useConfigStore();
|
|||||||
const documentStore = useDocumentStore();
|
const documentStore = useDocumentStore();
|
||||||
const logStore = useLogStore();
|
const logStore = useLogStore();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
const { createThemeExtension, updateTheme } = useEditorTheme();
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
initialDoc: {
|
initialDoc: {
|
||||||
@@ -51,6 +54,11 @@ const createEditor = async () => {
|
|||||||
// 获取基本扩展
|
// 获取基本扩展
|
||||||
const basicExtensions = createBasicSetup();
|
const basicExtensions = createBasicSetup();
|
||||||
|
|
||||||
|
// 获取主题扩展
|
||||||
|
const themeExtension = await createThemeExtension(
|
||||||
|
configStore.config.appearance.theme || 'default-dark' as ThemeType
|
||||||
|
);
|
||||||
|
|
||||||
// 获取Tab相关扩展
|
// 获取Tab相关扩展
|
||||||
const tabExtensions = getTabExtensions(
|
const tabExtensions = getTabExtensions(
|
||||||
configStore.config.editing.tabSize,
|
configStore.config.editing.tabSize,
|
||||||
@@ -90,6 +98,7 @@ const createEditor = async () => {
|
|||||||
|
|
||||||
// 组合所有扩展
|
// 组合所有扩展
|
||||||
const extensions: Extension[] = [
|
const extensions: Extension[] = [
|
||||||
|
themeExtension,
|
||||||
...basicExtensions,
|
...basicExtensions,
|
||||||
...tabExtensions,
|
...tabExtensions,
|
||||||
fontExtension,
|
fontExtension,
|
||||||
@@ -185,6 +194,13 @@ watch([
|
|||||||
editorStore.applyFontSize();
|
editorStore.applyFontSize();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 监听主题变化
|
||||||
|
watch(() => configStore.config.appearance.theme, async (newTheme) => {
|
||||||
|
if (newTheme && editorStore.editorView) {
|
||||||
|
await updateTheme(editorStore.editorView as EditorView, newTheme);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
// 创建编辑器
|
// 创建编辑器
|
||||||
createEditor();
|
createEditor();
|
||||||
|
@@ -23,13 +23,10 @@ import {defaultKeymap, history, historyKeymap,} from '@codemirror/commands';
|
|||||||
import {highlightSelectionMatches, searchKeymap} from '@codemirror/search';
|
import {highlightSelectionMatches, searchKeymap} from '@codemirror/search';
|
||||||
import {autocompletion, closeBrackets, closeBracketsKeymap, completionKeymap} from '@codemirror/autocomplete';
|
import {autocompletion, closeBrackets, closeBracketsKeymap, completionKeymap} from '@codemirror/autocomplete';
|
||||||
import {lintKeymap} from '@codemirror/lint';
|
import {lintKeymap} from '@codemirror/lint';
|
||||||
import {customHighlightActiveLine, defaultDark} from '@/views/editor/theme/default-dark';
|
|
||||||
|
|
||||||
// 基本编辑器设置,包含常用扩展
|
// 基本编辑器设置,包含常用扩展
|
||||||
export const createBasicSetup = (): Extension[] => {
|
export const createBasicSetup = (): Extension[] => {
|
||||||
return [
|
return [
|
||||||
// 主题相关
|
|
||||||
defaultDark,
|
|
||||||
|
|
||||||
// 基础UI
|
// 基础UI
|
||||||
lineNumbers(),
|
lineNumbers(),
|
||||||
@@ -46,7 +43,6 @@ export const createBasicSetup = (): Extension[] => {
|
|||||||
|
|
||||||
// 选择与高亮
|
// 选择与高亮
|
||||||
drawSelection(),
|
drawSelection(),
|
||||||
customHighlightActiveLine,
|
|
||||||
highlightActiveLine(),
|
highlightActiveLine(),
|
||||||
highlightSelectionMatches(),
|
highlightSelectionMatches(),
|
||||||
rectangularSelection(),
|
rectangularSelection(),
|
||||||
|
@@ -2,14 +2,18 @@
|
|||||||
import { useConfigStore } from '@/stores/configStore';
|
import { useConfigStore } from '@/stores/configStore';
|
||||||
import { useErrorHandler } from '@/utils/errorHandler';
|
import { useErrorHandler } from '@/utils/errorHandler';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { ref } from 'vue';
|
import { ref, computed, watch } from 'vue';
|
||||||
import SettingSection from '../components/SettingSection.vue';
|
import SettingSection from '../components/SettingSection.vue';
|
||||||
import SettingItem from '../components/SettingItem.vue';
|
import SettingItem from '../components/SettingItem.vue';
|
||||||
import { LanguageType } from '../../../../bindings/voidraft/internal/models/models';
|
import type { ThemeType } from '@/types';
|
||||||
|
import { LanguageType } from '@/../bindings/voidraft/internal/models';
|
||||||
|
import { AVAILABLE_THEMES } from '@/types/theme';
|
||||||
|
import { useTheme } from '@/composables/useTheme';
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const configStore = useConfigStore();
|
const configStore = useConfigStore();
|
||||||
const { safeCall } = useErrorHandler();
|
const { safeCall } = useErrorHandler();
|
||||||
|
const { setTheme: setThemeComposable } = useTheme();
|
||||||
|
|
||||||
// 语言选项
|
// 语言选项
|
||||||
const languageOptions = [
|
const languageOptions = [
|
||||||
@@ -28,19 +32,37 @@ const updateLanguage = async (event: Event) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 主题选择(未实际实现,仅界面展示)
|
// 主题选择
|
||||||
const themeOptions = [
|
const themeOptions = computed(() => AVAILABLE_THEMES);
|
||||||
{ id: 'dark', name: '深色', color: '#2a2a2a' },
|
const selectedTheme = ref<ThemeType>(configStore.config.appearance.theme || 'default-dark' as ThemeType);
|
||||||
{ id: 'darker', name: '暗黑', color: '#1a1a1a' },
|
|
||||||
{ id: 'light', name: '浅色', color: '#f5f5f5' },
|
|
||||||
{ id: 'blue', name: '蓝调', color: '#1e3a5f' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const selectedTheme = ref('dark');
|
// 当前主题预览信息
|
||||||
|
const currentPreviewTheme = computed(() => {
|
||||||
|
const theme = themeOptions.value.find(t => t.id === selectedTheme.value);
|
||||||
|
return theme || themeOptions.value[0];
|
||||||
|
});
|
||||||
|
|
||||||
const selectTheme = (themeId: string) => {
|
// 选择主题
|
||||||
|
const selectTheme = async (themeId: ThemeType) => {
|
||||||
selectedTheme.value = themeId;
|
selectedTheme.value = themeId;
|
||||||
|
|
||||||
|
// 更新配置(这会自动触发编辑器主题更新)
|
||||||
|
await safeCall(
|
||||||
|
() => configStore.setTheme(themeId),
|
||||||
|
'config.themeChangeFailed'
|
||||||
|
);
|
||||||
|
|
||||||
|
// 同步更新预览(用于设置页面的预览区域)
|
||||||
|
await setThemeComposable(themeId);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 监听配置变化,同步主题选择
|
||||||
|
watch(() => configStore.config.appearance.theme, (newTheme) => {
|
||||||
|
if (newTheme && newTheme !== selectedTheme.value) {
|
||||||
|
selectedTheme.value = newTheme;
|
||||||
|
setThemeComposable(newTheme);
|
||||||
|
}
|
||||||
|
}, { immediate: true });
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -56,31 +78,59 @@ const selectTheme = (themeId: string) => {
|
|||||||
</SettingSection>
|
</SettingSection>
|
||||||
|
|
||||||
<SettingSection :title="t('settings.appearance')">
|
<SettingSection :title="t('settings.appearance')">
|
||||||
<div class="theme-selector">
|
<div class="appearance-content">
|
||||||
<div class="selector-label">主题</div>
|
<div class="theme-selection-area">
|
||||||
<div class="theme-options">
|
<div class="theme-selector">
|
||||||
<div
|
<div class="selector-label">{{ t('settings.theme') }}</div>
|
||||||
v-for="theme in themeOptions"
|
<div class="theme-options">
|
||||||
:key="theme.id"
|
<div
|
||||||
class="theme-option"
|
v-for="theme in themeOptions"
|
||||||
:class="{ active: selectedTheme === theme.id }"
|
:key="theme.id"
|
||||||
@click="selectTheme(theme.id)"
|
class="theme-option"
|
||||||
>
|
:class="{ active: selectedTheme === theme.id }"
|
||||||
<div class="color-preview" :style="{ backgroundColor: theme.color }"></div>
|
@click="selectTheme(theme.id)"
|
||||||
<div class="theme-name">{{ theme.name }}</div>
|
>
|
||||||
|
<div class="color-preview" :style="{ backgroundColor: theme.previewColors.background }"></div>
|
||||||
|
<div class="theme-name">{{ theme.displayName }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
<div class="preview-area">
|
||||||
<div class="editor-preview">
|
<div class="editor-preview" :style="{ backgroundColor: currentPreviewTheme.previewColors.background }">
|
||||||
<div class="preview-header">
|
<div class="preview-header" :style="{ backgroundColor: currentPreviewTheme.previewColors.background, borderBottomColor: currentPreviewTheme.previewColors.foreground + '33' }">
|
||||||
<div class="preview-title">预览</div>
|
<div class="preview-title" :style="{ color: currentPreviewTheme.previewColors.foreground }">{{ currentPreviewTheme.displayName }} 预览</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="preview-content">
|
<div class="preview-content" :style="{ color: currentPreviewTheme.previewColors.foreground }">
|
||||||
<div class="preview-line"><span class="line-number">1</span><span class="keyword">function</span> <span class="function">example</span>() {</div>
|
<div class="preview-line">
|
||||||
<div class="preview-line"><span class="line-number">2</span> <span class="keyword">const</span> greeting = <span class="string">"Hello, World!"</span>;</div>
|
<span class="line-number" :style="{ color: currentPreviewTheme.previewColors.comment }">1</span>
|
||||||
<div class="preview-line"><span class="line-number">3</span> <span class="function">console.log</span>(greeting);</div>
|
<span class="keyword" :style="{ color: currentPreviewTheme.previewColors.keyword }">function</span>
|
||||||
<div class="preview-line"><span class="line-number">4</span>}</div>
|
<span> </span>
|
||||||
|
<span class="function" :style="{ color: currentPreviewTheme.previewColors.function }">exampleFunc</span>() {
|
||||||
|
</div>
|
||||||
|
<div class="preview-line">
|
||||||
|
<span class="line-number" :style="{ color: currentPreviewTheme.previewColors.comment }">2</span>
|
||||||
|
<span> </span>
|
||||||
|
<span class="keyword" :style="{ color: currentPreviewTheme.previewColors.keyword }">const</span>
|
||||||
|
<span> hello = </span>
|
||||||
|
<span class="string" :style="{ color: currentPreviewTheme.previewColors.string }">"你好,世界!"</span>;
|
||||||
|
</div>
|
||||||
|
<div class="preview-line">
|
||||||
|
<span class="line-number" :style="{ color: currentPreviewTheme.previewColors.comment }">3</span>
|
||||||
|
<span> </span>
|
||||||
|
<span class="function" :style="{ color: currentPreviewTheme.previewColors.function }">console.log</span>(hello);
|
||||||
|
</div>
|
||||||
|
<div class="preview-line">
|
||||||
|
<span class="line-number" :style="{ color: currentPreviewTheme.previewColors.comment }">4</span>
|
||||||
|
<span> </span>
|
||||||
|
<span class="comment" :style="{ color: currentPreviewTheme.previewColors.comment }">// 这是中文注释</span>
|
||||||
|
</div>
|
||||||
|
<div class="preview-line">
|
||||||
|
<span class="line-number" :style="{ color: currentPreviewTheme.previewColors.comment }">5</span>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</SettingSection>
|
</SettingSection>
|
||||||
@@ -89,7 +139,31 @@ const selectTheme = (themeId: string) => {
|
|||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.settings-page {
|
.settings-page {
|
||||||
max-width: 800px;
|
max-width: 1000px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.appearance-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 24px;
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 32px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-selection-area {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-area {
|
||||||
|
flex: 0 0 400px;
|
||||||
|
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.select-input {
|
.select-input {
|
||||||
@@ -118,7 +192,7 @@ const selectTheme = (themeId: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.theme-selector {
|
.theme-selector {
|
||||||
padding: 15px 16px;
|
padding: 0;
|
||||||
|
|
||||||
.selector-label {
|
.selector-label {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
@@ -128,96 +202,134 @@ const selectTheme = (themeId: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.theme-options {
|
.theme-options {
|
||||||
display: flex;
|
display: grid;
|
||||||
gap: 15px;
|
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
||||||
flex-wrap: wrap;
|
gap: 16px;
|
||||||
|
justify-content: start;
|
||||||
|
|
||||||
.theme-option {
|
.theme-option {
|
||||||
width: 100px;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
.color-preview {
|
.color-preview {
|
||||||
height: 60px;
|
height: 70px;
|
||||||
border-radius: 4px;
|
border-radius: 6px;
|
||||||
border: 2px solid transparent;
|
border: 2px solid transparent;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.3);
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.theme-name {
|
.theme-name {
|
||||||
margin-top: 6px;
|
margin-top: 8px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: #c0c0c0;
|
color: #c0c0c0;
|
||||||
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover .color-preview {
|
&:hover {
|
||||||
border-color: rgba(255, 255, 255, 0.3);
|
transform: translateY(-2px);
|
||||||
|
|
||||||
|
.color-preview {
|
||||||
|
border-color: rgba(255, 255, 255, 0.4);
|
||||||
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.active .color-preview {
|
&.active {
|
||||||
border-color: #4a9eff;
|
.color-preview {
|
||||||
}
|
border-color: #4a9eff;
|
||||||
|
box-shadow: 0 4px 20px rgba(74, 158, 255, 0.3);
|
||||||
&.active .theme-name {
|
|
||||||
color: #ffffff;
|
&::after {
|
||||||
|
content: '✓';
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
color: #4a9eff;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
text-shadow: 0 0 4px rgba(74, 158, 255, 0.8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-name {
|
||||||
|
color: #4a9eff;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.editor-preview {
|
.editor-preview {
|
||||||
margin: 20px 16px;
|
border-radius: 8px;
|
||||||
background-color: #252525;
|
|
||||||
border-radius: 6px;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.2);
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
width: 100%;
|
||||||
|
max-width: 400px;
|
||||||
|
|
||||||
.preview-header {
|
.preview-header {
|
||||||
padding: 10px 16px;
|
padding: 12px 16px;
|
||||||
background-color: #353535;
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
border-bottom: 1px solid #444444;
|
|
||||||
|
|
||||||
.preview-title {
|
.preview-title {
|
||||||
font-size: 13px;
|
font-size: 14px;
|
||||||
color: #b0b0b0;
|
font-weight: 600;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '🎨';
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.preview-content {
|
.preview-content {
|
||||||
padding: 12px 0;
|
padding: 16px 0;
|
||||||
font-family: 'Consolas', 'Courier New', monospace;
|
font-family: 'JetBrains Mono', 'Fira Code', 'Consolas', 'Courier New', monospace;
|
||||||
font-size: 14px;
|
font-size: 13px;
|
||||||
|
line-height: 1.6;
|
||||||
|
|
||||||
.preview-line {
|
.preview-line {
|
||||||
padding: 3px 16px;
|
padding: 2px 16px;
|
||||||
line-height: 1.5;
|
transition: background-color 0.2s ease;
|
||||||
white-space: pre;
|
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: rgba(255, 255, 255, 0.03);
|
background-color: rgba(255, 255, 255, 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
.line-number {
|
.line-number {
|
||||||
color: #707070;
|
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 25px;
|
width: 24px;
|
||||||
margin-right: 15px;
|
margin-right: 12px;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
font-size: 12px;
|
||||||
|
opacity: 0.7;
|
||||||
.keyword {
|
|
||||||
color: #569cd6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.function {
|
|
||||||
color: #dcdcaa;
|
|
||||||
}
|
|
||||||
|
|
||||||
.string {
|
|
||||||
color: #ce9178;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -26,6 +26,36 @@ const (
|
|||||||
LangEnUS LanguageType = "en-US"
|
LangEnUS LanguageType = "en-US"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ThemeType 主题类型定义
|
||||||
|
type ThemeType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
// ThemeDefaultDark 默认深色主题
|
||||||
|
ThemeDefaultDark ThemeType = "default-dark"
|
||||||
|
// ThemeDracula Dracula主题
|
||||||
|
ThemeDracula ThemeType = "dracula"
|
||||||
|
// ThemeAura Aura主题
|
||||||
|
ThemeAura ThemeType = "aura"
|
||||||
|
// ThemeGithubDark GitHub深色主题
|
||||||
|
ThemeGithubDark ThemeType = "github-dark"
|
||||||
|
// ThemeGithubLight GitHub浅色主题
|
||||||
|
ThemeGithubLight ThemeType = "github-light"
|
||||||
|
// ThemeMaterialDark Material深色主题
|
||||||
|
ThemeMaterialDark ThemeType = "material-dark"
|
||||||
|
// ThemeMaterialLight Material浅色主题
|
||||||
|
ThemeMaterialLight ThemeType = "material-light"
|
||||||
|
// ThemeSolarizedDark Solarized深色主题
|
||||||
|
ThemeSolarizedDark ThemeType = "solarized-dark"
|
||||||
|
// ThemeSolarizedLight Solarized浅色主题
|
||||||
|
ThemeSolarizedLight ThemeType = "solarized-light"
|
||||||
|
// ThemeTokyoNight Tokyo Night主题
|
||||||
|
ThemeTokyoNight ThemeType = "tokyo-night"
|
||||||
|
// ThemeTokyoNightStorm Tokyo Night Storm主题
|
||||||
|
ThemeTokyoNightStorm ThemeType = "tokyo-night-storm"
|
||||||
|
// ThemeTokyoNightDay Tokyo Night Day主题
|
||||||
|
ThemeTokyoNightDay ThemeType = "tokyo-night-day"
|
||||||
|
)
|
||||||
|
|
||||||
// GeneralConfig 通用设置配置
|
// GeneralConfig 通用设置配置
|
||||||
type GeneralConfig struct {
|
type GeneralConfig struct {
|
||||||
AlwaysOnTop bool `json:"alwaysOnTop" yaml:"always_on_top" mapstructure:"always_on_top"` // 窗口是否置顶
|
AlwaysOnTop bool `json:"alwaysOnTop" yaml:"always_on_top" mapstructure:"always_on_top"` // 窗口是否置顶
|
||||||
@@ -65,6 +95,7 @@ type EditingConfig struct {
|
|||||||
// AppearanceConfig 外观设置配置
|
// AppearanceConfig 外观设置配置
|
||||||
type AppearanceConfig struct {
|
type AppearanceConfig struct {
|
||||||
Language LanguageType `json:"language" yaml:"language" mapstructure:"language"` // 界面语言
|
Language LanguageType `json:"language" yaml:"language" mapstructure:"language"` // 界面语言
|
||||||
|
Theme ThemeType `json:"theme" yaml:"theme" mapstructure:"theme"` // 编辑器主题
|
||||||
}
|
}
|
||||||
|
|
||||||
// KeyBindingsConfig 快捷键设置配置
|
// KeyBindingsConfig 快捷键设置配置
|
||||||
@@ -132,6 +163,7 @@ func NewDefaultAppConfig() *AppConfig {
|
|||||||
},
|
},
|
||||||
Appearance: AppearanceConfig{
|
Appearance: AppearanceConfig{
|
||||||
Language: LangZhCN,
|
Language: LangZhCN,
|
||||||
|
Theme: ThemeDefaultDark, // 默认使用深色主题
|
||||||
},
|
},
|
||||||
KeyBindings: KeyBindingsConfig{
|
KeyBindings: KeyBindingsConfig{
|
||||||
// 预留给未来的快捷键配置
|
// 预留给未来的快捷键配置
|
||||||
|
@@ -125,6 +125,7 @@ func setDefaults(v *viper.Viper) {
|
|||||||
|
|
||||||
// 外观设置默认值
|
// 外观设置默认值
|
||||||
v.SetDefault("appearance.language", defaultConfig.Appearance.Language)
|
v.SetDefault("appearance.language", defaultConfig.Appearance.Language)
|
||||||
|
v.SetDefault("appearance.theme", defaultConfig.Appearance.Theme)
|
||||||
|
|
||||||
// 元数据默认值
|
// 元数据默认值
|
||||||
v.SetDefault("metadata.version", defaultConfig.Metadata.Version)
|
v.SetDefault("metadata.version", defaultConfig.Metadata.Version)
|
||||||
@@ -258,6 +259,7 @@ func (cs *ConfigService) ResetConfig() {
|
|||||||
|
|
||||||
// 外观设置 - 批量设置到viper中
|
// 外观设置 - 批量设置到viper中
|
||||||
cs.viper.Set("appearance.language", defaultConfig.Appearance.Language)
|
cs.viper.Set("appearance.language", defaultConfig.Appearance.Language)
|
||||||
|
cs.viper.Set("appearance.theme", defaultConfig.Appearance.Theme)
|
||||||
|
|
||||||
// 元数据 - 批量设置到viper中
|
// 元数据 - 批量设置到viper中
|
||||||
cs.viper.Set("metadata.version", defaultConfig.Metadata.Version)
|
cs.viper.Set("metadata.version", defaultConfig.Metadata.Version)
|
||||||
|
Reference in New Issue
Block a user