✨ Improve extension management
This commit is contained in:
@@ -460,11 +460,6 @@ export enum ExtensionID {
|
|||||||
*/
|
*/
|
||||||
ExtensionMinimap = "minimap",
|
ExtensionMinimap = "minimap",
|
||||||
|
|
||||||
/**
|
|
||||||
* 代码爆炸效果
|
|
||||||
*/
|
|
||||||
ExtensionCodeBlast = "codeBlast",
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 工具扩展
|
* 工具扩展
|
||||||
* 搜索功能
|
* 搜索功能
|
||||||
|
@@ -157,28 +157,7 @@ export default {
|
|||||||
categoryTools: 'Tools',
|
categoryTools: 'Tools',
|
||||||
configuration: 'Configuration',
|
configuration: 'Configuration',
|
||||||
resetToDefault: 'Reset to Default Configuration',
|
resetToDefault: 'Reset to Default Configuration',
|
||||||
minimap: {
|
// Keep necessary extension interface translations, configuration items display in English directly
|
||||||
displayText: 'Display Text',
|
|
||||||
displayTextCharacters: 'Characters',
|
|
||||||
displayTextBlocks: 'Blocks',
|
|
||||||
showOverlay: 'Show Overlay',
|
|
||||||
showOverlayAlways: 'Always',
|
|
||||||
showOverlayHover: 'On Hover',
|
|
||||||
showOverlayNever: 'Never',
|
|
||||||
autohide: 'Auto Hide'
|
|
||||||
},
|
|
||||||
codeBlast: {
|
|
||||||
effect: 'Effect Type',
|
|
||||||
effectParticles: 'Particle Effect',
|
|
||||||
effectExplosion: 'Explosion Effect',
|
|
||||||
shake: 'Shake Effect',
|
|
||||||
maxParticles: 'Max Particles',
|
|
||||||
shakeIntensity: 'Shake Intensity'
|
|
||||||
},
|
|
||||||
codeBlock: {
|
|
||||||
showBackground: 'Show Background',
|
|
||||||
enableAutoDetection: 'Enable Auto Detection'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
extensions: {
|
extensions: {
|
||||||
@@ -198,10 +177,7 @@ export default {
|
|||||||
name: 'Minimap',
|
name: 'Minimap',
|
||||||
description: 'Display minimap overview of the document'
|
description: 'Display minimap overview of the document'
|
||||||
},
|
},
|
||||||
codeBlast: {
|
|
||||||
name: 'Code Blast',
|
|
||||||
description: 'Animated effects while typing'
|
|
||||||
},
|
|
||||||
search: {
|
search: {
|
||||||
name: 'Search',
|
name: 'Search',
|
||||||
description: 'Text search and replace functionality'
|
description: 'Text search and replace functionality'
|
||||||
|
@@ -157,28 +157,7 @@ export default {
|
|||||||
categoryTools: '工具扩展',
|
categoryTools: '工具扩展',
|
||||||
configuration: '配置',
|
configuration: '配置',
|
||||||
resetToDefault: '重置为默认配置',
|
resetToDefault: '重置为默认配置',
|
||||||
minimap: {
|
// 保留必要的扩展界面翻译,配置项直接显示英文
|
||||||
displayText: '显示文本',
|
|
||||||
displayTextCharacters: '字符',
|
|
||||||
displayTextBlocks: '块',
|
|
||||||
showOverlay: '显示覆盖层',
|
|
||||||
showOverlayAlways: '始终',
|
|
||||||
showOverlayHover: '悬停时',
|
|
||||||
showOverlayNever: '从不',
|
|
||||||
autohide: '自动隐藏'
|
|
||||||
},
|
|
||||||
codeBlast: {
|
|
||||||
effect: '效果类型',
|
|
||||||
effectParticles: '粒子效果',
|
|
||||||
effectExplosion: '爆炸效果',
|
|
||||||
shake: '震动效果',
|
|
||||||
maxParticles: '最大粒子数',
|
|
||||||
shakeIntensity: '震动强度'
|
|
||||||
},
|
|
||||||
codeBlock: {
|
|
||||||
showBackground: '显示背景',
|
|
||||||
enableAutoDetection: '启用自动检测'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
extensions: {
|
extensions: {
|
||||||
@@ -198,10 +177,7 @@ export default {
|
|||||||
name: '小地图',
|
name: '小地图',
|
||||||
description: '显示小地图视图'
|
description: '显示小地图视图'
|
||||||
},
|
},
|
||||||
codeBlast: {
|
|
||||||
name: '爆炸效果',
|
|
||||||
description: '编写时的动画效果'
|
|
||||||
},
|
|
||||||
search: {
|
search: {
|
||||||
name: '搜索功能',
|
name: '搜索功能',
|
||||||
description: '文本搜索和替换功能'
|
description: '文本搜索和替换功能'
|
||||||
|
@@ -270,6 +270,7 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
// 如果需要更新配置
|
// 如果需要更新配置
|
||||||
await ExtensionService.UpdateExtensionState(id, enabled, config)
|
await ExtensionService.UpdateExtensionState(id, enabled, config)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新前端编辑器
|
// 更新前端编辑器
|
||||||
const manager = getExtensionManager()
|
const manager = getExtensionManager()
|
||||||
if (manager) {
|
if (manager) {
|
||||||
|
@@ -19,7 +19,7 @@ export const useExtensionStore = defineStore('extension', () => {
|
|||||||
try {
|
try {
|
||||||
extensions.value = await ExtensionService.GetAllExtensions()
|
extensions.value = await ExtensionService.GetAllExtensions()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to load extensions:', err)
|
console.error('[ExtensionStore] Failed to load extensions:', err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,368 +0,0 @@
|
|||||||
import { Extension } from '@codemirror/state';
|
|
||||||
import { EditorView, ViewPlugin, ViewUpdate } from '@codemirror/view';
|
|
||||||
import './styles.css';
|
|
||||||
|
|
||||||
// 粒子接口定义
|
|
||||||
interface Particle {
|
|
||||||
x: number;
|
|
||||||
y: number;
|
|
||||||
vx: number;
|
|
||||||
vy: number;
|
|
||||||
alpha: number;
|
|
||||||
size: number;
|
|
||||||
color: number[];
|
|
||||||
theta?: number;
|
|
||||||
drag?: number;
|
|
||||||
wander?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 配置接口
|
|
||||||
interface CodeBlastConfig {
|
|
||||||
effect?: 1 | 2; // effect 1: 随机粒子, effect 2: 追逐粒子
|
|
||||||
shake?: boolean; // 启用震动效果
|
|
||||||
maxParticles?: number; // 最大粒子数
|
|
||||||
particleRange?: { min: number; max: number }; // 粒子大小范围
|
|
||||||
shakeIntensity?: number; // 震动强度
|
|
||||||
gravity?: number; // 重力加速度 (仅 effect: 1)
|
|
||||||
alphaFadeout?: number; // 粒子透明度衰减
|
|
||||||
velocityRange?: { // 粒子速度范围
|
|
||||||
x: [number, number]; // x轴方向速度范围
|
|
||||||
y: [number, number]; // y轴方向速度范围
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
class CodeBlastEffect {
|
|
||||||
private canvas: HTMLCanvasElement | null = null;
|
|
||||||
private ctx: CanvasRenderingContext2D | null = null;
|
|
||||||
private particles: Particle[] = [];
|
|
||||||
private particlePointer = 0;
|
|
||||||
private isActive = false;
|
|
||||||
private lastTime = 0;
|
|
||||||
private shakeTime = 0;
|
|
||||||
private shakeTimeMax = 0;
|
|
||||||
private animationId: number | null = null;
|
|
||||||
|
|
||||||
// 配置参数
|
|
||||||
private config: Required<CodeBlastConfig> = {
|
|
||||||
effect: 2,
|
|
||||||
shake: true,
|
|
||||||
maxParticles: 500,
|
|
||||||
particleRange: { min: 5, max: 10 },
|
|
||||||
shakeIntensity: 5,
|
|
||||||
gravity: 0.08,
|
|
||||||
alphaFadeout: 0.96,
|
|
||||||
velocityRange: {
|
|
||||||
x: [-1, 1],
|
|
||||||
y: [-3.5, -1.5]
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(config?: CodeBlastConfig) {
|
|
||||||
if (config) {
|
|
||||||
this.config = { ...this.config, ...config };
|
|
||||||
}
|
|
||||||
this.particles = new Array(this.config.maxParticles);
|
|
||||||
}
|
|
||||||
|
|
||||||
private getRGBComponents(element: Element): number[] {
|
|
||||||
try {
|
|
||||||
const style = getComputedStyle(element);
|
|
||||||
const color = style.color;
|
|
||||||
if (color) {
|
|
||||||
const match = color.match(/(\d+),\s*(\d+),\s*(\d+)/);
|
|
||||||
if (match) {
|
|
||||||
return [parseInt(match[1]), parseInt(match[2]), parseInt(match[3])];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.warn('Failed to get RGB components:', e);
|
|
||||||
}
|
|
||||||
return [255, 255, 255]; // 默认白色
|
|
||||||
}
|
|
||||||
|
|
||||||
private random(min: number, max?: number): number {
|
|
||||||
if (max === undefined) {
|
|
||||||
max = min;
|
|
||||||
min = 0;
|
|
||||||
}
|
|
||||||
return min + Math.floor(Math.random() * (max - min + 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
private createParticle(x: number, y: number, color: number[]): Particle {
|
|
||||||
const particle: Particle = {
|
|
||||||
x,
|
|
||||||
y: y + 10,
|
|
||||||
alpha: 1,
|
|
||||||
color,
|
|
||||||
vx: 0,
|
|
||||||
vy: 0,
|
|
||||||
size: 0
|
|
||||||
};
|
|
||||||
|
|
||||||
if (this.config.effect === 1) {
|
|
||||||
particle.size = this.random(2, 4);
|
|
||||||
particle.vx = this.config.velocityRange.x[0] +
|
|
||||||
Math.random() * (this.config.velocityRange.x[1] - this.config.velocityRange.x[0]);
|
|
||||||
particle.vy = this.config.velocityRange.y[0] +
|
|
||||||
Math.random() * (this.config.velocityRange.y[1] - this.config.velocityRange.y[0]);
|
|
||||||
} else if (this.config.effect === 2) {
|
|
||||||
particle.size = this.random(2, 8);
|
|
||||||
particle.drag = 0.92;
|
|
||||||
particle.vx = this.random(-3, 3);
|
|
||||||
particle.vy = this.random(-3, 3);
|
|
||||||
particle.wander = 0.15;
|
|
||||||
particle.theta = this.random(0, 360) * Math.PI / 180;
|
|
||||||
}
|
|
||||||
|
|
||||||
return particle;
|
|
||||||
}
|
|
||||||
|
|
||||||
private spawnParticles(view: EditorView): void {
|
|
||||||
if (!this.ctx) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 获取光标位置
|
|
||||||
const selection = view.state.selection.main;
|
|
||||||
const coords = view.coordsAtPos(selection.head);
|
|
||||||
if (!coords) return;
|
|
||||||
|
|
||||||
// 获取光标处的元素来确定颜色
|
|
||||||
const element = document.elementFromPoint(coords.left, coords.top);
|
|
||||||
const color = element ? this.getRGBComponents(element) : [255, 255, 255];
|
|
||||||
|
|
||||||
const numParticles = this.random(
|
|
||||||
this.config.particleRange.min,
|
|
||||||
this.config.particleRange.max
|
|
||||||
);
|
|
||||||
|
|
||||||
for (let i = 0; i < numParticles; i++) {
|
|
||||||
this.particles[this.particlePointer] = this.createParticle(
|
|
||||||
coords.left + 10,
|
|
||||||
coords.top,
|
|
||||||
color
|
|
||||||
);
|
|
||||||
this.particlePointer = (this.particlePointer + 1) % this.config.maxParticles;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
// 如果在更新期间无法读取坐标,静默忽略
|
|
||||||
console.warn('Failed to spawn particles:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private effect1(particle: Particle): void {
|
|
||||||
if (!this.ctx) return;
|
|
||||||
|
|
||||||
particle.vy += this.config.gravity;
|
|
||||||
particle.x += particle.vx;
|
|
||||||
particle.y += particle.vy;
|
|
||||||
particle.alpha *= this.config.alphaFadeout;
|
|
||||||
|
|
||||||
this.ctx.fillStyle = `rgba(${particle.color[0]}, ${particle.color[1]}, ${particle.color[2]}, ${particle.alpha})`;
|
|
||||||
this.ctx.fillRect(
|
|
||||||
Math.round(particle.x - 1),
|
|
||||||
Math.round(particle.y - 1),
|
|
||||||
particle.size,
|
|
||||||
particle.size
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private effect2(particle: Particle): void {
|
|
||||||
if (!this.ctx || particle.theta === undefined || particle.drag === undefined) return;
|
|
||||||
|
|
||||||
particle.x += particle.vx;
|
|
||||||
particle.y += particle.vy;
|
|
||||||
particle.vx *= particle.drag;
|
|
||||||
particle.vy *= particle.drag;
|
|
||||||
particle.theta += this.random(-0.5, 0.5);
|
|
||||||
particle.vx += Math.sin(particle.theta) * 0.1;
|
|
||||||
particle.vy += Math.cos(particle.theta) * 0.1;
|
|
||||||
particle.size *= 0.96;
|
|
||||||
|
|
||||||
this.ctx.fillStyle = `rgba(${particle.color[0]}, ${particle.color[1]}, ${particle.color[2]}, ${particle.alpha})`;
|
|
||||||
this.ctx.beginPath();
|
|
||||||
this.ctx.arc(
|
|
||||||
Math.round(particle.x - 1),
|
|
||||||
Math.round(particle.y - 1),
|
|
||||||
particle.size,
|
|
||||||
0,
|
|
||||||
2 * Math.PI
|
|
||||||
);
|
|
||||||
this.ctx.fill();
|
|
||||||
}
|
|
||||||
|
|
||||||
private drawParticles(): void {
|
|
||||||
if (!this.ctx) return;
|
|
||||||
|
|
||||||
for (let i = 0; i < this.particles.length; i++) {
|
|
||||||
const particle = this.particles[i];
|
|
||||||
if (!particle || particle.alpha < 0.01 || particle.size <= 0.5) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.config.effect === 1) {
|
|
||||||
this.effect1(particle);
|
|
||||||
} else if (this.config.effect === 2) {
|
|
||||||
this.effect2(particle);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private shake(view: EditorView, duration: number): void {
|
|
||||||
if (!this.config.shake) return;
|
|
||||||
|
|
||||||
this.shakeTime = this.shakeTimeMax = duration;
|
|
||||||
const editorElement = view.dom;
|
|
||||||
|
|
||||||
const shakeAnimation = () => {
|
|
||||||
if (this.shakeTime <= 0) {
|
|
||||||
editorElement.style.transform = '';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const magnitude = (this.shakeTime / this.shakeTimeMax) * this.config.shakeIntensity;
|
|
||||||
const shakeX = this.random(-magnitude, magnitude);
|
|
||||||
const shakeY = this.random(-magnitude, magnitude);
|
|
||||||
editorElement.style.transform = `translate(${shakeX}px, ${shakeY}px)`;
|
|
||||||
|
|
||||||
this.shakeTime -= 0.016; // ~60fps
|
|
||||||
requestAnimationFrame(shakeAnimation);
|
|
||||||
};
|
|
||||||
|
|
||||||
requestAnimationFrame(shakeAnimation);
|
|
||||||
}
|
|
||||||
|
|
||||||
private loop = (): void => {
|
|
||||||
if (!this.isActive || !this.ctx || !this.canvas) return;
|
|
||||||
|
|
||||||
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
|
||||||
this.drawParticles();
|
|
||||||
this.animationId = requestAnimationFrame(this.loop);
|
|
||||||
};
|
|
||||||
|
|
||||||
public init(view: EditorView): void {
|
|
||||||
if (this.isActive) return;
|
|
||||||
|
|
||||||
this.isActive = true;
|
|
||||||
|
|
||||||
if (!this.canvas) {
|
|
||||||
this.canvas = document.createElement('canvas');
|
|
||||||
this.ctx = this.canvas.getContext('2d');
|
|
||||||
|
|
||||||
if (!this.ctx) {
|
|
||||||
console.error('Failed to get canvas context');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.canvas.id = 'code-blast-canvas';
|
|
||||||
this.canvas.style.position = 'absolute';
|
|
||||||
this.canvas.style.top = '0';
|
|
||||||
this.canvas.style.left = '0';
|
|
||||||
this.canvas.style.zIndex = '1';
|
|
||||||
this.canvas.style.pointerEvents = 'none';
|
|
||||||
this.canvas.width = window.innerWidth;
|
|
||||||
this.canvas.height = window.innerHeight;
|
|
||||||
|
|
||||||
document.body.appendChild(this.canvas);
|
|
||||||
this.loop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public destroy(): void {
|
|
||||||
this.isActive = false;
|
|
||||||
|
|
||||||
if (this.animationId) {
|
|
||||||
cancelAnimationFrame(this.animationId);
|
|
||||||
this.animationId = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.canvas) {
|
|
||||||
this.canvas.remove();
|
|
||||||
this.canvas = null;
|
|
||||||
this.ctx = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public onDocumentChange(view: EditorView): void {
|
|
||||||
if (this.config.shake) {
|
|
||||||
this.shake(view, 0.3);
|
|
||||||
}
|
|
||||||
// 使用 requestIdleCallback 或 setTimeout 延迟执行粒子生成
|
|
||||||
// 确保在 DOM 更新完成后再读取坐标
|
|
||||||
if (window.requestIdleCallback) {
|
|
||||||
window.requestIdleCallback(() => {
|
|
||||||
this.spawnParticles(view);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
setTimeout(() => {
|
|
||||||
this.spawnParticles(view);
|
|
||||||
}, 16); // ~60fps
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public updateCanvasSize(): void {
|
|
||||||
if (this.canvas) {
|
|
||||||
this.canvas.width = window.innerWidth;
|
|
||||||
this.canvas.height = window.innerHeight;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 节流函数
|
|
||||||
function throttle<T extends (...args: any[]) => void>(
|
|
||||||
func: T,
|
|
||||||
limit: number
|
|
||||||
): (...args: Parameters<T>) => void {
|
|
||||||
let inThrottle: boolean;
|
|
||||||
return function(this: any, ...args: Parameters<T>) {
|
|
||||||
if (!inThrottle) {
|
|
||||||
func.apply(this, args);
|
|
||||||
inThrottle = true;
|
|
||||||
setTimeout(() => inThrottle = false, limit);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建 CodeBlast 扩展
|
|
||||||
export function createCodeBlastExtension(config?: CodeBlastConfig): Extension {
|
|
||||||
let effect: CodeBlastEffect | null = null;
|
|
||||||
|
|
||||||
const plugin = ViewPlugin.fromClass(class {
|
|
||||||
private throttledOnChange: (view: EditorView) => void;
|
|
||||||
|
|
||||||
constructor(private view: EditorView) {
|
|
||||||
effect = new CodeBlastEffect(config);
|
|
||||||
effect.init(view);
|
|
||||||
|
|
||||||
this.throttledOnChange = throttle((view: EditorView) => {
|
|
||||||
effect?.onDocumentChange(view);
|
|
||||||
}, 100);
|
|
||||||
|
|
||||||
// 监听窗口大小变化
|
|
||||||
window.addEventListener('resize', this.handleResize);
|
|
||||||
}
|
|
||||||
|
|
||||||
update(update: ViewUpdate) {
|
|
||||||
if (update.docChanged) {
|
|
||||||
// 延迟执行,确保更新完成后再触发效果
|
|
||||||
setTimeout(() => {
|
|
||||||
this.throttledOnChange(this.view);
|
|
||||||
}, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
destroy() {
|
|
||||||
effect?.destroy();
|
|
||||||
effect = null;
|
|
||||||
window.removeEventListener('resize', this.handleResize);
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleResize = () => {
|
|
||||||
effect?.updateCanvasSize();
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return plugin;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 默认导出
|
|
||||||
export const codeBlast = createCodeBlastExtension();
|
|
@@ -1,52 +0,0 @@
|
|||||||
#code-blast-canvas {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100vw;
|
|
||||||
height: 100vh;
|
|
||||||
z-index: 1000;
|
|
||||||
pointer-events: none;
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 确保编辑器在震动时不会影响布局 */
|
|
||||||
.cm-editor {
|
|
||||||
transition: transform 0.1s ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 打字机效果的粒子样式 */
|
|
||||||
.code-blast-particle {
|
|
||||||
position: absolute;
|
|
||||||
pointer-events: none;
|
|
||||||
border-radius: 50%;
|
|
||||||
animation: particle-fade 2s ease-out forwards;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes particle-fade {
|
|
||||||
0% {
|
|
||||||
opacity: 1;
|
|
||||||
transform: scale(1);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
opacity: 0;
|
|
||||||
transform: scale(0.5);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 震动效果的缓动 */
|
|
||||||
.code-blast-shake {
|
|
||||||
animation: shake 0.3s ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes shake {
|
|
||||||
0%, 100% { transform: translate(0, 0); }
|
|
||||||
10% { transform: translate(-2px, -1px); }
|
|
||||||
20% { transform: translate(2px, 1px); }
|
|
||||||
30% { transform: translate(-1px, 2px); }
|
|
||||||
40% { transform: translate(1px, -2px); }
|
|
||||||
50% { transform: translate(-2px, 1px); }
|
|
||||||
60% { transform: translate(2px, -1px); }
|
|
||||||
70% { transform: translate(-1px, -2px); }
|
|
||||||
80% { transform: translate(1px, 2px); }
|
|
||||||
90% { transform: translate(-2px, -1px); }
|
|
||||||
}
|
|
@@ -122,6 +122,14 @@ const minimapClass = ViewPlugin.fromClass(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (now) {
|
if (now) {
|
||||||
|
if (prev && this.dom && prev.autohide !== now.autohide) {
|
||||||
|
if (now.autohide) {
|
||||||
|
this.dom.classList.add('cm-minimap-autohide');
|
||||||
|
} else {
|
||||||
|
this.dom.classList.remove('cm-minimap-autohide');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.text.update(update);
|
this.text.update(update);
|
||||||
this.selection.update(update);
|
this.selection.update(update);
|
||||||
this.diagnostic.update(update);
|
this.diagnostic.update(update);
|
||||||
@@ -279,7 +287,7 @@ const minimapClass = ViewPlugin.fromClass(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// 使用type定义,而不是interface
|
// 使用type定义
|
||||||
export type MinimapConfig = Omit<Options, "enabled"> & {
|
export type MinimapConfig = Omit<Options, "enabled"> & {
|
||||||
/**
|
/**
|
||||||
* A function that creates the element that contains the minimap
|
* A function that creates the element that contains the minimap
|
||||||
|
@@ -47,6 +47,8 @@ export class ExtensionManager {
|
|||||||
private view: EditorView | null = null
|
private view: EditorView | null = null
|
||||||
private compartments = new Map<ExtensionID, ExtensionCompartment>()
|
private compartments = new Map<ExtensionID, ExtensionCompartment>()
|
||||||
private extensionFactories = new Map<ExtensionID, ExtensionFactory>()
|
private extensionFactories = new Map<ExtensionID, ExtensionFactory>()
|
||||||
|
private updateQueue = new Map<ExtensionID, { enabled: boolean, config: any, timestamp: number }>()
|
||||||
|
private debounceTimeout: number | null = null
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 注册扩展工厂
|
* 注册扩展工厂
|
||||||
@@ -110,7 +112,7 @@ export class ExtensionManager {
|
|||||||
compartmentInfo.currentConfig = config.config
|
compartmentInfo.currentConfig = config.config
|
||||||
compartmentInfo.enabled = config.enabled
|
compartmentInfo.enabled = config.enabled
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to create extension ${config.id}:`, error)
|
console.error(`[ExtensionManager] Failed to create extension ${config.id}:`, error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,13 +128,33 @@ export class ExtensionManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 动态更新单个扩展
|
* 动态更新单个扩展(带防抖)
|
||||||
* @param id 扩展ID
|
* @param id 扩展ID
|
||||||
* @param enabled 是否启用
|
* @param enabled 是否启用
|
||||||
* @param config 扩展配置
|
* @param config 扩展配置
|
||||||
*/
|
*/
|
||||||
updateExtension(id: ExtensionID, enabled: boolean, config: any = {}): void {
|
updateExtension(id: ExtensionID, enabled: boolean, config: any = {}): void {
|
||||||
|
// 添加到更新队列
|
||||||
|
this.updateQueue.set(id, {enabled, config, timestamp: Date.now()})
|
||||||
|
|
||||||
|
// 清除之前的防抖定时器
|
||||||
|
if (this.debounceTimeout) {
|
||||||
|
clearTimeout(this.debounceTimeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置新的防抖定时器
|
||||||
|
this.debounceTimeout = window.setTimeout(() => {
|
||||||
|
this.flushUpdateQueue()
|
||||||
|
}, 100) // 100ms 防抖
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 立即更新扩展(无防抖)
|
||||||
|
* @param id 扩展ID
|
||||||
|
* @param enabled 是否启用
|
||||||
|
* @param config 扩展配置
|
||||||
|
*/
|
||||||
|
updateExtensionImmediate(id: ExtensionID, enabled: boolean, config: any = {}): void {
|
||||||
if (!this.view) {
|
if (!this.view) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -166,6 +188,52 @@ export class ExtensionManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理更新队列中的所有更新
|
||||||
|
*/
|
||||||
|
private flushUpdateQueue(): void {
|
||||||
|
if (!this.view) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const effects: StateEffect<any>[] = []
|
||||||
|
|
||||||
|
for (const [id, update] of this.updateQueue) {
|
||||||
|
const compartmentInfo = this.compartments.get(id)
|
||||||
|
if (!compartmentInfo) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 验证配置
|
||||||
|
if (compartmentInfo.factory.validateConfig &&
|
||||||
|
!compartmentInfo.factory.validateConfig(update.config)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const extension = update.enabled
|
||||||
|
? compartmentInfo.factory.create(update.config)
|
||||||
|
: []
|
||||||
|
|
||||||
|
effects.push(compartmentInfo.compartment.reconfigure(extension))
|
||||||
|
|
||||||
|
// 更新状态
|
||||||
|
compartmentInfo.currentConfig = update.config
|
||||||
|
compartmentInfo.enabled = update.enabled
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[ExtensionManager] Failed to update extension ${id}:`, error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (effects.length > 0) {
|
||||||
|
this.view.dispatch({effects})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清空更新队列
|
||||||
|
this.updateQueue.clear()
|
||||||
|
this.debounceTimeout = null
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 批量更新扩展
|
* 批量更新扩展
|
||||||
* @param updates 更新配置数组
|
* @param updates 更新配置数组
|
||||||
@@ -251,8 +319,15 @@ export class ExtensionManager {
|
|||||||
* 销毁管理器
|
* 销毁管理器
|
||||||
*/
|
*/
|
||||||
destroy(): void {
|
destroy(): void {
|
||||||
|
// 清除防抖定时器
|
||||||
|
if (this.debounceTimeout) {
|
||||||
|
clearTimeout(this.debounceTimeout)
|
||||||
|
this.debounceTimeout = null
|
||||||
|
}
|
||||||
|
|
||||||
this.view = null
|
this.view = null
|
||||||
this.compartments.clear()
|
this.compartments.clear()
|
||||||
this.extensionFactories.clear()
|
this.extensionFactories.clear()
|
||||||
|
this.updateQueue.clear()
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -5,7 +5,7 @@ import i18n from '@/i18n'
|
|||||||
// 导入现有扩展的创建函数
|
// 导入现有扩展的创建函数
|
||||||
import rainbowBracketsExtension from '../extensions/rainbowBracket/rainbowBracketsExtension'
|
import rainbowBracketsExtension from '../extensions/rainbowBracket/rainbowBracketsExtension'
|
||||||
import {createTextHighlighter} from '../extensions/textHighlight/textHighlightExtension'
|
import {createTextHighlighter} from '../extensions/textHighlight/textHighlightExtension'
|
||||||
import {createCodeBlastExtension} from '../extensions/codeblast'
|
|
||||||
import {color} from '../extensions/colorSelector'
|
import {color} from '../extensions/colorSelector'
|
||||||
import {hyperLink} from '../extensions/hyperlink'
|
import {hyperLink} from '../extensions/hyperlink'
|
||||||
import {minimap} from '../extensions/minimap'
|
import {minimap} from '../extensions/minimap'
|
||||||
@@ -73,35 +73,7 @@ export const minimapFactory: ExtensionFactory = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 代码爆炸效果扩展工厂
|
|
||||||
*/
|
|
||||||
export const codeBlastFactory: ExtensionFactory = {
|
|
||||||
create(config: any) {
|
|
||||||
const options = {
|
|
||||||
effect: config.effect || 1,
|
|
||||||
shake: config.shake !== false,
|
|
||||||
maxParticles: config.maxParticles || 300,
|
|
||||||
shakeIntensity: config.shakeIntensity || 3
|
|
||||||
}
|
|
||||||
return createCodeBlastExtension(options)
|
|
||||||
},
|
|
||||||
getDefaultConfig() {
|
|
||||||
return {
|
|
||||||
effect: 1,
|
|
||||||
shake: true,
|
|
||||||
maxParticles: 300,
|
|
||||||
shakeIntensity: 3
|
|
||||||
}
|
|
||||||
},
|
|
||||||
validateConfig(config: any) {
|
|
||||||
return typeof config === 'object' &&
|
|
||||||
(!config.effect || [1, 2].includes(config.effect)) &&
|
|
||||||
(!config.shake || typeof config.shake === 'boolean') &&
|
|
||||||
(!config.maxParticles || typeof config.maxParticles === 'number') &&
|
|
||||||
(!config.shakeIntensity || typeof config.shakeIntensity === 'number')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 超链接扩展工厂
|
* 超链接扩展工厂
|
||||||
@@ -213,11 +185,7 @@ const EXTENSION_CONFIGS = {
|
|||||||
displayNameKey: 'extensions.minimap.name',
|
displayNameKey: 'extensions.minimap.name',
|
||||||
descriptionKey: 'extensions.minimap.description'
|
descriptionKey: 'extensions.minimap.description'
|
||||||
},
|
},
|
||||||
[ExtensionID.ExtensionCodeBlast]: {
|
|
||||||
factory: codeBlastFactory,
|
|
||||||
displayNameKey: 'extensions.codeBlast.name',
|
|
||||||
descriptionKey: 'extensions.codeBlast.description'
|
|
||||||
},
|
|
||||||
|
|
||||||
// 工具扩展
|
// 工具扩展
|
||||||
[ExtensionID.ExtensionSearch]: {
|
[ExtensionID.ExtensionSearch]: {
|
||||||
@@ -272,6 +240,35 @@ export function getExtensionDescription(id: ExtensionID): string {
|
|||||||
return config?.descriptionKey ? i18n.global.t(config.descriptionKey) : ''
|
return config?.descriptionKey ? i18n.global.t(config.descriptionKey) : ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取扩展工厂实例
|
||||||
|
* @param id 扩展ID
|
||||||
|
* @returns 扩展工厂实例
|
||||||
|
*/
|
||||||
|
export function getExtensionFactory(id: ExtensionID): ExtensionFactory | undefined {
|
||||||
|
return EXTENSION_CONFIGS[id as ExtensionID]?.factory
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取扩展的默认配置
|
||||||
|
* @param id 扩展ID
|
||||||
|
* @returns 默认配置对象
|
||||||
|
*/
|
||||||
|
export function getExtensionDefaultConfig(id: ExtensionID): any {
|
||||||
|
const factory = getExtensionFactory(id)
|
||||||
|
return factory?.getDefaultConfig() || {}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查扩展是否有配置项
|
||||||
|
* @param id 扩展ID
|
||||||
|
* @returns 是否有配置项
|
||||||
|
*/
|
||||||
|
export function hasExtensionConfig(id: ExtensionID): boolean {
|
||||||
|
const defaultConfig = getExtensionDefaultConfig(id)
|
||||||
|
return Object.keys(defaultConfig).length > 0
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取所有可用扩展的ID列表
|
* 获取所有可用扩展的ID列表
|
||||||
* @returns 扩展ID数组
|
* @returns 扩展ID数组
|
||||||
|
@@ -1,11 +1,18 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {computed} from 'vue'
|
import {computed, ref} from 'vue'
|
||||||
import {useI18n} from 'vue-i18n'
|
import {useI18n} from 'vue-i18n'
|
||||||
import {useEditorStore} from '@/stores/editorStore'
|
import {useEditorStore} from '@/stores/editorStore'
|
||||||
import {useExtensionStore} from '@/stores/extensionStore'
|
import {useExtensionStore} from '@/stores/extensionStore'
|
||||||
import {ExtensionService} from '@/../bindings/voidraft/internal/services'
|
import {ExtensionService} from '@/../bindings/voidraft/internal/services'
|
||||||
import {ExtensionID} from '@/../bindings/voidraft/internal/models/models'
|
import {ExtensionID} from '@/../bindings/voidraft/internal/models/models'
|
||||||
import {getAllExtensionIds, getExtensionDescription, getExtensionDisplayName} from '@/views/editor/manager/factories'
|
import {getExtensionManager} from '@/views/editor/manager'
|
||||||
|
import {
|
||||||
|
getAllExtensionIds,
|
||||||
|
getExtensionDescription,
|
||||||
|
getExtensionDisplayName,
|
||||||
|
hasExtensionConfig,
|
||||||
|
getExtensionDefaultConfig
|
||||||
|
} from '@/views/editor/manager/factories'
|
||||||
import SettingSection from '../components/SettingSection.vue'
|
import SettingSection from '../components/SettingSection.vue'
|
||||||
import SettingItem from '../components/SettingItem.vue'
|
import SettingItem from '../components/SettingItem.vue'
|
||||||
import ToggleSwitch from '../components/ToggleSwitch.vue'
|
import ToggleSwitch from '../components/ToggleSwitch.vue'
|
||||||
@@ -14,6 +21,9 @@ const {t} = useI18n()
|
|||||||
const editorStore = useEditorStore()
|
const editorStore = useEditorStore()
|
||||||
const extensionStore = useExtensionStore()
|
const extensionStore = useExtensionStore()
|
||||||
|
|
||||||
|
// 展开状态管理
|
||||||
|
const expandedExtensions = ref<Set<ExtensionID>>(new Set())
|
||||||
|
|
||||||
// 获取所有可用的扩展
|
// 获取所有可用的扩展
|
||||||
const availableExtensions = computed(() => {
|
const availableExtensions = computed(() => {
|
||||||
return getAllExtensionIds().map(id => {
|
return getAllExtensionIds().map(id => {
|
||||||
@@ -23,57 +33,235 @@ const availableExtensions = computed(() => {
|
|||||||
displayName: getExtensionDisplayName(id),
|
displayName: getExtensionDisplayName(id),
|
||||||
description: getExtensionDescription(id),
|
description: getExtensionDescription(id),
|
||||||
enabled: extension?.enabled || false,
|
enabled: extension?.enabled || false,
|
||||||
isDefault: extension?.isDefault || false
|
isDefault: extension?.isDefault || false,
|
||||||
|
hasConfig: hasExtensionConfig(id),
|
||||||
|
config: extension?.config || {},
|
||||||
|
defaultConfig: getExtensionDefaultConfig(id)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 切换展开状态
|
||||||
|
const toggleExpanded = (extensionId: ExtensionID) => {
|
||||||
|
if (expandedExtensions.value.has(extensionId)) {
|
||||||
|
expandedExtensions.value.delete(extensionId)
|
||||||
|
} else {
|
||||||
|
expandedExtensions.value.add(extensionId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 更新扩展状态
|
// 更新扩展状态
|
||||||
const updateExtension = async (extensionId: ExtensionID, enabled: boolean) => {
|
const updateExtension = async (extensionId: ExtensionID, enabled: boolean) => {
|
||||||
await editorStore.updateExtension(extensionId, enabled)
|
try {
|
||||||
|
await editorStore.updateExtension(extensionId, enabled)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to update extension:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新扩展配置
|
||||||
|
const updateExtensionConfig = async (extensionId: ExtensionID, configKey: string, value: any) => {
|
||||||
|
try {
|
||||||
|
// 获取当前扩展状态
|
||||||
|
const extension = extensionStore.extensions.find(ext => ext.id === extensionId)
|
||||||
|
if (!extension) return
|
||||||
|
|
||||||
|
// 更新配置
|
||||||
|
const updatedConfig = { ...extension.config, [configKey]: value }
|
||||||
|
|
||||||
|
await editorStore.updateExtension(extensionId, extension.enabled, updatedConfig)
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to update extension config:', error)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 重置扩展到默认配置
|
// 重置扩展到默认配置
|
||||||
const resetExtension = async (extensionId: ExtensionID) => {
|
const resetExtension = async (extensionId: ExtensionID) => {
|
||||||
|
try {
|
||||||
|
await ExtensionService.ResetExtensionToDefault(extensionId)
|
||||||
|
|
||||||
|
// 重新加载扩展状态以获取最新配置
|
||||||
|
await extensionStore.loadExtensions()
|
||||||
|
|
||||||
await ExtensionService.ResetExtensionToDefault(extensionId)
|
// 获取重置后的状态,立即通知编辑器更新
|
||||||
|
const extension = extensionStore.extensions.find(ext => ext.id === extensionId)
|
||||||
// 重新加载扩展状态
|
if (extension) {
|
||||||
await extensionStore.loadExtensions()
|
const manager = getExtensionManager()
|
||||||
|
manager.updateExtension(extensionId, extension.enabled, extension.config)
|
||||||
// 获取重置后的状态,通知编辑器更新
|
}
|
||||||
const extension = extensionStore.extensions.find(ext => ext.id === extensionId)
|
} catch (error) {
|
||||||
if (extension) {
|
console.error('Failed to reset extension:', error)
|
||||||
await editorStore.updateExtension(extensionId, extension.enabled, extension.config)
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 配置项类型定义
|
||||||
|
type ConfigItemType = 'toggle' | 'number' | 'text' | 'select'
|
||||||
|
|
||||||
|
interface SelectOption {
|
||||||
|
value: any
|
||||||
|
label: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ConfigItemMeta {
|
||||||
|
type: ConfigItemType
|
||||||
|
options?: SelectOption[]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 扩展配置项元数据
|
||||||
|
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' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
autohide: { type: 'toggle' }
|
||||||
|
},
|
||||||
|
[ExtensionID.ExtensionCodeBlock]: {
|
||||||
|
showBackground: { type: 'toggle' },
|
||||||
|
enableAutoDetection: { type: 'toggle' }
|
||||||
|
},
|
||||||
|
[ExtensionID.ExtensionTextHighlight]: {
|
||||||
|
highlightClass: { type: 'text' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取配置项类型
|
||||||
|
const getConfigItemType = (extensionId: ExtensionID, configKey: string, defaultValue: any): string => {
|
||||||
|
const meta = extensionConfigMeta[extensionId]?.[configKey]
|
||||||
|
if (meta?.type) {
|
||||||
|
return meta.type
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据默认值类型自动推断
|
||||||
|
if (typeof defaultValue === 'boolean') return 'toggle'
|
||||||
|
if (typeof defaultValue === 'number') return 'number'
|
||||||
|
return 'text'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取选择项选项
|
||||||
|
const getSelectOptions = (extensionId: ExtensionID, configKey: string) => {
|
||||||
|
const meta = extensionConfigMeta[extensionId]?.[configKey]
|
||||||
|
if (meta?.type === 'select' && meta.options) {
|
||||||
|
return meta.options
|
||||||
|
}
|
||||||
|
return []
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="settings-page">
|
<div class="settings-page">
|
||||||
<SettingSection :title="t('settings.extensions')">
|
<SettingSection :title="t('settings.extensions')">
|
||||||
<SettingItem
|
<div
|
||||||
v-for="extension in availableExtensions"
|
v-for="extension in availableExtensions"
|
||||||
:key="extension.id"
|
:key="extension.id"
|
||||||
:title="extension.displayName"
|
class="extension-item"
|
||||||
:description="extension.description"
|
|
||||||
>
|
>
|
||||||
<div class="extension-controls">
|
<!-- 扩展主项 -->
|
||||||
<ToggleSwitch
|
<SettingItem
|
||||||
:model-value="extension.enabled"
|
:title="extension.displayName"
|
||||||
@update:model-value="updateExtension(extension.id, $event)"
|
:description="extension.description"
|
||||||
/>
|
>
|
||||||
<button
|
<div class="extension-controls">
|
||||||
v-if="!extension.isDefault"
|
<button
|
||||||
class="reset-button"
|
v-if="extension.hasConfig"
|
||||||
@click="resetExtension(extension.id)"
|
class="config-button"
|
||||||
:title="t('settings.extensionsPage.resetToDefault')"
|
@click="toggleExpanded(extension.id)"
|
||||||
|
:class="{ expanded: expandedExtensions.has(extension.id) }"
|
||||||
|
:title="t('settings.extensionsPage.configuration')"
|
||||||
|
>
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/>
|
||||||
|
<circle cx="12" cy="12" r="3"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<div v-else class="config-placeholder"></div>
|
||||||
|
<ToggleSwitch
|
||||||
|
:model-value="extension.enabled"
|
||||||
|
@update:model-value="updateExtension(extension.id, $event)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</SettingItem>
|
||||||
|
|
||||||
|
<!-- 可展开的配置区域 -->
|
||||||
|
<div
|
||||||
|
v-if="extension.hasConfig && expandedExtensions.has(extension.id)"
|
||||||
|
class="extension-config"
|
||||||
|
>
|
||||||
|
<!-- 配置项标题和重置按钮 -->
|
||||||
|
<div class="config-header">
|
||||||
|
<h4 class="config-title">{{ t('settings.extensionsPage.configuration') }}</h4>
|
||||||
|
<button
|
||||||
|
class="reset-button"
|
||||||
|
@click="resetExtension(extension.id)"
|
||||||
|
:title="t('settings.extensionsPage.resetToDefault')"
|
||||||
|
>
|
||||||
|
{{ t('settings.reset') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-for="[configKey, configValue] in Object.entries(extension.defaultConfig)"
|
||||||
|
:key="configKey"
|
||||||
|
class="config-item"
|
||||||
>
|
>
|
||||||
{{ t('settings.reset') }}
|
<SettingItem
|
||||||
</button>
|
: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)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 数字输入框 -->
|
||||||
|
<input
|
||||||
|
v-else-if="getConfigItemType(extension.id, configKey, configValue) === 'number'"
|
||||||
|
type="number"
|
||||||
|
class="config-input"
|
||||||
|
:value="extension.config[configKey] ?? configValue"
|
||||||
|
@input="updateExtensionConfig(extension.id, configKey, parseInt(($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
|
||||||
|
type="text"
|
||||||
|
class="config-input"
|
||||||
|
:value="extension.config[configKey] ?? configValue"
|
||||||
|
@input="updateExtensionConfig(extension.id, configKey, ($event.target as HTMLInputElement).value)"
|
||||||
|
/>
|
||||||
|
</SettingItem>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</SettingItem>
|
</div>
|
||||||
</SettingSection>
|
</SettingSection>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -83,25 +271,130 @@ const resetExtension = async (extensionId: ExtensionID) => {
|
|||||||
max-width: 1000px;
|
max-width: 1000px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.extension-item {
|
||||||
|
border-bottom: 1px solid var(--settings-input-border);
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.extension-controls {
|
.extension-controls {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 12px;
|
||||||
|
min-width: 140px;
|
||||||
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
.reset-button {
|
.config-button {
|
||||||
padding: 4px 8px;
|
padding: 4px;
|
||||||
font-size: 11px;
|
border: none;
|
||||||
border: 1px solid var(--settings-input-border);
|
background: none;
|
||||||
border-radius: 3px;
|
|
||||||
background-color: var(--settings-input-bg);
|
|
||||||
color: var(--settings-text-secondary);
|
color: var(--settings-text-secondary);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
|
border-radius: 4px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: var(--settings-hover);
|
background-color: var(--settings-hover);
|
||||||
color: var(--settings-text);
|
color: var(--settings-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.expanded {
|
||||||
|
color: var(--settings-accent);
|
||||||
|
background-color: var(--settings-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-placeholder {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.extension-config {
|
||||||
|
background-color: var(--settings-input-bg);
|
||||||
|
border-left: 3px solid var(--settings-accent);
|
||||||
|
margin: 8px 0 16px 0;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 13px; /* 调整字体大小 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-title {
|
||||||
|
font-size: 13px; /* 调整字体大小 */
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--settings-text);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reset-button {
|
||||||
|
padding: 6px 12px;
|
||||||
|
font-size: 11px; /* 调整字体大小 */
|
||||||
|
border: 1px solid var(--settings-input-border);
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: var(--settings-input-bg);
|
||||||
|
color: var(--settings-text-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--settings-hover);
|
||||||
|
color: var(--settings-text);
|
||||||
|
border-color: var(--settings-accent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-item {
|
||||||
|
&:not(:last-child) {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 配置项标题和描述字体大小 */
|
||||||
|
:deep(.setting-item-title) {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.setting-item-description) {
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-input, .config-select {
|
||||||
|
min-width: 120px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border: 1px solid var(--settings-input-border);
|
||||||
|
border-radius: 3px;
|
||||||
|
background-color: var(--settings-input-bg);
|
||||||
|
color: var(--settings-text);
|
||||||
|
font-size: 11px; /* 调整字体大小 */
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--settings-accent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-select {
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
@@ -23,8 +23,7 @@ const (
|
|||||||
ExtensionTextHighlight ExtensionID = "textHighlight"
|
ExtensionTextHighlight ExtensionID = "textHighlight"
|
||||||
|
|
||||||
// UI增强扩展
|
// UI增强扩展
|
||||||
ExtensionMinimap ExtensionID = "minimap" // 小地图
|
ExtensionMinimap ExtensionID = "minimap" // 小地图
|
||||||
ExtensionCodeBlast ExtensionID = "codeBlast" // 代码爆炸效果
|
|
||||||
|
|
||||||
// 工具扩展
|
// 工具扩展
|
||||||
ExtensionSearch ExtensionID = "search" // 搜索功能
|
ExtensionSearch ExtensionID = "search" // 搜索功能
|
||||||
@@ -104,18 +103,6 @@ func NewDefaultExtensions() []Extension {
|
|||||||
"autohide": false,
|
"autohide": false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
ID: ExtensionCodeBlast,
|
|
||||||
Category: CategoryUI,
|
|
||||||
Enabled: true,
|
|
||||||
IsDefault: true,
|
|
||||||
Config: ExtensionConfig{
|
|
||||||
"effect": 1,
|
|
||||||
"shake": true,
|
|
||||||
"maxParticles": 300,
|
|
||||||
"shakeIntensity": 3,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
// 工具扩展
|
// 工具扩展
|
||||||
{
|
{
|
||||||
|
Reference in New Issue
Block a user