tauri-markdown/src/components/LeftSidebar.vue
2025-05-06 01:48:20 +08:00

306 lines
15 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.

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