♻️ Refactor some code
This commit is contained in:
@@ -14,6 +14,7 @@ import {getTabExtensions, updateTabConfig} from '@/views/editor/basic/tabExtensi
|
||||
import {createFontExtensionFromBackend, updateFontConfig} from '@/views/editor/basic/fontExtension';
|
||||
import {createStatsUpdateExtension} from '@/views/editor/basic/statsExtension';
|
||||
import {createContentChangePlugin} from '@/views/editor/basic/contentChangeExtension';
|
||||
import {createWheelZoomExtension} from '@/views/editor/basic/wheelZoomExtension';
|
||||
import {createDynamicKeymapExtension, updateKeymapExtension} from '@/views/editor/keymap';
|
||||
import {
|
||||
createDynamicExtensions,
|
||||
@@ -242,6 +243,11 @@ export const useEditorStore = defineStore('editor', () => {
|
||||
fontWeight: configStore.config.editing.fontWeight
|
||||
});
|
||||
|
||||
const wheelZoomExtension = createWheelZoomExtension(
|
||||
() => configStore.increaseFontSize(),
|
||||
() => configStore.decreaseFontSize()
|
||||
);
|
||||
|
||||
// 统计扩展
|
||||
const statsExtension = createStatsUpdateExtension(updateDocumentStats);
|
||||
|
||||
@@ -287,6 +293,7 @@ export const useEditorStore = defineStore('editor', () => {
|
||||
themeExtension,
|
||||
...tabExtensions,
|
||||
fontExtension,
|
||||
wheelZoomExtension,
|
||||
statsExtension,
|
||||
contentChangeExtension,
|
||||
codeBlockExtension,
|
||||
@@ -707,12 +714,15 @@ export const useEditorStore = defineStore('editor', () => {
|
||||
// 更新前端编辑器扩展 - 应用于所有实例
|
||||
const manager = getExtensionManager();
|
||||
if (manager) {
|
||||
// 使用立即更新模式,跳过防抖
|
||||
manager.updateExtensionImmediate(id, enabled, config || {});
|
||||
// 直接更新前端扩展至所有视图
|
||||
manager.updateExtension(id, enabled, config);
|
||||
}
|
||||
|
||||
// 重新加载扩展配置
|
||||
await extensionStore.loadExtensions();
|
||||
if (manager) {
|
||||
manager.initExtensions(extensionStore.extensions);
|
||||
}
|
||||
|
||||
await applyKeymapSettings();
|
||||
};
|
||||
|
||||
@@ -3,7 +3,6 @@ import {computed, 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';
|
||||
@@ -19,12 +18,6 @@ const editorElement = ref<HTMLElement | null>(null);
|
||||
|
||||
const enableLoadingAnimation = computed(() => configStore.config.general.enableLoadingAnimation);
|
||||
|
||||
// 创建滚轮缩放处理器
|
||||
const wheelHandler = createWheelZoomHandler(
|
||||
configStore.increaseFontSize,
|
||||
configStore.decreaseFontSize
|
||||
);
|
||||
|
||||
onMounted(async () => {
|
||||
if (!editorElement.value) return;
|
||||
|
||||
@@ -38,16 +31,9 @@ onMounted(async () => {
|
||||
editorStore.setEditorContainer(editorElement.value);
|
||||
|
||||
await tabStore.initializeTab();
|
||||
|
||||
// 添加滚轮事件监听
|
||||
editorElement.value.addEventListener('wheel', wheelHandler, {passive: false});
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
// 移除滚轮事件监听
|
||||
if (editorElement.value) {
|
||||
editorElement.value.removeEventListener('wheel', wheelHandler);
|
||||
}
|
||||
editorStore.clearAllEditors();
|
||||
|
||||
});
|
||||
@@ -88,7 +74,6 @@ onBeforeUnmount(() => {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
// 加载动画过渡效果
|
||||
.loading-fade-enter-active,
|
||||
.loading-fade-leave-active {
|
||||
transition: opacity 0.3s ease;
|
||||
@@ -99,3 +84,4 @@ onBeforeUnmount(() => {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -1,22 +1,40 @@
|
||||
// 处理滚轮缩放字体的事件处理函数
|
||||
export const createWheelZoomHandler = (
|
||||
increaseFontSize: () => void,
|
||||
decreaseFontSize: () => void
|
||||
) => {
|
||||
return (event: WheelEvent) => {
|
||||
// 检查是否按住了Ctrl键
|
||||
if (event.ctrlKey) {
|
||||
// 阻止默认行为(防止页面缩放)
|
||||
import {EditorView} from '@codemirror/view';
|
||||
import type {Extension} from '@codemirror/state';
|
||||
|
||||
type FontAdjuster = () => Promise<void> | void;
|
||||
|
||||
const runAdjuster = (adjuster: FontAdjuster) => {
|
||||
try {
|
||||
const result = adjuster();
|
||||
if (result && typeof (result as Promise<void>).then === 'function') {
|
||||
(result as Promise<void>).catch((error) => {
|
||||
console.error('Failed to adjust font size:', error);
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to adjust font size:', error);
|
||||
}
|
||||
};
|
||||
|
||||
export const createWheelZoomExtension = (
|
||||
increaseFontSize: FontAdjuster,
|
||||
decreaseFontSize: FontAdjuster
|
||||
): Extension => {
|
||||
return EditorView.domEventHandlers({
|
||||
wheel(event) {
|
||||
if (!event.ctrlKey) {
|
||||
return false;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
// 根据滚轮方向增大或减小字体
|
||||
if (event.deltaY < 0) {
|
||||
// 向上滚动,增大字体
|
||||
increaseFontSize();
|
||||
} else {
|
||||
// 向下滚动,减小字体
|
||||
decreaseFontSize();
|
||||
runAdjuster(increaseFontSize);
|
||||
} else if (event.deltaY > 0) {
|
||||
runAdjuster(decreaseFontSize);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
};
|
||||
});
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Extension } from '@codemirror/state';
|
||||
import { useKeybindingStore } from '@/stores/keybindingStore';
|
||||
import { useExtensionStore } from '@/stores/extensionStore';
|
||||
import { KeymapManager } from './keymapManager';
|
||||
import { Manager } from './manager';
|
||||
|
||||
/**
|
||||
* 异步创建快捷键扩展
|
||||
@@ -23,7 +23,7 @@ export const createDynamicKeymapExtension = async (): Promise<Extension> => {
|
||||
// 获取启用的扩展ID列表
|
||||
const enabledExtensionIds = extensionStore.enabledExtensions.map(ext => ext.id);
|
||||
|
||||
return KeymapManager.createKeymapExtension(keybindingStore.keyBindings, enabledExtensionIds);
|
||||
return Manager.createKeymapExtension(keybindingStore.keyBindings, enabledExtensionIds);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -37,10 +37,10 @@ export const updateKeymapExtension = (view: any): void => {
|
||||
// 获取启用的扩展ID列表
|
||||
const enabledExtensionIds = extensionStore.enabledExtensions.map(ext => ext.id);
|
||||
|
||||
KeymapManager.updateKeymap(view, keybindingStore.keyBindings, enabledExtensionIds);
|
||||
Manager.updateKeymap(view, keybindingStore.keyBindings, enabledExtensionIds);
|
||||
};
|
||||
|
||||
// 导出相关模块
|
||||
export { KeymapManager } from './keymapManager';
|
||||
export { Manager } from './manager';
|
||||
export { commands, getCommandHandler, getCommandDescription, isCommandRegistered, getRegisteredCommands } from './commands';
|
||||
export type { KeyBinding, CommandHandler, CommandDefinition, KeymapResult } from './types';
|
||||
@@ -8,7 +8,7 @@ import {getCommandHandler, isCommandRegistered} from './commands';
|
||||
* 快捷键管理器
|
||||
* 负责将后端配置转换为CodeMirror快捷键扩展
|
||||
*/
|
||||
export class KeymapManager {
|
||||
export class Manager {
|
||||
private static compartment = new Compartment();
|
||||
|
||||
/**
|
||||
@@ -1,299 +0,0 @@
|
||||
import {Compartment, Extension} from '@codemirror/state';
|
||||
import {EditorView} from '@codemirror/view';
|
||||
import {Extension as ExtensionConfig, ExtensionID} from '@/../bindings/voidraft/internal/models/models';
|
||||
import {ExtensionState, EditorViewInfo, ExtensionFactory} from './types'
|
||||
import {createDebounce} from '@/common/utils/debounce';
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 扩展管理器
|
||||
* 负责管理所有动态扩展的注册、启用、禁用和配置更新
|
||||
* 采用统一配置,多视图同步的设计模式
|
||||
*/
|
||||
export class ExtensionManager {
|
||||
// 统一的扩展状态存储
|
||||
private extensionStates = new Map<ExtensionID, ExtensionState>();
|
||||
|
||||
// 编辑器视图管理
|
||||
private viewsMap = new Map<number, EditorViewInfo>();
|
||||
private activeViewId: number | null = null;
|
||||
|
||||
// 注册的扩展工厂
|
||||
private extensionFactories = new Map<ExtensionID, ExtensionFactory>();
|
||||
|
||||
// 防抖处理
|
||||
private debouncedUpdateFunctions = new Map<ExtensionID, {
|
||||
debouncedFn: (enabled: boolean, config: any) => void;
|
||||
cancel: () => void;
|
||||
flush: () => void;
|
||||
}>();
|
||||
|
||||
/**
|
||||
* 注册扩展工厂
|
||||
* @param id 扩展ID
|
||||
* @param factory 扩展工厂
|
||||
*/
|
||||
registerExtension(id: ExtensionID, factory: ExtensionFactory): void {
|
||||
this.extensionFactories.set(id, factory);
|
||||
|
||||
// 创建初始状态
|
||||
if (!this.extensionStates.has(id)) {
|
||||
const compartment = new Compartment();
|
||||
const defaultConfig = factory.getDefaultConfig();
|
||||
|
||||
this.extensionStates.set(id, {
|
||||
id,
|
||||
factory,
|
||||
config: defaultConfig,
|
||||
enabled: false,
|
||||
compartment,
|
||||
extension: [] // 默认为空扩展(禁用状态)
|
||||
});
|
||||
}
|
||||
|
||||
// 为每个扩展创建防抖函数
|
||||
if (!this.debouncedUpdateFunctions.has(id)) {
|
||||
const { debouncedFn, cancel, flush } = createDebounce(
|
||||
(enabled: boolean, config: any) => {
|
||||
this.updateExtensionImmediate(id, enabled, config);
|
||||
},
|
||||
{ delay: 300 }
|
||||
);
|
||||
|
||||
this.debouncedUpdateFunctions.set(id, {
|
||||
debouncedFn,
|
||||
cancel,
|
||||
flush
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有注册的扩展ID列表
|
||||
*/
|
||||
getRegisteredExtensions(): ExtensionID[] {
|
||||
return Array.from(this.extensionFactories.keys());
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查扩展是否已注册
|
||||
* @param id 扩展ID
|
||||
*/
|
||||
isExtensionRegistered(id: ExtensionID): boolean {
|
||||
return this.extensionFactories.has(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从后端配置初始化扩展状态
|
||||
* @param extensionConfigs 后端扩展配置列表
|
||||
*/
|
||||
initializeExtensionsFromConfig(extensionConfigs: ExtensionConfig[]): void {
|
||||
for (const config of extensionConfigs) {
|
||||
const factory = this.extensionFactories.get(config.id);
|
||||
if (!factory) continue;
|
||||
|
||||
// 验证配置
|
||||
if (factory.validateConfig && !factory.validateConfig(config.config)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
// 创建扩展实例
|
||||
const extension = config.enabled ? factory.create(config.config) : [];
|
||||
|
||||
// 如果状态已存在则更新,否则创建新状态
|
||||
if (this.extensionStates.has(config.id)) {
|
||||
const state = this.extensionStates.get(config.id)!;
|
||||
state.config = config.config;
|
||||
state.enabled = config.enabled;
|
||||
state.extension = extension;
|
||||
} else {
|
||||
const compartment = new Compartment();
|
||||
this.extensionStates.set(config.id, {
|
||||
id: config.id,
|
||||
factory,
|
||||
config: config.config,
|
||||
enabled: config.enabled,
|
||||
compartment,
|
||||
extension
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to initialize extension ${config.id}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取初始扩展配置数组(用于创建编辑器)
|
||||
* @returns CodeMirror扩展数组
|
||||
*/
|
||||
getInitialExtensions(): Extension[] {
|
||||
const extensions: Extension[] = [];
|
||||
|
||||
// 为每个注册的扩展添加compartment
|
||||
for (const state of this.extensionStates.values()) {
|
||||
extensions.push(state.compartment.of(state.extension));
|
||||
}
|
||||
|
||||
return extensions;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置编辑器视图
|
||||
* @param view 编辑器视图实例
|
||||
* @param documentId 文档ID
|
||||
*/
|
||||
setView(view: EditorView, documentId: number): void {
|
||||
// 保存视图信息
|
||||
this.viewsMap.set(documentId, {
|
||||
view,
|
||||
documentId,
|
||||
registered: true
|
||||
});
|
||||
|
||||
// 设置当前活动视图
|
||||
this.activeViewId = documentId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前活动视图
|
||||
*/
|
||||
private getActiveView(): EditorView | null {
|
||||
if (this.activeViewId === null) return null;
|
||||
const viewInfo = this.viewsMap.get(this.activeViewId);
|
||||
return viewInfo ? viewInfo.view : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新单个扩展配置并应用到所有视图(带防抖功能)
|
||||
* @param id 扩展ID
|
||||
* @param enabled 是否启用
|
||||
* @param config 扩展配置
|
||||
*/
|
||||
updateExtension(id: ExtensionID, enabled: boolean, config: any = {}): void {
|
||||
const debouncedUpdate = this.debouncedUpdateFunctions.get(id);
|
||||
if (debouncedUpdate) {
|
||||
debouncedUpdate.debouncedFn(enabled, config);
|
||||
} else {
|
||||
// 如果没有防抖函数,直接执行
|
||||
this.updateExtensionImmediate(id, enabled, config);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 立即更新扩展(无防抖)
|
||||
* @param id 扩展ID
|
||||
* @param enabled 是否启用
|
||||
* @param config 扩展配置
|
||||
*/
|
||||
updateExtensionImmediate(id: ExtensionID, enabled: boolean, config: any = {}): void {
|
||||
// 获取扩展状态
|
||||
const state = this.extensionStates.get(id);
|
||||
if (!state) return;
|
||||
|
||||
// 获取工厂
|
||||
const factory = state.factory;
|
||||
|
||||
// 验证配置
|
||||
if (factory.validateConfig && !factory.validateConfig(config)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 创建新的扩展实例
|
||||
const extension = enabled ? factory.create(config) : [];
|
||||
|
||||
// 更新内部状态
|
||||
state.config = config;
|
||||
state.enabled = enabled;
|
||||
state.extension = extension;
|
||||
|
||||
// 应用到所有视图
|
||||
this.applyExtensionToAllViews(id);
|
||||
} catch (error) {
|
||||
console.error(`Failed to update extension ${id}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将指定扩展的当前状态应用到所有视图
|
||||
* @param id 扩展ID
|
||||
*/
|
||||
private applyExtensionToAllViews(id: ExtensionID): void {
|
||||
const state = this.extensionStates.get(id);
|
||||
if (!state) return;
|
||||
|
||||
// 遍历所有视图并应用更改
|
||||
for (const viewInfo of this.viewsMap.values()) {
|
||||
try {
|
||||
if (!viewInfo.registered) continue;
|
||||
|
||||
viewInfo.view.dispatch({
|
||||
effects: state.compartment.reconfigure(state.extension)
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`Failed to apply extension ${id} to document ${viewInfo.documentId}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取扩展当前状态
|
||||
* @param id 扩展ID
|
||||
*/
|
||||
getExtensionState(id: ExtensionID): {
|
||||
enabled: boolean
|
||||
config: any
|
||||
} | null {
|
||||
const state = this.extensionStates.get(id);
|
||||
if (!state) return null;
|
||||
|
||||
return {
|
||||
enabled: state.enabled,
|
||||
config: state.config
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置扩展到默认配置
|
||||
* @param id 扩展ID
|
||||
*/
|
||||
resetExtensionToDefault(id: ExtensionID): void {
|
||||
const state = this.extensionStates.get(id);
|
||||
if (!state) return;
|
||||
|
||||
const defaultConfig = state.factory.getDefaultConfig();
|
||||
this.updateExtension(id, true, defaultConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从管理器中移除视图
|
||||
* @param documentId 文档ID
|
||||
*/
|
||||
removeView(documentId: number): void {
|
||||
if (this.activeViewId === documentId) {
|
||||
this.activeViewId = null;
|
||||
}
|
||||
|
||||
this.viewsMap.delete(documentId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁管理器
|
||||
*/
|
||||
destroy(): void {
|
||||
// 清除所有防抖函数
|
||||
for (const { cancel } of this.debouncedUpdateFunctions.values()) {
|
||||
cancel();
|
||||
}
|
||||
this.debouncedUpdateFunctions.clear();
|
||||
|
||||
this.viewsMap.clear();
|
||||
this.activeViewId = null;
|
||||
this.extensionFactories.clear();
|
||||
this.extensionStates.clear();
|
||||
}
|
||||
}
|
||||
@@ -1,301 +1,152 @@
|
||||
import {ExtensionManager} from './extensionManager';
|
||||
import {Manager} from './manager';
|
||||
import {ExtensionID} from '@/../bindings/voidraft/internal/models/models';
|
||||
import i18n from '@/i18n';
|
||||
import {ExtensionFactory} from './types'
|
||||
import {ExtensionDefinition} from './types';
|
||||
|
||||
// 导入现有扩展的创建函数
|
||||
import rainbowBracketsExtension from '../extensions/rainbowBracket/rainbowBracketsExtension';
|
||||
import {createTextHighlighter} from '../extensions/textHighlight/textHighlightExtension';
|
||||
|
||||
import {color} from '../extensions/colorSelector';
|
||||
import {hyperLink} from '../extensions/hyperlink';
|
||||
import {minimap} from '../extensions/minimap';
|
||||
import {vscodeSearch} from '../extensions/vscodeSearch';
|
||||
import {createCheckboxExtension} from '../extensions/checkbox';
|
||||
import {createTranslatorExtension} from '../extensions/translator';
|
||||
|
||||
import {foldingOnIndent} from '../extensions/fold/foldExtension';
|
||||
|
||||
/**
|
||||
* 彩虹括号扩展工厂
|
||||
*/
|
||||
export const rainbowBracketsFactory: ExtensionFactory = {
|
||||
create(_config: any) {
|
||||
return rainbowBracketsExtension();
|
||||
},
|
||||
getDefaultConfig() {
|
||||
return {};
|
||||
},
|
||||
validateConfig(config: any) {
|
||||
return typeof config === 'object';
|
||||
}
|
||||
type ExtensionEntry = {
|
||||
definition: ExtensionDefinition
|
||||
displayNameKey: string
|
||||
descriptionKey: string
|
||||
};
|
||||
|
||||
/**
|
||||
* 文本高亮扩展工厂
|
||||
*/
|
||||
export const textHighlightFactory: ExtensionFactory = {
|
||||
create(config: any) {
|
||||
return createTextHighlighter({
|
||||
backgroundColor: config.backgroundColor || '#FFD700',
|
||||
opacity: config.opacity || 0.3
|
||||
type RegisteredExtensionID = Exclude<ExtensionID, ExtensionID.$zero | ExtensionID.ExtensionEditor>;
|
||||
|
||||
const defineExtension = (create: (config: any) => any, defaultConfig: Record<string, any> = {}): ExtensionDefinition => ({
|
||||
create,
|
||||
defaultConfig
|
||||
});
|
||||
},
|
||||
getDefaultConfig() {
|
||||
return {
|
||||
backgroundColor: '#FFD700', // 金黄色
|
||||
opacity: 0.3 // 透明度
|
||||
};
|
||||
},
|
||||
validateConfig(config: any) {
|
||||
return typeof config === 'object' &&
|
||||
(!config.backgroundColor || typeof config.backgroundColor === 'string') &&
|
||||
(!config.opacity || (typeof config.opacity === 'number' && config.opacity >= 0 && config.opacity <= 1));
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 小地图扩展工厂
|
||||
*/
|
||||
export const minimapFactory: ExtensionFactory = {
|
||||
create(config: any) {
|
||||
const options = {
|
||||
displayText: config.displayText || 'characters',
|
||||
showOverlay: config.showOverlay || 'always',
|
||||
autohide: config.autohide || false
|
||||
};
|
||||
return minimap(options);
|
||||
},
|
||||
getDefaultConfig() {
|
||||
return {
|
||||
displayText: 'characters',
|
||||
showOverlay: 'always',
|
||||
autohide: false
|
||||
};
|
||||
},
|
||||
validateConfig(config: any) {
|
||||
return typeof config === 'object' &&
|
||||
(!config.displayText || typeof config.displayText === 'string') &&
|
||||
(!config.showOverlay || typeof config.showOverlay === 'string') &&
|
||||
(!config.autohide || typeof config.autohide === 'boolean');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 超链接扩展工厂
|
||||
*/
|
||||
export const hyperlinkFactory: ExtensionFactory = {
|
||||
create(_config: any) {
|
||||
return hyperLink;
|
||||
},
|
||||
getDefaultConfig() {
|
||||
return {};
|
||||
},
|
||||
validateConfig(config: any) {
|
||||
return typeof config === 'object';
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 颜色选择器扩展工厂
|
||||
*/
|
||||
export const colorSelectorFactory: ExtensionFactory = {
|
||||
create(_config: any) {
|
||||
return color;
|
||||
},
|
||||
getDefaultConfig() {
|
||||
return {};
|
||||
},
|
||||
validateConfig(config: any) {
|
||||
return typeof config === 'object';
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 搜索扩展工厂
|
||||
*/
|
||||
export const searchFactory: ExtensionFactory = {
|
||||
create(_config: any) {
|
||||
return vscodeSearch;
|
||||
},
|
||||
getDefaultConfig() {
|
||||
return {};
|
||||
},
|
||||
validateConfig(config: any) {
|
||||
return typeof config === 'object';
|
||||
}
|
||||
};
|
||||
|
||||
export const foldFactory: ExtensionFactory = {
|
||||
create(_config: any) {
|
||||
return foldingOnIndent;
|
||||
},
|
||||
getDefaultConfig(): any {
|
||||
return {};
|
||||
},
|
||||
validateConfig(config: any): boolean {
|
||||
return typeof config === 'object';
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 选择框扩展工厂
|
||||
*/
|
||||
export const checkboxFactory: ExtensionFactory = {
|
||||
create(_config: any) {
|
||||
return createCheckboxExtension();
|
||||
},
|
||||
getDefaultConfig() {
|
||||
return {};
|
||||
},
|
||||
validateConfig(config: any) {
|
||||
return typeof config === 'object';
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 翻译扩展工厂
|
||||
*/
|
||||
export const translatorFactory: ExtensionFactory = {
|
||||
create(config: any) {
|
||||
return createTranslatorExtension({
|
||||
minSelectionLength: config.minSelectionLength || 2,
|
||||
maxTranslationLength: config.maxTranslationLength || 5000,
|
||||
});
|
||||
},
|
||||
getDefaultConfig() {
|
||||
return {
|
||||
minSelectionLength: 2,
|
||||
maxTranslationLength: 5000,
|
||||
};
|
||||
},
|
||||
validateConfig(config: any) {
|
||||
return typeof config === 'object';
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 所有扩展的统一配置
|
||||
* 排除$zero值以避免TypeScript类型错误
|
||||
*/
|
||||
const EXTENSION_CONFIGS = {
|
||||
|
||||
// 编辑增强扩展
|
||||
const EXTENSION_REGISTRY: Record<RegisteredExtensionID, ExtensionEntry> = {
|
||||
[ExtensionID.ExtensionRainbowBrackets]: {
|
||||
factory: rainbowBracketsFactory,
|
||||
definition: defineExtension(() => rainbowBracketsExtension()),
|
||||
displayNameKey: 'extensions.rainbowBrackets.name',
|
||||
descriptionKey: 'extensions.rainbowBrackets.description'
|
||||
},
|
||||
[ExtensionID.ExtensionHyperlink]: {
|
||||
factory: hyperlinkFactory,
|
||||
definition: defineExtension(() => hyperLink),
|
||||
displayNameKey: 'extensions.hyperlink.name',
|
||||
descriptionKey: 'extensions.hyperlink.description'
|
||||
},
|
||||
[ExtensionID.ExtensionColorSelector]: {
|
||||
factory: colorSelectorFactory,
|
||||
definition: defineExtension(() => color),
|
||||
displayNameKey: 'extensions.colorSelector.name',
|
||||
descriptionKey: 'extensions.colorSelector.description'
|
||||
},
|
||||
[ExtensionID.ExtensionTranslator]: {
|
||||
factory: translatorFactory,
|
||||
definition: defineExtension((config: any) => createTranslatorExtension({
|
||||
minSelectionLength: config?.minSelectionLength ?? 2,
|
||||
maxTranslationLength: config?.maxTranslationLength ?? 5000
|
||||
}), {
|
||||
minSelectionLength: 2,
|
||||
maxTranslationLength: 5000
|
||||
}),
|
||||
displayNameKey: 'extensions.translator.name',
|
||||
descriptionKey: 'extensions.translator.description'
|
||||
},
|
||||
|
||||
// UI增强扩展
|
||||
[ExtensionID.ExtensionMinimap]: {
|
||||
factory: minimapFactory,
|
||||
definition: defineExtension((config: any) => minimap({
|
||||
displayText: config?.displayText ?? 'characters',
|
||||
showOverlay: config?.showOverlay ?? 'always',
|
||||
autohide: config?.autohide ?? false
|
||||
}), {
|
||||
displayText: 'characters',
|
||||
showOverlay: 'always',
|
||||
autohide: false
|
||||
}),
|
||||
displayNameKey: 'extensions.minimap.name',
|
||||
descriptionKey: 'extensions.minimap.description'
|
||||
},
|
||||
|
||||
// 工具扩展
|
||||
[ExtensionID.ExtensionSearch]: {
|
||||
factory: searchFactory,
|
||||
definition: defineExtension(() => vscodeSearch),
|
||||
displayNameKey: 'extensions.search.name',
|
||||
descriptionKey: 'extensions.search.description'
|
||||
},
|
||||
|
||||
[ExtensionID.ExtensionFold]: {
|
||||
factory: foldFactory,
|
||||
definition: defineExtension(() => foldingOnIndent),
|
||||
displayNameKey: 'extensions.fold.name',
|
||||
descriptionKey: 'extensions.fold.description'
|
||||
},
|
||||
[ExtensionID.ExtensionTextHighlight]: {
|
||||
factory: textHighlightFactory,
|
||||
definition: defineExtension((config: any) => createTextHighlighter({
|
||||
backgroundColor: config?.backgroundColor ?? '#FFD700',
|
||||
opacity: config?.opacity ?? 0.3
|
||||
}), {
|
||||
backgroundColor: '#FFD700',
|
||||
opacity: 0.3
|
||||
}),
|
||||
displayNameKey: 'extensions.textHighlight.name',
|
||||
descriptionKey: 'extensions.textHighlight.description'
|
||||
},
|
||||
[ExtensionID.ExtensionCheckbox]: {
|
||||
factory: checkboxFactory,
|
||||
definition: defineExtension(() => createCheckboxExtension()),
|
||||
displayNameKey: 'extensions.checkbox.name',
|
||||
descriptionKey: 'extensions.checkbox.description'
|
||||
}
|
||||
} as const;
|
||||
|
||||
const isRegisteredExtension = (id: ExtensionID): id is RegisteredExtensionID =>
|
||||
Object.prototype.hasOwnProperty.call(EXTENSION_REGISTRY, id);
|
||||
|
||||
const getRegistryEntry = (id: ExtensionID): ExtensionEntry | undefined => {
|
||||
if (!isRegisteredExtension(id)) {
|
||||
return undefined;
|
||||
}
|
||||
return EXTENSION_REGISTRY[id];
|
||||
};
|
||||
|
||||
/**
|
||||
* 注册所有扩展工厂到管理器
|
||||
* @param manager 扩展管理器实例
|
||||
*/
|
||||
export function registerAllExtensions(manager: ExtensionManager): void {
|
||||
Object.entries(EXTENSION_CONFIGS).forEach(([id, config]) => {
|
||||
manager.registerExtension(id as ExtensionID, config.factory);
|
||||
export function registerAllExtensions(manager: Manager): void {
|
||||
(Object.entries(EXTENSION_REGISTRY) as [RegisteredExtensionID, ExtensionEntry][]).forEach(([id, entry]) => {
|
||||
manager.registerExtension(id, entry.definition);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取扩展工厂的显示名称
|
||||
* @param id 扩展ID
|
||||
* @returns 显示名称
|
||||
*/
|
||||
export function getExtensionDisplayName(id: ExtensionID): string {
|
||||
const config = EXTENSION_CONFIGS[id as ExtensionID];
|
||||
return config?.displayNameKey ? i18n.global.t(config.displayNameKey) : id;
|
||||
const entry = getRegistryEntry(id);
|
||||
return entry?.displayNameKey ? i18n.global.t(entry.displayNameKey) : id;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取扩展工厂的描述
|
||||
* @param id 扩展ID
|
||||
* @returns 描述
|
||||
*/
|
||||
export function getExtensionDescription(id: ExtensionID): string {
|
||||
const config = EXTENSION_CONFIGS[id as ExtensionID];
|
||||
return config?.descriptionKey ? i18n.global.t(config.descriptionKey) : '';
|
||||
const entry = getRegistryEntry(id);
|
||||
return entry?.descriptionKey ? i18n.global.t(entry.descriptionKey) : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取扩展工厂实例
|
||||
* @param id 扩展ID
|
||||
* @returns 扩展工厂实例
|
||||
*/
|
||||
export function getExtensionFactory(id: ExtensionID): ExtensionFactory | undefined {
|
||||
return EXTENSION_CONFIGS[id as ExtensionID]?.factory;
|
||||
function getExtensionDefinition(id: ExtensionID): ExtensionDefinition | undefined {
|
||||
return getRegistryEntry(id)?.definition;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取扩展的默认配置
|
||||
* @param id 扩展ID
|
||||
* @returns 默认配置对象
|
||||
*/
|
||||
export function getExtensionDefaultConfig(id: ExtensionID): any {
|
||||
const factory = getExtensionFactory(id);
|
||||
return factory?.getDefaultConfig() || {};
|
||||
const definition = getExtensionDefinition(id);
|
||||
if (!definition) return {};
|
||||
return cloneConfig(definition.defaultConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查扩展是否有配置项
|
||||
* @param id 扩展ID
|
||||
* @returns 是否有配置项
|
||||
*/
|
||||
export function hasExtensionConfig(id: ExtensionID): boolean {
|
||||
const defaultConfig = getExtensionDefaultConfig(id);
|
||||
return Object.keys(defaultConfig).length > 0;
|
||||
return Object.keys(getExtensionDefaultConfig(id)).length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有可用扩展的ID列表
|
||||
* @returns 扩展ID数组
|
||||
*/
|
||||
export function getAllExtensionIds(): ExtensionID[] {
|
||||
return Object.keys(EXTENSION_CONFIGS) as ExtensionID[];
|
||||
return Object.keys(EXTENSION_REGISTRY) as RegisteredExtensionID[];
|
||||
}
|
||||
|
||||
const cloneConfig = (config: any) => {
|
||||
if (Array.isArray(config)) {
|
||||
return config.map(cloneConfig);
|
||||
}
|
||||
if (config && typeof config === 'object') {
|
||||
return Object.keys(config).reduce((acc, key) => {
|
||||
acc[key] = cloneConfig(config[key]);
|
||||
return acc;
|
||||
}, {} as Record<string, any>);
|
||||
}
|
||||
return config;
|
||||
};
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import {Extension} from '@codemirror/state';
|
||||
import {EditorView} from '@codemirror/view';
|
||||
import {useExtensionStore} from '@/stores/extensionStore';
|
||||
import {ExtensionManager} from './extensionManager';
|
||||
import {Manager} from './manager';
|
||||
import {registerAllExtensions} from './extensions';
|
||||
|
||||
/**
|
||||
* 全局扩展管理器实例
|
||||
*/
|
||||
const extensionManager = new ExtensionManager();
|
||||
const extensionManager = new Manager();
|
||||
|
||||
/**
|
||||
* 异步创建动态扩展
|
||||
@@ -26,7 +26,7 @@ export const createDynamicExtensions = async (_documentId?: number): Promise<Ext
|
||||
}
|
||||
|
||||
// 初始化扩展管理器配置
|
||||
extensionManager.initializeExtensionsFromConfig(extensionStore.extensions);
|
||||
extensionManager.initExtensions(extensionStore.extensions);
|
||||
|
||||
// 获取初始扩展配置
|
||||
return extensionManager.getInitialExtensions();
|
||||
@@ -36,7 +36,7 @@ export const createDynamicExtensions = async (_documentId?: number): Promise<Ext
|
||||
* 获取扩展管理器实例
|
||||
* @returns 扩展管理器
|
||||
*/
|
||||
export const getExtensionManager = (): ExtensionManager => {
|
||||
export const getExtensionManager = (): Manager => {
|
||||
return extensionManager;
|
||||
};
|
||||
|
||||
@@ -58,5 +58,5 @@ export const removeExtensionManagerView = (documentId: number): void => {
|
||||
};
|
||||
|
||||
// 导出相关模块
|
||||
export {ExtensionManager} from './extensionManager';
|
||||
export {Manager} from './manager';
|
||||
export {registerAllExtensions, getExtensionDisplayName, getExtensionDescription} from './extensions';
|
||||
135
frontend/src/views/editor/manager/manager.ts
Normal file
135
frontend/src/views/editor/manager/manager.ts
Normal file
@@ -0,0 +1,135 @@
|
||||
import {Compartment, Extension} from '@codemirror/state';
|
||||
import {EditorView} from '@codemirror/view';
|
||||
import {Extension as ExtensionConfig, ExtensionID} from '@/../bindings/voidraft/internal/models/models';
|
||||
import {ExtensionDefinition, ExtensionState} from './types';
|
||||
|
||||
/**
|
||||
* 扩展管理器
|
||||
* 负责注册、初始化与同步所有动态扩展
|
||||
*/
|
||||
export class Manager {
|
||||
private extensionStates = new Map<ExtensionID, ExtensionState>();
|
||||
private views = new Map<number, EditorView>();
|
||||
|
||||
registerExtension(id: ExtensionID, definition: ExtensionDefinition): void {
|
||||
const existingState = this.extensionStates.get(id);
|
||||
if (existingState) {
|
||||
existingState.definition = definition;
|
||||
if (existingState.config === undefined) {
|
||||
existingState.config = this.cloneConfig(definition.defaultConfig ?? {});
|
||||
}
|
||||
} else {
|
||||
const compartment = new Compartment();
|
||||
const defaultConfig = this.cloneConfig(definition.defaultConfig ?? {});
|
||||
this.extensionStates.set(id, {
|
||||
id,
|
||||
definition,
|
||||
config: defaultConfig,
|
||||
enabled: false,
|
||||
compartment,
|
||||
extension: []
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
initExtensions(extensionConfigs: ExtensionConfig[]): void {
|
||||
for (const config of extensionConfigs) {
|
||||
const state = this.extensionStates.get(config.id);
|
||||
if (!state) continue;
|
||||
const resolvedConfig = this.cloneConfig(config.config ?? state.definition.defaultConfig ?? {});
|
||||
this.commitExtensionState(state, config.enabled, resolvedConfig);
|
||||
}
|
||||
}
|
||||
|
||||
getInitialExtensions(): Extension[] {
|
||||
const extensions: Extension[] = [];
|
||||
for (const state of this.extensionStates.values()) {
|
||||
extensions.push(state.compartment.of(state.extension));
|
||||
}
|
||||
return extensions;
|
||||
}
|
||||
|
||||
setView(view: EditorView, documentId: number): void {
|
||||
this.views.set(documentId, view);
|
||||
this.applyAllExtensionsToView(view);
|
||||
}
|
||||
|
||||
updateExtension(id: ExtensionID, enabled: boolean, config?: any): void {
|
||||
const state = this.extensionStates.get(id);
|
||||
if (!state) return;
|
||||
|
||||
const resolvedConfig = this.resolveConfig(state, config);
|
||||
this.commitExtensionState(state, enabled, resolvedConfig);
|
||||
}
|
||||
|
||||
removeView(documentId: number): void {
|
||||
this.views.delete(documentId);
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
this.views.clear();
|
||||
this.extensionStates.clear();
|
||||
}
|
||||
|
||||
private resolveConfig(state: ExtensionState, config?: any): any {
|
||||
if (config !== undefined) {
|
||||
return this.cloneConfig(config);
|
||||
}
|
||||
if (state.config !== undefined) {
|
||||
return this.cloneConfig(state.config);
|
||||
}
|
||||
return this.cloneConfig(state.definition.defaultConfig ?? {});
|
||||
}
|
||||
|
||||
private commitExtensionState(state: ExtensionState, enabled: boolean, config: any): void {
|
||||
try {
|
||||
const runtimeExtension = enabled ? state.definition.create(config) : [];
|
||||
state.enabled = enabled;
|
||||
state.config = config;
|
||||
state.extension = runtimeExtension;
|
||||
this.applyExtensionToAllViews(state.id);
|
||||
} catch (error) {
|
||||
console.error(`Failed to update extension ${state.id}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
private applyExtensionToAllViews(id: ExtensionID): void {
|
||||
const state = this.extensionStates.get(id);
|
||||
if (!state) return;
|
||||
|
||||
for (const [documentId, view] of this.views.entries()) {
|
||||
try {
|
||||
view.dispatch({effects: state.compartment.reconfigure(state.extension)});
|
||||
} catch (error) {
|
||||
console.error(`Failed to apply extension ${id} to document ${documentId}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private applyAllExtensionsToView(view: EditorView): void {
|
||||
const effects: any[] = [];
|
||||
for (const state of this.extensionStates.values()) {
|
||||
effects.push(state.compartment.reconfigure(state.extension));
|
||||
}
|
||||
if (effects.length === 0) return;
|
||||
|
||||
try {
|
||||
view.dispatch({effects});
|
||||
} catch (error) {
|
||||
console.error('Failed to register extensions on view:', error);
|
||||
}
|
||||
}
|
||||
|
||||
private cloneConfig<T>(config: T): T {
|
||||
if (Array.isArray(config)) {
|
||||
return config.map(item => this.cloneConfig(item)) as unknown as T;
|
||||
}
|
||||
if (config && typeof config === 'object') {
|
||||
return Object.keys(config as Record<string, any>).reduce((acc, key) => {
|
||||
(acc as any)[key] = this.cloneConfig((config as Record<string, any>)[key]);
|
||||
return acc;
|
||||
}, {} as Record<string, any>) as T;
|
||||
}
|
||||
return config;
|
||||
}
|
||||
}
|
||||
@@ -1,49 +1 @@
|
||||
import {Compartment, Extension} from '@codemirror/state';
|
||||
import {EditorView} from '@codemirror/view';
|
||||
import {ExtensionID} from '@/../bindings/voidraft/internal/models/models';
|
||||
/**
|
||||
* 扩展工厂接口
|
||||
* 每个扩展需要实现此接口来创建和配置扩展
|
||||
*/
|
||||
export interface ExtensionFactory {
|
||||
/**
|
||||
* 创建扩展实例
|
||||
* @param config 扩展配置
|
||||
* @returns CodeMirror扩展
|
||||
*/
|
||||
create(config: any): Extension
|
||||
|
||||
/**
|
||||
* 获取默认配置
|
||||
* @returns 默认配置对象
|
||||
*/
|
||||
getDefaultConfig(): any
|
||||
|
||||
/**
|
||||
* 验证配置
|
||||
* @param config 配置对象
|
||||
* @returns 是否有效
|
||||
*/
|
||||
validateConfig?(config: any): boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* 扩展状态
|
||||
*/
|
||||
export interface ExtensionState {
|
||||
id: ExtensionID
|
||||
factory: ExtensionFactory
|
||||
config: any
|
||||
enabled: boolean
|
||||
compartment: Compartment
|
||||
extension: Extension
|
||||
}
|
||||
|
||||
/**
|
||||
* 视图信息
|
||||
*/
|
||||
export interface EditorViewInfo {
|
||||
view: EditorView
|
||||
documentId: number
|
||||
registered: boolean
|
||||
}
|
||||
import {Compartment, Extension} from '@codemirror/state';
|
||||
@@ -66,10 +66,12 @@ const updateExtensionConfig = async (extensionId: ExtensionID, configKey: string
|
||||
if (!extension) return;
|
||||
|
||||
// 更新配置
|
||||
const updatedConfig = {...extension.config, [configKey]: value};
|
||||
|
||||
console.log(`[ExtensionsPage] 更新扩展 ${extensionId} 配置, ${configKey}=${value}`);
|
||||
|
||||
const updatedConfig = {...extension.config};
|
||||
if (value === undefined) {
|
||||
delete updatedConfig[configKey];
|
||||
} else {
|
||||
updatedConfig[configKey] = value;
|
||||
}
|
||||
// 使用editorStore的updateExtension方法更新,确保应用到所有编辑器实例
|
||||
await editorStore.updateExtension(extensionId, extension.enabled, updatedConfig);
|
||||
|
||||
@@ -81,7 +83,7 @@ const updateExtensionConfig = async (extensionId: ExtensionID, configKey: string
|
||||
// 重置扩展到默认配置
|
||||
const resetExtension = async (extensionId: ExtensionID) => {
|
||||
try {
|
||||
// 重置到默认配置(后端)
|
||||
// 重置到默认配置
|
||||
await ExtensionService.ResetExtensionToDefault(extensionId);
|
||||
|
||||
// 重新加载扩展状态以获取最新配置
|
||||
@@ -92,63 +94,65 @@ const resetExtension = async (extensionId: ExtensionID) => {
|
||||
if (extension) {
|
||||
// 通过editorStore更新,确保所有视图都能同步
|
||||
await editorStore.updateExtension(extensionId, extension.enabled, extension.config);
|
||||
console.log(`[ExtensionsPage] 重置扩展 ${extensionId} 配置,同步应用到所有编辑器实例`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to reset extension:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 配置项类型定义
|
||||
type ConfigItemType = 'toggle' | 'number' | 'text' | 'select'
|
||||
const getConfigValue = (
|
||||
config: Record<string, any> | undefined,
|
||||
configKey: string,
|
||||
defaultValue: any
|
||||
) => {
|
||||
if (config && Object.prototype.hasOwnProperty.call(config, configKey)) {
|
||||
return config[configKey];
|
||||
}
|
||||
return defaultValue;
|
||||
|
||||
interface SelectOption {
|
||||
value: any
|
||||
label: string
|
||||
}
|
||||
|
||||
interface ConfigItemMeta {
|
||||
type: ConfigItemType
|
||||
options?: SelectOption[]
|
||||
}
|
||||
|
||||
// 只保留 select 类型的配置项元数据
|
||||
const extensionConfigMeta: Partial<Record<ExtensionID, Record<string, ConfigItemMeta>>> = {
|
||||
[ExtensionID.ExtensionMinimap]: {
|
||||
displayText: {
|
||||
type: 'select',
|
||||
options: [
|
||||
{value: 'characters', label: 'Characters'},
|
||||
{value: 'blocks', label: 'Blocks'}
|
||||
]
|
||||
},
|
||||
showOverlay: {
|
||||
type: 'select',
|
||||
options: [
|
||||
{value: 'always', label: 'Always'},
|
||||
{value: 'mouse-over', label: 'Mouse Over'}
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 获取配置项类型
|
||||
const getConfigItemType = (extensionId: ExtensionID, configKey: string, defaultValue: any): string => {
|
||||
const meta = extensionConfigMeta[extensionId]?.[configKey];
|
||||
if (meta?.type) {
|
||||
return meta.type;
|
||||
|
||||
const formatConfigValue = (value: any): string => {
|
||||
if (value === undefined) return '';
|
||||
try {
|
||||
const serialized = JSON.stringify(value);
|
||||
return serialized ?? '';
|
||||
} catch (error) {
|
||||
console.warn('Failed to stringify config value', error);
|
||||
return '';
|
||||
}
|
||||
|
||||
// 根据默认值类型自动推断
|
||||
if (typeof defaultValue === 'boolean') return 'toggle';
|
||||
if (typeof defaultValue === 'number') return 'number';
|
||||
return 'text';
|
||||
};
|
||||
|
||||
// 获取选择框的选项列表
|
||||
const getSelectOptions = (extensionId: ExtensionID, configKey: string): SelectOption[] => {
|
||||
return extensionConfigMeta[extensionId]?.[configKey]?.options || [];
|
||||
|
||||
const handleConfigInput = async (
|
||||
extensionId: ExtensionID,
|
||||
configKey: string,
|
||||
defaultValue: any,
|
||||
event: Event
|
||||
) => {
|
||||
const target = event.target as HTMLInputElement | null;
|
||||
if (!target) return;
|
||||
const rawValue = target.value;
|
||||
const trimmedValue = rawValue.trim();
|
||||
if (!trimmedValue.length) {
|
||||
await updateExtensionConfig(extensionId, configKey, undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsedValue = JSON.parse(trimmedValue);
|
||||
await updateExtensionConfig(extensionId, configKey, parsedValue);
|
||||
} catch (_error) {
|
||||
const extension = extensionStore.extensions.find(ext => ext.id === extensionId);
|
||||
const fallbackValue = getConfigValue(extension?.config, configKey, defaultValue);
|
||||
target.value = formatConfigValue(fallbackValue);
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -204,58 +208,28 @@ const getSelectOptions = (extensionId: ExtensionID, configKey: string): SelectOp
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
<div class="config-table-wrapper">
|
||||
<table class="config-table">
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="[configKey, configValue] in Object.entries(extension.defaultConfig)"
|
||||
:key="configKey"
|
||||
class="config-item"
|
||||
>
|
||||
<SettingItem
|
||||
:title="configKey"
|
||||
>
|
||||
<!-- 布尔值切换开关 -->
|
||||
<ToggleSwitch
|
||||
v-if="getConfigItemType(extension.id, configKey, configValue) === 'toggle'"
|
||||
:model-value="extension.config[configKey] ?? configValue"
|
||||
@update:model-value="updateExtensionConfig(extension.id, configKey, $event)"
|
||||
/>
|
||||
|
||||
<!-- 数字输入框 -->
|
||||
<th scope="row" class="config-table-key">
|
||||
{{ configKey }}
|
||||
</th>
|
||||
<td class="config-table-value">
|
||||
<input
|
||||
v-else-if="getConfigItemType(extension.id, configKey, configValue) === 'number'"
|
||||
type="number"
|
||||
class="config-input"
|
||||
:value="extension.config[configKey] ?? configValue"
|
||||
:min="configKey === 'opacity' ? 0 : undefined"
|
||||
:max="configKey === 'opacity' ? 1 : undefined"
|
||||
:step="configKey === 'opacity' ? 0.1 : 1"
|
||||
@input="updateExtensionConfig(extension.id, configKey, parseFloat(($event.target as HTMLInputElement).value))"
|
||||
/>
|
||||
|
||||
<!-- 选择框 -->
|
||||
<select
|
||||
v-else-if="getConfigItemType(extension.id, configKey, configValue) === 'select'"
|
||||
class="config-select"
|
||||
:value="extension.config[configKey] ?? configValue"
|
||||
@change="updateExtensionConfig(extension.id, configKey, ($event.target as HTMLSelectElement).value)"
|
||||
>
|
||||
<option
|
||||
v-for="option in getSelectOptions(extension.id, configKey)"
|
||||
:key="option.value"
|
||||
:value="option.value"
|
||||
>
|
||||
{{ option.label }}
|
||||
</option>
|
||||
</select>
|
||||
|
||||
<!-- 文本输入框 -->
|
||||
<input
|
||||
v-else
|
||||
class="config-value-input"
|
||||
type="text"
|
||||
class="config-input"
|
||||
:value="extension.config[configKey] ?? configValue"
|
||||
@input="updateExtensionConfig(extension.id, configKey, ($event.target as HTMLInputElement).value)"
|
||||
:value="formatConfigValue(getConfigValue(extension.config, configKey, configValue))"
|
||||
@change="handleConfigInput(extension.id, configKey, configValue, $event)"
|
||||
@keyup.enter.prevent="handleConfigInput(extension.id, configKey, configValue, $event)"
|
||||
/>
|
||||
</SettingItem>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -361,37 +335,65 @@ const getSelectOptions = (extensionId: ExtensionID, configKey: string): SelectOp
|
||||
}
|
||||
}
|
||||
|
||||
.config-item {
|
||||
&:not(:last-child) {
|
||||
margin-bottom: 12px;
|
||||
.config-table-wrapper {
|
||||
border: 1px solid var(--settings-input-border);
|
||||
border-radius: 6px;
|
||||
margin-top: 8px;
|
||||
overflow: hidden;
|
||||
background-color: var(--settings-panel, var(--settings-input-bg));
|
||||
}
|
||||
|
||||
/* 配置项标题和描述字体大小 */
|
||||
:deep(.setting-item-title) {
|
||||
.config-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
:deep(.setting-item-description) {
|
||||
font-size: 11px;
|
||||
}
|
||||
.config-table tr + tr {
|
||||
border-top: 1px solid var(--settings-input-border);
|
||||
}
|
||||
|
||||
.config-input, .config-select {
|
||||
min-width: 120px;
|
||||
padding: 4px 8px;
|
||||
border: 1px solid var(--settings-input-border);
|
||||
border-radius: 3px;
|
||||
.config-table th,
|
||||
.config-table td {
|
||||
padding: 10px 12px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.config-table-key {
|
||||
width: 36%;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
color: var(--settings-text-secondary);
|
||||
border-right: 1px solid var(--settings-input-border);
|
||||
background-color: var(--settings-input-bg);
|
||||
color: var(--settings-text);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
.config-table-value {
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.config-value-input {
|
||||
width: 100%;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 4px;
|
||||
background: transparent;
|
||||
color: var(--settings-text);
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
box-sizing: border-box;
|
||||
transition: border-color 0.2s ease, background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.config-value-input:hover {
|
||||
border-color: var(--settings-hover-border, var(--settings-input-border));
|
||||
background-color: var(--settings-hover);
|
||||
}
|
||||
|
||||
.config-value-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--settings-accent);
|
||||
}
|
||||
}
|
||||
|
||||
.config-select {
|
||||
cursor: pointer;
|
||||
background-color: var(--settings-input-bg);
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user