✨ Add multi-window document functionality
This commit is contained in:
@@ -14,6 +14,7 @@ VoidRaft is a modern developer-focused text editor that allows you to record, or
|
|||||||
- Smart language detection - Automatically recognizes code block language types
|
- Smart language detection - Automatically recognizes code block language types
|
||||||
- Code formatting - Built-in Prettier support for one-click code beautification
|
- Code formatting - Built-in Prettier support for one-click code beautification
|
||||||
- Block editing mode - Split content into independent code blocks, each with different language settings
|
- Block editing mode - Split content into independent code blocks, each with different language settings
|
||||||
|
- Multi-window support - edit multiple documents at the same time
|
||||||
|
|
||||||
### Modern Interface
|
### Modern Interface
|
||||||
|
|
||||||
@@ -53,6 +54,7 @@ cd voidraft
|
|||||||
# Install frontend dependencies
|
# Install frontend dependencies
|
||||||
cd frontend
|
cd frontend
|
||||||
npm install
|
npm install
|
||||||
|
npm run build
|
||||||
cd ..
|
cd ..
|
||||||
|
|
||||||
# Start development server
|
# Start development server
|
||||||
@@ -117,11 +119,8 @@ Voidraft/
|
|||||||
|
|
||||||
### Planned Features
|
### Planned Features
|
||||||
- [ ] Custom themes - Customize editor themes
|
- [ ] Custom themes - Customize editor themes
|
||||||
- [ ] Multi-window support - Support editing multiple documents simultaneously
|
- ✅ Multi-window support - Support editing multiple documents simultaneously
|
||||||
- [ ] Enhanced clipboard - Monitor and manage clipboard history
|
- [ ] Enhanced clipboard - Monitor and manage clipboard history
|
||||||
- Automatic text content saving
|
|
||||||
- Image content support
|
|
||||||
- History management
|
|
||||||
- [ ] Data synchronization - Cloud backup for configurations and documents
|
- [ ] Data synchronization - Cloud backup for configurations and documents
|
||||||
- [ ] Extension system - Support for custom plugins
|
- [ ] Extension system - Support for custom plugins
|
||||||
|
|
||||||
|
@@ -14,6 +14,7 @@ Voidraft 是一个现代化的开发者专用文本编辑器,让你能够随
|
|||||||
- 智能语言检测 - 自动识别代码块语言类型
|
- 智能语言检测 - 自动识别代码块语言类型
|
||||||
- 代码格式化 - 内置 Prettier 支持,一键美化代码
|
- 代码格式化 - 内置 Prettier 支持,一键美化代码
|
||||||
- 块状编辑模式 - 将内容分割为独立的代码块,每个块可设置不同语言
|
- 块状编辑模式 - 将内容分割为独立的代码块,每个块可设置不同语言
|
||||||
|
- 支持多窗口 - 同时编辑多个文档
|
||||||
|
|
||||||
### 现代化界面
|
### 现代化界面
|
||||||
|
|
||||||
@@ -54,6 +55,7 @@ cd voidraft
|
|||||||
# 安装前端依赖
|
# 安装前端依赖
|
||||||
cd frontend
|
cd frontend
|
||||||
npm install
|
npm install
|
||||||
|
npm run build
|
||||||
cd ..
|
cd ..
|
||||||
|
|
||||||
# 启动开发服务器
|
# 启动开发服务器
|
||||||
@@ -118,11 +120,8 @@ Voidraft/
|
|||||||
|
|
||||||
### 计划添加的功能
|
### 计划添加的功能
|
||||||
- [ ] 自定义主题 - 自定义编辑器主题
|
- [ ] 自定义主题 - 自定义编辑器主题
|
||||||
- [ ] 多窗口支持 - 支持同时编辑多个文档
|
- ✅ 多窗口支持 - 支持同时编辑多个文档
|
||||||
- [ ] 剪切板增强 - 监听和管理剪切板历史
|
- [ ] 剪切板增强 - 监听和管理剪切板历史
|
||||||
- 文本内容自动保存
|
|
||||||
- 图片内容支持
|
|
||||||
- 历史记录管理
|
|
||||||
- [ ] 数据同步 - 配置和文档云端备份
|
- [ ] 数据同步 - 配置和文档云端备份
|
||||||
- [ ] 扩展系统 - 支持自定义插件
|
- [ ] 扩展系统 - 支持自定义插件
|
||||||
|
|
||||||
|
@@ -14,6 +14,7 @@ import * as StartupService from "./startupservice.js";
|
|||||||
import * as SystemService from "./systemservice.js";
|
import * as SystemService from "./systemservice.js";
|
||||||
import * as TranslationService from "./translationservice.js";
|
import * as TranslationService from "./translationservice.js";
|
||||||
import * as TrayService from "./trayservice.js";
|
import * as TrayService from "./trayservice.js";
|
||||||
|
import * as WindowService from "./windowservice.js";
|
||||||
export {
|
export {
|
||||||
ConfigService,
|
ConfigService,
|
||||||
DatabaseService,
|
DatabaseService,
|
||||||
@@ -27,7 +28,8 @@ export {
|
|||||||
StartupService,
|
StartupService,
|
||||||
SystemService,
|
SystemService,
|
||||||
TranslationService,
|
TranslationService,
|
||||||
TrayService
|
TrayService,
|
||||||
|
WindowService
|
||||||
};
|
};
|
||||||
|
|
||||||
export * from "./models.js";
|
export * from "./models.js";
|
||||||
|
@@ -5,6 +5,10 @@
|
|||||||
// @ts-ignore: Unused imports
|
// @ts-ignore: Unused imports
|
||||||
import {Create as $Create} from "@wailsio/runtime";
|
import {Create as $Create} from "@wailsio/runtime";
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore: Unused imports
|
||||||
|
import * as application$0 from "../../../github.com/wailsapp/wails/v3/pkg/application/models.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* MemoryStats 内存统计信息
|
* MemoryStats 内存统计信息
|
||||||
*/
|
*/
|
||||||
@@ -197,3 +201,43 @@ export class SelfUpdateResult {
|
|||||||
return new SelfUpdateResult($$parsedSource as Partial<SelfUpdateResult>);
|
return new SelfUpdateResult($$parsedSource as Partial<SelfUpdateResult>);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WindowInfo 窗口信息
|
||||||
|
*/
|
||||||
|
export class WindowInfo {
|
||||||
|
"Window": application$0.WebviewWindow | null;
|
||||||
|
"DocumentID": number;
|
||||||
|
"Title": string;
|
||||||
|
|
||||||
|
/** Creates a new WindowInfo instance. */
|
||||||
|
constructor($$source: Partial<WindowInfo> = {}) {
|
||||||
|
if (!("Window" in $$source)) {
|
||||||
|
this["Window"] = null;
|
||||||
|
}
|
||||||
|
if (!("DocumentID" in $$source)) {
|
||||||
|
this["DocumentID"] = 0;
|
||||||
|
}
|
||||||
|
if (!("Title" in $$source)) {
|
||||||
|
this["Title"] = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.assign(this, $$source);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new WindowInfo instance from a string or object.
|
||||||
|
*/
|
||||||
|
static createFrom($$source: any = {}): WindowInfo {
|
||||||
|
const $$createField0_0 = $$createType1;
|
||||||
|
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||||
|
if ("Window" in $$parsedSource) {
|
||||||
|
$$parsedSource["Window"] = $$createField0_0($$parsedSource["Window"]);
|
||||||
|
}
|
||||||
|
return new WindowInfo($$parsedSource as Partial<WindowInfo>);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Private type creation functions
|
||||||
|
const $$createType0 = application$0.WebviewWindow.createFrom;
|
||||||
|
const $$createType1 = $Create.Nullable($$createType0);
|
||||||
|
@@ -0,0 +1,59 @@
|
|||||||
|
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||||
|
// This file is automatically generated. DO NOT EDIT
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WindowService 窗口管理服务
|
||||||
|
* @module
|
||||||
|
*/
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore: Unused imports
|
||||||
|
import {Call as $Call, Create as $Create} from "@wailsio/runtime";
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore: Unused imports
|
||||||
|
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 from "./models.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GetOpenWindows 获取所有打开的窗口信息
|
||||||
|
*/
|
||||||
|
export function GetOpenWindows(): Promise<$models.WindowInfo[]> & { cancel(): void } {
|
||||||
|
let $resultPromise = $Call.ByID(1464997251) as any;
|
||||||
|
let $typingPromise = $resultPromise.then(($result: any) => {
|
||||||
|
return $$createType1($result);
|
||||||
|
}) as any;
|
||||||
|
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
|
||||||
|
return $typingPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* IsDocumentWindowOpen 检查指定文档的窗口是否已打开
|
||||||
|
*/
|
||||||
|
export function IsDocumentWindowOpen(documentID: number): Promise<boolean> & { cancel(): void } {
|
||||||
|
let $resultPromise = $Call.ByID(1735611839, documentID) as any;
|
||||||
|
return $resultPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OpenDocumentWindow 为指定文档ID打开新窗口
|
||||||
|
*/
|
||||||
|
export function OpenDocumentWindow(documentID: number): Promise<void> & { cancel(): void } {
|
||||||
|
let $resultPromise = $Call.ByID(494716471, documentID) as any;
|
||||||
|
return $resultPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SetAppReferences 设置应用和主窗口引用
|
||||||
|
*/
|
||||||
|
export function SetAppReferences(app: application$0.App | null, mainWindow: application$0.WebviewWindow | null): Promise<void> & { cancel(): void } {
|
||||||
|
let $resultPromise = $Call.ByID(1120840759, app, mainWindow) as any;
|
||||||
|
return $resultPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Private type creation functions
|
||||||
|
const $$createType0 = $models.WindowInfo.createFrom;
|
||||||
|
const $$createType1 = $Create.Array($$createType0);
|
@@ -4,7 +4,7 @@
|
|||||||
<div class="titlebar-icon">
|
<div class="titlebar-icon">
|
||||||
<img src="/appicon.png" alt="voidraft" />
|
<img src="/appicon.png" alt="voidraft" />
|
||||||
</div>
|
</div>
|
||||||
<div class="titlebar-title">voidraft</div>
|
<div class="titlebar-title">{{ titleText }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="titlebar-controls" style="--wails-draggable:no-drag" @contextmenu.prevent>
|
<div class="titlebar-controls" style="--wails-draggable:no-drag" @contextmenu.prevent>
|
||||||
@@ -46,12 +46,15 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, onUnmounted } from 'vue';
|
import { ref, onMounted, onUnmounted, computed } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import * as runtime from '@wailsio/runtime';
|
import * as runtime from '@wailsio/runtime';
|
||||||
|
import { useWindowStore } from '@/stores/windowStore';
|
||||||
|
import { useDocumentStore } from '@/stores/documentStore';
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const isMaximized = ref(false);
|
const isMaximized = ref(false);
|
||||||
|
const documentStore = useDocumentStore();
|
||||||
|
|
||||||
const minimizeWindow = async () => {
|
const minimizeWindow = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -96,6 +99,12 @@ const checkMaximizedState = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 计算标题文本
|
||||||
|
const titleText = computed(() => {
|
||||||
|
const currentDoc = documentStore.currentDocument;
|
||||||
|
return currentDoc ? `voidraft - ${currentDoc.title}` : 'voidraft';
|
||||||
|
});
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await checkMaximizedState();
|
await checkMaximizedState();
|
||||||
|
|
||||||
@@ -285,4 +294,4 @@ onUnmounted(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
@@ -44,19 +44,22 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="titlebar-content" @dblclick="toggleMaximize" @contextmenu.prevent>
|
<div class="titlebar-content" @dblclick="toggleMaximize" @contextmenu.prevent>
|
||||||
<div class="titlebar-title">voidraft</div>
|
<div class="titlebar-title">{{ titleText }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, onUnmounted } from 'vue';
|
import { ref, onMounted, onUnmounted, computed } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import * as runtime from '@wailsio/runtime';
|
import * as runtime from '@wailsio/runtime';
|
||||||
|
import { useWindowStore } from '@/stores/windowStore';
|
||||||
|
import { useDocumentStore } from '@/stores/documentStore';
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const isMaximized = ref(false);
|
const isMaximized = ref(false);
|
||||||
const showControlIcons = ref(false);
|
const showControlIcons = ref(false);
|
||||||
|
const documentStore = useDocumentStore();
|
||||||
|
|
||||||
const minimizeWindow = async () => {
|
const minimizeWindow = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -101,6 +104,12 @@ const checkMaximizedState = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 计算标题文本
|
||||||
|
const titleText = computed(() => {
|
||||||
|
const currentDoc = documentStore.currentDocument;
|
||||||
|
return currentDoc ? `voidraft - ${currentDoc.title}` : 'voidraft';
|
||||||
|
});
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await checkMaximizedState();
|
await checkMaximizedState();
|
||||||
|
|
||||||
@@ -259,4 +268,4 @@ onUnmounted(() => {
|
|||||||
color: rgba(255, 255, 255, 0.8);
|
color: rgba(255, 255, 255, 0.8);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
@@ -5,7 +5,7 @@
|
|||||||
<div class="titlebar-icon">
|
<div class="titlebar-icon">
|
||||||
<img src="/appicon.png" alt="voidraft"/>
|
<img src="/appicon.png" alt="voidraft"/>
|
||||||
</div>
|
</div>
|
||||||
<div class="titlebar-title">voidraft</div>
|
<div class="titlebar-title">{{ titleText }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="titlebar-controls" style="--wails-draggable:no-drag" @contextmenu.prevent>
|
<div class="titlebar-controls" style="--wails-draggable:no-drag" @contextmenu.prevent>
|
||||||
@@ -40,13 +40,22 @@
|
|||||||
import {computed, onMounted, onUnmounted, ref} from 'vue';
|
import {computed, onMounted, onUnmounted, ref} from 'vue';
|
||||||
import {useI18n} from 'vue-i18n';
|
import {useI18n} from 'vue-i18n';
|
||||||
import * as runtime from '@wailsio/runtime';
|
import * as runtime from '@wailsio/runtime';
|
||||||
|
import { useWindowStore } from '@/stores/windowStore';
|
||||||
|
import { useDocumentStore } from '@/stores/documentStore';
|
||||||
|
|
||||||
const {t} = useI18n();
|
const {t} = useI18n();
|
||||||
const isMaximized = ref(false);
|
const isMaximized = ref(false);
|
||||||
|
const documentStore = useDocumentStore();
|
||||||
|
|
||||||
// 计算属性用于图标,减少重复渲染
|
// 计算属性用于图标,减少重复渲染
|
||||||
const maximizeIcon = computed(() => isMaximized.value ? '' : '');
|
const maximizeIcon = computed(() => isMaximized.value ? '' : '');
|
||||||
|
|
||||||
|
// 计算标题文本
|
||||||
|
const titleText = computed(() => {
|
||||||
|
const currentDoc = documentStore.currentDocument;
|
||||||
|
return currentDoc ? `voidraft - ${currentDoc.title}` : 'voidraft';
|
||||||
|
});
|
||||||
|
|
||||||
const minimizeWindow = async () => {
|
const minimizeWindow = async () => {
|
||||||
try {
|
try {
|
||||||
await runtime.Window.Minimise();
|
await runtime.Window.Minimise();
|
||||||
@@ -239,4 +248,4 @@ onUnmounted(() => {
|
|||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
@@ -1,11 +1,13 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue';
|
import {computed, nextTick, onMounted, onUnmounted, ref} from 'vue';
|
||||||
import { useDocumentStore } from '@/stores/documentStore';
|
import {useDocumentStore} from '@/stores/documentStore';
|
||||||
import { useI18n } from 'vue-i18n';
|
import {useI18n} from 'vue-i18n';
|
||||||
import type { Document } from '@/../bindings/voidraft/internal/models/models';
|
import type {Document} from '@/../bindings/voidraft/internal/models/models';
|
||||||
|
import {useWindowStore} from "@/stores/windowStore";
|
||||||
|
|
||||||
const documentStore = useDocumentStore();
|
const documentStore = useDocumentStore();
|
||||||
const { t } = useI18n();
|
const windowStore = useWindowStore();
|
||||||
|
const {t} = useI18n();
|
||||||
|
|
||||||
// 组件状态
|
// 组件状态
|
||||||
const showMenu = ref(false);
|
const showMenu = ref(false);
|
||||||
@@ -15,30 +17,33 @@ const editingId = ref<number | null>(null);
|
|||||||
const editingTitle = ref('');
|
const editingTitle = ref('');
|
||||||
const editInputRef = ref<HTMLInputElement>();
|
const editInputRef = ref<HTMLInputElement>();
|
||||||
const deleteConfirmId = ref<number | null>(null);
|
const deleteConfirmId = ref<number | null>(null);
|
||||||
|
// 添加错误提示状态
|
||||||
|
const alreadyOpenDocId = ref<number | null>(null);
|
||||||
|
const errorMessageTimer = ref<number | null>(null);
|
||||||
|
|
||||||
// 过滤后的文档列表 + 创建选项
|
// 过滤后的文档列表 + 创建选项
|
||||||
const filteredItems = computed(() => {
|
const filteredItems = computed(() => {
|
||||||
const docs = documentStore.documentList;
|
const docs = documentStore.documentList;
|
||||||
const query = inputValue.value.trim();
|
const query = inputValue.value.trim();
|
||||||
|
|
||||||
if (!query) {
|
if (!query) {
|
||||||
return docs;
|
return docs;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 过滤匹配的文档
|
// 过滤匹配的文档
|
||||||
const filtered = docs.filter(doc =>
|
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) {
|
if (!exactMatch && query.length > 0) {
|
||||||
return [
|
return [
|
||||||
{ id: -1, title: t('toolbar.createDocument') + ` "${query}"`, isCreateOption: true } as any,
|
{id: -1, title: t('toolbar.createDocument') + ` "${query}"`, isCreateOption: true} as any,
|
||||||
...filtered
|
...filtered
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
return filtered;
|
return filtered;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -65,6 +70,18 @@ const closeMenu = () => {
|
|||||||
editingId.value = null;
|
editingId.value = null;
|
||||||
editingTitle.value = '';
|
editingTitle.value = '';
|
||||||
deleteConfirmId.value = null;
|
deleteConfirmId.value = null;
|
||||||
|
|
||||||
|
// 清除错误状态和定时器
|
||||||
|
clearErrorMessage();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 清除错误提示和定时器
|
||||||
|
const clearErrorMessage = () => {
|
||||||
|
if (errorMessageTimer.value) {
|
||||||
|
clearTimeout(errorMessageTimer.value);
|
||||||
|
errorMessageTimer.value = null;
|
||||||
|
}
|
||||||
|
alreadyOpenDocId.value = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 切换菜单
|
// 切换菜单
|
||||||
@@ -90,6 +107,23 @@ const selectItem = async (item: any) => {
|
|||||||
// 选择文档
|
// 选择文档
|
||||||
const selectDoc = async (doc: Document) => {
|
const selectDoc = async (doc: Document) => {
|
||||||
try {
|
try {
|
||||||
|
const hasOpen = await windowStore.isDocumentWindowOpen(doc.id);
|
||||||
|
if (hasOpen) {
|
||||||
|
// 设置错误状态并启动定时器
|
||||||
|
alreadyOpenDocId.value = doc.id;
|
||||||
|
|
||||||
|
// 清除之前的定时器(如果存在)
|
||||||
|
if (errorMessageTimer.value) {
|
||||||
|
clearTimeout(errorMessageTimer.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置新的定时器,3秒后清除错误信息
|
||||||
|
errorMessageTimer.value = window.setTimeout(() => {
|
||||||
|
alreadyOpenDocId.value = null;
|
||||||
|
errorMessageTimer.value = null;
|
||||||
|
}, 3000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const success = await documentStore.openDocument(doc.id);
|
const success = await documentStore.openDocument(doc.id);
|
||||||
if (success) {
|
if (success) {
|
||||||
closeMenu();
|
closeMenu();
|
||||||
@@ -108,7 +142,7 @@ const validateTitle = (title: string): string | null => {
|
|||||||
return t('toolbar.documentNameRequired');
|
return t('toolbar.documentNameRequired');
|
||||||
}
|
}
|
||||||
if (title.trim().length > MAX_TITLE_LENGTH) {
|
if (title.trim().length > MAX_TITLE_LENGTH) {
|
||||||
return t('toolbar.documentNameTooLong', { max: MAX_TITLE_LENGTH });
|
return t('toolbar.documentNameTooLong', {max: MAX_TITLE_LENGTH});
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
@@ -118,7 +152,6 @@ const createDoc = async (title: string) => {
|
|||||||
const trimmedTitle = title.trim();
|
const trimmedTitle = title.trim();
|
||||||
const error = validateTitle(trimmedTitle);
|
const error = validateTitle(trimmedTitle);
|
||||||
if (error) {
|
if (error) {
|
||||||
console.error('创建文档失败:', error);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,16 +198,26 @@ const saveEdit = async () => {
|
|||||||
editingTitle.value = '';
|
editingTitle.value = '';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 在新窗口打开文档
|
||||||
|
const openInNewWindow = async (doc: Document, event: Event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
try {
|
||||||
|
await documentStore.openDocumentInNewWindow(doc.id);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to open document in new window:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 处理删除 - 简化确认机制
|
// 处理删除 - 简化确认机制
|
||||||
const handleDelete = async (doc: Document, event: Event) => {
|
const handleDelete = async (doc: Document, event: Event) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
||||||
if (deleteConfirmId.value === doc.id) {
|
if (deleteConfirmId.value === doc.id) {
|
||||||
// 确认删除
|
// 确认删除
|
||||||
try {
|
try {
|
||||||
await documentStore.deleteDocument(doc.id);
|
await documentStore.deleteDocument(doc.id);
|
||||||
await documentStore.updateDocuments();
|
await documentStore.updateDocuments();
|
||||||
|
|
||||||
// 如果删除的是当前文档,切换到第一个文档
|
// 如果删除的是当前文档,切换到第一个文档
|
||||||
if (documentStore.currentDocument?.id === doc.id && documentStore.documentList.length > 0) {
|
if (documentStore.currentDocument?.id === doc.id && documentStore.documentList.length > 0) {
|
||||||
const firstDoc = documentStore.documentList[0];
|
const firstDoc = documentStore.documentList[0];
|
||||||
@@ -190,7 +233,7 @@ const handleDelete = async (doc: Document, event: Event) => {
|
|||||||
// 进入确认状态
|
// 进入确认状态
|
||||||
deleteConfirmId.value = doc.id;
|
deleteConfirmId.value = doc.id;
|
||||||
editingId.value = null; // 清除编辑状态
|
editingId.value = null; // 清除编辑状态
|
||||||
|
|
||||||
// 3秒后自动取消确认状态
|
// 3秒后自动取消确认状态
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (deleteConfirmId.value === doc.id) {
|
if (deleteConfirmId.value === doc.id) {
|
||||||
@@ -203,11 +246,11 @@ const handleDelete = async (doc: Document, event: Event) => {
|
|||||||
// 格式化时间
|
// 格式化时间
|
||||||
const formatTime = (dateString: string | null) => {
|
const formatTime = (dateString: string | null) => {
|
||||||
if (!dateString) return t('toolbar.unknownTime');
|
if (!dateString) return t('toolbar.unknownTime');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const date = new Date(dateString);
|
const date = new Date(dateString);
|
||||||
if (isNaN(date.getTime())) return t('toolbar.invalidDate');
|
if (isNaN(date.getTime())) return t('toolbar.invalidDate');
|
||||||
|
|
||||||
// 根据当前语言显示时间格式
|
// 根据当前语言显示时间格式
|
||||||
const locale = t('locale') === 'zh-CN' ? 'zh-CN' : 'en-US';
|
const locale = t('locale') === 'zh-CN' ? 'zh-CN' : 'en-US';
|
||||||
return date.toLocaleString(locale, {
|
return date.toLocaleString(locale, {
|
||||||
@@ -285,6 +328,10 @@ onMounted(() => {
|
|||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
document.removeEventListener('click', handleClickOutside);
|
document.removeEventListener('click', handleClickOutside);
|
||||||
document.removeEventListener('keydown', handleKeydown);
|
document.removeEventListener('keydown', handleKeydown);
|
||||||
|
// 清理定时器
|
||||||
|
if (errorMessageTimer.value) {
|
||||||
|
clearTimeout(errorMessageTimer.value);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -300,79 +347,103 @@ onUnmounted(() => {
|
|||||||
<div v-if="showMenu" class="doc-menu">
|
<div v-if="showMenu" class="doc-menu">
|
||||||
<!-- 输入框 -->
|
<!-- 输入框 -->
|
||||||
<div class="input-box">
|
<div class="input-box">
|
||||||
<input
|
<input
|
||||||
ref="inputRef"
|
ref="inputRef"
|
||||||
v-model="inputValue"
|
v-model="inputValue"
|
||||||
type="text"
|
type="text"
|
||||||
class="main-input"
|
class="main-input"
|
||||||
:placeholder="t('toolbar.searchOrCreateDocument')"
|
:placeholder="t('toolbar.searchOrCreateDocument')"
|
||||||
:maxlength="MAX_TITLE_LENGTH"
|
:maxlength="MAX_TITLE_LENGTH"
|
||||||
@keydown="handleInputKeydown"
|
@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">
|
<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>
|
<circle cx="11" cy="11" r="8"></circle>
|
||||||
<path d="m21 21-4.35-4.35"></path>
|
<path d="m21 21-4.35-4.35"></path>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 项目列表 -->
|
<!-- 项目列表 -->
|
||||||
<div class="item-list">
|
<div class="item-list">
|
||||||
<div
|
<div
|
||||||
v-for="item in filteredItems"
|
v-for="item in filteredItems"
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
class="list-item"
|
class="list-item"
|
||||||
:class="{
|
:class="{
|
||||||
'active': !item.isCreateOption && documentStore.currentDocument?.id === item.id,
|
'active': !item.isCreateOption && documentStore.currentDocument?.id === item.id,
|
||||||
'create-item': item.isCreateOption
|
'create-item': item.isCreateOption
|
||||||
}"
|
}"
|
||||||
@click="selectItem(item)"
|
@click="selectItem(item)"
|
||||||
>
|
>
|
||||||
<!-- 创建选项 -->
|
<!-- 创建选项 -->
|
||||||
<div v-if="item.isCreateOption" class="create-option">
|
<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">
|
<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="M5 12h14"></path>
|
||||||
<path d="M12 5v14"></path>
|
<path d="M12 5v14"></path>
|
||||||
</svg>
|
</svg>
|
||||||
<span>{{ item.title }}</span>
|
<span>{{ item.title }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 文档项 -->
|
<!-- 文档项 -->
|
||||||
<div v-else class="doc-item-content">
|
<div v-else class="doc-item-content">
|
||||||
<!-- 普通显示 -->
|
<!-- 普通显示 -->
|
||||||
<div v-if="editingId !== item.id" class="doc-info">
|
<div v-if="editingId !== item.id" class="doc-info">
|
||||||
<div class="doc-title">{{ item.title }}</div>
|
<div class="doc-title">{{ item.title }}</div>
|
||||||
<div class="doc-date">{{ formatTime(item.updatedAt) }}</div>
|
<!-- 根据状态显示错误信息或时间 -->
|
||||||
|
<div v-if="alreadyOpenDocId === item.id" class="doc-error">
|
||||||
|
{{ t('toolbar.alreadyOpenInNewWindow') }}
|
||||||
|
</div>
|
||||||
|
<div v-else class="doc-date">{{ formatTime(item.updatedAt) }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 编辑状态 -->
|
<!-- 编辑状态 -->
|
||||||
<div v-else class="doc-edit">
|
<div v-else class="doc-edit">
|
||||||
<input
|
<input
|
||||||
:ref="el => editInputRef = el as HTMLInputElement"
|
:ref="el => editInputRef = el as HTMLInputElement"
|
||||||
v-model="editingTitle"
|
v-model="editingTitle"
|
||||||
type="text"
|
type="text"
|
||||||
class="edit-input"
|
class="edit-input"
|
||||||
:maxlength="MAX_TITLE_LENGTH"
|
:maxlength="MAX_TITLE_LENGTH"
|
||||||
@keydown="handleEditKeydown"
|
@keydown="handleEditKeydown"
|
||||||
@blur="saveEdit"
|
@blur="saveEdit"
|
||||||
@click.stop
|
@click.stop
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 操作按钮 -->
|
<!-- 操作按钮 -->
|
||||||
<div v-if="editingId !== item.id" class="doc-actions">
|
<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')">
|
<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">
|
<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>
|
<path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"></path>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="documentStore.documentList.length > 1 && item.id !== 1"
|
v-if="documentStore.documentList.length > 1 && item.id !== 1"
|
||||||
class="action-btn delete-btn"
|
class="action-btn delete-btn"
|
||||||
:class="{ 'delete-confirm': deleteConfirmId === item.id }"
|
:class="{ 'delete-confirm': deleteConfirmId === item.id }"
|
||||||
@click="handleDelete(item, $event)"
|
@click="handleDelete(item, $event)"
|
||||||
:title="deleteConfirmId === item.id ? t('toolbar.confirmDelete') : t('toolbar.delete')"
|
: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">
|
<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>
|
<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>
|
<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>
|
</svg>
|
||||||
@@ -381,12 +452,12 @@ onUnmounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 空状态 -->
|
<!-- 空状态 -->
|
||||||
<div v-if="filteredItems.length === 0" class="empty">
|
<div v-if="filteredItems.length === 0" class="empty">
|
||||||
{{ t('toolbar.noDocumentFound') }}
|
{{ t('toolbar.noDocumentFound') }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 加载状态 -->
|
<!-- 加载状态 -->
|
||||||
<div v-if="documentStore.isLoading" class="loading">
|
<div v-if="documentStore.isLoading" class="loading">
|
||||||
{{ t('toolbar.loading') }}
|
{{ t('toolbar.loading') }}
|
||||||
@@ -399,7 +470,7 @@ onUnmounted(() => {
|
|||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.document-selector {
|
.document-selector {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
.doc-btn {
|
.doc-btn {
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
@@ -411,30 +482,30 @@ onUnmounted(() => {
|
|||||||
gap: 3px;
|
gap: 3px;
|
||||||
padding: 2px 4px;
|
padding: 2px 4px;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: var(--border-color);
|
background-color: var(--border-color);
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
}
|
}
|
||||||
|
|
||||||
.doc-name {
|
.doc-name {
|
||||||
max-width: 80px;
|
max-width: 80px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.arrow {
|
.arrow {
|
||||||
font-size: 8px;
|
font-size: 8px;
|
||||||
margin-left: 2px;
|
margin-left: 2px;
|
||||||
transition: transform 0.2s ease;
|
transition: transform 0.2s ease;
|
||||||
|
|
||||||
&.open {
|
&.open {
|
||||||
transform: rotate(180deg);
|
transform: rotate(180deg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.doc-menu {
|
.doc-menu {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 100%;
|
bottom: 100%;
|
||||||
@@ -448,12 +519,12 @@ onUnmounted(() => {
|
|||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
.input-box {
|
.input-box {
|
||||||
position: relative;
|
position: relative;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
border-bottom: 1px solid var(--border-color);
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
|
||||||
.main-input {
|
.main-input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
@@ -464,16 +535,16 @@ onUnmounted(() => {
|
|||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
outline: none;
|
outline: none;
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
border-color: var(--text-muted);
|
border-color: var(--text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
&::placeholder {
|
&::placeholder {
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-icon {
|
.input-icon {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 14px;
|
left: 14px;
|
||||||
@@ -483,34 +554,34 @@ onUnmounted(() => {
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.item-list {
|
.item-list {
|
||||||
max-height: 240px;
|
max-height: 240px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
|
||||||
.list-item {
|
.list-item {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border-bottom: 1px solid var(--border-color);
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: var(--bg-hover);
|
background-color: var(--bg-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
background-color: var(--selection-bg);
|
background-color: var(--selection-bg);
|
||||||
|
|
||||||
.doc-item-content .doc-info {
|
.doc-item-content .doc-info {
|
||||||
.doc-title {
|
.doc-title {
|
||||||
color: var(--selection-text);
|
color: var(--selection-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.doc-date {
|
.doc-date, .doc-error {
|
||||||
color: var(--selection-text);
|
color: var(--selection-text);
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.create-item {
|
&.create-item {
|
||||||
.create-option {
|
.create-option {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -519,24 +590,24 @@ onUnmounted(() => {
|
|||||||
padding: 8px 8px;
|
padding: 8px 8px;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.doc-item-content {
|
.doc-item-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 8px 8px;
|
padding: 8px 8px;
|
||||||
|
|
||||||
.doc-info {
|
.doc-info {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
|
||||||
.doc-title {
|
.doc-title {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
margin-bottom: 2px;
|
margin-bottom: 2px;
|
||||||
@@ -545,17 +616,24 @@ onUnmounted(() => {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
.doc-date {
|
.doc-date {
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.doc-error {
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--text-danger);
|
||||||
|
font-weight: 500;
|
||||||
|
animation: fadeInOut 3s forwards;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.doc-edit {
|
.doc-edit {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|
||||||
.edit-input {
|
.edit-input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
@@ -566,19 +644,19 @@ onUnmounted(() => {
|
|||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
outline: none;
|
outline: none;
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
border-color: var(--text-muted);
|
border-color: var(--text-muted);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.doc-actions {
|
.doc-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: opacity 0.2s ease;
|
transition: opacity 0.2s ease;
|
||||||
|
|
||||||
.action-btn {
|
.action-btn {
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
@@ -591,31 +669,31 @@ onUnmounted(() => {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
min-width: 20px;
|
min-width: 20px;
|
||||||
min-height: 20px;
|
min-height: 20px;
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
width: 12px;
|
width: 12px;
|
||||||
height: 12px;
|
height: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: var(--border-color);
|
background-color: var(--border-color);
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.delete-btn:hover {
|
&.delete-btn:hover {
|
||||||
color: var(--text-danger);
|
color: var(--text-danger);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.delete-confirm {
|
&.delete-confirm {
|
||||||
background-color: var(--text-danger);
|
background-color: var(--text-danger);
|
||||||
color: white;
|
color: white;
|
||||||
|
|
||||||
.confirm-text {
|
.confirm-text {
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
padding: 0 4px;
|
padding: 0 4px;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: var(--text-danger);
|
background-color: var(--text-danger);
|
||||||
color: white !important; // 确保确认状态下文字始终为白色
|
color: white !important; // 确保确认状态下文字始终为白色
|
||||||
@@ -625,12 +703,12 @@ onUnmounted(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover .doc-actions {
|
&:hover .doc-actions {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty, .loading {
|
.empty, .loading {
|
||||||
padding: 12px 8px;
|
padding: 12px 8px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@@ -639,25 +717,37 @@ onUnmounted(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 自定义滚动条
|
// 自定义滚动条
|
||||||
.item-list {
|
.item-list {
|
||||||
&::-webkit-scrollbar {
|
&::-webkit-scrollbar {
|
||||||
width: 4px;
|
width: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&::-webkit-scrollbar-track {
|
&::-webkit-scrollbar-track {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
&::-webkit-scrollbar-thumb {
|
&::-webkit-scrollbar-thumb {
|
||||||
background-color: var(--border-color);
|
background-color: var(--border-color);
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: var(--text-muted);
|
background-color: var(--text-muted);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
|
||||||
|
@keyframes fadeInOut {
|
||||||
|
0% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
70% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@@ -4,6 +4,7 @@ import {onMounted, onUnmounted, ref, watch, computed} from 'vue';
|
|||||||
import {useConfigStore} from '@/stores/configStore';
|
import {useConfigStore} from '@/stores/configStore';
|
||||||
import {useEditorStore} from '@/stores/editorStore';
|
import {useEditorStore} from '@/stores/editorStore';
|
||||||
import {useUpdateStore} from '@/stores/updateStore';
|
import {useUpdateStore} from '@/stores/updateStore';
|
||||||
|
import {useWindowStore} from '@/stores/windowStore';
|
||||||
import * as runtime from '@wailsio/runtime';
|
import * as runtime from '@wailsio/runtime';
|
||||||
import {useRouter} from 'vue-router';
|
import {useRouter} from 'vue-router';
|
||||||
import BlockLanguageSelector from './BlockLanguageSelector.vue';
|
import BlockLanguageSelector from './BlockLanguageSelector.vue';
|
||||||
@@ -15,20 +16,23 @@ import {formatBlockContent} from '@/views/editor/extensions/codeblock/formatCode
|
|||||||
const editorStore = useEditorStore();
|
const editorStore = useEditorStore();
|
||||||
const configStore = useConfigStore();
|
const configStore = useConfigStore();
|
||||||
const updateStore = useUpdateStore();
|
const updateStore = useUpdateStore();
|
||||||
|
const windowStore = useWindowStore();
|
||||||
const {t} = useI18n();
|
const {t} = useI18n();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
// 当前块是否支持格式化的响应式状态
|
// 当前块是否支持格式化的响应式状态
|
||||||
const canFormatCurrentBlock = ref(false);
|
const canFormatCurrentBlock = ref(false);
|
||||||
|
|
||||||
// 窗口置顶状态管理
|
// 窗口置顶状态管理(仅当前窗口,不同步到配置文件)
|
||||||
|
const isCurrentWindowOnTop = ref(false);
|
||||||
|
|
||||||
const setWindowAlwaysOnTop = async (isTop: boolean) => {
|
const setWindowAlwaysOnTop = async (isTop: boolean) => {
|
||||||
await runtime.Window.SetAlwaysOnTop(isTop);
|
await runtime.Window.SetAlwaysOnTop(isTop);
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleAlwaysOnTop = async () => {
|
const toggleAlwaysOnTop = async () => {
|
||||||
await configStore.toggleAlwaysOnTop();
|
isCurrentWindowOnTop.value = !isCurrentWindowOnTop.value;
|
||||||
await runtime.Window.SetAlwaysOnTop(configStore.config.general.alwaysOnTop);
|
await runtime.Window.SetAlwaysOnTop(isCurrentWindowOnTop.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 跳转到设置页面
|
// 跳转到设置页面
|
||||||
@@ -136,20 +140,12 @@ onUnmounted(() => {
|
|||||||
cleanupListeners = [];
|
cleanupListeners = [];
|
||||||
});
|
});
|
||||||
|
|
||||||
// 监听置顶设置变化
|
// 组件加载后初始化置顶状态
|
||||||
watch(
|
|
||||||
() => configStore.config.general.alwaysOnTop,
|
|
||||||
async (newValue) => {
|
|
||||||
if (isLoaded.value) {
|
|
||||||
await runtime.Window.SetAlwaysOnTop(newValue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// 组件加载后应用置顶设置
|
|
||||||
watch(isLoaded, async (loaded) => {
|
watch(isLoaded, async (loaded) => {
|
||||||
if (loaded && configStore.config.general.alwaysOnTop) {
|
if (loaded) {
|
||||||
await setWindowAlwaysOnTop(true);
|
// 初始化时从配置文件读取置顶状态
|
||||||
|
isCurrentWindowOnTop.value = configStore.config.general.alwaysOnTop;
|
||||||
|
await setWindowAlwaysOnTop(isCurrentWindowOnTop.value);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -197,7 +193,7 @@ const updateButtonTitle = computed(() => {
|
|||||||
</span>
|
</span>
|
||||||
|
|
||||||
<!-- 文档选择器 -->
|
<!-- 文档选择器 -->
|
||||||
<DocumentSelector/>
|
<DocumentSelector v-if="windowStore.isMainWindow"/>
|
||||||
|
|
||||||
<!-- 块语言选择器 -->
|
<!-- 块语言选择器 -->
|
||||||
<BlockLanguageSelector/>
|
<BlockLanguageSelector/>
|
||||||
@@ -265,7 +261,7 @@ const updateButtonTitle = computed(() => {
|
|||||||
<!-- 窗口置顶图标按钮 -->
|
<!-- 窗口置顶图标按钮 -->
|
||||||
<div
|
<div
|
||||||
class="pin-button"
|
class="pin-button"
|
||||||
:class="{ 'active': configStore.config.general.alwaysOnTop }"
|
:class="{ 'active': isCurrentWindowOnTop }"
|
||||||
:title="t('toolbar.alwaysOnTop')"
|
:title="t('toolbar.alwaysOnTop')"
|
||||||
@click="toggleAlwaysOnTop"
|
@click="toggleAlwaysOnTop"
|
||||||
>
|
>
|
||||||
@@ -276,7 +272,7 @@ const updateButtonTitle = computed(() => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<button class="settings-btn" :title="t('toolbar.settings')" @click="goToSettings">
|
<button v-if="windowStore.isMainWindow" class="settings-btn" :title="t('toolbar.settings')" @click="goToSettings">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none"
|
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none"
|
||||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
<circle cx="12" cy="12" r="3"></circle>
|
<circle cx="12" cy="12" r="3"></circle>
|
||||||
|
@@ -29,6 +29,8 @@ export default {
|
|||||||
delete: 'Delete',
|
delete: 'Delete',
|
||||||
confirm: 'Confirm',
|
confirm: 'Confirm',
|
||||||
confirmDelete: 'Click again to confirm delete',
|
confirmDelete: 'Click again to confirm delete',
|
||||||
|
openInNewWindow: 'Open in New Window',
|
||||||
|
alreadyOpenInNewWindow: 'Already open in another window',
|
||||||
documentNameTooLong: 'Document name cannot exceed {max} characters',
|
documentNameTooLong: 'Document name cannot exceed {max} characters',
|
||||||
documentNameRequired: 'Document name cannot be empty',
|
documentNameRequired: 'Document name cannot be empty',
|
||||||
cannotDeleteLastDocument: 'Cannot delete the last document',
|
cannotDeleteLastDocument: 'Cannot delete the last document',
|
||||||
@@ -143,6 +145,26 @@ export default {
|
|||||||
fontFamilyDescription: 'Choose editor font family',
|
fontFamilyDescription: 'Choose editor font family',
|
||||||
fontWeight: 'Font Weight',
|
fontWeight: 'Font Weight',
|
||||||
fontWeightDescription: 'Set the thickness of the font',
|
fontWeightDescription: 'Set the thickness of the font',
|
||||||
|
fontWeights: {
|
||||||
|
'100': 'Thin (100)',
|
||||||
|
'200': 'Extra Light (200)',
|
||||||
|
'300': 'Light (300)',
|
||||||
|
'normal': 'Regular (400)',
|
||||||
|
'500': 'Medium (500)',
|
||||||
|
'600': 'Semi Bold (600)',
|
||||||
|
'bold': 'Bold (700)',
|
||||||
|
'800': 'Extra Bold (800)',
|
||||||
|
'900': 'Black (900)'
|
||||||
|
},
|
||||||
|
fontFamilies: {
|
||||||
|
harmonyOS: 'HarmonyOS Sans',
|
||||||
|
microsoftYahei: 'Microsoft YaHei',
|
||||||
|
pingfang: 'PingFang SC',
|
||||||
|
jetbrainsMono: 'JetBrains Mono',
|
||||||
|
firaCode: 'Fira Code',
|
||||||
|
sourceCodePro: 'Source Code Pro',
|
||||||
|
cascadiaCode: 'Cascadia Code'
|
||||||
|
},
|
||||||
lineHeight: 'Line Height',
|
lineHeight: 'Line Height',
|
||||||
lineHeightDescription: 'Set the spacing between text lines',
|
lineHeightDescription: 'Set the spacing between text lines',
|
||||||
tabSettings: 'Tab Settings',
|
tabSettings: 'Tab Settings',
|
||||||
@@ -176,13 +198,14 @@ export default {
|
|||||||
categoryTools: 'Tools',
|
categoryTools: 'Tools',
|
||||||
configuration: 'Configuration',
|
configuration: 'Configuration',
|
||||||
resetToDefault: 'Reset to Default Configuration',
|
resetToDefault: 'Reset to Default Configuration',
|
||||||
// Keep necessary extension interface translations, configuration items display in English directly
|
|
||||||
},
|
},
|
||||||
updateNow: 'Update Now',
|
updateNow: 'Update Now',
|
||||||
updating: 'Updating...',
|
updating: 'Updating...',
|
||||||
updateSuccess: 'Update Success',
|
updateSuccess: 'Update Success',
|
||||||
updateSuccessRestartRequired: 'Update has been successfully applied. Please restart the application.',
|
updateSuccessRestartRequired: 'Update has been successfully applied. Please restart the application.',
|
||||||
restartNow: 'Restart Now',
|
restartNow: 'Restart Now',
|
||||||
|
hotkeyPreview: 'Preview:',
|
||||||
|
none: 'None',
|
||||||
},
|
},
|
||||||
extensions: {
|
extensions: {
|
||||||
rainbowBrackets: {
|
rainbowBrackets: {
|
||||||
@@ -232,4 +255,4 @@ export default {
|
|||||||
memory: 'Memory',
|
memory: 'Memory',
|
||||||
clickToClean: 'Click to clean memory'
|
clickToClean: 'Click to clean memory'
|
||||||
}
|
}
|
||||||
};
|
};
|
@@ -29,6 +29,8 @@ export default {
|
|||||||
delete: '删除',
|
delete: '删除',
|
||||||
confirm: '确认',
|
confirm: '确认',
|
||||||
confirmDelete: '再次点击确认删除',
|
confirmDelete: '再次点击确认删除',
|
||||||
|
openInNewWindow: '在新窗口中打开',
|
||||||
|
alreadyOpenInNewWindow: '已在新窗口中打开',
|
||||||
documentNameTooLong: '文档名称不能超过{max}个字符',
|
documentNameTooLong: '文档名称不能超过{max}个字符',
|
||||||
documentNameRequired: '文档名称不能为空',
|
documentNameRequired: '文档名称不能为空',
|
||||||
cannotDeleteLastDocument: '无法删除最后一个文档',
|
cannotDeleteLastDocument: '无法删除最后一个文档',
|
||||||
@@ -182,8 +184,29 @@ export default {
|
|||||||
categoryTools: '工具扩展',
|
categoryTools: '工具扩展',
|
||||||
configuration: '配置',
|
configuration: '配置',
|
||||||
resetToDefault: '重置为默认配置',
|
resetToDefault: '重置为默认配置',
|
||||||
// 保留必要的扩展界面翻译,配置项直接显示英文
|
},
|
||||||
}
|
fontWeights: {
|
||||||
|
'100': '极细 (100)',
|
||||||
|
'200': '超细 (200)',
|
||||||
|
'300': '细 (300)',
|
||||||
|
'normal': '正常 (400)',
|
||||||
|
'500': '中等 (500)',
|
||||||
|
'600': '半粗 (600)',
|
||||||
|
'bold': '粗体 (700)',
|
||||||
|
'800': '超粗 (800)',
|
||||||
|
'900': '极粗 (900)'
|
||||||
|
},
|
||||||
|
fontFamilies: {
|
||||||
|
harmonyOS: '鸿蒙字体',
|
||||||
|
microsoftYahei: '微软雅黑',
|
||||||
|
pingfang: '苹方字体',
|
||||||
|
jetbrainsMono: 'JetBrains Mono',
|
||||||
|
firaCode: 'Fira Code',
|
||||||
|
sourceCodePro: 'Source Code Pro',
|
||||||
|
cascadiaCode: 'Cascadia Code'
|
||||||
|
},
|
||||||
|
hotkeyPreview: '预览:',
|
||||||
|
none: '无',
|
||||||
},
|
},
|
||||||
extensions: {
|
extensions: {
|
||||||
rainbowBrackets: {
|
rainbowBrackets: {
|
||||||
@@ -233,4 +256,4 @@ export default {
|
|||||||
memory: '内存',
|
memory: '内存',
|
||||||
clickToClean: '点击清理内存'
|
clickToClean: '点击清理内存'
|
||||||
}
|
}
|
||||||
};
|
};
|
@@ -95,26 +95,40 @@ const CONFIG_LIMITS = {
|
|||||||
tabType: {values: [TabType.TabTypeSpaces, TabType.TabTypeTab], default: TabType.TabTypeSpaces}
|
tabType: {values: [TabType.TabTypeSpaces, TabType.TabTypeTab], default: TabType.TabTypeSpaces}
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
// 常用字体选项
|
// 创建获取翻译的函数
|
||||||
export const FONT_OPTIONS = [
|
export const createFontOptions = (t: (key: string) => string) => [
|
||||||
{
|
{
|
||||||
label: '鸿蒙字体',
|
label: t('settings.fontFamilies.harmonyOS'),
|
||||||
value: '"HarmonyOS Sans SC", "HarmonyOS Sans", "Microsoft YaHei", "PingFang SC", "Helvetica Neue", Arial, sans-serif'
|
value: '"HarmonyOS Sans SC", "HarmonyOS Sans", "Microsoft YaHei", "PingFang SC", "Helvetica Neue", Arial, sans-serif'
|
||||||
},
|
},
|
||||||
{label: '微软雅黑', value: '"Microsoft YaHei", "PingFang SC", "Helvetica Neue", Arial, sans-serif'},
|
|
||||||
{label: '苹方字体', value: '"PingFang SC", "Microsoft YaHei", "Helvetica Neue", Arial, sans-serif'},
|
|
||||||
{
|
{
|
||||||
label: 'JetBrains Mono',
|
label: t('settings.fontFamilies.microsoftYahei'),
|
||||||
|
value: '"Microsoft YaHei", "PingFang SC", "Helvetica Neue", Arial, sans-serif'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('settings.fontFamilies.pingfang'),
|
||||||
|
value: '"PingFang SC", "Microsoft YaHei", "Helvetica Neue", Arial, sans-serif'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('settings.fontFamilies.jetbrainsMono'),
|
||||||
value: '"JetBrains Mono", "Fira Code", "SF Mono", Monaco, Consolas, "Ubuntu Mono", monospace'
|
value: '"JetBrains Mono", "Fira Code", "SF Mono", Monaco, Consolas, "Ubuntu Mono", monospace'
|
||||||
},
|
},
|
||||||
{label: 'Fira Code', value: '"Fira Code", "JetBrains Mono", "SF Mono", Monaco, Consolas, "Ubuntu Mono", monospace'},
|
|
||||||
{label: 'Source Code Pro', value: '"Source Code Pro", "SF Mono", Monaco, Consolas, "Ubuntu Mono", monospace'},
|
|
||||||
{label: 'Cascadia Code', value: '"Cascadia Code", "SF Mono", Monaco, Consolas, "Ubuntu Mono", monospace'},
|
|
||||||
{
|
{
|
||||||
label: '系统等宽字体',
|
label: t('settings.fontFamilies.firaCode'),
|
||||||
value: '"SF Mono", Monaco, "Cascadia Code", "Roboto Mono", Consolas, "Courier New", monospace'
|
value: '"Fira Code", "JetBrains Mono", "SF Mono", Monaco, Consolas, "Ubuntu Mono", monospace'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('settings.fontFamilies.sourceCodePro'),
|
||||||
|
value: '"Source Code Pro", "SF Mono", Monaco, Consolas, "Ubuntu Mono", monospace'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('settings.fontFamilies.cascadiaCode'),
|
||||||
|
value: '"Cascadia Code", "SF Mono", Monaco, Consolas, "Ubuntu Mono", monospace'
|
||||||
}
|
}
|
||||||
] as const;
|
];
|
||||||
|
|
||||||
|
// 常用字体选项
|
||||||
|
export const FONT_OPTIONS = createFontOptions((key) => key);
|
||||||
|
|
||||||
// 获取浏览器的默认语言
|
// 获取浏览器的默认语言
|
||||||
const getBrowserLanguage = (): SupportedLocaleType => {
|
const getBrowserLanguage = (): SupportedLocaleType => {
|
||||||
@@ -184,7 +198,7 @@ const DEFAULT_CONFIG: AppConfig = {
|
|||||||
|
|
||||||
|
|
||||||
export const useConfigStore = defineStore('config', () => {
|
export const useConfigStore = defineStore('config', () => {
|
||||||
const {locale} = useI18n();
|
const {locale, t} = useI18n();
|
||||||
|
|
||||||
// 响应式状态
|
// 响应式状态
|
||||||
const state = reactive({
|
const state = reactive({
|
||||||
@@ -192,6 +206,9 @@ export const useConfigStore = defineStore('config', () => {
|
|||||||
isLoading: false,
|
isLoading: false,
|
||||||
configLoaded: false
|
configLoaded: false
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 初始化FONT_OPTIONS国际化版本
|
||||||
|
const localizedFontOptions = computed(() => createFontOptions(t));
|
||||||
|
|
||||||
// 计算属性 - 使用工厂函数简化
|
// 计算属性 - 使用工厂函数简化
|
||||||
const createLimitComputed = (key: NumberConfigKey) => computed(() => CONFIG_LIMITS[key]);
|
const createLimitComputed = (key: NumberConfigKey) => computed(() => CONFIG_LIMITS[key]);
|
||||||
@@ -394,7 +411,8 @@ export const useConfigStore = defineStore('config', () => {
|
|||||||
config: computed(() => state.config),
|
config: computed(() => state.config),
|
||||||
configLoaded: computed(() => state.configLoaded),
|
configLoaded: computed(() => state.configLoaded),
|
||||||
isLoading: computed(() => state.isLoading),
|
isLoading: computed(() => state.isLoading),
|
||||||
|
localizedFontOptions,
|
||||||
|
|
||||||
// 限制常量
|
// 限制常量
|
||||||
...limits,
|
...limits,
|
||||||
|
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import {defineStore} from 'pinia';
|
import {defineStore} from 'pinia';
|
||||||
import {computed, ref} from 'vue';
|
import {computed, ref} from 'vue';
|
||||||
import {DocumentService} from '@/../bindings/voidraft/internal/services';
|
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/models';
|
||||||
|
|
||||||
const SCRATCH_DOCUMENT_ID = 1; // 默认草稿文档ID
|
const SCRATCH_DOCUMENT_ID = 1; // 默认草稿文档ID
|
||||||
@@ -50,6 +51,17 @@ export const useDocumentStore = defineStore('document', () => {
|
|||||||
|
|
||||||
// === 公共API ===
|
// === 公共API ===
|
||||||
|
|
||||||
|
// 在新窗口中打开文档
|
||||||
|
const openDocumentInNewWindow = async (docId: number): Promise<boolean> => {
|
||||||
|
try {
|
||||||
|
await OpenDocumentWindow(docId);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to open document in new window:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 更新文档列表
|
// 更新文档列表
|
||||||
const updateDocuments = async () => {
|
const updateDocuments = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -186,12 +198,15 @@ export const useDocumentStore = defineStore('document', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// === 初始化 ===
|
// === 初始化 ===
|
||||||
const initialize = async (): Promise<void> => {
|
const initialize = async (urlDocumentId?: number): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
await updateDocuments();
|
await updateDocuments();
|
||||||
|
|
||||||
// 如果存在持久化的文档ID,尝试打开该文档
|
// 优先使用URL参数中的文档ID
|
||||||
if (currentDocumentId.value && documents.value[currentDocumentId.value]) {
|
if (urlDocumentId && documents.value[urlDocumentId]) {
|
||||||
|
await openDocument(urlDocumentId);
|
||||||
|
} else if (currentDocumentId.value && documents.value[currentDocumentId.value]) {
|
||||||
|
// 如果URL中没有指定文档ID,则使用持久化的文档ID
|
||||||
await openDocument(currentDocumentId.value);
|
await openDocument(currentDocumentId.value);
|
||||||
} else {
|
} else {
|
||||||
// 否则获取第一个文档ID并打开
|
// 否则获取第一个文档ID并打开
|
||||||
@@ -218,6 +233,7 @@ export const useDocumentStore = defineStore('document', () => {
|
|||||||
// 方法
|
// 方法
|
||||||
updateDocuments,
|
updateDocuments,
|
||||||
openDocument,
|
openDocument,
|
||||||
|
openDocumentInNewWindow,
|
||||||
createNewDocument,
|
createNewDocument,
|
||||||
saveNewDocument,
|
saveNewDocument,
|
||||||
updateDocumentMetadata,
|
updateDocumentMetadata,
|
||||||
@@ -232,4 +248,4 @@ export const useDocumentStore = defineStore('document', () => {
|
|||||||
storage: localStorage,
|
storage: localStorage,
|
||||||
pick: ['currentDocumentId']
|
pick: ['currentDocumentId']
|
||||||
}
|
}
|
||||||
});
|
});
|
31
frontend/src/stores/windowStore.ts
Normal file
31
frontend/src/stores/windowStore.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import {computed} from 'vue';
|
||||||
|
import {defineStore} from 'pinia';
|
||||||
|
import {IsDocumentWindowOpen} from "@/../bindings/voidraft/internal/services/windowservice";
|
||||||
|
|
||||||
|
|
||||||
|
export const useWindowStore = defineStore('window', () => {
|
||||||
|
// 判断是否为主窗口
|
||||||
|
const isMainWindow = computed(() => {
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
return !urlParams.has('documentId');
|
||||||
|
});
|
||||||
|
|
||||||
|
// 获取当前窗口的documentId
|
||||||
|
const currentDocumentId = computed(() => {
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
return urlParams.get('documentId');
|
||||||
|
});
|
||||||
|
/**
|
||||||
|
* 判断文档窗口是否打开
|
||||||
|
* @param documentId 文档ID
|
||||||
|
*/
|
||||||
|
async function isDocumentWindowOpen(documentId: number) {
|
||||||
|
return IsDocumentWindowOpen(documentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
isMainWindow,
|
||||||
|
currentDocumentId,
|
||||||
|
isDocumentWindowOpen
|
||||||
|
};
|
||||||
|
});
|
@@ -5,10 +5,13 @@ import {useDocumentStore} from '@/stores/documentStore';
|
|||||||
import {useConfigStore} from '@/stores/configStore';
|
import {useConfigStore} from '@/stores/configStore';
|
||||||
import {createWheelZoomHandler} from './basic/wheelZoomExtension';
|
import {createWheelZoomHandler} from './basic/wheelZoomExtension';
|
||||||
import Toolbar from '@/components/toolbar/Toolbar.vue';
|
import Toolbar from '@/components/toolbar/Toolbar.vue';
|
||||||
|
import {useWindowStore} from "@/stores/windowStore";
|
||||||
|
|
||||||
const editorStore = useEditorStore();
|
const editorStore = useEditorStore();
|
||||||
const documentStore = useDocumentStore();
|
const documentStore = useDocumentStore();
|
||||||
const configStore = useConfigStore();
|
const configStore = useConfigStore();
|
||||||
|
const windowStore = useWindowStore();
|
||||||
|
|
||||||
|
|
||||||
const editorElement = ref<HTMLElement | null>(null);
|
const editorElement = ref<HTMLElement | null>(null);
|
||||||
|
|
||||||
@@ -21,8 +24,12 @@ const wheelHandler = createWheelZoomHandler(
|
|||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
if (!editorElement.value) return;
|
if (!editorElement.value) return;
|
||||||
|
|
||||||
// 初始化文档存储,会自动使用持久化的文档ID
|
// 从URL查询参数中获取documentId
|
||||||
await documentStore.initialize();
|
|
||||||
|
const urlDocumentId = windowStore.currentDocumentId ? parseInt(windowStore.currentDocumentId) : undefined;
|
||||||
|
|
||||||
|
// 初始化文档存储,优先使用URL参数中的文档ID
|
||||||
|
await documentStore.initialize(urlDocumentId);
|
||||||
|
|
||||||
// 设置编辑器容器
|
// 设置编辑器容器
|
||||||
editorStore.setEditorContainer(editorElement.value);
|
editorStore.setEditorContainer(editorElement.value);
|
||||||
@@ -69,4 +76,4 @@ onBeforeUnmount(() => {
|
|||||||
:deep(.cm-scroller) {
|
:deep(.cm-scroller) {
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
@@ -1,6 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useConfigStore } from '@/stores/configStore';
|
import { useConfigStore } from '@/stores/configStore';
|
||||||
import { FONT_OPTIONS } from '@/stores/configStore';
|
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import {computed, onMounted } from 'vue';
|
import {computed, onMounted } from 'vue';
|
||||||
import SettingSection from '../components/SettingSection.vue';
|
import SettingSection from '../components/SettingSection.vue';
|
||||||
@@ -19,7 +18,7 @@ onMounted(async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 字体选择选项
|
// 字体选择选项
|
||||||
const fontFamilyOptions = FONT_OPTIONS;
|
const fontFamilyOptions = computed(() => configStore.localizedFontOptions);
|
||||||
const currentFontFamily = computed(() => configStore.config.editing.fontFamily);
|
const currentFontFamily = computed(() => configStore.config.editing.fontFamily);
|
||||||
|
|
||||||
// 字体选择
|
// 字体选择
|
||||||
@@ -33,15 +32,15 @@ const handleFontFamilyChange = async (event: Event) => {
|
|||||||
|
|
||||||
// 字体粗细选项
|
// 字体粗细选项
|
||||||
const fontWeightOptions = [
|
const fontWeightOptions = [
|
||||||
{ value: '100', label: '极细 (100)' },
|
{ value: '100', label: t('settings.fontWeights.100') },
|
||||||
{ value: '200', label: '超细 (200)' },
|
{ value: '200', label: t('settings.fontWeights.200') },
|
||||||
{ value: '300', label: '细 (300)' },
|
{ value: '300', label: t('settings.fontWeights.300') },
|
||||||
{ value: 'normal', label: '正常 (400)' },
|
{ value: 'normal', label: t('settings.fontWeights.normal') },
|
||||||
{ value: '500', label: '中等 (500)' },
|
{ value: '500', label: t('settings.fontWeights.500') },
|
||||||
{ value: '600', label: '半粗 (600)' },
|
{ value: '600', label: t('settings.fontWeights.600') },
|
||||||
{ value: 'bold', label: '粗体 (700)' },
|
{ value: 'bold', label: t('settings.fontWeights.bold') },
|
||||||
{ value: '800', label: '超粗 (800)' },
|
{ value: '800', label: t('settings.fontWeights.800') },
|
||||||
{ value: '900', label: '极粗 (900)' }
|
{ value: '900', label: t('settings.fontWeights.900') }
|
||||||
];
|
];
|
||||||
|
|
||||||
// 字体粗细选择
|
// 字体粗细选择
|
||||||
@@ -213,7 +212,7 @@ const handleAutoSaveDelayChange = async (event: Event) => {
|
|||||||
</SettingSection>
|
</SettingSection>
|
||||||
|
|
||||||
<SettingSection :title="t('settings.saveOptions')">
|
<SettingSection :title="t('settings.saveOptions')">
|
||||||
<SettingItem :title="t('settings.autoSaveDelay')" :description="'定时保存间隔,每隔指定时间自动保存(仅在有变更时)'">
|
<SettingItem :title="t('settings.autoSaveDelay')">
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
class="number-input"
|
class="number-input"
|
||||||
|
@@ -321,8 +321,8 @@ onUnmounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="hotkey-preview">
|
<div class="hotkey-preview">
|
||||||
<span class="preview-label">预览:</span>
|
<span class="preview-label">{{ t('settings.hotkeyPreview') }}</span>
|
||||||
<span class="preview-hotkey">{{ hotkeyPreview || '无' }}</span>
|
<span class="preview-hotkey">{{ hotkeyPreview || t('settings.none') }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</SettingSection>
|
</SettingSection>
|
||||||
|
@@ -33,13 +33,13 @@ func RegisterTrayEvents(app *application.App, systray *application.SystemTray, m
|
|||||||
|
|
||||||
// RegisterTrayMenuEvents 注册系统托盘菜单事件
|
// RegisterTrayMenuEvents 注册系统托盘菜单事件
|
||||||
func RegisterTrayMenuEvents(app *application.App, menu *application.Menu, mainWindow *application.WebviewWindow) {
|
func RegisterTrayMenuEvents(app *application.App, menu *application.Menu, mainWindow *application.WebviewWindow) {
|
||||||
menu.Add("主窗口").OnClick(func(data *application.Context) {
|
menu.Add("Main window").OnClick(func(data *application.Context) {
|
||||||
mainWindow.Show()
|
mainWindow.Show()
|
||||||
})
|
})
|
||||||
|
|
||||||
menu.AddSeparator()
|
menu.AddSeparator()
|
||||||
|
|
||||||
menu.Add("退出").OnClick(func(data *application.Context) {
|
menu.Add("Quit").OnClick(func(data *application.Context) {
|
||||||
app.Quit()
|
app.Quit()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@@ -14,6 +14,7 @@ type ServiceManager struct {
|
|||||||
databaseService *DatabaseService
|
databaseService *DatabaseService
|
||||||
sqliteService *sqlite.Service
|
sqliteService *sqlite.Service
|
||||||
documentService *DocumentService
|
documentService *DocumentService
|
||||||
|
windowService *WindowService
|
||||||
migrationService *MigrationService
|
migrationService *MigrationService
|
||||||
systemService *SystemService
|
systemService *SystemService
|
||||||
hotkeyService *HotkeyService
|
hotkeyService *HotkeyService
|
||||||
@@ -47,6 +48,9 @@ func NewServiceManager() *ServiceManager {
|
|||||||
// 初始化文档服务
|
// 初始化文档服务
|
||||||
documentService := NewDocumentService(databaseService, logger)
|
documentService := NewDocumentService(databaseService, logger)
|
||||||
|
|
||||||
|
// 初始化窗口服务
|
||||||
|
windowService := NewWindowService(logger, documentService)
|
||||||
|
|
||||||
// 初始化系统服务
|
// 初始化系统服务
|
||||||
systemService := NewSystemService(logger)
|
systemService := NewSystemService(logger)
|
||||||
|
|
||||||
@@ -98,6 +102,7 @@ func NewServiceManager() *ServiceManager {
|
|||||||
databaseService: databaseService,
|
databaseService: databaseService,
|
||||||
sqliteService: sqliteService,
|
sqliteService: sqliteService,
|
||||||
documentService: documentService,
|
documentService: documentService,
|
||||||
|
windowService: windowService,
|
||||||
migrationService: migrationService,
|
migrationService: migrationService,
|
||||||
systemService: systemService,
|
systemService: systemService,
|
||||||
hotkeyService: hotkeyService,
|
hotkeyService: hotkeyService,
|
||||||
@@ -113,13 +118,13 @@ func NewServiceManager() *ServiceManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetServices 获取所有wails服务列表
|
// GetServices 获取所有wails服务列表
|
||||||
// 注意:服务启动顺序很重要,DatabaseService 必须在依赖数据库的服务之前启动
|
|
||||||
func (sm *ServiceManager) GetServices() []application.Service {
|
func (sm *ServiceManager) GetServices() []application.Service {
|
||||||
services := []application.Service{
|
services := []application.Service{
|
||||||
application.NewService(sm.configService),
|
application.NewService(sm.configService),
|
||||||
application.NewService(sm.sqliteService), // SQLite服务必须在数据库服务之前初始化
|
application.NewService(sm.sqliteService),
|
||||||
application.NewService(sm.databaseService), // 数据库服务必须在依赖它的服务之前初始化
|
application.NewService(sm.databaseService),
|
||||||
application.NewService(sm.documentService),
|
application.NewService(sm.documentService),
|
||||||
|
application.NewService(sm.windowService),
|
||||||
application.NewService(sm.keyBindingService),
|
application.NewService(sm.keyBindingService),
|
||||||
application.NewService(sm.extensionService),
|
application.NewService(sm.extensionService),
|
||||||
application.NewService(sm.migrationService),
|
application.NewService(sm.migrationService),
|
||||||
@@ -193,3 +198,13 @@ func (sm *ServiceManager) GetDatabaseService() *DatabaseService {
|
|||||||
func (sm *ServiceManager) GetSQLiteService() *sqlite.Service {
|
func (sm *ServiceManager) GetSQLiteService() *sqlite.Service {
|
||||||
return sm.sqliteService
|
return sm.sqliteService
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetWindowService 获取窗口服务实例
|
||||||
|
func (sm *ServiceManager) GetWindowService() *WindowService {
|
||||||
|
return sm.windowService
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDocumentService 获取文档服务实例
|
||||||
|
func (sm *ServiceManager) GetDocumentService() *DocumentService {
|
||||||
|
return sm.documentService
|
||||||
|
}
|
||||||
|
149
internal/services/window_service.go
Normal file
149
internal/services/window_service.go
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/wailsapp/wails/v3/pkg/application"
|
||||||
|
"github.com/wailsapp/wails/v3/pkg/events"
|
||||||
|
"github.com/wailsapp/wails/v3/pkg/services/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// WindowInfo 窗口信息
|
||||||
|
type WindowInfo struct {
|
||||||
|
Window *application.WebviewWindow
|
||||||
|
DocumentID int64
|
||||||
|
Title string
|
||||||
|
}
|
||||||
|
|
||||||
|
// WindowService 窗口管理服务
|
||||||
|
type WindowService struct {
|
||||||
|
logger *log.Service
|
||||||
|
documentService *DocumentService
|
||||||
|
app *application.App
|
||||||
|
mainWindow *application.WebviewWindow
|
||||||
|
windows map[int64]*WindowInfo // documentID -> WindowInfo
|
||||||
|
mu sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewWindowService 创建新的窗口服务实例
|
||||||
|
func NewWindowService(logger *log.Service, documentService *DocumentService) *WindowService {
|
||||||
|
if logger == nil {
|
||||||
|
logger = log.New()
|
||||||
|
}
|
||||||
|
|
||||||
|
return &WindowService{
|
||||||
|
logger: logger,
|
||||||
|
documentService: documentService,
|
||||||
|
windows: make(map[int64]*WindowInfo),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetAppReferences 设置应用和主窗口引用
|
||||||
|
func (ws *WindowService) SetAppReferences(app *application.App, mainWindow *application.WebviewWindow) {
|
||||||
|
ws.app = app
|
||||||
|
ws.mainWindow = mainWindow
|
||||||
|
}
|
||||||
|
|
||||||
|
// OpenDocumentWindow 为指定文档ID打开新窗口
|
||||||
|
func (ws *WindowService) OpenDocumentWindow(documentID int64) error {
|
||||||
|
ws.mu.Lock()
|
||||||
|
defer ws.mu.Unlock()
|
||||||
|
|
||||||
|
// 检查窗口是否已经存在
|
||||||
|
if windowInfo, exists := ws.windows[documentID]; exists {
|
||||||
|
// 窗口已存在,显示并聚焦
|
||||||
|
windowInfo.Window.Show()
|
||||||
|
windowInfo.Window.Restore()
|
||||||
|
windowInfo.Window.Focus()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取文档信息
|
||||||
|
doc, err := ws.documentService.GetDocumentByID(documentID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get document: %w", err)
|
||||||
|
}
|
||||||
|
if doc == nil {
|
||||||
|
return fmt.Errorf("document not found: %d", documentID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建新窗口
|
||||||
|
newWindow := ws.app.Window.NewWithOptions(application.WebviewWindowOptions{
|
||||||
|
Title: fmt.Sprintf("voidraft - %s", doc.Title),
|
||||||
|
Width: 700,
|
||||||
|
Height: 800,
|
||||||
|
Hidden: false,
|
||||||
|
Frameless: true,
|
||||||
|
DevToolsEnabled: false,
|
||||||
|
DefaultContextMenuDisabled: false,
|
||||||
|
Mac: application.MacWindow{
|
||||||
|
InvisibleTitleBarHeight: 50,
|
||||||
|
Backdrop: application.MacBackdropTranslucent,
|
||||||
|
TitleBar: application.MacTitleBarHiddenInset,
|
||||||
|
},
|
||||||
|
Windows: application.WindowsWindow{
|
||||||
|
Theme: application.SystemDefault,
|
||||||
|
},
|
||||||
|
BackgroundColour: application.NewRGB(27, 38, 54),
|
||||||
|
URL: fmt.Sprintf("/?documentId=%d", documentID),
|
||||||
|
})
|
||||||
|
|
||||||
|
newWindow.Center()
|
||||||
|
|
||||||
|
ws.app.Window.Add(newWindow)
|
||||||
|
|
||||||
|
// 保存窗口信息
|
||||||
|
windowInfo := &WindowInfo{
|
||||||
|
Window: newWindow,
|
||||||
|
DocumentID: documentID,
|
||||||
|
Title: doc.Title,
|
||||||
|
}
|
||||||
|
ws.windows[documentID] = windowInfo
|
||||||
|
|
||||||
|
// 注册窗口关闭事件
|
||||||
|
ws.registerWindowEvents(newWindow, documentID)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// registerWindowEvents 注册窗口事件
|
||||||
|
func (ws *WindowService) registerWindowEvents(window *application.WebviewWindow, documentID int64) {
|
||||||
|
// 注册窗口关闭事件
|
||||||
|
window.RegisterHook(events.Common.WindowClosing, func(event *application.WindowEvent) {
|
||||||
|
ws.onWindowClosing(documentID)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// onWindowClosing 处理窗口关闭事件
|
||||||
|
func (ws *WindowService) onWindowClosing(documentID int64) {
|
||||||
|
ws.mu.Lock()
|
||||||
|
defer ws.mu.Unlock()
|
||||||
|
windowInfo, exists := ws.windows[documentID]
|
||||||
|
if exists {
|
||||||
|
windowInfo.Window.Close()
|
||||||
|
delete(ws.windows, documentID)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetOpenWindows 获取所有打开的窗口信息
|
||||||
|
func (ws *WindowService) GetOpenWindows() []WindowInfo {
|
||||||
|
ws.mu.RLock()
|
||||||
|
defer ws.mu.RUnlock()
|
||||||
|
|
||||||
|
var windows []WindowInfo
|
||||||
|
for _, windowInfo := range ws.windows {
|
||||||
|
windows = append(windows, *windowInfo)
|
||||||
|
}
|
||||||
|
return windows
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsDocumentWindowOpen 检查指定文档的窗口是否已打开
|
||||||
|
func (ws *WindowService) IsDocumentWindowOpen(documentID int64) bool {
|
||||||
|
ws.mu.RLock()
|
||||||
|
defer ws.mu.RUnlock()
|
||||||
|
|
||||||
|
_, exists := ws.windows[documentID]
|
||||||
|
return exists
|
||||||
|
}
|
20
main.go
20
main.go
@@ -32,7 +32,6 @@ func main() {
|
|||||||
0x0e, 0x0f, 0x0c, 0x0d, 0x0a, 0x0b, 0x08, 0x09,
|
0x0e, 0x0f, 0x0c, 0x0d, 0x0a, 0x0b, 0x08, 0x09,
|
||||||
0x06, 0x07, 0x04, 0x05, 0x02, 0x03, 0x00, 0x01,
|
0x06, 0x07, 0x04, 0x05, 0x02, 0x03, 0x00, 0x01,
|
||||||
}
|
}
|
||||||
var window *application.WebviewWindow
|
|
||||||
// Create a new Wails application by providing the necessary options.
|
// Create a new Wails application by providing the necessary options.
|
||||||
// Variables 'Name' and 'Description' are for application metadata.
|
// Variables 'Name' and 'Description' are for application metadata.
|
||||||
// 'Assets' configures the asset server with the 'FS' variable pointing to the frontend files.
|
// 'Assets' configures the asset server with the 'FS' variable pointing to the frontend files.
|
||||||
@@ -52,18 +51,6 @@ func main() {
|
|||||||
SingleInstance: &application.SingleInstanceOptions{
|
SingleInstance: &application.SingleInstanceOptions{
|
||||||
UniqueID: "com.voidraft",
|
UniqueID: "com.voidraft",
|
||||||
EncryptionKey: encryptionKey,
|
EncryptionKey: encryptionKey,
|
||||||
OnSecondInstanceLaunch: func(data application.SecondInstanceData) {
|
|
||||||
if window != nil {
|
|
||||||
window.EmitEvent("secondInstanceLaunched", data)
|
|
||||||
window.Restore()
|
|
||||||
window.Focus()
|
|
||||||
}
|
|
||||||
log.Printf("Second instance launched with args: %v\n", data.Args)
|
|
||||||
log.Printf("Working directory: %s\n", data.WorkingDir)
|
|
||||||
if data.AdditionalData != nil {
|
|
||||||
log.Printf("Additional data: %v\n", data.AdditionalData)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
AdditionalData: map[string]string{
|
AdditionalData: map[string]string{
|
||||||
"launchtime": time.Now().Local().String(),
|
"launchtime": time.Now().Local().String(),
|
||||||
},
|
},
|
||||||
@@ -100,6 +87,10 @@ func main() {
|
|||||||
trayService := serviceManager.GetTrayService()
|
trayService := serviceManager.GetTrayService()
|
||||||
trayService.SetAppReferences(app, mainWindow)
|
trayService.SetAppReferences(app, mainWindow)
|
||||||
|
|
||||||
|
// 获取窗口服务并设置应用引用
|
||||||
|
windowService := serviceManager.GetWindowService()
|
||||||
|
windowService.SetAppReferences(app, mainWindow)
|
||||||
|
|
||||||
// 设置系统托盘
|
// 设置系统托盘
|
||||||
systray.SetupSystemTray(app, mainWindow, assets, trayService)
|
systray.SetupSystemTray(app, mainWindow, assets, trayService)
|
||||||
|
|
||||||
@@ -114,9 +105,6 @@ func main() {
|
|||||||
dialogService := serviceManager.GetDialogService()
|
dialogService := serviceManager.GetDialogService()
|
||||||
dialogService.SetWindow(mainWindow)
|
dialogService.SetWindow(mainWindow)
|
||||||
|
|
||||||
// 设置全局变量供单实例处理使用
|
|
||||||
window = mainWindow
|
|
||||||
|
|
||||||
// Create a goroutine that emits an event containing the current time every second.
|
// Create a goroutine that emits an event containing the current time every second.
|
||||||
// The frontend can listen to this event and update the UI accordingly.
|
// The frontend can listen to this event and update the UI accordingly.
|
||||||
go func() {
|
go func() {
|
||||||
|
Reference in New Issue
Block a user