🚧 Refactor basic services

This commit is contained in:
2025-12-14 02:19:50 +08:00
parent d16905c0a3
commit cc4c2189dc
126 changed files with 18164 additions and 4247 deletions

View File

@@ -0,0 +1,4 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
export * from "./models.js";

View File

@@ -0,0 +1,302 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import {Create as $Create} from "@wailsio/runtime";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import * as theme$0 from "./theme/models.js";
/**
* Document is the model entity for the Document schema.
*/
export class Document {
/**
* ID of the ent.
*/
"id"?: number;
/**
* 创建时间
*/
"created_at": string;
/**
* 最后更新时间
*/
"updated_at": string;
/**
* 删除时间NULL表示未删除
*/
"deleted_at"?: string | null;
/**
* 文档标题
*/
"title": string;
/**
* 文档内容
*/
"content": string;
/**
* 是否锁定
*/
"locked": boolean;
/** Creates a new Document instance. */
constructor($$source: Partial<Document> = {}) {
if (!("created_at" in $$source)) {
this["created_at"] = "";
}
if (!("updated_at" in $$source)) {
this["updated_at"] = "";
}
if (!("title" in $$source)) {
this["title"] = "";
}
if (!("content" in $$source)) {
this["content"] = "";
}
if (!("locked" in $$source)) {
this["locked"] = false;
}
Object.assign(this, $$source);
}
/**
* Creates a new Document instance from a string or object.
*/
static createFrom($$source: any = {}): Document {
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
return new Document($$parsedSource as Partial<Document>);
}
}
/**
* Extension is the model entity for the Extension schema.
*/
export class Extension {
/**
* ID of the ent.
*/
"id"?: number;
/**
* 创建时间
*/
"created_at": string;
/**
* 最后更新时间
*/
"updated_at": string;
/**
* 删除时间NULL表示未删除
*/
"deleted_at"?: string | null;
/**
* 扩展标识符
*/
"key": string;
/**
* 是否启用
*/
"enabled": boolean;
/**
* 扩展配置
*/
"config": { [_: string]: any };
/** Creates a new Extension instance. */
constructor($$source: Partial<Extension> = {}) {
if (!("created_at" in $$source)) {
this["created_at"] = "";
}
if (!("updated_at" in $$source)) {
this["updated_at"] = "";
}
if (!("key" in $$source)) {
this["key"] = "";
}
if (!("enabled" in $$source)) {
this["enabled"] = false;
}
if (!("config" in $$source)) {
this["config"] = {};
}
Object.assign(this, $$source);
}
/**
* Creates a new Extension instance from a string or object.
*/
static createFrom($$source: any = {}): Extension {
const $$createField6_0 = $$createType0;
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
if ("config" in $$parsedSource) {
$$parsedSource["config"] = $$createField6_0($$parsedSource["config"]);
}
return new Extension($$parsedSource as Partial<Extension>);
}
}
/**
* KeyBinding is the model entity for the KeyBinding schema.
*/
export class KeyBinding {
/**
* ID of the ent.
*/
"id"?: number;
/**
* 创建时间
*/
"created_at": string;
/**
* 最后更新时间
*/
"updated_at": string;
/**
* 删除时间NULL表示未删除
*/
"deleted_at"?: string | null;
/**
* 快捷键标识符
*/
"key": string;
/**
* 快捷键命令
*/
"command": string;
/**
* 所属扩展标识符
*/
"extension"?: string;
/**
* 是否启用
*/
"enabled": boolean;
/** Creates a new KeyBinding instance. */
constructor($$source: Partial<KeyBinding> = {}) {
if (!("created_at" in $$source)) {
this["created_at"] = "";
}
if (!("updated_at" in $$source)) {
this["updated_at"] = "";
}
if (!("key" in $$source)) {
this["key"] = "";
}
if (!("command" in $$source)) {
this["command"] = "";
}
if (!("enabled" in $$source)) {
this["enabled"] = false;
}
Object.assign(this, $$source);
}
/**
* Creates a new KeyBinding instance from a string or object.
*/
static createFrom($$source: any = {}): KeyBinding {
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
return new KeyBinding($$parsedSource as Partial<KeyBinding>);
}
}
/**
* Theme is the model entity for the Theme schema.
*/
export class Theme {
/**
* ID of the ent.
*/
"id"?: number;
/**
* 创建时间
*/
"created_at": string;
/**
* 最后更新时间
*/
"updated_at": string;
/**
* 删除时间NULL表示未删除
*/
"deleted_at"?: string | null;
/**
* 主题标识符
*/
"key": string;
/**
* 主题类型
*/
"type": theme$0.Type;
/**
* 主题颜色配置
*/
"colors": { [_: string]: any };
/** Creates a new Theme instance. */
constructor($$source: Partial<Theme> = {}) {
if (!("created_at" in $$source)) {
this["created_at"] = "";
}
if (!("updated_at" in $$source)) {
this["updated_at"] = "";
}
if (!("key" in $$source)) {
this["key"] = "";
}
if (!("type" in $$source)) {
this["type"] = ("" as theme$0.Type);
}
if (!("colors" in $$source)) {
this["colors"] = {};
}
Object.assign(this, $$source);
}
/**
* Creates a new Theme instance from a string or object.
*/
static createFrom($$source: any = {}): Theme {
const $$createField6_0 = $$createType0;
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
if ("colors" in $$parsedSource) {
$$parsedSource["colors"] = $$createField6_0($$parsedSource["colors"]);
}
return new Theme($$parsedSource as Partial<Theme>);
}
}
// Private type creation functions
const $$createType0 = $Create.Map($Create.Any, $Create.Any);

View File

@@ -0,0 +1,4 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
export * from "./models.js";

View File

@@ -0,0 +1,22 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import {Create as $Create} from "@wailsio/runtime";
/**
* Type defines the type for the "type" enum field.
*/
export enum Type {
/**
* The Go zero value for the underlying type of the enum.
*/
$zero = "",
/**
* Type values.
*/
TypeDark = "dark",
TypeLight = "light",
};

View File

