markdown/src/App.vue

495 lines
13 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script setup>
import { onMounted, onUnmounted, ref, watch } from "vue";
import Toolbar from "./components/Toolbar.vue";
import FileTree from "./components/FileTree.vue";
import TabContainer from "./components/TabContainer.vue";
import StatusBar from "./components/StatusBar.vue";
import SearchPanel from "./components/SearchPanel.vue";
import GitPanel from "./components/GitPanel.vue";
import { useFileTree } from "./composables/useFileTree";
import { useConfig } from "./composables/useConfig";
import { useTheme } from "./composables/useTheme";
import { ConfigProvider } from 'ant-design-vue';
import { SettingOutlined, DeleteOutlined, FolderOpenOutlined } from '@ant-design/icons-vue';
const { setRootPath } = useFileTree();
const { loadConfig } = useConfig();
const {
getAntdTheme,
primaryColor,
toggleTheme,
isDark,
toggleDarkMode,
setGlobalTheme,
fontFamily,
codeFontFamily,
setFontFamily,
setCodeFontFamily
} = useTheme();
const route = ref("");
const showSearch = ref(false);
const showGit = ref(false);
const showSettings = ref(false);
const showHistory = ref(false); // 历史记录弹窗
const historyDirs = ref([]); // 历史目录列表
const lastOpenedDirCache = ref('');
const themeColors = [
'#1890ff', // 默认蓝
'#f5222d', // 薄暮
'#fa541c', // 火山
'#faad14', // 日暮
'#13c2c2', // 明青
'#52c41a', // 极光绿
'#2f54eb', // 极客蓝
'#722ed1', // 酱紫
];
const antdTheme = ref(getAntdTheme());
const DEFAULT_SIDEBAR_WIDTH = 250;
const sidebarWidth = ref(DEFAULT_SIDEBAR_WIDTH);
const isResizingSidebar = ref(false);
const sidebarResizeState = ref({ startX: 0, startWidth: DEFAULT_SIDEBAR_WIDTH });
const clampSidebarWidth = (width) => {
const minWidth = 180;
const maxWidth = 720;
return Math.min(maxWidth, Math.max(minWidth, Math.round(width)));
};
const stopSidebarResize = () => {
if (!isResizingSidebar.value) return;
isResizingSidebar.value = false;
window.removeEventListener('pointermove', handleSidebarPointerMove);
window.removeEventListener('pointerup', handleSidebarPointerUp);
window.removeEventListener('pointercancel', handleSidebarPointerUp);
document.body.style.userSelect = '';
document.body.style.cursor = '';
};
const handleSidebarPointerMove = (e) => {
if (!isResizingSidebar.value) return;
const deltaX = e.clientX - sidebarResizeState.value.startX;
sidebarWidth.value = clampSidebarWidth(sidebarResizeState.value.startWidth + deltaX);
};
const handleSidebarPointerUp = () => {
stopSidebarResize();
saveGlobalConfig();
};
const startSidebarResize = (e) => {
if (e.button !== 0) return;
e.preventDefault();
isResizingSidebar.value = true;
sidebarResizeState.value = { startX: e.clientX, startWidth: sidebarWidth.value };
document.body.style.userSelect = 'none';
document.body.style.cursor = 'col-resize';
window.addEventListener('pointermove', handleSidebarPointerMove);
window.addEventListener('pointerup', handleSidebarPointerUp);
window.addEventListener('pointercancel', handleSidebarPointerUp);
try {
e.currentTarget?.setPointerCapture?.(e.pointerId);
} catch {}
};
const resetSidebarWidth = () => {
sidebarWidth.value = DEFAULT_SIDEBAR_WIDTH;
saveGlobalConfig();
};
// 加载全局配置
const loadGlobalConfig = async () => {
if (window.services?.loadGlobalData) {
const data = await window.services.loadGlobalData();
if (data) {
// 恢复主题
if (data.theme) {
setGlobalTheme(
data.theme.primaryColor,
data.theme.isDark,
data.theme.fontFamily,
data.theme.codeFontFamily
);
}
// 恢复历史记录
if (data.historyDirs) {
historyDirs.value = data.historyDirs;
}
if (data.lastOpenedDir) {
lastOpenedDirCache.value = data.lastOpenedDir;
}
if (data.layout?.sidebarWidth) {
sidebarWidth.value = clampSidebarWidth(data.layout.sidebarWidth);
}
// 自动加载上次打开的目录
if (data.lastOpenedDir && window.services.pathExists(data.lastOpenedDir)) {
await openDirectory(data.lastOpenedDir);
} else if (historyDirs.value.length > 0) {
// 如果上次打开的目录不存在,但有历史记录,可以弹窗让用户选择,或者默认不打开
// 这里选择展示历史记录弹窗让用户选
showHistory.value = true;
}
}
}
};
// 保存全局配置
const saveGlobalConfig = async () => {
if (window.services?.saveGlobalData) {
await window.services.saveGlobalData({
lastOpenedDir: lastOpenedDirCache.value,
historyDirs: historyDirs.value,
layout: {
sidebarWidth: sidebarWidth.value
},
theme: {
primaryColor: primaryColor.value,
isDark: isDark.value,
fontFamily: fontFamily.value,
codeFontFamily: codeFontFamily.value
}
});
}
};
// 封装打开目录的逻辑,包含更新历史记录
const openDirectory = async (path) => {
await setRootPath(path);
await loadConfig(path);
lastOpenedDirCache.value = path;
// 更新历史记录
if (!historyDirs.value.includes(path)) {
historyDirs.value.unshift(path);
if (historyDirs.value.length > 10) historyDirs.value.pop();
} else {
// 移到第一位
const index = historyDirs.value.indexOf(path);
historyDirs.value.splice(index, 1);
historyDirs.value.unshift(path);
}
await saveGlobalConfig();
};
watch([primaryColor, isDark, fontFamily, codeFontFamily], async () => {
antdTheme.value = getAntdTheme();
// 主题变化时保存
await saveGlobalConfig();
});
onMounted(() => {
window.utools.onPluginEnter(async (action) => {
route.value = action.code;
// 加载全局配置 (包含主题和上次打开的目录)
// 注意:如果 action 指定了打开特定目录,则覆盖上次打开的目录
await loadGlobalConfig();
// 如果有payload可能是直接打开文件或目录
if (action.type === 'files' && action.payload.length > 0) {
const file = action.payload[0];
if (file.isDirectory) {
await openDirectory(file.path);
} else {
// 如果是文件...
}
}
});
window.utools.onPluginOut((isKill) => {
// 退出处理
});
});
onUnmounted(() => {
stopSidebarResize();
});
const handleSelectDirectory = async () => {
if (window.services?.selectDirectory) {
const paths = window.services.selectDirectory();
if (paths && paths.length > 0) {
await openDirectory(paths[0]);
}
}
};
const removeHistoryDir = async (path) => {
const index = historyDirs.value.indexOf(path);
if (index > -1) {
historyDirs.value.splice(index, 1);
await saveGlobalConfig();
}
};
const handleHistorySelect = (path) => {
openDirectory(path);
showHistory.value = false;
};
const { state } = useFileTree(); // 需要获取 rootPath 用于保存
</script>
<template>
<ConfigProvider :theme="antdTheme">
<div class="app-container">
<Toolbar
@select-directory="handleSelectDirectory"
@toggle-search="showSearch = !showSearch"
@toggle-git="showGit = !showGit"
@toggle-settings="showSettings = !showSettings"
@toggle-history="showHistory = !showHistory"
/>
<div class="main-content">
<div class="sidebar" :style="{ width: `${sidebarWidth}px` }">
<FileTree />
</div>
<div
class="sidebar-resizer"
:class="{ resizing: isResizingSidebar }"
@pointerdown="startSidebarResize"
@dblclick="resetSidebarWidth"
/>
<div class="editor-area">
<TabContainer />
</div>
</div>
<StatusBar />
<!-- 弹窗或侧滑面板 -->
<a-modal v-model:open="showSearch" title="搜索" :footer="null" width="600px">
<SearchPanel />
</a-modal>
<a-modal v-model:open="showGit" title="Git 操作" :footer="null" width="600px">
<GitPanel />
</a-modal>
<a-modal v-model:open="showSettings" title="设置" :footer="null" width="400px">
<div class="settings-item">
<div class="settings-label">主题色</div>
<div class="color-picker">
<div
v-for="color in themeColors"
:key="color"
class="color-block"
:style="{ backgroundColor: color }"
:class="{ active: primaryColor === color }"
@click="toggleTheme(color)"
></div>
</div>
</div>
<div class="settings-item">
<div class="settings-label">暗黑模式</div>
<a-switch v-model:checked="isDark" @change="toggleDarkMode" />
</div>
<div class="settings-item">
<div class="settings-label">应用字体</div>
<a-select :value="fontFamily" style="width: 100%" @change="setFontFamily">
<a-select-option value="default">系统默认</a-select-option>
<a-select-option value="sans">思源黑体 (Noto Sans)</a-select-option>
<a-select-option value="serif">思源宋体 (Noto Serif)</a-select-option>
<a-select-option value="lxgw">霞鹜文楷 (LXGW WenKai)</a-select-option>
<a-select-option value="rounded">圆体 (M PLUS Rounded)</a-select-option>
<a-select-option value="zen">丸黑 (Zen Maru Gothic)</a-select-option>
</a-select>
</div>
<div class="settings-item">
<div class="settings-label">代码字体</div>
<a-select :value="codeFontFamily" style="width: 100%" @change="setCodeFontFamily">
<a-select-option value="default">系统默认 (Monospace)</a-select-option>
<a-select-option value="fira">Fira Code</a-select-option>
<a-select-option value="jetbrains">JetBrains Mono</a-select-option>
<a-select-option value="roboto">Roboto Mono</a-select-option>
<a-select-option value="source">Source Code Pro</a-select-option>
</a-select>
</div>
</a-modal>
<!-- 历史记录弹窗 -->
<a-modal v-model:open="showHistory" title="选择历史目录" :footer="null" width="600px">
<div v-if="historyDirs.length === 0" class="empty-history">
暂无历史记录
</div>
<a-list v-else :data-source="historyDirs" size="small" :split="false">
<template #renderItem="{ item }">
<a-list-item class="history-item">
<div class="history-content" @click="handleHistorySelect(item)">
<FolderOpenOutlined class="history-icon" />
<span class="history-text" :title="item">{{ item }}</span>
</div>
<a-button
type="text"
danger
size="small"
class="delete-btn"
@click.stop="removeHistoryDir(item)"
>
<DeleteOutlined />
</a-button>
</a-list-item>
</template>
</a-list>
</a-modal>
</div>
</ConfigProvider>
</template>
<style scoped>
.app-container {
display: flex;
flex-direction: column;
height: 100vh;
width: 100vw;
overflow: hidden;
background-color: var(--card-background, #f0f2f5);
color: var(--text-color, #000);
}
.main-content {
flex: 1;
display: flex;
overflow: hidden;
}
.sidebar {
flex: 0 0 auto;
background-color: var(--card-background, #fff);
display: flex;
flex-direction: column;
}
.sidebar-resizer {
width: 6px;
cursor: col-resize;
position: relative;
flex: 0 0 auto;
background: transparent;
}
.sidebar-resizer::before {
content: "";
position: absolute;
top: 0;
bottom: 0;
left: 2px;
width: 1px;
background: var(--border-color, #e8e8e8);
opacity: 0.8;
}
.sidebar-resizer:hover::before,
.sidebar-resizer.resizing::before {
left: 1px;
width: 2px;
background: var(--primary-color, #1890ff);
opacity: 1;
}
.editor-area {
flex: 1;
display: flex;
flex-direction: column;
background-color: var(--card-background, #fff);
overflow: hidden;
}
.settings-item {
margin-bottom: 24px;
}
.settings-label {
margin-bottom: 12px;
font-weight: 500;
}
.color-picker {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
.color-block {
width: 24px;
height: 24px;
border-radius: 4px;
cursor: pointer;
border: 2px solid transparent;
transition: all 0.3s;
}
.color-block:hover {
transform: scale(1.1);
}
.color-block.active {
border-color: var(--text-color);
box-shadow: 0 0 0 2px var(--primary-color-bg);
}
.history-item {
padding: 4px 8px !important;
border-radius: 6px;
cursor: pointer;
transition: background-color 0.2s;
display: flex;
align-items: center;
justify-content: space-between;
}
.history-item:hover {
background-color: rgba(0, 0, 0, 0.04);
}
.history-content {
flex: 1;
display: flex;
align-items: center;
overflow: hidden;
margin-right: 8px;
}
.history-icon {
margin-right: 8px;
color: var(--primary-color, #1890ff);
font-size: 16px;
flex-shrink: 0;
}
.history-text {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: var(--text-color);
}
.delete-btn {
opacity: 0;
transition: opacity 0.2s;
}
.history-item:hover .delete-btn {
opacity: 1;
}
.empty-history {
text-align: center;
padding: 40px 0;
color: #999;
}
</style>