287 lines
10 KiB
Vue

<template>
<Split v-model="split" min="5px">
<template #left>
<div class="split-left" :class="hiddenSplitLeft">
<LeftSidebar v-model:rootFolderPath="rootFolderPath" v-model:currentFilePath="currentFilePath"
:confirmSwitchingFile="confirmSwitchingFile">
</LeftSidebar>
</div>
</template>
<template #trigger>
<div ref="splitTrigger" class="split-trigger"></div>
</template>
<template #right>
<div ref="splitRight" class="split-right">
<MainEditor ref="mainEditor" v-model:markdownCode="markdownCode"
v-model:leftSidebarState="leftSidebarState" :save="writeFileContent" @exportPDF="exportMarkdownPDF">
</MainEditor>
</div>
</template>
</Split>
</template>
<script setup>
import LeftSidebar from './components/LeftSidebar.vue'
import MainEditor from './components/MainEditor.vue'
import { ref, watch, useTemplateRef, nextTick, onMounted, onUnmounted } from "vue";
import { invoke } from "@tauri-apps/api/core";
import { getCurrentWindow } from "@tauri-apps/api/window";
import { readTextFile, writeTextFile, writeFile, exists } from '@tauri-apps/plugin-fs';
import { save, confirm } from '@tauri-apps/plugin-dialog';
import { Message, Notice, Modal } from 'view-ui-plus'
import { GetSingleArticle, UpdateArticle } from '/src/utils/userHandler';
// 原始示例数据,仅供参考
const greetMsg = ref("");
const name = ref("");
async function greet() {
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
greetMsg.value = await invoke("greet", { name: name.value });
}
// 使用 Split 组件动态控制页面左右布局
const defaultSplit = 0.15; // 默认左侧边栏和右侧区域的比例为 1.5:8.5
const initSplit = localStorage.getItem('splitValueOfFolderTree');
const split = initSplit === null ? ref(defaultSplit) : ref(Number(initSplit));
const splitTrigger = useTemplateRef('splitTrigger');
const splitRight = useTemplateRef('splitRight');
const splitRightWidth = ref(0);
const hiddenSplitLeft = ref('');
const leftSidebarState = split.value > 0 ? ref('open') : ref('close');
watch(split, (newSplit) => {
localStorage.setItem('splitValueOfFolderTree', newSplit);
splitRightWidth.value = splitRight.value.offsetWidth;
// 当 split 小于某个值时,隐藏左边布局
if (newSplit < 0.05) {
split.value = 0;
leftSidebarState.value = 'close';
if (hiddenSplitLeft.value == '') {
hiddenSplitLeft.value = 'hidden';
}
} else {
leftSidebarState.value = 'open';
if (hiddenSplitLeft.value == 'hidden') {
hiddenSplitLeft.value = '';
}
}
})
watch(leftSidebarState, (state) => {
if (state == 'open') {
split.value = defaultSplit;
} else {
split.value = 0;
}
})
// 监听根目录路径。如果之前已经选过目录,初始界面直接加载该目录
const rootFolderPath = ref(localStorage.getItem('rootPathOfFolderTree') || "");
watch(rootFolderPath, (newRootPath) => {
localStorage.setItem('rootPathOfFolderTree', newRootPath);
})
// <FolderTree> 完成文件树渲染后,点击文件得到 currentFilePath
const currentFilePath = ref(localStorage.getItem('currentSelectedFilePath') || "");
let switchFilePath = true; // 切换文件时,避免触发 markdownCode 的 watch 事件
// 读取 currentFilePath 的文件内容,填充到 MarkdownEditor 之中
watch(currentFilePath, async (newFilePath) => {
switchFilePath = true;
localStorage.setItem('currentSelectedFilePath', newFilePath);
await readFileContent(newFilePath);
})
// 切换文件时,如果当前文件的变动未保存,允许用户取消切换动作,保存当前文件内容
const confirmSwitchingFile = () => {
return new Promise((resolve) => {
// 如果当前文件内容未被修改,允许切换文件
if (currentFileSaved()) {
resolve(true);
return;
}
// 如果当前文件内容被修改,允许用户取消切换,可以保存后再切换
Modal.confirm({
title: '当前文件的内容变动未保存',
content: `<p><b>${currentFilePath.value}</b> 内容被修改但仍未保存,确认切换文件?<br/><br/>注:确认切换之后,内容变动会丢失</p>`,
okText: "仍然切换",
cancelText: "取消切换",
onOk: () => {
resolve(true);
},
onCancel: () => {
resolve(false);
}
});
});
}
const currentFileSaved = () => {
// 判断当前文件内容是否被修改但未保存,如果已保存则返回 true
const lastSaveTimeValue = lastSaveTime.get(currentFilePath.value);
const isUpdated = lastSaveTimeValue && lastUpdateTime.value > lastSaveTimeValue;
return !isUpdated;
}
const getFileNameFromFilePath = (filePath) => {
const fullFileName = filePath.split('/').pop();
const splitResult = fullFileName.split(".");
splitResult.pop(); // 丢掉后缀
return splitResult.join('.');
}
const mainEditor = ref(null);
const markdownCode = ref("# Hello Markdown");
const lastSaveTime = new Map(); // 记录每个文件的最后保存时间
const lastUpdateTime = ref(0); // 记录当前文件的最后更新时间
watch(markdownCode, (newMarkdownCode) => {
if (switchFilePath === true) {
switchFilePath = false;
return;
}
lastUpdateTime.value = new Date().getTime();
console.log("code be updated");
})
async function readFileContent(filePath) {
if (window.__TAURI_INTERNALS__ === undefined) {
// 调用接口读取 currentFilePath 文件内容
const match = filePath.match(/(\d+)$/);
const articleID = match ? parseInt(match[1], 10) : null;
GetSingleArticle(articleID).then((data) => {
markdownCode.value = data.mdCode;
currentFilePath.value = filePath;
lastSaveTime.set(filePath, new Date().getTime());
Message.success('已读取文件内容,并加载到编辑器中:' + filePath);
}).catch((error) => {
Message.error(`调用异常: ${error}`);
});
return;
}
try {
const fileContent = await readTextFile(filePath);
markdownCode.value = fileContent;
currentFilePath.value = filePath;
lastSaveTime.set(filePath, new Date().getTime());
Message.success('已读取文件内容,并加载到编辑器中:' + filePath);
} catch (err) {
Message.error('文件读取失败:' + err);
}
}
async function writeFileContent(markdownCode) {
// 如果未选择任何文件,则不触发写文件事件
if (currentFilePath.value.length == 0) {
Message.warning('当前编辑器暂未关联目标文件,请在左侧打开目录并选择一个文件')
return;
}
try {
if (window.__TAURI_INTERNALS__ === undefined) {
// 调用接口更新 currentFilePath 文件内容
const match = currentFilePath.value.match(/(\d+)$/);
const articleID = match ? parseInt(match[1], 10) : null;
UpdateArticle(articleID, null, markdownCode).then((data) => {
Message.success('更新成功');
}).catch((error) => {
Message.error(`更新异常: ${error}`);
});
} else {
const exist = await exists(currentFilePath.value);
if (!exist) {
throw new Error(currentFilePath.value + " 文件不存在.");
}
await writeTextFile(currentFilePath.value, markdownCode);
Message.success('文件更新成功:' + currentFilePath.value);
}
lastSaveTime.set(currentFilePath.value, new Date().getTime());
} catch (err) {
Message.error('文件更新失败:' + err);
}
}
async function exportMarkdownPDF(pdfBuffer) {
let fileName = 'markdown.pdf';
if (currentFilePath.value.length > 0) {
fileName = getFileNameFromFilePath(currentFilePath.value) + '.pdf';
}
try {
const filePath = await save({
title: '保存 PDF', // 对话框标题
defaultPath: fileName, // 保存的文件名称
});
if (filePath) {
await writeFile(filePath, new Uint8Array(pdfBuffer));
Notice.success({
title: '导出成功',
desc: 'PDF 已保存至 ' + filePath,
});
} else {
console.log('用户主动取消');
}
} catch (err) {
console.log(err);
Notice.error({
title: '导出失败',
desc: '请将文件保存在用户有权限的目录之下',
});
}
}
const resizeHandler = () => {
// splitRight 的宽度为视口宽度右边百分比,再减去 splitTrigger 的宽度
splitRightWidth.value = window.innerWidth * (1 - split.value) - splitTrigger.value.offsetWidth;
}
onMounted(async () => {
// 使用 nextTick 确保页面组件已完成挂载和渲染
await nextTick();
resizeHandler();
// 监听页面宽度和高度,调整 markdown 编辑器的高宽度
window.addEventListener('resize', resizeHandler);
// 打开上一次选择的文件的内容
if (currentFilePath.value.length > 0) {
await readFileContent(currentFilePath.value);
}
// 监听窗口关闭事件。如果当前文件内容未保存,允许取消关闭事件
await getCurrentWindow().onCloseRequested(async (event) => {
if (currentFileSaved()) {
return; // 如果当前文件已保存,允许关闭窗口
}
const confirmed = await confirm('当前文件内容未保存,是否确认关闭窗口?');
if (!confirmed) {
// user did not confirm closing the window; let's prevent it
event.preventDefault();
}
});
});
onUnmounted(() => {
window.removeEventListener('resize', resizeHandler);
});
</script>
<style>
body {
overflow: hidden;
/* 禁用所有方向滚动 */
}
.split-left {
height: 100vh;
border-right: 1px solid #dcdee2;
overflow-x: hidden;
overflow-y: auto;
scrollbar-width: none;
}
.split-trigger {
width: 2px;
height: 100vh;
background-color: #e6e6e6;
}
.split-trigger:hover {
cursor: ew-resize;
}
.split-right {
margin-left: 2px;
}
.hidden {
display: none;
}
</style>