✨ Complete multi-document mode
This commit is contained in:
1
frontend/components.d.ts
vendored
1
frontend/components.d.ts
vendored
@@ -9,6 +9,7 @@ export {}
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
BlockLanguageSelector: typeof import('./src/components/toolbar/BlockLanguageSelector.vue')['default']
|
||||
DocumentSelector: typeof import('./src/components/toolbar/DocumentSelector.vue')['default']
|
||||
LinuxTitleBar: typeof import('./src/components/titlebar/LinuxTitleBar.vue')['default']
|
||||
MacOSTitleBar: typeof import('./src/components/titlebar/MacOSTitleBar.vue')['default']
|
||||
MemoryMonitor: typeof import('./src/components/monitor/MemoryMonitor.vue')['default']
|
||||
|
52
frontend/package-lock.json
generated
52
frontend/package-lock.json
generated
@@ -40,8 +40,6 @@
|
||||
"@codemirror/view": "^6.37.2",
|
||||
"@lezer/highlight": "^1.2.1",
|
||||
"@lezer/lr": "^1.4.2",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"@vueuse/core": "^13.3.0",
|
||||
"codemirror": "^6.0.1",
|
||||
"codemirror-lang-elixir": "^4.0.0",
|
||||
"colors-named": "^1.0.2",
|
||||
@@ -2103,18 +2101,6 @@
|
||||
"undici-types": "~7.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/uuid": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/@types/uuid/-/uuid-10.0.0.tgz",
|
||||
"integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/web-bluetooth": {
|
||||
"version": "0.0.21",
|
||||
"resolved": "https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz",
|
||||
"integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.34.1",
|
||||
"resolved": "https://registry.npmmirror.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.34.1.tgz",
|
||||
@@ -2610,44 +2596,6 @@
|
||||
"integrity": "sha512-CabR+UN630VnsJO/jHWYBC1YVXyMq94KKp6iF5MQgZJs5I8cmjw6oVMO1oDbtBkENSHSSn/UadWlW/OAgdmKrg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@vueuse/core": {
|
||||
"version": "13.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/@vueuse/core/-/core-13.3.0.tgz",
|
||||
"integrity": "sha512-uYRz5oEfebHCoRhK4moXFM3NSCd5vu2XMLOq/Riz5FdqZMy2RvBtazdtL3gEcmDyqkztDe9ZP/zymObMIbiYSg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/web-bluetooth": "^0.0.21",
|
||||
"@vueuse/metadata": "13.3.0",
|
||||
"@vueuse/shared": "13.3.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "^3.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vueuse/metadata": {
|
||||
"version": "13.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/@vueuse/metadata/-/metadata-13.3.0.tgz",
|
||||
"integrity": "sha512-42IzJIOYCKIb0Yjv1JfaKpx8JlCiTmtCWrPxt7Ja6Wzoq0h79+YVXmBV03N966KEmDEESTbp5R/qO3AB5BDnGw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
}
|
||||
},
|
||||
"node_modules/@vueuse/shared": {
|
||||
"version": "13.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/@vueuse/shared/-/shared-13.3.0.tgz",
|
||||
"integrity": "sha512-L1QKsF0Eg9tiZSFXTgodYnu0Rsa2P0En2LuLrIs/jgrkyiDuJSsPZK+tx+wU0mMsYHUYEjNsuE41uqqkuR8VhA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "^3.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@wailsio/runtime": {
|
||||
"version": "3.0.0-alpha.66",
|
||||
"resolved": "https://registry.npmmirror.com/@wailsio/runtime/-/runtime-3.0.0-alpha.66.tgz",
|
||||
|
@@ -44,8 +44,6 @@
|
||||
"@codemirror/view": "^6.37.2",
|
||||
"@lezer/highlight": "^1.2.1",
|
||||
"@lezer/lr": "^1.4.2",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"@vueuse/core": "^13.3.0",
|
||||
"codemirror": "^6.0.1",
|
||||
"codemirror-lang-elixir": "^4.0.0",
|
||||
"colors-named": "^1.0.2",
|
||||
|
@@ -23,6 +23,11 @@
|
||||
--dark-scrollbar-track: #2a2a2a;
|
||||
--dark-scrollbar-thumb: #555555;
|
||||
--dark-scrollbar-thumb-hover: #666666;
|
||||
--dark-selection-bg: rgba(181, 206, 168, 0.1);
|
||||
--dark-selection-text: #b5cea8;
|
||||
--dark-danger-color: #ff6b6b;
|
||||
--dark-bg-primary: #1a1a1a;
|
||||
--dark-bg-hover: #2a2a2a;
|
||||
|
||||
/* 浅色主题颜色变量 */
|
||||
--light-toolbar-bg: #f8f9fa;
|
||||
@@ -45,6 +50,11 @@
|
||||
--light-scrollbar-track: #f1f3f4;
|
||||
--light-scrollbar-thumb: #c1c1c1;
|
||||
--light-scrollbar-thumb-hover: #a8a8a8;
|
||||
--light-selection-bg: rgba(59, 130, 246, 0.15);
|
||||
--light-selection-text: #2563eb;
|
||||
--light-danger-color: #dc3545;
|
||||
--light-bg-primary: #ffffff;
|
||||
--light-bg-hover: #f1f3f4;
|
||||
|
||||
/* 默认使用深色主题 */
|
||||
--toolbar-bg: var(--dark-toolbar-bg);
|
||||
@@ -68,6 +78,11 @@
|
||||
--scrollbar-track: var(--dark-scrollbar-track);
|
||||
--scrollbar-thumb: var(--dark-scrollbar-thumb);
|
||||
--scrollbar-thumb-hover: var(--dark-scrollbar-thumb-hover);
|
||||
--selection-bg: var(--dark-selection-bg);
|
||||
--selection-text: var(--dark-selection-text);
|
||||
--text-danger: var(--dark-danger-color);
|
||||
--bg-primary: var(--dark-bg-primary);
|
||||
--bg-hover: var(--dark-bg-hover);
|
||||
|
||||
color-scheme: light dark;
|
||||
}
|
||||
@@ -96,6 +111,11 @@
|
||||
--scrollbar-track: var(--dark-scrollbar-track);
|
||||
--scrollbar-thumb: var(--dark-scrollbar-thumb);
|
||||
--scrollbar-thumb-hover: var(--dark-scrollbar-thumb-hover);
|
||||
--selection-bg: var(--dark-selection-bg);
|
||||
--selection-text: var(--dark-selection-text);
|
||||
--text-danger: var(--dark-danger-color);
|
||||
--bg-primary: var(--dark-bg-primary);
|
||||
--bg-hover: var(--dark-bg-hover);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,6 +143,11 @@
|
||||
--scrollbar-track: var(--light-scrollbar-track);
|
||||
--scrollbar-thumb: var(--light-scrollbar-thumb);
|
||||
--scrollbar-thumb-hover: var(--light-scrollbar-thumb-hover);
|
||||
--selection-bg: var(--light-selection-bg);
|
||||
--selection-text: var(--light-selection-text);
|
||||
--text-danger: var(--light-danger-color);
|
||||
--bg-primary: var(--light-bg-primary);
|
||||
--bg-hover: var(--light-bg-hover);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,6 +174,11 @@
|
||||
--scrollbar-track: var(--light-scrollbar-track);
|
||||
--scrollbar-thumb: var(--light-scrollbar-thumb);
|
||||
--scrollbar-thumb-hover: var(--light-scrollbar-thumb-hover);
|
||||
--selection-bg: var(--light-selection-bg);
|
||||
--selection-text: var(--light-selection-text);
|
||||
--text-danger: var(--light-danger-color);
|
||||
--bg-primary: var(--light-bg-primary);
|
||||
--bg-hover: var(--light-bg-hover);
|
||||
}
|
||||
|
||||
/* 手动选择深色主题 */
|
||||
@@ -174,4 +204,7 @@
|
||||
--scrollbar-track: var(--dark-scrollbar-track);
|
||||
--scrollbar-thumb: var(--dark-scrollbar-thumb);
|
||||
--scrollbar-thumb-hover: var(--dark-scrollbar-thumb-hover);
|
||||
--selection-bg: var(--dark-selection-bg);
|
||||
--selection-text: var(--dark-selection-text);
|
||||
--text-danger: var(--dark-danger-color);
|
||||
}
|
@@ -505,11 +505,12 @@ const scrollToCurrentLanguage = () => {
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: rgba(181, 206, 168, 0.1);
|
||||
color: #b5cea8;
|
||||
background-color: var(--selection-bg);
|
||||
color: var(--selection-text);
|
||||
|
||||
.language-alias {
|
||||
color: rgba(181, 206, 168, 0.7);
|
||||
color: var(--selection-text);
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
|
666
frontend/src/components/toolbar/DocumentSelector.vue
Normal file
666
frontend/src/components/toolbar/DocumentSelector.vue
Normal file
@@ -0,0 +1,666 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue';
|
||||
import { useDocumentStore } from '@/stores/documentStore';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import type { Document } from '@/../bindings/voidraft/internal/models/models';
|
||||
|
||||
const documentStore = useDocumentStore();
|
||||
const { t } = useI18n();
|
||||
|
||||
// 组件状态
|
||||
const showMenu = ref(false);
|
||||
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 filteredItems = computed(() => {
|
||||
const docs = documentStore.documentList;
|
||||
const query = inputValue.value.trim();
|
||||
|
||||
if (!query) {
|
||||
return docs;
|
||||
}
|
||||
|
||||
// 过滤匹配的文档
|
||||
const filtered = docs.filter(doc =>
|
||||
doc.title.toLowerCase().includes(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,
|
||||
...filtered
|
||||
];
|
||||
}
|
||||
|
||||
return filtered;
|
||||
});
|
||||
|
||||
// 当前文档显示名称
|
||||
const currentDocName = computed(() => {
|
||||
if (!documentStore.currentDocument) return t('toolbar.selectDocument');
|
||||
const title = documentStore.currentDocument.title;
|
||||
return title.length > 12 ? title.substring(0, 12) + '...' : title;
|
||||
});
|
||||
|
||||
// 打开菜单
|
||||
const openMenu = async () => {
|
||||
showMenu.value = true;
|
||||
await documentStore.updateDocuments();
|
||||
nextTick(() => {
|
||||
inputRef.value?.focus();
|
||||
});
|
||||
};
|
||||
|
||||
// 关闭菜单
|
||||
const closeMenu = () => {
|
||||
showMenu.value = false;
|
||||
inputValue.value = '';
|
||||
editingId.value = null;
|
||||
editingTitle.value = '';
|
||||
deleteConfirmId.value = null;
|
||||
};
|
||||
|
||||
// 切换菜单
|
||||
const toggleMenu = () => {
|
||||
if (showMenu.value) {
|
||||
closeMenu();
|
||||
} else {
|
||||
openMenu();
|
||||
}
|
||||
};
|
||||
|
||||
// 选择文档或创建文档
|
||||
const selectItem = async (item: any) => {
|
||||
if (item.isCreateOption) {
|
||||
// 创建新文档
|
||||
await createDoc(inputValue.value.trim());
|
||||
} else {
|
||||
// 选择现有文档
|
||||
await selectDoc(item);
|
||||
}
|
||||
};
|
||||
|
||||
// 选择文档
|
||||
const selectDoc = async (doc: Document) => {
|
||||
try {
|
||||
const success = await documentStore.openDocument(doc.id);
|
||||
if (success) {
|
||||
closeMenu();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('切换文档失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 文档名称长度限制
|
||||
const MAX_TITLE_LENGTH = 50;
|
||||
|
||||
// 验证文档名称
|
||||
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 createDoc = async (title: string) => {
|
||||
const trimmedTitle = title.trim();
|
||||
const error = validateTitle(trimmedTitle);
|
||||
if (error) {
|
||||
console.error('创建文档失败:', error);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const newDoc = await documentStore.createNewDocument(trimmedTitle);
|
||||
if (newDoc) {
|
||||
await selectDoc(newDoc);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('创建文档失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 开始重命名
|
||||
const startRename = (doc: Document, event: Event) => {
|
||||
event.stopPropagation();
|
||||
editingId.value = doc.id;
|
||||
editingTitle.value = doc.title;
|
||||
deleteConfirmId.value = null; // 清除删除确认状态
|
||||
nextTick(() => {
|
||||
editInputRef.value?.focus();
|
||||
editInputRef.value?.select();
|
||||
});
|
||||
};
|
||||
|
||||
// 保存编辑
|
||||
const saveEdit = async () => {
|
||||
if (editingId.value && editingTitle.value.trim()) {
|
||||
const trimmedTitle = editingTitle.value.trim();
|
||||
const error = validateTitle(trimmedTitle);
|
||||
if (error) {
|
||||
console.error('保存失败:', error);
|
||||
// 保持编辑状态,不清除
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await documentStore.updateDocumentMetadata(editingId.value, trimmedTitle);
|
||||
await documentStore.updateDocuments();
|
||||
} catch (error) {
|
||||
console.error('保存失败:', error);
|
||||
// 保持编辑状态,不清除
|
||||
return;
|
||||
}
|
||||
}
|
||||
editingId.value = null;
|
||||
editingTitle.value = '';
|
||||
};
|
||||
|
||||
// 处理删除 - 简化确认机制
|
||||
const handleDelete = async (doc: Document, event: Event) => {
|
||||
event.stopPropagation();
|
||||
|
||||
if (deleteConfirmId.value === doc.id) {
|
||||
// 确认删除
|
||||
try {
|
||||
await documentStore.deleteDocument(doc.id);
|
||||
await documentStore.updateDocuments();
|
||||
|
||||
// 如果删除的是当前文档,切换到第一个文档
|
||||
if (documentStore.currentDocument?.id === doc.id && documentStore.documentList.length > 0) {
|
||||
const firstDoc = documentStore.documentList[0];
|
||||
if (firstDoc) {
|
||||
await selectDoc(firstDoc);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除失败:', error);
|
||||
}
|
||||
deleteConfirmId.value = null;
|
||||
} else {
|
||||
// 进入确认状态
|
||||
deleteConfirmId.value = doc.id;
|
||||
editingId.value = null; // 清除编辑状态
|
||||
|
||||
// 3秒后自动取消确认状态
|
||||
setTimeout(() => {
|
||||
if (deleteConfirmId.value === doc.id) {
|
||||
deleteConfirmId.value = null;
|
||||
}
|
||||
}, 3000);
|
||||
}
|
||||
};
|
||||
|
||||
// 格式化时间
|
||||
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 (error) {
|
||||
return t('toolbar.timeError');
|
||||
}
|
||||
};
|
||||
|
||||
// 键盘事件
|
||||
const handleKeydown = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape') {
|
||||
if (editingId.value) {
|
||||
editingId.value = null;
|
||||
editingTitle.value = '';
|
||||
} else if (deleteConfirmId.value) {
|
||||
deleteConfirmId.value = null;
|
||||
} else {
|
||||
closeMenu();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 输入框键盘事件
|
||||
const handleInputKeydown = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
const query = inputValue.value.trim();
|
||||
if (query) {
|
||||
// 如果有匹配的项目,选择第一个
|
||||
if (filteredItems.value.length > 0) {
|
||||
selectItem(filteredItems.value[0]);
|
||||
}
|
||||
}
|
||||
} else if (event.key === 'Escape') {
|
||||
event.preventDefault();
|
||||
closeMenu();
|
||||
}
|
||||
event.stopPropagation();
|
||||
};
|
||||
|
||||
// 编辑键盘事件
|
||||
const handleEditKeydown = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
saveEdit();
|
||||
} else if (event.key === 'Escape') {
|
||||
event.preventDefault();
|
||||
editingId.value = null;
|
||||
editingTitle.value = '';
|
||||
}
|
||||
event.stopPropagation();
|
||||
};
|
||||
|
||||
// 点击外部关闭
|
||||
const handleClickOutside = (event: Event) => {
|
||||
const target = event.target as HTMLElement;
|
||||
if (!target.closest('.document-selector')) {
|
||||
closeMenu();
|
||||
}
|
||||
};
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
document.addEventListener('click', handleClickOutside);
|
||||
document.addEventListener('keydown', handleKeydown);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('click', handleClickOutside);
|
||||
document.removeEventListener('keydown', handleKeydown);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="document-selector">
|
||||
<!-- 选择器按钮 -->
|
||||
<button class="doc-btn" @click="toggleMenu">
|
||||
<span class="doc-name">{{ currentDocName }}</span>
|
||||
<span class="arrow" :class="{ open: showMenu }">▼</span>
|
||||
</button>
|
||||
|
||||
<!-- 菜单 -->
|
||||
<div v-if="showMenu" 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>
|
||||
|
||||
<!-- 项目列表 -->
|
||||
<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 class="doc-date">{{ formatTime(item.updatedAt) }}</div>
|
||||
</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-if="editingId !== item.id" class="doc-actions">
|
||||
<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>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.document-selector {
|
||||
position: relative;
|
||||
|
||||
.doc-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
font-size: 11px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
padding: 2px 4px;
|
||||
border-radius: 3px;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--border-color);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.doc-name {
|
||||
max-width: 80px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
font-size: 8px;
|
||||
margin-left: 2px;
|
||||
transition: transform 0.2s ease;
|
||||
|
||||
&.open {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.doc-menu {
|
||||
position: absolute;
|
||||
bottom: 100%;
|
||||
right: 0;
|
||||
background-color: var(--bg-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 3px;
|
||||
margin-bottom: 4px;
|
||||
width: 260px;
|
||||
max-height: 320px;
|
||||
z-index: 1000;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
||||
overflow: hidden;
|
||||
|
||||
.input-box {
|
||||
position: relative;
|
||||
padding: 8px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
|
||||
.main-input {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
background-color: var(--bg-primary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 2px;
|
||||
padding: 5px 8px 5px 26px;
|
||||
font-size: 11px;
|
||||
color: var(--text-primary);
|
||||
outline: none;
|
||||
|
||||
&:focus {
|
||||
border-color: var(--text-muted);
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
}
|
||||
|
||||
.input-icon {
|
||||
position: absolute;
|
||||
left: 14px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: var(--text-muted);
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.item-list {
|
||||
max-height: 240px;
|
||||
overflow-y: auto;
|
||||
|
||||
.list-item {
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--bg-hover);
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: var(--selection-bg);
|
||||
|
||||
.doc-item-content .doc-info {
|
||||
.doc-title {
|
||||
color: var(--selection-text);
|
||||
}
|
||||
|
||||
.doc-date {
|
||||
color: var(--selection-text);
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.create-item {
|
||||
.create-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 8px;
|
||||
font-size: 11px;
|
||||
font-weight: normal;
|
||||
|
||||
svg {
|
||||
flex-shrink: 0;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.doc-item-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 8px;
|
||||
|
||||
.doc-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
.doc-title {
|
||||
font-size: 12px;
|
||||
margin-bottom: 2px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.doc-date {
|
||||
font-size: 10px;
|
||||
color: var(--text-muted);
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
.doc-edit {
|
||||
flex: 1;
|
||||
|
||||
.edit-input {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
background-color: var(--bg-primary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 2px;
|
||||
padding: 4px 6px;
|
||||
font-size: 11px;
|
||||
color: var(--text-primary);
|
||||
outline: none;
|
||||
|
||||
&:focus {
|
||||
border-color: var(--text-muted);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.doc-actions {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
|
||||
.action-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
border-radius: 2px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 20px;
|
||||
min-height: 20px;
|
||||
|
||||
svg {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--border-color);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
&.delete-btn:hover {
|
||||
color: var(--text-danger);
|
||||
}
|
||||
|
||||
&.delete-confirm {
|
||||
background-color: var(--text-danger);
|
||||
color: white;
|
||||
|
||||
.confirm-text {
|
||||
font-size: 10px;
|
||||
padding: 0 4px;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--text-danger);
|
||||
color: white !important; // 确保确认状态下文字始终为白色
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover .doc-actions {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.empty, .loading {
|
||||
padding: 12px 8px;
|
||||
text-align: center;
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 自定义滚动条
|
||||
.item-list {
|
||||
&::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: var(--border-color);
|
||||
border-radius: 2px;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--text-muted);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -7,6 +7,7 @@ import {useUpdateStore} from '@/stores/updateStore';
|
||||
import * as runtime from '@wailsio/runtime';
|
||||
import {useRouter} from 'vue-router';
|
||||
import BlockLanguageSelector from './BlockLanguageSelector.vue';
|
||||
import DocumentSelector from './DocumentSelector.vue';
|
||||
import {getActiveNoteBlock} from '@/views/editor/extensions/codeblock/state';
|
||||
import {getLanguage} from '@/views/editor/extensions/codeblock/lang-parser/languages';
|
||||
|
||||
@@ -95,7 +96,7 @@ watch(
|
||||
{immediate: true}
|
||||
);
|
||||
|
||||
// 定期更新格式化按钮状态(作为备用机制)
|
||||
// 定期更新格式化按钮状态
|
||||
let formatButtonUpdateTimer: number | null = null;
|
||||
|
||||
const isLoaded = ref(false);
|
||||
@@ -158,6 +159,9 @@ watch(isLoaded, async (newLoaded) => {
|
||||
{{ configStore.config.editing.fontSize }}px
|
||||
</span>
|
||||
|
||||
<!-- 文档选择器 -->
|
||||
<DocumentSelector/>
|
||||
|
||||
<!-- 块语言选择器 -->
|
||||
<BlockLanguageSelector/>
|
||||
|
||||
|
@@ -1,4 +1,5 @@
|
||||
export default {
|
||||
locale: 'en-US',
|
||||
titlebar: {
|
||||
minimize: 'Minimize',
|
||||
maximize: 'Maximize',
|
||||
@@ -18,6 +19,23 @@ export default {
|
||||
searchLanguage: 'Search language...',
|
||||
noLanguageFound: 'No language found',
|
||||
formatHint: 'Current block supports formatting, use Ctrl+Shift+F shortcut for formatting',
|
||||
// Document selector
|
||||
selectDocument: 'Select Document',
|
||||
searchOrCreateDocument: 'Search or enter new document name...',
|
||||
createDocument: 'Create',
|
||||
noDocumentFound: 'No document found',
|
||||
loading: 'Loading...',
|
||||
rename: 'Rename',
|
||||
delete: 'Delete',
|
||||
confirm: 'Confirm',
|
||||
confirmDelete: 'Click again to confirm delete',
|
||||
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',
|
||||
|
@@ -1,4 +1,5 @@
|
||||
export default {
|
||||
locale: 'zh-CN',
|
||||
titlebar: {
|
||||
minimize: '最小化',
|
||||
maximize: '最大化',
|
||||
@@ -18,6 +19,23 @@ export default {
|
||||
searchLanguage: '搜索语言...',
|
||||
noLanguageFound: '未找到匹配的语言',
|
||||
formatHint: '当前区块支持格式化,使用 Ctrl+Shift+F 进行格式化',
|
||||
// 文档选择器
|
||||
selectDocument: '选择文档',
|
||||
searchOrCreateDocument: '搜索或输入新文档名...',
|
||||
createDocument: '创建',
|
||||
noDocumentFound: '没有找到文档',
|
||||
loading: '加载中...',
|
||||
rename: '重命名',
|
||||
delete: '删除',
|
||||
confirm: '确认',
|
||||
confirmDelete: '再次点击确认删除',
|
||||
documentNameTooLong: '文档名称不能超过{max}个字符',
|
||||
documentNameRequired: '文档名称不能为空',
|
||||
cannotDeleteLastDocument: '无法删除最后一个文档',
|
||||
cannotDeleteDefaultDocument: '无法删除默认文档',
|
||||
unknownTime: '未知时间',
|
||||
invalidDate: '无效日期',
|
||||
timeError: '时间错误',
|
||||
},
|
||||
languages: {
|
||||
'zh-CN': '简体中文',
|
||||
|
@@ -3,114 +3,222 @@ import {computed, ref} from 'vue';
|
||||
import {DocumentService} from '@/../bindings/voidraft/internal/services';
|
||||
import {Document} from '@/../bindings/voidraft/internal/models/models';
|
||||
|
||||
const SCRATCH_DOCUMENT_ID = 1; // 默认草稿文档ID
|
||||
|
||||
export const useDocumentStore = defineStore('document', () => {
|
||||
// 状态
|
||||
// === 核心状态 ===
|
||||
const documents = ref<Record<number, Document>>({});
|
||||
const recentDocumentIds = ref<number[]>([SCRATCH_DOCUMENT_ID]);
|
||||
const currentDocumentId = ref<number | null>(null);
|
||||
const currentDocument = ref<Document | null>(null);
|
||||
|
||||
// === UI状态 ===
|
||||
const showDocumentSelector = ref(false);
|
||||
const isLoading = ref(false);
|
||||
const isSaving = ref(false);
|
||||
const lastSaved = ref<Date | null>(null);
|
||||
|
||||
// 计算属性
|
||||
const documentContent = computed(() => currentDocument.value?.content ?? '');
|
||||
const documentTitle = computed(() => currentDocument.value?.title ?? '');
|
||||
const hasDocument = computed(() => !!currentDocument.value);
|
||||
const isSaveInProgress = computed(() => isSaving.value);
|
||||
const lastSavedTime = computed(() => lastSaved.value);
|
||||
// === 计算属性 ===
|
||||
const documentList = computed(() =>
|
||||
Object.values(documents.value).sort((a, b) => {
|
||||
const aIndex = recentDocumentIds.value.indexOf(a.id);
|
||||
const bIndex = recentDocumentIds.value.indexOf(b.id);
|
||||
|
||||
// 加载文档
|
||||
const loadDocument = async (documentId = 1): Promise<Document | null> => {
|
||||
if (isLoading.value) return currentDocument.value;
|
||||
// 按最近使用排序
|
||||
if (aIndex !== -1 && bIndex !== -1) {
|
||||
return aIndex - bIndex;
|
||||
}
|
||||
if (aIndex !== -1) return -1;
|
||||
if (bIndex !== -1) return 1;
|
||||
|
||||
isLoading.value = true;
|
||||
// 然后按更新时间排序
|
||||
return new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime();
|
||||
})
|
||||
);
|
||||
|
||||
// === 私有方法 ===
|
||||
const addRecentDocument = (docId: number) => {
|
||||
const recent = recentDocumentIds.value.filter(id => id !== docId);
|
||||
recent.unshift(docId);
|
||||
recentDocumentIds.value = recent.slice(0, 100); // 保留最近100个
|
||||
};
|
||||
|
||||
const setDocuments = (docs: Document[]) => {
|
||||
documents.value = {};
|
||||
docs.forEach(doc => {
|
||||
documents.value[doc.id] = doc;
|
||||
});
|
||||
};
|
||||
|
||||
// === 公共API ===
|
||||
|
||||
// 更新文档列表
|
||||
const updateDocuments = async () => {
|
||||
try {
|
||||
const doc = await DocumentService.GetDocumentByID(documentId);
|
||||
const docs = await DocumentService.ListAllDocumentsMeta();
|
||||
if (docs) {
|
||||
setDocuments(docs.filter((doc): doc is Document => doc !== null));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to update documents:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 打开文档
|
||||
const openDocument = async (docId: number): Promise<boolean> => {
|
||||
try {
|
||||
closeDialog();
|
||||
|
||||
// 获取完整文档数据
|
||||
const doc = await DocumentService.GetDocumentByID(docId);
|
||||
if (!doc) {
|
||||
throw new Error(`Document ${docId} not found`);
|
||||
}
|
||||
|
||||
currentDocumentId.value = docId;
|
||||
currentDocument.value = doc;
|
||||
addRecentDocument(docId);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Failed to open document:', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// 创建新文档
|
||||
const createNewDocument = async (title: string): Promise<Document | null> => {
|
||||
try {
|
||||
const newDoc = await DocumentService.CreateDocument(title);
|
||||
if (!newDoc) {
|
||||
throw new Error('Failed to create document');
|
||||
}
|
||||
|
||||
// 更新文档列表
|
||||
documents.value[newDoc.id] = newDoc;
|
||||
|
||||
return newDoc;
|
||||
} catch (error) {
|
||||
console.error('Failed to create document:', error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// 保存新文档
|
||||
const saveNewDocument = async (title: string, content: string): Promise<boolean> => {
|
||||
try {
|
||||
const newDoc = await createNewDocument(title);
|
||||
if (!newDoc) return false;
|
||||
|
||||
// 更新内容
|
||||
await DocumentService.UpdateDocumentContent(newDoc.id, content);
|
||||
newDoc.content = content;
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Failed to save new document:', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// 更新文档元数据
|
||||
const updateDocumentMetadata = async (docId: number, title: string, newPath?: string): Promise<boolean> => {
|
||||
try {
|
||||
await DocumentService.UpdateDocumentTitle(docId, title);
|
||||
|
||||
// 更新本地状态
|
||||
const doc = documents.value[docId];
|
||||
if (doc) {
|
||||
currentDocument.value = doc;
|
||||
return doc;
|
||||
doc.title = title;
|
||||
doc.updatedAt = new Date();
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
return null;
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 保存文档内容
|
||||
const saveDocumentContent = async (content: string): Promise<boolean> => {
|
||||
// 如果内容没有变化,直接返回成功
|
||||
if (currentDocument.value?.content === content) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 如果正在保存中,直接返回
|
||||
if (isSaving.value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
isSaving.value = true;
|
||||
try {
|
||||
const documentId = currentDocument.value?.id || 1;
|
||||
await DocumentService.UpdateDocumentContent(documentId, content);
|
||||
|
||||
const now = new Date();
|
||||
lastSaved.value = now;
|
||||
|
||||
// 更新本地副本
|
||||
if (currentDocument.value) {
|
||||
currentDocument.value.content = content;
|
||||
currentDocument.value.updatedAt = now;
|
||||
if (currentDocument.value?.id === docId) {
|
||||
currentDocument.value.title = title;
|
||||
currentDocument.value.updatedAt = new Date();
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Failed to update document metadata:', error);
|
||||
return false;
|
||||
} finally {
|
||||
isSaving.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 保存文档标题
|
||||
const saveDocumentTitle = async (title: string): Promise<boolean> => {
|
||||
if (!currentDocument.value || currentDocument.value.title === title) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 删除文档
|
||||
const deleteDocument = async (docId: number): Promise<boolean> => {
|
||||
try {
|
||||
await DocumentService.UpdateDocumentTitle(currentDocument.value.id, title);
|
||||
const now = new Date();
|
||||
lastSaved.value = now;
|
||||
currentDocument.value.title = title;
|
||||
currentDocument.value.updatedAt = now;
|
||||
// 检查是否是默认文档(使用ID判断)
|
||||
if (docId === SCRATCH_DOCUMENT_ID) {
|
||||
return false;
|
||||
}
|
||||
|
||||
await DocumentService.DeleteDocument(docId);
|
||||
|
||||
// 更新本地状态
|
||||
delete documents.value[docId];
|
||||
recentDocumentIds.value = recentDocumentIds.value.filter(id => id !== docId);
|
||||
|
||||
// 如果删除的是当前文档,切换到第一个可用文档
|
||||
if (currentDocumentId.value === docId) {
|
||||
const availableDocs = Object.values(documents.value);
|
||||
if (availableDocs.length > 0) {
|
||||
await openDocument(availableDocs[0].id);
|
||||
} else {
|
||||
currentDocumentId.value = null;
|
||||
currentDocument.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Failed to delete document:', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// 初始化
|
||||
// === UI控制 ===
|
||||
const openDocumentSelector = () => {
|
||||
closeDialog();
|
||||
showDocumentSelector.value = true;
|
||||
};
|
||||
|
||||
const closeDialog = () => {
|
||||
showDocumentSelector.value = false;
|
||||
};
|
||||
|
||||
// === 初始化 ===
|
||||
const initialize = async (): Promise<void> => {
|
||||
await loadDocument();
|
||||
try {
|
||||
await updateDocuments();
|
||||
|
||||
// 获取第一个文档ID并打开
|
||||
const firstDocId = await DocumentService.GetFirstDocumentID();
|
||||
if (firstDocId && documents.value[firstDocId]) {
|
||||
await openDocument(firstDocId);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize document store:', error);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
// 状态
|
||||
documents,
|
||||
documentList,
|
||||
recentDocumentIds,
|
||||
currentDocumentId,
|
||||
currentDocument,
|
||||
showDocumentSelector,
|
||||
isLoading,
|
||||
isSaving,
|
||||
lastSaved,
|
||||
|
||||
// 计算属性
|
||||
documentContent,
|
||||
documentTitle,
|
||||
hasDocument,
|
||||
isSaveInProgress,
|
||||
lastSavedTime,
|
||||
|
||||
// 方法
|
||||
loadDocument,
|
||||
saveDocumentContent,
|
||||
saveDocumentTitle,
|
||||
initialize
|
||||
updateDocuments,
|
||||
openDocument,
|
||||
createNewDocument,
|
||||
saveNewDocument,
|
||||
updateDocumentMetadata,
|
||||
deleteDocument,
|
||||
openDocumentSelector,
|
||||
closeDialog,
|
||||
initialize,
|
||||
};
|
||||
});
|
@@ -1,24 +1,26 @@
|
||||
import {defineStore} from 'pinia';
|
||||
import {ref, watch} from 'vue';
|
||||
import {ref, watch, nextTick} from 'vue';
|
||||
import {EditorView} from '@codemirror/view';
|
||||
import {EditorState, Extension} from '@codemirror/state';
|
||||
import {useConfigStore} from './configStore';
|
||||
import {useDocumentStore} from './documentStore';
|
||||
import {useThemeStore} from './themeStore';
|
||||
import {SystemThemeType} from '@/../bindings/voidraft/internal/models/models';
|
||||
import {ExtensionService} from '@/../bindings/voidraft/internal/services';
|
||||
import {SystemThemeType, ExtensionID} from '@/../bindings/voidraft/internal/models/models';
|
||||
import {DocumentService} from '@/../bindings/voidraft/internal/services';
|
||||
import {ensureSyntaxTree} from "@codemirror/language"
|
||||
import {createBasicSetup} from '@/views/editor/basic/basicSetup';
|
||||
import {createThemeExtension, updateEditorTheme} from '@/views/editor/basic/themeExtension';
|
||||
import {getTabExtensions, updateTabConfig} from '@/views/editor/basic/tabExtension';
|
||||
import {createFontExtensionFromBackend, updateFontConfig} from '@/views/editor/basic/fontExtension';
|
||||
import {createStatsUpdateExtension} from '@/views/editor/basic/statsExtension';
|
||||
import {createAutoSavePlugin} from '@/views/editor/basic/autoSaveExtension';
|
||||
import {createContentChangePlugin} from '@/views/editor/basic/contentChangeExtension';
|
||||
import {createDynamicKeymapExtension, updateKeymapExtension} from '@/views/editor/keymap';
|
||||
import {createDynamicExtensions, getExtensionManager, setExtensionManagerView} from '@/views/editor/manager';
|
||||
import {useExtensionStore} from './extensionStore';
|
||||
import {ExtensionService} from '@/../bindings/voidraft/internal/services';
|
||||
import createCodeBlockExtension from "@/views/editor/extensions/codeblock";
|
||||
import {triggerFontChange} from '@/views/editor/extensions/checkbox';
|
||||
|
||||
const NUM_EDITOR_INSTANCES = 5; // 最多缓存5个编辑器实例
|
||||
|
||||
export interface DocumentStats {
|
||||
lines: number;
|
||||
@@ -26,78 +28,50 @@ export interface DocumentStats {
|
||||
selectedCharacters: number;
|
||||
}
|
||||
|
||||
interface EditorInstance {
|
||||
view: EditorView;
|
||||
documentId: number;
|
||||
content: string;
|
||||
isDirty: boolean;
|
||||
lastModified: Date;
|
||||
autoSaveTimer: number | null;
|
||||
}
|
||||
|
||||
export const useEditorStore = defineStore('editor', () => {
|
||||
// 引用配置store
|
||||
// === 依赖store ===
|
||||
const configStore = useConfigStore();
|
||||
const documentStore = useDocumentStore();
|
||||
const themeStore = useThemeStore();
|
||||
const extensionStore = useExtensionStore();
|
||||
|
||||
// 状态
|
||||
// === 核心状态 ===
|
||||
const editorCache = ref<{
|
||||
lru: number[];
|
||||
instances: Record<number, EditorInstance>;
|
||||
containerElement: HTMLElement | null;
|
||||
}>({
|
||||
lru: [],
|
||||
instances: {},
|
||||
containerElement: null
|
||||
});
|
||||
|
||||
const currentEditor = ref<EditorView | null>(null);
|
||||
const documentStats = ref<DocumentStats>({
|
||||
lines: 0,
|
||||
characters: 0,
|
||||
selectedCharacters: 0
|
||||
});
|
||||
// 编辑器视图
|
||||
const editorView = ref<EditorView | null>(null);
|
||||
// 编辑器是否已初始化
|
||||
const isEditorInitialized = ref(false);
|
||||
// 编辑器容器元素
|
||||
const editorContainer = ref<HTMLElement | null>(null);
|
||||
|
||||
// 方法
|
||||
function setEditorView(view: EditorView | null) {
|
||||
editorView.value = view;
|
||||
}
|
||||
// 自动保存设置 - 从配置动态获取
|
||||
const getAutoSaveDelay = () => configStore.config.editing.autoSaveDelay;
|
||||
|
||||
// 设置编辑器容器
|
||||
function setEditorContainer(container: HTMLElement | null) {
|
||||
editorContainer.value = container;
|
||||
// 如果编辑器已经创建但容器改变了,需要重新挂载
|
||||
if (editorView.value && container && editorView.value.dom.parentElement !== container) {
|
||||
container.appendChild(editorView.value.dom);
|
||||
// 重新挂载后立即滚动到底部
|
||||
scrollToBottom(editorView.value as EditorView);
|
||||
// === 私有方法 ===
|
||||
|
||||
// 创建编辑器实例
|
||||
const createEditorInstance = async (content: string): Promise<EditorView> => {
|
||||
if (!editorCache.value.containerElement) {
|
||||
throw new Error('Editor container not set');
|
||||
}
|
||||
}
|
||||
|
||||
// 更新文档统计信息
|
||||
function updateDocumentStats(stats: DocumentStats) {
|
||||
documentStats.value = stats;
|
||||
}
|
||||
|
||||
// 应用字体大小
|
||||
function applyFontSize() {
|
||||
if (!editorView.value) return;
|
||||
// 更新编辑器的字体大小
|
||||
const editorDOM = editorView.value.dom;
|
||||
if (editorDOM) {
|
||||
editorDOM.style.fontSize = `${configStore.config.editing.fontSize}px`;
|
||||
editorView.value?.requestMeasure();
|
||||
}
|
||||
}
|
||||
|
||||
// 滚动到文档底部的辅助函数
|
||||
const scrollToBottom = (view: EditorView) => {
|
||||
if (!view) return;
|
||||
|
||||
const lines = view.state.doc.lines;
|
||||
if (lines > 0) {
|
||||
const lastLinePos = view.state.doc.line(lines).to;
|
||||
view.dispatch({
|
||||
effects: EditorView.scrollIntoView(lastLinePos)
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 创建编辑器
|
||||
const createEditor = async (initialDoc: string = '') => {
|
||||
if (isEditorInitialized.value || !editorContainer.value) return;
|
||||
|
||||
// 加载文档内容
|
||||
await documentStore.initialize();
|
||||
const docContent = documentStore.documentContent || initialDoc;
|
||||
|
||||
// 获取基本扩展
|
||||
const basicExtensions = createBasicSetup();
|
||||
@@ -107,14 +81,14 @@ export const useEditorStore = defineStore('editor', () => {
|
||||
configStore.config.appearance.systemTheme || SystemThemeType.SystemThemeAuto
|
||||
);
|
||||
|
||||
// 获取Tab相关扩展
|
||||
// Tab相关扩展
|
||||
const tabExtensions = getTabExtensions(
|
||||
configStore.config.editing.tabSize,
|
||||
configStore.config.editing.enableTabIndent,
|
||||
configStore.config.editing.tabType
|
||||
);
|
||||
|
||||
// 创建字体扩展
|
||||
// 字体扩展
|
||||
const fontExtension = createFontExtensionFromBackend({
|
||||
fontFamily: configStore.config.editing.fontFamily,
|
||||
fontSize: configStore.config.editing.fontSize,
|
||||
@@ -122,180 +96,426 @@ export const useEditorStore = defineStore('editor', () => {
|
||||
fontWeight: configStore.config.editing.fontWeight
|
||||
});
|
||||
|
||||
// 创建统计信息更新扩展
|
||||
const statsExtension = createStatsUpdateExtension(
|
||||
updateDocumentStats
|
||||
);
|
||||
// 统计扩展
|
||||
const statsExtension = createStatsUpdateExtension(updateDocumentStats);
|
||||
|
||||
// 内容变化扩展
|
||||
const contentChangeExtension = createContentChangePlugin();
|
||||
|
||||
// 创建自动保存插件
|
||||
const autoSaveExtension = createAutoSavePlugin({
|
||||
debounceDelay: configStore.config.editing.autoSaveDelay
|
||||
});
|
||||
// 代码块功能
|
||||
// 代码块扩展
|
||||
const codeBlockExtension = createCodeBlockExtension({
|
||||
showBackground: true,
|
||||
enableAutoDetection: true
|
||||
});
|
||||
|
||||
// 创建动态快捷键扩展
|
||||
// 快捷键扩展
|
||||
const keymapExtension = await createDynamicKeymapExtension();
|
||||
|
||||
// 创建动态扩展
|
||||
// 动态扩展
|
||||
const dynamicExtensions = await createDynamicExtensions();
|
||||
|
||||
// 组合所有扩展
|
||||
const extensions: Extension[] = [
|
||||
keymapExtension,
|
||||
themeExtension,
|
||||
...basicExtensions,
|
||||
themeExtension,
|
||||
...tabExtensions,
|
||||
fontExtension,
|
||||
statsExtension,
|
||||
autoSaveExtension,
|
||||
contentChangeExtension,
|
||||
codeBlockExtension,
|
||||
...dynamicExtensions
|
||||
];
|
||||
|
||||
// 创建编辑器状态
|
||||
const state = EditorState.create({
|
||||
doc: docContent,
|
||||
doc: content,
|
||||
extensions
|
||||
});
|
||||
|
||||
// 创建编辑器视图
|
||||
const view = new EditorView({
|
||||
state,
|
||||
parent: editorContainer.value
|
||||
state
|
||||
});
|
||||
|
||||
// 将编辑器实例保存到store
|
||||
setEditorView(view);
|
||||
// 初始化语法树
|
||||
ensureSyntaxTree(view.state, view.state.doc.length, 5000);
|
||||
|
||||
// 设置编辑器视图到扩展管理器
|
||||
setExtensionManagerView(view);
|
||||
// 将光标定位到文档末尾
|
||||
const docLength = view.state.doc.length;
|
||||
view.dispatch({
|
||||
selection: { anchor: docLength, head: docLength }
|
||||
});
|
||||
|
||||
isEditorInitialized.value = true;
|
||||
|
||||
scrollToBottom(view);
|
||||
|
||||
ensureSyntaxTree(view.state, view.state.doc.length, 5000)
|
||||
|
||||
// 应用初始字体大小
|
||||
applyFontSize();
|
||||
return view;
|
||||
};
|
||||
|
||||
// 重新配置编辑器
|
||||
const reconfigureTabSettings = () => {
|
||||
if (!editorView.value) return;
|
||||
updateTabConfig(
|
||||
editorView.value as EditorView,
|
||||
configStore.config.editing.tabSize,
|
||||
configStore.config.editing.enableTabIndent,
|
||||
configStore.config.editing.tabType
|
||||
// 添加编辑器到缓存
|
||||
const addEditorToCache = (documentId: number, view: EditorView, content: string) => {
|
||||
// 如果缓存已满,移除最少使用的编辑器
|
||||
if (editorCache.value.lru.length >= NUM_EDITOR_INSTANCES) {
|
||||
const oldestId = editorCache.value.lru.shift();
|
||||
if (oldestId && editorCache.value.instances[oldestId]) {
|
||||
const oldInstance = editorCache.value.instances[oldestId];
|
||||
// 清除自动保存定时器
|
||||
if (oldInstance.autoSaveTimer) {
|
||||
clearTimeout(oldInstance.autoSaveTimer);
|
||||
}
|
||||
// 移除DOM元素
|
||||
if (oldInstance.view.dom.parentElement) {
|
||||
oldInstance.view.dom.remove();
|
||||
}
|
||||
oldInstance.view.destroy();
|
||||
delete editorCache.value.instances[oldestId];
|
||||
}
|
||||
}
|
||||
|
||||
// 添加新的编辑器实例
|
||||
editorCache.value.instances[documentId] = {
|
||||
view,
|
||||
documentId,
|
||||
content,
|
||||
isDirty: false,
|
||||
lastModified: new Date(),
|
||||
autoSaveTimer: null
|
||||
};
|
||||
|
||||
// 添加到LRU列表
|
||||
editorCache.value.lru.push(documentId);
|
||||
};
|
||||
|
||||
// 更新LRU
|
||||
const updateLRU = (documentId: number) => {
|
||||
const lru = editorCache.value.lru;
|
||||
const index = lru.indexOf(documentId);
|
||||
if (index > -1) {
|
||||
lru.splice(index, 1);
|
||||
}
|
||||
lru.push(documentId);
|
||||
};
|
||||
|
||||
// 获取或创建编辑器
|
||||
const getOrCreateEditor = async (documentId: number, content: string): Promise<EditorView> => {
|
||||
// 检查缓存
|
||||
const cached = editorCache.value.instances[documentId];
|
||||
if (cached) {
|
||||
updateLRU(documentId);
|
||||
return cached.view;
|
||||
}
|
||||
|
||||
// 创建新的编辑器实例
|
||||
const view = await createEditorInstance(content);
|
||||
addEditorToCache(documentId, view, content);
|
||||
|
||||
return view;
|
||||
};
|
||||
|
||||
// 显示编辑器
|
||||
const showEditor = (documentId: number) => {
|
||||
const instance = editorCache.value.instances[documentId];
|
||||
if (!instance || !editorCache.value.containerElement) return;
|
||||
|
||||
try {
|
||||
// 移除当前编辑器DOM
|
||||
if (currentEditor.value && currentEditor.value.dom && currentEditor.value.dom.parentElement) {
|
||||
currentEditor.value.dom.remove();
|
||||
}
|
||||
|
||||
// 确保容器为空
|
||||
editorCache.value.containerElement.innerHTML = '';
|
||||
|
||||
// 将目标编辑器DOM添加到容器
|
||||
editorCache.value.containerElement.appendChild(instance.view.dom);
|
||||
currentEditor.value = instance.view;
|
||||
|
||||
// 设置扩展管理器视图
|
||||
setExtensionManagerView(instance.view);
|
||||
|
||||
// 更新LRU
|
||||
updateLRU(documentId);
|
||||
|
||||
// 重新测量和聚焦编辑器
|
||||
nextTick(() => {
|
||||
instance.view.requestMeasure();
|
||||
// 将光标定位到文档末尾
|
||||
const docLength = instance.view.state.doc.length;
|
||||
instance.view.dispatch({
|
||||
selection: { anchor: docLength, head: docLength }
|
||||
});
|
||||
instance.view.focus();
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error showing editor:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 保存编辑器内容
|
||||
const saveEditorContent = async (documentId: number): Promise<boolean> => {
|
||||
const instance = editorCache.value.instances[documentId];
|
||||
if (!instance || !instance.isDirty) return true;
|
||||
|
||||
try {
|
||||
const content = instance.view.state.doc.toString();
|
||||
await DocumentService.UpdateDocumentContent(documentId, content);
|
||||
|
||||
instance.content = content;
|
||||
instance.isDirty = false;
|
||||
instance.lastModified = new Date();
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Failed to save editor content:', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// 内容变化处理
|
||||
const onContentChange = (documentId: number) => {
|
||||
const instance = editorCache.value.instances[documentId];
|
||||
if (!instance) return;
|
||||
|
||||
instance.isDirty = true;
|
||||
instance.lastModified = new Date();
|
||||
|
||||
// 清除之前的定时器
|
||||
if (instance.autoSaveTimer) {
|
||||
clearTimeout(instance.autoSaveTimer);
|
||||
}
|
||||
|
||||
// 设置新的自动保存定时器
|
||||
instance.autoSaveTimer = window.setTimeout(() => {
|
||||
saveEditorContent(documentId);
|
||||
}, getAutoSaveDelay());
|
||||
};
|
||||
|
||||
// === 公共API ===
|
||||
|
||||
// 设置编辑器容器
|
||||
const setEditorContainer = (container: HTMLElement | null) => {
|
||||
editorCache.value.containerElement = container;
|
||||
|
||||
// 如果设置容器时已有当前文档,立即加载编辑器
|
||||
if (container && documentStore.currentDocument) {
|
||||
loadEditor(documentStore.currentDocument.id, documentStore.currentDocument.content);
|
||||
}
|
||||
};
|
||||
|
||||
// 加载编辑器
|
||||
const loadEditor = async (documentId: number, content: string) => {
|
||||
try {
|
||||
// 验证参数
|
||||
if (!documentId) {
|
||||
throw new Error('Invalid parameters for loadEditor');
|
||||
}
|
||||
|
||||
// 保存当前编辑器内容
|
||||
if (currentEditor.value) {
|
||||
const currentDocId = documentStore.currentDocumentId;
|
||||
if (currentDocId && currentDocId !== documentId) {
|
||||
await saveEditorContent(currentDocId);
|
||||
}
|
||||
}
|
||||
|
||||
// 获取或创建编辑器
|
||||
const view = await getOrCreateEditor(documentId, content);
|
||||
|
||||
// 更新内容(如果需要)
|
||||
const instance = editorCache.value.instances[documentId];
|
||||
if (instance && instance.content !== content) {
|
||||
// 确保编辑器视图有效
|
||||
if (view && view.state && view.dispatch) {
|
||||
view.dispatch({
|
||||
changes: {
|
||||
from: 0,
|
||||
to: view.state.doc.length,
|
||||
insert: content
|
||||
}
|
||||
});
|
||||
instance.content = content;
|
||||
instance.isDirty = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 显示编辑器
|
||||
showEditor(documentId);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to load editor:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 移除编辑器
|
||||
const removeEditor = (documentId: number) => {
|
||||
const instance = editorCache.value.instances[documentId];
|
||||
if (instance) {
|
||||
try {
|
||||
// 清除自动保存定时器
|
||||
if (instance.autoSaveTimer) {
|
||||
clearTimeout(instance.autoSaveTimer);
|
||||
instance.autoSaveTimer = null;
|
||||
}
|
||||
|
||||
// 移除DOM元素
|
||||
if (instance.view && instance.view.dom && instance.view.dom.parentElement) {
|
||||
instance.view.dom.remove();
|
||||
}
|
||||
|
||||
// 销毁编辑器
|
||||
if (instance.view && instance.view.destroy) {
|
||||
instance.view.destroy();
|
||||
}
|
||||
|
||||
// 清理引用
|
||||
if (currentEditor.value === instance.view) {
|
||||
currentEditor.value = null;
|
||||
}
|
||||
|
||||
delete editorCache.value.instances[documentId];
|
||||
|
||||
const lruIndex = editorCache.value.lru.indexOf(documentId);
|
||||
if (lruIndex > -1) {
|
||||
editorCache.value.lru.splice(lruIndex, 1);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error removing editor:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 更新文档统计
|
||||
const updateDocumentStats = (stats: DocumentStats) => {
|
||||
documentStats.value = stats;
|
||||
};
|
||||
|
||||
// 应用字体设置
|
||||
const applyFontSettings = () => {
|
||||
Object.values(editorCache.value.instances).forEach(instance => {
|
||||
updateFontConfig(instance.view, {
|
||||
fontFamily: configStore.config.editing.fontFamily,
|
||||
fontSize: configStore.config.editing.fontSize,
|
||||
lineHeight: configStore.config.editing.lineHeight,
|
||||
fontWeight: configStore.config.editing.fontWeight
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// 应用主题设置
|
||||
const applyThemeSettings = () => {
|
||||
Object.values(editorCache.value.instances).forEach(instance => {
|
||||
updateEditorTheme(instance.view,
|
||||
themeStore.currentTheme || SystemThemeType.SystemThemeAuto
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
// 应用Tab设置
|
||||
const applyTabSettings = () => {
|
||||
Object.values(editorCache.value.instances).forEach(instance => {
|
||||
updateTabConfig(
|
||||
instance.view,
|
||||
configStore.config.editing.tabSize,
|
||||
configStore.config.editing.enableTabIndent,
|
||||
configStore.config.editing.tabType
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
// 应用快捷键设置
|
||||
const applyKeymapSettings = async () => {
|
||||
await Promise.all(
|
||||
Object.values(editorCache.value.instances).map(instance =>
|
||||
updateKeymapExtension(instance.view)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
// 重新配置字体设置
|
||||
const reconfigureFontSettings = () => {
|
||||
if (!editorView.value) return;
|
||||
updateFontConfig(editorView.value as EditorView, {
|
||||
fontFamily: configStore.config.editing.fontFamily,
|
||||
fontSize: configStore.config.editing.fontSize,
|
||||
lineHeight: configStore.config.editing.lineHeight,
|
||||
fontWeight: configStore.config.editing.fontWeight
|
||||
// 清空所有编辑器
|
||||
const clearAllEditors = () => {
|
||||
Object.values(editorCache.value.instances).forEach(instance => {
|
||||
// 清除自动保存定时器
|
||||
if (instance.autoSaveTimer) {
|
||||
clearTimeout(instance.autoSaveTimer);
|
||||
}
|
||||
// 移除DOM元素
|
||||
if (instance.view.dom.parentElement) {
|
||||
instance.view.dom.remove();
|
||||
}
|
||||
// 销毁编辑器
|
||||
instance.view.destroy();
|
||||
});
|
||||
|
||||
editorCache.value.instances = {};
|
||||
editorCache.value.lru = [];
|
||||
currentEditor.value = null;
|
||||
};
|
||||
|
||||
// 销毁编辑器
|
||||
const destroyEditor = () => {
|
||||
if (editorView.value) {
|
||||
editorView.value.destroy();
|
||||
editorView.value = null;
|
||||
isEditorInitialized.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 监听Tab设置变化
|
||||
watch([
|
||||
() => configStore.config.editing.tabSize,
|
||||
() => configStore.config.editing.enableTabIndent,
|
||||
() => configStore.config.editing.tabType,
|
||||
], () => {
|
||||
reconfigureTabSettings();
|
||||
});
|
||||
|
||||
// 监听字体大小变化
|
||||
watch([
|
||||
() => configStore.config.editing.fontFamily,
|
||||
() => configStore.config.editing.fontSize,
|
||||
() => configStore.config.editing.lineHeight,
|
||||
() => configStore.config.editing.fontWeight,
|
||||
], () => {
|
||||
reconfigureFontSettings();
|
||||
applyFontSize();
|
||||
// 通知checkbox扩展字体已变化,需要重新渲染
|
||||
if (editorView.value) {
|
||||
triggerFontChange(editorView.value as EditorView);
|
||||
}
|
||||
});
|
||||
|
||||
// 监听主题变化
|
||||
watch(() => themeStore.currentTheme, (newTheme) => {
|
||||
if (editorView.value && newTheme) {
|
||||
updateEditorTheme(editorView.value as EditorView, newTheme);
|
||||
}
|
||||
});
|
||||
|
||||
// 扩展管理方法
|
||||
const updateExtension = async (id: any, enabled: boolean, config?: any) => {
|
||||
// 更新扩展
|
||||
const updateExtension = async (id: ExtensionID, enabled: boolean, config?: any) => {
|
||||
try {
|
||||
// 如果只是更新启用状态
|
||||
if (config === undefined) {
|
||||
await ExtensionService.UpdateExtensionEnabled(id, enabled)
|
||||
await ExtensionService.UpdateExtensionEnabled(id, enabled);
|
||||
} else {
|
||||
// 如果需要更新配置
|
||||
await ExtensionService.UpdateExtensionState(id, enabled, config)
|
||||
await ExtensionService.UpdateExtensionState(id, enabled, config);
|
||||
}
|
||||
|
||||
// 更新前端编辑器扩展
|
||||
const manager = getExtensionManager()
|
||||
const manager = getExtensionManager();
|
||||
if (manager) {
|
||||
manager.updateExtension(id, enabled, config || {})
|
||||
manager.updateExtension(id, enabled, config || {});
|
||||
}
|
||||
|
||||
// 重新加载扩展配置
|
||||
await extensionStore.loadExtensions()
|
||||
await extensionStore.loadExtensions();
|
||||
|
||||
// 更新快捷键映射
|
||||
if (editorView.value) {
|
||||
updateKeymapExtension(editorView.value)
|
||||
if (currentEditor.value) {
|
||||
updateKeymapExtension(currentEditor.value);
|
||||
}
|
||||
} catch (error) {
|
||||
throw error
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 监听扩展状态变化,自动更新快捷键
|
||||
watch(() => extensionStore.enabledExtensions.length, () => {
|
||||
if (editorView.value) {
|
||||
updateKeymapExtension(editorView.value)
|
||||
// 监听文档切换
|
||||
watch(() => documentStore.currentDocument, (newDoc) => {
|
||||
if (newDoc && editorCache.value.containerElement) {
|
||||
loadEditor(newDoc.id, newDoc.content);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
// 监听配置变化
|
||||
watch(() => configStore.config.editing.fontSize, applyFontSettings);
|
||||
watch(() => configStore.config.editing.fontFamily, applyFontSettings);
|
||||
watch(() => configStore.config.editing.lineHeight, applyFontSettings);
|
||||
watch(() => configStore.config.editing.fontWeight, applyFontSettings);
|
||||
watch(() => configStore.config.editing.tabSize, applyTabSettings);
|
||||
watch(() => configStore.config.editing.enableTabIndent, applyTabSettings);
|
||||
watch(() => configStore.config.editing.tabType, applyTabSettings);
|
||||
watch(() => themeStore.currentTheme, applyThemeSettings);
|
||||
|
||||
return {
|
||||
// 状态
|
||||
currentEditor,
|
||||
documentStats,
|
||||
editorView,
|
||||
isEditorInitialized,
|
||||
editorContainer,
|
||||
|
||||
// 方法
|
||||
setEditorContainer,
|
||||
createEditor,
|
||||
reconfigureTabSettings,
|
||||
reconfigureFontSettings,
|
||||
destroyEditor,
|
||||
updateExtension
|
||||
loadEditor,
|
||||
removeEditor,
|
||||
clearAllEditors,
|
||||
onContentChange,
|
||||
|
||||
// 配置更新方法
|
||||
applyFontSettings,
|
||||
applyThemeSettings,
|
||||
applyTabSettings,
|
||||
applyKeymapSettings,
|
||||
|
||||
// 扩展管理方法
|
||||
updateExtension,
|
||||
|
||||
editorView: currentEditor,
|
||||
};
|
||||
});
|
@@ -1,39 +1,30 @@
|
||||
<script setup lang="ts">
|
||||
import {onBeforeUnmount, onMounted, ref} from 'vue';
|
||||
import {useEditorStore} from '@/stores/editorStore';
|
||||
import {useDocumentStore} from '@/stores/documentStore';
|
||||
import {useConfigStore} from '@/stores/configStore';
|
||||
import {createWheelZoomHandler} from './basic/wheelZoomExtension';
|
||||
import Toolbar from '@/components/toolbar/Toolbar.vue';
|
||||
|
||||
const editorStore = useEditorStore();
|
||||
const documentStore = useDocumentStore();
|
||||
const configStore = useConfigStore();
|
||||
|
||||
const props = defineProps({
|
||||
initialDoc: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
});
|
||||
|
||||
const editorElement = ref<HTMLElement | null>(null);
|
||||
|
||||
// 创建滚轮缩放处理器
|
||||
const wheelHandler = createWheelZoomHandler(
|
||||
configStore.increaseFontSize,
|
||||
configStore.decreaseFontSize
|
||||
configStore.increaseFontSize,
|
||||
configStore.decreaseFontSize
|
||||
);
|
||||
|
||||
onMounted(async () => {
|
||||
if (!editorElement.value) return;
|
||||
|
||||
await documentStore.initialize();
|
||||
// 设置编辑器容器
|
||||
editorStore.setEditorContainer(editorElement.value);
|
||||
|
||||
// 如果编辑器还没有初始化,创建编辑器
|
||||
if (!editorStore.isEditorInitialized) {
|
||||
await editorStore.createEditor(props.initialDoc);
|
||||
}
|
||||
|
||||
// 添加滚轮事件监听
|
||||
editorElement.value.addEventListener('wheel', wheelHandler, {passive: false});
|
||||
});
|
||||
@@ -49,7 +40,7 @@ onBeforeUnmount(() => {
|
||||
<template>
|
||||
<div class="editor-container">
|
||||
<div ref="editorElement" class="editor"></div>
|
||||
<Toolbar />
|
||||
<Toolbar/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@@ -1,109 +0,0 @@
|
||||
import { EditorView, ViewPlugin, ViewUpdate } from '@codemirror/view';
|
||||
import { useDocumentStore } from '@/stores/documentStore';
|
||||
|
||||
// 自动保存配置选项
|
||||
export interface AutoSaveOptions {
|
||||
// 防抖延迟(毫秒)
|
||||
debounceDelay?: number;
|
||||
// 保存状态回调
|
||||
onSaveStart?: () => void;
|
||||
onSaveSuccess?: () => void;
|
||||
onSaveError?: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 简单防抖函数
|
||||
*/
|
||||
function debounce<T extends (...args: any[]) => any>(
|
||||
func: T,
|
||||
delay: number
|
||||
): T & { cancel: () => void } {
|
||||
let timeoutId: number | null = null;
|
||||
|
||||
const debounced = ((...args: Parameters<T>) => {
|
||||
if (timeoutId !== null) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
|
||||
timeoutId = window.setTimeout(() => {
|
||||
timeoutId = null;
|
||||
func(...args);
|
||||
}, delay);
|
||||
}) as T & { cancel: () => void };
|
||||
|
||||
debounced.cancel = () => {
|
||||
if (timeoutId !== null) {
|
||||
clearTimeout(timeoutId);
|
||||
timeoutId = null;
|
||||
}
|
||||
};
|
||||
|
||||
return debounced;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建自动保存插件
|
||||
*/
|
||||
export function createAutoSavePlugin(options: AutoSaveOptions = {}) {
|
||||
const {
|
||||
debounceDelay = 2000,
|
||||
onSaveStart = () => {},
|
||||
onSaveSuccess = () => {},
|
||||
onSaveError = () => {}
|
||||
} = options;
|
||||
|
||||
return ViewPlugin.fromClass(
|
||||
class AutoSavePlugin {
|
||||
private documentStore = useDocumentStore();
|
||||
private debouncedSave: ((content: string) => void) & { cancel: () => void };
|
||||
private isDestroyed = false;
|
||||
private lastContent = '';
|
||||
|
||||
constructor(private view: EditorView) {
|
||||
this.lastContent = view.state.doc.toString();
|
||||
this.debouncedSave = debounce(
|
||||
(content: string) => this.performSave(content),
|
||||
debounceDelay
|
||||
);
|
||||
}
|
||||
|
||||
private async performSave(content: string): Promise<void> {
|
||||
if (this.isDestroyed) return;
|
||||
|
||||
try {
|
||||
onSaveStart();
|
||||
const success = await this.documentStore.saveDocumentContent(content);
|
||||
|
||||
if (success) {
|
||||
this.lastContent = content;
|
||||
onSaveSuccess();
|
||||
} else {
|
||||
onSaveError();
|
||||
}
|
||||
} catch (error) {
|
||||
onSaveError();
|
||||
}
|
||||
}
|
||||
|
||||
update(update: ViewUpdate) {
|
||||
if (!update.docChanged || this.isDestroyed) return;
|
||||
|
||||
const newContent = this.view.state.doc.toString();
|
||||
if (newContent === this.lastContent) return;
|
||||
|
||||
this.debouncedSave(newContent);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.isDestroyed = true;
|
||||
this.debouncedSave.cancel();
|
||||
|
||||
// 如果内容有变化,立即保存
|
||||
const currentContent = this.view.state.doc.toString();
|
||||
if (currentContent !== this.lastContent) {
|
||||
this.documentStore.saveDocumentContent(currentContent).catch(() => {});
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
39
frontend/src/views/editor/basic/contentChangeExtension.ts
Normal file
39
frontend/src/views/editor/basic/contentChangeExtension.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { EditorView, ViewPlugin, ViewUpdate } from '@codemirror/view';
|
||||
import { useDocumentStore } from '@/stores/documentStore';
|
||||
import { useEditorStore } from '@/stores/editorStore';
|
||||
|
||||
/**
|
||||
* 内容变化监听插件 - 集成文档和编辑器管理
|
||||
*/
|
||||
export function createContentChangePlugin() {
|
||||
return ViewPlugin.fromClass(
|
||||
class ContentChangePlugin {
|
||||
private documentStore = useDocumentStore();
|
||||
private editorStore = useEditorStore();
|
||||
private lastContent = '';
|
||||
|
||||
constructor(private view: EditorView) {
|
||||
this.lastContent = view.state.doc.toString();
|
||||
}
|
||||
|
||||
update(update: ViewUpdate) {
|
||||
if (!update.docChanged) return;
|
||||
|
||||
const newContent = this.view.state.doc.toString();
|
||||
if (newContent === this.lastContent) return;
|
||||
|
||||
this.lastContent = newContent;
|
||||
|
||||
// 通知编辑器管理器内容已变化
|
||||
const currentDocId = this.documentStore.currentDocumentId;
|
||||
if (currentDocId) {
|
||||
this.editorStore.onContentChange(currentDocId);
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
Reference in New Issue
Block a user