@@ -193,58 +193,6 @@ export class ConfigMetadata {
}
}
/**
* Document represents a document in the system
*/
export class Document {
"id": number;
"title": string;
"content": string;
"createdAt": string;
"updatedAt": string;
"is_deleted": boolean;
/**
* 锁定标志,锁定的文档无法被删除
*/
"is_locked": boolean;
/** Creates a new Document instance. */
constructor($$source: Partial<Document> = {}) {
if (!("id" in $$source)) {
this["id"] = 0;
}
if (!("title" in $$source)) {
this["title"] = "";
}
if (!("content" in $$source)) {
this["content"] = "";
}
if (!("createdAt" in $$source)) {
this["createdAt"] = "";
}
if (!("updatedAt" in $$source)) {
this["updatedAt"] = "";
}
if (!("is_deleted" in $$source)) {
this["is_deleted"] = false;
}
if (!("is_locked" in $$source)) {
this["is_locked"] = false;
}
Object.assign(this, $$source);
}
/**
* Creates a new Document instance from a string or object.
*/
static createFrom($$source: any = {}): Document {
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
return new Document($$parsedSource as Partial<Document>);
}
}
/**
* EditingConfig 编辑设置配置
*/
@@ -332,40 +280,21 @@ export class EditingConfig {
}
/**
* Extension 单个扩展配置
* Extension 扩展配置
*/
export class Extension {
/**
* 扩展唯一标识
*/
"id": ExtensionID;
/**
* 是否启用
*/
"key": ExtensionKey;
"enabled": boolean;
/**
* 是否为默认扩展
*/
"isDefault": boolean;
/**
* 扩展配置项
*/
"config": ExtensionConfig;
/** Creates a new Extension instance. */
constructor($$source: Partial<Extension> = {}) {
if (!("id" in $$source)) {
this["id"] = ("" as ExtensionID);
if (!("key" in $$source)) {
this["key"] = ("" as ExtensionKey);
}
if (!("enabled" in $$source)) {
this["enabled"] = false;
}
if (!("isDefault" in $$source)) {
this["isDefault"] = false;
}
if (!("config" in $$source)) {
this["config"] = ({} as ExtensionConfig);
}
@@ -377,10 +306,10 @@ export class Extension {
* Creates a new Extension instance from a string or object.
*/
static createFrom($$source: any = {}): Extension {
const $$createField3_0 = $$createType6;
const $$createField2_0 = $$createType6;
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
if ("config" in $$parsedSource) {
$$parsedSource["config"] = $$createField3_0($$parsedSource["config"]);
$$parsedSource["config"] = $$createField2_0($$parsedSource["config"]);
}
return new Extension($$parsedSource as Partial<Extension>);
}
@@ -392,9 +321,9 @@ export class Extension {
export type ExtensionConfig = { [_: string]: any };
/**
* ExtensionID 扩展标识符
* ExtensionKey 扩展标识符
*/
export enum ExtensionID {
export enum ExtensionKey {
/**
* The Go zero value for the underlying type of the enum.
*/
@@ -442,7 +371,6 @@ export enum ExtensionID {
ExtensionHighlightTrailingWhitespace = "highlightTrailingWhitespace",
/**
* UI增强扩展
* 小地图
*/
ExtensionMinimap = "minimap",
@@ -458,16 +386,14 @@ export enum ExtensionID {
ExtensionContextMenu = "contextMenu",
/**
* 工具扩展
* 搜索功能
*/
ExtensionSearch = "search",
/**
* 核心扩展
* 编辑器核心功能
* HTTP 客户端
*/
ExtensionEditor = "editor",
ExtensionHttpClient = "httpClient",
};
/**
@@ -758,48 +684,25 @@ export class HotkeyCombo {
* KeyBinding 单个快捷键绑定
*/
export class KeyBinding {
/**
* 快捷键动作
*/
"command": KeyBindingCommand;
/**
* 所属扩展
*/
"extension": ExtensionID;
/**
* 快捷键组合(如 "Mod-f", "Ctrl-Shift-p"
*/
"key": string;
/**
* 是否启用
*/
"key": KeyBindingKey;
"command": string;
"extension": ExtensionKey;
"enabled": boolean;
/**
* 是否为默认快捷键
*/
"isDefault": boolean;
/** Creates a new KeyBinding instance. */
constructor($$source: Partial<KeyBinding> = {}) {
if (!("key" in $$source)) {
this["key"] = ("" as KeyBindingKey);
}
if (!("command" in $$source)) {
this["command"] = ("" as KeyBindingCommand);
this["command"] = "";
}
if (!("extension" in $$source)) {
this["extension"] = ("" as ExtensionID);
}
if (!("key" in $$source)) {
this["key"] = "";
this["extension"] = ("" as ExtensionKey);
}
if (!("enabled" in $$source)) {
this["enabled"] = false;
}
if (!("isDefault" in $$source)) {
this["isDefault"] = false;
}
Object.assign(this, $$source);
}
@@ -814,263 +717,258 @@ export class KeyBinding {
}
/**
* KeyBindingCommand 快捷键命令
* KeyBindingKey 快捷键命令
*/
export enum KeyBindingCommand {
export enum KeyBindingKey {
/**
* The Go zero value for the underlying type of the enum.
*/
$zero = "",
/**
* 搜索扩展相关
* 显示搜索
*/
ShowSearchCommand = "showSearch",
ShowSearchKeyBindingKey = "showSearch",
/**
* 隐藏搜索
*/
HideSearchCommand = "hideSearch",
HideSearchKeyBindingKey = "hideSearch",
/**
* 代码块扩展相关
* 块内选择全部
*/
BlockSelectAllCommand = "blockSelectAll",
BlockSelectAllKeyBindingKey = "blockSelectAll",
/**
* 在当前块后添加新块
*/
BlockAddAfterCurrentCommand = "blockAddAfterCurrent",
BlockAddAfterCurrentKeyBindingKey = "blockAddAfterCurrent",
/**
* 在最后添加新块
*/
BlockAddAfterLastCommand = "blockAddAfterLast",
BlockAddAfterLastKeyBindingKey = "blockAddAfterLast",
/**
* 在当前块前添加新块
*/
BlockAddBeforeCurrentCommand = "blockAddBeforeCurrent",
BlockAddBeforeCurrentKeyBindingKey = "blockAddBeforeCurrent",
/**
* 跳转到上一个块
*/
BlockGotoPreviousCommand = "blockGotoPrevious",
BlockGotoPreviousKeyBindingKey = "blockGotoPrevious",
/**
* 跳转到下一个块
*/
BlockGotoNextCommand = "blockGotoNext",
BlockGotoNextKeyBindingKey = "blockGotoNext",
/**
* 选择上一个块
*/
BlockSelectPreviousCommand = "blockSelectPrevious",
BlockSelectPreviousKeyBindingKey = "blockSelectPrevious",
/**
* 选择下一个块
*/
BlockSelectNextCommand = "blockSelectNext",
BlockSelectNextKeyBindingKey = "blockSelectNext",
/**
* 删除当前块
*/
BlockDeleteCommand = "blockDelete",
BlockDeleteKeyBindingKey = "blockDelete",
/**
* 向上移动当前块
*/
BlockMoveUpCommand = "blockMoveUp",
BlockMoveUpKeyBindingKey = "blockMoveUp",
/**
* 向下移动当前块
*/
BlockMoveDownCommand = "blockMoveDown",
BlockMoveDownKeyBindingKey = "blockMoveDown",
/**
* 删除行
*/
BlockDeleteLineCommand = "blockDeleteLine",
BlockDeleteLineKeyBindingKey = "blockDeleteLine",
/**
* 向上移动行
*/
BlockMoveLineUpCommand = "blockMoveLineUp",
BlockMoveLineUpKeyBindingKey = "blockMoveLineUp",
/**
* 向下移动行
*/
BlockMoveLineDownCommand = "blockMoveLineDown",
BlockMoveLineDownKeyBindingKey = "blockMoveLineDown",
/**
* 字符转置
*/
BlockTransposeCharsCommand = "blockTransposeChars",
BlockTransposeCharsKeyBindingKey = "blockTransposeChars",
/**
* 格式化代码块
*/
BlockFormatCommand = "blockFormat",
BlockFormatKeyBindingKey = "blockFormat",
/**
* 复制
*/
BlockCopyCommand = "blockCopy",
BlockCopyKeyBindingKey = "blockCopy",
/**
* 剪切
*/
BlockCutCommand = "blockCut",
BlockCutKeyBindingKey = "blockCut",
/**
* 粘贴
*/
BlockPasteCommand = "blockPaste",
BlockPasteKeyBindingKey = "blockPaste",
/**
* 代码折叠扩展相关
* 折叠代码
*/
FoldCodeCommand = "foldCode",
FoldCodeKeyBindingKey = "foldCode",
/**
* 展开代码
*/
UnfoldCodeCommand = "unfoldCode",
UnfoldCodeKeyBindingKey = "unfoldCode",
/**
* 折叠全部
*/
FoldAllCommand = "foldAll",
FoldAllKeyBindingKey = "foldAll",
/**
* 展开全部
*/
UnfoldAllCommand = "unfoldAll",
UnfoldAllKeyBindingKey = "unfoldAll",
/**
* 通用编辑扩展相关
* 光标按语法左移
*/
CursorSyntaxLeftCommand = "cursorSyntaxLeft",
CursorSyntaxLeftKeyBindingKey = "cursorSyntaxLeft",
/**
* 光标按语法右移
*/
CursorSyntaxRightCommand = "cursorSyntaxRight",
CursorSyntaxRightKeyBindingKey = "cursorSyntaxRight",
/**
* 按语法选择左侧
*/
SelectSyntaxLeftCommand = "selectSyntaxLeft",
SelectSyntaxLeftKeyBindingKey = "selectSyntaxLeft",
/**
* 按语法选择右侧
*/
SelectSyntaxRightCommand = "selectSyntaxRight",
SelectSyntaxRightKeyBindingKey = "selectSyntaxRight",
/**
* 向上复制行
*/
CopyLineUpCommand = "copyLineUp",
CopyLineUpKeyBindingKey = "copyLineUp",
/**
* 向下复制行
*/
CopyLineDownCommand = "copyLineDown",
CopyLineDownKeyBindingKey = "copyLineDown",
/**
* 插入空行
*/
InsertBlankLineCommand = "insertBlankLine",
InsertBlankLineKeyBindingKey = "insertBlankLine",
/**
* 选择行
*/
SelectLineCommand = "selectLine",
SelectLineKeyBindingKey = "selectLine",
/**
* 选择父级语法
*/
SelectParentSyntaxCommand = "selectParentSyntax",
SelectParentSyntaxKeyBindingKey = "selectParentSyntax",
/**
* 减少缩进
*/
IndentLessCommand = "indentLess",
IndentLessKeyBindingKey = "indentLess",
/**
* 增加缩进
*/
IndentMoreCommand = "indentMore",
IndentMoreKeyBindingKey = "indentMore",
/**
* 缩进选择
*/
IndentSelectionCommand = "indentSelection",
IndentSelectionKeyBindingKey = "indentSelection",
/**
* 光标到匹配括号
*/
CursorMatchingBracketCommand = "cursorMatchingBracket",
CursorMatchingBracketKeyBindingKey = "cursorMatchingBracket",
/**
* 切换注释
*/
ToggleCommentCommand = "toggleComment",
ToggleCommentKeyBindingKey = "toggleComment",
/**
* 切换块注释
*/
ToggleBlockCommentCommand = "toggleBlockComment",
ToggleBlockCommentKeyBindingKey = "toggleBlockComment",
/**
* 插入新行并缩进
*/
InsertNewlineAndIndentCommand = "insertNewlineAndIndent",
InsertNewlineAndIndentKeyBindingKey = "insertNewlineAndIndent",
/**
* 向后删除字符
*/
DeleteCharBackwardCommand = "deleteCharBackward",
DeleteCharBackwardKeyBindingKey = "deleteCharBackward",
/**
* 向前删除字符
*/
DeleteCharForwardCommand = "deleteCharForward",
DeleteCharForwardKeyBindingKey = "deleteCharForward",
/**
* 向后删除组
*/
DeleteGroupBackwardCommand = "deleteGroupBackward",
DeleteGroupBackwardKeyBindingKey = "deleteGroupBackward",
/**
* 向前删除组
*/
DeleteGroupForwardCommand = "deleteGroupForward",
DeleteGroupForwardKeyBindingKey = "deleteGroupForward",
/**
* 历史记录扩展相关
* 撤销
*/
HistoryUndoCommand = "historyUndo",
HistoryUndoKeyBindingKey = "historyUndo",
/**
* 重做
*/
HistoryRedoCommand = "historyRedo",
HistoryRedoKeyBindingKey = "historyRedo",
/**
* 撤销选择
*/
HistoryUndoSelectionCommand = "historyUndoSelection",
HistoryUndoSelectionKeyBindingKey = "historyUndoSelection",
/**
* 重做选择
*/
HistoryRedoSelectionCommand = "historyRedoSelection",
HistoryRedoSelectionKeyBindingKey = "historyRedoSelection",
};
/**
@@ -1138,76 +1036,6 @@ export enum TabType {
TabTypeTab = "tab",
};
/**
* Theme 主题数据库模型
*/
export class Theme {
"id": number;
"name": string;
"type": ThemeType;
"colors": ThemeColorConfig;
"isDefault": boolean;
"createdAt": string;
"updatedAt": string;
/** Creates a new Theme instance. */
constructor($$source: Partial<Theme> = {}) {
if (!("id" in $$source)) {
this["id"] = 0;
}
if (!("name" in $$source)) {
this["name"] = "";
}
if (!("type" in $$source)) {
this["type"] = ("" as ThemeType);
}
if (!("colors" in $$source)) {
this["colors"] = ({} as ThemeColorConfig);
}
if (!("isDefault" in $$source)) {
this["isDefault"] = false;
}
if (!("createdAt" in $$source)) {
this["createdAt"] = "";
}
if (!("updatedAt" in $$source)) {
this["updatedAt"] = "";
}
Object.assign(this, $$source);
}
/**
* Creates a new Theme instance from a string or object.
*/
static createFrom($$source: any = {}): Theme {
const $$createField3_0 = $$createType9;
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
if ("colors" in $$parsedSource) {
$$parsedSource["colors"] = $$createField3_0($$parsedSource["colors"]);
}
return new Theme($$parsedSource as Partial<Theme>);
}
}
/**
* ThemeColorConfig 使用与前端 ThemeColors 相同的结构,存储任意主题键值
*/
export type ThemeColorConfig = { [_: string]: any };
/**
* ThemeType 主题类型枚举
*/
export enum ThemeType {
/**
* The Go zero value for the underlying type of the enum.
*/
$zero = "",
ThemeTypeDark = "dark",
ThemeTypeLight = "light",
};
/**
* UpdateSourceType 更新源类型
*/
@@ -1306,8 +1134,8 @@ export class UpdatesConfig {
* Creates a new UpdatesConfig instance from a string or object.
*/
static createFrom($$source: any = {}): UpdatesConfig {
const $$createField6_0 = $$createType10;
const $$createField7_0 = $$createType11;
const $$createField6_0 = $$createType9;
const $$createField7_0 = $$createType10;
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
if ("github" in $$parsedSource) {
$$parsedSource["github"] = $$createField6_0($$parsedSource["github"]);
@@ -1334,11 +1162,5 @@ var $$createType6 = (function $$initCreateType6(...args): any {
});
const $$createType7 = $Create.Map($Create.Any, $Create.Any);
const $$createType8 = HotkeyCombo.createFrom;
var $$createType9 = (function $$initCreateType9(...args): any {
if ($$createType9 === $$initCreateType9) {
$$createType9 = $$createType7;
}
return $$createType9(...args);
});
const $$createType10 = GithubConfig.createFrom;
const $$createType11 = GiteaConfig.createFrom;
const $$createType9 = GithubConfig.createFrom;
const $$createType10 = GiteaConfig.createFrom;

View File

@@ -2,7 +2,7 @@
// This file is automatically generated. DO NOT EDIT
/**
* DatabaseService provides shared database functionality
* DatabaseService 数据库服务
* @module
*/
@@ -15,15 +15,7 @@ import {Call as $Call, Create as $Create} from "@wailsio/runtime";
import * as application$0 from "../../../github.com/wailsapp/wails/v3/pkg/application/models.js";
/**
* RegisterModel 注册模型与表的映射关系
*/
export function RegisterModel(tableName: string, model: any): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(175397515, tableName, model) as any;
return $resultPromise;
}
/**
* ServiceShutdown shuts down the service when the application closes
* ServiceShutdown 服务关闭
*/
export function ServiceShutdown(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(3907893632) as any;
@@ -31,7 +23,7 @@ export function ServiceShutdown(): Promise<void> & { cancel(): void } {
}
/**
* ServiceStartup initializes the service when the application starts
* ServiceStartup 服务启动
*/
export function ServiceStartup(options: application$0.ServiceOptions): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(2067840771, options) as any;

View File

@@ -2,7 +2,7 @@
// This file is automatically generated. DO NOT EDIT
/**
* DocumentService provides document management functionality
* DocumentService 文档服务
* @module
*/
@@ -15,12 +15,12 @@ import {Call as $Call, Create as $Create} from "@wailsio/runtime";
import * as application$0 from "../../../github.com/wailsapp/wails/v3/pkg/application/models.js";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import * as models$0 from "../models/models.js";
import * as ent$0 from "../models/ent/models.js";
/**
* CreateDocument creates a new document and returns the created document with ID
* CreateDocument 创建文档
*/
export function CreateDocument(title: string): Promise<models$0.Document | null> & { cancel(): void } {
export function CreateDocument(title: string): Promise<ent$0.Document | null> & { cancel(): void } {
let $resultPromise = $Call.ByID(3360680842, title) as any;
let $typingPromise = $resultPromise.then(($result: any) => {
return $$createType1($result);
@@ -30,7 +30,7 @@ export function CreateDocument(title: string): Promise<models$0.Document | null>
}
/**
* DeleteDocument marks a document as deleted (default document with ID=1 cannot be deleted)
* DeleteDocument 删除文档
*/
export function DeleteDocument(id: number): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(412287269, id) as any;
@@ -38,9 +38,9 @@ export function DeleteDocument(id: number): Promise<void> & { cancel(): void } {
}
/**
* GetDocumentByID gets a document by ID
* GetDocumentByID 根据ID获取文档
*/
export function GetDocumentByID(id: number): Promise<models$0.Document | null> & { cancel(): void } {
export function GetDocumentByID(id: number): Promise<ent$0.Document | null> & { cancel(): void } {
let $resultPromise = $Call.ByID(3468193232, id) as any;
let $typingPromise = $resultPromise.then(($result: any) => {
return $$createType1($result);
@@ -50,9 +50,9 @@ export function GetDocumentByID(id: number): Promise<models$0.Document | null> &
}
/**
* ListAllDocumentsMeta lists all active (non-deleted) document metadata
* ListAllDocumentsMeta 列出所有文档
*/
export function ListAllDocumentsMeta(): Promise<(models$0.Document | null)[]> & { cancel(): void } {
export function ListAllDocumentsMeta(): Promise<(ent$0.Document | null)[]> & { cancel(): void } {
let $resultPromise = $Call.ByID(3073950297) as any;
let $typingPromise = $resultPromise.then(($result: any) => {
return $$createType2($result);
@@ -62,19 +62,7 @@ export function ListAllDocumentsMeta(): Promise<(models$0.Document | null)[]> &
}
/**
* ListDeletedDocumentsMeta lists all deleted document metadata
*/
export function ListDeletedDocumentsMeta(): Promise<(models$0.Document | null)[]> & { cancel(): void } {
let $resultPromise = $Call.ByID(490143787) as any;
let $typingPromise = $resultPromise.then(($result: any) => {
return $$createType2($result);
}) as any;
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
return $typingPromise;
}
/**
* LockDocument 锁定文档,防止删除
* LockDocument 锁定文档
*/
export function LockDocument(id: number): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(1889494473, id) as any;
@@ -82,15 +70,7 @@ export function LockDocument(id: number): Promise<void> & { cancel(): void } {
}
/**
* RestoreDocument restores a deleted document
*/
export function RestoreDocument(id: number): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(784200778, id) as any;
return $resultPromise;
}
/**
* ServiceStartup initializes the service when the application starts
* ServiceStartup 服务启动
*/
export function ServiceStartup(options: application$0.ServiceOptions): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(1474135487, options) as any;
@@ -106,7 +86,7 @@ export function UnlockDocument(id: number): Promise<void> & { cancel(): void } {
}
/**
* UpdateDocumentContent updates the content of a document
* UpdateDocumentContent 更新文档内容
*/
export function UpdateDocumentContent(id: number, content: string): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(3251897116, id, content) as any;
@@ -114,7 +94,7 @@ export function UpdateDocumentContent(id: number, content: string): Promise<void
}
/**
* UpdateDocumentTitle updates the title of a document
* UpdateDocumentTitle 更新文档标题
*/
export function UpdateDocumentTitle(id: number, title: string): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(2045530459, id, title) as any;
@@ -122,6 +102,6 @@ export function UpdateDocumentTitle(id: number, title: string): Promise<void> &
}
// Private type creation functions
const $$createType0 = models$0.Document.createFrom;
const $$createType0 = ent$0.Document.createFrom;
const $$createType1 = $Create.Nullable($$createType0);
const $$createType2 = $Create.Array($$createType1);

View File

@@ -2,7 +2,7 @@
// This file is automatically generated. DO NOT EDIT
/**
* ExtensionService 扩展管理服务
* ExtensionService 扩展服务
* @module
*/
@@ -16,12 +16,39 @@ import * as application$0 from "../../../github.com/wailsapp/wails/v3/pkg/applic
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import * as models$0 from "../models/models.js";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import * as ent$0 from "../models/ent/models.js";
/**
* GetAllExtensions 获取所有扩展配置
* GetAllExtensions 获取所有扩展
*/
export function GetAllExtensions(): Promise<models$0.Extension[]> & { cancel(): void } {
export function GetAllExtensions(): Promise<(ent$0.Extension | null)[]> & { cancel(): void } {
let $resultPromise = $Call.ByID(3094292124) as any;
let $typingPromise = $resultPromise.then(($result: any) => {
return $$createType2($result);
}) as any;
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
return $typingPromise;
}
/**
* GetDefaultExtensions 获取默认扩展配置(用于前端绑定生成)
*/
export function GetDefaultExtensions(): Promise<models$0.Extension[]> & { cancel(): void } {
let $resultPromise = $Call.ByID(4036328166) as any;
let $typingPromise = $resultPromise.then(($result: any) => {
return $$createType4($result);
}) as any;
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
return $typingPromise;
}
/**
* GetExtensionByKey 根据Key获取扩展
*/
export function GetExtensionByKey(key: string): Promise<ent$0.Extension | null> & { cancel(): void } {
let $resultPromise = $Call.ByID(2551065776, key) as any;
let $typingPromise = $resultPromise.then(($result: any) => {
return $$createType1($result);
}) as any;
@@ -30,23 +57,15 @@ export function GetAllExtensions(): Promise<models$0.Extension[]> & { cancel():
}
/**
* ResetAllExtensionsToDefault 重置所有扩展到默认状态
* ResetExtensionConfig 重置单个扩展到默认状态
*/
export function ResetAllExtensionsToDefault(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(270611949) as any;
export function ResetExtensionConfig(key: string): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(3990780299, key) as any;
return $resultPromise;
}
/**
* ResetExtensionToDefault 重置扩展到默认状态
*/
export function ResetExtensionToDefault(id: models$0.ExtensionID): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(868308101, id) as any;
return $resultPromise;
}
/**
* ServiceStartup 启动时调用
* ServiceStartup 服务启动
*/
export function ServiceStartup(options: application$0.ServiceOptions): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(40324057, options) as any;
@@ -54,21 +73,32 @@ export function ServiceStartup(options: application$0.ServiceOptions): Promise<v
}
/**
* UpdateExtensionEnabled 更新扩展启用状态
* SyncExtensions 同步扩展配置
*/
export function UpdateExtensionEnabled(id: models$0.ExtensionID, enabled: boolean): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(1067300094, id, enabled) as any;
export function SyncExtensions(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(167560004) as any;
return $resultPromise;
}
/**
* UpdateExtensionState 更新扩展状态
* UpdateExtensionConfig 更新扩展配置
*/
export function UpdateExtensionState(id: models$0.ExtensionID, enabled: boolean, config: models$0.ExtensionConfig): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(2917946454, id, enabled, config) as any;
export function UpdateExtensionConfig(key: string, config: { [_: string]: any }): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(3184142503, key, config) as any;
return $resultPromise;
}
/**
* UpdateExtensionEnabled 更新扩展启用状态
*/
export function UpdateExtensionEnabled(key: string, enabled: boolean): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(1067300094, key, enabled) as any;
return $resultPromise;
}
// Private type creation functions
const $$createType0 = models$0.Extension.createFrom;
const $$createType1 = $Create.Array($$createType0);
const $$createType0 = ent$0.Extension.createFrom;
const $$createType1 = $Create.Nullable($$createType0);
const $$createType2 = $Create.Array($$createType1);
const $$createType3 = models$0.Extension.createFrom;
const $$createType4 = $Create.Array($$createType3);

View File

@@ -17,7 +17,6 @@ import * as SystemService from "./systemservice.js";
import * as TestService from "./testservice.js";
import * as ThemeService from "./themeservice.js";
import * as TranslationService from "./translationservice.js";
import * as TrayService from "./trayservice.js";
import * as WindowService from "./windowservice.js";
export {
BackupService,
@@ -36,7 +35,6 @@ export {
TestService,
ThemeService,
TranslationService,
TrayService,
WindowService
};

View File

@@ -2,7 +2,7 @@
// This file is automatically generated. DO NOT EDIT
/**
* KeyBindingService 快捷键管理服务
* KeyBindingService 快捷键服务
* @module
*/
@@ -16,12 +16,39 @@ import * as application$0 from "../../../github.com/wailsapp/wails/v3/pkg/applic
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import * as models$0 from "../models/models.js";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import * as ent$0 from "../models/ent/models.js";
/**
* GetAllKeyBindings 获取所有快捷键配置
* GetAllKeyBindings 获取所有快捷键
*/
export function GetAllKeyBindings(): Promise<models$0.KeyBinding[]> & { cancel(): void } {
export function GetAllKeyBindings(): Promise<(ent$0.KeyBinding | null)[]> & { cancel(): void } {
let $resultPromise = $Call.ByID(1633502882) as any;
let $typingPromise = $resultPromise.then(($result: any) => {
return $$createType2($result);
}) as any;
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
return $typingPromise;
}
/**
* GetDefaultKeyBindings 获取默认快捷键配置
*/
export function GetDefaultKeyBindings(): Promise<models$0.KeyBinding[]> & { cancel(): void } {
let $resultPromise = $Call.ByID(3843471588) as any;
let $typingPromise = $resultPromise.then(($result: any) => {
return $$createType4($result);
}) as any;
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
return $typingPromise;
}
/**
* GetKeyBindingByKey 根据Key获取快捷键
*/
export function GetKeyBindingByKey(key: string): Promise<ent$0.KeyBinding | null> & { cancel(): void } {
let $resultPromise = $Call.ByID(852938650, key) as any;
let $typingPromise = $resultPromise.then(($result: any) => {
return $$createType1($result);
}) as any;
@@ -30,13 +57,40 @@ export function GetAllKeyBindings(): Promise<models$0.KeyBinding[]> & { cancel()
}
/**
* ServiceStartup 启动时调用
* ServiceStartup 服务启动
*/
export function ServiceStartup(options: application$0.ServiceOptions): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(2057121990, options) as any;
return $resultPromise;
}
/**
* SyncKeyBindings 同步快捷键配置
*/
export function SyncKeyBindings(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(1522202638) as any;
return $resultPromise;
}
/**
* UpdateKeyBindingCommand 更新快捷键命令
*/
export function UpdateKeyBindingCommand(key: string, command: string): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(1293670628, key, command) as any;
return $resultPromise;
}
/**
* UpdateKeyBindingEnabled 更新快捷键启用状态
*/
export function UpdateKeyBindingEnabled(key: string, enabled: boolean): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(843626124, key, enabled) as any;
return $resultPromise;
}
// Private type creation functions
const $$createType0 = models$0.KeyBinding.createFrom;
const $$createType1 = $Create.Array($$createType0);
const $$createType0 = ent$0.KeyBinding.createFrom;
const $$createType1 = $Create.Nullable($$createType0);
const $$createType2 = $Create.Array($$createType1);
const $$createType3 = models$0.KeyBinding.createFrom;
const $$createType4 = $Create.Array($$createType3);

View File

@@ -191,15 +191,14 @@ export class MemoryStats {
* MigrationProgress 迁移进度信息
*/
export class MigrationProgress {
"status": MigrationStatus;
/**
* 0-100
*/
"progress": number;
"error"?: string;
/** Creates a new MigrationProgress instance. */
constructor($$source: Partial<MigrationProgress> = {}) {
if (!("status" in $$source)) {
this["status"] = ("" as MigrationStatus);
}
if (!("progress" in $$source)) {
this["progress"] = 0;
}
@@ -216,20 +215,6 @@ export class MigrationProgress {
}
}
/**
* MigrationStatus 迁移状态
*/
export enum MigrationStatus {
/**
* The Go zero value for the underlying type of the enum.
*/
$zero = "",
MigrationStatusMigrating = "migrating",
MigrationStatusCompleted = "completed",
MigrationStatusFailed = "failed",
};
/**
* OSInfo 操作系统信息
*/

View File

@@ -15,13 +15,13 @@ import {Call as $Call, Create as $Create} from "@wailsio/runtime";
import * as application$0 from "../../../github.com/wailsapp/wails/v3/pkg/application/models.js";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import * as models$0 from "../models/models.js";
import * as ent$0 from "../models/ent/models.js";
/**
* GetThemeByName 通过名称获取主题覆盖,若不存在则返回 nil
* GetThemeByKey 根据Key获取主题
*/
export function GetThemeByName(name: string): Promise<models$0.Theme | null> & { cancel(): void } {
let $resultPromise = $Call.ByID(1938954770, name) as any;
export function GetThemeByKey(key: string): Promise<ent$0.Theme | null> & { cancel(): void } {
let $resultPromise = $Call.ByID(808794256, key) as any;
let $typingPromise = $resultPromise.then(($result: any) => {
return $$createType1($result);
}) as any;
@@ -30,18 +30,10 @@ export function GetThemeByName(name: string): Promise<models$0.Theme | null> & {
}
/**
* ResetTheme 删除指定主题的覆盖配置
* ResetTheme 删除主题
*/
export function ResetTheme(name: string): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(1806334457, name) as any;
return $resultPromise;
}
/**
* ServiceShutdown 服务关闭
*/
export function ServiceShutdown(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(1676749034) as any;
export function ResetTheme(key: string): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(1806334457, key) as any;
return $resultPromise;
}
@@ -54,13 +46,13 @@ export function ServiceStartup(options: application$0.ServiceOptions): Promise<v
}
/**
* UpdateTheme 保存或更新主题覆盖
* UpdateTheme 保存或更新主题
*/
export function UpdateTheme(name: string, colors: models$0.ThemeColorConfig): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(70189749, name, colors) as any;
export function UpdateTheme(key: string, colors: { [_: string]: any }): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(70189749, key, colors) as any;
return $resultPromise;
}
// Private type creation functions
const $$createType0 = models$0.Theme.createFrom;
const $$createType0 = ent$0.Theme.createFrom;
const $$createType1 = $Create.Nullable($$createType0);

View File

@@ -1,59 +0,0 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
/**
* TrayService 系统托盘服务
* @module
*/
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import {Call as $Call, Create as $Create} from "@wailsio/runtime";
/**
* AutoShowHide 自动显示/隐藏主窗口
*/
export function AutoShowHide(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(4044219428) as any;
return $resultPromise;
}
/**
* HandleWindowClose 处理窗口关闭事件
*/
export function HandleWindowClose(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(1824247204) as any;
return $resultPromise;
}
/**
* HandleWindowMinimize 处理窗口最小化事件
*/
export function HandleWindowMinimize(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(178686624) as any;
return $resultPromise;
}
/**
* MinimizeButtonClicked 处理标题栏最小化按钮点击
*/
export function MinimizeButtonClicked(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(2477618539) as any;
return $resultPromise;
}
/**
* ShouldMinimizeToTray 检查是否应该最小化到托盘
*/
export function ShouldMinimizeToTray(): Promise<boolean> & { cancel(): void } {
let $resultPromise = $Call.ByID(3403884012) as any;
return $resultPromise;
}
/**
* ShowWindow 显示主窗口
*/
export function ShowWindow(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(1315913255) as any;
return $resultPromise;
}

View File

@@ -19,13 +19,13 @@ onBeforeMount(async () => {
// 并行初始化配置、系统信息和快捷键配置
await Promise.all([
configStore.initConfig(),
systemStore.initializeSystemInfo(),
systemStore.initSystemInfo(),
keybindingStore.loadKeyBindings(),
]);
// 初始化语言和主题
await configStore.initializeLanguage();
await themeStore.initializeTheme();
await configStore.initLanguage();
await themeStore.initTheme();
await translationStore.loadTranslators();
// 启动时检查更新

View File

@@ -1,7 +0,0 @@
/**
* 翻译图标SVG
*/
export const TRANSLATION_ICON_SVG = `
<svg class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="24" height="24">
<path d="M599.68 485.056h-8l30.592 164.672c20.352-7.04 38.72-17.344 54.912-31.104a271.36 271.36 0 0 1-40.704-64.64l32.256-4.032c8.896 17.664 19.072 33.28 30.592 46.72 23.872-27.968 42.24-65.152 55.04-111.744l-154.688 0.128z m121.92 133.76c18.368 15.36 39.36 26.56 62.848 33.472l14.784 4.416-8.64 30.336-14.72-4.352a205.696 205.696 0 0 1-76.48-41.728c-20.672 17.92-44.928 31.552-71.232 40.064l20.736 110.912H519.424l-9.984 72.512h385.152c18.112 0 32.704-14.144 32.704-31.616V295.424a32.128 32.128 0 0 0-32.704-31.552H550.528l35.2 189.696h79.424v-31.552h61.44v31.552h102.4v31.616h-42.688c-14.272 55.488-35.712 100.096-64.64 133.568zM479.36 791.68H193.472c-36.224 0-65.472-28.288-65.472-63.168V191.168C128 156.16 157.312 128 193.472 128h327.68l20.544 104.32h352.832c36.224 0 65.472 28.224 65.472 63.104v537.408c0 34.944-29.312 63.168-65.472 63.168H468.608l10.688-104.32zM337.472 548.352v-33.28H272.768v-48.896h60.16V433.28h-60.16v-41.728h64.704v-32.896h-102.4v189.632h102.4z m158.272 0V453.76c0-17.216-4.032-30.272-12.16-39.488-8.192-9.152-20.288-13.696-36.032-13.696a55.04 55.04 0 0 0-24.768 5.376 39.04 39.04 0 0 0-17.088 15.936h-1.984l-5.056-18.56h-28.352V548.48h37.12V480c0-17.088 2.304-29.376 6.912-36.736 4.608-7.424 12.16-11.072 22.528-11.072 7.616 0 13.248 2.56 16.64 7.872 3.52 5.248 5.312 13.056 5.312 23.488v84.736h36.928z" fill="currentColor"></path>
</svg>`;

View File

@@ -0,0 +1,65 @@
/**
* Formatter utility functions
*/
export interface DateTimeFormatOptions {
locale?: string;
includeTime?: boolean;
hour12?: boolean;
}
/**
* Format date time string to localized format
* @param dateString - ISO date string or null
* @param options - Formatting options
* @returns Formatted date string or error message
*/
export const formatDateTime = (
dateString: string | null,
options: DateTimeFormatOptions = {}
): string => {
const {
locale = 'en-US',
includeTime = true,
hour12 = false
} = options;
if (!dateString) {
return 'Unknown time';
}
try {
const date = new Date(dateString);
if (isNaN(date.getTime())) {
return 'Invalid date';
}
const formatOptions: Intl.DateTimeFormatOptions = {
year: 'numeric',
month: '2-digit',
day: '2-digit',
};
if (includeTime) {
formatOptions.hour = '2-digit';
formatOptions.minute = '2-digit';
formatOptions.hour12 = hour12;
}
return date.toLocaleString(locale, formatOptions);
} catch {
return 'Time error';
}
};
/**
* Truncate string with ellipsis
* @param str - String to truncate
* @param maxLength - Maximum length before truncation
* @returns Truncated string with ellipsis if needed
*/
export const truncateString = (str: string, maxLength: number): string => {
if (!str) return '';
return str.length > maxLength ? str.substring(0, maxLength) + '...' : str;
};

View File

@@ -0,0 +1,42 @@
/**
* Validation utility functions
*/
/**
* Validate document title
* @param title - The title to validate
* @param maxLength - Maximum allowed length (default: 50)
* @returns Error message if invalid, null if valid
*/
export const validateDocumentTitle = (title: string, maxLength: number = 50): string | null => {
const trimmed = title.trim();
if (!trimmed) {
return 'Document name cannot be empty';
}
if (trimmed.length > maxLength) {
return `Document name cannot exceed ${maxLength} characters`;
}
return null;
};
/**
* Check if a string is empty or whitespace only
* @param value - The string to check
* @returns true if empty or whitespace only
*/
export const isEmpty = (value: string | null | undefined): boolean => {
return !value || value.trim().length === 0;
};
/**
* Check if a string exceeds max length
* @param value - The string to check
* @param maxLength - Maximum allowed length
* @returns true if exceeds max length
*/
export const exceedsMaxLength = (value: string, maxLength: number): boolean => {
return value.trim().length > maxLength;
};

View File

@@ -1,35 +1,40 @@
<template>
<div
v-if="visible && canClose"
class="tab-context-menu"
:style="{
left: position.x + 'px',
top: position.y + 'px'
}"
@click.stop
v-if="visible && canClose"
v-click-outside="handleClose"
class="tab-context-menu"
:style="{
left: position.x + 'px',
top: position.y + 'px'
}"
@click.stop
>
<div v-if="canClose" class="menu-item" @click="handleMenuClick('close')">
<svg class="menu-icon" xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<svg class="menu-icon" xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M18 6L6 18M6 6l12 12"/>
</svg>
<span class="menu-text">{{ t('tabs.contextMenu.closeTab') }}</span>
</div>
<div v-if="hasOtherTabs" class="menu-item" @click="handleMenuClick('closeOthers')">
<svg class="menu-icon" xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<svg class="menu-icon" xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"/>
<path d="M9 9l6 6M15 9l-6 6"/>
</svg>
<span class="menu-text">{{ t('tabs.contextMenu.closeOthers') }}</span>
</div>
<div v-if="hasTabsToLeft" class="menu-item" @click="handleMenuClick('closeLeft')">
<svg class="menu-icon" xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<svg class="menu-icon" xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M15 18l-6-6 6-6"/>
<path d="M9 18l-6-6 6-6"/>
</svg>
<span class="menu-text">{{ t('tabs.contextMenu.closeLeft') }}</span>
</div>
<div v-if="hasTabsToRight" class="menu-item" @click="handleMenuClick('closeRight')">
<svg class="menu-icon" xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<svg class="menu-icon" xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M9 18l6-6-6-6"/>
<path d="M15 18l6-6-6-6"/>
</svg>
@@ -39,9 +44,9 @@
</template>
<script setup lang="ts">
import { computed, onMounted, onUnmounted } from 'vue';
import { useI18n } from 'vue-i18n';
import { useTabStore } from '@/stores/tabStore';
import {computed, onMounted, onUnmounted} from 'vue';
import {useI18n} from 'vue-i18n';
import {useTabStore} from '@/stores/tabStore';
interface Props {
visible: boolean;
@@ -54,7 +59,7 @@ const emit = defineEmits<{
close: [];
}>();
const { t } = useI18n();
const {t} = useI18n();
const tabStore = useTabStore();
// 计算属性
@@ -79,6 +84,9 @@ const hasTabsToLeft = computed(() => {
return index > 0;
});
const handleClose = () => {
emit('close');
};
// 处理菜单项点击
const handleMenuClick = (action: string) => {
if (!props.targetDocumentId) return;
@@ -97,34 +105,9 @@ const handleMenuClick = (action: string) => {
tabStore.closeTabsToRight(props.targetDocumentId);
break;
}
emit('close');
handleClose();
};
// 处理外部点击
const handleClickOutside = (_event: MouseEvent) => {
if (props.visible) {
emit('close');
}
};
// 处理ESC键
const handleEscapeKey = (event: KeyboardEvent) => {
if (event.key === 'Escape' && props.visible) {
emit('close');
}
};
// 生命周期
onMounted(() => {
document.addEventListener('click', handleClickOutside);
document.addEventListener('keydown', handleEscapeKey);
});
onUnmounted(() => {
document.removeEventListener('click', handleClickOutside);
document.removeEventListener('keydown', handleEscapeKey);
});
</script>
<style scoped lang="scss">
@@ -150,12 +133,12 @@ onUnmounted(() => {
color: var(--text-primary);
transition: all 0.15s ease;
gap: 8px;
&:hover {
background-color: var(--toolbar-button-hover);
color: var(--text-primary);
}
&:active {
background-color: var(--border-color);
}
@@ -167,7 +150,7 @@ onUnmounted(() => {
height: 12px;
color: var(--text-primary);
transition: color 0.15s ease;
.menu-item:hover & {
color: var(--text-primary);
}
@@ -178,4 +161,4 @@ onUnmounted(() => {
font-weight: 400;
flex: 1;
}
</style>
</style>

View File

@@ -1,13 +1,15 @@
<template>
<div class="linux-titlebar" style="--wails-draggable:drag" @contextmenu.prevent>
<div class="titlebar-content" @dblclick="toggleMaximize" @contextmenu.prevent>
<div class="titlebar-content" @dblclick="handleToggleMaximize" @contextmenu.prevent>
<div class="titlebar-icon">
<img src="/appicon.png" alt="voidraft"/>
</div>
<div v-if="!tabStore.isTabsEnabled && !isInSettings" class="titlebar-title" :title="fullTitleText">{{ titleText }}</div>
<div v-if="!tabStore.isTabsEnabled && !isInSettings" class="titlebar-title" :title="fullTitleText">
{{ titleText }}
</div>
<!-- 标签页容器区域 -->
<div class="titlebar-tabs" v-if="tabStore.isTabsEnabled && !isInSettings" style="--wails-draggable:drag">
<TabContainer />
<TabContainer/>
</div>
<!-- 设置页面标题 -->
<div v-if="isInSettings" class="titlebar-title" :title="fullTitleText">{{ titleText }}</div>
@@ -26,7 +28,7 @@
<button
class="titlebar-button maximize-button"
@click="toggleMaximize"
@click="handleToggleMaximize"
:title="isMaximized ? t('titlebar.restore') : t('titlebar.maximize')"
>
<svg width="16" height="16" viewBox="0 0 16 16" v-if="!isMaximized">
@@ -55,81 +57,43 @@
import {computed, onMounted, ref} from 'vue';
import {useI18n} from 'vue-i18n';
import {useRoute} from 'vue-router';
import * as runtime from '@wailsio/runtime';
import {useDocumentStore} from '@/stores/documentStore';
import {useTabStore} from '@/stores/tabStore';
import TabContainer from '@/components/tabs/TabContainer.vue';
import {useTabStore} from "@/stores/tabStore";
import {
minimizeWindow,
toggleMaximize,
closeWindow,
getMaximizedState,
generateTitleText,
generateFullTitleText
} from './index';
const tabStore = useTabStore();
const {t} = useI18n();
const route = useRoute();
const isMaximized = ref(false);
const tabStore = useTabStore();
const documentStore = useDocumentStore();
// 判断是否在设置页面
const isMaximized = ref(false);
const isInSettings = computed(() => route.path.startsWith('/settings'));
// 计算标题文本
const titleText = computed(() => {
if (isInSettings.value) {
return `voidraft - ` + t('settings.title');
}
const currentDoc = documentStore.currentDocument;
if (currentDoc) {
// 限制文档标题长度,避免标题栏换行
const maxTitleLength = 30;
const truncatedTitle = currentDoc.title.length > maxTitleLength
? currentDoc.title.substring(0, maxTitleLength) + '...'
: currentDoc.title;
return `voidraft - ${truncatedTitle}`;
}
return 'voidraft';
if (isInSettings.value) return `voidraft - ${t('settings.title')}`;
return generateTitleText(documentStore.currentDocument?.title);
});
// 计算完整标题文本用于tooltip
const fullTitleText = computed(() => {
if (isInSettings.value) {
return `voidraft - ` + t('settings.title');
}
const currentDoc = documentStore.currentDocument;
return currentDoc ? `voidraft - ${currentDoc.title}` : 'voidraft';
if (isInSettings.value) return `voidraft - ${t('settings.title')}`;
return generateFullTitleText(documentStore.currentDocument?.title);
});
const minimizeWindow = async () => {
try {
await runtime.Window.Minimise();
} catch (error) {
console.error(error);
}
};
const toggleMaximize = async () => {
try {
await runtime.Window.ToggleMaximise();
await checkMaximizedState();
} catch (error) {
console.error(error);
}
};
const closeWindow = async () => {
try {
await runtime.Window.Close();
} catch (error) {
console.error(error);
}
};
const checkMaximizedState = async () => {
try {
isMaximized.value = await runtime.Window.IsMaximised();
} catch (error) {
console.error(error);
}
const handleToggleMaximize = async () => {
await toggleMaximize();
isMaximized.value = await getMaximizedState();
};
onMounted(async () => {
await checkMaximizedState();
isMaximized.value = await getMaximizedState();
});
</script>
@@ -160,7 +124,7 @@ onMounted(async () => {
font-size: 13px;
font-weight: 500;
cursor: default;
min-width: 0; /* 允许内容收缩 */
min-width: 0;
-webkit-context-menu: none;
-moz-context-menu: none;
@@ -310,4 +274,4 @@ onMounted(async () => {
}
}
}
</style>
</style>

View File

@@ -1,10 +1,10 @@
<template>
<div class="macos-titlebar" style="--wails-draggable:drag" @contextmenu.prevent>
<div class="titlebar-controls" style="--wails-draggable:no-drag" @contextmenu.prevent>
<button
class="titlebar-button close-button"
@click="closeWindow"
:title="t('titlebar.close')"
<button
class="titlebar-button close-button"
@click="closeWindow"
:title="t('titlebar.close')"
>
<div class="button-icon">
<svg width="6" height="6" viewBox="0 0 6 6" v-show="showControlIcons">
@@ -12,11 +12,11 @@
</svg>
</div>
</button>
<button
class="titlebar-button minimize-button"
@click="minimizeWindow"
:title="t('titlebar.minimize')"
<button
class="titlebar-button minimize-button"
@click="minimizeWindow"
:title="t('titlebar.minimize')"
>
<div class="button-icon">
<svg width="8" height="1" viewBox="0 0 8 1" v-show="showControlIcons">
@@ -24,11 +24,11 @@
</svg>
</div>
</button>
<button
class="titlebar-button maximize-button"
@click="toggleMaximize"
:title="isMaximized ? t('titlebar.restore') : t('titlebar.maximize')"
<button
class="titlebar-button maximize-button"
@click="handleToggleMaximize"
:title="isMaximized ? t('titlebar.restore') : t('titlebar.maximize')"
>
<div class="button-icon">
<svg width="6" height="6" viewBox="0 0 6 6" v-show="showControlIcons && !isMaximized">
@@ -42,98 +42,61 @@
</div>
</button>
</div>
<!-- 标签页容器区域 -->
<div class="titlebar-tabs" v-if="tabStore.isTabsEnabled && !isInSettings" style="--wails-draggable:drag">
<TabContainer />
<TabContainer/>
</div>
<div class="titlebar-content" @dblclick="toggleMaximize" @contextmenu.prevent v-if="!tabStore.isTabsEnabled || isInSettings">
<div class="titlebar-content" @dblclick="handleToggleMaximize" @contextmenu.prevent
v-if="!tabStore.isTabsEnabled || isInSettings">
<div class="titlebar-title" :title="fullTitleText">{{ titleText }}</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import * as runtime from '@wailsio/runtime';
import { useDocumentStore } from '@/stores/documentStore';
import {computed, onMounted, ref} from 'vue';
import {useI18n} from 'vue-i18n';
import {useRoute} from 'vue-router';
import {useDocumentStore} from '@/stores/documentStore';
import {useTabStore} from '@/stores/tabStore';
import TabContainer from '@/components/tabs/TabContainer.vue';
import { useTabStore } from "@/stores/tabStore";
import {
minimizeWindow,
toggleMaximize,
closeWindow,
getMaximizedState,
generateTitleText,
generateFullTitleText
} from './index';
const tabStore = useTabStore();
const { t } = useI18n();
const {t} = useI18n();
const route = useRoute();
const isMaximized = ref(false);
const showControlIcons = ref(false);
const tabStore = useTabStore();
const documentStore = useDocumentStore();
// 判断是否在设置页面
const isMaximized = ref(false);
const showControlIcons = ref(false);
const isInSettings = computed(() => route.path.startsWith('/settings'));
const minimizeWindow = async () => {
try {
await runtime.Window.Minimise();
} catch (error) {
console.error(error);
}
};
const toggleMaximize = async () => {
try {
await runtime.Window.ToggleMaximise();
await checkMaximizedState();
} catch (error) {
console.error(error);
}
};
const closeWindow = async () => {
try {
await runtime.Window.Close();
} catch (error) {
console.error(error);
}
};
const checkMaximizedState = async () => {
try {
isMaximized.value = await runtime.Window.IsMaximised();
} catch (error) {
console.error(error);
}
};
// 计算标题文本
const titleText = computed(() => {
if (isInSettings.value) {
return `voidraft - ` + t('settings.title');
}
const currentDoc = documentStore.currentDocument;
if (currentDoc) {
// 限制文档标题长度,避免标题栏换行
const maxTitleLength = 30;
const truncatedTitle = currentDoc.title.length > maxTitleLength
? currentDoc.title.substring(0, maxTitleLength) + '...'
: currentDoc.title;
return `voidraft - ${truncatedTitle}`;
}
return 'voidraft';
if (isInSettings.value) return `voidraft - ${t('settings.title')}`;
return generateTitleText(documentStore.currentDocument?.title);
});
// 计算完整标题文本用于tooltip
const fullTitleText = computed(() => {
if (isInSettings.value) {
return `voidraft - ` + t('settings.title');
}
const currentDoc = documentStore.currentDocument;
return currentDoc ? `voidraft - ${currentDoc.title}` : 'voidraft';
if (isInSettings.value) return `voidraft - ${t('settings.title')}`;
return generateFullTitleText(documentStore.currentDocument?.title);
});
const handleToggleMaximize = async () => {
await toggleMaximize();
isMaximized.value = await getMaximizedState();
};
onMounted(async () => {
await checkMaximizedState();
isMaximized.value = await getMaximizedState();
});
</script>
@@ -147,11 +110,11 @@ onMounted(async () => {
-webkit-user-select: none;
width: 100%;
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', system-ui, sans-serif;
-webkit-context-menu: none;
-moz-context-menu: none;
context-menu: none;
&:hover {
.titlebar-button {
.button-icon {
@@ -168,7 +131,7 @@ onMounted(async () => {
padding-left: 8px;
gap: 8px;
flex-shrink: 0;
-webkit-context-menu: none;
-moz-context-menu: none;
context-menu: none;
@@ -187,7 +150,7 @@ onMounted(async () => {
padding: 0;
margin: 0;
position: relative;
.button-icon {
opacity: 0;
transition: opacity 0.2s ease;
@@ -198,7 +161,7 @@ onMounted(async () => {
height: 100%;
color: rgba(0, 0, 0, 0.7);
}
&:hover .button-icon {
opacity: 1;
}
@@ -206,11 +169,11 @@ onMounted(async () => {
.close-button {
background: #ff5f57;
&:hover {
background: #ff453a;
}
&:active {
background: #d7463f;
}
@@ -218,11 +181,11 @@ onMounted(async () => {
.minimize-button {
background: #ffbd2e;
&:hover {
background: #ffb524;
}
&:active {
background: #e6a220;
}
@@ -230,11 +193,11 @@ onMounted(async () => {
.maximize-button {
background: #28ca42;
&:hover {
background: #1ebe36;
}
&:active {
background: #1ba932;
}
@@ -247,7 +210,7 @@ onMounted(async () => {
flex: 1;
cursor: default;
min-width: 0;
-webkit-context-menu: none;
-moz-context-menu: none;
context-menu: none;
@@ -261,34 +224,32 @@ onMounted(async () => {
margin-left: 8px;
margin-right: 8px;
min-width: 0;
overflow: visible; /* 允许TabContainer内部处理滚动 */
/* 确保TabContainer能够正确处理滚动 */
overflow: visible;
:deep(.tab-container) {
width: 100%;
height: 100%;
}
:deep(.tab-bar) {
width: 100%;
height: 100%;
}
:deep(.tab-scroll-wrapper) {
overflow-x: auto;
overflow-y: hidden;
scrollbar-width: none;
-ms-overflow-style: none;
&::-webkit-scrollbar {
display: none;
}
}
/* 确保底部线条能够正确显示 */
:deep(.tab-item) {
position: relative;
&::after {
content: '';
position: absolute;
@@ -319,13 +280,13 @@ onMounted(async () => {
background: var(--toolbar-bg, #2d2d2d);
border-bottom-color: var(--toolbar-border, rgba(255, 255, 255, 0.1));
}
.titlebar-title {
color: var(--toolbar-text, #fff);
}
.titlebar-button .button-icon {
color: rgba(255, 255, 255, 0.8);
}
}
</style>
</style>

View File

@@ -1,13 +1,15 @@
<template>
<div class="windows-titlebar" style="--wails-draggable:drag">
<div class="titlebar-content" @dblclick="toggleMaximize" @contextmenu.prevent>
<div class="titlebar-content" @dblclick="handleToggleMaximize" @contextmenu.prevent>
<div class="titlebar-icon">
<img src="/appicon.png" alt="voidraft"/>
</div>
<div v-if="!tabStore.isTabsEnabled && !isInSettings" class="titlebar-title" :title="fullTitleText">{{ titleText }}</div>
<div v-if="!tabStore.isTabsEnabled && !isInSettings" class="titlebar-title" :title="fullTitleText">
{{ titleText }}
</div>
<!-- 标签页容器区域 -->
<div class="titlebar-tabs" v-if="tabStore.isTabsEnabled && !isInSettings" style="--wails-draggable:drag">
<TabContainer />
<TabContainer/>
</div>
<!-- 设置页面标题 -->
<div v-if="isInSettings" class="titlebar-title" :title="fullTitleText">{{ titleText }}</div>
@@ -24,7 +26,7 @@
<button
class="titlebar-button maximize-button"
@click="toggleMaximize"
@click="handleToggleMaximize"
:title="isMaximized ? t('titlebar.restore') : t('titlebar.maximize')"
>
<span class="titlebar-icon" v-html="maximizeIcon"></span>
@@ -45,84 +47,44 @@
import {computed, onMounted, ref} from 'vue';
import {useI18n} from 'vue-i18n';
import {useRoute} from 'vue-router';
import * as runtime from '@wailsio/runtime';
import {useDocumentStore} from '@/stores/documentStore';
import {useTabStore} from '@/stores/tabStore';
import TabContainer from '@/components/tabs/TabContainer.vue';
import {useTabStore} from "@/stores/tabStore";
import {
minimizeWindow,
toggleMaximize,
closeWindow,
getMaximizedState,
generateTitleText,
generateFullTitleText
} from './index';
const tabStore = useTabStore();
const {t} = useI18n();
const route = useRoute();
const isMaximized = ref(false);
const tabStore = useTabStore();
const documentStore = useDocumentStore();
// 计算属性用于图标,减少重复渲染
const isMaximized = ref(false);
const maximizeIcon = computed(() => isMaximized.value ? '&#xE923;' : '&#xE922;');
// 判断是否在设置页面
const isInSettings = computed(() => route.path.startsWith('/settings'));
// 计算标题文本
const titleText = computed(() => {
if (isInSettings.value) {
return `voidraft - ` + t('settings.title');
}
const currentDoc = documentStore.currentDocument;
if (currentDoc) {
// 限制文档标题长度,避免标题栏换行
const maxTitleLength = 30;
const truncatedTitle = currentDoc.title.length > maxTitleLength
? currentDoc.title.substring(0, maxTitleLength) + '...'
: currentDoc.title;
return `voidraft - ${truncatedTitle}`;
}
return 'voidraft';
if (isInSettings.value) return `voidraft - ${t('settings.title')}`;
return generateTitleText(documentStore.currentDocument?.title);
});
// 计算完整标题文本用于tooltip
const fullTitleText = computed(() => {
if (isInSettings.value) {
return `voidraft - ` + t('settings.title');
}
const currentDoc = documentStore.currentDocument;
return currentDoc ? `voidraft - ${currentDoc.title}` : 'voidraft';
if (isInSettings.value) return `voidraft - ${t('settings.title')}`;
return generateFullTitleText(documentStore.currentDocument?.title);
});
const minimizeWindow = async () => {
try {
await runtime.Window.Minimise();
} catch (error) {
console.error(error);
}
};
const toggleMaximize = async () => {
try {
await runtime.Window.ToggleMaximise();
await checkMaximizedState();
} catch (error) {
console.error(error);
}
};
const closeWindow = async () => {
try {
await runtime.Window.Close();
} catch (error) {
console.error(error);
}
};
const checkMaximizedState = async () => {
try {
isMaximized.value = await runtime.Window.IsMaximised();
} catch (error) {
console.error(error);
}
const handleToggleMaximize = async () => {
await toggleMaximize();
isMaximized.value = await getMaximizedState();
};
onMounted(async () => {
await checkMaximizedState();
isMaximized.value = await getMaximizedState();
});
</script>
@@ -152,7 +114,7 @@ onMounted(async () => {
font-size: 12px;
font-weight: 400;
cursor: default;
min-width: 0; /* 允许内容收缩 */
min-width: 0;
-webkit-context-menu: none;
-moz-context-menu: none;
@@ -178,7 +140,6 @@ onMounted(async () => {
overflow: hidden;
margin-left: 8px;
min-width: 0;
//margin-right: 8px;
}
.titlebar-controls {
@@ -254,4 +215,4 @@ onMounted(async () => {
opacity: 1;
}
}
</style>
</style>

View File

@@ -0,0 +1,60 @@
import * as runtime from '@wailsio/runtime';
/**
* Titlebar utility functions
*/
// Window control functions
export const minimizeWindow = async () => {
try {
await runtime.Window.Minimise();
} catch (error) {
console.error('Failed to minimize window:', error);
}
};
export const toggleMaximize = async () => {
try {
await runtime.Window.ToggleMaximise();
} catch (error) {
console.error('Failed to toggle maximize:', error);
}
};
export const closeWindow = async () => {
try {
await runtime.Window.Close();
} catch (error) {
console.error('Failed to close window:', error);
}
};
export const getMaximizedState = async (): Promise<boolean> => {
try {
return await runtime.Window.IsMaximised();
} catch (error) {
console.error('Failed to check maximized state:', error);
return false;
}
};
/**
* Generate title text with optional truncation
*/
export const generateTitleText = (
title: string | undefined,
maxLength: number = 30
): string => {
if (!title) return 'voidraft';
const truncated = title.length > maxLength
? title.substring(0, maxLength) + '...'
: title;
return `voidraft - ${truncated}`;
};
/**
* Generate full title text (no truncation)
*/
export const generateFullTitleText = (title: string | undefined): string => {
return title ? `voidraft - ${title}` : 'voidraft';
};

View File

@@ -1,49 +1,63 @@
<script setup lang="ts">
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
import { useDocumentStore } from '@/stores/documentStore';
import { useTabStore } from '@/stores/tabStore';
import { useWindowStore } from '@/stores/windowStore';
import { useI18n } from 'vue-i18n';
import type { Document } from '@/../bindings/voidraft/internal/models/models';
import {computed, nextTick, reactive, ref, watch} from 'vue';
import {useDocumentStore} from '@/stores/documentStore';
import {useTabStore} from '@/stores/tabStore';
import {useWindowStore} from '@/stores/windowStore';
import {useI18n} from 'vue-i18n';
import {useConfirm} from '@/composables';
import {validateDocumentTitle} from '@/common/utils/validation';
import {formatDateTime, truncateString} from '@/common/utils/formatter';
import type {Document} from '@/../bindings/voidraft/internal/models/ent/models';
// 类型定义
interface DocumentItem extends Document {
isCreateOption?: boolean;
}
const documentStore = useDocumentStore();
const tabStore = useTabStore();
const windowStore = useWindowStore();
const { t } = useI18n();
const {t} = useI18n();
// DOM 引用
const inputRef = ref<HTMLInputElement>();
const editInputRef = ref<HTMLInputElement>();
// 组件状态
const inputValue = ref('');
const inputRef = ref<HTMLInputElement>();
const editingId = ref<number | null>(null);
const editingTitle = ref('');
const editInputRef = ref<HTMLInputElement>();
const deleteConfirmId = ref<number | null>(null);
const state = reactive({
isLoaded: false,
searchQuery: '',
editing: {
id: null as number | null,
title: ''
}
});
// 常量
const MAX_TITLE_LENGTH = 50;
const DELETE_CONFIRM_TIMEOUT = 3000;
// 计算属性
const currentDocName = computed(() => {
if (!documentStore.currentDocument) return t('toolbar.selectDocument');
const title = documentStore.currentDocument.title;
return title.length > 12 ? title.substring(0, 12) + '...' : title;
return truncateString(documentStore.currentDocument.title || '', 12);
});
const filteredItems = computed(() => {
const filteredItems = computed<DocumentItem[]>(() => {
const docs = documentStore.documentList;
const query = inputValue.value.trim();
const query = state.searchQuery.trim();
if (!query) return docs;
const filtered = docs.filter(doc =>
doc.title.toLowerCase().includes(query.toLowerCase())
(doc.title || '').toLowerCase().includes(query.toLowerCase())
);
// 如果输入的不是已存在文档的完整标题,添加创建选项
const exactMatch = docs.some(doc => doc.title.toLowerCase() === query.toLowerCase());
const exactMatch = docs.some(doc => (doc.title || '').toLowerCase() === query.toLowerCase());
if (!exactMatch && query.length > 0) {
return [
{ id: -1, title: t('toolbar.createDocument') + ` "${query}"`, isCreateOption: true } as any,
{id: -1, title: t('toolbar.createDocument') + ` "${query}"`, isCreateOption: true} as DocumentItem,
...filtered
];
}
@@ -51,53 +65,32 @@ const filteredItems = computed(() => {
return filtered;
});
// 工具函数
const validateTitle = (title: string): string | null => {
if (!title.trim()) return t('toolbar.documentNameRequired');
if (title.trim().length > MAX_TITLE_LENGTH) {
return t('toolbar.documentNameTooLong', { max: MAX_TITLE_LENGTH });
}
return null;
};
const formatTime = (dateString: string | null) => {
if (!dateString) return t('toolbar.unknownTime');
try {
const date = new Date(dateString);
if (isNaN(date.getTime())) return t('toolbar.invalidDate');
const locale = t('locale') === 'zh-CN' ? 'zh-CN' : 'en-US';
return date.toLocaleString(locale, {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
hour12: false
});
} catch {
return t('toolbar.timeError');
}
};
// 核心操作
const openMenu = async () => {
documentStore.openDocumentSelector();
await documentStore.getDocumentMetaList();
documentStore.openDocumentSelector();
state.isLoaded = true;
await nextTick();
inputRef.value?.focus();
};
// 删除确认
const {isConfirming: isDeleting, startConfirm: startDeleteConfirm, reset: resetDeleteConfirm} = useConfirm({
timeout: DELETE_CONFIRM_TIMEOUT
});
const closeMenu = () => {
state.isLoaded = false;
documentStore.closeDocumentSelector();
inputValue.value = '';
editingId.value = null;
editingTitle.value = '';
deleteConfirmId.value = null;
state.searchQuery = '';
state.editing.id = null;
state.editing.title = '';
resetDeleteConfirm();
};
const selectDoc = async (doc: Document) => {
if (doc.id === undefined) return;
// 如果选择的就是当前文档,直接关闭菜单
if (documentStore.currentDocument?.id === doc.id) {
closeMenu();
@@ -121,7 +114,7 @@ const selectDoc = async (doc: Document) => {
const createDoc = async (title: string) => {
const trimmedTitle = title.trim();
const error = validateTitle(trimmedTitle);
const error = validateDocumentTitle(trimmedTitle, MAX_TITLE_LENGTH);
if (error) return;
try {
@@ -132,20 +125,28 @@ const createDoc = async (title: string) => {
}
};
const selectItem = async (item: any) => {
const selectDocItem = async (item: any) => {
if (item.isCreateOption) {
await createDoc(inputValue.value.trim());
await createDoc(state.searchQuery.trim());
} else {
await selectDoc(item);
}
};
// 搜索框回车处理
const handleSearchEnter = () => {
const query = state.searchQuery.trim();
if (query && filteredItems.value.length > 0) {
selectDocItem(filteredItems.value[0]);
}
};
// 编辑操作
const startRename = (doc: Document, event: Event) => {
const renameDoc = (doc: Document, event: Event) => {
event.stopPropagation();
editingId.value = doc.id;
editingTitle.value = doc.title;
deleteConfirmId.value = null;
state.editing.id = doc.id ?? null;
state.editing.title = doc.title || '';
resetDeleteConfirm();
nextTick(() => {
editInputRef.value?.focus();
editInputRef.value?.select();
@@ -153,35 +154,41 @@ const startRename = (doc: Document, event: Event) => {
};
const saveEdit = async () => {
if (!editingId.value || !editingTitle.value.trim()) {
editingId.value = null;
editingTitle.value = '';
if (!state.editing.id || !state.editing.title.trim()) {
state.editing.id = null;
state.editing.title = '';
return;
}
const trimmedTitle = editingTitle.value.trim();
const error = validateTitle(trimmedTitle);
const trimmedTitle = state.editing.title.trim();
const error = validateDocumentTitle(trimmedTitle, MAX_TITLE_LENGTH);
if (error) return;
try {
await documentStore.updateDocumentMetadata(editingId.value, trimmedTitle);
await documentStore.updateDocumentMetadata(state.editing.id, trimmedTitle);
await documentStore.getDocumentMetaList();
// 如果tabs功能开启且该文档有标签页更新标签页标题
if (tabStore.isTabsEnabled && tabStore.hasTab(editingId.value)) {
tabStore.updateTabTitle(editingId.value, trimmedTitle);
if (tabStore.isTabsEnabled && tabStore.hasTab(state.editing.id)) {
tabStore.updateTabTitle(state.editing.id, trimmedTitle);
}
} catch (error) {
console.error('Failed to update document:', error);
} finally {
editingId.value = null;
editingTitle.value = '';
state.editing.id = null;
state.editing.title = '';
}
};
const cancelEdit = () => {
state.editing.id = null;
state.editing.title = '';
};
// 其他操作
const openInNewWindow = async (doc: Document, event: Event) => {
event.stopPropagation();
if (doc.id === undefined) return;
try {
await documentStore.openDocumentInNewWindow(doc.id);
} catch (error) {
@@ -191,13 +198,14 @@ const openInNewWindow = async (doc: Document, event: Event) => {
const handleDelete = async (doc: Document, event: Event) => {
event.stopPropagation();
if (doc.id === undefined) return;
if (deleteConfirmId.value === doc.id) {
if (isDeleting(doc.id)) {
// 确认删除前检查文档是否在其他窗口打开
const hasOpen = await windowStore.isDocumentWindowOpen(doc.id);
if (hasOpen) {
documentStore.setError(doc.id, t('toolbar.alreadyOpenInNewWindow'));
deleteConfirmId.value = null;
resetDeleteConfirm();
return;
}
@@ -210,228 +218,181 @@ const handleDelete = async (doc: Document, event: Event) => {
if (firstDoc) await selectDoc(firstDoc);
}
}
deleteConfirmId.value = null;
resetDeleteConfirm();
} else {
// 进入确认状态
deleteConfirmId.value = doc.id;
editingId.value = null;
// 3秒后自动取消确认状态
setTimeout(() => {
if (deleteConfirmId.value === doc.id) {
deleteConfirmId.value = null;
}
}, 3000);
startDeleteConfirm(doc.id);
state.editing.id = null;
}
};
// 键盘事件处理
const createKeyHandler = (handlers: Record<string, () => void>) => (event: KeyboardEvent) => {
const handler = handlers[event.key];
if (handler) {
event.preventDefault();
event.stopPropagation();
handler();
}
};
const handleGlobalKeydown = createKeyHandler({
Escape: () => {
if (editingId.value) {
editingId.value = null;
editingTitle.value = '';
} else if (deleteConfirmId.value) {
deleteConfirmId.value = null;
} else {
closeMenu();
}
}
});
const handleInputKeydown = createKeyHandler({
Enter: () => {
const query = inputValue.value.trim();
if (query && filteredItems.value.length > 0) {
selectItem(filteredItems.value[0]);
}
},
Escape: closeMenu
});
const handleEditKeydown = createKeyHandler({
Enter: saveEdit,
Escape: () => {
editingId.value = null;
editingTitle.value = '';
}
});
// 点击外部关闭
const handleClickOutside = (event: Event) => {
const target = event.target as HTMLElement;
if (!target.closest('.document-selector')) {
// 切换菜单
const toggleMenu = () => {
if (documentStore.showDocumentSelector) {
closeMenu();
} else {
openMenu();
}
};
// 生命周期
onMounted(() => {
document.addEventListener('click', handleClickOutside);
document.addEventListener('keydown', handleGlobalKeydown);
});
onUnmounted(() => {
document.removeEventListener('click', handleClickOutside);
document.removeEventListener('keydown', handleGlobalKeydown);
});
// 监听菜单状态变化
watch(() => documentStore.showDocumentSelector, (isOpen) => {
if (isOpen) {
if (isOpen && !state.isLoaded) {
openMenu();
}
});
</script>
<template>
<div class="document-selector">
<div class="document-selector" v-click-outside="closeMenu">
<!-- 选择器按钮 -->
<button class="doc-btn" @click="documentStore.toggleDocumentSelector">
<button class="doc-btn" @click="toggleMenu">
<span class="doc-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"></path>
<polyline points="14,2 14,8 20,8"></polyline>
</svg>
</span>
<span class="doc-name">{{ currentDocName }}</span>
<span class="arrow" :class="{ open: documentStore.showDocumentSelector }"></span>
<span class="arrow" :class="{ open: state.isLoaded }"></span>
</button>
<!-- 菜单 -->
<div v-if="documentStore.showDocumentSelector" class="doc-menu">
<!-- 输入框 -->
<div class="input-box">
<input
ref="inputRef"
v-model="inputValue"
type="text"
class="main-input"
:placeholder="t('toolbar.searchOrCreateDocument')"
:maxlength="MAX_TITLE_LENGTH"
@keydown="handleInputKeydown"
/>
<svg class="input-icon" xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="11" cy="11" r="8"></circle>
<path d="m21 21-4.35-4.35"></path>
</svg>
</div>
<Transition name="slide-up">
<div v-if="state.isLoaded" class="doc-menu">
<!-- 输入框 -->
<div class="input-box">
<input
ref="inputRef"
v-model="state.searchQuery"
type="text"
class="main-input"
:placeholder="t('toolbar.searchOrCreateDocument')"
:maxlength="MAX_TITLE_LENGTH"
@keydown.enter="handleSearchEnter"
@keydown.esc="closeMenu"
/>
<svg class="input-icon" xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="11" cy="11" r="8"></circle>
<path d="m21 21-4.35-4.35"></path>
</svg>
</div>
<!-- 项目列表 -->
<div class="item-list">
<div
v-for="item in filteredItems"
:key="item.id"
class="list-item"
:class="{
'active': !item.isCreateOption && documentStore.currentDocument?.id === item.id,
'create-item': item.isCreateOption
}"
@click="selectItem(item)"
>
<!-- 创建选项 -->
<div v-if="item.isCreateOption" class="create-option">
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M5 12h14"></path>
<path d="M12 5v14"></path>
</svg>
<span>{{ item.title }}</span>
</div>
<!-- 文档项 -->
<div v-else class="doc-item-content">
<!-- 普通显示 -->
<div v-if="editingId !== item.id" class="doc-info">
<div class="doc-title">{{ item.title }}</div>
<!-- 根据状态显示错误信息或时间 -->
<div v-if="documentStore.selectorError?.docId === item.id" class="doc-error">
{{ documentStore.selectorError?.message }}
</div>
<div v-else class="doc-date">{{ formatTime(item.updatedAt) }}</div>
<!-- 项目列表 -->
<div class="item-list">
<div
v-for="item in filteredItems"
:key="item.id"
class="list-item"
:class="{
'active': !item.isCreateOption && documentStore.currentDocument?.id === item.id,
'create-item': item.isCreateOption
}"
@click="selectDocItem(item)"
>
<!-- 创建选项 -->
<div v-if="item.isCreateOption" class="create-option">
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M5 12h14"></path>
<path d="M12 5v14"></path>
</svg>
<span>{{ item.title }}</span>
</div>
<!-- 编辑状态 -->
<div v-else class="doc-edit">
<input
:ref="el => editInputRef = el as HTMLInputElement"
v-model="editingTitle"
type="text"
class="edit-input"
:maxlength="MAX_TITLE_LENGTH"
@keydown="handleEditKeydown"
@blur="saveEdit"
@click.stop
/>
</div>
<!-- 文档项 -->
<div v-else class="doc-item-content">
<!-- 普通显示 -->
<div v-if="state.editing.id !== item.id" class="doc-info">
<div class="doc-title">{{ item.title }}</div>
<!-- 根据状态显示错误信息或时间 -->
<div v-if="documentStore.selectorError?.docId === item.id" class="doc-error">
{{ documentStore.selectorError?.message }}
</div>
<div v-else class="doc-date">{{ formatDateTime(item.updated_at) }}</div>
</div>
<!-- 操作按钮 -->
<div v-if="editingId !== item.id" class="doc-actions">
<!-- 只有非当前文档才显示在新窗口打开按钮 -->
<button
v-if="documentStore.currentDocument?.id !== item.id"
class="action-btn"
@click="openInNewWindow(item, $event)"
:title="t('toolbar.openInNewWindow')"
>
<svg width="12" height="12" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"
fill="currentColor">
<path
d="M172.8 1017.6c-89.6 0-166.4-70.4-166.4-166.4V441.6c0-89.6 70.4-166.4 166.4-166.4h416c89.6 0 166.4 70.4 166.4 166.4v416c0 89.6-70.4 166.4-166.4 166.4l-416-6.4z m0-659.2c-51.2 0-89.6 38.4-89.6 89.6v416c0 51.2 38.4 89.6 89.6 89.6h416c51.2 0 89.6-38.4 89.6-89.6V441.6c0-51.2-38.4-89.6-89.6-89.6H172.8z"></path>
<path
d="M851.2 19.2H435.2C339.2 19.2 268.8 96 268.8 185.6v25.6h70.4v-25.6c0-51.2 38.4-89.6 89.6-89.6h409.6c51.2 0 89.6 38.4 89.6 89.6v409.6c0 51.2-38.4 89.6-89.6 89.6h-38.4V768h51.2c96 0 166.4-76.8 166.4-166.4V185.6c0-96-76.8-166.4-166.4-166.4z"></path>
</svg>
</button>
<button class="action-btn" @click="startRename(item, $event)" :title="t('toolbar.rename')">
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"></path>
</svg>
</button>
<button
v-if="documentStore.documentList.length > 1 && item.id !== 1"
class="action-btn delete-btn"
:class="{ 'delete-confirm': deleteConfirmId === item.id }"
@click="handleDelete(item, $event)"
:title="deleteConfirmId === item.id ? t('toolbar.confirmDelete') : t('toolbar.delete')"
>
<svg v-if="deleteConfirmId !== item.id" xmlns="http://www.w3.org/2000/svg" width="12" height="12"
viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round">
<polyline points="3,6 5,6 21,6"></polyline>
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
</svg>
<span v-else class="confirm-text">{{ t('toolbar.confirm') }}</span>
</button>
<!-- 编辑状态 -->
<div v-else class="doc-edit">
<input
:ref="el => editInputRef = el as HTMLInputElement"
v-model="state.editing.title"
type="text"
class="edit-input"
:maxlength="MAX_TITLE_LENGTH"
@keydown.enter="saveEdit"
@keydown.esc="cancelEdit"
@blur="saveEdit"
@click.stop
/>
</div>
<!-- 操作按钮 -->
<div v-if="state.editing.id !== item.id" class="doc-actions">
<!-- 只有非当前文档才显示在新窗口打开按钮 -->
<button
v-if="documentStore.currentDocument?.id !== item.id"
class="action-btn"
@click="openInNewWindow(item, $event)"
:title="t('toolbar.openInNewWindow')"
>
<svg width="12" height="12" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"
fill="currentColor">
<path
d="M172.8 1017.6c-89.6 0-166.4-70.4-166.4-166.4V441.6c0-89.6 70.4-166.4 166.4-166.4h416c89.6 0 166.4 70.4 166.4 166.4v416c0 89.6-70.4 166.4-166.4 166.4l-416-6.4z m0-659.2c-51.2 0-89.6 38.4-89.6 89.6v416c0 51.2 38.4 89.6 89.6 89.6h416c51.2 0 89.6-38.4 89.6-89.6V441.6c0-51.2-38.4-89.6-89.6-89.6H172.8z"></path>
<path
d="M851.2 19.2H435.2C339.2 19.2 268.8 96 268.8 185.6v25.6h70.4v-25.6c0-51.2 38.4-89.6 89.6-89.6h409.6c51.2 0 89.6 38.4 89.6 89.6v409.6c0 51.2-38.4 89.6-89.6 89.6h-38.4V768h51.2c96 0 166.4-76.8 166.4-166.4V185.6c0-96-76.8-166.4-166.4-166.4z"></path>
</svg>
</button>
<button class="action-btn" @click="renameDoc(item, $event)" :title="t('toolbar.rename')">
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"></path>
</svg>
</button>
<button
v-if="documentStore.documentList.length > 1 && item.id !== 1"
class="action-btn delete-btn"
:class="{ 'delete-confirm': isDeleting(item.id!) }"
@click="handleDelete(item, $event)"
:title="isDeleting(item.id!) ? t('toolbar.confirmDelete') : t('toolbar.delete')"
>
<svg v-if="!isDeleting(item.id!)" xmlns="http://www.w3.org/2000/svg" width="12" height="12"
viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round">
<polyline points="3,6 5,6 21,6"></polyline>
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
</svg>
<span v-else class="confirm-text">{{ t('toolbar.confirm') }}</span>
</button>
</div>
</div>
</div>
</div>
<!-- 空状态 -->
<div v-if="filteredItems.length === 0" class="empty">
{{ t('toolbar.noDocumentFound') }}
</div>
<!-- 加载状态 -->
<div v-if="documentStore.isLoading" class="loading">
{{ t('toolbar.loading') }}
<!-- 空状态 -->
<div v-if="filteredItems.length === 0" class="empty">
{{ t('toolbar.noDocumentFound') }}
</div>
</div>
</div>
</div>
</Transition>
</div>
</template>
<style scoped lang="scss">
.slide-up-enter-active,
.slide-up-leave-active {
transition: opacity 0.15s ease, transform 0.15s ease;
}
.slide-up-enter-from,
.slide-up-leave-to {
opacity: 0;
transform: translateY(8px);
}
.document-selector {
position: relative;
@@ -483,8 +444,8 @@ watch(() => documentStore.showDocumentSelector, (isOpen) => {
border: 1px solid var(--border-color);
border-radius: 3px;
margin-bottom: 4px;
width: 260px;
max-height: calc(100vh - 40px); // 限制最大高度留出titlebar空间(32px)和一些边距
width: 300px;
max-height: calc(100vh - 40px);
z-index: 1000;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
overflow: hidden;
@@ -527,7 +488,7 @@ watch(() => documentStore.showDocumentSelector, (isOpen) => {
}
.item-list {
max-height: calc(100vh - 100px); // 为输入框和边距预留空间
max-height: calc(100vh - 100px);
overflow-y: auto;
flex: 1;
@@ -594,7 +555,7 @@ watch(() => documentStore.showDocumentSelector, (isOpen) => {
color: var(--text-muted);
opacity: 0.6;
}
.doc-error {
font-size: 10px;
color: var(--text-danger);
@@ -669,7 +630,7 @@ watch(() => documentStore.showDocumentSelector, (isOpen) => {
}
}
.empty, .loading {
.empty {
padding: 16px 8px;
text-align: center;
font-size: 11px;
@@ -680,9 +641,17 @@ watch(() => documentStore.showDocumentSelector, (isOpen) => {
}
@keyframes fadeInOut {
0% { opacity: 0; }
10% { opacity: 1; }
90% { opacity: 1; }
100% { opacity: 0; }
0% {
opacity: 0;
}
10% {
opacity: 1;
}
90% {
opacity: 1;
}
100% {
opacity: 0;
}
}
</style>
</style>

View File

@@ -0,0 +1,5 @@
export { useConfirm } from './useConfirm';
export type { UseConfirmOptions } from './useConfirm';
export { usePolling } from './usePolling';
export type { UsePollingOptions, UsePollingReturn } from './usePolling';

View File

@@ -0,0 +1,174 @@
import { ref, readonly, onUnmounted, type Ref, type DeepReadonly } from 'vue';
export interface UseConfirmOptions<T extends string | number = string | number> {
/** Auto cancel timeout in ms (default: 3000, set 0 to disable) */
timeout?: number;
/** Callback when confirmed */
onConfirm?: (id: T) => void | Promise<void>;
/** Callback when cancelled (timeout or manual) */
onCancel?: (id: T) => void;
}
export interface UseConfirmReturn<T extends string | number = string | number> {
/** Current confirming id (readonly) */
confirmId: DeepReadonly<Ref<T | null>>;
/** Whether confirm action is executing */
isPending: DeepReadonly<Ref<boolean>>;
/** Check if a specific id is in confirming state */
isConfirming: (id: T) => boolean;
/** Start confirming state (with auto timeout) */
startConfirm: (id: T) => void;
/** Request confirmation (toggle between request and execute) */
requestConfirm: (id: T) => Promise<boolean>;
/** Manually confirm current id */
confirm: () => Promise<void>;
/** Cancel confirmation */
cancel: () => void;
/** Reset without triggering callbacks */
reset: () => void;
}
/**
* Composable for handling confirm actions (e.g., delete confirmation)
*
* @example
* ```ts
* // Basic usage
* const { isConfirming, requestConfirm } = useConfirm({
* timeout: 3000,
* onConfirm: async (id) => { await deleteItem(id) }
* })
*
* // In template
* <button @click="requestConfirm('delete')">
* {{ isConfirming('delete') ? 'Confirm?' : 'Delete' }}
* </button>
*
* // With loading state
* const { isPending, requestConfirm } = useConfirm({ ... })
* <button :disabled="isPending" @click="requestConfirm('id')">
* {{ isPending ? 'Processing...' : 'Delete' }}
* </button>
* ```
*/
export function useConfirm<T extends string | number = string | number>(
options: UseConfirmOptions<T> = {}
): UseConfirmReturn<T> {
const { timeout = 3000, onConfirm, onCancel } = options;
const confirmId = ref<T | null>(null) as Ref<T | null>;
const isPending = ref(false);
let timeoutId: ReturnType<typeof setTimeout> | null = null;
const clearTimer = (): void => {
if (timeoutId) {
clearTimeout(timeoutId);
timeoutId = null;
}
};
/**
* Check if a specific id is in confirming state
*/
const isConfirming = (id: T): boolean => {
return confirmId.value === id;
};
/**
* Start confirming state for an id (with auto timeout)
*/
const startConfirm = (id: T): void => {
clearTimer();
confirmId.value = id;
// Auto cancel after timeout (0 = disabled)
if (timeout > 0) {
timeoutId = setTimeout(() => {
if (confirmId.value === id) {
confirmId.value = null;
onCancel?.(id);
}
}, timeout);
}
};
/**
* Request confirmation for an id
* - First click: enter confirming state
* - Second click: execute confirm action
* @returns true if confirmed, false if entered confirming state
*/
const requestConfirm = async (id: T): Promise<boolean> => {
// Prevent action while pending
if (isPending.value) return false;
if (confirmId.value === id) {
// Already confirming, execute action
clearTimer();
isPending.value = true;
try {
await onConfirm?.(id);
return true;
} finally {
confirmId.value = null;
isPending.value = false;
}
} else {
// Enter confirming state
startConfirm(id);
return false;
}
};
/**
* Manually confirm the current id
*/
const confirm = async (): Promise<void> => {
if (confirmId.value === null || isPending.value) return;
clearTimer();
const id = confirmId.value;
isPending.value = true;
try {
await onConfirm?.(id);
} finally {
confirmId.value = null;
isPending.value = false;
}
};
/**
* Cancel the confirming state
*/
const cancel = (): void => {
if (confirmId.value === null) return;
const id = confirmId.value;
clearTimer();
confirmId.value = null;
onCancel?.(id);
};
/**
* Reset state without triggering callbacks
*/
const reset = (): void => {
clearTimer();
confirmId.value = null;
isPending.value = false;
};
// Cleanup on unmount
onUnmounted(clearTimer);
return {
confirmId: readonly(confirmId),
isPending: readonly(isPending),
isConfirming,
startConfirm,
requestConfirm,
confirm,
cancel,
reset
};
}

View File

@@ -0,0 +1,147 @@
import { ref, readonly, onUnmounted, type Ref, type DeepReadonly } from 'vue';
export interface UsePollingOptions<T> {
/** Polling interval in ms (default: 500) */
interval?: number;
/** Execute immediately when started (default: true) */
immediate?: boolean;
/** Auto-stop condition, return true to stop polling */
shouldStop?: (data: T) => boolean;
/** Callback on each successful poll */
onSuccess?: (data: T) => void;
/** Callback when error occurs */
onError?: (error: unknown) => void;
/** Callback when polling stops (either manual or auto) */
onStop?: () => void;
}
export interface UsePollingReturn<T> {
/** Latest fetched data (readonly) */
data: DeepReadonly<Ref<T | null>>;
/** Error message if any (readonly) */
error: DeepReadonly<Ref<string>>;
/** Whether polling is active (readonly) */
isActive: DeepReadonly<Ref<boolean>>;
/** Start polling */
start: () => void;
/** Stop polling */
stop: () => void;
/** Reset all state (also stops polling) */
reset: () => void;
}
/**
* Composable for polling async operations with auto-stop support
*
* @example
* ```ts
* // Basic usage
* const { data, isActive, start, stop } = usePolling(
* () => api.getProgress(),
* {
* interval: 200,
* shouldStop: (d) => d.progress >= 100 || !!d.error,
* onSuccess: (d) => console.log('Progress:', d.progress)
* }
* )
*
* // Start polling
* start()
*
* // With reactive data binding
* <div>{{ isActive ? `${data?.progress}%` : 'Idle' }}</div>
* ```
*/
export function usePolling<T>(
fetcher: () => Promise<T>,
options: UsePollingOptions<T> = {}
): UsePollingReturn<T> {
const {
interval = 500,
immediate = true,
shouldStop,
onSuccess,
onError,
onStop
} = options;
const data = ref<T | null>(null) as Ref<T | null>;
const error = ref('');
const isActive = ref(false);
let timerId = 0;
const clearTimer = (): void => {
if (timerId) {
clearInterval(timerId);
timerId = 0;
}
};
const poll = async (): Promise<void> => {
try {
const result = await fetcher();
data.value = result;
error.value = '';
onSuccess?.(result);
// Check auto-stop condition
if (shouldStop?.(result)) {
stop();
}
} catch (e) {
error.value = e instanceof Error ? e.message : String(e);
onError?.(e);
stop();
}
};
/**
* Start polling
*/
const start = (): void => {
if (isActive.value) return;
isActive.value = true;
error.value = '';
// Execute immediately if configured
if (immediate) {
poll();
}
timerId = window.setInterval(poll, interval);
};
/**
* Stop polling
*/
const stop = (): void => {
if (!isActive.value) return;
clearTimer();
isActive.value = false;
onStop?.();
};
/**
* Reset all state to initial values
*/
const reset = (): void => {
clearTimer();
data.value = null;
error.value = '';
isActive.value = false;
};
// Cleanup on unmount
onUnmounted(clearTimer);
return {
data: readonly(data),
error: readonly(error),
isActive: readonly(isActive),
start,
stop,
reset
};
}

View File

@@ -0,0 +1,37 @@
import type { Directive, DirectiveBinding } from 'vue';
type ClickOutsideHandler = (event: MouseEvent) => void;
interface ClickOutsideElement extends HTMLElement {
_clickOutsideHandler?: (event: MouseEvent) => void;
}
/**
* v-click-outside directive
* Triggers a callback when clicking outside the element
*
* Usage:
* <div v-click-outside="handleClickOutside">...</div>
*/
export const clickOutside: Directive<ClickOutsideElement, ClickOutsideHandler> = {
mounted(el: ClickOutsideElement, binding: DirectiveBinding<ClickOutsideHandler>) {
const handler = (event: MouseEvent) => {
const target = event.target as Node;
if (el && !el.contains(target)) {
binding.value(event);
}
};
el._clickOutsideHandler = handler;
document.addEventListener('click', handler);
},
unmounted(el: ClickOutsideElement) {
if (el._clickOutsideHandler) {
document.removeEventListener('click', el._clickOutsideHandler);
delete el._clickOutsideHandler;
}
}
};
export default clickOutside;

View File

@@ -0,0 +1,13 @@
import type { App } from 'vue';
import { clickOutside } from './clickOutside';
export { clickOutside };
/**
* Register all custom directives
*/
export function registerDirectives(app: App) {
app.directive('click-outside', clickOutside);
}
export default registerDirectives;

View File

@@ -33,13 +33,6 @@ export default {
confirmDelete: 'Click again to confirm delete',
openInNewWindow: 'Open in New Window',
alreadyOpenInNewWindow: 'Already open in another window',
documentNameTooLong: 'Document name cannot exceed {max} characters',
documentNameRequired: 'Document name cannot be empty',
cannotDeleteLastDocument: 'Cannot delete the last document',
cannotDeleteDefaultDocument: 'Cannot delete the default document',
unknownTime: 'Unknown time',
invalidDate: 'Invalid date',
timeError: 'Time error',
},
languages: {
'zh-CN': 'Chinese',
@@ -297,6 +290,10 @@ export default {
highlightTrailingWhitespace: {
name: 'Highlight Trailing Whitespace',
description: 'Highlight trailing whitespace at the end of lines'
},
httpClient: {
name: 'HTTP Client',
description: 'Send HTTP requests directly in the editor and view responses'
}
},
monitor: {

View File

@@ -33,13 +33,6 @@ export default {
confirmDelete: '再次点击确认删除',
openInNewWindow: '在新窗口中打开',
alreadyOpenInNewWindow: '已在新窗口中打开',
documentNameTooLong: '文档名称不能超过{max}个字符',
documentNameRequired: '文档名称不能为空',
cannotDeleteLastDocument: '无法删除最后一个文档',
cannotDeleteDefaultDocument: '无法删除默认文档',
unknownTime: '未知时间',
invalidDate: '无效日期',
timeError: '时间错误',
},
languages: {
'zh-CN': '简体中文',
@@ -299,6 +292,10 @@ export default {
highlightTrailingWhitespace: {
name: '高亮行尾空白',
description: '高亮显示行尾的多余空白字符'
},
httpClient: {
name: 'HTTP 客户端',
description: '在编辑器中直接发送 HTTP 请求并查看响应'
}
},
monitor: {

View File

@@ -1,15 +1,18 @@
import {createApp} from 'vue';
import { createApp } from 'vue';
import App from './App.vue';
import '@/assets/styles/index.css';
import {createPinia} from 'pinia';
import { createPinia } from 'pinia';
import i18n from './i18n';
import router from './router';
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate';
import { registerDirectives } from './directives';
const pinia = createPinia();
pinia.use(piniaPluginPersistedstate);
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
const app = createApp(App);
app.use(pinia)
app.use(pinia);
app.use(i18n);
app.use(router);
registerDirectives(app);
app.mount('#app');

View File

@@ -161,7 +161,7 @@ export const useConfigStore = defineStore('config', () => {
// 初始化语言设置
const initializeLanguage = async (): Promise<void> => {
const initLanguage = async (): Promise<void> => {
try {
// 如果配置未加载,先加载配置
if (!state.configLoaded) {
@@ -210,7 +210,7 @@ export const useConfigStore = defineStore('config', () => {
// 语言相关方法
setLanguage,
initializeLanguage,
initLanguage,
// 主题相关方法
setSystemTheme,

View File

@@ -2,12 +2,11 @@ import {defineStore} from 'pinia';
import {computed, ref} from 'vue';
import {DocumentService} from '@/../bindings/voidraft/internal/services';
import {OpenDocumentWindow} from '@/../bindings/voidraft/internal/services/windowservice';
import {Document} from '@/../bindings/voidraft/internal/models/models';
import {Document} from '@/../bindings/voidraft/internal/models/ent/models';
import {useTabStore} from "@/stores/tabStore";
import type {EditorViewState} from '@/stores/editorStore';
export const useDocumentStore = defineStore('document', () => {
const DEFAULT_DOCUMENT_ID = ref<number>(1); // 默认草稿文档ID
// === 核心状态 ===
const documents = ref<Record<number, Document>>({});
@@ -15,7 +14,6 @@ export const useDocumentStore = defineStore('document', () => {
const currentDocument = ref<Document | null>(null);
// === 编辑器状态持久化 ===
// 修复:使用统一的 EditorViewState 类型定义
const documentStates = ref<Record<number, EditorViewState>>({});
// === UI状态 ===
@@ -26,15 +24,18 @@ export const useDocumentStore = defineStore('document', () => {
// === 计算属性 ===
const documentList = computed(() =>
Object.values(documents.value).sort((a, b) => {
return new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime();
const timeA = a.updated_at ? new Date(a.updated_at).getTime() : 0;
const timeB = b.updated_at ? new Date(b.updated_at).getTime() : 0;
return timeB - timeA;
})
);
// === 私有方法 ===
const setDocuments = (docs: Document[]) => {
documents.value = {};
docs.forEach(doc => {
documents.value[doc.id] = doc;
if (doc.id !== undefined) {
documents.value[doc.id] = doc;
}
});
};
@@ -64,15 +65,7 @@ export const useDocumentStore = defineStore('document', () => {
clearError();
};
const toggleDocumentSelector = () => {
if (showDocumentSelector.value) {
closeDocumentSelector();
} else {
openDocumentSelector();
}
};
// === 文档操作方法 ===
// 在新窗口中打开文档
const openDocumentInNewWindow = async (docId: number): Promise<boolean> => {
@@ -94,7 +87,7 @@ export const useDocumentStore = defineStore('document', () => {
const createNewDocument = async (title: string): Promise<Document | null> => {
try {
const doc = await DocumentService.CreateDocument(title);
if (doc) {
if (doc && doc.id !== undefined) {
documents.value[doc.id] = doc;
return doc;
}
@@ -123,8 +116,6 @@ export const useDocumentStore = defineStore('document', () => {
// 打开文档
const openDocument = async (docId: number): Promise<boolean> => {
try {
closeDocumentSelector();
// 获取完整文档数据
const doc = await DocumentService.GetDocumentByID(docId);
if (!doc) {
@@ -150,12 +141,12 @@ export const useDocumentStore = defineStore('document', () => {
const doc = documents.value[docId];
if (doc) {
doc.title = title;
doc.updatedAt = new Date().toISOString();
doc.updated_at = new Date().toISOString();
}
if (currentDocument.value?.id === docId) {
currentDocument.value.title = title;
currentDocument.value.updatedAt = new Date().toISOString();
currentDocument.value.updated_at = new Date().toISOString();
}
// 同步更新标签页标题
@@ -172,11 +163,6 @@ export const useDocumentStore = defineStore('document', () => {
// 删除文档
const deleteDocument = async (docId: number): Promise<boolean> => {
try {
// 检查是否是默认文档使用ID判断
if (docId === DEFAULT_DOCUMENT_ID.value) {
return false;
}
await DocumentService.DeleteDocument(docId);
// 更新本地状态
@@ -191,7 +177,7 @@ export const useDocumentStore = defineStore('document', () => {
// 如果删除的是当前文档,切换到第一个可用文档
if (currentDocumentId.value === docId) {
const availableDocs = Object.values(documents.value);
if (availableDocs.length > 0) {
if (availableDocs.length > 0 && availableDocs[0].id !== undefined) {
await openDocument(availableDocs[0].id);
} else {
currentDocumentId.value = null;
@@ -218,8 +204,10 @@ export const useDocumentStore = defineStore('document', () => {
// 如果URL中没有指定文档ID则使用持久化的文档ID
await openDocument(currentDocumentId.value);
} else {
// 否则打开默认文档
await openDocument(DEFAULT_DOCUMENT_ID.value);
// 否则打开第一个文档
if (documents.value[0].id) {
await openDocument(documents.value[0].id);
}
}
} catch (error) {
console.error('Failed to initialize document store:', error);
@@ -227,7 +215,6 @@ export const useDocumentStore = defineStore('document', () => {
};
return {
DEFAULT_DOCUMENT_ID,
// 状态
documents,
documentList,
@@ -247,7 +234,6 @@ export const useDocumentStore = defineStore('document', () => {
deleteDocument,
openDocumentSelector,
closeDocumentSelector,
toggleDocumentSelector,
setError,
clearError,
initialize,

View File

@@ -4,7 +4,6 @@ import {EditorView} from '@codemirror/view';
import {EditorState, Extension} from '@codemirror/state';
import {useConfigStore} from './configStore';
import {useDocumentStore} from './documentStore';
import {ExtensionID} from '@/../bindings/voidraft/internal/models/models';
import {DocumentService, ExtensionService} from '@/../bindings/voidraft/internal/services';
import {ensureSyntaxTree} from "@codemirror/language";
import {createBasicSetup} from '@/views/editor/basic/basicSetup';
@@ -28,7 +27,6 @@ import {AsyncManager} from '@/common/utils/asyncManager';
import {generateContentHash} from "@/common/utils/hashUtils";
import {createTimerManager, type TimerManager} from '@/common/utils/timerUtils';
import {EDITOR_CONFIG} from '@/common/constant/editor';
import {createHttpClientExtension} from "@/views/editor/extensions/httpclient";
import {createDebounce} from '@/common/utils/debounce';
export interface DocumentStats {
@@ -93,84 +91,6 @@ export const useEditorStore = defineStore('editor', () => {
}
}, { delay: 500 }); // 500ms 内的多次输入只清理一次
// === 私有方法 ===
/**
* 检查位置是否在代码块分隔符区域内
*/
const isPositionInDelimiter = (view: EditorView, pos: number): boolean => {
try {
const blocks = view.state.field(blockState, false);
if (!blocks) return false;
for (const block of blocks) {
if (pos >= block.delimiter.from && pos < block.delimiter.to) {
return true;
}
}
return false;
} catch {
return false;
}
};
/**
* 调整光标位置到有效的内容区域
* 如果位置在分隔符内,移动到该块的内容开始位置
*/
const adjustCursorPosition = (view: EditorView, pos: number): number => {
try {
const blocks = view.state.field(blockState, false);
if (!blocks || blocks.length === 0) return pos;
// 如果位置在分隔符内,移动到该块的内容开始位置
for (const block of blocks) {
if (pos >= block.delimiter.from && pos < block.delimiter.to) {
return block.content.from;
}
}
return pos;
} catch {
return pos;
}
};
/**
* 恢复编辑器的光标位置(自动滚动到光标处)
*/
const restoreEditorState = (instance: EditorInstance, documentId: number): void => {
const savedState = instance.editorState;
if (savedState) {
// 有保存的状态,恢复光标位置
let pos = Math.min(savedState.cursorPos, instance.view.state.doc.length);
// 确保位置不在分隔符上
if (isPositionInDelimiter(instance.view, pos)) {
pos = adjustCursorPosition(instance.view, pos);
}
// 修复:设置光标位置并居中滚动(更好的用户体验)
instance.view.dispatch({
selection: {anchor: pos, head: pos},
effects: EditorView.scrollIntoView(pos, {
y: "center", // 垂直居中显示
yMargin: 100 // 上下留一些边距
})
});
} else {
// 首次打开或没有记录,光标在文档末尾
const docLength = instance.view.state.doc.length;
instance.view.dispatch({
selection: {anchor: docLength, head: docLength},
effects: EditorView.scrollIntoView(docLength, {
y: "center",
yMargin: 100
})
});
}
};
// 缓存化的语法树确保方法
const ensureSyntaxTreeCached = (view: EditorView, documentId: number): void => {
@@ -245,7 +165,7 @@ export const useEditorStore = defineStore('editor', () => {
increaseFontSize: () => configStore.increaseFontSizeLocal(),
decreaseFontSize: () => configStore.decreaseFontSizeLocal(),
onSave: () => configStore.saveFontSize(),
saveDelay: 500
saveDelay: 1000
});
// 统计扩展
@@ -260,9 +180,6 @@ export const useEditorStore = defineStore('editor', () => {
enableAutoDetection: true
});
const httpExtension = createHttpClientExtension();
// 再次检查操作有效性
if (!operationManager.isOperationValid(operationId, documentId)) {
throw new Error('Operation cancelled');
@@ -277,7 +194,7 @@ export const useEditorStore = defineStore('editor', () => {
}
// 动态扩展传递文档ID以便扩展管理器可以预初始化
const dynamicExtensions = await createDynamicExtensions(documentId);
const dynamicExtensions = await createDynamicExtensions();
// 最终检查操作有效性
if (!operationManager.isOperationValid(operationId, documentId)) {
@@ -296,7 +213,6 @@ export const useEditorStore = defineStore('editor', () => {
contentChangeExtension,
codeBlockExtension,
...dynamicExtensions,
...httpExtension,
];
// 创建编辑器状态
@@ -320,7 +236,6 @@ export const useEditorStore = defineStore('editor', () => {
lastModified: new Date(),
autoSaveTimer: createTimerManager(),
syntaxTreeCache: null,
// 修复:创建实例时从 documentStore 读取持久化的编辑器状态
editorState: documentStore.documentStates[documentId]
};
@@ -401,9 +316,6 @@ export const useEditorStore = defineStore('editor', () => {
//使用 nextTick + requestAnimationFrame 确保 DOM 完全渲染
nextTick(() => {
requestAnimationFrame(() => {
// 恢复编辑器状态(光标位置和滚动位置)
restoreEditorState(instance, documentId);
// 聚焦编辑器
instance.view.focus();
@@ -433,7 +345,6 @@ export const useEditorStore = defineStore('editor', () => {
instance.isDirty = false;
instance.lastModified = new Date();
}
// 如果内容在保存期间被修改了,保持 isDirty 状态
return true;
} catch (error) {
@@ -462,15 +373,14 @@ export const useEditorStore = defineStore('editor', () => {
}, getAutoSaveDelay());
};
// === 公共API ===
// 设置编辑器容器
const setEditorContainer = (container: HTMLElement | null) => {
containerElement.value = container;
// 如果设置容器时已有当前文档,立即加载编辑器
if (container && documentStore.currentDocument) {
loadEditor(documentStore.currentDocument.id, documentStore.currentDocument.content);
if (container && documentStore.currentDocument && documentStore.currentDocument.id !== undefined) {
loadEditor(documentStore.currentDocument.id, documentStore.currentDocument.content || '');
}
};
@@ -696,20 +606,20 @@ export const useEditorStore = defineStore('editor', () => {
};
// 更新扩展
const updateExtension = async (id: ExtensionID, enabled: boolean, config?: any) => {
// 如果只是更新启用状态
if (config === undefined) {
await ExtensionService.UpdateExtensionEnabled(id, enabled);
} else {
// 如果需要更新配置
await ExtensionService.UpdateExtensionState(id, enabled, config);
const updateExtension = async (key: string, enabled: boolean, config?: any) => {
// 更新启用状态
await ExtensionService.UpdateExtensionEnabled(key, enabled);
// 如果需要更新配置
if (config !== undefined) {
await ExtensionService.UpdateExtensionConfig(key, config);
}
// 更新前端编辑器扩展 - 应用于所有实例
const manager = getExtensionManager();
if (manager) {
// 直接更新前端扩展至所有视图
manager.updateExtension(id, enabled, config);
manager.updateExtension(key, enabled, config);
}
// 重新加载扩展配置
@@ -723,23 +633,10 @@ export const useEditorStore = defineStore('editor', () => {
// 监听文档切换
watch(() => documentStore.currentDocument, async (newDoc, oldDoc) => {
if (newDoc && containerElement.value) {
// 修复:在切换到新文档前,只保存旧文档的光标位置
if (oldDoc && oldDoc.id !== newDoc.id && currentEditor.value) {
const oldInstance = editorCache.get(oldDoc.id);
if (oldInstance) {
const currentState: EditorViewState = {
cursorPos: currentEditor.value.state.selection.main.head
};
// 同时保存到实例和 documentStore
oldInstance.editorState = currentState;
documentStore.documentStates[oldDoc.id] = currentState;
}
}
if (newDoc && newDoc.id !== undefined && containerElement.value) {
// 等待 DOM 更新完成,再加载新文档的编辑器
await nextTick();
loadEditor(newDoc.id, newDoc.content);
loadEditor(newDoc.id, newDoc.content || '');
}
});

View File

@@ -1,6 +1,6 @@
import { defineStore } from 'pinia';
import { computed, ref } from 'vue';
import { Extension, ExtensionID } from '@/../bindings/voidraft/internal/models/models';
import { Extension } from '@/../bindings/voidraft/internal/models/ent/models';
import { ExtensionService } from '@/../bindings/voidraft/internal/services';
export const useExtensionStore = defineStore('extension', () => {
@@ -12,9 +12,9 @@ export const useExtensionStore = defineStore('extension', () => {
extensions.value.filter(ext => ext.enabled)
);
// 获取启用的扩展ID列表
// 获取启用的扩展ID列表 (key)
const enabledExtensionIds = computed(() =>
enabledExtensions.value.map(ext => ext.id)
enabledExtensions.value.map(ext => ext.key).filter((k): k is string => k !== undefined)
);
/**
@@ -22,7 +22,8 @@ export const useExtensionStore = defineStore('extension', () => {
*/
const loadExtensions = async (): Promise<void> => {
try {
extensions.value = await ExtensionService.GetAllExtensions();
const result = await ExtensionService.GetAllExtensions();
extensions.value = result.filter((ext): ext is Extension => ext !== null);
} catch (err) {
console.error('[ExtensionStore] Failed to load extensions:', err);
}
@@ -31,8 +32,8 @@ export const useExtensionStore = defineStore('extension', () => {
/**
* 获取扩展配置
*/
const getExtensionConfig = (id: ExtensionID): any => {
const extension = extensions.value.find(ext => ext.id === id);
const getExtensionConfig = (key: string): any => {
const extension = extensions.value.find(ext => ext.key === key);
return extension?.config ?? {};
};

View File

@@ -1,6 +1,6 @@
import {defineStore} from 'pinia';
import {computed, ref} from 'vue';
import {ExtensionID, KeyBinding, KeyBindingCommand} from '@/../bindings/voidraft/internal/models/models';
import {KeyBinding} from '@/../bindings/voidraft/internal/models/ent/models';
import {GetAllKeyBindings} from '@/../bindings/voidraft/internal/services/keybindingservice';
export const useKeybindingStore = defineStore('keybinding', () => {
@@ -14,13 +14,14 @@ export const useKeybindingStore = defineStore('keybinding', () => {
// 按扩展分组的快捷键
const keyBindingsByExtension = computed(() => {
const groups = new Map<ExtensionID, KeyBinding[]>();
const groups = new Map<string, KeyBinding[]>();
for (const binding of keyBindings.value) {
if (!groups.has(binding.extension)) {
groups.set(binding.extension, []);
const ext = binding.extension || '';
if (!groups.has(ext)) {
groups.set(ext, []);
}
groups.get(binding.extension)!.push(binding);
groups.get(ext)!.push(binding);
}
return groups;
@@ -28,13 +29,13 @@ export const useKeybindingStore = defineStore('keybinding', () => {
// 获取指定扩展的快捷键
const getKeyBindingsByExtension = computed(() =>
(extension: ExtensionID) =>
(extension: string) =>
keyBindings.value.filter(kb => kb.extension === extension)
);
// 按命令获取快捷键
const getKeyBindingByCommand = computed(() =>
(command: KeyBindingCommand) =>
(command: string) =>
keyBindings.value.find(kb => kb.command === command)
);
@@ -42,13 +43,14 @@ export const useKeybindingStore = defineStore('keybinding', () => {
* 从后端加载快捷键配置
*/
const loadKeyBindings = async (): Promise<void> => {
keyBindings.value = await GetAllKeyBindings();
const result = await GetAllKeyBindings();
keyBindings.value = result.filter((kb): kb is KeyBinding => kb !== null);
};
/**
* 检查是否存在指定命令的快捷键
*/
const hasCommand = (command: KeyBindingCommand): boolean => {
const hasCommand = (command: string): boolean => {
return keyBindings.value.some(kb => kb.command === command && kb.enabled);
};
@@ -57,9 +59,11 @@ export const useKeybindingStore = defineStore('keybinding', () => {
* 获取扩展相关的所有扩展ID
*/
const getAllExtensionIds = computed(() => {
const extensionIds = new Set<ExtensionID>();
const extensionIds = new Set<string>();
for (const binding of keyBindings.value) {
extensionIds.add(binding.extension);
if (binding.extension) {
extensionIds.add(binding.extension);
}
}
return Array.from(extensionIds);
});

View File

@@ -38,7 +38,7 @@ export const useSystemStore = defineStore('system', () => {
});
// 初始化系统信息
const initializeSystemInfo = async (): Promise<void> => {
const initSystemInfo = async (): Promise<void> => {
if (isLoading.value) return;
isLoading.value = true;
@@ -102,7 +102,7 @@ export const useSystemStore = defineStore('system', () => {
titleBarHeight,
// 方法
initializeSystemInfo,
initSystemInfo,
setWindowOnTop,
toggleWindowOnTop,
resetWindowOnTop,

View File

@@ -2,7 +2,7 @@ import {defineStore} from 'pinia';
import {computed, readonly, ref} from 'vue';
import {useConfigStore} from './configStore';
import {useDocumentStore} from './documentStore';
import type {Document} from '@/../bindings/voidraft/internal/models/models';
import type {Document} from '@/../bindings/voidraft/internal/models/ent/models';
export interface Tab {
documentId: number; // 直接使用文档ID作为唯一标识
@@ -55,6 +55,7 @@ export const useTabStore = defineStore('tab', () => {
*/
const addOrActivateTab = (document: Document) => {
const documentId = document.id;
if (documentId === undefined) return;
if (hasTab(documentId)) {
// 标签页已存在,无需重复添加
@@ -64,7 +65,7 @@ export const useTabStore = defineStore('tab', () => {
// 创建新标签页
const newTab: Tab = {
documentId,
title: document.title
title: document.title || ''
};
tabsMap.value[documentId] = newTab;

View File

@@ -1,159 +1,161 @@
import { defineStore } from 'pinia';
import { computed, ref } from 'vue';
import { SystemThemeType, ThemeType, ThemeColorConfig } from '@/../bindings/voidraft/internal/models/models';
import { ThemeService } from '@/../bindings/voidraft/internal/services';
import { useConfigStore } from './configStore';
import { useEditorStore } from './editorStore';
import type { ThemeColors } from '@/views/editor/theme/types';
import { cloneThemeColors, FALLBACK_THEME_NAME, themePresetList, themePresetMap } from '@/views/editor/theme/presets';
import {defineStore} from 'pinia';
import {computed, ref} from 'vue';
import {SystemThemeType} from '@/../bindings/voidraft/internal/models/models';
import {Type as ThemeType} from '@/../bindings/voidraft/internal/models/ent/theme/models';
import {ThemeService} from '@/../bindings/voidraft/internal/services';
import {useConfigStore} from './configStore';
import {useEditorStore} from './editorStore';
import type {ThemeColors} from '@/views/editor/theme/types';
import {cloneThemeColors, FALLBACK_THEME_NAME, themePresetList, themePresetMap} from '@/views/editor/theme/presets';
type ThemeColorConfig = { [_: string]: any };
type ThemeOption = { name: string; type: ThemeType };
const resolveThemeName = (name?: string) =>
name && themePresetMap[name] ? name : FALLBACK_THEME_NAME;
name && themePresetMap[name] ? name : FALLBACK_THEME_NAME;
const createThemeOptions = (type: ThemeType): ThemeOption[] =>
themePresetList
.filter(preset => preset.type === type)
.map(preset => ({ name: preset.name, type: preset.type }));
themePresetList
.filter(preset => preset.type === type)
.map(preset => ({name: preset.name, type: preset.type}));
const darkThemeOptions = createThemeOptions(ThemeType.ThemeTypeDark);
const lightThemeOptions = createThemeOptions(ThemeType.ThemeTypeLight);
const darkThemeOptions = createThemeOptions(ThemeType.TypeDark);
const lightThemeOptions = createThemeOptions(ThemeType.TypeLight);
const cloneColors = (colors: ThemeColorConfig): ThemeColors =>
JSON.parse(JSON.stringify(colors)) as ThemeColors;
JSON.parse(JSON.stringify(colors)) as ThemeColors;
const getPresetColors = (name: string): ThemeColors => {
const preset = themePresetMap[name] ?? themePresetMap[FALLBACK_THEME_NAME];
const colors = cloneThemeColors(preset.colors);
colors.themeName = name;
return colors;
const preset = themePresetMap[name] ?? themePresetMap[FALLBACK_THEME_NAME];
const colors = cloneThemeColors(preset.colors);
colors.themeName = name;
return colors;
};
const fetchThemeColors = async (themeName: string): Promise<ThemeColors> => {
const safeName = resolveThemeName(themeName);
try {
const theme = await ThemeService.GetThemeByName(safeName);
if (theme?.colors) {
const colors = cloneColors(theme.colors);
colors.themeName = safeName;
return colors;
const safeName = resolveThemeName(themeName);
try {
const theme = await ThemeService.GetThemeByKey(safeName);
if (theme?.colors) {
const colors = cloneColors(theme.colors);
colors.themeName = safeName;
return colors;
}
} catch (error) {
console.error('Failed to load theme override:', error);
}
} catch (error) {
console.error('Failed to load theme override:', error);
}
return getPresetColors(safeName);
return getPresetColors(safeName);
};
export const useThemeStore = defineStore('theme', () => {
const configStore = useConfigStore();
const currentColors = ref<ThemeColors | null>(null);
const configStore = useConfigStore();
const currentColors = ref<ThemeColors | null>(null);
const currentTheme = computed(
() => configStore.config?.appearance?.systemTheme || SystemThemeType.SystemThemeAuto
);
const isDarkMode = computed(
() =>
currentTheme.value === SystemThemeType.SystemThemeDark ||
(currentTheme.value === SystemThemeType.SystemThemeAuto &&
window.matchMedia('(prefers-color-scheme: dark)').matches)
);
const availableThemes = computed<ThemeOption[]>(() =>
isDarkMode.value ? darkThemeOptions : lightThemeOptions
);
const applyThemeToDOM = (theme: SystemThemeType) => {
const themeMap = {
[SystemThemeType.SystemThemeAuto]: 'auto',
[SystemThemeType.SystemThemeDark]: 'dark',
[SystemThemeType.SystemThemeLight]: 'light',
};
document.documentElement.setAttribute('data-theme', themeMap[theme]);
};
const loadThemeColors = async (themeName?: string) => {
const targetName = resolveThemeName(
themeName || configStore.config?.appearance?.currentTheme
const currentTheme = computed(
() => configStore.config?.appearance?.systemTheme || SystemThemeType.SystemThemeAuto
);
currentColors.value = await fetchThemeColors(targetName);
};
const initializeTheme = async () => {
applyThemeToDOM(currentTheme.value);
await loadThemeColors();
};
const isDarkMode = computed(
() =>
currentTheme.value === SystemThemeType.SystemThemeDark ||
(currentTheme.value === SystemThemeType.SystemThemeAuto &&
window.matchMedia('(prefers-color-scheme: dark)').matches)
);
const setTheme = async (theme: SystemThemeType) => {
await configStore.setSystemTheme(theme);
applyThemeToDOM(theme);
refreshEditorTheme();
};
const availableThemes = computed<ThemeOption[]>(() =>
isDarkMode.value ? darkThemeOptions : lightThemeOptions
);
const switchToTheme = async (themeName: string) => {
if (!themePresetMap[themeName]) {
console.error('Theme not found:', themeName);
return false;
}
const applyThemeToDOM = (theme: SystemThemeType) => {
const themeMap = {
[SystemThemeType.SystemThemeAuto]: 'auto',
[SystemThemeType.SystemThemeDark]: 'dark',
[SystemThemeType.SystemThemeLight]: 'light',
};
document.documentElement.setAttribute('data-theme', themeMap[theme]);
};
await loadThemeColors(themeName);
await configStore.setCurrentTheme(themeName);
refreshEditorTheme();
return true;
};
const loadThemeColors = async (themeName?: string) => {
const targetName = resolveThemeName(
themeName || configStore.config?.appearance?.currentTheme
);
currentColors.value = await fetchThemeColors(targetName);
};
const updateCurrentColors = (colors: Partial<ThemeColors>) => {
if (!currentColors.value) return;
Object.assign(currentColors.value, colors);
};
const initTheme = async () => {
applyThemeToDOM(currentTheme.value);
await loadThemeColors();
};
const saveCurrentTheme = async () => {
if (!currentColors.value) {
throw new Error('No theme selected');
}
const setTheme = async (theme: SystemThemeType) => {
await configStore.setSystemTheme(theme);
applyThemeToDOM(theme);
refreshEditorTheme();
};
const themeName = resolveThemeName(currentColors.value.themeName);
currentColors.value.themeName = themeName;
const switchToTheme = async (themeName: string) => {
if (!themePresetMap[themeName]) {
console.error('Theme not found:', themeName);
return false;
}
await ThemeService.UpdateTheme(themeName, currentColors.value as unknown as ThemeColorConfig);
await loadThemeColors(themeName);
await configStore.setCurrentTheme(themeName);
refreshEditorTheme();
return true;
};
await loadThemeColors(themeName);
refreshEditorTheme();
return true;
};
const updateCurrentColors = (colors: Partial<ThemeColors>) => {
if (!currentColors.value) return;
Object.assign(currentColors.value, colors);
};
const resetCurrentTheme = async () => {
if (!currentColors.value) {
throw new Error('No theme selected');
}
const saveCurrentTheme = async () => {
if (!currentColors.value) {
throw new Error('No theme selected');
}
const themeName = resolveThemeName(currentColors.value.themeName);
await ThemeService.ResetTheme(themeName);
const themeName = resolveThemeName(currentColors.value.themeName);
currentColors.value.themeName = themeName;
await loadThemeColors(themeName);
refreshEditorTheme();
return true;
};
await ThemeService.UpdateTheme(themeName, currentColors.value as ThemeColorConfig);
const refreshEditorTheme = () => {
applyThemeToDOM(currentTheme.value);
const editorStore = useEditorStore();
editorStore?.applyThemeSettings();
};
await loadThemeColors(themeName);
refreshEditorTheme();
return true;
};
return {
availableThemes,
currentTheme,
currentColors,
isDarkMode,
setTheme,
switchToTheme,
initializeTheme,
updateCurrentColors,
saveCurrentTheme,
resetCurrentTheme,
refreshEditorTheme,
applyThemeToDOM,
};
const resetCurrentTheme = async () => {
if (!currentColors.value) {
throw new Error('No theme selected');
}
const themeName = resolveThemeName(currentColors.value.themeName);
await ThemeService.ResetTheme(themeName);
await loadThemeColors(themeName);
refreshEditorTheme();
return true;
};
const refreshEditorTheme = () => {
applyThemeToDOM(currentTheme.value);
const editorStore = useEditorStore();
editorStore?.applyThemeSettings();
};
return {
availableThemes,
currentTheme,
currentColors,
isDarkMode,
setTheme,
switchToTheme,
initTheme,
updateCurrentColors,
saveCurrentTheme,
resetCurrentTheme,
refreshEditorTheme,
applyThemeToDOM,
};
});

View File

@@ -1,7 +1,7 @@
import { EditorView } from '@codemirror/view';
import { Extension } from '@codemirror/state';
import { copyCommand, cutCommand, pasteCommand } from '../codeblock/copyPaste';
import { KeyBindingCommand } from '../../../../../bindings/voidraft/internal/models/models';
import { KeyBindingKey } from '@/../bindings/voidraft/internal/models/models';
import { useKeybindingStore } from '@/stores/keybindingStore';
import { undo, redo } from '@codemirror/commands';
import i18n from '@/i18n';
@@ -32,53 +32,54 @@ function formatKeyBinding(keyBinding: string): string {
.replace(/-/g, " + ");
}
const shortcutCache = new Map<KeyBindingCommand, string>();
const shortcutCache = new Map<KeyBindingKey, string>();
function getShortcutText(command?: KeyBindingCommand): string {
if (command === undefined) {
function getShortcutText(keyBindingKey?: KeyBindingKey): string {
if (keyBindingKey === undefined) {
return "";
}
const cached = shortcutCache.get(command);
const cached = shortcutCache.get(keyBindingKey);
if (cached !== undefined) {
return cached;
}
try {
const keybindingStore = useKeybindingStore();
// binding.key 是命令标识符binding.command 是快捷键组合
const binding = keybindingStore.keyBindings.find(
(kb) => kb.command === command && kb.enabled
(kb) => kb.key === keyBindingKey && kb.enabled
);
if (binding?.key) {
const formatted = formatKeyBinding(binding.key);
shortcutCache.set(command, formatted);
if (binding?.command) {
const formatted = formatKeyBinding(binding.command);
shortcutCache.set(keyBindingKey, formatted);
return formatted;
}
} catch (error) {
console.warn("An error occurred while getting the shortcut:", error);
}
shortcutCache.set(command, "");
shortcutCache.set(keyBindingKey, "");
return "";
}
function getBuiltinMenuNodes(): MenuSchemaNode[] {
function builtinMenuNodes(): MenuSchemaNode[] {
return [
{
id: "copy",
labelKey: "keybindings.commands.blockCopy",
command: copyCommand,
shortcutCommand: KeyBindingCommand.BlockCopyCommand,
keyBindingKey: KeyBindingKey.BlockCopyKeyBindingKey,
enabled: (context) => context.hasSelection
},
{
id: "cut",
labelKey: "keybindings.commands.blockCut",
command: cutCommand,
shortcutCommand: KeyBindingCommand.BlockCutCommand,
keyBindingKey: KeyBindingKey.BlockCutKeyBindingKey,
visible: (context) => context.isEditable,
enabled: (context) => context.hasSelection && context.isEditable
},
@@ -86,21 +87,21 @@ function getBuiltinMenuNodes(): MenuSchemaNode[] {
id: "paste",
labelKey: "keybindings.commands.blockPaste",
command: pasteCommand,
shortcutCommand: KeyBindingCommand.BlockPasteCommand,
keyBindingKey: KeyBindingKey.BlockPasteKeyBindingKey,
visible: (context) => context.isEditable
},
{
id: "undo",
labelKey: "keybindings.commands.historyUndo",
command: undo,
shortcutCommand: KeyBindingCommand.HistoryUndoCommand,
keyBindingKey: KeyBindingKey.HistoryUndoKeyBindingKey,
visible: (context) => context.isEditable
},
{
id: "redo",
labelKey: "keybindings.commands.historyRedo",
command: redo,
shortcutCommand: KeyBindingCommand.HistoryRedoCommand,
keyBindingKey: KeyBindingKey.HistoryRedoKeyBindingKey,
visible: (context) => context.isEditable
}
];
@@ -110,7 +111,7 @@ let builtinMenuRegistered = false;
function ensureBuiltinMenuRegistered(): void {
if (builtinMenuRegistered) return;
registerMenuNodes(getBuiltinMenuNodes());
registerMenuNodes(builtinMenuNodes());
builtinMenuRegistered = true;
}

View File

@@ -1,6 +1,6 @@
import type { EditorView } from '@codemirror/view';
import { EditorState } from '@codemirror/state';
import type { KeyBindingCommand } from '../../../../../bindings/voidraft/internal/models/models';
import { KeyBindingKey } from '@/../bindings/voidraft/internal/models/models';
export interface MenuContext {
view: EditorView;
@@ -16,7 +16,7 @@ export type MenuSchemaNode =
type?: "action";
labelKey: string;
command?: (view: EditorView) => boolean;
shortcutCommand?: KeyBindingCommand;
keyBindingKey?: KeyBindingKey;
visible?: (context: MenuContext) => boolean;
enabled?: (context: MenuContext) => boolean;
}
@@ -37,7 +37,7 @@ export interface RenderMenuItem {
interface MenuBuildOptions {
translate: (key: string) => string;
formatShortcut: (command?: KeyBindingCommand) => string;
formatShortcut: (keyBindingKey?: KeyBindingKey) => string;
}
const menuRegistry: MenuSchemaNode[] = [];
@@ -89,7 +89,7 @@ function convertNode(
}
const disabled = node.enabled ? !node.enabled(context) : false;
const shortcut = options.formatShortcut(node.shortcutCommand);
const shortcut = options.formatShortcut(node.keyBindingKey);
return {
id: node.id,

View File

@@ -1,7 +1,11 @@
import { Extension, StateField } from '@codemirror/state';
import { EditorView, showTooltip, Tooltip } from '@codemirror/view';
import { translatorManager } from './manager';
import { TRANSLATION_ICON_SVG } from '@/common/constant/translation';
const TRANSLATION_ICON_SVG = `
<svg class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="24" height="24">
<path d="M599.68 485.056h-8l30.592 164.672c20.352-7.04 38.72-17.344 54.912-31.104a271.36 271.36 0 0 1-40.704-64.64l32.256-4.032c8.896 17.664 19.072 33.28 30.592 46.72 23.872-27.968 42.24-65.152 55.04-111.744l-154.688 0.128z m121.92 133.76c18.368 15.36 39.36 26.56 62.848 33.472l14.784 4.416-8.64 30.336-14.72-4.352a205.696 205.696 0 0 1-76.48-41.728c-20.672 17.92-44.928 31.552-71.232 40.064l20.736 110.912H519.424l-9.984 72.512h385.152c18.112 0 32.704-14.144 32.704-31.616V295.424a32.128 32.128 0 0 0-32.704-31.552H550.528l35.2 189.696h79.424v-31.552h61.44v31.552h102.4v31.616h-42.688c-14.272 55.488-35.712 100.096-64.64 133.568zM479.36 791.68H193.472c-36.224 0-65.472-28.288-65.472-63.168V191.168C128 156.16 157.312 128 193.472 128h327.68l20.544 104.32h352.832c36.224 0 65.472 28.224 65.472 63.104v537.408c0 34.944-29.312 63.168-65.472 63.168H468.608l10.688-104.32zM337.472 548.352v-33.28H272.768v-48.896h60.16V433.28h-60.16v-41.728h64.704v-32.896h-102.4v189.632h102.4z m158.272 0V453.76c0-17.216-4.032-30.272-12.16-39.488-8.192-9.152-20.288-13.696-36.032-13.696a55.04 55.04 0 0 0-24.768 5.376 39.04 39.04 0 0 0-17.088 15.936h-1.984l-5.056-18.56h-28.352V548.48h37.12V480c0-17.088 2.304-29.376 6.912-36.736 4.608-7.424 12.16-11.072 22.528-11.072 7.616 0 13.248 2.56 16.64 7.872 3.52 5.248 5.312 13.056 5.312 23.488v84.736h36.928z" fill="currentColor"></path>
</svg>`;
function TranslationTooltips(state: any): readonly Tooltip[] {
const selection = state.selection.main;

View File

@@ -1,8 +1,4 @@
import {KeyBindingCommand} from '@/../bindings/voidraft/internal/models/models';
import {
openSearchPanel,
closeSearchPanel,
} from '@codemirror/search';
import {closeSearchPanel, openSearchPanel,} from '@codemirror/search';
import {
addNewBlockAfterCurrent,
addNewBlockAfterLast,
@@ -49,249 +45,249 @@ import {
} from '@codemirror/commands';
import {foldAll, foldCode, unfoldAll, unfoldCode} from '@codemirror/language';
import i18n from '@/i18n';
import {KeyBindingKey} from '@/../bindings/voidraft/internal/models/models';
// 默认编辑器选项
const defaultEditorOptions = {
// 默认代码块扩展选项
const defaultBlockExtensionOptions = {
defaultBlockToken: 'text',
defaultBlockAutoDetect: true,
};
/**
* 前端命令注册表
* 将后端定义的command字段映射到具体的前端方法和翻译键
* 将后端定义的key字段映射到具体的前端方法和翻译键
*/
export const commands = {
[KeyBindingCommand.ShowSearchCommand]: {
export const commands: Record<string, { handler: any; descriptionKey: string }> = {
[KeyBindingKey.ShowSearchKeyBindingKey]: {
handler: openSearchPanel,
descriptionKey: 'keybindings.commands.showSearch'
},
[KeyBindingCommand.HideSearchCommand]: {
[KeyBindingKey.HideSearchKeyBindingKey]: {
handler: closeSearchPanel,
descriptionKey: 'keybindings.commands.hideSearch'
},
// 代码块操作命令
[KeyBindingCommand.BlockSelectAllCommand]: {
[KeyBindingKey.BlockSelectAllKeyBindingKey]: {
handler: selectAll,
descriptionKey: 'keybindings.commands.blockSelectAll'
},
[KeyBindingCommand.BlockAddAfterCurrentCommand]: {
handler: addNewBlockAfterCurrent(defaultEditorOptions),
[KeyBindingKey.BlockAddAfterCurrentKeyBindingKey]: {
handler: addNewBlockAfterCurrent(defaultBlockExtensionOptions),
descriptionKey: 'keybindings.commands.blockAddAfterCurrent'
},
[KeyBindingCommand.BlockAddAfterLastCommand]: {
handler: addNewBlockAfterLast(defaultEditorOptions),
[KeyBindingKey.BlockAddAfterLastKeyBindingKey]: {
handler: addNewBlockAfterLast(defaultBlockExtensionOptions),
descriptionKey: 'keybindings.commands.blockAddAfterLast'
},
[KeyBindingCommand.BlockAddBeforeCurrentCommand]: {
handler: addNewBlockBeforeCurrent(defaultEditorOptions),
[KeyBindingKey.BlockAddBeforeCurrentKeyBindingKey]: {
handler: addNewBlockBeforeCurrent(defaultBlockExtensionOptions),
descriptionKey: 'keybindings.commands.blockAddBeforeCurrent'
},
[KeyBindingCommand.BlockGotoPreviousCommand]: {
[KeyBindingKey.BlockGotoPreviousKeyBindingKey]: {
handler: gotoPreviousBlock,
descriptionKey: 'keybindings.commands.blockGotoPrevious'
},
[KeyBindingCommand.BlockGotoNextCommand]: {
[KeyBindingKey.BlockGotoNextKeyBindingKey]: {
handler: gotoNextBlock,
descriptionKey: 'keybindings.commands.blockGotoNext'
},
[KeyBindingCommand.BlockSelectPreviousCommand]: {
[KeyBindingKey.BlockSelectPreviousKeyBindingKey]: {
handler: selectPreviousBlock,
descriptionKey: 'keybindings.commands.blockSelectPrevious'
},
[KeyBindingCommand.BlockSelectNextCommand]: {
[KeyBindingKey.BlockSelectNextKeyBindingKey]: {
handler: selectNextBlock,
descriptionKey: 'keybindings.commands.blockSelectNext'
},
[KeyBindingCommand.BlockDeleteCommand]: {
handler: deleteBlock(defaultEditorOptions),
[KeyBindingKey.BlockDeleteKeyBindingKey]: {
handler: deleteBlock(defaultBlockExtensionOptions),
descriptionKey: 'keybindings.commands.blockDelete'
},
[KeyBindingCommand.BlockMoveUpCommand]: {
[KeyBindingKey.BlockMoveUpKeyBindingKey]: {
handler: moveCurrentBlockUp,
descriptionKey: 'keybindings.commands.blockMoveUp'
},
[KeyBindingCommand.BlockMoveDownCommand]: {
[KeyBindingKey.BlockMoveDownKeyBindingKey]: {
handler: moveCurrentBlockDown,
descriptionKey: 'keybindings.commands.blockMoveDown'
},
[KeyBindingCommand.BlockDeleteLineCommand]: {
[KeyBindingKey.BlockDeleteLineKeyBindingKey]: {
handler: deleteLineCommand,
descriptionKey: 'keybindings.commands.blockDeleteLine'
},
[KeyBindingCommand.BlockMoveLineUpCommand]: {
[KeyBindingKey.BlockMoveLineUpKeyBindingKey]: {
handler: moveLineUp,
descriptionKey: 'keybindings.commands.blockMoveLineUp'
},
[KeyBindingCommand.BlockMoveLineDownCommand]: {
[KeyBindingKey.BlockMoveLineDownKeyBindingKey]: {
handler: moveLineDown,
descriptionKey: 'keybindings.commands.blockMoveLineDown'
},
[KeyBindingCommand.BlockTransposeCharsCommand]: {
[KeyBindingKey.BlockTransposeCharsKeyBindingKey]: {
handler: transposeChars,
descriptionKey: 'keybindings.commands.blockTransposeChars'
},
[KeyBindingCommand.BlockFormatCommand]: {
[KeyBindingKey.BlockFormatKeyBindingKey]: {
handler: formatCurrentBlock,
descriptionKey: 'keybindings.commands.blockFormat'
},
[KeyBindingCommand.BlockCopyCommand]: {
[KeyBindingKey.BlockCopyKeyBindingKey]: {
handler: copyCommand,
descriptionKey: 'keybindings.commands.blockCopy'
},
[KeyBindingCommand.BlockCutCommand]: {
[KeyBindingKey.BlockCutKeyBindingKey]: {
handler: cutCommand,
descriptionKey: 'keybindings.commands.blockCut'
},
[KeyBindingCommand.BlockPasteCommand]: {
[KeyBindingKey.BlockPasteKeyBindingKey]: {
handler: pasteCommand,
descriptionKey: 'keybindings.commands.blockPaste'
},
[KeyBindingCommand.HistoryUndoCommand]: {
[KeyBindingKey.HistoryUndoKeyBindingKey]: {
handler: undo,
descriptionKey: 'keybindings.commands.historyUndo'
},
[KeyBindingCommand.HistoryRedoCommand]: {
[KeyBindingKey.HistoryRedoKeyBindingKey]: {
handler: redo,
descriptionKey: 'keybindings.commands.historyRedo'
},
[KeyBindingCommand.HistoryUndoSelectionCommand]: {
[KeyBindingKey.HistoryUndoSelectionKeyBindingKey]: {
handler: undoSelection,
descriptionKey: 'keybindings.commands.historyUndoSelection'
},
[KeyBindingCommand.HistoryRedoSelectionCommand]: {
[KeyBindingKey.HistoryRedoSelectionKeyBindingKey]: {
handler: redoSelection,
descriptionKey: 'keybindings.commands.historyRedoSelection'
},
[KeyBindingCommand.FoldCodeCommand]: {
[KeyBindingKey.FoldCodeKeyBindingKey]: {
handler: foldCode,
descriptionKey: 'keybindings.commands.foldCode'
},
[KeyBindingCommand.UnfoldCodeCommand]: {
[KeyBindingKey.UnfoldCodeKeyBindingKey]: {
handler: unfoldCode,
descriptionKey: 'keybindings.commands.unfoldCode'
},
[KeyBindingCommand.FoldAllCommand]: {
[KeyBindingKey.FoldAllKeyBindingKey]: {
handler: foldAll,
descriptionKey: 'keybindings.commands.foldAll'
},
[KeyBindingCommand.UnfoldAllCommand]: {
[KeyBindingKey.UnfoldAllKeyBindingKey]: {
handler: unfoldAll,
descriptionKey: 'keybindings.commands.unfoldAll'
},
[KeyBindingCommand.CursorSyntaxLeftCommand]: {
[KeyBindingKey.CursorSyntaxLeftKeyBindingKey]: {
handler: cursorSyntaxLeft,
descriptionKey: 'keybindings.commands.cursorSyntaxLeft'
},
[KeyBindingCommand.CursorSyntaxRightCommand]: {
[KeyBindingKey.CursorSyntaxRightKeyBindingKey]: {
handler: cursorSyntaxRight,
descriptionKey: 'keybindings.commands.cursorSyntaxRight'
},
[KeyBindingCommand.SelectSyntaxLeftCommand]: {
[KeyBindingKey.SelectSyntaxLeftKeyBindingKey]: {
handler: selectSyntaxLeft,
descriptionKey: 'keybindings.commands.selectSyntaxLeft'
},
[KeyBindingCommand.SelectSyntaxRightCommand]: {
[KeyBindingKey.SelectSyntaxRightKeyBindingKey]: {
handler: selectSyntaxRight,
descriptionKey: 'keybindings.commands.selectSyntaxRight'
},
[KeyBindingCommand.CopyLineUpCommand]: {
[KeyBindingKey.CopyLineUpKeyBindingKey]: {
handler: copyLineUp,
descriptionKey: 'keybindings.commands.copyLineUp'
},
[KeyBindingCommand.CopyLineDownCommand]: {
[KeyBindingKey.CopyLineDownKeyBindingKey]: {
handler: copyLineDown,
descriptionKey: 'keybindings.commands.copyLineDown'
},
[KeyBindingCommand.InsertBlankLineCommand]: {
[KeyBindingKey.InsertBlankLineKeyBindingKey]: {
handler: insertBlankLine,
descriptionKey: 'keybindings.commands.insertBlankLine'
},
[KeyBindingCommand.SelectLineCommand]: {
[KeyBindingKey.SelectLineKeyBindingKey]: {
handler: selectLine,
descriptionKey: 'keybindings.commands.selectLine'
},
[KeyBindingCommand.SelectParentSyntaxCommand]: {
[KeyBindingKey.SelectParentSyntaxKeyBindingKey]: {
handler: selectParentSyntax,
descriptionKey: 'keybindings.commands.selectParentSyntax'
},
[KeyBindingCommand.IndentLessCommand]: {
[KeyBindingKey.IndentLessKeyBindingKey]: {
handler: indentLess,
descriptionKey: 'keybindings.commands.indentLess'
},
[KeyBindingCommand.IndentMoreCommand]: {
[KeyBindingKey.IndentMoreKeyBindingKey]: {
handler: indentMore,
descriptionKey: 'keybindings.commands.indentMore'
},
[KeyBindingCommand.IndentSelectionCommand]: {
[KeyBindingKey.IndentSelectionKeyBindingKey]: {
handler: indentSelection,
descriptionKey: 'keybindings.commands.indentSelection'
},
[KeyBindingCommand.CursorMatchingBracketCommand]: {
[KeyBindingKey.CursorMatchingBracketKeyBindingKey]: {
handler: cursorMatchingBracket,
descriptionKey: 'keybindings.commands.cursorMatchingBracket'
},
[KeyBindingCommand.ToggleCommentCommand]: {
[KeyBindingKey.ToggleCommentKeyBindingKey]: {
handler: toggleComment,
descriptionKey: 'keybindings.commands.toggleComment'
},
[KeyBindingCommand.ToggleBlockCommentCommand]: {
[KeyBindingKey.ToggleBlockCommentKeyBindingKey]: {
handler: toggleBlockComment,
descriptionKey: 'keybindings.commands.toggleBlockComment'
},
[KeyBindingCommand.InsertNewlineAndIndentCommand]: {
[KeyBindingKey.InsertNewlineAndIndentKeyBindingKey]: {
handler: insertNewlineAndIndent,
descriptionKey: 'keybindings.commands.insertNewlineAndIndent'
},
[KeyBindingCommand.DeleteCharBackwardCommand]: {
[KeyBindingKey.DeleteCharBackwardKeyBindingKey]: {
handler: deleteCharBackward,
descriptionKey: 'keybindings.commands.deleteCharBackward'
},
[KeyBindingCommand.DeleteCharForwardCommand]: {
[KeyBindingKey.DeleteCharForwardKeyBindingKey]: {
handler: deleteCharForward,
descriptionKey: 'keybindings.commands.deleteCharForward'
},
[KeyBindingCommand.DeleteGroupBackwardCommand]: {
[KeyBindingKey.DeleteGroupBackwardKeyBindingKey]: {
handler: deleteGroupBackward,
descriptionKey: 'keybindings.commands.deleteGroupBackward'
},
[KeyBindingCommand.DeleteGroupForwardCommand]: {
[KeyBindingKey.DeleteGroupForwardKeyBindingKey]: {
handler: deleteGroupForward,
descriptionKey: 'keybindings.commands.deleteGroupForward'
},
} as const;
};
/**
* 获取命令处理函数
* @param command 命令名称
* @param key 命令标识符
* @returns 对应的处理函数,如果不存在则返回 undefined
*/
export const getCommandHandler = (command: KeyBindingCommand) => {
return commands[command]?.handler;
export const getCommandHandler = (key: string) => {
return commands[key]?.handler;
};
/**
* 获取命令描述
* @param command 命令名称
* @param key 命令标识符
* @returns 对应的描述,如果不存在则返回 undefined
*/
export const getCommandDescription = (command: KeyBindingCommand) => {
const descriptionKey = commands[command]?.descriptionKey;
export const getCommandDescription = (key: string) => {
const descriptionKey = commands[key]?.descriptionKey;
return descriptionKey ? i18n.global.t(descriptionKey) : undefined;
};
/**
* 检查命令是否已注册
* @param command 命令名称
* @param key 命令标识符
* @returns 是否已注册
*/
export const isCommandRegistered = (command: KeyBindingCommand): boolean => {
return command in commands;
export const isCommandRegistered = (key: string): boolean => {
return key in commands;
};
/**
* 获取所有已注册的命令
* @returns 已注册的命令列表
*/
export const getRegisteredCommands = (): KeyBindingCommand[] => {
return Object.keys(commands) as KeyBindingCommand[];
};
export const getRegisteredCommands = (): string[] => {
return Object.keys(commands);
};

View File

@@ -20,10 +20,12 @@ export const createDynamicKeymapExtension = async (): Promise<Extension> => {
await extensionStore.loadExtensions();
}
// 获取启用的扩展ID列表
const enabledExtensionIds = extensionStore.enabledExtensions.map(ext => ext.id);
// 获取启用的扩展key列表
const enabledExtensionKeys = extensionStore.enabledExtensions
.map(ext => ext.key)
.filter((key): key is string => key !== undefined);
return Manager.createKeymapExtension(keybindingStore.keyBindings, enabledExtensionIds);
return Manager.createKeymapExtension(keybindingStore.keyBindings, enabledExtensionKeys);
};
/**
@@ -34,10 +36,12 @@ export const updateKeymapExtension = (view: any): void => {
const keybindingStore = useKeybindingStore();
const extensionStore = useExtensionStore();
// 获取启用的扩展ID列表
const enabledExtensionIds = extensionStore.enabledExtensions.map(ext => ext.id);
// 获取启用的扩展key列表
const enabledExtensionKeys = extensionStore.enabledExtensions
.map(ext => ext.key)
.filter((key): key is string => key !== undefined);
Manager.updateKeymap(view, keybindingStore.keyBindings, enabledExtensionIds);
Manager.updateKeymap(view, keybindingStore.keyBindings, enabledExtensionKeys);
};
// 导出相关模块

View File

@@ -1,6 +1,6 @@
import {keymap} from '@codemirror/view';
import {Extension, Compartment} from '@codemirror/state';
import {KeyBinding as KeyBindingConfig, ExtensionID} from '@/../bindings/voidraft/internal/models/models';
import {KeyBinding as KeyBindingConfig} from '@/../bindings/voidraft/internal/models/ent/models';
import {KeyBinding, KeymapResult} from './types';
import {getCommandHandler, isCommandRegistered} from './commands';
@@ -14,10 +14,10 @@ export class Manager {
/**
* 将后端快捷键配置转换为CodeMirror快捷键绑定
* @param keyBindings 后端快捷键配置列表
* @param enabledExtensions 启用的扩展ID列表,如果不提供则使用所有启用的快捷键
* @param enabledExtensions 启用的扩展key列表,如果不提供则使用所有启用的快捷键
* @returns 转换结果
*/
static convertToKeyBindings(keyBindings: KeyBindingConfig[], enabledExtensions?: ExtensionID[]): KeymapResult {
static convertToKeyBindings(keyBindings: KeyBindingConfig[], enabledExtensions?: string[]): KeymapResult {
const result: KeyBinding[] = [];
for (const binding of keyBindings) {
@@ -27,24 +27,25 @@ export class Manager {
}
// 如果提供了扩展列表,则只处理启用扩展的快捷键
if (enabledExtensions && !enabledExtensions.includes(binding.extension)) {
if (enabledExtensions && binding.extension && !enabledExtensions.includes(binding.extension)) {
continue;
}
// 检查命令是否已注册
if (!isCommandRegistered(binding.command)) {
// 检查命令是否已注册(使用 key 字段作为命令标识符)
if (!binding.key || !isCommandRegistered(binding.key)) {
continue;
}
// 获取命令处理函数
const handler = getCommandHandler(binding.command);
const handler = getCommandHandler(binding.key);
if (!handler) {
continue;
}
// 转换为CodeMirror快捷键格式
// binding.command 是快捷键组合 (如 "Mod-f")binding.key 是命令标识符
const keyBinding: KeyBinding = {
key: binding.key,
key: binding.command || '',
run: handler,
preventDefault: true
};
@@ -58,10 +59,10 @@ export class Manager {
/**
* 创建CodeMirror快捷键扩展
* @param keyBindings 后端快捷键配置列表
* @param enabledExtensions 启用的扩展ID列表
* @param enabledExtensions 启用的扩展key列表
* @returns CodeMirror扩展
*/
static createKeymapExtension(keyBindings: KeyBindingConfig[], enabledExtensions?: ExtensionID[]): Extension {
static createKeymapExtension(keyBindings: KeyBindingConfig[], enabledExtensions?: string[]): Extension {
const {keyBindings: cmKeyBindings} =
this.convertToKeyBindings(keyBindings, enabledExtensions);
@@ -72,9 +73,9 @@ export class Manager {
* 动态更新快捷键扩展
* @param view 编辑器视图
* @param keyBindings 后端快捷键配置列表
* @param enabledExtensions 启用的扩展ID列表
* @param enabledExtensions 启用的扩展key列表
*/
static updateKeymap(view: any, keyBindings: KeyBindingConfig[], enabledExtensions: ExtensionID[]): void {
static updateKeymap(view: any, keyBindings: KeyBindingConfig[], enabledExtensions: string[]): void {
const {keyBindings: cmKeyBindings} =
this.convertToKeyBindings(keyBindings, enabledExtensions);

View File

@@ -1,5 +1,4 @@
import {Manager} from './manager';
import {ExtensionID} from '@/../bindings/voidraft/internal/models/models';
import i18n from '@/i18n';
import {ExtensionDefinition} from './types';
import {Prec} from '@codemirror/state';
@@ -15,6 +14,8 @@ import {foldGutter} from "@codemirror/language";
import {highlightActiveLineGutter, highlightWhitespace, highlightTrailingWhitespace} from "@codemirror/view";
import createEditorContextMenu from '../extensions/contextMenu';
import {blockLineNumbers} from '../extensions/codeblock';
import {createHttpClientExtension} from '../extensions/httpclient';
import {ExtensionKey} from '@/../bindings/voidraft/internal/models/models';
type ExtensionEntry = {
definition: ExtensionDefinition
@@ -22,35 +23,36 @@ type ExtensionEntry = {
descriptionKey: string
};
type RegisteredExtensionID = Exclude<ExtensionID, ExtensionID.$zero | ExtensionID.ExtensionEditor>;
// 排除 $zero 的有效扩展 Key 类型
type ValidExtensionKey = Exclude<ExtensionKey, ExtensionKey.$zero>;
const defineExtension = (create: (config: any) => any, defaultConfig: Record<string, any> = {}): ExtensionDefinition => ({
create,
defaultConfig
});
const EXTENSION_REGISTRY: Record<RegisteredExtensionID, ExtensionEntry> = {
[ExtensionID.ExtensionRainbowBrackets]: {
const EXTENSION_REGISTRY: Record<ValidExtensionKey, ExtensionEntry> = {
[ExtensionKey.ExtensionRainbowBrackets]: {
definition: defineExtension(() => rainbowBrackets()),
displayNameKey: 'extensions.rainbowBrackets.name',
descriptionKey: 'extensions.rainbowBrackets.description'
},
[ExtensionID.ExtensionHyperlink]: {
[ExtensionKey.ExtensionHyperlink]: {
definition: defineExtension(() => hyperLink),
displayNameKey: 'extensions.hyperlink.name',
descriptionKey: 'extensions.hyperlink.description'
},
[ExtensionID.ExtensionColorSelector]: {
[ExtensionKey.ExtensionColorSelector]: {
definition: defineExtension(() => color),
displayNameKey: 'extensions.colorSelector.name',
descriptionKey: 'extensions.colorSelector.description'
},
[ExtensionID.ExtensionTranslator]: {
[ExtensionKey.ExtensionTranslator]: {
definition: defineExtension(() => createTranslatorExtension()),
displayNameKey: 'extensions.translator.name',
descriptionKey: 'extensions.translator.description'
},
[ExtensionID.ExtensionMinimap]: {
[ExtensionKey.ExtensionMinimap]: {
definition: defineExtension((config: any) => minimap({
displayText: config?.displayText ?? 'characters',
showOverlay: config?.showOverlay ?? 'always',
@@ -63,85 +65,90 @@ const EXTENSION_REGISTRY: Record<RegisteredExtensionID, ExtensionEntry> = {
displayNameKey: 'extensions.minimap.name',
descriptionKey: 'extensions.minimap.description'
},
[ExtensionID.ExtensionSearch]: {
[ExtensionKey.ExtensionSearch]: {
definition: defineExtension(() => vscodeSearch),
displayNameKey: 'extensions.search.name',
descriptionKey: 'extensions.search.description'
},
[ExtensionID.ExtensionFold]: {
[ExtensionKey.ExtensionFold]: {
definition: defineExtension(() => Prec.low(foldGutter())),
displayNameKey: 'extensions.fold.name',
descriptionKey: 'extensions.fold.description'
},
[ExtensionID.ExtensionMarkdown]: {
[ExtensionKey.ExtensionMarkdown]: {
definition: defineExtension(() => markdownExtensions),
displayNameKey: 'extensions.markdown.name',
descriptionKey: 'extensions.markdown.description'
},
[ExtensionID.ExtensionLineNumbers]: {
[ExtensionKey.ExtensionLineNumbers]: {
definition: defineExtension(() => Prec.high([blockLineNumbers, highlightActiveLineGutter()])),
displayNameKey: 'extensions.lineNumbers.name',
descriptionKey: 'extensions.lineNumbers.description'
},
[ExtensionID.ExtensionContextMenu]: {
[ExtensionKey.ExtensionContextMenu]: {
definition: defineExtension(() => createEditorContextMenu()),
displayNameKey: 'extensions.contextMenu.name',
descriptionKey: 'extensions.contextMenu.description'
},
[ExtensionID.ExtensionHighlightWhitespace]: {
[ExtensionKey.ExtensionHighlightWhitespace]: {
definition: defineExtension(() => highlightWhitespace()),
displayNameKey: 'extensions.highlightWhitespace.name',
descriptionKey: 'extensions.highlightWhitespace.description'
},
[ExtensionID.ExtensionHighlightTrailingWhitespace]: {
[ExtensionKey.ExtensionHighlightTrailingWhitespace]: {
definition: defineExtension(() => highlightTrailingWhitespace()),
displayNameKey: 'extensions.highlightTrailingWhitespace.name',
descriptionKey: 'extensions.highlightTrailingWhitespace.description'
},
[ExtensionKey.ExtensionHttpClient]: {
definition: defineExtension(() => createHttpClientExtension()),
displayNameKey: 'extensions.httpClient.name',
descriptionKey: 'extensions.httpClient.description'
}
} as const;
};
const isRegisteredExtension = (id: ExtensionID): id is RegisteredExtensionID =>
Object.prototype.hasOwnProperty.call(EXTENSION_REGISTRY, id);
const isRegisteredExtension = (key: string): key is ValidExtensionKey =>
Object.prototype.hasOwnProperty.call(EXTENSION_REGISTRY, key);
const getRegistryEntry = (id: ExtensionID): ExtensionEntry | undefined => {
if (!isRegisteredExtension(id)) {
const getRegistryEntry = (key: string): ExtensionEntry | undefined => {
if (!isRegisteredExtension(key)) {
return undefined;
}
return EXTENSION_REGISTRY[id];
return EXTENSION_REGISTRY[key];
};
export function registerAllExtensions(manager: Manager): void {
(Object.entries(EXTENSION_REGISTRY) as [RegisteredExtensionID, ExtensionEntry][]).forEach(([id, entry]) => {
(Object.entries(EXTENSION_REGISTRY) as [ValidExtensionKey, ExtensionEntry][]).forEach(([id, entry]) => {
manager.registerExtension(id, entry.definition);
});
}
export function getExtensionDisplayName(id: ExtensionID): string {
const entry = getRegistryEntry(id);
return entry?.displayNameKey ? i18n.global.t(entry.displayNameKey) : id;
export function getExtensionDisplayName(key: string): string {
const entry = getRegistryEntry(key);
return entry?.displayNameKey ? i18n.global.t(entry.displayNameKey) : key;
}
export function getExtensionDescription(id: ExtensionID): string {
const entry = getRegistryEntry(id);
export function getExtensionDescription(key: string): string {
const entry = getRegistryEntry(key);
return entry?.descriptionKey ? i18n.global.t(entry.descriptionKey) : '';
}
function getExtensionDefinition(id: ExtensionID): ExtensionDefinition | undefined {
return getRegistryEntry(id)?.definition;
function getExtensionDefinition(key: string): ExtensionDefinition | undefined {
return getRegistryEntry(key)?.definition;
}
export function getExtensionDefaultConfig(id: ExtensionID): any {
const definition = getExtensionDefinition(id);
export function getExtensionDefaultConfig(key: string): any {
const definition = getExtensionDefinition(key);
if (!definition) return {};
return cloneConfig(definition.defaultConfig);
}
export function hasExtensionConfig(id: ExtensionID): boolean {
return Object.keys(getExtensionDefaultConfig(id)).length > 0;
export function hasExtensionConfig(key: string): boolean {
return Object.keys(getExtensionDefaultConfig(key)).length > 0;
}
export function getAllExtensionIds(): ExtensionID[] {
return Object.keys(EXTENSION_REGISTRY) as RegisteredExtensionID[];
export function getAllExtensionIds(): string[] {
return Object.keys(EXTENSION_REGISTRY);
}
const cloneConfig = (config: any) => {

View File

@@ -12,9 +12,8 @@ const extensionManager = new Manager();
/**
* 异步创建动态扩展
* 确保扩展配置已加载
* @param _documentId 可选的文档ID用于提前初始化视图
*/
export const createDynamicExtensions = async (_documentId?: number): Promise<Extension[]> => {
export const createDynamicExtensions = async (): Promise<Extension[]> => {
const extensionStore = useExtensionStore();
// 注册所有扩展工厂

View File

@@ -1,6 +1,6 @@
import {Compartment, Extension} from '@codemirror/state';
import {EditorView} from '@codemirror/view';
import {Extension as ExtensionConfig, ExtensionID} from '@/../bindings/voidraft/internal/models/models';
import {Extension as ExtensionConfig} from '@/../bindings/voidraft/internal/models/ent/models';
import {ExtensionDefinition, ExtensionState} from './types';
/**
@@ -8,10 +8,10 @@ import {ExtensionDefinition, ExtensionState} from './types';
* 负责注册、初始化与同步所有动态扩展
*/
export class Manager {
private extensionStates = new Map<ExtensionID, ExtensionState>();
private extensionStates = new Map<string, ExtensionState>();
private views = new Map<number, EditorView>();
registerExtension(id: ExtensionID, definition: ExtensionDefinition): void {
registerExtension(id: string, definition: ExtensionDefinition): void {
const existingState = this.extensionStates.get(id);
if (existingState) {
existingState.definition = definition;
@@ -34,10 +34,11 @@ export class Manager {
initExtensions(extensionConfigs: ExtensionConfig[]): void {
for (const config of extensionConfigs) {
const state = this.extensionStates.get(config.id);
if (!config.key) continue;
const state = this.extensionStates.get(config.key);
if (!state) continue;
const resolvedConfig = this.cloneConfig(config.config ?? state.definition.defaultConfig ?? {});
this.commitExtensionState(state, config.enabled, resolvedConfig);
this.commitExtensionState(state, config.enabled ?? false, resolvedConfig);
}
}
@@ -54,7 +55,7 @@ export class Manager {
this.applyAllExtensionsToView(view);
}
updateExtension(id: ExtensionID, enabled: boolean, config?: any): void {
updateExtension(id: string, enabled: boolean, config?: any): void {
const state = this.extensionStates.get(id);
if (!state) return;
@@ -93,7 +94,7 @@ export class Manager {
}
}
private applyExtensionToAllViews(id: ExtensionID): void {
private applyExtensionToAllViews(id: string): void {
const state = this.extensionStates.get(id);
if (!state) return;

View File

@@ -1 +1,23 @@
import {Compartment, Extension} from '@codemirror/state';
import {Compartment, Extension} from '@codemirror/state';
/**
* 扩展定义
* 标准化 create 方法和默认配置
*/
export interface ExtensionDefinition {
create(config: any): Extension
defaultConfig: Record<string, any>
}
/**
* 扩展运行时状态
*/
export interface ExtensionState {
id: string // 扩展 key
definition: ExtensionDefinition
config: any
enabled: boolean
compartment: Compartment
extension: Extension
}

View File

@@ -1,5 +1,5 @@
import type {ThemeColors} from './types';
import {ThemeType} from '@/../bindings/voidraft/internal/models/models';
import {Type as ThemeType} from '@/../bindings/voidraft/internal/models/ent/theme/models';
import {defaultDarkColors} from './dark/default-dark';
import {defaultLightColors} from './light/default-light';
import {config as draculaColors} from './dark/dracula';
@@ -24,20 +24,20 @@ export interface ThemePreset {
export const FALLBACK_THEME_NAME = defaultDarkColors.themeName;
export const themePresetList: ThemePreset[] = [
{name: defaultDarkColors.themeName, type: ThemeType.ThemeTypeDark, colors: defaultDarkColors},
{name: draculaColors.themeName, type: ThemeType.ThemeTypeDark, colors: draculaColors},
{name: auraColors.themeName, type: ThemeType.ThemeTypeDark, colors: auraColors},
{name: githubDarkColors.themeName, type: ThemeType.ThemeTypeDark, colors: githubDarkColors},
{name: materialDarkColors.themeName, type: ThemeType.ThemeTypeDark, colors: materialDarkColors},
{name: oneDarkColors.themeName, type: ThemeType.ThemeTypeDark, colors: oneDarkColors},
{name: solarizedDarkColors.themeName, type: ThemeType.ThemeTypeDark, colors: solarizedDarkColors},
{name: tokyoNightColors.themeName, type: ThemeType.ThemeTypeDark, colors: tokyoNightColors},
{name: tokyoNightStormColors.themeName, type: ThemeType.ThemeTypeDark, colors: tokyoNightStormColors},
{name: defaultLightColors.themeName, type: ThemeType.ThemeTypeLight, colors: defaultLightColors},
{name: githubLightColors.themeName, type: ThemeType.ThemeTypeLight, colors: githubLightColors},
{name: materialLightColors.themeName, type: ThemeType.ThemeTypeLight, colors: materialLightColors},
{name: solarizedLightColors.themeName, type: ThemeType.ThemeTypeLight, colors: solarizedLightColors},
{name: tokyoNightDayColors.themeName, type: ThemeType.ThemeTypeLight, colors: tokyoNightDayColors},
{name: defaultDarkColors.themeName, type: ThemeType.TypeDark, colors: defaultDarkColors},
{name: draculaColors.themeName, type: ThemeType.TypeDark, colors: draculaColors},
{name: auraColors.themeName, type: ThemeType.TypeDark, colors: auraColors},
{name: githubDarkColors.themeName, type: ThemeType.TypeDark, colors: githubDarkColors},
{name: materialDarkColors.themeName, type: ThemeType.TypeDark, colors: materialDarkColors},
{name: oneDarkColors.themeName, type: ThemeType.TypeDark, colors: oneDarkColors},
{name: solarizedDarkColors.themeName, type: ThemeType.TypeDark, colors: solarizedDarkColors},
{name: tokyoNightColors.themeName, type: ThemeType.TypeDark, colors: tokyoNightColors},
{name: tokyoNightStormColors.themeName, type: ThemeType.TypeDark, colors: tokyoNightStormColors},
{name: defaultLightColors.themeName, type: ThemeType.TypeLight, colors: defaultLightColors},
{name: githubLightColors.themeName, type: ThemeType.TypeLight, colors: githubLightColors},
{name: materialLightColors.themeName, type: ThemeType.TypeLight, colors: materialLightColors},
{name: solarizedLightColors.themeName, type: ThemeType.TypeLight, colors: solarizedLightColors},
{name: tokyoNightDayColors.themeName, type: ThemeType.TypeLight, colors: tokyoNightDayColors},
];
export const themePresetMap: Record<string, ThemePreset> = themePresetList.reduce(

View File

@@ -7,7 +7,7 @@ import SettingSection from '../components/SettingSection.vue';
import SettingItem from '../components/SettingItem.vue';
import { SystemThemeType, LanguageType } from '@/../bindings/voidraft/internal/models/models';
import { createDebounce } from '@/common/utils/debounce';
import { createTimerManager } from '@/common/utils/timerUtils';
import { useConfirm } from '@/composables/useConfirm';
import PickColors from 'vue-pick-colors';
import type { ThemeColors } from '@/views/editor/theme/types';
@@ -21,31 +21,22 @@ const { debouncedFn: debouncedUpdateColor } = createDebounce(
{ delay: 100 }
);
const { debouncedFn: debouncedResetTheme } = createDebounce(
async () => {
const success = await themeStore.resetCurrentTheme();
if (success) {
// 重新加载临时颜色
syncTempColors();
hasUnsavedChanges.value = false;
}
},
{ delay: 300 }
);
// 创建定时器管理器
const resetTimer = createTimerManager();
// 临时颜色状态(用于编辑)
const tempColors = ref<ThemeColors | null>(null);
// 标记是否有未保存的更改
const hasUnsavedChanges = ref(false);
// 重置按钮状态
const resetButtonState = ref({
confirming: false
// 重置主题确认
const { isConfirming: isResetConfirming, requestConfirm: requestResetConfirm } = useConfirm({
timeout: 3000,
onConfirm: async () => {
const success = await themeStore.resetCurrentTheme();
if (success) {
syncTempColors();
hasUnsavedChanges.value = false;
}
}
});
// 当前选中的主题名称
@@ -125,23 +116,6 @@ const toggleSearch = async () => {
}
};
// 处理重置按钮点击
const handleResetClick = () => {
if (resetButtonState.value.confirming) {
debouncedResetTheme();
resetButtonState.value.confirming = false;
resetTimer.clear();
} else {
resetButtonState.value.confirming = true;
// 设置3秒后自动恢复
resetTimer.set(() => {
resetButtonState.value.confirming = false;
}, 3000);
}
};
// 更新本地颜色配置
const updateLocalColor = (colorKey: string, value: string) => {
if (!tempColors.value) return;
@@ -295,10 +269,10 @@ const handlePickerClose = () => {
</button>
<button
v-if="!hasUnsavedChanges"
:class="['reset-button', resetButtonState.confirming ? 'reset-button-confirming' : '']"
@click="handleResetClick"
:class="['reset-button', isResetConfirming('theme') ? 'reset-button-confirming' : '']"
@click="requestResetConfirm('theme')"
>
{{ resetButtonState.confirming ? t('settings.confirmReset') : t('settings.resetToDefault') }}
{{ isResetConfirming('theme') ? t('settings.confirmReset') : t('settings.resetToDefault') }}
</button>
<template v-else>
<button class="apply-button" @click="applyChanges">

View File

@@ -4,7 +4,6 @@ import {useI18n} from 'vue-i18n';
import {useEditorStore} from '@/stores/editorStore';
import {useExtensionStore} from '@/stores/extensionStore';
import {ExtensionService} from '@/../bindings/voidraft/internal/services';
import {ExtensionID} from '@/../bindings/voidraft/internal/models/models';
import {
getAllExtensionIds,
getExtensionDefaultConfig,
@@ -21,59 +20,58 @@ const editorStore = useEditorStore();
const extensionStore = useExtensionStore();
// 展开状态管理
const expandedExtensions = ref<Set<ExtensionID>>(new Set());
const expandedExtensions = ref<Set<string>>(new Set());
// 获取所有可用的扩展
const availableExtensions = computed(() => {
return getAllExtensionIds().map(id => {
const extension = extensionStore.extensions.find(ext => ext.id === id);
return getAllExtensionIds().map(key => {
const extension = extensionStore.extensions.find(ext => ext.key === key);
return {
id,
displayName: getExtensionDisplayName(id),
description: getExtensionDescription(id),
id: key,
displayName: getExtensionDisplayName(key),
description: getExtensionDescription(key),
enabled: extension?.enabled || false,
isDefault: extension?.isDefault || false,
hasConfig: hasExtensionConfig(id),
hasConfig: hasExtensionConfig(key),
config: extension?.config || {},
defaultConfig: getExtensionDefaultConfig(id)
defaultConfig: getExtensionDefaultConfig(key)
};
});
});
// 切换展开状态
const toggleExpanded = (extensionId: ExtensionID) => {
if (expandedExtensions.value.has(extensionId)) {
expandedExtensions.value.delete(extensionId);
const toggleExpanded = (extensionKey: string) => {
if (expandedExtensions.value.has(extensionKey)) {
expandedExtensions.value.delete(extensionKey);
} else {
expandedExtensions.value.add(extensionId);
expandedExtensions.value.add(extensionKey);
}
};
// 更新扩展状态
const updateExtension = async (extensionId: ExtensionID, enabled: boolean) => {
const updateExtension = async (extensionKey: string, enabled: boolean) => {
try {
await editorStore.updateExtension(extensionId, enabled);
await editorStore.updateExtension(extensionKey, enabled);
} catch (error) {
console.error('Failed to update extension:', error);
}
};
// 更新扩展配置
const updateExtensionConfig = async (extensionId: ExtensionID, configKey: string, value: any) => {
const updateExtensionConfig = async (extensionKey: string, configKey: string, value: any) => {
try {
// 获取当前扩展状态
const extension = extensionStore.extensions.find(ext => ext.id === extensionId);
const extension = extensionStore.extensions.find(ext => ext.key === extensionKey);
if (!extension) return;
// 更新配置
const updatedConfig = {...extension.config};
const updatedConfig = {...(extension.config || {})};
if (value === undefined) {
delete updatedConfig[configKey];
} else {
updatedConfig[configKey] = value;
}
// 使用editorStore的updateExtension方法更新确保应用到所有编辑器实例
await editorStore.updateExtension(extensionId, extension.enabled, updatedConfig);
await editorStore.updateExtension(extensionKey, extension.enabled ?? false, updatedConfig);
} catch (error) {
console.error('Failed to update extension config:', error);
@@ -81,19 +79,19 @@ const updateExtensionConfig = async (extensionId: ExtensionID, configKey: string
};
// 重置扩展到默认配置
const resetExtension = async (extensionId: ExtensionID) => {
const resetExtension = async (extensionKey: string) => {
try {
// 重置到默认配置
await ExtensionService.ResetExtensionToDefault(extensionId);
await ExtensionService.ResetExtensionConfig(extensionKey);
// 重新加载扩展状态以获取最新配置
await extensionStore.loadExtensions();
// 获取重置后的状态,立即应用到所有编辑器视图
const extension = extensionStore.extensions.find(ext => ext.id === extensionId);
const extension = extensionStore.extensions.find(ext => ext.key === extensionKey);
if (extension) {
// 通过editorStore更新确保所有视图都能同步
await editorStore.updateExtension(extensionId, extension.enabled, extension.config);
await editorStore.updateExtension(extensionKey, extension.enabled ?? false, extension.config);
}
} catch (error) {
console.error('Failed to reset extension:', error);
@@ -127,7 +125,7 @@ const formatConfigValue = (value: any): string => {
const handleConfigInput = async (
extensionId: ExtensionID,
extensionKey: string,
configKey: string,
defaultValue: any,
event: Event
@@ -137,15 +135,15 @@ const handleConfigInput = async (
const rawValue = target.value;
const trimmedValue = rawValue.trim();
if (!trimmedValue.length) {
await updateExtensionConfig(extensionId, configKey, undefined);
await updateExtensionConfig(extensionKey, configKey, undefined);
return;
}
try {
const parsedValue = JSON.parse(trimmedValue);
await updateExtensionConfig(extensionId, configKey, parsedValue);
await updateExtensionConfig(extensionKey, configKey, parsedValue);
} catch (_error) {
const extension = extensionStore.extensions.find(ext => ext.id === extensionId);
const extension = extensionStore.extensions.find(ext => ext.key === extensionKey);
const fallbackValue = getConfigValue(extension?.config, configKey, defaultValue);
target.value = formatConfigValue(fallbackValue);

View File

@@ -2,133 +2,67 @@
import {useConfigStore} from '@/stores/configStore';
import {useTabStore} from '@/stores/tabStore';
import {useI18n} from 'vue-i18n';
import {computed, onUnmounted, ref} from 'vue';
import {computed, ref} from 'vue';
import SettingSection from '../components/SettingSection.vue';
import SettingItem from '../components/SettingItem.vue';
import ToggleSwitch from '../components/ToggleSwitch.vue';
import {
DialogService,
MigrationProgress,
MigrationService,
MigrationStatus
} from '@/../bindings/voidraft/internal/services';
import {DialogService, MigrationService} from '@/../bindings/voidraft/internal/services';
import {useSystemStore} from "@/stores/systemStore";
import {useConfirm, usePolling} from '@/composables';
const {t} = useI18n();
const configStore = useConfigStore();
const systemStore = useSystemStore();
const tabStore = useTabStore();
// 迁移进度状态
const migrationProgress = ref<MigrationProgress>(new MigrationProgress({
status: MigrationStatus.MigrationStatusCompleted,
progress: 0
}));
// 轮询相关
let pollingTimer: number | null = null;
const isPolling = ref(false);
// 进度条显示控制
const showProgress = ref(false);
const progressError = ref('');
let hideProgressTimer: any = null;
const showBar = ref(false);
const manualError = ref(''); // 用于捕获 MigrateDirectory 抛出的错误
let hideTimer = 0;
// 开始轮询迁移进度
const startPolling = () => {
if (isPolling.value) return;
isPolling.value = true;
showProgress.value = true;
progressError.value = '';
// 立即重置迁移进度状态,避免从之前的失败状态渐变
migrationProgress.value = new MigrationProgress({
status: MigrationStatus.MigrationStatusMigrating,
progress: 0
});
pollingTimer = window.setInterval(async () => {
try {
const progress = await MigrationService.GetProgress();
migrationProgress.value = progress;
const {status, error} = progress;
const isCompleted = [MigrationStatus.MigrationStatusCompleted, MigrationStatus.MigrationStatusFailed].includes(status);
if (isCompleted) {
stopPolling();
// 设置错误信息(如果是失败状态)
progressError.value = (status === MigrationStatus.MigrationStatusFailed) ? (error || 'Migration failed') : '';
const delay = status === MigrationStatus.MigrationStatusCompleted ? 3000 : 5000;
hideProgressTimer = setTimeout(hideProgress, delay);
// 轮询迁移进度
const {data: progress, error: pollError, isActive: migrating, start, stop, reset} = usePolling(
() => MigrationService.GetProgress(),
{
interval: 300,
shouldStop: ({progress, error}) => !!error || progress >= 100,
onStop: () => {
const hasError = pollError.value || progress.value?.error;
hideTimer = window.setTimeout(hideAll, hasError ? 5000 : 3000);
}
} catch (_error) {
stopPolling();
// 使用常量简化错误处理
const errorMsg = 'Failed to get migration progress';
Object.assign(migrationProgress.value, {
status: MigrationStatus.MigrationStatusFailed,
progress: 0,
error: errorMsg
});
progressError.value = errorMsg;
hideProgressTimer = setTimeout(hideProgress, 5000);
}
}, 200);
};
// 停止轮询
const stopPolling = () => {
if (pollingTimer) {
clearInterval(pollingTimer);
pollingTimer = null;
}
isPolling.value = false;
};
// 隐藏进度条
const hideProgress = () => {
showProgress.value = false;
progressError.value = '';
// 重置迁移状态,避免下次显示时状态不正确
migrationProgress.value = new MigrationProgress({
status: MigrationStatus.MigrationStatusCompleted,
progress: 0
});
if (hideProgressTimer) {
clearTimeout(hideProgressTimer);
hideProgressTimer = null;
}
};
// 简化的迁移状态管理
const isMigrating = computed(() => migrationProgress.value.status === MigrationStatus.MigrationStatusMigrating);
// 进度条样式 - 使用 Map 简化条件判断
const statusClassMap = new Map([
[MigrationStatus.MigrationStatusMigrating, 'migrating'],
[MigrationStatus.MigrationStatusCompleted, 'success'],
[MigrationStatus.MigrationStatusFailed, 'error']
]);
const progressBarClass = computed(() =>
showProgress.value ? statusClassMap.get(migrationProgress.value.status) ?? '' : ''
);
const progressBarWidth = computed(() => {
if (!showProgress.value) return '0%';
return isMigrating.value ? `${migrationProgress.value.progress}%` : '100%';
// 派生状态
const migrationError = computed(() => manualError.value || pollError.value || progress.value?.error || '');
const currentProgress = computed(() => progress.value?.progress ?? 0);
const barClass = computed(() => {
if (!showBar.value) return '';
return migrationError.value ? 'error' : currentProgress.value >= 100 ? 'success' : 'migrating';
});
// 重置确认状态
const resetConfirmState = ref<'idle' | 'confirming'>('idle');
let resetConfirmTimer: any = null;
const barWidth = computed(() => {
if (!showBar.value) return '0%';
return (migrationError.value || currentProgress.value >= 100) ? '100%' : `${currentProgress.value}%`;
});
// 隐藏进度条并清除所有状态
const hideAll = () => {
clearTimeout(hideTimer);
hideTimer = 0;
showBar.value = false;
manualError.value = '';
reset(); // 清除轮询状态
};
// 重置设置确认
const {isConfirming: isResetConfirming, requestConfirm: requestResetConfirm} = useConfirm({
timeout: 3000,
onConfirm: async () => {
await configStore.resetConfig();
}
});
// 可选键列表
const keyOptions = [
@@ -201,7 +135,7 @@ const modifierKeys = computed(() => ({
win: configStore.config.general.globalHotkey.win
}));
// 主键配置 - 只读计算属性
// 主键配置
const selectedKey = computed(() => configStore.config.general.globalHotkey.key);
// 切换修饰键
@@ -218,29 +152,8 @@ const updateSelectedKey = (event: Event) => {
configStore.setGlobalHotkey(newHotkey);
};
// 重置设置
const resetSettings = () => {
if (resetConfirmState.value === 'idle') {
// 第一次点击,进入确认状态
resetConfirmState.value = 'confirming';
// 3秒后自动返回idle状态
resetConfirmTimer = setTimeout(() => {
resetConfirmState.value = 'idle';
}, 3000);
} else if (resetConfirmState.value === 'confirming') {
// 第二次点击,执行重置
clearTimeout(resetConfirmTimer);
resetConfirmState.value = 'idle';
confirmReset();
}
};
// 确认重置
const confirmReset = async () => {
await configStore.resetConfig();
};
// 计算热键预览文本 - 使用现代语法简化
// 计算热键预览文本
const hotkeyPreview = computed(() => {
if (!enableGlobalHotkey.value) return '';
@@ -261,54 +174,30 @@ const currentDataPath = computed(() => configStore.config.general.dataPath);
// 选择数据存储目录
const selectDataDirectory = async () => {
if (isMigrating.value) return;
if (migrating.value) return;
const selectedPath = await DialogService.SelectDirectory();
if (!selectedPath?.trim() || selectedPath === currentDataPath.value) return;
// 检查用户是否取消了选择或路径为空
if (!selectedPath || !selectedPath.trim() || selectedPath === currentDataPath.value) {
return;
}
const oldPath = currentDataPath.value;
const newPath = selectedPath.trim();
const [oldPath, newPath] = [currentDataPath.value, selectedPath.trim()];
// 清除之前的进度状态
hideProgress();
// 清除之前的状态并开始轮询
hideAll();
showBar.value = true;
manualError.value = '';
start();
// 开始轮询迁移进度
startPolling();
// 开始迁移
try {
await MigrationService.MigrateDirectory(oldPath, newPath);
await configStore.setDataPath(newPath);
} catch (error) {
stopPolling();
// 使用解构和默认值简化错误处理
const errorMsg = error?.toString() || 'Migration failed';
showProgress.value = true;
Object.assign(migrationProgress.value, {
status: MigrationStatus.MigrationStatusFailed,
progress: 0,
error: errorMsg
});
progressError.value = errorMsg;
hideProgressTimer = setTimeout(hideProgress, 5000);
} catch (e) {
stop();
// 设置手动捕获的错误(当轮询还没获取到错误时)
manualError.value = String(e).replace(/^Error:\s*/i, '') || 'Migration failed';
showBar.value = true;
hideTimer = window.setTimeout(hideAll, 5000);
}
};
// 清理定时器
onUnmounted(() => {
stopPolling();
hideProgress();
if (resetConfirmTimer) {
clearTimeout(resetConfirmTimer);
}
});
</script>
<template>
@@ -394,24 +283,15 @@ onUnmounted(() => {
class="path-display-input"
@click="selectDataDirectory"
:title="t('settings.clickToSelectPath')"
:disabled="isMigrating"
:disabled="migrating"
/>
<!-- 简洁的进度条 -->
<div
class="progress-bar"
:class="[
{ 'active': showProgress },
progressBarClass
]"
:style="{ width: progressBarWidth }"
></div>
<!-- 进度条 -->
<div class="progress-bar" :class="[{'active': showBar}, barClass]" :style="{width: barWidth}"/>
</div>
<!-- 错误提示 -->
<Transition name="error-fade">
<div v-if="progressError" class="progress-error">
{{ progressError }}
</div>
<div v-if="migrationError" class="progress-error">{{ migrationError }}</div>
</Transition>
</div>
</div>
@@ -421,15 +301,10 @@ onUnmounted(() => {
<SettingItem :title="t('settings.resetAllSettings')">
<button
class="reset-button"
:class="{ 'confirming': resetConfirmState === 'confirming' }"
@click="resetSettings"
:class="{ 'confirming': isResetConfirming('reset') }"
@click="requestResetConfirm('reset')"
>
<template v-if="resetConfirmState === 'idle'">
{{ t('settings.reset') }}
</template>
<template v-else-if="resetConfirmState === 'confirming'">
{{ t('settings.confirmReset') }}
</template>
{{ isResetConfirming('reset') ? t('settings.confirmReset') : t('settings.reset') }}
</button>
</SettingItem>
</SettingSection>
@@ -656,11 +531,8 @@ onUnmounted(() => {
}
.progress-error {
margin-top: 6px;
font-size: 12px;
color: #ef4444;
padding: 0 2px;
line-height: 1.4;
opacity: 1;
transition: all 0.3s ease;
}

View File

@@ -6,7 +6,7 @@ import { useKeybindingStore } from '@/stores/keybindingStore';
import { useExtensionStore } from '@/stores/extensionStore';
import { useSystemStore } from '@/stores/systemStore';
import { getCommandDescription } from '@/views/editor/keymap/commands';
import {KeyBindingCommand} from "@/../bindings/voidraft/internal/models";
import { KeyBindingKey } from '@/../bindings/voidraft/internal/models/models';
const { t } = useI18n();
const keybindingStore = useKeybindingStore();
@@ -25,21 +25,21 @@ const keyBindings = computed(() => {
const enabledExtensionIds = new Set(extensionStore.enabledExtensionIds);
return keybindingStore.keyBindings
.filter(kb => kb.enabled && enabledExtensionIds.has(kb.extension))
.filter(kb => kb.enabled && (!kb.extension || enabledExtensionIds.has(kb.extension)))
.map(kb => ({
id: kb.command,
keys: parseKeyBinding(kb.key, kb.command),
category: kb.extension,
description: getCommandDescription(kb.command) || kb.command
id: kb.key,
keys: parseKeyBinding(kb.command || '', kb.key),
category: kb.extension || '',
description: kb.key ? (getCommandDescription(kb.key) || kb.key) : ''
}));
});
// 解析快捷键字符串为显示数组
const parseKeyBinding = (keyStr: string, command?: string): string[] => {
const parseKeyBinding = (keyStr: string, keyBindingKey?: string): string[] => {
if (!keyStr) return [];
// 特殊处理重做快捷键的操作系统差异
if (command === KeyBindingCommand.HistoryRedoCommand && keyStr === 'Mod-Shift-z') {
if (keyBindingKey === KeyBindingKey.HistoryRedoKeyBindingKey && keyStr === 'Mod-Shift-z') {
if (systemStore.isMacOS) {
return ['⌘', '⇧', 'Z']; // macOS: Cmd+Shift+Z
} else {
@@ -48,7 +48,7 @@ const parseKeyBinding = (keyStr: string, command?: string): string[] => {
}
// 特殊处理重做选择快捷键的操作系统差异
if (command === KeyBindingCommand.HistoryRedoSelectionCommand && keyStr === 'Mod-Shift-u') {
if (keyBindingKey === KeyBindingKey.HistoryRedoSelectionKeyBindingKey && keyStr === 'Mod-Shift-u') {
if (systemStore.isMacOS) {
return ['⌘', '⇧', 'U']; // macOS: Cmd+Shift+U
} else {
@@ -57,7 +57,7 @@ const parseKeyBinding = (keyStr: string, command?: string): string[] => {
}
// 特殊处理代码折叠快捷键的操作系统差异
if (command === KeyBindingCommand.FoldCodeCommand && keyStr === 'Ctrl-Shift-[') {
if (keyBindingKey === KeyBindingKey.FoldCodeKeyBindingKey && keyStr === 'Ctrl-Shift-[') {
if (systemStore.isMacOS) {
return ['⌘', '⌥', '[']; // macOS: Cmd+Alt+[
} else {
@@ -65,7 +65,7 @@ const parseKeyBinding = (keyStr: string, command?: string): string[] => {
}
}
if (command === KeyBindingCommand.UnfoldCodeCommand && keyStr === 'Ctrl-Shift-]') {
if (keyBindingKey === KeyBindingKey.UnfoldCodeKeyBindingKey && keyStr === 'Ctrl-Shift-]') {
if (systemStore.isMacOS) {
return ['⌘', '⌥', ']']; // macOS: Cmd+Alt+]
} else {
@@ -74,7 +74,7 @@ const parseKeyBinding = (keyStr: string, command?: string): string[] => {
}
// 特殊处理编辑快捷键的操作系统差异
if (command === KeyBindingCommand.CursorSyntaxLeftCommand && keyStr === 'Alt-ArrowLeft') {
if (keyBindingKey === KeyBindingKey.CursorSyntaxLeftKeyBindingKey && keyStr === 'Alt-ArrowLeft') {
if (systemStore.isMacOS) {
return ['Ctrl', '←']; // macOS: Ctrl+ArrowLeft
} else {
@@ -82,7 +82,7 @@ const parseKeyBinding = (keyStr: string, command?: string): string[] => {
}
}
if (command === KeyBindingCommand.CursorSyntaxRightCommand && keyStr === 'Alt-ArrowRight') {
if (keyBindingKey === KeyBindingKey.CursorSyntaxRightKeyBindingKey && keyStr === 'Alt-ArrowRight') {
if (systemStore.isMacOS) {
return ['Ctrl', '→']; // macOS: Ctrl+ArrowRight
} else {
@@ -90,7 +90,7 @@ const parseKeyBinding = (keyStr: string, command?: string): string[] => {
}
}
if (command === KeyBindingCommand.InsertBlankLineCommand && keyStr === 'Ctrl-Enter') {
if (keyBindingKey === KeyBindingKey.InsertBlankLineKeyBindingKey && keyStr === 'Ctrl-Enter') {
if (systemStore.isMacOS) {
return ['⌘', 'Enter']; // macOS: Cmd+Enter
} else {
@@ -98,7 +98,7 @@ const parseKeyBinding = (keyStr: string, command?: string): string[] => {
}
}
if (command === KeyBindingCommand.SelectLineCommand && keyStr === 'Alt-l') {
if (keyBindingKey === KeyBindingKey.SelectLineKeyBindingKey && keyStr === 'Alt-l') {
if (systemStore.isMacOS) {
return ['Ctrl', 'L']; // macOS: Ctrl+l
} else {
@@ -106,7 +106,7 @@ const parseKeyBinding = (keyStr: string, command?: string): string[] => {
}
}
if (command === KeyBindingCommand.SelectParentSyntaxCommand && keyStr === 'Ctrl-i') {
if (keyBindingKey === KeyBindingKey.SelectParentSyntaxKeyBindingKey && keyStr === 'Ctrl-i') {
if (systemStore.isMacOS) {
return ['⌘', 'I']; // macOS: Cmd+i
} else {
@@ -114,7 +114,7 @@ const parseKeyBinding = (keyStr: string, command?: string): string[] => {
}
}
if (command === KeyBindingCommand.IndentLessCommand && keyStr === 'Ctrl-[') {
if (keyBindingKey === KeyBindingKey.IndentLessKeyBindingKey && keyStr === 'Ctrl-[') {
if (systemStore.isMacOS) {
return ['⌘', '[']; // macOS: Cmd+[
} else {
@@ -122,7 +122,7 @@ const parseKeyBinding = (keyStr: string, command?: string): string[] => {
}
}
if (command === KeyBindingCommand.IndentMoreCommand && keyStr === 'Ctrl-]') {
if (keyBindingKey === KeyBindingKey.IndentMoreKeyBindingKey && keyStr === 'Ctrl-]') {
if (systemStore.isMacOS) {
return ['⌘', ']']; // macOS: Cmd+]
} else {
@@ -130,7 +130,7 @@ const parseKeyBinding = (keyStr: string, command?: string): string[] => {
}
}
if (command === KeyBindingCommand.IndentSelectionCommand && keyStr === 'Ctrl-Alt-\\') {
if (keyBindingKey === KeyBindingKey.IndentSelectionKeyBindingKey && keyStr === 'Ctrl-Alt-\\') {
if (systemStore.isMacOS) {
return ['⌘', '⌥', '\\']; // macOS: Cmd+Alt+\
} else {
@@ -138,7 +138,7 @@ const parseKeyBinding = (keyStr: string, command?: string): string[] => {
}
}
if (command === KeyBindingCommand.CursorMatchingBracketCommand && keyStr === 'Shift-Ctrl-\\') {
if (keyBindingKey === KeyBindingKey.CursorMatchingBracketKeyBindingKey && keyStr === 'Shift-Ctrl-\\') {
if (systemStore.isMacOS) {
return ['⇧', '⌘', '\\']; // macOS: Shift+Cmd+\
} else {
@@ -146,7 +146,7 @@ const parseKeyBinding = (keyStr: string, command?: string): string[] => {
}
}
if (command === KeyBindingCommand.ToggleCommentCommand && keyStr === 'Ctrl-/') {
if (keyBindingKey === KeyBindingKey.ToggleCommentKeyBindingKey && keyStr === 'Ctrl-/') {
if (systemStore.isMacOS) {
return ['⌘', '/']; // macOS: Cmd+/
} else {
@@ -155,7 +155,7 @@ const parseKeyBinding = (keyStr: string, command?: string): string[] => {
}
// 特殊处理删除快捷键的操作系统差异
if (command === KeyBindingCommand.DeleteGroupBackwardCommand && keyStr === 'Ctrl-Backspace') {
if (keyBindingKey === KeyBindingKey.DeleteGroupBackwardKeyBindingKey && keyStr === 'Ctrl-Backspace') {
if (systemStore.isMacOS) {
return ['⌘', 'Backspace']; // macOS: Cmd+Backspace
} else {
@@ -163,7 +163,7 @@ const parseKeyBinding = (keyStr: string, command?: string): string[] => {
}
}
if (command === KeyBindingCommand.DeleteGroupForwardCommand && keyStr === 'Ctrl-Delete') {
if (keyBindingKey === KeyBindingKey.DeleteGroupForwardKeyBindingKey && keyStr === 'Ctrl-Delete') {
if (systemStore.isMacOS) {
return ['⌘', 'Delete']; // macOS: Cmd+Delete
} else {