Complete the custom editor theme

This commit is contained in:
2025-07-11 23:02:25 +08:00
parent b5510d605c
commit a720a4cfb8
26 changed files with 1838 additions and 369 deletions

View File

@@ -1,18 +1,18 @@
<script setup lang="ts">
import {onBeforeUnmount, onMounted, ref, watch} from 'vue';
import {onBeforeUnmount, onMounted, ref} from 'vue';
import {useEditorStore} from '@/stores/editorStore';
import {useDocumentStore} from '@/stores/documentStore';
import {useConfigStore} from '@/stores/configStore';
import {createWheelZoomHandler} from './basic/wheelZoomExtension';
import Toolbar from '@/components/toolbar/Toolbar.vue';
import {useWindowStore} from "@/stores/windowStore";
import LoadingScreen from '@/components/loading/LoadingScreen.vue';
const editorStore = useEditorStore();
const documentStore = useDocumentStore();
const configStore = useConfigStore();
const windowStore = useWindowStore();
const editorElement = ref<HTMLElement | null>(null);
// 创建滚轮缩放处理器
@@ -25,7 +25,6 @@ onMounted(async () => {
if (!editorElement.value) return;
// 从URL查询参数中获取documentId
const urlDocumentId = windowStore.currentDocumentId ? parseInt(windowStore.currentDocumentId) : undefined;
// 初始化文档存储优先使用URL参数中的文档ID
@@ -48,6 +47,7 @@ onBeforeUnmount(() => {
<template>
<div class="editor-container">
<LoadingScreen v-if="editorStore.isLoading" text="VOIDRAFT" />
<div ref="editorElement" class="editor"></div>
<Toolbar/>
</div>
@@ -60,6 +60,7 @@ onBeforeUnmount(() => {
overflow: hidden;
display: flex;
flex-direction: column;
position: relative;
.editor {
width: 100%;

View File

@@ -1,8 +1,9 @@
import { Extension, Compartment } from '@codemirror/state';
import { EditorView } from '@codemirror/view';
import { SystemThemeType } from '@/../bindings/voidraft/internal/models/models';
import { dark } from '@/views/editor/theme/dark';
import { light } from '@/views/editor/theme/light';
import { createDarkTheme } from '@/views/editor/theme/dark';
import { createLightTheme } from '@/views/editor/theme/light';
import { useThemeStore } from '@/stores/themeStore';
// 主题区间 - 用于动态切换主题
export const themeCompartment = new Compartment();
@@ -11,6 +12,8 @@ export const themeCompartment = new Compartment();
* 根据主题类型获取主题扩展
*/
const getThemeExtension = (themeType: SystemThemeType): Extension => {
const themeStore = useThemeStore();
// 处理 auto 主题类型
let actualTheme: SystemThemeType = themeType;
if (themeType === SystemThemeType.SystemThemeAuto) {
@@ -19,13 +22,11 @@ const getThemeExtension = (themeType: SystemThemeType): Extension => {
: SystemThemeType.SystemThemeLight;
}
// 直接返回对应的主题扩展
switch (actualTheme) {
case SystemThemeType.SystemThemeLight:
return light;
case SystemThemeType.SystemThemeDark:
default:
return dark;
// 根据主题类型创建主题
if (actualTheme === SystemThemeType.SystemThemeLight) {
return createLightTheme(themeStore.themeColors.lightTheme);
} else {
return createDarkTheme(themeStore.themeColors.darkTheme);
}
};
@@ -45,9 +46,13 @@ export const updateEditorTheme = (view: EditorView, themeType: SystemThemeType):
return;
}
const extension = getThemeExtension(themeType);
view.dispatch({
effects: themeCompartment.reconfigure(extension)
});
try {
const extension = getThemeExtension(themeType);
view.dispatch({
effects: themeCompartment.reconfigure(extension)
});
} catch (error) {
console.error('Failed to update editor theme:', error);
}
};

View File

@@ -3,8 +3,7 @@ import { Range } from '@codemirror/state';
// 生成彩虹颜色数组
function generateColors(): string[] {
return [
'red', 'orange', 'yellow', 'green', 'blue', 'indigo', 'violet',
return ['red', 'orange', 'yellow', 'green', 'blue', 'indigo', 'violet'
];
}
@@ -75,13 +74,13 @@ export default function rainbowBracketsExtension() {
rainbowBracketsPlugin,
EditorView.baseTheme({
// 为每种颜色定义CSS样式
'.cm-rainbow-bracket-red': { color: 'red' },
'.cm-rainbow-bracket-orange': { color: 'orange' },
'.cm-rainbow-bracket-yellow': { color: 'yellow' },
'.cm-rainbow-bracket-green': { color: 'green' },
'.cm-rainbow-bracket-blue': { color: 'blue' },
'.cm-rainbow-bracket-indigo': { color: 'indigo' },
'.cm-rainbow-bracket-violet': { color: 'violet' },
'.cm-rainbow-bracket-red': { color: '#FF6B6B' },
'.cm-rainbow-bracket-orange': { color: '#FF9E6B' },
'.cm-rainbow-bracket-yellow': { color: '#FFD166' },
'.cm-rainbow-bracket-green': { color: '#06D6A0' },
'.cm-rainbow-bracket-blue': { color: '#118AB2' },
'.cm-rainbow-bracket-indigo': { color: '#6B5B95' },
'.cm-rainbow-bracket-violet': { color: '#9B5DE5' },
}),
];
}

View File

@@ -65,6 +65,10 @@ export class ExtensionManager {
// 注册的扩展工厂
private extensionFactories = new Map<ExtensionID, ExtensionFactory>()
// 防抖处理
private debounceTimers = new Map<ExtensionID, number>()
private debounceDelay = 300 // 默认防抖时间为300毫秒
/**
* 注册扩展工厂
* @param id 扩展ID
@@ -187,13 +191,24 @@ export class ExtensionManager {
}
/**
* 更新单个扩展配置并应用到所有视图
* 更新单个扩展配置并应用到所有视图(带防抖功能)
* @param id 扩展ID
* @param enabled 是否启用
* @param config 扩展配置
*/
updateExtension(id: ExtensionID, enabled: boolean, config: any = {}): void {
this.updateExtensionImmediate(id, enabled, config)
// 清除之前的定时器
if (this.debounceTimers.has(id)) {
window.clearTimeout(this.debounceTimers.get(id))
}
// 设置新的定时器
const timerId = window.setTimeout(() => {
this.updateExtensionImmediate(id, enabled, config)
this.debounceTimers.delete(id)
}, this.debounceDelay)
this.debounceTimers.set(id, timerId)
}
/**
@@ -262,6 +277,14 @@ export class ExtensionManager {
enabled: boolean
config: any
}>): void {
// 清除所有相关的防抖定时器
for (const update of updates) {
if (this.debounceTimers.has(update.id)) {
window.clearTimeout(this.debounceTimers.get(update.id))
this.debounceTimers.delete(update.id)
}
}
// 更新所有扩展状态
for (const update of updates) {
// 获取扩展状态
@@ -357,6 +380,12 @@ export class ExtensionManager {
* 销毁管理器
*/
destroy(): void {
// 清除所有防抖定时器
for (const timerId of this.debounceTimers.values()) {
window.clearTimeout(timerId)
}
this.debounceTimers.clear()
this.viewsMap.clear()
this.activeViewId = null
this.extensionFactories.clear()

View File

@@ -2,203 +2,205 @@ import {EditorView} from '@codemirror/view';
import {HighlightStyle, syntaxHighlighting} from '@codemirror/language';
import {tags} from '@lezer/highlight';
const colors = {
// 基础色调
background: '#252B37', // 主背景色
// backgroundAlt: '#252B37', // 交替背景色
backgroundSecondary: '#213644', // 次要背景色
surface: '#474747', // 面板背景
// 默认深色主题颜色
export const defaultDarkColors = {
// 基础色调
background: '#252B37', // 背景色
backgroundSecondary: '#213644', // 次要背景色
surface: '#474747', // 面板背景
// 文本颜色
foreground: '#9BB586', // 主文本色
foregroundSecondary: '#9c9c9c', // 次要文本色
comment: '#6272a4', // 注释色
// 文本颜色
foreground: '#9BB586', // 主文本色
foregroundSecondary: '#9c9c9c', // 次要文本色
comment: '#6272a4', // 注释色
// 语法高亮色
keyword: '#ff79c6', // 关键字
string: '#f1fa8c', // 字符串
function: '#50fa7b', // 函数名
number: '#bd93f9', // 数字
operator: '#ff79c6', // 操作符
variable: '#8fbcbb', // 变量
type: '#8be9fd', // 类型
// 语法高亮色
keyword: '#ff79c6', // 关键字
string: '#f1fa8c', // 字符串
function: '#50fa7b', // 函数名
number: '#bd93f9', // 数字
operator: '#ff79c6', // 操作符
variable: '#8fbcbb', // 变量
type: '#8be9fd', // 类型
// 界面元素
cursor: '#fff', // 光标
selection: '#0865a9aa', // 选中背景
selectionBlur: '#225377aa', // 失焦选中背景
activeLine: 'rgba(255,255,255,0.04)', // 当前行高亮
lineNumber: 'rgba(255,255,255, 0.15)', // 行号
activeLineNumber: 'rgba(255,255,255, 0.6)', // 活动行号
// 界面元素
cursor: '#ffffff', // 光标
selection: '#0865a9', // 选中背景
selectionBlur: '#225377', // 失焦选中背景
activeLine: '#ffffff0a', // 当前行高亮
lineNumber: '#ffffff26', // 行号
activeLineNumber: '#ffffff99', // 活动行号
// 边框和分割线
border: '#1e222a', // 边框色
borderLight: 'rgba(255,255,255, 0.1)', // 浅色边框
// 边框和分割线
borderColor: '#1e222a', // 边框色
borderLight: '#ffffff19', // 浅色边框
// 搜索和匹配
searchMatch: '#8fbcbb', // 搜索匹配
matchingBracket: 'rgba(255,255,255,0.1)', // 匹配括号
// 搜索和匹配
searchMatch: '#8fbcbb', // 搜索匹配
matchingBracket: '#ffffff19', // 匹配括号
};
const darkTheme = EditorView.theme({
// 创建深色主题
export function createDarkTheme(colors = defaultDarkColors) {
const darkTheme = EditorView.theme({
'&': {
color: colors.foreground,
backgroundColor: colors.background,
color: colors.foreground,
backgroundColor: colors.background,
},
// 确保编辑器容器背景一致
'.cm-editor': {
backgroundColor: colors.background,
backgroundColor: colors.background,
},
// 确保滚动区域背景一致
'.cm-scroller': {
backgroundColor: colors.background,
backgroundColor: colors.background,
},
// 编辑器内容
'.cm-content': {
caretColor: colors.cursor,
paddingTop: '4px',
caretColor: colors.cursor,
paddingTop: '4px',
},
// 光标
'.cm-cursor, .cm-dropCursor': {
borderLeftColor: colors.cursor,
borderLeftWidth: '2px',
paddingTop: '4px',
marginTop: '-2px',
borderLeftColor: colors.cursor,
borderLeftWidth: '2px',
paddingTop: '4px',
marginTop: '-2px',
},
// 选择
'.cm-selectionBackground': {
backgroundColor: colors.selectionBlur,
backgroundColor: colors.selectionBlur,
},
'&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground': {
backgroundColor: colors.selection,
backgroundColor: colors.selection,
},
'.cm-activeLine.code-empty-block-selected': {
backgroundColor: colors.selection,
backgroundColor: colors.selection,
},
// 当前行高亮
'.cm-activeLine': {
backgroundColor: colors.activeLine
backgroundColor: colors.activeLine
},
// 行号区域
'.cm-gutters': {
backgroundColor: 'rgba(0,0,0, 0.1)',
color: colors.lineNumber,
border: 'none',
padding: '0 2px 0 4px',
userSelect: 'none',
backgroundColor: 'rgba(0,0,0, 0.1)',
color: colors.lineNumber,
border: 'none',
padding: '0 2px 0 4px',
userSelect: 'none',
},
'.cm-activeLineGutter': {
backgroundColor: 'transparent',
color: colors.activeLineNumber,
backgroundColor: 'transparent',
color: colors.activeLineNumber,
},
// 折叠功能
'.cm-foldGutter': {
marginLeft: '0px',
marginLeft: '0px',
},
'.cm-foldGutter .cm-gutterElement': {
opacity: 0,
transition: 'opacity 400ms',
opacity: 0,
transition: 'opacity 400ms',
},
'.cm-gutters:hover .cm-gutterElement': {
opacity: 1,
opacity: 1,
},
'.cm-foldPlaceholder': {
backgroundColor: 'transparent',
border: 'none',
color: '#ddd',
backgroundColor: 'transparent',
border: 'none',
color: '#ddd',
},
// 搜索匹配
'.cm-searchMatch': {
backgroundColor: 'transparent',
outline: `1px solid ${colors.searchMatch}`,
backgroundColor: 'transparent',
outline: `1px solid ${colors.searchMatch}`,
},
'.cm-searchMatch.cm-searchMatch-selected': {
backgroundColor: colors.foreground,
color: colors.background,
backgroundColor: colors.foreground,
color: colors.background,
},
'.cm-selectionMatch': {
backgroundColor: '#50606D',
backgroundColor: '#50606D',
},
// 括号匹配
'&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket': {
outline: `0.5px solid ${colors.searchMatch}`,
outline: `0.5px solid ${colors.searchMatch}`,
},
'&.cm-focused .cm-matchingBracket': {
backgroundColor: colors.matchingBracket,
color: 'inherit',
backgroundColor: colors.matchingBracket,
color: 'inherit',
},
'&.cm-focused .cm-nonmatchingBracket': {
outline: '0.5px solid #bc8f8f',
outline: '0.5px solid #bc8f8f',
},
// 编辑器焦点
'&.cm-editor.cm-focused': {
outline: 'none',
outline: 'none',
},
// 工具提示
'.cm-tooltip': {
border: 'none',
backgroundColor: colors.surface,
border: 'none',
backgroundColor: colors.surface,
},
'.cm-tooltip .cm-tooltip-arrow:before': {
borderTopColor: 'transparent',
borderBottomColor: 'transparent',
borderTopColor: 'transparent',
borderBottomColor: 'transparent',
},
'.cm-tooltip .cm-tooltip-arrow:after': {
borderTopColor: colors.surface,
borderBottomColor: colors.surface,
borderTopColor: colors.surface,
borderBottomColor: colors.surface,
},
'.cm-tooltip-autocomplete': {
'& > ul > li[aria-selected]': {
backgroundColor: colors.activeLine,
color: colors.foreground,
},
'& > ul > li[aria-selected]': {
backgroundColor: colors.activeLine,
color: colors.foreground,
},
},
// 代码块层
'.code-blocks-layer': {
width: '100%',
width: '100%',
},
'.code-blocks-layer .block-even, .code-blocks-layer .block-odd': {
width: '100%',
boxSizing: 'content-box',
width: '100%',
boxSizing: 'content-box',
},
'.code-blocks-layer .block-even': {
background: colors.background,
borderTop: `1px solid ${colors.border}`,
background: colors.background,
borderTop: `1px solid ${colors.borderColor}`,
},
'.code-blocks-layer .block-even:first-child': {
borderTop: 'none',
borderTop: 'none',
},
'.code-blocks-layer .block-odd': {
background: colors.backgroundSecondary,
borderTop: `1px solid ${colors.border}`,
background: colors.backgroundSecondary,
borderTop: `1px solid ${colors.borderColor}`,
},
// 代码块开始标记
'.code-block-start': {
height: '12px',
position: 'relative',
height: '12px',
position: 'relative',
},
'.code-block-start.first': {
height: '0px',
height: '0px',
},
}, {dark: true});
}, {dark: true});
// 语法高亮样式
const darkHighlightStyle = HighlightStyle.define([
// 语法高亮样式
const darkHighlightStyle = HighlightStyle.define([
{tag: tags.keyword, color: colors.keyword},
{tag: [tags.name, tags.deleted, tags.character, tags.propertyName, tags.macroName], color: colors.variable},
{tag: [tags.variableName], color: colors.variable},
@@ -229,9 +231,13 @@ const darkHighlightStyle = HighlightStyle.define([
{tag: [tags.heading1, tags.heading2], fontSize: '1.4em'},
{tag: [tags.heading3, tags.heading4], fontSize: '1.2em'},
{tag: [tags.heading5, tags.heading6], fontSize: '1.1em'},
]);
]);
export const dark = [
return [
darkTheme,
syntaxHighlighting(darkHighlightStyle),
];
];
}
// 默认深色主题
export const dark = createDarkTheme(defaultDarkColors);

View File

@@ -2,10 +2,10 @@ import { EditorView } from '@codemirror/view';
import { HighlightStyle, syntaxHighlighting } from '@codemirror/language';
import { tags } from '@lezer/highlight';
const colors = {
// 默认浅色主题颜色
export const defaultLightColors = {
// 基础色调
background: '#ffffff', // 主背景色
// backgroundAlt: '#f4f8f4', // 交替背景色
backgroundSecondary: '#f1faf1', // 次要背景色
surface: '#f5f5f5', // 面板背景
@@ -24,216 +24,221 @@ const colors = {
type: '#6f42c1', // 类型
// 界面元素
cursor: '#000', // 光标
selection: '#77baff8c', // 选中背景
selectionBlur: '#b2c2ca85', // 失焦选中背景
activeLine: 'rgba(0,0,0, 0.04)', // 当前行高亮
lineNumber: 'rgba(0,0,0, 0.25)', // 行号
activeLineNumber: 'rgba(0,0,0, 0.6)', // 活动行号
cursor: '#000000', // 光标
selection: '#77baff', // 选中背景
selectionBlur: '#b2c2ca', // 失焦选中背景
activeLine: '#0000000a', // 当前行高亮
lineNumber: '#00000040', // 行号
activeLineNumber: '#000000aa', // 活动行号
// 边框和分割线
border: '#dfdfdf', // 边框色
borderLight: 'rgba(0,0,0, 0.05)', // 浅色边框
borderColor: '#dfdfdf', // 边框色
borderLight: '#0000000c', // 浅色边框
// 搜索和匹配
searchMatch: '#005cc5', // 搜索匹配
matchingBracket: 'rgba(0,0,0,0.1)', // 匹配括号
matchingBracket: '#00000019', // 匹配括号
};
const lightTheme = EditorView.theme({
'&': {
color: colors.foreground,
backgroundColor: colors.background,
},
// 确保编辑器容器背景一致
'.cm-editor': {
backgroundColor: colors.background,
},
// 确保滚动区域背景一致
'.cm-scroller': {
backgroundColor: colors.background,
},
// 编辑器内容
'.cm-content': {
caretColor: colors.cursor,
paddingTop: '4px',
},
// 光标
'.cm-cursor, .cm-dropCursor': {
borderLeftColor: colors.cursor,
borderLeftWidth: '2px',
paddingTop: '4px',
marginTop: '-2px',
},
// 选择
'.cm-selectionBackground': {
backgroundColor: colors.selectionBlur,
},
'&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground': {
backgroundColor: colors.selection,
},
'.cm-activeLine.code-empty-block-selected': {
backgroundColor: colors.selection,
},
// 当前行高亮
'.cm-activeLine': {
backgroundColor: colors.activeLine
},
// 行号区域
'.cm-gutters': {
backgroundColor: 'rgba(0,0,0, 0.04)',
color: colors.lineNumber,
border: 'none',
borderRight: `1px solid ${colors.borderLight}`,
padding: '0 2px 0 4px',
userSelect: 'none',
},
'.cm-activeLineGutter': {
backgroundColor: 'transparent',
color: colors.activeLineNumber,
},
// 折叠功能
'.cm-foldGutter': {
marginLeft: '0px',
},
'.cm-foldGutter .cm-gutterElement': {
opacity: 0,
transition: 'opacity 400ms',
},
'.cm-gutters:hover .cm-gutterElement': {
opacity: 1,
},
'.cm-foldPlaceholder': {
backgroundColor: 'transparent',
border: 'none',
color: colors.comment,
},
// 搜索匹配
'.cm-searchMatch': {
backgroundColor: 'transparent',
outline: `1px solid ${colors.searchMatch}`,
},
'.cm-searchMatch.cm-searchMatch-selected': {
backgroundColor: colors.searchMatch,
color: colors.background,
},
'.cm-selectionMatch': {
backgroundColor: '#e6f3ff',
},
// 括号匹配
'&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket': {
outline: `0.5px solid ${colors.searchMatch}`,
},
'&.cm-focused .cm-matchingBracket': {
backgroundColor: colors.matchingBracket,
color: 'inherit',
},
'&.cm-focused .cm-nonmatchingBracket': {
outline: '0.5px solid #d73a49',
},
// 编辑器焦点
'&.cm-editor.cm-focused': {
outline: 'none',
},
// 工具提示
'.cm-tooltip': {
border: 'none',
backgroundColor: colors.surface,
boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
},
'.cm-tooltip .cm-tooltip-arrow:before': {
borderTopColor: 'transparent',
borderBottomColor: 'transparent',
},
'.cm-tooltip .cm-tooltip-arrow:after': {
borderTopColor: colors.surface,
borderBottomColor: colors.surface,
},
'.cm-tooltip-autocomplete': {
'& > ul > li[aria-selected]': {
backgroundColor: colors.activeLine,
// 创建浅色主题
export function createLightTheme(colors = defaultLightColors) {
const lightTheme = EditorView.theme({
'&': {
color: colors.foreground,
backgroundColor: colors.background,
},
// 确保编辑器容器背景一致
'.cm-editor': {
backgroundColor: colors.background,
},
// 确保滚动区域背景一致
'.cm-scroller': {
backgroundColor: colors.background,
},
// 编辑器内容
'.cm-content': {
caretColor: colors.cursor,
paddingTop: '4px',
},
// 光标
'.cm-cursor, .cm-dropCursor': {
borderLeftColor: colors.cursor,
borderLeftWidth: '2px',
paddingTop: '4px',
marginTop: '-2px',
},
// 选择
'.cm-selectionBackground': {
backgroundColor: colors.selectionBlur,
},
'&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground': {
backgroundColor: colors.selection,
},
'.cm-activeLine.code-empty-block-selected': {
backgroundColor: colors.selection,
},
// 当前行高亮
'.cm-activeLine': {
backgroundColor: colors.activeLine
},
// 行号区域
'.cm-gutters': {
backgroundColor: 'rgba(0,0,0, 0.04)',
color: colors.lineNumber,
border: 'none',
borderRight: `1px solid ${colors.borderLight}`,
padding: '0 2px 0 4px',
userSelect: 'none',
},
'.cm-activeLineGutter': {
backgroundColor: 'transparent',
color: colors.activeLineNumber,
},
// 折叠功能
'.cm-foldGutter': {
marginLeft: '0px',
},
'.cm-foldGutter .cm-gutterElement': {
opacity: 0,
transition: 'opacity 400ms',
},
'.cm-gutters:hover .cm-gutterElement': {
opacity: 1,
},
'.cm-foldPlaceholder': {
backgroundColor: 'transparent',
border: 'none',
color: colors.comment,
},
},
// 代码块层
'.code-blocks-layer': {
width: '100%',
},
'.code-blocks-layer .block-even, .code-blocks-layer .block-odd': {
width: '100%',
boxSizing: 'content-box',
},
'.code-blocks-layer .block-even': {
background: colors.background,
borderTop: `1px solid ${colors.border}`,
},
'.code-blocks-layer .block-even:first-child': {
borderTop: 'none',
},
'.code-blocks-layer .block-odd': {
background: colors.backgroundSecondary,
borderTop: `1px solid ${colors.border}`,
},
// 代码块开始标记
'.code-block-start': {
height: '12px',
},
'.code-block-start.first': {
height: '0px',
},
}, { dark: false });
// 语法高亮样式
const lightHighlightStyle = HighlightStyle.define([
{ tag: tags.keyword, color: colors.keyword },
{ tag: [tags.name, tags.deleted, tags.character, tags.propertyName, tags.macroName], color: colors.variable },
{ tag: [tags.variableName], color: colors.variable },
{ tag: [tags.function(tags.variableName)], color: colors.function },
{ tag: [tags.labelName], color: colors.operator },
{ tag: [tags.color, tags.constant(tags.name), tags.standard(tags.name)], color: colors.keyword },
{ tag: [tags.definition(tags.name), tags.separator], color: colors.function },
{ tag: [tags.brace], color: colors.variable },
{ tag: [tags.annotation], color: '#d73a49' },
{ tag: [tags.number, tags.changed, tags.annotation, tags.modifier, tags.self, tags.namespace], color: colors.number },
{ tag: [tags.typeName, tags.className], color: colors.type },
{ tag: [tags.operator, tags.operatorKeyword], color: colors.operator },
{ tag: [tags.tagName], color: colors.type },
{ tag: [tags.squareBracket], color: colors.keyword },
{ tag: [tags.angleBracket], color: colors.operator },
{ tag: [tags.attributeName], color: colors.variable },
{ tag: [tags.regexp], color: colors.string },
{ tag: [tags.quote], color: colors.comment },
{ tag: [tags.string], color: colors.string },
{ tag: tags.link, color: colors.function, textDecoration: 'underline' },
{ tag: [tags.url, tags.escape, tags.special(tags.string)], color: colors.string },
{ tag: [tags.meta], color: colors.comment },
{ tag: [tags.comment], color: colors.comment, fontStyle: 'italic' },
{ tag: tags.strong, fontWeight: 'bold' },
{ tag: tags.emphasis, fontStyle: 'italic' },
{ tag: tags.strikethrough, textDecoration: 'line-through' },
{ tag: tags.heading, fontWeight: 'bold', color: colors.keyword },
{ tag: [tags.heading1, tags.heading2], fontSize: '1.4em' },
{ tag: [tags.heading3, tags.heading4], fontSize: '1.2em' },
{ tag: [tags.heading5, tags.heading6], fontSize: '1.1em' },
]);
// 搜索匹配
'.cm-searchMatch': {
backgroundColor: 'transparent',
outline: `1px solid ${colors.searchMatch}`,
},
'.cm-searchMatch.cm-searchMatch-selected': {
backgroundColor: colors.searchMatch,
color: colors.background,
},
'.cm-selectionMatch': {
backgroundColor: '#e6f3ff',
},
// 括号匹配
'&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket': {
outline: `0.5px solid ${colors.searchMatch}`,
},
'&.cm-focused .cm-matchingBracket': {
backgroundColor: colors.matchingBracket,
color: 'inherit',
},
'&.cm-focused .cm-nonmatchingBracket': {
outline: '0.5px solid #d73a49',
},
// 编辑器焦点
'&.cm-editor.cm-focused': {
outline: 'none',
},
// 工具提示
'.cm-tooltip': {
border: 'none',
backgroundColor: colors.surface,
boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
},
'.cm-tooltip .cm-tooltip-arrow:before': {
borderTopColor: 'transparent',
borderBottomColor: 'transparent',
},
'.cm-tooltip .cm-tooltip-arrow:after': {
borderTopColor: colors.surface,
borderBottomColor: colors.surface,
},
'.cm-tooltip-autocomplete': {
'& > ul > li[aria-selected]': {
backgroundColor: colors.activeLine,
color: colors.foreground,
},
},
// 代码块层
'.code-blocks-layer': {
width: '100%',
},
'.code-blocks-layer .block-even, .code-blocks-layer .block-odd': {
width: '100%',
boxSizing: 'content-box',
},
'.code-blocks-layer .block-even': {
background: colors.background,
borderTop: `1px solid ${colors.borderColor}`,
},
'.code-blocks-layer .block-even:first-child': {
borderTop: 'none',
},
'.code-blocks-layer .block-odd': {
background: colors.backgroundSecondary,
borderTop: `1px solid ${colors.borderColor}`,
},
// 代码块开始标记
'.code-block-start': {
height: '12px',
},
'.code-block-start.first': {
height: '0px',
},
}, { dark: false });
export const light = [
lightTheme,
syntaxHighlighting(lightHighlightStyle),
];
// 语法高亮样式
const lightHighlightStyle = HighlightStyle.define([
{ tag: tags.keyword, color: colors.keyword },
{ tag: [tags.name, tags.deleted, tags.character, tags.propertyName, tags.macroName], color: colors.variable },
{ tag: [tags.variableName], color: colors.variable },
{ tag: [tags.function(tags.variableName)], color: colors.function },
{ tag: [tags.labelName], color: colors.operator },
{ tag: [tags.color, tags.constant(tags.name), tags.standard(tags.name)], color: colors.keyword },
{ tag: [tags.definition(tags.name), tags.separator], color: colors.function },
{ tag: [tags.brace], color: colors.variable },
{ tag: [tags.annotation], color: '#d73a49' },
{ tag: [tags.number, tags.changed, tags.annotation, tags.modifier, tags.self, tags.namespace], color: colors.number },
{ tag: [tags.typeName, tags.className], color: colors.type },
{ tag: [tags.operator, tags.operatorKeyword], color: colors.operator },
{ tag: [tags.tagName], color: colors.type },
{ tag: [tags.squareBracket], color: colors.keyword },
{ tag: [tags.angleBracket], color: colors.operator },
{ tag: [tags.attributeName], color: colors.variable },
{ tag: [tags.regexp], color: colors.string },
{ tag: [tags.quote], color: colors.comment },
{ tag: [tags.string], color: colors.string },
{ tag: tags.link, color: colors.function, textDecoration: 'underline' },
{ tag: [tags.url, tags.escape, tags.special(tags.string)], color: colors.string },
{ tag: [tags.meta], color: colors.comment },
{ tag: [tags.comment], color: colors.comment, fontStyle: 'italic' },
{ tag: tags.strong, fontWeight: 'bold' },
{ tag: tags.emphasis, fontStyle: 'italic' },
{ tag: tags.strikethrough, textDecoration: 'line-through' },
{ tag: tags.heading, fontWeight: 'bold', color: colors.keyword },
{ tag: [tags.heading1, tags.heading2], fontSize: '1.4em' },
{ tag: [tags.heading3, tags.heading4], fontSize: '1.2em' },
{ tag: [tags.heading5, tags.heading6], fontSize: '1.1em' },
]);
return [
lightTheme,
syntaxHighlighting(lightHighlightStyle),
];
}
// 默认浅色主题
export const light = createLightTheme(defaultLightColors);

View File

@@ -6,7 +6,12 @@ defineProps<{
<template>
<div class="setting-section">
<h2 class="section-title">{{ title }}</h2>
<div class="section-header">
<h2 class="section-title">{{ title }}</h2>
<div class="section-title-right">
<slot name="title-right"></slot>
</div>
</div>
<div class="section-content">
<slot></slot>
</div>
@@ -22,18 +27,29 @@ defineProps<{
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
border: 1px solid var(--settings-border);
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 14px;
background-color: var(--settings-hover);
border-bottom: 1px solid var(--settings-border);
}
.section-title {
font-size: 13px;
font-weight: 600;
margin: 0;
padding: 10px 14px;
background-color: var(--settings-hover);
color: var(--settings-text);
border-bottom: 1px solid var(--settings-border);
}
.section-title-right {
display: flex;
align-items: center;
}
.section-content {
padding: 6px 14px;
}
}
</style>
</style>

View File

@@ -2,14 +2,260 @@
import { useConfigStore } from '@/stores/configStore';
import { useThemeStore } from '@/stores/themeStore';
import { useI18n } from 'vue-i18n';
import { computed, watch, onMounted, ref } from 'vue';
import SettingSection from '../components/SettingSection.vue';
import SettingItem from '../components/SettingItem.vue';
import { SystemThemeType, LanguageType } from '@/../bindings/voidraft/internal/models/models';
import { defaultDarkColors } from '@/views/editor/theme/dark';
import { defaultLightColors } from '@/views/editor/theme/light';
import PickColors from 'vue-pick-colors';
const { t } = useI18n();
const configStore = useConfigStore();
const themeStore = useThemeStore();
// 添加临时颜色状态
const tempColors = ref({
darkTheme: { ...configStore.config.appearance.customTheme?.darkTheme || defaultDarkColors },
lightTheme: { ...configStore.config.appearance.customTheme?.lightTheme || defaultLightColors }
});
// 标记是否有未保存的更改
const hasUnsavedChanges = ref(false);
// 重置按钮状态
const resetButtonState = ref({
confirming: false,
timer: null as number | null
});
// 防抖函数
const debounce = <T extends (...args: any[]) => any>(
func: T,
wait: number
): ((...args: Parameters<T>) => void) => {
let timeout: number | undefined;
return function(...args: Parameters<T>): void {
clearTimeout(timeout);
timeout = window.setTimeout(() => {
func(...args);
}, wait);
};
};
// 当前激活的主题类型(基于当前系统主题)
const activeThemeType = computed(() => {
const isDark =
themeStore.currentTheme === SystemThemeType.SystemThemeDark ||
(themeStore.currentTheme === SystemThemeType.SystemThemeAuto &&
window.matchMedia('(prefers-color-scheme: dark)').matches);
return isDark ? 'darkTheme' : 'lightTheme';
});
// 当前主题的颜色配置 - 使用临时状态
const currentColors = computed(() => {
const themeType = activeThemeType.value;
return tempColors.value[themeType] ||
(themeType === 'darkTheme' ? defaultDarkColors : defaultLightColors);
});
// 获取当前主题模式
const currentThemeMode = computed(() => {
const isDark =
themeStore.currentTheme === SystemThemeType.SystemThemeDark ||
(themeStore.currentTheme === SystemThemeType.SystemThemeAuto &&
window.matchMedia('(prefers-color-scheme: dark)').matches);
return isDark ? 'dark' : 'light';
});
// 监听配置变更,更新临时颜色
watch(
() => configStore.config.appearance.customTheme,
(newValue) => {
if (!hasUnsavedChanges.value) {
tempColors.value = {
darkTheme: { ...newValue.darkTheme },
lightTheme: { ...newValue.lightTheme }
};
}
},
{ deep: true, immediate: true }
);
// 初始化时加载主题颜色
onMounted(() => {
// 使用themeStore中的颜色作为初始值
tempColors.value = {
darkTheme: { ...themeStore.themeColors.darkTheme },
lightTheme: { ...themeStore.themeColors.lightTheme }
};
});
// 颜色配置分组
const colorGroups = computed(() => [
{
key: 'basic',
title: t('settings.themeColors.basic'),
colors: [
{ key: 'background', label: t('settings.themeColors.background') },
{ key: 'backgroundSecondary', label: t('settings.themeColors.backgroundSecondary') },
{ key: 'surface', label: t('settings.themeColors.surface') }
]
},
{
key: 'text',
title: t('settings.themeColors.text'),
colors: [
{ key: 'foreground', label: t('settings.themeColors.foreground') },
{ key: 'foregroundSecondary', label: t('settings.themeColors.foregroundSecondary') },
{ key: 'comment', label: t('settings.themeColors.comment') }
]
},
{
key: 'syntax',
title: t('settings.themeColors.syntax'),
colors: [
{ key: 'keyword', label: t('settings.themeColors.keyword') },
{ key: 'string', label: t('settings.themeColors.string') },
{ key: 'function', label: t('settings.themeColors.function') },
{ key: 'number', label: t('settings.themeColors.number') },
{ key: 'operator', label: t('settings.themeColors.operator') },
{ key: 'variable', label: t('settings.themeColors.variable') },
{ key: 'type', label: t('settings.themeColors.type') }
]
},
{
key: 'interface',
title: t('settings.themeColors.interface'),
colors: [
{ key: 'cursor', label: t('settings.themeColors.cursor') },
{ key: 'selection', label: t('settings.themeColors.selection') },
{ key: 'selectionBlur', label: t('settings.themeColors.selectionBlur') },
{ key: 'activeLine', label: t('settings.themeColors.activeLine') },
{ key: 'lineNumber', label: t('settings.themeColors.lineNumber') },
{ key: 'activeLineNumber', label: t('settings.themeColors.activeLineNumber') }
]
},
{
key: 'border',
title: t('settings.themeColors.border'),
colors: [
{ key: 'borderColor', label: t('settings.themeColors.borderColor') },
{ key: 'borderLight', label: t('settings.themeColors.borderLight') }
]
},
{
key: 'search',
title: t('settings.themeColors.search'),
colors: [
{ key: 'searchMatch', label: t('settings.themeColors.searchMatch') },
{ key: 'matchingBracket', label: t('settings.themeColors.matchingBracket') }
]
}
]);
// 处理重置按钮点击
const handleResetClick = () => {
if (resetButtonState.value.confirming) {
// 如果已经在确认状态,执行重置操作
resetCurrentTheme();
// 重置按钮状态
resetButtonState.value.confirming = false;
if (resetButtonState.value.timer !== null) {
clearTimeout(resetButtonState.value.timer);
resetButtonState.value.timer = null;
}
} else {
// 进入确认状态
resetButtonState.value.confirming = true;
// 设置3秒后自动恢复
resetButtonState.value.timer = window.setTimeout(() => {
resetButtonState.value.confirming = false;
resetButtonState.value.timer = null;
}, 3000);
}
};
// 重置当前主题为默认配置
const resetCurrentTheme = debounce(async () => {
// 使用themeStore的原子重置操作
const themeType = activeThemeType.value;
const success = await themeStore.resetThemeColors(themeType);
if (success) {
// 更新临时颜色状态
tempColors.value = {
darkTheme: { ...themeStore.themeColors.darkTheme },
lightTheme: { ...themeStore.themeColors.lightTheme }
};
// 标记没有未保存的更改
hasUnsavedChanges.value = false;
}
}, 300);
// 更新本地颜色配置 - 仅更新临时状态,不提交到后端
const updateLocalColor = (colorKey: string, value: string) => {
const themeType = activeThemeType.value;
// 更新临时颜色
tempColors.value = {
...tempColors.value,
[themeType]: {
...tempColors.value[themeType],
[colorKey]: value
}
};
// 标记有未保存的更改
hasUnsavedChanges.value = true;
};
// 防抖包装的颜色更新函数
const updateColor = debounce(updateLocalColor, 100);
// 应用颜色更改到系统
const applyChanges = async () => {
try {
// 获取当前主题的自定义颜色
const customTheme = {
darkTheme: tempColors.value.darkTheme,
lightTheme: tempColors.value.lightTheme
};
// 更新themeStore中的颜色
themeStore.updateThemeColors(customTheme.darkTheme, customTheme.lightTheme);
// 保存到配置
await themeStore.saveThemeColors();
// 刷新编辑器主题
themeStore.refreshEditorTheme();
// 清除未保存标记
hasUnsavedChanges.value = false;
} catch (error) {
console.error('Failed to apply theme change:', error);
}
};
// 取消颜色更改
const cancelChanges = () => {
// 恢复到themeStore中的颜色
tempColors.value = {
darkTheme: { ...themeStore.themeColors.darkTheme },
lightTheme: { ...themeStore.themeColors.lightTheme }
};
// 清除未保存标记
hasUnsavedChanges.value = false;
};
// 语言选项
const languageOptions = [
{ value: LanguageType.LangZhCN, label: t('languages.zh-CN') },
@@ -38,6 +284,24 @@ const updateSystemTheme = async (event: Event) => {
await themeStore.setTheme(selectedSystemTheme);
};
// 控制颜色选择器显示状态
const showPickerMap = ref<Record<string, boolean>>({});
// 切换颜色选择器显示状态
const toggleColorPicker = (colorKey: string) => {
showPickerMap.value[colorKey] = !showPickerMap.value[colorKey];
};
// 颜色变更处理
const handleColorChange = (colorKey: string, value: string) => {
updateColor(colorKey, value);
};
// 颜色选择器关闭处理
const handlePickerClose = () => {
// 可以在此添加额外的逻辑
};
</script>
<template>
@@ -61,6 +325,70 @@ const updateSystemTheme = async (event: Event) => {
</select>
</SettingItem>
</SettingSection>
<!-- 自定义主题颜色配置 -->
<SettingSection :title="t('settings.customThemeColors')">
<template #title-right>
<div class="theme-controls">
<button
v-if="!hasUnsavedChanges"
:class="['reset-button', resetButtonState.confirming ? 'reset-button-confirming' : '']"
@click="handleResetClick"
>
{{ resetButtonState.confirming ? t('settings.confirmReset') : t('settings.resetToDefault') }}
</button>
<template v-else>
<button class="apply-button" @click="applyChanges">
{{ t('settings.apply') }}
</button>
<button class="cancel-button" @click="cancelChanges">
{{ t('settings.cancel') }}
</button>
</template>
</div>
</template>
<div class="color-groups">
<div v-for="group in colorGroups" :key="group.key" class="color-group">
<h4 class="group-title">{{ group.title }}</h4>
<div class="color-items">
<SettingItem
v-for="color in group.colors"
:key="color.key"
:title="color.label"
class="color-setting-item"
>
<div class="color-input-wrapper">
<div class="color-picker-wrapper">
<PickColors
v-model:value="currentColors[color.key]"
v-model:show-picker="showPickerMap[color.key]"
:size="28"
show-alpha
:theme="currentThemeMode"
:colors="[]"
format="hex"
:format-options="['rgb', 'hex', 'hsl', 'hsv']"
placement="bottom"
position="absolute"
:z-index="1000"
@change="(val) => handleColorChange(color.key, val)"
@close-picker="handlePickerClose"
/>
</div>
<input
type="text"
:value="currentColors[color.key] || ''"
@input="updateColor(color.key, ($event.target as HTMLInputElement).value)"
class="color-text-input"
:placeholder="t('settings.colorValue')"
/>
</div>
</SettingItem>
</div>
</div>
</div>
</SettingSection>
</div>
</template>
@@ -95,4 +423,146 @@ const updateSystemTheme = async (event: Event) => {
color: var(--settings-text);
}
}
</style>
// 主题控制区域
.theme-controls {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 8px;
}
// 主题颜色配置样式
.reset-button, .apply-button, .cancel-button {
padding: 6px 12px;
font-size: 12px;
border: 1px solid var(--settings-input-border);
border-radius: 4px;
cursor: pointer;
transition: all 0.2s ease;
&:hover {
border-color: #4a9eff;
}
&:active {
transform: translateY(1px);
}
}
.reset-button {
background-color: var(--settings-button-bg);
color: var(--settings-button-text);
&:hover {
background-color: var(--settings-button-hover-bg);
}
&.reset-button-confirming {
background-color: #e74c3c;
color: white;
border-color: #c0392b;
&:hover {
background-color: #c0392b;
}
}
}
.apply-button {
background-color: #4a9eff;
color: white;
font-weight: 500;
&:hover {
background-color: #3a8eef;
}
}
.cancel-button {
background-color: var(--settings-button-bg);
color: var(--settings-button-text);
&:hover {
background-color: var(--settings-button-hover-bg);
}
}
.color-groups {
display: flex;
flex-direction: column;
gap: 24px;
}
.color-group {
.group-title {
font-size: 14px;
font-weight: 600;
color: var(--settings-text);
margin: 0 0 12px 0;
padding-bottom: 6px;
border-bottom: 1px solid var(--settings-input-border);
}
.color-items {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 8px;
}
}
.color-setting-item {
:deep(.setting-item-content) {
align-items: center;
}
:deep(.setting-item-title) {
font-size: 12px;
min-width: 120px;
}
}
.color-input-wrapper {
display: flex;
align-items: center;
gap: 10px;
flex: 1;
}
.color-picker-wrapper {
display: flex;
align-items: center;
height: 28px;
cursor: pointer;
:deep(.pick-colors-trigger) {
border: 1px solid var(--settings-input-border);
border-radius: 4px;
overflow: hidden;
}
}
.color-text-input {
flex: 1;
min-width: 160px;
padding: 4px 8px;
border: 1px solid var(--settings-input-border);
border-radius: 4px;
background-color: var(--settings-input-bg);
color: var(--settings-text);
font-size: 11px;
font-family: 'Courier New', monospace;
transition: border-color 0.2s ease;
height: 28px;
box-sizing: border-box;
&:focus {
outline: none;
border-color: #4a9eff;
}
&::placeholder {
color: var(--settings-text-secondary);
}
}
</style>