495 lines
13 KiB
Vue
495 lines
13 KiB
Vue
<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>
|