Compare commits

..

No commits in common. "main" and "release/v1.1.0" have entirely different histories.

15 changed files with 390 additions and 2607 deletions

View File

@ -56,12 +56,6 @@
2. 监听 Split 的数值变化,调整 MarkdownEditor 的宽度
3. 监听窗口 resize 事件,调整 MarkdownEditor 的高度和宽度
### 改用 [md-editor-v3](https://imzbf.github.io/md-editor-v3/zh-CN) 编辑器组件
1. `npm install md-editor-v3 --save`
2. 在 src/components/MainEditor.vue 组件里使用 md-editor-v3 组件
3. 实现保存功能;自定义工具栏实现显示/隐藏目录以及发布等功能
### 调试应用
- 开发调试:`npm run tauri dev`

1383
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -14,11 +14,6 @@
"@tauri-apps/plugin-dialog": "^2.2.1",
"@tauri-apps/plugin-fs": "^2.2.1",
"@tauri-apps/plugin-opener": "^2",
"@vavt/v3-extension": "^3.0.0",
"js-md5": "^0.8.3",
"markdown-it-link-attributes": "^4.0.1",
"md-editor-v3": "^5.5.0",
"plantuml-encoder": "^1.4.0",
"scriptjs": "^2.5.9",
"view-ui-plus": "^1.3.19",
"vue": "^3.5.13"

View File

