Added tab functionality and optimized related configurations

This commit is contained in:
2025-10-04 02:27:32 +08:00
parent 2d02bf7f1f
commit 45968cd353
21 changed files with 689 additions and 166 deletions

View File

@@ -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);
} }

View File

@@ -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']

View File

@@ -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);

View File

@@ -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,

View File

@@ -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;

View File

@@ -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 {

View 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>

View File

@@ -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 {

View File

@@ -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 ? '&#xE923;' : '&#xE922;'); const maximizeIcon = computed(() => isMaximized.value ? '&#xE923;' : '&#xE922;');
// 判断是否在设置页面
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';
}); });

View File

@@ -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;

View File

@@ -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',

View File

@@ -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: '数据存储',

View File

@@ -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';
@@ -38,7 +38,7 @@ export const useConfigStore = defineStore('config', () => {
isLoading: false, isLoading: false,
configLoaded: false configLoaded: false
}); });
// Font options (no longer localized) // Font options (no longer localized)
const fontOptions = computed(() => FONT_OPTIONS); const fontOptions = computed(() => FONT_OPTIONS);
@@ -205,7 +205,6 @@ export const useConfigStore = defineStore('config', () => {
}; };
// 初始化语言设置 // 初始化语言设置
const initializeLanguage = async (): Promise<void> => { const initializeLanguage = async (): Promise<void> => {
try { try {
@@ -255,7 +254,7 @@ export const useConfigStore = defineStore('config', () => {
configLoaded: computed(() => state.configLoaded), configLoaded: computed(() => state.configLoaded),
isLoading: computed(() => state.isLoading), isLoading: computed(() => state.isLoading),
fontOptions, fontOptions,
// 限制常量 // 限制常量
...limits, ...limits,
@@ -317,19 +316,26 @@ export const useConfigStore = defineStore('config', () => {
// 再调用系统设置API // 再调用系统设置API
await StartupService.SetEnabled(value); await StartupService.SetEnabled(value);
}, },
// 窗口吸附配置相关方法 // 窗口吸附配置相关方法
setEnableWindowSnap: async (value: boolean) => await updateGeneralConfig('enableWindowSnap', value), setEnableWindowSnap: async (value: boolean) => await updateGeneralConfig('enableWindowSnap', value),
// 加载动画配置相关方法 // 加载动画配置相关方法
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),

View 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
};
});

View File

@@ -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});
}); });

View File

@@ -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;
}
}
}
/** /**
* 交换源语言和目标语言 * 交换源语言和目标语言
*/ */

View File

@@ -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')">
@@ -734,4 +753,4 @@ onUnmounted(() => {
opacity: 0; opacity: 0;
transform: translateY(-4px); transform: translateY(-4px);
} }
</style> </style>

View File

@@ -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设置

View File

@@ -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)
)` )`
) )

View File

@@ -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,
} }

View File

@@ -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)
} }