✨ Added tab functionality and optimized related configurations
This commit is contained in:
@@ -486,6 +486,11 @@ export class GeneralConfig {
|
|||||||
*/
|
*/
|
||||||
"enableLoadingAnimation": boolean;
|
"enableLoadingAnimation": boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否启用标签页模式
|
||||||
|
*/
|
||||||
|
"enableTabs": boolean;
|
||||||
|
|
||||||
/** Creates a new GeneralConfig instance. */
|
/** Creates a new GeneralConfig instance. */
|
||||||
constructor($$source: Partial<GeneralConfig> = {}) {
|
constructor($$source: Partial<GeneralConfig> = {}) {
|
||||||
if (!("alwaysOnTop" in $$source)) {
|
if (!("alwaysOnTop" in $$source)) {
|
||||||
@@ -512,6 +517,9 @@ export class GeneralConfig {
|
|||||||
if (!("enableLoadingAnimation" in $$source)) {
|
if (!("enableLoadingAnimation" in $$source)) {
|
||||||
this["enableLoadingAnimation"] = false;
|
this["enableLoadingAnimation"] = false;
|
||||||
}
|
}
|
||||||
|
if (!("enableTabs" in $$source)) {
|
||||||
|
this["enableTabs"] = false;
|
||||||
|
}
|
||||||
|
|
||||||
Object.assign(this, $$source);
|
Object.assign(this, $$source);
|
||||||
}
|
}
|
||||||
|
|||||||
1
frontend/components.d.ts
vendored
1
frontend/components.d.ts
vendored
@@ -17,6 +17,7 @@ declare module 'vue' {
|
|||||||
RouterLink: typeof import('vue-router')['RouterLink']
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
RouterView: typeof import('vue-router')['RouterView']
|
RouterView: typeof import('vue-router')['RouterView']
|
||||||
TabContainer: typeof import('./src/components/tabs/TabContainer.vue')['default']
|
TabContainer: typeof import('./src/components/tabs/TabContainer.vue')['default']
|
||||||
|
TabContextMenu: typeof import('./src/components/tabs/TabContextMenu.vue')['default']
|
||||||
TabItem: typeof import('./src/components/tabs/TabItem.vue')['default']
|
TabItem: typeof import('./src/components/tabs/TabItem.vue')['default']
|
||||||
Toolbar: typeof import('./src/components/toolbar/Toolbar.vue')['default']
|
Toolbar: typeof import('./src/components/toolbar/Toolbar.vue')['default']
|
||||||
WindowsTitleBar: typeof import('./src/components/titlebar/WindowsTitleBar.vue')['default']
|
WindowsTitleBar: typeof import('./src/components/titlebar/WindowsTitleBar.vue')['default']
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
--dark-toolbar-text: #ffffff;
|
--dark-toolbar-text: #ffffff;
|
||||||
--dark-toolbar-text-secondary: #cccccc;
|
--dark-toolbar-text-secondary: #cccccc;
|
||||||
--dark-toolbar-button-hover: #404040;
|
--dark-toolbar-button-hover: #404040;
|
||||||
|
--dark-tab-active-line: linear-gradient(90deg, #007acc 0%, #0099ff 100%);
|
||||||
--dark-bg-secondary: #0E1217;
|
--dark-bg-secondary: #0E1217;
|
||||||
--dark-text-secondary: #a0aec0;
|
--dark-text-secondary: #a0aec0;
|
||||||
--dark-text-muted: #666;
|
--dark-text-muted: #666;
|
||||||
@@ -40,6 +41,7 @@
|
|||||||
--light-toolbar-text: #212529;
|
--light-toolbar-text: #212529;
|
||||||
--light-toolbar-text-secondary: #495057;
|
--light-toolbar-text-secondary: #495057;
|
||||||
--light-toolbar-button-hover: #e9ecef;
|
--light-toolbar-button-hover: #e9ecef;
|
||||||
|
--light-tab-active-line: linear-gradient(90deg, #0066cc 0%, #0088ff 100%);
|
||||||
--light-bg-secondary: #f7fef7;
|
--light-bg-secondary: #f7fef7;
|
||||||
--light-text-secondary: #374151;
|
--light-text-secondary: #374151;
|
||||||
--light-text-muted: #6b7280;
|
--light-text-muted: #6b7280;
|
||||||
@@ -73,6 +75,7 @@
|
|||||||
--toolbar-text-secondary: var(--dark-toolbar-text-secondary);
|
--toolbar-text-secondary: var(--dark-toolbar-text-secondary);
|
||||||
--toolbar-button-hover: var(--dark-toolbar-button-hover);
|
--toolbar-button-hover: var(--dark-toolbar-button-hover);
|
||||||
--toolbar-separator: var(--dark-toolbar-button-hover);
|
--toolbar-separator: var(--dark-toolbar-button-hover);
|
||||||
|
--tab-active-line: var(--dark-tab-active-line);
|
||||||
--bg-secondary: var(--dark-bg-secondary);
|
--bg-secondary: var(--dark-bg-secondary);
|
||||||
--text-secondary: var(--dark-text-secondary);
|
--text-secondary: var(--dark-text-secondary);
|
||||||
--text-muted: var(--dark-text-muted);
|
--text-muted: var(--dark-text-muted);
|
||||||
@@ -112,6 +115,7 @@
|
|||||||
--toolbar-text-secondary: var(--dark-toolbar-text-secondary);
|
--toolbar-text-secondary: var(--dark-toolbar-text-secondary);
|
||||||
--toolbar-button-hover: var(--dark-toolbar-button-hover);
|
--toolbar-button-hover: var(--dark-toolbar-button-hover);
|
||||||
--toolbar-separator: var(--dark-toolbar-button-hover);
|
--toolbar-separator: var(--dark-toolbar-button-hover);
|
||||||
|
--tab-active-line: var(--dark-tab-active-line);
|
||||||
--bg-secondary: var(--dark-bg-secondary);
|
--bg-secondary: var(--dark-bg-secondary);
|
||||||
--text-secondary: var(--dark-text-secondary);
|
--text-secondary: var(--dark-text-secondary);
|
||||||
--text-muted: var(--dark-text-muted);
|
--text-muted: var(--dark-text-muted);
|
||||||
@@ -149,6 +153,7 @@
|
|||||||
--toolbar-text-secondary: var(--light-toolbar-text-secondary);
|
--toolbar-text-secondary: var(--light-toolbar-text-secondary);
|
||||||
--toolbar-button-hover: var(--light-toolbar-button-hover);
|
--toolbar-button-hover: var(--light-toolbar-button-hover);
|
||||||
--toolbar-separator: var(--light-toolbar-button-hover);
|
--toolbar-separator: var(--light-toolbar-button-hover);
|
||||||
|
--tab-active-line: var(--light-tab-active-line);
|
||||||
--bg-secondary: var(--light-bg-secondary);
|
--bg-secondary: var(--light-bg-secondary);
|
||||||
--text-secondary: var(--light-text-secondary);
|
--text-secondary: var(--light-text-secondary);
|
||||||
--text-muted: var(--light-text-muted);
|
--text-muted: var(--light-text-muted);
|
||||||
@@ -185,6 +190,7 @@
|
|||||||
--toolbar-text-secondary: var(--light-toolbar-text-secondary);
|
--toolbar-text-secondary: var(--light-toolbar-text-secondary);
|
||||||
--toolbar-button-hover: var(--light-toolbar-button-hover);
|
--toolbar-button-hover: var(--light-toolbar-button-hover);
|
||||||
--toolbar-separator: var(--light-toolbar-button-hover);
|
--toolbar-separator: var(--light-toolbar-button-hover);
|
||||||
|
--tab-active-line: var(--light-tab-active-line);
|
||||||
--bg-secondary: var(--light-bg-secondary);
|
--bg-secondary: var(--light-bg-secondary);
|
||||||
--text-secondary: var(--light-text-secondary);
|
--text-secondary: var(--light-text-secondary);
|
||||||
--text-muted: var(--light-text-muted);
|
--text-muted: var(--light-text-muted);
|
||||||
@@ -220,6 +226,7 @@
|
|||||||
--toolbar-text-secondary: var(--dark-toolbar-text-secondary);
|
--toolbar-text-secondary: var(--dark-toolbar-text-secondary);
|
||||||
--toolbar-button-hover: var(--dark-toolbar-button-hover);
|
--toolbar-button-hover: var(--dark-toolbar-button-hover);
|
||||||
--toolbar-separator: var(--dark-toolbar-button-hover);
|
--toolbar-separator: var(--dark-toolbar-button-hover);
|
||||||
|
--tab-active-line: var(--dark-tab-active-line);
|
||||||
--bg-secondary: var(--dark-bg-secondary);
|
--bg-secondary: var(--dark-bg-secondary);
|
||||||
--text-secondary: var(--dark-text-secondary);
|
--text-secondary: var(--dark-text-secondary);
|
||||||
--text-muted: var(--dark-text-muted);
|
--text-muted: var(--dark-text-muted);
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ export const GENERAL_CONFIG_KEY_MAP: GeneralConfigKeyMap = {
|
|||||||
globalHotkey: 'general.globalHotkey',
|
globalHotkey: 'general.globalHotkey',
|
||||||
enableWindowSnap: 'general.enableWindowSnap',
|
enableWindowSnap: 'general.enableWindowSnap',
|
||||||
enableLoadingAnimation: 'general.enableLoadingAnimation',
|
enableLoadingAnimation: 'general.enableLoadingAnimation',
|
||||||
|
enableTabs: 'general.enableTabs',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const EDITING_CONFIG_KEY_MAP: EditingConfigKeyMap = {
|
export const EDITING_CONFIG_KEY_MAP: EditingConfigKeyMap = {
|
||||||
@@ -113,6 +114,7 @@ export const DEFAULT_CONFIG: AppConfig = {
|
|||||||
},
|
},
|
||||||
enableWindowSnap: true,
|
enableWindowSnap: true,
|
||||||
enableLoadingAnimation: true,
|
enableLoadingAnimation: true,
|
||||||
|
enableTabs: false,
|
||||||
},
|
},
|
||||||
editing: {
|
editing: {
|
||||||
fontSize: CONFIG_LIMITS.fontSize.default,
|
fontSize: CONFIG_LIMITS.fontSize.default,
|
||||||
|
|||||||
@@ -9,5 +9,5 @@ export const EDITOR_CONFIG = {
|
|||||||
/** 语法树缓存过期时间(毫秒) */
|
/** 语法树缓存过期时间(毫秒) */
|
||||||
SYNTAX_TREE_CACHE_TIMEOUT: 30000,
|
SYNTAX_TREE_CACHE_TIMEOUT: 30000,
|
||||||
/** 加载状态延迟时间(毫秒) */
|
/** 加载状态延迟时间(毫秒) */
|
||||||
LOADING_DELAY: 800,
|
LOADING_DELAY: 500,
|
||||||
} as const;
|
} as const;
|
||||||
@@ -1,127 +1,105 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="tab-container" style="--wails-draggable:no-drag">
|
<div class="tab-container" style="--wails-draggable:drag">
|
||||||
<div class="tab-bar" ref="tabBarRef">
|
<div class="tab-bar" ref="tabBarRef">
|
||||||
<div class="tab-scroll-wrapper" ref="tabScrollWrapperRef" style="--wails-draggable:no-drag" @wheel.prevent.stop="onWheelScroll">
|
<div class="tab-scroll-wrapper" ref="tabScrollWrapperRef" style="--wails-draggable:drag" @wheel.prevent.stop="onWheelScroll">
|
||||||
<div class="tab-list" ref="tabListRef">
|
<div class="tab-list" ref="tabListRef">
|
||||||
<TabItem
|
<TabItem
|
||||||
v-for="tab in mockTabs"
|
v-for="tab in tabStore.tabs"
|
||||||
:key="tab.id"
|
:key="tab.documentId"
|
||||||
:tab="tab"
|
:tab="tab"
|
||||||
:is-active="tab.id === activeTabId"
|
:isActive="tab.documentId === tabStore.currentDocumentId"
|
||||||
@click="switchToTab(tab.id)"
|
:canClose="tabStore.canCloseTab"
|
||||||
@close="closeTab(tab.id)"
|
@click="switchToTab"
|
||||||
@dragstart="onDragStart($event, tab.id)"
|
@close="closeTab"
|
||||||
|
@dragstart="onDragStart"
|
||||||
@dragover="onDragOver"
|
@dragover="onDragOver"
|
||||||
@drop="onDrop($event, tab.id)"
|
@drop="onDrop"
|
||||||
|
@contextmenu="onContextMenu"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 右键菜单占位 -->
|
<!-- 右键菜单 -->
|
||||||
<div v-if="showContextMenu" class="context-menu-placeholder">
|
<TabContextMenu
|
||||||
<!-- 这里将来会放置 TabContextMenu 组件 -->
|
:visible="showContextMenu"
|
||||||
</div>
|
:position="contextMenuPosition"
|
||||||
|
:targetDocumentId="contextMenuTargetId"
|
||||||
|
@close="closeContextMenu"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue';
|
import { ref, onMounted, onUnmounted } from 'vue';
|
||||||
import TabItem from './TabItem.vue';
|
import TabItem from './TabItem.vue';
|
||||||
|
import TabContextMenu from './TabContextMenu.vue';
|
||||||
|
import { useTabStore } from '@/stores/tabStore';
|
||||||
|
|
||||||
// 模拟数据接口
|
const tabStore = useTabStore();
|
||||||
interface MockTab {
|
|
||||||
id: number;
|
|
||||||
title: string;
|
|
||||||
isDirty: boolean;
|
|
||||||
isActive: boolean;
|
|
||||||
documentId: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 模拟标签页数据
|
|
||||||
const mockTabs = ref<MockTab[]>([
|
|
||||||
{ id: 1, title: 'Document 1', isDirty: false, isActive: true, documentId: 1 },
|
|
||||||
{ id: 2, title: 'Long Document Name Example', isDirty: true, isActive: false, documentId: 2 },
|
|
||||||
{ id: 3, title: 'README.md', isDirty: false, isActive: false, documentId: 3 },
|
|
||||||
{ id: 4, title: 'config.json', isDirty: true, isActive: false, documentId: 4 },
|
|
||||||
{ id: 5, title: 'Another Very Long Document Title', isDirty: false, isActive: false, documentId: 5 },
|
|
||||||
{ id: 6, title: 'package.json', isDirty: false, isActive: false, documentId: 6 },
|
|
||||||
{ id: 7, title: 'index.html', isDirty: true, isActive: false, documentId: 7 },
|
|
||||||
{ id: 8, title: 'styles.css', isDirty: false, isActive: false, documentId: 8 },
|
|
||||||
{ id: 9, title: 'main.js', isDirty: false, isActive: false, documentId: 9 },
|
|
||||||
{ id: 10, title: 'utils.ts', isDirty: true, isActive: false, documentId: 10 },
|
|
||||||
]);
|
|
||||||
|
|
||||||
const activeTabId = ref(1);
|
|
||||||
const showContextMenu = ref(false);
|
|
||||||
|
|
||||||
// DOM 引用
|
// DOM 引用
|
||||||
const tabBarRef = ref<HTMLElement>();
|
const tabBarRef = ref<HTMLElement>();
|
||||||
const tabListRef = ref<HTMLElement>();
|
const tabListRef = ref<HTMLElement>();
|
||||||
// 新增:滚动容器引用
|
|
||||||
const tabScrollWrapperRef = ref<HTMLDivElement | null>(null);
|
const tabScrollWrapperRef = ref<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
// 拖拽状态
|
// 右键菜单状态
|
||||||
let draggedTabId: number | null = null;
|
const showContextMenu = ref(false);
|
||||||
|
const contextMenuPosition = ref({ x: 0, y: 0 });
|
||||||
|
const contextMenuTargetId = ref<number | null>(null);
|
||||||
|
|
||||||
// 切换标签页
|
|
||||||
const switchToTab = (tabId: number) => {
|
// 标签页操作
|
||||||
activeTabId.value = tabId;
|
const switchToTab = (documentId: number) => {
|
||||||
mockTabs.value.forEach(tab => {
|
tabStore.switchToTabAndDocument(documentId);
|
||||||
tab.isActive = tab.id === tabId;
|
|
||||||
});
|
|
||||||
console.log('Switch to tab:', tabId);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 关闭标签页
|
const closeTab = (documentId: number) => {
|
||||||
const closeTab = (tabId: number) => {
|
tabStore.closeTab(documentId);
|
||||||
const index = mockTabs.value.findIndex(tab => tab.id === tabId);
|
|
||||||
if (index === -1) return;
|
|
||||||
|
|
||||||
mockTabs.value.splice(index, 1);
|
|
||||||
|
|
||||||
// 如果关闭的是活跃标签页,切换到其他标签页
|
|
||||||
if (activeTabId.value === tabId && mockTabs.value.length > 0) {
|
|
||||||
const nextTab = mockTabs.value[Math.max(0, index - 1)];
|
|
||||||
switchToTab(nextTab.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Close tab:', tabId);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 拖拽开始
|
// 拖拽操作
|
||||||
const onDragStart = (event: DragEvent, tabId: number) => {
|
const onDragStart = (event: DragEvent, documentId: number) => {
|
||||||
draggedTabId = tabId;
|
tabStore.draggedTabId = documentId;
|
||||||
event.dataTransfer?.setData('text/plain', tabId.toString());
|
event.dataTransfer!.effectAllowed = 'move';
|
||||||
console.log('Drag start:', tabId);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 拖拽悬停
|
|
||||||
const onDragOver = (event: DragEvent) => {
|
const onDragOver = (event: DragEvent) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
event.dataTransfer!.dropEffect = 'move';
|
||||||
};
|
};
|
||||||
|
|
||||||
// 拖拽放置
|
const onDrop = (event: DragEvent, targetDocumentId: number) => {
|
||||||
const onDrop = (event: DragEvent, targetTabId: number) => {
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
if (draggedTabId && draggedTabId !== targetTabId) {
|
if (tabStore.draggedTabId && tabStore.draggedTabId !== targetDocumentId) {
|
||||||
const draggedIndex = mockTabs.value.findIndex(tab => tab.id === draggedTabId);
|
const draggedIndex = tabStore.getTabIndex(tabStore.draggedTabId);
|
||||||
const targetIndex = mockTabs.value.findIndex(tab => tab.id === targetTabId);
|
const targetIndex = tabStore.getTabIndex(targetDocumentId);
|
||||||
|
|
||||||
if (draggedIndex !== -1 && targetIndex !== -1) {
|
if (draggedIndex !== -1 && targetIndex !== -1) {
|
||||||
const draggedTab = mockTabs.value.splice(draggedIndex, 1)[0];
|
tabStore.moveTab(draggedIndex, targetIndex);
|
||||||
mockTabs.value.splice(targetIndex, 0, draggedTab);
|
|
||||||
console.log('Reorder tabs:', draggedTabId, 'to position of', targetTabId);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
draggedTabId = null;
|
tabStore.draggedTabId = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 右键菜单操作
|
||||||
|
const onContextMenu = (event: MouseEvent, documentId: number) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
contextMenuPosition.value = { x: event.clientX, y: event.clientY };
|
||||||
|
contextMenuTargetId.value = documentId;
|
||||||
|
showContextMenu.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeContextMenu = () => {
|
||||||
|
showContextMenu.value = false;
|
||||||
|
contextMenuTargetId.value = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 滚轮滚动处理
|
||||||
const onWheelScroll = (event: WheelEvent) => {
|
const onWheelScroll = (event: WheelEvent) => {
|
||||||
const el = tabScrollWrapperRef.value;
|
const el = tabScrollWrapperRef.value;
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
@@ -129,7 +107,6 @@ const onWheelScroll = (event: WheelEvent) => {
|
|||||||
el.scrollLeft += delta;
|
el.scrollLeft += delta;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
// 组件挂载时的初始化逻辑
|
// 组件挂载时的初始化逻辑
|
||||||
});
|
});
|
||||||
@@ -206,18 +183,6 @@ onUnmounted(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.context-menu-placeholder {
|
|
||||||
position: absolute;
|
|
||||||
top: 100%;
|
|
||||||
left: 0;
|
|
||||||
z-index: 1000;
|
|
||||||
background: var(--bg-primary);
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 4px 0;
|
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 响应式设计 */
|
/* 响应式设计 */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.tab-action-btn {
|
.tab-action-btn {
|
||||||
|
|||||||
228
frontend/src/components/tabs/TabContextMenu.vue
Normal file
228
frontend/src/components/tabs/TabContextMenu.vue
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-if="visible && canClose"
|
||||||
|
class="tab-context-menu"
|
||||||
|
:style="{
|
||||||
|
left: position.x + 'px',
|
||||||
|
top: position.y + 'px'
|
||||||
|
}"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<div v-if="canClose" class="menu-item" @click="handleMenuClick('close')">
|
||||||
|
<svg class="menu-icon" xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M18 6L6 18M6 6l12 12"/>
|
||||||
|
</svg>
|
||||||
|
<span class="menu-text">{{ t('tabs.contextMenu.closeTab') }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="hasOtherTabs" class="menu-item" @click="handleMenuClick('closeOthers')">
|
||||||
|
<svg class="menu-icon" xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M3 6h18M3 12h18M3 18h18"/>
|
||||||
|
<path d="M18 6L6 18M6 6l12 12"/>
|
||||||
|
</svg>
|
||||||
|
<span class="menu-text">{{ t('tabs.contextMenu.closeOthers') }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="hasTabsToLeft" class="menu-item" @click="handleMenuClick('closeLeft')">
|
||||||
|
<svg class="menu-icon" xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M15 18l-6-6 6-6"/>
|
||||||
|
<path d="M18 6L6 18M6 6l12 12"/>
|
||||||
|
</svg>
|
||||||
|
<span class="menu-text">{{ t('tabs.contextMenu.closeLeft') }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="hasTabsToRight" class="menu-item" @click="handleMenuClick('closeRight')">
|
||||||
|
<svg class="menu-icon" xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M9 18l6-6-6-6"/>
|
||||||
|
<path d="M18 6L6 18M6 6l12 12"/>
|
||||||
|
</svg>
|
||||||
|
<span class="menu-text">{{ t('tabs.contextMenu.closeRight') }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onMounted, onUnmounted } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { useTabStore } from '@/stores/tabStore';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
visible: boolean;
|
||||||
|
position: { x: number; y: number };
|
||||||
|
targetDocumentId: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
const emit = defineEmits<{
|
||||||
|
close: [];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
const tabStore = useTabStore();
|
||||||
|
|
||||||
|
// 计算属性
|
||||||
|
const canClose = computed(() => tabStore.canCloseTab);
|
||||||
|
|
||||||
|
const hasOtherTabs = computed(() => {
|
||||||
|
return tabStore.tabs.length > 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentTabIndex = computed(() => {
|
||||||
|
if (!props.targetDocumentId) return -1;
|
||||||
|
return tabStore.getTabIndex(props.targetDocumentId);
|
||||||
|
});
|
||||||
|
|
||||||
|
const hasTabsToRight = computed(() => {
|
||||||
|
const index = currentTabIndex.value;
|
||||||
|
return index !== -1 && index < tabStore.tabs.length - 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
const hasTabsToLeft = computed(() => {
|
||||||
|
const index = currentTabIndex.value;
|
||||||
|
return index > 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 处理菜单项点击
|
||||||
|
const handleMenuClick = (action: string) => {
|
||||||
|
if (!props.targetDocumentId) return;
|
||||||
|
|
||||||
|
switch (action) {
|
||||||
|
case 'close':
|
||||||
|
tabStore.closeTab(props.targetDocumentId);
|
||||||
|
break;
|
||||||
|
case 'closeOthers':
|
||||||
|
tabStore.closeOtherTabs(props.targetDocumentId);
|
||||||
|
break;
|
||||||
|
case 'closeLeft':
|
||||||
|
tabStore.closeTabsToLeft(props.targetDocumentId);
|
||||||
|
break;
|
||||||
|
case 'closeRight':
|
||||||
|
tabStore.closeTabsToRight(props.targetDocumentId);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
emit('close');
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理外部点击
|
||||||
|
const handleClickOutside = (_event: MouseEvent) => {
|
||||||
|
if (props.visible) {
|
||||||
|
emit('close');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理ESC键
|
||||||
|
const handleEscapeKey = (event: KeyboardEvent) => {
|
||||||
|
if (event.key === 'Escape' && props.visible) {
|
||||||
|
emit('close');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 生命周期
|
||||||
|
onMounted(() => {
|
||||||
|
document.addEventListener('click', handleClickOutside);
|
||||||
|
document.addEventListener('keydown', handleEscapeKey);
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
document.removeEventListener('click', handleClickOutside);
|
||||||
|
document.removeEventListener('keydown', handleEscapeKey);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.tab-context-menu {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 1000;
|
||||||
|
background-color: var(--bg-secondary);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||||
|
overflow: hidden;
|
||||||
|
min-width: 140px;
|
||||||
|
user-select: none;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-primary);
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--toolbar-button-hover);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background-color: var(--border-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
transition: color 0.15s ease;
|
||||||
|
|
||||||
|
.menu-item:hover & {
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-text {
|
||||||
|
white-space: nowrap;
|
||||||
|
font-weight: 400;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 主题适配 */
|
||||||
|
:root[data-theme="dark"] .tab-context-menu {
|
||||||
|
background-color: var(--bg-secondary);
|
||||||
|
border-color: var(--border-color);
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||||
|
|
||||||
|
.menu-item {
|
||||||
|
color: var(--text-primary);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--toolbar-button-hover);
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-icon {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
|
||||||
|
.menu-item:hover & {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme="light"] .tab-context-menu {
|
||||||
|
background-color: var(--bg-secondary);
|
||||||
|
border-color: var(--border-color);
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
|
||||||
|
.menu-item {
|
||||||
|
color: var(--text-primary);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--toolbar-button-hover);
|
||||||
|
color: #000000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-icon {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
|
||||||
|
.menu-item:hover & {
|
||||||
|
color: #000000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -39,9 +39,10 @@
|
|||||||
|
|
||||||
<!-- 关闭按钮 -->
|
<!-- 关闭按钮 -->
|
||||||
<div
|
<div
|
||||||
|
v-if="props.canClose"
|
||||||
class="tab-close"
|
class="tab-close"
|
||||||
@click.stop="handleClose"
|
@click.stop="handleClose"
|
||||||
:title="'关闭标签页'"
|
:title="'Close tab'"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
@@ -70,25 +71,23 @@ import { computed, ref } from 'vue';
|
|||||||
// 组件属性
|
// 组件属性
|
||||||
interface TabProps {
|
interface TabProps {
|
||||||
tab: {
|
tab: {
|
||||||
id: number;
|
documentId: number; // 直接使用文档ID作为唯一标识
|
||||||
title: string;
|
title: string; // 标签页标题
|
||||||
isDirty: boolean;
|
|
||||||
isActive: boolean;
|
|
||||||
documentId: number;
|
|
||||||
};
|
};
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
|
canClose?: boolean; // 是否可以关闭标签页
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = defineProps<TabProps>();
|
const props = defineProps<TabProps>();
|
||||||
|
|
||||||
// 组件事件
|
// 组件事件
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
click: [tabId: number];
|
click: [documentId: number];
|
||||||
close: [tabId: number];
|
close: [documentId: number];
|
||||||
dragstart: [event: DragEvent, tabId: number];
|
dragstart: [event: DragEvent, documentId: number];
|
||||||
dragover: [event: DragEvent];
|
dragover: [event: DragEvent];
|
||||||
drop: [event: DragEvent, tabId: number];
|
drop: [event: DragEvent, documentId: number];
|
||||||
contextmenu: [event: MouseEvent, tabId: number];
|
contextmenu: [event: MouseEvent, documentId: number];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
// 组件状态
|
// 组件状态
|
||||||
@@ -103,16 +102,16 @@ const displayTitle = computed(() => {
|
|||||||
|
|
||||||
// 事件处理
|
// 事件处理
|
||||||
const handleClick = () => {
|
const handleClick = () => {
|
||||||
emit('click', props.tab.id);
|
emit('click', props.tab.documentId);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
emit('close', props.tab.id);
|
emit('close', props.tab.documentId);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDragStart = (event: DragEvent) => {
|
const handleDragStart = (event: DragEvent) => {
|
||||||
isDragging.value = true;
|
isDragging.value = true;
|
||||||
emit('dragstart', event, props.tab.id);
|
emit('dragstart', event, props.tab.documentId);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDragOver = (event: DragEvent) => {
|
const handleDragOver = (event: DragEvent) => {
|
||||||
@@ -121,7 +120,7 @@ const handleDragOver = (event: DragEvent) => {
|
|||||||
|
|
||||||
const handleDrop = (event: DragEvent) => {
|
const handleDrop = (event: DragEvent) => {
|
||||||
isDragging.value = false;
|
isDragging.value = false;
|
||||||
emit('drop', event, props.tab.id);
|
emit('drop', event, props.tab.documentId);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDragEnd = () => {
|
const handleDragEnd = () => {
|
||||||
@@ -130,7 +129,7 @@ const handleDragEnd = () => {
|
|||||||
|
|
||||||
const handleContextMenu = (event: MouseEvent) => {
|
const handleContextMenu = (event: MouseEvent) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
emit('contextmenu', event, props.tab.id);
|
emit('contextmenu', event, props.tab.documentId);
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -159,14 +158,36 @@ const handleContextMenu = (event: MouseEvent) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
background-color: var(--toolbar-button-hover);
|
background-color: var(--toolbar-button-active, var(--toolbar-button-hover));
|
||||||
border-bottom: 2px solid var(--accent-color);
|
|
||||||
color: var(--toolbar-text);
|
color: var(--toolbar-text);
|
||||||
|
position: relative;
|
||||||
.tab-title {
|
.tab-title {
|
||||||
color: var(--toolbar-text);
|
color: var(--toolbar-text);
|
||||||
/* 不用加粗,避免抖动 */
|
font-weight: 600; /* 字体加粗 */
|
||||||
|
text-shadow: 0 0 1px rgba(0, 0, 0, 0.1); /* 轻微阴影增强可读性 */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tab-icon {
|
||||||
|
color: var(--accent-color);
|
||||||
|
filter: brightness(1.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 底部活跃线条 */
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 0;
|
||||||
|
height: 2px;
|
||||||
|
background: var(--tab-active-line);
|
||||||
|
transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active::after {
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.dragging {
|
&.dragging {
|
||||||
|
|||||||
@@ -4,11 +4,13 @@
|
|||||||
<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">{{ titleText }}</div>-->
|
<div v-if="!tabStore.isTabsEnabled && !isInSettings" class="titlebar-title">{{ titleText }}</div>
|
||||||
<!-- 标签页容器区域 -->
|
<!-- 标签页容器区域 -->
|
||||||
<div class="titlebar-tabs" style="--wails-draggable:no-drag">
|
<div class="titlebar-tabs" v-if="tabStore.isTabsEnabled && !isInSettings" style="--wails-draggable:drag">
|
||||||
<TabContainer />
|
<TabContainer />
|
||||||
</div>
|
</div>
|
||||||
|
<!-- 设置页面标题 -->
|
||||||
|
<div v-if="isInSettings" 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>
|
||||||
@@ -42,19 +44,29 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {computed, onMounted, ref} from 'vue';
|
import {computed, onMounted, ref} from 'vue';
|
||||||
import {useI18n} from 'vue-i18n';
|
import {useI18n} from 'vue-i18n';
|
||||||
|
import {useRoute} from 'vue-router';
|
||||||
import * as runtime from '@wailsio/runtime';
|
import * as runtime from '@wailsio/runtime';
|
||||||
import {useDocumentStore} from '@/stores/documentStore';
|
import {useDocumentStore} from '@/stores/documentStore';
|
||||||
import TabContainer from '@/components/tabs/TabContainer.vue';
|
import TabContainer from '@/components/tabs/TabContainer.vue';
|
||||||
|
import {useTabStore} from "@/stores/tabStore";
|
||||||
|
|
||||||
|
const tabStore = useTabStore();
|
||||||
const {t} = useI18n();
|
const {t} = useI18n();
|
||||||
|
const route = useRoute();
|
||||||
const isMaximized = ref(false);
|
const isMaximized = ref(false);
|
||||||
const documentStore = useDocumentStore();
|
const documentStore = useDocumentStore();
|
||||||
|
|
||||||
// 计算属性用于图标,减少重复渲染
|
// 计算属性用于图标,减少重复渲染
|
||||||
const maximizeIcon = computed(() => isMaximized.value ? '' : '');
|
const maximizeIcon = computed(() => isMaximized.value ? '' : '');
|
||||||
|
|
||||||
|
// 判断是否在设置页面
|
||||||
|
const isInSettings = computed(() => route.path.startsWith('/settings'));
|
||||||
|
|
||||||
// 计算标题文本
|
// 计算标题文本
|
||||||
const titleText = computed(() => {
|
const titleText = computed(() => {
|
||||||
|
if (isInSettings.value) {
|
||||||
|
return 'voidraft - settings';
|
||||||
|
}
|
||||||
const currentDoc = documentStore.currentDocument;
|
const currentDoc = documentStore.currentDocument;
|
||||||
return currentDoc ? `voidraft - ${currentDoc.title}` : 'voidraft';
|
return currentDoc ? `voidraft - ${currentDoc.title}` : 'voidraft';
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
|
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||||
import { useDocumentStore } from '@/stores/documentStore';
|
import { useDocumentStore } from '@/stores/documentStore';
|
||||||
|
import { useTabStore } from '@/stores/tabStore';
|
||||||
import { useWindowStore } from '@/stores/windowStore';
|
import { useWindowStore } from '@/stores/windowStore';
|
||||||
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';
|
||||||
|
|
||||||
const documentStore = useDocumentStore();
|
const documentStore = useDocumentStore();
|
||||||
|
const tabStore = useTabStore();
|
||||||
const windowStore = useWindowStore();
|
const windowStore = useWindowStore();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
@@ -109,7 +111,12 @@ const selectDoc = async (doc: Document) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const success = await documentStore.openDocument(doc.id);
|
const success = await documentStore.openDocument(doc.id);
|
||||||
if (success) closeMenu();
|
if (success) {
|
||||||
|
if (tabStore.isTabsEnabled) {
|
||||||
|
tabStore.addOrActivateTab(doc);
|
||||||
|
}
|
||||||
|
closeMenu();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const createDoc = async (title: string) => {
|
const createDoc = async (title: string) => {
|
||||||
@@ -159,6 +166,11 @@ const saveEdit = async () => {
|
|||||||
try {
|
try {
|
||||||
await documentStore.updateDocumentMetadata(editingId.value, trimmedTitle);
|
await documentStore.updateDocumentMetadata(editingId.value, trimmedTitle);
|
||||||
await documentStore.getDocumentMetaList();
|
await documentStore.getDocumentMetaList();
|
||||||
|
|
||||||
|
// 如果tabs功能开启且该文档有标签页,更新标签页标题
|
||||||
|
if (tabStore.isTabsEnabled && tabStore.hasTab(editingId.value)) {
|
||||||
|
tabStore.updateTabTitle(editingId.value, trimmedTitle);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to update document:', error);
|
console.error('Failed to update document:', error);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -472,10 +484,12 @@ watch(() => documentStore.showDocumentSelector, (isOpen) => {
|
|||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
width: 260px;
|
width: 260px;
|
||||||
//max-height: 320px;
|
max-height: calc(100vh - 40px); // 限制最大高度,留出titlebar空间(32px)和一些边距
|
||||||
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;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
.input-box {
|
.input-box {
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -513,8 +527,9 @@ watch(() => documentStore.showDocumentSelector, (isOpen) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.item-list {
|
.item-list {
|
||||||
max-height: 240px;
|
max-height: calc(100vh - 100px); // 为输入框和边距预留空间
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
.list-item {
|
.list-item {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|||||||
@@ -112,6 +112,14 @@ export default {
|
|||||||
textHighlightToggle: 'Toggle text highlight',
|
textHighlightToggle: 'Toggle text highlight',
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
tabs: {
|
||||||
|
contextMenu: {
|
||||||
|
closeTab: 'Close Tab',
|
||||||
|
closeOthers: 'Close Others',
|
||||||
|
closeLeft: 'Close Left',
|
||||||
|
closeRight: 'Close Right'
|
||||||
|
}
|
||||||
|
},
|
||||||
settings: {
|
settings: {
|
||||||
title: 'Settings',
|
title: 'Settings',
|
||||||
backToEditor: 'Back to Editor',
|
backToEditor: 'Back to Editor',
|
||||||
@@ -136,6 +144,7 @@ export default {
|
|||||||
alwaysOnTop: 'Always on Top',
|
alwaysOnTop: 'Always on Top',
|
||||||
enableWindowSnap: 'Enable Window Snapping',
|
enableWindowSnap: 'Enable Window Snapping',
|
||||||
enableLoadingAnimation: 'Enable Loading Animation',
|
enableLoadingAnimation: 'Enable Loading Animation',
|
||||||
|
enableTabs: 'Enable Tabs',
|
||||||
startup: 'Startup Settings',
|
startup: 'Startup Settings',
|
||||||
startAtLogin: 'Start at Login',
|
startAtLogin: 'Start at Login',
|
||||||
dataStorage: 'Data Storage',
|
dataStorage: 'Data Storage',
|
||||||
|
|||||||
@@ -112,6 +112,14 @@ export default {
|
|||||||
textHighlightToggle: '切换文本高亮',
|
textHighlightToggle: '切换文本高亮',
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
tabs: {
|
||||||
|
contextMenu: {
|
||||||
|
closeTab: '关闭标签',
|
||||||
|
closeOthers: '关闭其他',
|
||||||
|
closeLeft: '关闭左侧',
|
||||||
|
closeRight: '关闭右侧'
|
||||||
|
}
|
||||||
|
},
|
||||||
settings: {
|
settings: {
|
||||||
title: '设置',
|
title: '设置',
|
||||||
backToEditor: '返回编辑器',
|
backToEditor: '返回编辑器',
|
||||||
@@ -137,6 +145,7 @@ export default {
|
|||||||
alwaysOnTop: '窗口始终置顶',
|
alwaysOnTop: '窗口始终置顶',
|
||||||
enableWindowSnap: '启用窗口吸附',
|
enableWindowSnap: '启用窗口吸附',
|
||||||
enableLoadingAnimation: '启用加载动画',
|
enableLoadingAnimation: '启用加载动画',
|
||||||
|
enableTabs: '启用标签页',
|
||||||
startup: '启动设置',
|
startup: '启动设置',
|
||||||
startAtLogin: '开机自启动',
|
startAtLogin: '开机自启动',
|
||||||
dataStorage: '数据存储',
|
dataStorage: '数据存储',
|
||||||
|
|||||||
@@ -4,28 +4,28 @@ import {ConfigService, StartupService} from '@/../bindings/voidraft/internal/ser
|
|||||||
import {
|
import {
|
||||||
AppConfig,
|
AppConfig,
|
||||||
AppearanceConfig,
|
AppearanceConfig,
|
||||||
|
AuthMethod,
|
||||||
EditingConfig,
|
EditingConfig,
|
||||||
GeneralConfig,
|
GeneralConfig,
|
||||||
|
GitBackupConfig,
|
||||||
LanguageType,
|
LanguageType,
|
||||||
SystemThemeType,
|
SystemThemeType,
|
||||||
TabType,
|
TabType,
|
||||||
UpdatesConfig,
|
UpdatesConfig
|
||||||
GitBackupConfig,
|
|
||||||
AuthMethod
|
|
||||||
} from '@/../bindings/voidraft/internal/models/models';
|
} from '@/../bindings/voidraft/internal/models/models';
|
||||||
import {useI18n} from 'vue-i18n';
|
import {useI18n} from 'vue-i18n';
|
||||||
import {ConfigUtils} from '@/common/utils/configUtils';
|
import {ConfigUtils} from '@/common/utils/configUtils';
|
||||||
import {FONT_OPTIONS} from '@/common/constant/fonts';
|
import {FONT_OPTIONS} from '@/common/constant/fonts';
|
||||||
import {SUPPORTED_LOCALES} from '@/common/constant/locales';
|
import {SUPPORTED_LOCALES} from '@/common/constant/locales';
|
||||||
import {
|
import {
|
||||||
NumberConfigKey,
|
|
||||||
GENERAL_CONFIG_KEY_MAP,
|
|
||||||
EDITING_CONFIG_KEY_MAP,
|
|
||||||
APPEARANCE_CONFIG_KEY_MAP,
|
APPEARANCE_CONFIG_KEY_MAP,
|
||||||
UPDATES_CONFIG_KEY_MAP,
|
|
||||||
BACKUP_CONFIG_KEY_MAP,
|
BACKUP_CONFIG_KEY_MAP,
|
||||||
CONFIG_LIMITS,
|
CONFIG_LIMITS,
|
||||||
DEFAULT_CONFIG
|
DEFAULT_CONFIG,
|
||||||
|
EDITING_CONFIG_KEY_MAP,
|
||||||
|
GENERAL_CONFIG_KEY_MAP,
|
||||||
|
NumberConfigKey,
|
||||||
|
UPDATES_CONFIG_KEY_MAP
|
||||||
} from '@/common/constant/config';
|
} from '@/common/constant/config';
|
||||||
import * as runtime from '@wailsio/runtime';
|
import * as runtime from '@wailsio/runtime';
|
||||||
|
|
||||||
@@ -205,7 +205,6 @@ export const useConfigStore = defineStore('config', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 初始化语言设置
|
// 初始化语言设置
|
||||||
const initializeLanguage = async (): Promise<void> => {
|
const initializeLanguage = async (): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
@@ -324,12 +323,19 @@ export const useConfigStore = defineStore('config', () => {
|
|||||||
// 加载动画配置相关方法
|
// 加载动画配置相关方法
|
||||||
setEnableLoadingAnimation: async (value: boolean) => await updateGeneralConfig('enableLoadingAnimation', value),
|
setEnableLoadingAnimation: async (value: boolean) => await updateGeneralConfig('enableLoadingAnimation', value),
|
||||||
|
|
||||||
|
// 标签页配置相关方法
|
||||||
|
setEnableTabs: async (value: boolean) => await updateGeneralConfig('enableTabs', value),
|
||||||
|
|
||||||
// 更新配置相关方法
|
// 更新配置相关方法
|
||||||
setAutoUpdate: async (value: boolean) => await updateUpdatesConfig('autoUpdate', value),
|
setAutoUpdate: async (value: boolean) => await updateUpdatesConfig('autoUpdate', value),
|
||||||
|
|
||||||
// 备份配置相关方法
|
// 备份配置相关方法
|
||||||
setEnableBackup: async (value: boolean) => {await updateBackupConfig('enabled', value);},
|
setEnableBackup: async (value: boolean) => {
|
||||||
setAutoBackup: async (value: boolean) => {await updateBackupConfig('auto_backup', value);},
|
await updateBackupConfig('enabled', value);
|
||||||
|
},
|
||||||
|
setAutoBackup: async (value: boolean) => {
|
||||||
|
await updateBackupConfig('auto_backup', value);
|
||||||
|
},
|
||||||
setRepoUrl: async (value: string) => await updateBackupConfig('repo_url', value),
|
setRepoUrl: async (value: string) => await updateBackupConfig('repo_url', value),
|
||||||
setAuthMethod: async (value: AuthMethod) => await updateBackupConfig('auth_method', value),
|
setAuthMethod: async (value: AuthMethod) => await updateBackupConfig('auth_method', value),
|
||||||
setUsername: async (value: string) => await updateBackupConfig('username', value),
|
setUsername: async (value: string) => await updateBackupConfig('username', value),
|
||||||
|
|||||||
227
frontend/src/stores/tabStore.ts
Normal file
227
frontend/src/stores/tabStore.ts
Normal file
@@ -0,0 +1,227 @@
|
|||||||
|
import {defineStore} from 'pinia';
|
||||||
|
import {computed, readonly, ref} from 'vue';
|
||||||
|
import {useConfigStore} from './configStore';
|
||||||
|
import {useDocumentStore} from './documentStore';
|
||||||
|
import type {Document} from '@/../bindings/voidraft/internal/models/models';
|
||||||
|
|
||||||
|
export interface Tab {
|
||||||
|
documentId: number; // 直接使用文档ID作为唯一标识
|
||||||
|
title: string; // 标签页标题
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useTabStore = defineStore('tab', () => {
|
||||||
|
// === 依赖store ===
|
||||||
|
const configStore = useConfigStore();
|
||||||
|
const documentStore = useDocumentStore();
|
||||||
|
|
||||||
|
// === 核心状态 ===
|
||||||
|
const tabsMap = ref<Map<number, Tab>>(new Map());
|
||||||
|
const tabOrder = ref<number[]>([]); // 维护标签页顺序
|
||||||
|
const draggedTabId = ref<number | null>(null);
|
||||||
|
|
||||||
|
// === 计算属性 ===
|
||||||
|
|
||||||
|
const isTabsEnabled = computed(() => configStore.config.general.enableTabs);
|
||||||
|
const canCloseTab = computed(() => tabOrder.value.length > 1);
|
||||||
|
const currentDocumentId = computed(() => documentStore.currentDocumentId);
|
||||||
|
|
||||||
|
// 按顺序返回标签页数组(用于UI渲染)
|
||||||
|
const tabs = computed(() => {
|
||||||
|
return tabOrder.value
|
||||||
|
.map(documentId => tabsMap.value.get(documentId))
|
||||||
|
.filter(tab => tab !== undefined) as Tab[];
|
||||||
|
});
|
||||||
|
|
||||||
|
// === 私有方法 ===
|
||||||
|
const hasTab = (documentId: number): boolean => {
|
||||||
|
return tabsMap.value.has(documentId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTab = (documentId: number): Tab | undefined => {
|
||||||
|
return tabsMap.value.get(documentId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateTabTitle = (documentId: number, title: string) => {
|
||||||
|
const tab = tabsMap.value.get(documentId);
|
||||||
|
if (tab) {
|
||||||
|
tab.title = title;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// === 公共方法 ===
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加或激活标签页
|
||||||
|
*/
|
||||||
|
const addOrActivateTab = (document: Document) => {
|
||||||
|
const documentId = document.id;
|
||||||
|
|
||||||
|
if (hasTab(documentId)) {
|
||||||
|
// 标签页已存在,无需重复添加
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建新标签页
|
||||||
|
const newTab: Tab = {
|
||||||
|
documentId,
|
||||||
|
title: document.title
|
||||||
|
};
|
||||||
|
|
||||||
|
tabsMap.value.set(documentId, newTab);
|
||||||
|
tabOrder.value.push(documentId);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 关闭标签页
|
||||||
|
*/
|
||||||
|
const closeTab = (documentId: number) => {
|
||||||
|
if (!hasTab(documentId)) return;
|
||||||
|
|
||||||
|
const tabIndex = tabOrder.value.indexOf(documentId);
|
||||||
|
if (tabIndex === -1) return;
|
||||||
|
|
||||||
|
// 从映射和顺序数组中移除
|
||||||
|
tabsMap.value.delete(documentId);
|
||||||
|
tabOrder.value.splice(tabIndex, 1);
|
||||||
|
|
||||||
|
// 如果关闭的是当前文档,需要切换到其他文档
|
||||||
|
if (documentStore.currentDocument?.id === documentId) {
|
||||||
|
// 优先选择下一个标签页,如果没有则选择上一个
|
||||||
|
let nextIndex = tabIndex;
|
||||||
|
if (nextIndex >= tabOrder.value.length) {
|
||||||
|
nextIndex = tabOrder.value.length - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextIndex >= 0 && tabOrder.value[nextIndex]) {
|
||||||
|
const nextDocumentId = tabOrder.value[nextIndex];
|
||||||
|
switchToTabAndDocument(nextDocumentId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 切换到指定标签页并打开对应文档
|
||||||
|
*/
|
||||||
|
const switchToTabAndDocument = (documentId: number) => {
|
||||||
|
if (!hasTab(documentId)) return;
|
||||||
|
|
||||||
|
// 如果点击的是当前已激活的文档,不需要重复请求
|
||||||
|
if (documentStore.currentDocumentId === documentId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
documentStore.openDocument(documentId);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移动标签页位置
|
||||||
|
*/
|
||||||
|
const moveTab = (fromIndex: number, toIndex: number) => {
|
||||||
|
if (fromIndex === toIndex || fromIndex < 0 || toIndex < 0 ||
|
||||||
|
fromIndex >= tabOrder.value.length || toIndex >= tabOrder.value.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [movedTab] = tabOrder.value.splice(fromIndex, 1);
|
||||||
|
tabOrder.value.splice(toIndex, 0, movedTab);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取标签页在顺序中的索引
|
||||||
|
*/
|
||||||
|
const getTabIndex = (documentId: number): number => {
|
||||||
|
return tabOrder.value.indexOf(documentId);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化标签页(当前文档)
|
||||||
|
*/
|
||||||
|
const initializeTab = () => {
|
||||||
|
if (isTabsEnabled.value) {
|
||||||
|
const currentDoc = documentStore.currentDocument;
|
||||||
|
if (currentDoc) {
|
||||||
|
addOrActivateTab(currentDoc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// === 公共方法 ===
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 关闭其他标签页(除了指定的标签页)
|
||||||
|
*/
|
||||||
|
const closeOtherTabs = (keepDocumentId: number) => {
|
||||||
|
if (!hasTab(keepDocumentId)) return;
|
||||||
|
|
||||||
|
// 获取所有其他标签页的ID
|
||||||
|
const otherTabIds = tabOrder.value.filter(id => id !== keepDocumentId);
|
||||||
|
|
||||||
|
// 关闭其他标签页
|
||||||
|
otherTabIds.forEach(id => closeTab(id));
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 关闭指定标签页右侧的所有标签页
|
||||||
|
*/
|
||||||
|
const closeTabsToRight = (documentId: number) => {
|
||||||
|
const index = getTabIndex(documentId);
|
||||||
|
if (index === -1) return;
|
||||||
|
|
||||||
|
// 获取右侧所有标签页的ID
|
||||||
|
const rightTabIds = tabOrder.value.slice(index + 1);
|
||||||
|
|
||||||
|
// 关闭右侧标签页
|
||||||
|
rightTabIds.forEach(id => closeTab(id));
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 关闭指定标签页左侧的所有标签页
|
||||||
|
*/
|
||||||
|
const closeTabsToLeft = (documentId: number) => {
|
||||||
|
const index = getTabIndex(documentId);
|
||||||
|
if (index <= 0) return;
|
||||||
|
|
||||||
|
// 获取左侧所有标签页的ID
|
||||||
|
const leftTabIds = tabOrder.value.slice(0, index);
|
||||||
|
|
||||||
|
// 关闭左侧标签页
|
||||||
|
leftTabIds.forEach(id => closeTab(id));
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清空所有标签页
|
||||||
|
*/
|
||||||
|
const clearAllTabs = () => {
|
||||||
|
tabsMap.value.clear();
|
||||||
|
tabOrder.value = [];
|
||||||
|
};
|
||||||
|
|
||||||
|
// === 公共API ===
|
||||||
|
return {
|
||||||
|
// 状态
|
||||||
|
tabs: readonly(tabs),
|
||||||
|
draggedTabId,
|
||||||
|
|
||||||
|
// 计算属性
|
||||||
|
isTabsEnabled,
|
||||||
|
canCloseTab,
|
||||||
|
currentDocumentId,
|
||||||
|
|
||||||
|
// 方法
|
||||||
|
addOrActivateTab,
|
||||||
|
closeTab,
|
||||||
|
closeOtherTabs,
|
||||||
|
closeTabsToLeft,
|
||||||
|
closeTabsToRight,
|
||||||
|
switchToTabAndDocument,
|
||||||
|
moveTab,
|
||||||
|
getTabIndex,
|
||||||
|
initializeTab,
|
||||||
|
clearAllTabs,
|
||||||
|
updateTabTitle,
|
||||||
|
|
||||||
|
// 工具方法
|
||||||
|
hasTab,
|
||||||
|
getTab
|
||||||
|
};
|
||||||
|
});
|
||||||
@@ -7,11 +7,13 @@ import {createWheelZoomHandler} from './basic/wheelZoomExtension';
|
|||||||
import Toolbar from '@/components/toolbar/Toolbar.vue';
|
import Toolbar from '@/components/toolbar/Toolbar.vue';
|
||||||
import {useWindowStore} from "@/stores/windowStore";
|
import {useWindowStore} from "@/stores/windowStore";
|
||||||
import LoadingScreen from '@/components/loading/LoadingScreen.vue';
|
import LoadingScreen from '@/components/loading/LoadingScreen.vue';
|
||||||
|
import {useTabStore} from "@/stores/tabStore";
|
||||||
|
|
||||||
const editorStore = useEditorStore();
|
const editorStore = useEditorStore();
|
||||||
const documentStore = useDocumentStore();
|
const documentStore = useDocumentStore();
|
||||||
const configStore = useConfigStore();
|
const configStore = useConfigStore();
|
||||||
const windowStore = useWindowStore();
|
const windowStore = useWindowStore();
|
||||||
|
const tabStore = useTabStore();
|
||||||
|
|
||||||
const editorElement = ref<HTMLElement | null>(null);
|
const editorElement = ref<HTMLElement | null>(null);
|
||||||
|
|
||||||
@@ -33,6 +35,8 @@ onMounted(async () => {
|
|||||||
// 设置编辑器容器
|
// 设置编辑器容器
|
||||||
editorStore.setEditorContainer(editorElement.value);
|
editorStore.setEditorContainer(editorElement.value);
|
||||||
|
|
||||||
|
await tabStore.initializeTab();
|
||||||
|
|
||||||
// 添加滚轮事件监听
|
// 添加滚轮事件监听
|
||||||
editorElement.value.addEventListener('wheel', wheelHandler, {passive: false});
|
editorElement.value.addEventListener('wheel', wheelHandler, {passive: false});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -285,19 +285,6 @@ export class TranslationTooltip implements TooltipView {
|
|||||||
this.translate();
|
this.translate();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 寻找替代的目标语言
|
|
||||||
*/
|
|
||||||
private findAlternativeTargetLanguage(): void {
|
|
||||||
const options = Array.from(this.targetLangSelector.options);
|
|
||||||
for (const option of options) {
|
|
||||||
if (option.value !== this.sourceLangSelector.value) {
|
|
||||||
this.targetLangSelector.value = option.value;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 交换源语言和目标语言
|
* 交换源语言和目标语言
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {useConfigStore} from '@/stores/configStore';
|
import {useConfigStore} from '@/stores/configStore';
|
||||||
|
import {useTabStore} from '@/stores/tabStore';
|
||||||
import {useI18n} from 'vue-i18n';
|
import {useI18n} from 'vue-i18n';
|
||||||
import {computed, onUnmounted, ref} from 'vue';
|
import {computed, onUnmounted, ref} from 'vue';
|
||||||
import SettingSection from '../components/SettingSection.vue';
|
import SettingSection from '../components/SettingSection.vue';
|
||||||
@@ -16,7 +17,7 @@ import {useSystemStore} from "@/stores/systemStore";
|
|||||||
const {t} = useI18n();
|
const {t} = useI18n();
|
||||||
const configStore = useConfigStore();
|
const configStore = useConfigStore();
|
||||||
const systemStore = useSystemStore();
|
const systemStore = useSystemStore();
|
||||||
|
const tabStore = useTabStore();
|
||||||
// 迁移进度状态
|
// 迁移进度状态
|
||||||
const migrationProgress = ref<MigrationProgress>(new MigrationProgress({
|
const migrationProgress = ref<MigrationProgress>(new MigrationProgress({
|
||||||
status: MigrationStatus.MigrationStatusCompleted,
|
status: MigrationStatus.MigrationStatusCompleted,
|
||||||
@@ -171,6 +172,21 @@ const enableLoadingAnimation = computed({
|
|||||||
set: (value: boolean) => configStore.setEnableLoadingAnimation(value)
|
set: (value: boolean) => configStore.setEnableLoadingAnimation(value)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 计算属性 - 启用标签页
|
||||||
|
const enableTabs = computed({
|
||||||
|
get: () => configStore.config.general.enableTabs,
|
||||||
|
set: async (value: boolean) => {
|
||||||
|
await configStore.setEnableTabs(value);
|
||||||
|
if (value) {
|
||||||
|
// 开启tabs功能时,初始化当前文档到标签页
|
||||||
|
tabStore.initializeTab();
|
||||||
|
} else {
|
||||||
|
// 关闭tabs功能时,清空所有标签页
|
||||||
|
tabStore.clearAllTabs();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// 计算属性 - 开机启动
|
// 计算属性 - 开机启动
|
||||||
const startAtLogin = computed({
|
const startAtLogin = computed({
|
||||||
get: () => configStore.config.general.startAtLogin,
|
get: () => configStore.config.general.startAtLogin,
|
||||||
@@ -352,6 +368,9 @@ onUnmounted(() => {
|
|||||||
<SettingItem :title="t('settings.enableLoadingAnimation')">
|
<SettingItem :title="t('settings.enableLoadingAnimation')">
|
||||||
<ToggleSwitch v-model="enableLoadingAnimation"/>
|
<ToggleSwitch v-model="enableLoadingAnimation"/>
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
|
<SettingItem :title="t('settings.enableTabs')">
|
||||||
|
<ToggleSwitch v-model="enableTabs"/>
|
||||||
|
</SettingItem>
|
||||||
</SettingSection>
|
</SettingSection>
|
||||||
|
|
||||||
<SettingSection :title="t('settings.startup')">
|
<SettingSection :title="t('settings.startup')">
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ type GeneralConfig struct {
|
|||||||
|
|
||||||
// 界面设置
|
// 界面设置
|
||||||
EnableLoadingAnimation bool `json:"enableLoadingAnimation"` // 是否启用加载动画
|
EnableLoadingAnimation bool `json:"enableLoadingAnimation"` // 是否启用加载动画
|
||||||
|
EnableTabs bool `json:"enableTabs"` // 是否启用标签页模式
|
||||||
}
|
}
|
||||||
|
|
||||||
// HotkeyCombo 热键组合定义
|
// HotkeyCombo 热键组合定义
|
||||||
@@ -154,7 +155,8 @@ func NewDefaultAppConfig() *AppConfig {
|
|||||||
StartAtLogin: false,
|
StartAtLogin: false,
|
||||||
EnableWindowSnap: true, // 默认启用窗口吸附
|
EnableWindowSnap: true, // 默认启用窗口吸附
|
||||||
EnableGlobalHotkey: false,
|
EnableGlobalHotkey: false,
|
||||||
EnableLoadingAnimation: true, // 默认启用加载动画
|
EnableLoadingAnimation: true, // 默认启用加载动画
|
||||||
|
EnableTabs: false, // 默认不启用标签页模式
|
||||||
GlobalHotkey: HotkeyCombo{
|
GlobalHotkey: HotkeyCombo{
|
||||||
Ctrl: false,
|
Ctrl: false,
|
||||||
Shift: false,
|
Shift: false,
|
||||||
@@ -166,7 +168,7 @@ func NewDefaultAppConfig() *AppConfig {
|
|||||||
Editing: EditingConfig{
|
Editing: EditingConfig{
|
||||||
// 字体设置
|
// 字体设置
|
||||||
FontSize: 13,
|
FontSize: 13,
|
||||||
FontFamily: `"HarmonyOS SC", "HarmonyOS", "Microsoft YaHei", "PingFang SC", "Helvetica Neue", Arial, sans-serif`,
|
FontFamily: `"HarmonyOS"`,
|
||||||
FontWeight: "normal",
|
FontWeight: "normal",
|
||||||
LineHeight: 1.5,
|
LineHeight: 1.5,
|
||||||
// Tab设置
|
// Tab设置
|
||||||
|
|||||||
@@ -32,8 +32,8 @@ CREATE TABLE IF NOT EXISTS documents (
|
|||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
title TEXT NOT NULL,
|
title TEXT NOT NULL,
|
||||||
content TEXT DEFAULT '∞∞∞text-a',
|
content TEXT DEFAULT '∞∞∞text-a',
|
||||||
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
created_at TEXT NOT NULL,
|
||||||
updated_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
updated_at TEXT NOT NULL,
|
||||||
is_deleted INTEGER DEFAULT 0,
|
is_deleted INTEGER DEFAULT 0,
|
||||||
is_locked INTEGER DEFAULT 0
|
is_locked INTEGER DEFAULT 0
|
||||||
)`
|
)`
|
||||||
@@ -45,8 +45,8 @@ CREATE TABLE IF NOT EXISTS extensions (
|
|||||||
enabled INTEGER NOT NULL DEFAULT 1,
|
enabled INTEGER NOT NULL DEFAULT 1,
|
||||||
is_default INTEGER NOT NULL DEFAULT 0,
|
is_default INTEGER NOT NULL DEFAULT 0,
|
||||||
config TEXT DEFAULT '{}',
|
config TEXT DEFAULT '{}',
|
||||||
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
created_at TEXT NOT NULL,
|
||||||
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
|
updated_at TEXT NOT NULL
|
||||||
)`
|
)`
|
||||||
|
|
||||||
// Key bindings table
|
// Key bindings table
|
||||||
@@ -58,8 +58,8 @@ CREATE TABLE IF NOT EXISTS key_bindings (
|
|||||||
key TEXT NOT NULL,
|
key TEXT NOT NULL,
|
||||||
enabled INTEGER NOT NULL DEFAULT 1,
|
enabled INTEGER NOT NULL DEFAULT 1,
|
||||||
is_default INTEGER NOT NULL DEFAULT 0,
|
is_default INTEGER NOT NULL DEFAULT 0,
|
||||||
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
created_at TEXT NOT NULL,
|
||||||
updated_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
updated_at TEXT NOT NULL,
|
||||||
UNIQUE(command, extension)
|
UNIQUE(command, extension)
|
||||||
)`
|
)`
|
||||||
|
|
||||||
@@ -71,8 +71,8 @@ CREATE TABLE IF NOT EXISTS themes (
|
|||||||
type TEXT NOT NULL,
|
type TEXT NOT NULL,
|
||||||
colors TEXT NOT NULL,
|
colors TEXT NOT NULL,
|
||||||
is_default INTEGER NOT NULL DEFAULT 0,
|
is_default INTEGER NOT NULL DEFAULT 0,
|
||||||
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
created_at TEXT NOT NULL,
|
||||||
updated_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
updated_at TEXT NOT NULL,
|
||||||
UNIQUE(type, is_default)
|
UNIQUE(type, is_default)
|
||||||
)`
|
)`
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -177,12 +177,12 @@ func (ds *DocumentService) CreateDocument(title string) (*models.Document, error
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create document with default content
|
// Create document with default content
|
||||||
now := time.Now()
|
now := time.Now().Format("2006-01-02 15:04:05")
|
||||||
doc := &models.Document{
|
doc := &models.Document{
|
||||||
Title: title,
|
Title: title,
|
||||||
Content: "∞∞∞text-a\n",
|
Content: "∞∞∞text-a\n",
|
||||||
CreatedAt: now.String(),
|
CreatedAt: now,
|
||||||
UpdatedAt: now.String(),
|
UpdatedAt: now,
|
||||||
IsDeleted: false,
|
IsDeleted: false,
|
||||||
IsLocked: false,
|
IsLocked: false,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,13 +62,14 @@ func (ts *ThemeService) initializeDefaultThemes() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 创建默认深色主题
|
// 创建默认深色主题
|
||||||
|
now := time.Now().Format("2006-01-02 15:04:05")
|
||||||
darkTheme := &models.Theme{
|
darkTheme := &models.Theme{
|
||||||
Name: "Default Dark",
|
Name: "Default Dark",
|
||||||
Type: models.ThemeTypeDark,
|
Type: models.ThemeTypeDark,
|
||||||
Colors: *models.NewDefaultDarkTheme(),
|
Colors: *models.NewDefaultDarkTheme(),
|
||||||
IsDefault: true,
|
IsDefault: true,
|
||||||
CreatedAt: time.Now().String(),
|
CreatedAt: now,
|
||||||
UpdatedAt: time.Now().String(),
|
UpdatedAt: now,
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建默认浅色主题
|
// 创建默认浅色主题
|
||||||
@@ -77,8 +78,8 @@ func (ts *ThemeService) initializeDefaultThemes() error {
|
|||||||
Type: models.ThemeTypeLight,
|
Type: models.ThemeTypeLight,
|
||||||
Colors: *models.NewDefaultLightTheme(),
|
Colors: *models.NewDefaultLightTheme(),
|
||||||
IsDefault: true,
|
IsDefault: true,
|
||||||
CreatedAt: time.Now().String(),
|
CreatedAt: now,
|
||||||
UpdatedAt: time.Now().String(),
|
UpdatedAt: now,
|
||||||
}
|
}
|
||||||
|
|
||||||
// 插入默认主题
|
// 插入默认主题
|
||||||
@@ -174,7 +175,7 @@ func (ts *ThemeService) UpdateThemeColors(themeType models.ThemeType, colors mod
|
|||||||
`
|
`
|
||||||
|
|
||||||
db := ts.getDB()
|
db := ts.getDB()
|
||||||
_, err := db.Exec(query, colors, time.Now(), themeType)
|
_, err := db.Exec(query, colors, time.Now().Format("2006-01-02 15:04:05"), themeType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to update theme colors: %w", err)
|
return fmt.Errorf("failed to update theme colors: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user