Compare commits

...

2 Commits

Author SHA1 Message Date
Frankie Huang
f2fa1992e6 feat: 支持 PDF 导出功能 2025-05-01 15:30:24 +08:00
Frankie Huang
94ebd22875 feat: 使用 @vavt/v3-extension 扩展组件实现主题切换 2025-05-01 15:27:10 +08:00
4 changed files with 83 additions and 43 deletions

10
package-lock.json generated
View File

@ -12,6 +12,7 @@
"@tauri-apps/plugin-dialog": "^2.2.1", "@tauri-apps/plugin-dialog": "^2.2.1",
"@tauri-apps/plugin-fs": "^2.2.1", "@tauri-apps/plugin-fs": "^2.2.1",
"@tauri-apps/plugin-opener": "^2", "@tauri-apps/plugin-opener": "^2",
"@vavt/v3-extension": "^3.0.0",
"md-editor-v3": "^5.5.0", "md-editor-v3": "^5.5.0",
"scriptjs": "^2.5.9", "scriptjs": "^2.5.9",
"view-ui-plus": "^1.3.19", "view-ui-plus": "^1.3.19",
@ -1641,6 +1642,15 @@
"integrity": "sha512-YIfAvArSFVXmWvoF+DEGD0FhkhVNcCtVWWkfYtj76eSrwHh/wuEEFhiEubg1XLNM3tChO8FH8xJCT/hnizjgFQ==", "integrity": "sha512-YIfAvArSFVXmWvoF+DEGD0FhkhVNcCtVWWkfYtj76eSrwHh/wuEEFhiEubg1XLNM3tChO8FH8xJCT/hnizjgFQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@vavt/v3-extension": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@vavt/v3-extension/-/v3-extension-3.0.0.tgz",
"integrity": "sha512-R3XFrihlk+FmoTEivToszgcv7D168hHkfMyS6pkHWyrvQy0MNP8m8b6qIWuINfvpbhr0OiMa9ktzyUH2DLFWfA==",
"license": "MIT",
"peerDependencies": {
"md-editor-v3": ">=5.2.0"
}
},
"node_modules/@vitejs/plugin-vue": { "node_modules/@vitejs/plugin-vue": {
"version": "5.2.3", "version": "5.2.3",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.3.tgz", "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.3.tgz",

View File

@ -14,6 +14,7 @@
"@tauri-apps/plugin-dialog": "^2.2.1", "@tauri-apps/plugin-dialog": "^2.2.1",
"@tauri-apps/plugin-fs": "^2.2.1", "@tauri-apps/plugin-fs": "^2.2.1",
"@tauri-apps/plugin-opener": "^2", "@tauri-apps/plugin-opener": "^2",
"@vavt/v3-extension": "^3.0.0",
"md-editor-v3": "^5.5.0", "md-editor-v3": "^5.5.0",
"scriptjs": "^2.5.9", "scriptjs": "^2.5.9",
"view-ui-plus": "^1.3.19", "view-ui-plus": "^1.3.19",

View File

@ -12,7 +12,7 @@
<template #right> <template #right>
<div ref="splitRight" class="split-right"> <div ref="splitRight" class="split-right">
<MainEditor ref="mainEditor" v-model:markdownCode="markdownCode" <MainEditor ref="mainEditor" v-model:markdownCode="markdownCode"
v-model:leftSidebarState="leftSidebarState" :save="writeFileContent"> v-model:leftSidebarState="leftSidebarState" :save="writeFileContent" @exportPDF="exportMarkdownPDF">
</MainEditor> </MainEditor>
</div> </div>
</template> </template>
@ -24,8 +24,9 @@ import LeftSidebar from './components/LeftSidebar.vue'
import MainEditor from './components/MainEditor.vue' import MainEditor from './components/MainEditor.vue'
import { ref, watch, useTemplateRef, nextTick, onMounted, onUnmounted } from "vue"; import { ref, watch, useTemplateRef, nextTick, onMounted, onUnmounted } from "vue";
import { invoke } from "@tauri-apps/api/core"; import { invoke } from "@tauri-apps/api/core";
import { readTextFile, writeTextFile, exists } from '@tauri-apps/plugin-fs'; import { readTextFile, writeTextFile, writeFile, exists } from '@tauri-apps/plugin-fs';
import { Message } from 'view-ui-plus' import { save } from '@tauri-apps/plugin-dialog';
import { Message, Notice } from 'view-ui-plus'
// //
const greetMsg = ref(""); const greetMsg = ref("");
@ -79,6 +80,12 @@ watch(currentFilePath, async (newFilePath) => {
localStorage.setItem('currentSelectedFilePath', newFilePath); localStorage.setItem('currentSelectedFilePath', newFilePath);
await readFileContent(newFilePath); await readFileContent(newFilePath);
}) })
const getFileNameFromFilePath = (filePath) => {
const fullFileName = filePath.split('/').pop();
const splitResult = fullFileName.split(".");
splitResult.pop(); //
return splitResult.join('.');
}
const mainEditor = ref(null); const mainEditor = ref(null);
const markdownCode = ref("# Hello Markdown"); const markdownCode = ref("# Hello Markdown");
@ -122,6 +129,35 @@ async function writeFileContent(markdownCode) {
Message.error('文件更新失败:' + 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 = () => { const resizeHandler = () => {
// splitRight splitTrigger // splitRight splitTrigger

View File

@ -18,33 +18,15 @@
size="22" /> size="22" />
</template> </template>
</NormalToolbar> </NormalToolbar>
<ExportPDF :modelValue="editorState.text" :customize="customizePDF"
@onProgress="handleExportProgress" />
<NormalToolbar title="发布文章" @onClick="publish"> <NormalToolbar title="发布文章" @onClick="publish">
<template #trigger> <template #trigger>
<Icon class="md-editor-icon" type="md-cloud-upload" size="22" /> <Icon class="md-editor-icon" type="md-cloud-upload" size="22" />
</template> </template>
</NormalToolbar> </NormalToolbar>
<NormalToolbar title="切换编辑器主题" @onClick="toggleEditorTheme"> <ThemeSwitch v-model="editorState.theme" />
<template #trigger> <PreviewThemeSwitch v-model="editorState.previewTheme" />
<Icon v-if="editorState.theme == 'dark'" type="ios-sunny-outline" size="22" />
<Icon v-else type="md-moon" size="22" />
</template>
</NormalToolbar>
<DropdownToolbar title="切换预览主题" :visible="editorState.previewThemeListVisible"
:onChange="showPreviewThemeList">
<template #overlay>
<div>
<List border size="small">
<ListItem v-for="theme in editorState.previewThemeList" :key="theme"
:class="theme == editorState.previewTheme ? 'md-editor-toolbar-active' : ''"
@click="togglePreviewTheme(theme)">{{ theme }}
</ListItem>
</List>
</div>
</template>
<template #trigger>
<Icon class="md-editor-icon" type="ios-color-filter-outline" :size="22" />
</template>
</DropdownToolbar>
</template> </template>
</MdEditor> </MdEditor>
</template> </template>
@ -61,7 +43,10 @@
<script setup> <script setup>
import 'md-editor-v3/lib/style.css'; import 'md-editor-v3/lib/style.css';
import { MdEditor, MdCatalog, NormalToolbar, DropdownToolbar } from 'md-editor-v3'; import '@vavt/v3-extension/lib/asset/PreviewThemeSwitch.css';
import '@vavt/v3-extension/lib/asset/ExportPDF.css';
import { MdEditor, MdCatalog, NormalToolbar } from 'md-editor-v3';
import { ThemeSwitch, PreviewThemeSwitch, ExportPDF } from '@vavt/v3-extension';
import { ref, reactive, watch } from "vue"; import { ref, reactive, watch } from "vue";
import { Message } from 'view-ui-plus' import { Message } from 'view-ui-plus'
@ -88,6 +73,7 @@ const props = defineProps({
const emit = defineEmits([ const emit = defineEmits([
'update:markdownCode', 'update:markdownCode',
'update:leftSidebarState', 'update:leftSidebarState',
'exportPDF',
]); ]);
// //
@ -111,10 +97,8 @@ watch(split, (newSplit) => {
const editorRef = ref(null); const editorRef = ref(null);
const editorState = reactive({ const editorState = reactive({
id: 'markdown-editor', id: 'markdown-editor',
theme: '', theme: 'light',
previewTheme: 'default', previewTheme: 'default',
previewThemeList: ['default', 'github', 'vuepress', 'mk-cute', 'smart-blue', 'cyanosis'],
previewThemeListVisible: false,
height: '100vh', height: '100vh',
text: props.markdownCode, text: props.markdownCode,
toolbars: [ toolbars: [
@ -146,6 +130,7 @@ const editorState = reactive({
'next', 'next',
'save', 'save',
2, 2,
3,
'=', '=',
'prettier', 'prettier',
'pageFullscreen', 'pageFullscreen',
@ -155,10 +140,11 @@ const editorState = reactive({
'htmlPreview', 'htmlPreview',
'catalog', 'catalog',
'github', 'github',
3,
4, 4,
5,
], ],
toolbarsExclude: ['fullscreen', 'github'], // github toolbarsExclude: ['fullscreen', 'github'], // github
pdfIns: null, // PDF jsPDF
}); });
// markdownCode text // markdownCode text
watch(() => props.markdownCode, (newCode) => { watch(() => props.markdownCode, (newCode) => {
@ -186,19 +172,6 @@ const toggleCatalog = () => {
} }
} }
const toggleEditorTheme = () => {
editorState.theme = editorState.theme == '' ? 'dark' : '';
}
const showPreviewThemeList = (visible) => {
editorState.previewThemeListVisible = visible;
}
const togglePreviewTheme = (theme) => {
editorState.previewTheme = theme;
editorState.previewThemeListVisible = false;
}
const onSave = (v, h) => { const onSave = (v, h) => {
// save v markdown code // save v markdown code
props.save(v); props.save(v);
@ -213,6 +186,26 @@ const getPreviewedHTML = (html) => {
console.log(html); console.log(html);
} }
const customizePDF = (ins) => {
// jsPDF editorState.pdfIns 便
editorState.pdfIns = ins;
}
const handleExportProgress = (progress) => {
// console.log(`Export progress: ${progress.ratio * 100}%`);
// 100%
if (progress.ratio == 1) {
// ExportPDF
if (window.__TAURI_INTERNALS__ === undefined) {
return;
}
// tauri API
if (editorState.pdfIns !== null) {
emit('exportPDF', editorState.pdfIns.output('arraybuffer'));
}
}
};
const publish = () => { const publish = () => {
// TODO markdown HTML // TODO markdown HTML
Message.warning('抱歉,该功能暂未开放'); Message.warning('抱歉,该功能暂未开放');