306 lines
15 KiB
Vue
306 lines
15 KiB
Vue
<template>
|
||
<Space direction="vertical" style="margin-left: 5px; margin-top: 5px;">
|
||
<Space>
|
||
<Button v-if="!loginStatus" type="primary" shape="circle" icon="md-person"
|
||
@click="showLoginModal = true"></Button>
|
||
<Button v-else type="error" shape="circle" icon="md-log-out" @click="logout"></Button>
|
||
<SelectFolder ref="selectFolderRef" :rootPath="rootFolderPath" :nodeDataCallback="appendDataForNode"
|
||
@update:rootPath="changeRootPath" @folder-selected="showFileTree" />
|
||
</Space>
|
||
<FolderTree ref="folderTreeRef" :treeData="folderTreeData" :expandLevel="1" :selectedNode="[currentFilePath]"
|
||
:specifyFileSuffix="['md']" @file-selected="fileSelected" />
|
||
</Space>
|
||
<UserLogin ref="loginRef" v-model:visible="showLoginModal" v-model:loginStatus="loginStatus"></UserLogin>
|
||
</template>
|
||
|
||
<script setup>
|
||
import UserLogin from './UI/UserLogin.vue';
|
||
import SelectFolder from './UI/SelectFolder.vue';
|
||
import FolderTree from './UI/FolderTree.vue';
|
||
import { ref, watch } from 'vue';
|
||
import { mkdir, remove, create, exists, rename } from '@tauri-apps/plugin-fs';
|
||
import { Message } from 'view-ui-plus'
|
||
|
||
defineProps({
|
||
currentFilePath: {
|
||
type: String,
|
||
required: false,
|
||
default: ''
|
||
},
|
||
rootFolderPath: {
|
||
type: String,
|
||
required: false,
|
||
default: ''
|
||
},
|
||
});
|
||
|
||
const emit = defineEmits([
|
||
'update:currentFilePath',
|
||
'update:rootFolderPath'
|
||
]);
|
||
|
||
// 调用登录组件,获取登录状态,以及点击可登出
|
||
const loginRef = ref(null);
|
||
const loginStatus = ref(false);
|
||
const showLoginModal = ref(false);
|
||
const logout = () => {
|
||
loginRef.value.logout();
|
||
};
|
||
|
||
const selectFolderRef = ref(null)
|
||
watch(loginStatus, (newStatus) => {
|
||
if (newStatus === true && window.__TAURI_INTERNALS__ === undefined) {
|
||
// (仅限浏览器环境)如果登录成功,获取当前用户的文件树
|
||
selectFolderRef.value.exposeTreeData();
|
||
}
|
||
})
|
||
|
||
const folderTreeRef = ref(null)
|
||
// <SelectFolder> 返回的文件树数据,增加自定义数据,便于传给 <FolderTree> 进行渲染
|
||
function appendDataForNode(nodeData) {
|
||
if (nodeData.directory) {
|
||
// 当前节点为文件夹,增加“新建目录”“新建文件”等右键菜单
|
||
nodeData.contextMenuData = [
|
||
{
|
||
name: "新建目录",
|
||
handler: (nodeRenderData) => {
|
||
// 新建子节点时自动展开当前文件夹
|
||
nodeRenderData.expand = true;
|
||
// 定义子节点为 input 输入框
|
||
let subNodeData = {
|
||
input: true,
|
||
completer: async (data, newName) => {
|
||
newName = newName.trim();
|
||
// 如果输入名称为空,则取消当前节点
|
||
if (newName.length == 0) {
|
||
folderTreeRef.value.removeTreeNode(data);
|
||
return;
|
||
}
|
||
// 文件名不能包含 / 符号
|
||
if (newName.indexOf('/') >= 0) {
|
||
Message.error('不能包含 / 符号');
|
||
folderTreeRef.value.removeTreeNode(data);
|
||
return;
|
||
}
|
||
let createSubNodeOfParent = false;
|
||
// 输入名称不为空,尝试在该目录下新建子节点
|
||
try {
|
||
const newPath = nodeData.path + '/' + newName;
|
||
data.sourceData.path = newPath;
|
||
data.sourceData.name = newName;
|
||
// 给新增的子节点也执行 appendDataForNode 操作
|
||
appendDataForNode(data.sourceData);
|
||
// 重新渲染当前节点
|
||
if (folderTreeRef.value.reRenderTreeNode(data) === false) {
|
||
throw new Error("变更文件树失败");
|
||
}
|
||
createSubNodeOfParent = true;
|
||
// 最后再尝试操作系统创建目录
|
||
await mkdir(newPath);
|
||
Message.success('创建目录成功: ' + newPath);
|
||
} catch (err) {
|
||
Message.error('创建目录失败: ' + err);
|
||
// 如果最后操作系统接口失败,需要回滚文件树新增的节点
|
||
if (createSubNodeOfParent) {
|
||
folderTreeRef.value.removeTreeNode(data);
|
||
}
|
||
}
|
||
},
|
||
path: "", // 留空,在 completer 中填充
|
||
name: "", // 留空,在 completer 中填充
|
||
directory: true, // 当前新建的是目录,所以为 true
|
||
};
|
||
// 将子节点挂载到文件树上,在当前目录之下
|
||
folderTreeRef.value.appendTreeNode(nodeRenderData, subNodeData);
|
||
}
|
||
},
|
||
{
|
||
name: "新建文件",
|
||
handler: (nodeRenderData) => {
|
||
// 新建子节点时自动展开当前文件夹
|
||
nodeRenderData.expand = true;
|
||
// 定义子节点为 input 输入框
|
||
let subNodeData = {
|
||
input: true,
|
||
completer: async (data, newName) => {
|
||
newName = newName.trim();
|
||
// 如果输入名称为空,则取消当前节点
|
||
if (newName.length == 0) {
|
||
folderTreeRef.value.removeTreeNode(data);
|
||
return;
|
||
}
|
||
// 文件名不能包含 / 符号
|
||
if (newName.indexOf('/') >= 0) {
|
||
Message.error('不能包含 / 符号');
|
||
folderTreeRef.value.removeTreeNode(data);
|
||
return;
|
||
}
|
||
// 如果文件不以 .md 为后缀,同样取消当前输入框
|
||
if (!newName.endsWith('.md')) {
|
||
Message.error('只允许 .md 文件');
|
||
folderTreeRef.value.removeTreeNode(data);
|
||
return;
|
||
}
|
||
let createSubNodeOfParent = false;
|
||
// 输入名称不为空,尝试在该目录下新建子节点
|
||
try {
|
||
const newPath = nodeData.path + '/' + newName;
|
||
// 检查新的路径是否已存在,避免覆盖其他文件
|
||
const exist = await exists(newPath);
|
||
if (exist) {
|
||
// 恢复当前节点并报错
|
||
folderTreeRef.value.removeTreeNode(data);
|
||
throw new Error("存在同名文件.");
|
||
}
|
||
data.sourceData.path = newPath;
|
||
data.sourceData.name = newName;
|
||
// 给新增的子节点也执行 appendDataForNode 操作
|
||
appendDataForNode(data.sourceData);
|
||
// 重新渲染当前节点
|
||
if (folderTreeRef.value.reRenderTreeNode(data) === false) {
|
||
throw new Error("变更文件树失败");
|
||
}
|
||
createSubNodeOfParent = true;
|
||
// 最后再尝试操作系统创建目录
|
||
await create(newPath);
|
||
Message.success('创建文件成功: ' + newPath);
|
||
} catch (err) {
|
||
Message.error('创建文件失败: ' + err);
|
||
// 如果最后操作系统接口失败,需要回滚文件树新增的节点
|
||
if (createSubNodeOfParent) {
|
||
folderTreeRef.value.removeTreeNode(data);
|
||
}
|
||
}
|
||
},
|
||
path: "", // 留空,在 completer 中填充
|
||
name: "", // 留空,在 completer 中填充
|
||
directory: false, // 当前新建的是文件,所以为 false
|
||
};
|
||
// 将子节点挂载到文件树上,在当前目录之下
|
||
folderTreeRef.value.appendTreeNode(nodeRenderData, subNodeData);
|
||
}
|
||
},
|
||
{
|
||
name: "删除",
|
||
handler: async (nodeRenderData) => {
|
||
try {
|
||
if (folderTreeRef.value.removeTreeNode(nodeRenderData) === false) {
|
||
throw new Error("变更文件树失败");
|
||
}
|
||
await remove(nodeRenderData.path, { recursive: true });
|
||
Message.success('删除目录成功: ' + nodeRenderData.path);
|
||
} catch (err) {
|
||
Message.error('删除目录失败: ' + err);
|
||
}
|
||
}
|
||
}
|
||
];
|
||
} else {
|
||
// 给文件节点,增加 suffix 字段,用于表示文件后缀,便于 <FolderTree> 过滤文件
|
||
let splitResult = nodeData.name.split('.');
|
||
let fileSuffix = splitResult.length > 1 ? splitResult.pop() : '';
|
||
nodeData.suffix = fileSuffix;
|
||
// 当前节点为文件,增加“重命名”“删除”等右键菜单
|
||
nodeData.contextMenuData = [
|
||
{
|
||
name: "重命名",
|
||
handler: (nodeRenderData) => {
|
||
// 将该节点转为 input 输入框
|
||
nodeRenderData.sourceData.input = true;
|
||
nodeRenderData.sourceData.completer = async (data, newName) => {
|
||
newName = newName.trim();
|
||
// 如果输入名称为空,不做任何变动,恢复当前节点
|
||
if (newName.length == 0) {
|
||
folderTreeRef.value.reRenderTreeNode(data);
|
||
return;
|
||
}
|
||
// 文件名不能包含 / 符号
|
||
if (newName.indexOf('/') >= 0) {
|
||
Message.error('不能包含 / 符号');
|
||
folderTreeRef.value.reRenderTreeNode(data);
|
||
return;
|
||
}
|
||
// 如果文件不以 .md 为后缀,同样恢复当前节点
|
||
if (!newName.endsWith('.md')) {
|
||
Message.error('只允许 .md 文件');
|
||
folderTreeRef.value.reRenderTreeNode(data);
|
||
return;
|
||
}
|
||
// 尝试重命名该节点
|
||
let newRenderNode = null;
|
||
let oldName = '';
|
||
let oldPath = nodeData.path;
|
||
let splitPathResult = oldPath.split('/');
|
||
try {
|
||
if (splitPathResult.length <= 1) {
|
||
// 恢复当前节点并报错
|
||
folderTreeRef.value.reRenderTreeNode(data);
|
||
throw new Error("该节点文件路径非法.");
|
||
}
|
||
oldName = splitPathResult.pop();
|
||
let newPath = splitPathResult.join('/') + '/' + newName;
|
||
// 检查新的路径是否已存在,避免覆盖其他文件
|
||
const exist = await exists(newPath);
|
||
if (exist) {
|
||
// 恢复当前节点并报错
|
||
folderTreeRef.value.reRenderTreeNode(data);
|
||
throw new Error("存在同名文件.");
|
||
}
|
||
data.sourceData.path = newPath;
|
||
data.sourceData.name = newName;
|
||
// 新的节点数据需要执行 appendDataForNode 操作
|
||
appendDataForNode(data.sourceData);
|
||
// 重新渲染当前节点
|
||
newRenderNode = folderTreeRef.value.reRenderTreeNode(data);
|
||
if (newRenderNode === false) {
|
||
throw new Error("变更文件树失败");
|
||
}
|
||
// 最后再尝试操作系统重命名
|
||
await rename(oldPath, newPath);
|
||
Message.success('重命名成功: ' + newPath);
|
||
} catch (err) {
|
||
Message.error('重命名失败: ' + err);
|
||
// 如果最后操作系统接口失败,需要回滚文件树新增的节点
|
||
if (newRenderNode !== null && newRenderNode !== false) {
|
||
newRenderNode.sourceData.path = oldPath;
|
||
newRenderNode.sourceData.name = oldName;
|
||
folderTreeRef.value.reRenderTreeNode(newRenderNode);
|
||
}
|
||
}
|
||
};
|
||
// 将当前节点重新渲染为输入框
|
||
folderTreeRef.value.reRenderTreeNode(nodeRenderData);
|
||
}
|
||
},
|
||
{
|
||
name: "删除",
|
||
handler: async (nodeRenderData) => {
|
||
try {
|
||
if (folderTreeRef.value.removeTreeNode(nodeRenderData) === false) {
|
||
throw new Error("变更文件树失败");
|
||
}
|
||
await remove(nodeRenderData.path, { recursive: true });
|
||
Message.success('删除文件成功: ' + nodeRenderData.path);
|
||
} catch (err) {
|
||
Message.error('删除文件失败: ' + err);
|
||
}
|
||
}
|
||
},
|
||
];
|
||
}
|
||
}
|
||
|
||
// <SelectFolder> 选择目录后赋值给 folderTreeData,然后再传递给 <FolderTree>
|
||
const folderTreeData = ref(null)
|
||
function showFileTree(treeData) {
|
||
folderTreeData.value = treeData
|
||
}
|
||
|
||
const changeRootPath = (newRootPath) => {
|
||
emit('update:rootFolderPath', newRootPath); // 触发双向绑定更新
|
||
}
|
||
|
||
const fileSelected = (fileRenderData) => {
|
||
emit('update:currentFilePath', fileRenderData.path); // 触发双向绑定更新
|
||
}
|
||
</script> |