♻️ Refactor theme module
This commit is contained in:
@@ -1170,7 +1170,7 @@ export class Theme {
|
||||
this["type"] = ("" as ThemeType);
|
||||
}
|
||||
if (!("colors" in $$source)) {
|
||||
this["colors"] = (new ThemeColorConfig());
|
||||
this["colors"] = ({} as ThemeColorConfig);
|
||||
}
|
||||
if (!("isDefault" in $$source)) {
|
||||
this["isDefault"] = false;
|
||||
@@ -1199,303 +1199,9 @@ export class Theme {
|
||||
}
|
||||
|
||||
/**
|
||||
* ThemeColorConfig 主题颜色配置(与前端 ThemeColors 接口保持一致)
|
||||
* ThemeColorConfig 使用与前端 ThemeColors 相同的结构,存储任意主题键值
|
||||
*/
|
||||
export class ThemeColorConfig {
|
||||
/**
|
||||
* 主题基本信息
|
||||
* 主题名称
|
||||
*/
|
||||
"name": string;
|
||||
|
||||
/**
|
||||
* 是否为深色主题
|
||||
*/
|
||||
"dark": boolean;
|
||||
|
||||
/**
|
||||
* 基础色调
|
||||
* 主背景色
|
||||
*/
|
||||
"background": string;
|
||||
|
||||
/**
|
||||
* 次要背景色(用于代码块交替背景)
|
||||
*/
|
||||
"backgroundSecondary": string;
|
||||
|
||||
/**
|
||||
* 面板背景
|
||||
*/
|
||||
"surface": string;
|
||||
|
||||
/**
|
||||
* 下拉菜单背景
|
||||
*/
|
||||
"dropdownBackground": string;
|
||||
|
||||
/**
|
||||
* 下拉菜单边框
|
||||
*/
|
||||
"dropdownBorder": string;
|
||||
|
||||
/**
|
||||
* 文本颜色
|
||||
* 主文本色
|
||||
*/
|
||||
"foreground": string;
|
||||
|
||||
/**
|
||||
* 次要文本色
|
||||
*/
|
||||
"foregroundSecondary": string;
|
||||
|
||||
/**
|
||||
* 注释色
|
||||
*/
|
||||
"comment": string;
|
||||
|
||||
/**
|
||||
* 语法高亮色 - 核心
|
||||
* 关键字
|
||||
*/
|
||||
"keyword": string;
|
||||
|
||||
/**
|
||||
* 字符串
|
||||
*/
|
||||
"string": string;
|
||||
|
||||
/**
|
||||
* 函数名
|
||||
*/
|
||||
"function": string;
|
||||
|
||||
/**
|
||||
* 数字
|
||||
*/
|
||||
"number": string;
|
||||
|
||||
/**
|
||||
* 操作符
|
||||
*/
|
||||
"operator": string;
|
||||
|
||||
/**
|
||||
* 变量
|
||||
*/
|
||||
"variable": string;
|
||||
|
||||
/**
|
||||
* 类型
|
||||
*/
|
||||
"type": string;
|
||||
|
||||
/**
|
||||
* 语法高亮色 - 扩展
|
||||
* 常量
|
||||
*/
|
||||
"constant": string;
|
||||
|
||||
/**
|
||||
* 存储类型(如 static, const)
|
||||
*/
|
||||
"storage": string;
|
||||
|
||||
/**
|
||||
* 参数
|
||||
*/
|
||||
"parameter": string;
|
||||
|
||||
/**
|
||||
* 类名
|
||||
*/
|
||||
"class": string;
|
||||
|
||||
/**
|
||||
* 标题(Markdown等)
|
||||
*/
|
||||
"heading": string;
|
||||
|
||||
/**
|
||||
* 无效内容/错误
|
||||
*/
|
||||
"invalid": string;
|
||||
|
||||
/**
|
||||
* 正则表达式
|
||||
*/
|
||||
"regexp": string;
|
||||
|
||||
/**
|
||||
* 界面元素
|
||||
* 光标
|
||||
*/
|
||||
"cursor": string;
|
||||
|
||||
/**
|
||||
* 选中背景
|
||||
*/
|
||||
"selection": string;
|
||||
|
||||
/**
|
||||
* 失焦选中背景
|
||||
*/
|
||||
"selectionBlur": string;
|
||||
|
||||
/**
|
||||
* 当前行高亮
|
||||
*/
|
||||
"activeLine": string;
|
||||
|
||||
/**
|
||||
* 行号
|
||||
*/
|
||||
"lineNumber": string;
|
||||
|
||||
/**
|
||||
* 活动行号颜色
|
||||
*/
|
||||
"activeLineNumber": string;
|
||||
|
||||
/**
|
||||
* 边框和分割线
|
||||
* 边框色
|
||||
*/
|
||||
"borderColor": string;
|
||||
|
||||
/**
|
||||
* 浅色边框
|
||||
*/
|
||||
"borderLight": string;
|
||||
|
||||
/**
|
||||
* 搜索和匹配
|
||||
* 搜索匹配
|
||||
*/
|
||||
"searchMatch": string;
|
||||
|
||||
/**
|
||||
* 匹配括号
|
||||
*/
|
||||
"matchingBracket": string;
|
||||
|
||||
/** Creates a new ThemeColorConfig instance. */
|
||||
constructor($$source: Partial<ThemeColorConfig> = {}) {
|
||||
if (!("name" in $$source)) {
|
||||
this["name"] = "";
|
||||
}
|
||||
if (!("dark" in $$source)) {
|
||||
this["dark"] = false;
|
||||
}
|
||||
if (!("background" in $$source)) {
|
||||
this["background"] = "";
|
||||
}
|
||||
if (!("backgroundSecondary" in $$source)) {
|
||||
this["backgroundSecondary"] = "";
|
||||
}
|
||||
if (!("surface" in $$source)) {
|
||||
this["surface"] = "";
|
||||
}
|
||||
if (!("dropdownBackground" in $$source)) {
|
||||
this["dropdownBackground"] = "";
|
||||
}
|
||||
if (!("dropdownBorder" in $$source)) {
|
||||
this["dropdownBorder"] = "";
|
||||
}
|
||||
if (!("foreground" in $$source)) {
|
||||
this["foreground"] = "";
|
||||
}
|
||||
if (!("foregroundSecondary" in $$source)) {
|
||||
this["foregroundSecondary"] = "";
|
||||
}
|
||||
if (!("comment" in $$source)) {
|
||||
this["comment"] = "";
|
||||
}
|
||||
if (!("keyword" in $$source)) {
|
||||
this["keyword"] = "";
|
||||
}
|
||||
if (!("string" in $$source)) {
|
||||
this["string"] = "";
|
||||
}
|
||||
if (!("function" in $$source)) {
|
||||
this["function"] = "";
|
||||
}
|
||||
if (!("number" in $$source)) {
|
||||
this["number"] = "";
|
||||
}
|
||||
if (!("operator" in $$source)) {
|
||||
this["operator"] = "";
|
||||
}
|
||||
if (!("variable" in $$source)) {
|
||||
this["variable"] = "";
|
||||
}
|
||||
if (!("type" in $$source)) {
|
||||
this["type"] = "";
|
||||
}
|
||||
if (!("constant" in $$source)) {
|
||||
this["constant"] = "";
|
||||
}
|
||||
if (!("storage" in $$source)) {
|
||||
this["storage"] = "";
|
||||
}
|
||||
if (!("parameter" in $$source)) {
|
||||
this["parameter"] = "";
|
||||
}
|
||||
if (!("class" in $$source)) {
|
||||
this["class"] = "";
|
||||
}
|
||||
if (!("heading" in $$source)) {
|
||||
this["heading"] = "";
|
||||
}
|
||||
if (!("invalid" in $$source)) {
|
||||
this["invalid"] = "";
|
||||
}
|
||||
if (!("regexp" in $$source)) {
|
||||
this["regexp"] = "";
|
||||
}
|
||||
if (!("cursor" in $$source)) {
|
||||
this["cursor"] = "";
|
||||
}
|
||||
if (!("selection" in $$source)) {
|
||||
this["selection"] = "";
|
||||
}
|
||||
if (!("selectionBlur" in $$source)) {
|
||||
this["selectionBlur"] = "";
|
||||
}
|
||||
if (!("activeLine" in $$source)) {
|
||||
this["activeLine"] = "";
|
||||
}
|
||||
if (!("lineNumber" in $$source)) {
|
||||
this["lineNumber"] = "";
|
||||
}
|
||||
if (!("activeLineNumber" in $$source)) {
|
||||
this["activeLineNumber"] = "";
|
||||
}
|
||||
if (!("borderColor" in $$source)) {
|
||||
this["borderColor"] = "";
|
||||
}
|
||||
if (!("borderLight" in $$source)) {
|
||||
this["borderLight"] = "";
|
||||
}
|
||||
if (!("searchMatch" in $$source)) {
|
||||
this["searchMatch"] = "";
|
||||
}
|
||||
if (!("matchingBracket" in $$source)) {
|
||||
this["matchingBracket"] = "";
|
||||
}
|
||||
|
||||
Object.assign(this, $$source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new ThemeColorConfig instance from a string or object.
|
||||
*/
|
||||
static createFrom($$source: any = {}): ThemeColorConfig {
|
||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||
return new ThemeColorConfig($$parsedSource as Partial<ThemeColorConfig>);
|
||||
}
|
||||
}
|
||||
export type ThemeColorConfig = { [_: string]: any };
|
||||
|
||||
/**
|
||||
* ThemeType 主题类型枚举
|
||||
@@ -1636,6 +1342,11 @@ var $$createType6 = (function $$initCreateType6(...args): any {
|
||||
});
|
||||
const $$createType7 = $Create.Map($Create.Any, $Create.Any);
|
||||
const $$createType8 = HotkeyCombo.createFrom;
|
||||
const $$createType9 = ThemeColorConfig.createFrom;
|
||||
var $$createType9 = (function $$initCreateType9(...args): any {
|
||||
if ($$createType9 === $$initCreateType9) {
|
||||
$$createType9 = $$createType7;
|
||||
}
|
||||
return $$createType9(...args);
|
||||
});
|
||||
const $$createType10 = GithubConfig.createFrom;
|
||||
const $$createType11 = GiteaConfig.createFrom;
|
||||
|
||||
@@ -18,23 +18,10 @@ import * as application$0 from "../../../github.com/wailsapp/wails/v3/pkg/applic
|
||||
import * as models$0 from "../models/models.js";
|
||||
|
||||
/**
|
||||
* GetAllThemes 获取所有主题
|
||||
* GetThemeByName 通过名称获取主题覆盖,若不存在则返回 nil
|
||||
*/
|
||||
export function GetAllThemes(): Promise<(models$0.Theme | null)[]> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(2425053076) as any;
|
||||
let $typingPromise = $resultPromise.then(($result: any) => {
|
||||
return $$createType2($result);
|
||||
}) as any;
|
||||
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
|
||||
return $typingPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* GetThemeByID 根据ID或名称获取主题
|
||||
* 如果 id > 0,按ID查询;如果 id = 0,按名称查询
|
||||
*/
|
||||
export function GetThemeByIdOrName(id: number, ...name: string[]): Promise<models$0.Theme | null> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(127385338, id, name) as any;
|
||||
export function GetThemeByName(name: string): Promise<models$0.Theme | null> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(1938954770, name) as any;
|
||||
let $typingPromise = $resultPromise.then(($result: any) => {
|
||||
return $$createType1($result);
|
||||
}) as any;
|
||||
@@ -43,10 +30,10 @@ export function GetThemeByIdOrName(id: number, ...name: string[]): Promise<model
|
||||
}
|
||||
|
||||
/**
|
||||
* ResetTheme 重置主题为预设配置
|
||||
* ResetTheme 删除指定主题的覆盖配置
|
||||
*/
|
||||
export function ResetTheme(id: number, ...name: string[]): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(1806334457, id, name) as any;
|
||||
export function ResetTheme(name: string): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(1806334457, name) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
@@ -59,7 +46,7 @@ export function ServiceShutdown(): Promise<void> & { cancel(): void } {
|
||||
}
|
||||
|
||||
/**
|
||||
* ServiceStartup 服务启动时初始化
|
||||
* ServiceStartup 服务启动
|
||||
*/
|
||||
export function ServiceStartup(options: application$0.ServiceOptions): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(2915959937, options) as any;
|
||||
@@ -67,14 +54,13 @@ export function ServiceStartup(options: application$0.ServiceOptions): Promise<v
|
||||
}
|
||||
|
||||
/**
|
||||
* UpdateTheme 更新主题
|
||||
* UpdateTheme 保存或更新主题覆盖
|
||||
*/
|
||||
export function UpdateTheme(id: number, colors: models$0.ThemeColorConfig): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(70189749, id, colors) as any;
|
||||
export function UpdateTheme(name: string, colors: models$0.ThemeColorConfig): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(70189749, name, colors) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
// Private type creation functions
|
||||
const $$createType0 = models$0.Theme.createFrom;
|
||||
const $$createType1 = $Create.Nullable($$createType0);
|
||||
const $$createType2 = $Create.Array($$createType1);
|
||||
|
||||
@@ -1,191 +1,156 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { computed, ref } from 'vue';
|
||||
import {SystemThemeType, ThemeType, Theme, ThemeColorConfig} from '@/../bindings/voidraft/internal/models/models';
|
||||
import { SystemThemeType, ThemeType, ThemeColorConfig } from '@/../bindings/voidraft/internal/models/models';
|
||||
import { ThemeService } from '@/../bindings/voidraft/internal/services';
|
||||
import { useConfigStore } from './configStore';
|
||||
import { useEditorStore } from './editorStore';
|
||||
import type { ThemeColors } from '@/views/editor/theme/types';
|
||||
import { cloneThemeColors, FALLBACK_THEME_NAME, themePresetList, themePresetMap } from '@/views/editor/theme/presets';
|
||||
|
||||
type ThemeOption = { name: string; type: ThemeType };
|
||||
|
||||
const resolveThemeName = (name?: string) =>
|
||||
name && themePresetMap[name] ? name : FALLBACK_THEME_NAME;
|
||||
|
||||
const createThemeOptions = (type: ThemeType): ThemeOption[] =>
|
||||
themePresetList
|
||||
.filter(preset => preset.type === type)
|
||||
.map(preset => ({ name: preset.name, type: preset.type }));
|
||||
|
||||
const darkThemeOptions = createThemeOptions(ThemeType.ThemeTypeDark);
|
||||
const lightThemeOptions = createThemeOptions(ThemeType.ThemeTypeLight);
|
||||
|
||||
const cloneColors = (colors: ThemeColorConfig): ThemeColors =>
|
||||
JSON.parse(JSON.stringify(colors)) as ThemeColors;
|
||||
|
||||
const getPresetColors = (name: string): ThemeColors => {
|
||||
const preset = themePresetMap[name] ?? themePresetMap[FALLBACK_THEME_NAME];
|
||||
const colors = cloneThemeColors(preset.colors);
|
||||
colors.themeName = name;
|
||||
return colors;
|
||||
};
|
||||
|
||||
const fetchThemeColors = async (themeName: string): Promise<ThemeColors> => {
|
||||
const safeName = resolveThemeName(themeName);
|
||||
try {
|
||||
const theme = await ThemeService.GetThemeByName(safeName);
|
||||
if (theme?.colors) {
|
||||
const colors = cloneColors(theme.colors);
|
||||
colors.themeName = safeName;
|
||||
return colors;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load theme override:', error);
|
||||
}
|
||||
return getPresetColors(safeName);
|
||||
};
|
||||
|
||||
/**
|
||||
* 主题管理 Store
|
||||
* 职责:管理主题状态、颜色配置和预设主题列表
|
||||
*/
|
||||
export const useThemeStore = defineStore('theme', () => {
|
||||
const configStore = useConfigStore();
|
||||
|
||||
// 所有主题列表
|
||||
const allThemes = ref<Theme[]>([]);
|
||||
|
||||
// 当前主题的颜色配置
|
||||
const currentColors = ref<ThemeColors | null>(null);
|
||||
|
||||
// 计算属性:当前系统主题模式
|
||||
const currentTheme = computed(() =>
|
||||
configStore.config?.appearance?.systemTheme || SystemThemeType.SystemThemeAuto
|
||||
|
||||
const currentTheme = computed(
|
||||
() => configStore.config?.appearance?.systemTheme || SystemThemeType.SystemThemeAuto
|
||||
);
|
||||
|
||||
// 计算属性:当前是否为深色模式
|
||||
const isDarkMode = computed(() =>
|
||||
currentTheme.value === SystemThemeType.SystemThemeDark ||
|
||||
(currentTheme.value === SystemThemeType.SystemThemeAuto &&
|
||||
window.matchMedia('(prefers-color-scheme: dark)').matches)
|
||||
const isDarkMode = computed(
|
||||
() =>
|
||||
currentTheme.value === SystemThemeType.SystemThemeDark ||
|
||||
(currentTheme.value === SystemThemeType.SystemThemeAuto &&
|
||||
window.matchMedia('(prefers-color-scheme: dark)').matches)
|
||||
);
|
||||
|
||||
// 计算属性:根据类型获取主题列表
|
||||
const darkThemes = computed(() =>
|
||||
allThemes.value.filter(t => t.type === ThemeType.ThemeTypeDark)
|
||||
);
|
||||
|
||||
const lightThemes = computed(() =>
|
||||
allThemes.value.filter(t => t.type === ThemeType.ThemeTypeLight)
|
||||
);
|
||||
|
||||
// 计算属性:当前可用的主题列表
|
||||
const availableThemes = computed(() =>
|
||||
isDarkMode.value ? darkThemes.value : lightThemes.value
|
||||
const availableThemes = computed<ThemeOption[]>(() =>
|
||||
isDarkMode.value ? darkThemeOptions : lightThemeOptions
|
||||
);
|
||||
|
||||
// 应用主题到 DOM
|
||||
const applyThemeToDOM = (theme: SystemThemeType) => {
|
||||
const themeMap = {
|
||||
[SystemThemeType.SystemThemeAuto]: 'auto',
|
||||
[SystemThemeType.SystemThemeDark]: 'dark',
|
||||
[SystemThemeType.SystemThemeLight]: 'light'
|
||||
[SystemThemeType.SystemThemeLight]: 'light',
|
||||
};
|
||||
document.documentElement.setAttribute('data-theme', themeMap[theme]);
|
||||
};
|
||||
|
||||
// 从数据库加载所有主题
|
||||
const loadAllThemes = async () => {
|
||||
try {
|
||||
const themes = await ThemeService.GetAllThemes();
|
||||
allThemes.value = (themes || []).filter((t): t is Theme => t !== null);
|
||||
return allThemes.value;
|
||||
} catch (error) {
|
||||
console.error('Failed to load themes from database:', error);
|
||||
allThemes.value = [];
|
||||
return [];
|
||||
}
|
||||
const loadThemeColors = async (themeName?: string) => {
|
||||
const targetName = resolveThemeName(
|
||||
themeName || configStore.config?.appearance?.currentTheme
|
||||
);
|
||||
currentColors.value = await fetchThemeColors(targetName);
|
||||
};
|
||||
|
||||
// 初始化主题颜色
|
||||
const initThemeColors = async () => {
|
||||
// 加载所有主题
|
||||
await loadAllThemes();
|
||||
|
||||
// 从配置获取当前主题名称并加载
|
||||
const currentThemeName = configStore.config?.appearance?.currentTheme || 'default-dark';
|
||||
|
||||
const theme = allThemes.value.find(t => t.name === currentThemeName);
|
||||
|
||||
if (!theme) {
|
||||
console.error(`Theme not found: ${currentThemeName}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 直接设置当前主题颜色
|
||||
currentColors.value = theme.colors as ThemeColors;
|
||||
};
|
||||
|
||||
// 初始化主题
|
||||
const initializeTheme = async () => {
|
||||
const theme = currentTheme.value;
|
||||
applyThemeToDOM(theme);
|
||||
await initThemeColors();
|
||||
applyThemeToDOM(currentTheme.value);
|
||||
await loadThemeColors();
|
||||
};
|
||||
|
||||
// 设置系统主题模式(深色/浅色/自动)
|
||||
const setTheme = async (theme: SystemThemeType) => {
|
||||
await configStore.setSystemTheme(theme);
|
||||
applyThemeToDOM(theme);
|
||||
refreshEditorTheme();
|
||||
};
|
||||
|
||||
// 切换到指定的预设主题
|
||||
|
||||
const switchToTheme = async (themeName: string) => {
|
||||
const theme = allThemes.value.find(t => t.name === themeName);
|
||||
if (!theme) {
|
||||
if (!themePresetMap[themeName]) {
|
||||
console.error('Theme not found:', themeName);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 直接设置当前主题颜色
|
||||
currentColors.value = theme.colors as ThemeColors;
|
||||
|
||||
// 持久化到配置
|
||||
await loadThemeColors(themeName);
|
||||
await configStore.setCurrentTheme(themeName);
|
||||
|
||||
// 刷新编辑器
|
||||
refreshEditorTheme();
|
||||
return true;
|
||||
};
|
||||
|
||||
// 更新当前主题的颜色配置
|
||||
|
||||
const updateCurrentColors = (colors: Partial<ThemeColors>) => {
|
||||
if (!currentColors.value) return;
|
||||
Object.assign(currentColors.value, colors);
|
||||
};
|
||||
|
||||
// 保存当前主题颜色到数据库
|
||||
|
||||
const saveCurrentTheme = async () => {
|
||||
if (!currentColors.value) {
|
||||
throw new Error('No theme selected');
|
||||
}
|
||||
|
||||
const theme = allThemes.value.find(t => t.name === currentColors.value!.themeName);
|
||||
if (!theme) {
|
||||
throw new Error('Theme not found');
|
||||
}
|
||||
|
||||
await ThemeService.UpdateTheme(theme.id, currentColors.value as ThemeColorConfig);
|
||||
const themeName = resolveThemeName(currentColors.value.themeName);
|
||||
currentColors.value.themeName = themeName;
|
||||
|
||||
await ThemeService.UpdateTheme(themeName, currentColors.value as unknown as ThemeColorConfig);
|
||||
|
||||
await loadThemeColors(themeName);
|
||||
refreshEditorTheme();
|
||||
return true;
|
||||
};
|
||||
|
||||
// 重置当前主题为预设配置
|
||||
|
||||
const resetCurrentTheme = async () => {
|
||||
if (!currentColors.value) {
|
||||
throw new Error('No theme selected');
|
||||
}
|
||||
|
||||
// 调用后端重置
|
||||
await ThemeService.ResetTheme(0, currentColors.value.themeName);
|
||||
|
||||
// 重新加载所有主题
|
||||
await loadAllThemes();
|
||||
|
||||
const updatedTheme = allThemes.value.find(t => t.name === currentColors.value!.themeName);
|
||||
|
||||
if (updatedTheme) {
|
||||
currentColors.value = updatedTheme.colors as ThemeColors;
|
||||
}
|
||||
|
||||
const themeName = resolveThemeName(currentColors.value.themeName);
|
||||
await ThemeService.ResetTheme(themeName);
|
||||
|
||||
await loadThemeColors(themeName);
|
||||
refreshEditorTheme();
|
||||
return true;
|
||||
};
|
||||
|
||||
// 刷新编辑器主题
|
||||
|
||||
const refreshEditorTheme = () => {
|
||||
applyThemeToDOM(currentTheme.value);
|
||||
|
||||
|
||||
const editorStore = useEditorStore();
|
||||
editorStore?.applyThemeSettings();
|
||||
};
|
||||
|
||||
return {
|
||||
// 状态
|
||||
allThemes,
|
||||
darkThemes,
|
||||
lightThemes,
|
||||
availableThemes,
|
||||
currentTheme,
|
||||
currentColors,
|
||||
isDarkMode,
|
||||
|
||||
// 方法
|
||||
setTheme,
|
||||
switchToTheme,
|
||||
initializeTheme,
|
||||
loadAllThemes,
|
||||
updateCurrentColors,
|
||||
saveCurrentTheme,
|
||||
resetCurrentTheme,
|
||||
|
||||
52
frontend/src/views/editor/theme/presets.ts
Normal file
52
frontend/src/views/editor/theme/presets.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import type {ThemeColors} from './types';
|
||||
import {ThemeType} from '@/../bindings/voidraft/internal/models/models';
|
||||
import {defaultDarkColors} from './dark/default-dark';
|
||||
import {defaultLightColors} from './light/default-light';
|
||||
import {config as draculaColors} from './dark/dracula';
|
||||
import {config as auraColors} from './dark/aura';
|
||||
import {config as githubDarkColors} from './dark/github-dark';
|
||||
import {config as materialDarkColors} from './dark/material-dark';
|
||||
import {config as oneDarkColors} from './dark/one-dark';
|
||||
import {config as solarizedDarkColors} from './dark/solarized-dark';
|
||||
import {config as tokyoNightColors} from './dark/tokyo-night';
|
||||
import {config as tokyoNightStormColors} from './dark/tokyo-night-storm';
|
||||
import {config as githubLightColors} from './light/github-light';
|
||||
import {config as materialLightColors} from './light/material-light';
|
||||
import {config as solarizedLightColors} from './light/solarized-light';
|
||||
import {config as tokyoNightDayColors} from './light/tokyo-night-day';
|
||||
|
||||
export interface ThemePreset {
|
||||
name: string;
|
||||
type: ThemeType;
|
||||
colors: ThemeColors;
|
||||
}
|
||||
|
||||
export const FALLBACK_THEME_NAME = defaultDarkColors.themeName;
|
||||
|
||||
export const themePresetList: ThemePreset[] = [
|
||||
{name: defaultDarkColors.themeName, type: ThemeType.ThemeTypeDark, colors: defaultDarkColors},
|
||||
{name: draculaColors.themeName, type: ThemeType.ThemeTypeDark, colors: draculaColors},
|
||||
{name: auraColors.themeName, type: ThemeType.ThemeTypeDark, colors: auraColors},
|
||||
{name: githubDarkColors.themeName, type: ThemeType.ThemeTypeDark, colors: githubDarkColors},
|
||||
{name: materialDarkColors.themeName, type: ThemeType.ThemeTypeDark, colors: materialDarkColors},
|
||||
{name: oneDarkColors.themeName, type: ThemeType.ThemeTypeDark, colors: oneDarkColors},
|
||||
{name: solarizedDarkColors.themeName, type: ThemeType.ThemeTypeDark, colors: solarizedDarkColors},
|
||||
{name: tokyoNightColors.themeName, type: ThemeType.ThemeTypeDark, colors: tokyoNightColors},
|
||||
{name: tokyoNightStormColors.themeName, type: ThemeType.ThemeTypeDark, colors: tokyoNightStormColors},
|
||||
{name: defaultLightColors.themeName, type: ThemeType.ThemeTypeLight, colors: defaultLightColors},
|
||||
{name: githubLightColors.themeName, type: ThemeType.ThemeTypeLight, colors: githubLightColors},
|
||||
{name: materialLightColors.themeName, type: ThemeType.ThemeTypeLight, colors: materialLightColors},
|
||||
{name: solarizedLightColors.themeName, type: ThemeType.ThemeTypeLight, colors: solarizedLightColors},
|
||||
{name: tokyoNightDayColors.themeName, type: ThemeType.ThemeTypeLight, colors: tokyoNightDayColors},
|
||||
];
|
||||
|
||||
export const themePresetMap: Record<string, ThemePreset> = themePresetList.reduce(
|
||||
(map, preset) => {
|
||||
map[preset.name] = preset;
|
||||
return map;
|
||||
},
|
||||
{} as Record<string, ThemePreset>
|
||||
);
|
||||
|
||||
export const cloneThemeColors = (colors: ThemeColors): ThemeColors =>
|
||||
JSON.parse(JSON.stringify(colors)) as ThemeColors;
|
||||
@@ -2,7 +2,7 @@
|
||||
import { useConfigStore } from '@/stores/configStore';
|
||||
import { useThemeStore } from '@/stores/themeStore';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { computed, watch, onMounted, ref } from 'vue';
|
||||
import { computed, watch, onMounted, ref, nextTick } from 'vue';
|
||||
import SettingSection from '../components/SettingSection.vue';
|
||||
import SettingItem from '../components/SettingItem.vue';
|
||||
import { SystemThemeType, LanguageType } from '@/../bindings/voidraft/internal/models/models';
|
||||
@@ -50,7 +50,10 @@ const resetButtonState = ref({
|
||||
|
||||
// 当前选中的主题名称
|
||||
const currentThemeName = computed({
|
||||
get: () => themeStore.currentColors?.name || '',
|
||||
get: () =>
|
||||
themeStore.currentColors?.themeName ||
|
||||
configStore.config.appearance.currentTheme ||
|
||||
'',
|
||||
set: async (themeName: string) => {
|
||||
await themeStore.switchToTheme(themeName);
|
||||
syncTempColors();
|
||||
@@ -89,21 +92,39 @@ onMounted(() => {
|
||||
// 从 ThemeColors 接口自动提取所有颜色字段
|
||||
const colorKeys = computed(() => {
|
||||
if (!tempColors.value) return [];
|
||||
|
||||
// 获取所有字段,排除 name 和 dark(这两个是元数据)
|
||||
return Object.keys(tempColors.value).filter(key =>
|
||||
key !== 'name' && key !== 'dark'
|
||||
);
|
||||
|
||||
return Object.keys(tempColors.value)
|
||||
.filter(key => key !== 'themeName' && key !== 'dark')
|
||||
.sort((a, b) => a.localeCompare(b));
|
||||
});
|
||||
|
||||
// 颜色配置列表
|
||||
const colorList = computed(() =>
|
||||
const colorList = computed(() =>
|
||||
colorKeys.value.map(colorKey => ({
|
||||
key: colorKey,
|
||||
label: t(`settings.themeColors.${colorKey}`)
|
||||
label: colorKey
|
||||
}))
|
||||
);
|
||||
|
||||
const colorSearch = ref('');
|
||||
const showSearch = ref(false);
|
||||
const searchInputRef = ref<HTMLInputElement | null>(null);
|
||||
|
||||
const filteredColorList = computed(() => {
|
||||
const keyword = colorSearch.value.trim().toLowerCase();
|
||||
if (!keyword) return colorList.value;
|
||||
return colorList.value.filter(color => color.key.toLowerCase().includes(keyword));
|
||||
});
|
||||
|
||||
const toggleSearch = async () => {
|
||||
showSearch.value = !showSearch.value;
|
||||
if (showSearch.value) {
|
||||
await nextTick();
|
||||
searchInputRef.value?.focus();
|
||||
} else {
|
||||
colorSearch.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
// 处理重置按钮点击
|
||||
const handleResetClick = () => {
|
||||
if (resetButtonState.value.confirming) {
|
||||
@@ -193,7 +214,7 @@ const updateSystemTheme = async (event: Event) => {
|
||||
await themeStore.setTheme(selectedSystemTheme);
|
||||
|
||||
const availableThemes = themeStore.availableThemes;
|
||||
const currentThemeName = currentColors.value?.name;
|
||||
const currentThemeName = currentColors.value?.themeName;
|
||||
const isCurrentThemeAvailable = availableThemes.some(t => t.name === currentThemeName);
|
||||
|
||||
if (!isCurrentThemeAvailable && availableThemes.length > 0) {
|
||||
@@ -242,7 +263,7 @@ const handlePickerClose = () => {
|
||||
<!-- 预设主题选择 -->
|
||||
<SettingItem :title="t('settings.presetTheme')">
|
||||
<select class="select-input" v-model="currentThemeName" :disabled="hasUnsavedChanges">
|
||||
<option v-for="theme in themeStore.availableThemes" :key="theme.id" :value="theme.name">
|
||||
<option v-for="theme in themeStore.availableThemes" :key="theme.name" :value="theme.name">
|
||||
{{ theme.name }}
|
||||
</option>
|
||||
</select>
|
||||
@@ -251,14 +272,27 @@ const handlePickerClose = () => {
|
||||
|
||||
<!-- 自定义主题颜色配置 -->
|
||||
<SettingSection :title="t('settings.customThemeColors')">
|
||||
<template #title>
|
||||
<div class="theme-section-title">
|
||||
<span class="section-title-text">{{ t('settings.customThemeColors') }}</span>
|
||||
<span v-if="currentColors.name" class="current-theme-name">{{ currentColors.name }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template #title-right>
|
||||
<div class="theme-controls">
|
||||
<div :class="['theme-search-wrapper', showSearch ? 'active' : '']">
|
||||
<input
|
||||
ref="searchInputRef"
|
||||
class="theme-search-input"
|
||||
type="text"
|
||||
v-model="colorSearch"
|
||||
placeholder="Search..."
|
||||
/>
|
||||
</div>
|
||||
<button class="search-toggle-button" @click="toggleSearch" :title="showSearch ? '关闭搜索' : '搜索颜色'">
|
||||
<svg v-if="!showSearch" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="11" cy="11" r="8"></circle>
|
||||
<path d="m21 21-4.35-4.35"></path>
|
||||
</svg>
|
||||
<svg v-else xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
v-if="!hasUnsavedChanges"
|
||||
:class="['reset-button', resetButtonState.confirming ? 'reset-button-confirming' : '']"
|
||||
@@ -280,7 +314,7 @@ const handlePickerClose = () => {
|
||||
<!-- 颜色列表 -->
|
||||
<div class="color-list">
|
||||
<SettingItem
|
||||
v-for="color in colorList"
|
||||
v-for="color in filteredColorList"
|
||||
:key="color.key"
|
||||
:title="color.label"
|
||||
class="color-setting-item"
|
||||
@@ -318,10 +352,6 @@ const handlePickerClose = () => {
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.settings-page {
|
||||
//max-width: 1000px;
|
||||
}
|
||||
|
||||
.select-input {
|
||||
min-width: 140px;
|
||||
padding: 6px 10px;
|
||||
@@ -349,27 +379,6 @@ const handlePickerClose = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 主题部分标题
|
||||
.theme-section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.section-title-text {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.current-theme-name {
|
||||
font-size: 13px;
|
||||
color: var(--settings-text-secondary);
|
||||
font-weight: normal;
|
||||
padding: 2px 8px;
|
||||
background-color: var(--settings-input-bg);
|
||||
border: 1px solid var(--settings-input-border);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
// 主题控制区域
|
||||
.theme-controls {
|
||||
display: flex;
|
||||
@@ -441,6 +450,78 @@ const handlePickerClose = () => {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.theme-search-wrapper {
|
||||
width: 0;
|
||||
overflow: hidden;
|
||||
transition: width 0.3s ease;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.theme-search-wrapper.active {
|
||||
width: 150px;
|
||||
margin-right: 8px;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.theme-search-input {
|
||||
width: 100%;
|
||||
padding: 6px 12px;
|
||||
border: 1px solid var(--settings-input-border);
|
||||
border-radius: 20px;
|
||||
background-color: var(--settings-input-bg);
|
||||
color: var(--settings-text);
|
||||
font-size: 12px;
|
||||
transition: all 0.2s ease;
|
||||
height: 25px;
|
||||
box-sizing: border-box;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: #4a9eff;
|
||||
background-color: var(--settings-card-bg);
|
||||
box-shadow: 0 0 0 3px rgba(74, 158, 255, 0.1);
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: var(--settings-text-secondary);
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
.search-toggle-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
margin-right: 8px;
|
||||
padding: 0;
|
||||
border: 1px solid var(--settings-input-border);
|
||||
border-radius: 50%;
|
||||
background-color: var(--settings-button-bg);
|
||||
color: var(--settings-button-text);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
svg {
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: #4a9eff;
|
||||
background-color: var(--settings-button-hover-bg);
|
||||
transform: scale(1.05);
|
||||
|
||||
svg {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
}
|
||||
|
||||
.color-setting-item {
|
||||
:deep(.setting-item-content) {
|
||||
align-items: center;
|
||||
@@ -495,4 +576,4 @@ const handlePickerClose = () => {
|
||||
color: var(--settings-text-secondary);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -19,16 +19,17 @@ onMounted(async () => {
|
||||
|
||||
// 字体选择选项
|
||||
const fontFamilyOptions = computed(() => configStore.fontOptions);
|
||||
const currentFontFamily = computed(() => configStore.config.editing.fontFamily);
|
||||
|
||||
// 字体选择
|
||||
const handleFontFamilyChange = async (event: Event) => {
|
||||
const target = event.target as HTMLSelectElement;
|
||||
const fontFamily = target.value;
|
||||
if (fontFamily) {
|
||||
await configStore.setFontFamily(fontFamily);
|
||||
const fontFamilyModel = computed({
|
||||
get: () =>
|
||||
configStore.config.editing.fontFamily ||
|
||||
fontFamilyOptions.value[0]?.value ||
|
||||
'',
|
||||
set: async (fontFamily: string) => {
|
||||
if (fontFamily) {
|
||||
await configStore.setFontFamily(fontFamily);
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// 字体粗细选项
|
||||
const fontWeightOptions = [
|
||||
@@ -44,10 +45,14 @@ const fontWeightOptions = [
|
||||
];
|
||||
|
||||
// 字体粗细选择
|
||||
const handleFontWeightChange = async (event: Event) => {
|
||||
const target = event.target as HTMLSelectElement;
|
||||
await configStore.setFontWeight(target.value);
|
||||
};
|
||||
const fontWeightModel = computed({
|
||||
get: () => configStore.config.editing.fontWeight || fontWeightOptions[0].value,
|
||||
set: async (value: string) => {
|
||||
if (value) {
|
||||
await configStore.setFontWeight(value);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 行高控制
|
||||
const increaseLineHeight = async () => {
|
||||
@@ -118,8 +123,7 @@ const handleAutoSaveDelayChange = async (event: Event) => {
|
||||
>
|
||||
<select
|
||||
class="font-family-select"
|
||||
:value="currentFontFamily"
|
||||
@change="handleFontFamilyChange"
|
||||
v-model="fontFamilyModel"
|
||||
>
|
||||
<option
|
||||
v-for="option in fontFamilyOptions"
|
||||
@@ -146,8 +150,7 @@ const handleAutoSaveDelayChange = async (event: Event) => {
|
||||
>
|
||||
<select
|
||||
class="font-weight-select"
|
||||
:value="configStore.config.editing.fontWeight"
|
||||
@change="handleFontWeightChange"
|
||||
v-model="fontWeightModel"
|
||||
>
|
||||
<option
|
||||
v-for="option in fontWeightOptions"
|
||||
@@ -385,4 +388,4 @@ const handleAutoSaveDelayChange = async (event: Event) => {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user