@ -10,9 +10,6 @@
"opener:default",
"fs:default",
"dialog:default",
{
"identifier": "core:window:allow-destroy"
},
{
"identifier": "fs:scope",
"allow": [

View File

@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "markdown-editor",
"version": "2.2.0",
"version": "1.1.0",
"identifier": "com.markdown-editor.app",
"build": {
"beforeDevCommand": "npm run dev",

View File

@ -1,35 +1,35 @@
<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>
<div>
<Split v-model="split">
<template #left>
<div class="split-left" :class="hiddenSplitLeft">
<LeftSidebar v-model:rootFolderPath="rootFolderPath" v-model:currentFilePath="currentFilePath">
</LeftSidebar>
</div>
</template>
<template #trigger>
<div class="split-trigger"></div>
</template>
<template #right>
<div ref="splitRight" class="split-right">
<MarkdownEditor ref="markdownRef" width="100%" height="100%" :autoInit="false"
:markdownCode="markdownCode" :imageUpload="true" :imageUploadURL="imageUploadURLOfEditor"
:imageUploadURLChange="changeImageUploadURL" :onload="reloadEditorHeight"
:onFullScreenExit="handleWindowResize" @update:markdownCode="newCode => markdownCode = newCode"
:appendToolbar="appendToolbar" />
</div>
</template>
</Split>
</div>
</template>
<script setup>
import MarkdownEditor from './components/MarkdownEditor.vue'
import LeftSidebar from './components/LeftSidebar.vue'
import MainEditor from './components/MainEditor.vue'
import { ref, watch, useTemplateRef, nextTick, onMounted, onUnmounted } from "vue";
import { ref, watch, useTemplateRef, 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';
import { readTextFile, writeTextFile, exists } from '@tauri-apps/plugin-fs';
import { Message } from 'view-ui-plus'
//
const greetMsg = ref("");
@ -40,37 +40,24 @@ async function greet() {
}
// 使 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 split = ref(0.2);
const splitRight = useTemplateRef('splitRight');
const splitRightWidth = ref(0);
const hiddenSplitLeft = ref('');
const leftSidebarState = split.value > 0 ? ref('open') : ref('close');
const hiddenSplitLeft = ref("");
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';
split.value = 0.01
if (hiddenSplitLeft.value == "") {
hiddenSplitLeft.value = "hidden"
}
} else {
leftSidebarState.value = 'open';
if (hiddenSplitLeft.value == 'hidden') {
hiddenSplitLeft.value = '';
if (hiddenSplitLeft.value == "hidden") {
hiddenSplitLeft.value = ""
}
}
})
watch(leftSidebarState, (state) => {
if (state == 'open') {
split.value = defaultSplit;
} else {
split.value = 0;
}
// MarkdownEditor reset CodeMirror
reloadEditorWidth()
})
//
@ -81,178 +68,113 @@ watch(rootFolderPath, (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;
// Markdown
const markdownRef = ref(null);
const markdownCode = ref("# Hello Markdown");
const appendToolbar = [
{
name: "|",
},
{
name: "save-markdown-code",
icon: "fa-save", // font-awesome icon. editormd.css
title: "保存当前内容",
shortcut: [
navigator.userAgent.toUpperCase().indexOf('MAC') >= 0 ? "Cmd-S" : "Ctrl-S"
],
handler: () => {
writeFileContent();
},
nofocus: true,
},
{
name: "publish",
icon: "fa-cloud-upload",
title: "发布文章",
handler: () => {
// TODO markdown HTML
Message.warning('抱歉,该功能暂未开放');
},
nofocus: true,
},
];
// localStorage imageUploadURL
const imageUploadURLOfEditor = ref(localStorage.getItem('imageUploadURLOfEditor') || "");
function changeImageUploadURL(newImageUploadURL) {
console.log("new imageUploadURL: ", newImageUploadURL);
localStorage.setItem('imageUploadURLOfEditor', newImageUploadURL);
}
async function initMarkdownEditor() {
//
if (currentFilePath.value.length > 0) {
try {
markdownCode.value = await readTextFile(currentFilePath.value);
Message.success('已读取文件内容,并加载到编辑器中:' + currentFilePath.value);
} catch (err) {
Message.error('文件读取失败:' + err);
}
//
Modal.confirm({
title: '当前文件的内容变动未保存',
content: `<p><b>${currentFilePath.value}</b> 内容被修改但仍未保存,确认切换文件?<br/><br/>注:确认切换之后,内容变动会丢失</p>`,
okText: "仍然切换",
cancelText: "取消切换",
onOk: () => {
resolve(true);
},
onCancel: () => {
resolve(false);
}
});
});
}
markdownRef.value.initEditor()
}
const currentFileSaved = () => {
// true
const lastSaveTimeValue = lastSaveTime.get(currentFilePath.value);
const isUpdated = lastSaveTimeValue && lastUpdateTime.value > lastSaveTimeValue;
return !isUpdated;
// markdown
function reloadEditorHeight() {
markdownRef.value.resetHeight(window.innerHeight);
}
const getFileNameFromFilePath = (filePath) => {
const fullFileName = filePath.split('/').pop();
const splitResult = fullFileName.split(".");
splitResult.pop(); //
return splitResult.join('.');
// markdown
function reloadEditorWidth() {
// 使 splitRight.value.offsetWidth 使 100%
const width = splitRight.value.offsetWidth;
markdownRef.value.resetWidth("100%");
}
// markdown
const handleWindowResize = () => {
reloadEditorHeight();
reloadEditorWidth();
}
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;
markdownRef.value.setMarkdownCode(await readTextFile(filePath));
currentFilePath.value = filePath;
lastSaveTime.set(filePath, new Date().getTime());
Message.success('已读取文件内容,并加载到编辑器中:' + filePath);
} catch (err) {
Message.error('文件读取失败:' + err);
}
}
async function writeFileContent(markdownCode) {
async function writeFileContent() {
//
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);
const exist = await exists(currentFilePath.value);
if (!exist) {
throw new Error(currentFilePath.value + " 文件不存在.");
}
lastSaveTime.set(currentFilePath.value, new Date().getTime());
await writeTextFile(currentFilePath.value, markdownRef.value.getMarkdownCode())
Message.success('文件更新成功:' + currentFilePath.value);
} 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();
}
});
});
onMounted(() => {
window.addEventListener('resize', handleWindowResize);
initMarkdownEditor();
})
onUnmounted(() => {
window.removeEventListener('resize', resizeHandler);
});
window.removeEventListener('resize', handleWindowResize);
})
</script>
<style>
<style scoped>
body {
overflow: hidden;
/* 禁用所有方向滚动 */
@ -267,9 +189,9 @@ body {
}
.split-trigger {
width: 2px;
width: 5px;
height: 100vh;
background-color: #e6e6e6;
background-color: gray;
}
.split-trigger:hover {
@ -277,10 +199,11 @@ body {
}
.split-right {
margin-left: 2px;
margin-left: 5px;
}
.hidden {
display: none;
}
</style>
<style></style>

View File

@ -60,14 +60,6 @@ export default {
required: false,
default: [],
},
// Promise resolve(true) resolve(false)
confirmSwitchingNode: {
type: Function,
required: false,
default: () => new Promise((resolve) => {
resolve(true);
}),
},
},
data() {
return {
@ -228,22 +220,15 @@ export default {
return
}
this.confirmSwitchingNode().then((result) => {
//
if (result === false) {
return;
}
//
let selectedNodes = this.$refs.tree.getSelectedNodes();
selectedNodes.forEach(element => {
element.selected = false;
});
nodeRenderData.selected = true;
//
this.$emit('file-selected', nodeRenderData);
//
let selectedNodes = this.$refs.tree.getSelectedNodes()
selectedNodes.forEach(element => {
element.selected = false
});
nodeRenderData.selected = true
//
this.$emit('file-selected', nodeRenderData);
},
//
insertTreeNode(parentRenderData, nodeData, index) {

View File

@ -1,23 +1,14 @@
<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]"
:confirmSwitchingNode="confirmSwitchingFile" :specifyFileSuffix="['md']" @file-selected="fileSelected" />
</Space>
<UserLogin ref="loginRef" v-model:visible="showLoginModal" v-model:loginStatus="loginStatus"></UserLogin>
<SelectFolder :rootPath="rootFolderPath" :nodeDataCallback="appendDataForNode" @update:rootPath="changeRootPath"
@folder-selected="showFileTree" />
<FolderTree ref="folderTreeRef" :treeData="folderTreeData" :expandLevel="1" :selectedNode="[currentFilePath]"
:specifyFileSuffix="['md']" @file-selected="fileSelected" />
</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 SelectFolder from './SelectFolder.vue';
import FolderTree from './FolderTree.vue';
import { ref } from 'vue';
import { mkdir, remove, create, exists, rename } from '@tauri-apps/plugin-fs';
import { Message } from 'view-ui-plus'
@ -32,11 +23,6 @@ defineProps({
required: false,
default: ''
},
confirmSwitchingFile: {
type: Function,
required: false,
default: undefined,
}
});
const emit = defineEmits([
@ -44,22 +30,6 @@ const emit = defineEmits([
'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) {

View File

@ -1,475 +0,0 @@
<template>
<Split v-model="split">
<template #left>
<MdEditor ref="editorRef" v-model="editorState.text" :id="editorState.id" :theme="editorState.theme"
:previewTheme="editorState.previewTheme" :style="{ height: editorState.height }"
:toolbars="editorState.toolbars" :toolbarsExclude="editorState.toolbarsExclude" :sanitize="sanitize"
@onSave="onSave" @onHtmlChanged="getPreviewedHTML" @onUploadImg="onUploadImg">
<template #defToolbars>
<NormalToolbar :title="leftSidebarState == 'open' ? '隐藏文件树' : '显示文件树'" @onClick="toggleLeftSideBar">
<template #trigger>
<Icon v-if="leftSidebarState == 'open'" type="ios-arrow-dropleft" size="22" />
<Icon v-else type="ios-arrow-dropright" size="22" />
</template>
</NormalToolbar>
<NormalToolbar :title="split == 1 ? '显示目录' : '隐藏目录'" @onClick="toggleCatalog">
<template #trigger>
<Icon :class="split == 1 ? '' : 'md-editor-toolbar-active'" type="ios-list-box-outline"
size="22" />
</template>
</NormalToolbar>
<ExportPDF :modelValue="editorState.text" :customize="customizePDF"
@onProgress="handleExportProgress" />
<NormalToolbar title="同步到云端" @onClick="uploadCloud">
<template #trigger>
<Icon class="md-editor-icon" type="md-cloud-upload" size="22" />
</template>
</NormalToolbar>
<ThemeSwitch v-model="editorState.theme" />
<PreviewThemeSwitch v-model="editorState.previewTheme" />
</template>
</MdEditor>
</template>
<template #trigger>
<div class="split-trigger"></div>
</template>
<template #right>
<div class="split-right md-catalog">
<MdCatalog :class="hiddenSplitRight" :editorId="editorState.id" :theme="editorState.theme" />
</div>
</template>
</Split>
<UploadImageConfig v-model:visible="uploadImgForm.showFormModal" v-model:uploadImgAPI="uploadImgForm.uploadImgAPI"
v-model:uploadImgField="uploadImgForm.uploadImgField" v-model:otherField="uploadImgForm.otherField">
</UploadImageConfig>
</template>
<script setup>
import 'md-editor-v3/lib/style.css';
import '@vavt/v3-extension/lib/asset/PreviewThemeSwitch.css';
import '@vavt/v3-extension/lib/asset/ExportPDF.css';
import UploadImageConfig from './UI/UploadImageConfig.vue';
import scriptjs from 'scriptjs'
import { config, MdEditor, MdCatalog, NormalToolbar, XSSPlugin } from 'md-editor-v3';
import { ThemeSwitch, PreviewThemeSwitch, ExportPDF } from '@vavt/v3-extension';
import { lineNumbers } from '@codemirror/view';
import LinkAttr from 'markdown-it-link-attributes';
import { ref, reactive, watch, nextTick, onMounted } from "vue";
import { Message, Modal } from 'view-ui-plus';
import { encode as plantumlEncoder } from 'plantuml-encoder';
import { SubmitArticle, UpdateArticle } from '/src/utils/userHandler';
// fetchScript JS
const fetchScript = (url) => new Promise((resolve) => scriptjs(url, () => resolve()));
const props = defineProps({
markdownCode: {
type: String,
required: false,
default: ''
},
leftSidebarState: {
type: String,
required: false,
default: 'open'
},
save: {
type: Function,
required: false,
default: (markdownCode) => {
console.log("save markdown code: " + markdownCode);
}
},
});
const emit = defineEmits([
'update:markdownCode',
'update:leftSidebarState',
'exportPDF',
]);
//
const split = ref(1);
const defaultCatalogSplit = 0.2; // 8:2
const hiddenSplitRight = split.value == 1 ? ref('hidden') : ref('');
watch(split, (newSplit) => {
if (newSplit > 0.95) {
split.value = 1
if (hiddenSplitRight.value == '') {
hiddenSplitRight.value = 'hidden'
}
} else {
if (hiddenSplitRight.value == 'hidden') {
hiddenSplitRight.value = ''
}
}
})
// Markdown
const editorRef = ref(null);
const editorState = reactive({
id: 'markdown-editor',
theme: 'light',
previewTheme: 'default',
height: '100vh',
text: props.markdownCode,
toolbars: [
0,
1,
'-',
'bold',
'underline',
'italic',
'-',
'title',
'strikeThrough',
'sub',
'sup',
'quote',
'unorderedList',
'orderedList',
'task',
'-',
'codeRow',
'code',
'link',
'image',
'table',
'mermaid',
'katex',
'-',
'revoke',
'next',
'save',
2,
3,
'=',
'prettier',
'pageFullscreen',
'fullscreen',
'preview',
'previewOnly',
'htmlPreview',
'catalog',
'github',
4,
5,
],
toolbarsExclude: ['fullscreen', 'github'], // github
pdfIns: null, // PDF jsPDF
});
// markdownCode text
watch(() => props.markdownCode, (newCode) => {
// markdownCode text
editorState.text = newCode;
})
watch(() => editorState.text, (newCode) => {
// text
emit('update:markdownCode', newCode);
})
config({
codeMirrorExtensions(_theme, extensions) {
//
return [...extensions, lineNumbers()];
},
markdownItPlugins(plugins) {
return [
...plugins,
{
type: 'xss',
plugin: XSSPlugin,
options: {},
},
{
type: 'linkAttr',
plugin: LinkAttr,
options: {
matcher(href) {
// 使 markdown-it-anchor
//
return !href.startsWith('#');
},
attrs: {
target: '_blank',
},
},
},
];
},
});
const toggleLeftSideBar = () => {
if (props.leftSidebarState == 'open') {
emit('update:leftSidebarState', 'close');
} else {
emit('update:leftSidebarState', 'open');
}
}
const toggleCatalog = () => {
if (split.value < 1) {
split.value = 1;
} else {
split.value = 1 - defaultCatalogSplit;
}
}
const sanitize = (html) => {
// XSS 使 sanitize-html html
// !!! <h>
// html = sanitizeHtml(html);
// PlantUML
html = html.replace(/@startuml([\s\S]*?)@enduml/g, (_, umlCode) => {
// umlCode @startuml @enduml
const sourceCode = `@startuml${umlCode}@enduml`;
const lines = umlCode.split('\n');
if (lines.length == 0) {
return sourceCode;
}
// @startuml <br> </p>
if (lines[0] !== '<br>' && lines[0] !== '</p>') {
return sourceCode;
}
const lastLine = lines[lines.length - 1];
// @enduml \n
// <p data-line="7">
// @enduml
if (lastLine.length > 0) {
// <p data-line="7">
if (!/<p\s+[^>]*>$/i.test(lastLine)) {
return sourceCode;
}
// @enduml > >@enduml
// <p data-line="7"> <blockquote data-line="7">
if (lines.length >= 2 && /<blockquote\s+[^>]*>$/i.test(lines[lines.length - 2])) {
return sourceCode;
}
}
// @startuml
lines.shift();
// @enduml
lines.pop();
const removeLineBreak = /(.*?)(?=<br>|<\/p>)/i;
const extractedLines = lines.map(line => {
// <p data-line="2">text<br>
line = line.trim().replace(/<\w+\s+[^>]*>(.*?)<[^>]*>/i, '$1');
//
let match = line.match(removeLineBreak);
line = match ? match[1].trim() : line;
// HTML &gt; >
const decodedText = decodeHTMLEntities(line);
return decodedText;
});
extractedLines.unshift('@startuml');
extractedLines.push('@enduml');
const plantUMLCode = extractedLines.join('\n');
console.log(plantUMLCode);
// PlantUML
const encoded = plantumlEncoder(plantUMLCode);
return `<img class="plantuml-diagram"
src="https://www.plantuml.com/plantuml/svg/${encoded}"
alt="PlantUML Diagram">`;
});
return html;
};
const decodeHTMLEntities = (text) => {
const textArea = document.createElement('textarea');
textArea.innerHTML = text;
return textArea.value;
};
const onSave = (v, h) => {
// save v markdown code
props.save(v);
// 使 h html
// h.then((html) => {
// console.log(html);
// });
};
const getPreviewedHTML = (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 uploadCloud = () => {
// markdown
const markdownLines = editorState.text.split('\n');
let firstLine = 0;
for (let index = 0; index < markdownLines.length; index++) {
if (markdownLines[index].trim().length > 0) {
firstLine = index;
break;
}
}
for (let index = 0; index < firstLine; index++) {
markdownLines.shift();
}
const firstLineText = markdownLines[0].trim();
if (!firstLineText.startsWith('# ')) {
Message.warning('第一行请以 “# 文章标题” 作为开头');
return;
}
// ID
let title = firstLineText.slice(2).trim();
let articleID = '';
const match = title.match(/^\[(.*?)\]\((.*?)\)$/);
if (match) {
title = match[1].trim(); // ""
let url = match[2].trim(); // " https://example.com/blog/1"
const matchID = url.match(/(\d+)$/);
articleID = matchID ? matchID[1] : '';
}
// ID ID
if (articleID.length > 0 && !isNaN(Number(articleID, 10))) {
editorState.text = markdownLines.join('\n');
//
UpdateArticle(parseInt(articleID, 10), title, editorState.text)
.then((data) => {
Modal.remove();
Message.success('同步成功');
if (window.__TAURI_INTERNALS__ !== undefined) {
// onSave
onSave(editorState.text);
}
})
.catch((error) => {
Message.error(`同步异常: ${error}`);
});
} else {
// title
Modal.confirm({
title: '发布前确认',
content: `<p>确认使用《${title}》作为文章标题将其同步到云端。<br/>点击确认之后,会同步文章,并修改第一行嵌入文章 ID最后将当前内容保存操作到本地。</p>`,
loading: true,
onOk: () => {
SubmitArticle(title, markdownLines.join('\n'))
.then((data) => {
articleID = data.id;
markdownLines[0] = '# [' + title + '](../' + articleID + ')';
editorState.text = markdownLines.join('\n');
Modal.remove();
Message.success('同步成功');
if (window.__TAURI_INTERNALS__ !== undefined) {
// onSave
onSave(editorState.text);
}
})
.catch((error) => {
Message.error(`同步异常: ${error}`);
});
},
});
}
}
const uploadImgForm = reactive({
//
showFormModal: false,
// API
uploadImgAPI: localStorage.getItem('imageUploadURLOfEditor') || '',
// FormData
uploadImgField: localStorage.getItem('imageUploadFieldOfEditor') || '',
// FormData . example: [{ key: 'token', value: 'uuid' }]
otherField: JSON.parse(localStorage.getItem('imageUploadOtherFieldOfEditor')) || [],
});
watch(() => uploadImgForm.uploadImgAPI, (newAPI) => {
localStorage.setItem('imageUploadURLOfEditor', newAPI);
})
watch(() => uploadImgForm.uploadImgField, (newField) => {
localStorage.setItem('imageUploadFieldOfEditor', newField);
})
watch(() => uploadImgForm.otherField, (newData) => {
localStorage.setItem('imageUploadOtherFieldOfEditor', JSON.stringify(newData));
})
const onUploadImg = async (files, callback) => {
if (uploadImgForm.uploadImgAPI.trim().length == 0 || uploadImgForm.uploadImgField.trim().length == 0) {
Message.warning("请先配置图片上传的后端接口,以及对应的字段名称");
uploadImgForm.showFormModal = true;
return;
}
const res = await Promise.all(
files.map((file) => {
return new Promise((rev, rej) => {
const formData = new FormData();
formData.append(uploadImgForm.uploadImgField.trim(), file);
uploadImgForm.otherField.forEach(field => {
formData.append(field.key.trim(), field.value);
});
fetch(uploadImgForm.uploadImgAPI.trim(), {
method: 'POST',
body: formData,
}).then(response => {
if (!response.ok) throw new Error(`HTTP 错误: ${response.status}`);
return response.json();
}).then(data => {
rev(data);
}).catch(error => {
rej(error);
Message.error(`上传失败: ${error}`);
});
});
})
);
callback(res.map((item) => item.url));
};
onMounted(async () => {
// jQuery 便使 jQuery
await fetchScript('/static/jquery/jquery.min.js');
// 使 nextTick
await nextTick();
//
let imageIcon = $('div.md-editor-toolbar-item[title="图片"]').first();
let dropdownOverlay = imageIcon.next().children().first();
let ulOfDropdownOverlay = dropdownOverlay.children().first();
let updateLi = $('<li class="md-editor-menu-item md-editor-menu-item-image" role="menuitem" tabindex="0">更新配置</li>');
updateLi.on('click', () => uploadImgForm.showFormModal = true);
updateLi.appendTo(ulOfDropdownOverlay);
});
//
defineExpose({})
</script>
<style scoped>
.md-catalog {
height: 100vh;
overflow-x: hidden;
overflow-y: auto;
scrollbar-width: none;
}
</style>

View File

@ -3,7 +3,7 @@
<!-- rewrite Markdown CSS(Cmd Markdown Style) from https://github.com/wakaryry/editorMd -->
<link rel="stylesheet" href="/static/CmdMarkdown/custom.css">
<div :id="editorID"></div>
<div id="markdown-editor"></div>
</template>
<script>
@ -15,11 +15,6 @@ let editorClient = defaultEditorValue;
export default {
name: 'MarkdownEditor',
props: {
editorID: {
type: String,
required: false,
default: 'markdown-editor'
},
// initEditor
// false
autoInit: {
@ -111,7 +106,6 @@ export default {
data() {
return {
loadingCompleted: false,
editormdPreview: null,
}
},
mounted() {
@ -139,7 +133,7 @@ export default {
// JS
this.$nextTick(() => {
const vm = this
const editor = window.editormd(vm.editorID, {
const editor = window.editormd('markdown-editor', {
//
markdown: vm.markdownCode,
//
@ -196,7 +190,6 @@ export default {
// editor editorClient便 API
editorClient = this
vm.loadingCompleted = true
vm.editormdPreview = $('#' + vm.editorID + ' .editormd-preview-container')[0]
// callback
vm.onload()
@ -256,105 +249,8 @@ export default {
}
return editorClient.getPreviewedHTML()
},
getTableOfContents() {
return generateTableOfContents(this.editormdPreview);
},
}
}
/**
* 获取一篇文档的章节标题的目录
*
* 入参示例
* <div>
* <h1>1</h1>
* <h2>1.1</h2>
* <h1>2</h1>
* <h2>2.1</h2>
* </div>
*
* 输出示例
* [{
* "name": "1",
* "nodes": [{
* "name": "1.1",
* "nodes": []
* }]
* },
* {
* "name": "2",
* "nodes": [{
* "name": "2.1",
* "nodes": []
* }]
* }]
*
* @param {Element} element 文档的 document 对象
* @returns {Array} tocObject 目录列表Table of Contents
*/
function generateTableOfContents(element) {
//
let tableOfContents = {
name: "",
parent: null,
nodes: [],
};
// <h>
const hTags = element.querySelectorAll(":scope > h1, :scope > h2, :scope > h3, :scope > h4, :scope > h5, :scope > h6");
let lastLevel = 0;
let lastNode = tableOfContents;
// H
hTags.forEach(hTag => {
// currentLevel H H1 currentLevel 1
let currentLevel = hTag.tagName[1];
// headTitle H
let headTitle = hTag.innerText;
lastNode = generateNodeOfTOC(lastNode, lastLevel, currentLevel, headTitle);
lastLevel = currentLevel;
});
// node parent circular structure
deleteParentAssociation(tableOfContents);
return tableOfContents.nodes;
}
function generateNodeOfTOC(lastNode, lastLevel, currentLevel, headTitle) {
// lastNode
if (currentLevel > lastLevel) {
// H H
let diff = currentLevel - lastLevel;
for (let i = 0; i < diff - 1; i++) {
let node = {
name: "",
nodes: [],
parent: lastNode,
}
lastNode.nodes.push(node);
lastNode = node;
}
} else {
// H H
let diff = lastLevel - currentLevel;
for (let i = 0; i <= diff; i++) {
lastNode = lastNode.parent;
}
}
let currentNode = {
name: headTitle,
nodes: [],
parent: lastNode,
}
lastNode.nodes.push(currentNode);
return currentNode;
}
function deleteParentAssociation(node) {
delete node.parent;
node.nodes.forEach(n => {
deleteParentAssociation(n);
})
}
</script>
<style scoped></style>

View File

@ -35,8 +35,6 @@
import { join } from '@tauri-apps/api/path';
import { open } from '@tauri-apps/plugin-dialog';
import { readDir } from '@tauri-apps/plugin-fs';
import { GetUserArticle } from '/src/utils/userHandler';
import { Message } from 'view-ui-plus';
export default {
props: {
@ -61,39 +59,12 @@ export default {
},
methods: {
async exposeTreeData(folderPath) {
if (window.__TAURI_INTERNALS__ === undefined) {
GetUserArticle().then((data) => {
folderPath = '/Users/' + data.username;
const folderTreeData = {
"path": folderPath,
"name": data.username,
"nodes": [],
"directory": true,
};
data.articles.forEach((article) => {
folderTreeData.nodes.push({
"path": folderPath + '/' + article.article_id,
"name": article.article_id + '_' + article.title,
"suffix": "md",
"directory": false,
});
})
this.$emit('folder-selected', folderTreeData);
}).catch((error) => {
Message.error(`调用异常: ${error}`);
});
return;
}
if (folderPath != null && folderPath.length > 0) {
const folderTreeData = await this.readDirRecursively(folderPath);
this.$emit('folder-selected', folderTreeData);
}
},
async selectFolder() {
if (window.__TAURI_INTERNALS__ === undefined) {
// Tauri
return;
}
const folderPath = await open(
{
filters: [{ name: 'All Files', extensions: ['.'] }],

View File

@ -1,36 +0,0 @@
<template>
<Anchor v-if="nodes !== null" :affix="false">
<AnchorLink v-for="node in nodes" :key="node.name" :href="'#' + node.name" :title="node.name">
<AnchorList :nodes="node.nodes"></AnchorList>
</AnchorLink>
</Anchor>
</template>
<script setup>
defineProps({
/**
* 输出示例
* [{
* "name": "1",
* "nodes": [{
* "name": "1.1",
* "nodes": []
* }]
* },
* {
* "name": "2",
* "nodes": [{
* "name": "2.1",
* "nodes": []
* }]
* }]
*/
nodes: {
type: Array,
required: false,
default: () => {
return null;
}
},
});
</script>

View File

@ -1,145 +0,0 @@
<template>
<Modal v-model="showFormModal" :footer-hide="true">
<template #header>
<Space>
<span>上传图片配置configuration for uploading image</span>
<Poptip placement="bottom" width="400" word-wrap>
<Button type="info" shape="circle" size="small" icon="md-help"></Button>
<template #content>
<Typography>
<blockquote>
假设已经拥有一个图片上传的 HTTP 接口该接口:
<ul>
<li>接收<code>Content-Type: multipart/form-data</code>数据</li>
<li>能够通过<code>response.url</code>得到上传成功的图片 URL</li>
</ul>
</blockquote>
<Title :level="6">API必填</Title>
<Paragraph>
后端接口的 URL示例 https://api.example.com/upload
</Paragraph>
<Title :level="6">Field必填</Title>
<Paragraph>
上传图片时该图片文件在 form-data 中的字段名
</Paragraph>
<Title :level="6">More Fields可选</Title>
<Paragraph>
其他附带字段名比如 token可用于辅助鉴权
</Paragraph>
</Typography>
</template>
</Poptip>
</Space>
</template>
<Form ref="uploadImgFormRef" :model="configForm" :label-width="80" style="width: 300px">
<FormItem label="API" prop="uploadImgAPI" required>
<Tooltip content="上传图片的后端 API 接口" placement="top" max-width="300">
<Input type="text" v-model="configForm.uploadImgAPI"
placeholder="example: https://api.example.com/upload" style="width: 290px;"></Input>
</Tooltip>
</FormItem>
<FormItem label="Field" prop="uploadImgField" required>
<Tooltip content="上传图片时,该图片文件在 form-data 中的字段名" placement="top" max-width="300">
<Input type="text" v-model="configForm.uploadImgField"
placeholder="File Field. example: image or file ..." style="width: 290px;"></Input>
</Tooltip>
</FormItem>
<FormItem v-for="(field, index) in configForm.otherField" :key="index" :label="'Field ' + (index + 1)"
:prop="'otherField.' + index + '.key'"
:rules="{ required: true, message: 'Field Key can not be empty', trigger: 'blur' }">
<Space>
<Tooltip content="其他附带字段名,比如 token可用于辅助鉴权" placement="top" max-width="300">
<Input type="text" v-model="field.key" placeholder="Field Key" style="width: 80px;"></Input>
</Tooltip>
<Input type="text" v-model="field.value" placeholder="Field Value" style="width: 200px;"></Input>
<Button @click="removeField(index)">Delete</Button>
</Space>
</FormItem>
<FormItem>
<Button type="dashed" long @click="addField()" icon="md-add">Add More Field</Button>
</FormItem>
<FormItem>
<Button type="primary" @click="handleSubmit()">Submit</Button>
<Button @click="handleReset()" style="margin-left: 8px">Reset</Button>
</FormItem>
</Form>
</Modal>
</template>
<script setup>
import { ref, reactive, watch, useTemplateRef } from "vue";
const props = defineProps({
visible: {
type: Boolean,
required: true,
default: false,
},
uploadImgAPI: {
type: String,
required: true,
default: '',
},
uploadImgField: {
type: String,
required: true,
default: '',
},
// [{ key: 'token', value: 'uuid' }]
otherField: {
type: Array,
required: true,
default: [],
},
});
const emit = defineEmits([
'update:visible',
'update:uploadImgAPI',
'update:uploadImgField',
'update:otherField',
]);
const uploadImgFormRef = useTemplateRef('uploadImgFormRef');
//
const showFormModal = ref(props.visible);
watch(() => props.visible, (visible) => {
showFormModal.value = visible;
})
watch(showFormModal, (visible) => {
emit('update:visible', visible);
})
const configForm = reactive({
// API
uploadImgAPI: props.uploadImgAPI,
// FormData
uploadImgField: props.uploadImgField,
// FormData
otherField: JSON.parse(JSON.stringify(props.otherField)),
});
const addField = () => {
configForm.otherField.push({ key: '', value: '' });
};
const removeField = (index) => {
configForm.otherField.splice(index, 1)
};
const handleSubmit = () => {
uploadImgFormRef.value.validate((valid) => {
if (valid) {
emit('update:uploadImgAPI', configForm.uploadImgAPI);
emit('update:uploadImgField', configForm.uploadImgField);
emit('update:otherField', JSON.parse(JSON.stringify(configForm.otherField)));
showFormModal.value = false;
}
})
};
const handleReset = () => {
uploadImgFormRef.value.resetFields();
};
</script>

View File

@ -1,157 +0,0 @@
<template>
<Modal v-model="showFormModal" :footer-hide="true" class="login-modal">
<div style="padding-top: 30px;"></div>
<Login v-if="signInOrSignUp" @on-submit="handleSignIn">
<UserName name="username" />
<Password name="password" />
<div class="auto-login">
<Checkbox v-model="autoLogin" size="large">记住登录状态</Checkbox>
<!-- <a>忘记密码</a> -->
</div>
<Submit :loading="submitLoading" />
<Space split>还未拥有账号<Button type="text" @click="signInOrSignUp = false">前往注册</Button></Space>
</Login>
<Login v-else @on-submit="handleSignUp">
<UserName name="username" />
<Email name="mail" />
<Password name="password" />
<Password name="passwordConfirm" placeholder="确认密码" />
<Submit :loading="submitLoading">注册</Submit>
<Space split>已经拥有账号<Button type="text" @click="signInOrSignUp = true">前往登录</Button></Space>
</Login>
</Modal>
</template>
<script setup>
import { SignIn, SignUp, SendSignUpEmail, CheckLoginStatus, Logout } from '/src/utils/userHandler';
import { ref, watch, onMounted } from "vue";
import { Message } from 'view-ui-plus';
const props = defineProps({
visible: {
type: Boolean,
required: true,
default: false,
},
loginStatus: {
type: Boolean,
required: true,
default: false,
},
});
const emit = defineEmits([
'update:visible',
'update:loginStatus',
]);
//
const showFormModal = ref(props.visible);
watch(() => props.visible, (visible) => {
showFormModal.value = visible;
})
watch(showFormModal, (visible) => {
emit('update:visible', visible);
})
const signInOrSignUp = ref(true);
const autoLogin = ref(false);
const submitLoading = ref(false);
const handleSignIn = (valid, { username, password }) => {
if (!valid) {
return;
}
submitLoading.value = true;
SignIn(username, password, autoLogin.value)
.then((data) => {
Message.success('登录成功');
//
emit('update:loginStatus', true);
// loading
submitLoading.value = false;
showFormModal.value = false;
})
.catch((error) => {
submitLoading.value = false;
Message.error(`登录发生异常: ${error}`);
});
};
const handleSignUp = (valid, { username, mail, password, passwordConfirm }) => {
if (!valid) {
return;
}
if (passwordConfirm !== password) {
Message.error('输入的密码不一致,请重新输入');
return;
}
submitLoading.value = true;
SignUp(username, mail, password)
.then((data) => {
// userID
SendSignUpEmail(data.id)
.then((data) => {
Message.success('请前往邮箱激活账号(没收到的话检查垃圾箱),激活后再回来登录账号');
//
submitLoading.value = false;
signInOrSignUp.value = true;
})
.catch((error) => {
submitLoading.value = false;
Message.error(`发送激活邮件发生异常: ${error}`);
});
})
.catch((error) => {
submitLoading.value = false;
Message.error(`注册发生异常: ${error}`);
});
};
const checkLoginStatus = async () => {
CheckLoginStatus()
.then((data) => {
emit('update:loginStatus', true);
})
.catch((error) => {
Message.error(`检查登录状态发生异常: ${error}`);
});
}
const logout = async () => {
Logout()
.then((data) => {
Message.info('登出成功');
emit('update:loginStatus', false);
})
.catch((error) => {
Message.error(`登出发生异常: ${error}`);
});
}
defineExpose({
logout,
})
onMounted(async () => {
//
checkLoginStatus();
});
</script>
<style scoped>
.login-modal {
width: 400px;
margin: 0 auto !important;
}
.auto-login {
margin-bottom: 24px;
text-align: left;
}
.auto-login a {
float: right;
}
</style>

View File

@ -1,268 +0,0 @@
import md5 from 'js-md5';
// 由于是跨域登录,需要手动设置并携带 Cookie
const setPHPSessionToCookie = (sessionID) => {
if (sessionID) {
// 使用 SameSite=None 和 secure 属性,在 tauri 环境会设置失败
// document.cookie = `PHPSESSID=${sessionID}; max-age=604800; SameSite=None; secure; path=/`;
localStorage.setItem('PHPSESSID', sessionID);
}
}
const getPHPSessionFromCookie = () => {
let PHPSESSID = localStorage.getItem('PHPSESSID');
// const cookies = document.cookie.split('; ');
// cookies.forEach(cookie => {
// const [key, value] = cookie.split('=');
// if (key === 'PHPSESSID') {
// PHPSESSID = decodeURIComponent(value); // 解码特殊字符(如空格、%20
// }
// });
return PHPSESSID;
}
const delPHPSessionFromCookie = () => {
// document.cookie = `PHPSESSID=; expires=Thu, 01 Jan 1970 00:00:00 GMT`;
localStorage.removeItem('PHPSESSID');
}
const SignIn = (username, password, autoLogin) => {
return new Promise((resolve, reject) => {
fetch('https://myafei.cn/php/ajax.php', {
method: 'POST',
body: JSON.stringify({
'func': 'login',
'username': username,
'is_email': /^[0-9a-zA-Z_-]{1,100}$/.test(username) ? false : true,
'password': md5(password),
'isRemember': autoLogin,
}),
}).then(response => {
if (!response.ok) throw new Error(`HTTP 错误: ${response.status}`);
return response.json();
}).then(data => {
if (data.status == 0) {
setPHPSessionToCookie(data.PHPSESSID);
resolve(data);
} else {
reject(new Error(data.error));
}
}).catch(error => {
reject(error);
});
})
}
const SignUp = (username, mail, password) => {
return new Promise((resolve, reject) => {
fetch('https://myafei.cn/php/ajax.php', {
method: 'POST',
body: JSON.stringify({
'func': 'register',
'username': username,
'email': mail,
'password': md5(password),
}),
}).then(response => {
if (!response.ok) throw new Error(`HTTP 错误: ${response.status}`);
return response.json();
}).then(data => {
if (data.status == 0) {
resolve(data);
} else {
reject(new Error(data.error));
}
}).catch(error => {
reject(error);
});
});
}
const SendSignUpEmail = (userID) => {
return new Promise((resolve, reject) => {
fetch('https://myafei.cn/php/ajax.php', {
method: 'POST',
body: JSON.stringify({
'func': 'resend_email',
'id': userID,
'path': '/',
}),
}).then(response => {
if (!response.ok) throw new Error(`HTTP 错误: ${response.status}`);
return response.json();
}).then(data => {
if (data.status == 0) {
resolve(data);
} else if (data.status == -100) {
reject(new Error('邮箱已被激活'));
} else {
reject(new Error(data.error));
}
}).catch(error => {
reject(error);
});
});
}
const CheckLoginStatus = () => {
return new Promise((resolve, reject) => {
// 先从 cookie 中拿到 PHPSESSID
const PHPSESSID = getPHPSessionFromCookie();
fetch('https://myafei.cn/php/ajax.php?PHPSESSID=' + PHPSESSID, {
method: 'POST',
body: JSON.stringify({
'func': 'is_login',
}),
}).then(response => {
if (!response.ok) throw new Error(`HTTP 错误: ${response.status}`);
return response.json();
}).then(data => {
if (data.status == 0) {
resolve(data);
} else {
// 未登录不抛出异常
}
}).catch(error => {
reject(error);
});
});
}
const Logout = () => {
return new Promise((resolve, reject) => {
const PHPSESSID = getPHPSessionFromCookie();
fetch('https://myafei.cn/php/ajax.php?PHPSESSID=' + PHPSESSID, {
method: 'POST',
body: JSON.stringify({
'func': 'logout',
}),
}).then(response => {
if (!response.ok) throw new Error(`HTTP 错误: ${response.status}`);
return response.json();
}).then(data => {
if (data.status == 0) {
delPHPSessionFromCookie();
resolve();
} else {
reject(new Error(data.error));
}
}).catch(error => {
reject(error);
});
});
}
const SubmitArticle = (title, mdCode) => {
return new Promise((resolve, reject) => {
const PHPSESSID = getPHPSessionFromCookie();
fetch('https://myafei.cn/php/ajax.php?PHPSESSID=' + PHPSESSID, {
method: 'POST',
body: JSON.stringify({
'func': 'submit_article',
'title': title,
'mdCode': mdCode,
}),
}).then(response => {
if (!response.ok) throw new Error(`HTTP 错误: ${response.status}`);
return response.json();
}).then(data => {
if (data.status == 0) {
resolve(data);
} else {
reject(new Error(data.error));
}
}).catch(error => {
reject(error);
});
});
}
const UpdateArticle = (id, title, mdCode) => {
return new Promise((resolve, reject) => {
const PHPSESSID = getPHPSessionFromCookie();
let body = {
'func': 'update_article',
'id': id,
}
if (title !== null && title !== undefined) {
body.title = title;
}
if (mdCode !== null && mdCode !== undefined) {
body.mdCode = mdCode;
}
fetch('https://myafei.cn/php/ajax.php?PHPSESSID=' + PHPSESSID, {
method: 'POST',
body: JSON.stringify(body),
}).then(response => {
if (!response.ok) throw new Error(`HTTP 错误: ${response.status}`);
return response.json();
}).then(data => {
if (data.status == 0) {
resolve(data);
} else {
reject(new Error(data.error));
}
}).catch(error => {
reject(error);
});
});
}
const GetSingleArticle = (id) => {
return new Promise((resolve, reject) => {
const PHPSESSID = getPHPSessionFromCookie();
fetch('https://myafei.cn/php/ajax.php?PHPSESSID=' + PHPSESSID, {
method: 'POST',
body: JSON.stringify({
'func': 'init_submit',
'id': id,
}),
}).then(response => {
if (!response.ok) throw new Error(`HTTP 错误: ${response.status}`);
return response.json();
}).then(data => {
if (data.status == 0) {
resolve(data);
} else {
reject(new Error(data.error));
}
}).catch(error => {
reject(error);
});
});
}
const GetUserArticle = (id, title, mdCode) => {
return new Promise((resolve, reject) => {
const PHPSESSID = getPHPSessionFromCookie();
fetch('https://myafei.cn/php/ajax.php?PHPSESSID=' + PHPSESSID, {
method: 'POST',
body: JSON.stringify({
'func': 'get_articles',
}),
}).then(response => {
if (!response.ok) throw new Error(`HTTP 错误: ${response.status}`);
return response.json();
}).then(data => {
if (data.status == 0) {
resolve(data);
} else {
// 未登录不抛出异常
}
}).catch(error => {
reject(error);
});
});
}
export {
SignIn,
SignUp,
SendSignUpEmail,
CheckLoginStatus,
Logout,
SubmitArticle,
UpdateArticle,
GetSingleArticle,
GetUserArticle,
};