287 lines
10 KiB
Vue
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>
|