|
@@ -1,7 +1,7 @@
|
|
|
<script setup lang="ts">
|
|
|
import { ref, onMounted, computed, nextTick } from "vue";
|
|
|
-import _ from 'lodash'
|
|
|
-import {v4 as uuid4} from 'uuid'
|
|
|
+import _ from "lodash";
|
|
|
+import { v4 as uuid4 } from "uuid";
|
|
|
import {
|
|
|
S3Client,
|
|
|
PutObjectCommand,
|
|
@@ -10,10 +10,7 @@ import {
|
|
|
DeleteObjectsCommand,
|
|
|
CopyObjectCommand,
|
|
|
} from "@aws-sdk/client-s3";
|
|
|
-import {
|
|
|
- Plus,
|
|
|
- Refresh,
|
|
|
-} from "@element-plus/icons-vue";
|
|
|
+import { Plus, Refresh } from "@element-plus/icons-vue";
|
|
|
import Node from "./Node.vue";
|
|
|
import AppendModal from "./AppendModal.vue";
|
|
|
import type { TreeData } from "./Tree";
|
|
@@ -21,12 +18,12 @@ import { ElNotification, type ElTree } from "element-plus";
|
|
|
import { MdEditor } from "md-editor-v3";
|
|
|
import { s3ContentsToTree, getTreeFlatten } from "@/utils/s3Helper";
|
|
|
import "md-editor-v3/lib/style.css";
|
|
|
-import type ETNode from 'element-plus/es/components/tree/src/model/node'
|
|
|
-import type { DragEvents } from 'element-plus/es/components/tree/src/model/useDragNode'
|
|
|
+import type ETNode from "element-plus/es/components/tree/src/model/node";
|
|
|
+import type { DragEvents } from "element-plus/es/components/tree/src/model/useDragNode";
|
|
|
import type {
|
|
|
AllowDropType,
|
|
|
NodeDropType,
|
|
|
-} from 'element-plus/es/components/tree/src/tree.type'
|
|
|
+} from "element-plus/es/components/tree/src/tree.type";
|
|
|
import path from "path-browserify";
|
|
|
|
|
|
const s3 = new S3Client({
|
|
@@ -37,42 +34,77 @@ const s3 = new S3Client({
|
|
|
region: import.meta.env.VITE_AWS_S3_REGION,
|
|
|
});
|
|
|
|
|
|
+const SIDEBAR_SORTED_MAP_KEY = "SIDEBAR_SORTED_MAP.json";
|
|
|
+
|
|
|
const tree$ = ref<InstanceType<typeof ElTree>>();
|
|
|
|
|
|
-const langSelect = ref<'zh-CN'|'zh-HK'|'en-US'>('zh-CN')
|
|
|
+const langSelect = ref<"zh-CN" | "zh-HK" | "en-US">("zh-CN");
|
|
|
const langKeyPrefix = computed(() => {
|
|
|
return {
|
|
|
- 'zh-CN': 'docs/',
|
|
|
- 'zh-HK': 'zh-HK/docs/',
|
|
|
- 'en-US': 'en-US/docs/',
|
|
|
- }[langSelect.value]
|
|
|
-})
|
|
|
-const langDataSource = ref<Partial<Record<'zh-CN'|'zh-HK'|'en-US', TreeData[]>>>({})
|
|
|
+ "zh-CN": "docs/",
|
|
|
+ "zh-HK": "zh-HK/docs/",
|
|
|
+ "en-US": "en-US/docs/",
|
|
|
+ }[langSelect.value];
|
|
|
+});
|
|
|
+const langDataSource = ref<
|
|
|
+ Partial<Record<"zh-CN" | "zh-HK" | "en-US", TreeData[]>>
|
|
|
+>({});
|
|
|
const langSelectedDataSource = computed(() => {
|
|
|
- return langDataSource.value[langSelect.value]
|
|
|
-})
|
|
|
+ return langDataSource.value[langSelect.value];
|
|
|
+});
|
|
|
const dataSource = ref<unknown[]>([]);
|
|
|
const sideLoading = ref(false);
|
|
|
const loadS3DocsListObjects = async () => {
|
|
|
- const command = new ListObjectsCommand({ Bucket: import.meta.env.VITE_DOCS_LIST_BUCKET });
|
|
|
+ const command = new ListObjectsCommand({
|
|
|
+ Bucket: import.meta.env.VITE_DOCS_LIST_BUCKET,
|
|
|
+ });
|
|
|
const result = await s3.send(command);
|
|
|
return result.Contents!;
|
|
|
};
|
|
|
+const loadS3SideBarSort = async (lang) => {
|
|
|
+ try {
|
|
|
+ const command = new GetObjectCommand({
|
|
|
+ Bucket: import.meta.env.VITE_DOCS_LIST_BUCKET,
|
|
|
+ Key: `${lang}::${SIDEBAR_SORTED_MAP_KEY}`,
|
|
|
+ ResponseCacheControl: "no-cache",
|
|
|
+ });
|
|
|
+ const file = await s3.send(command);
|
|
|
+ return JSON.parse(await file.Body?.transformToString()!);
|
|
|
+ } catch (e) {
|
|
|
+ console.warn(e);
|
|
|
+ }
|
|
|
+ return {};
|
|
|
+};
|
|
|
const loadSideBar = async () => {
|
|
|
sideLoading.value = true;
|
|
|
dataSource.value = await loadS3DocsListObjects();
|
|
|
- [ 'zh-CN','zh-HK','en-US' ].forEach((lang) => {
|
|
|
- const prefix = {
|
|
|
- 'zh-CN': 'docs/',
|
|
|
- 'zh-HK': 'zh-HK/docs/',
|
|
|
- 'en-US': 'en-US/docs/',
|
|
|
- }[lang]
|
|
|
- const filtered = dataSource.value.filter(cont => cont.Key.startsWith(prefix)).map(cont => ({...cont, Key: cont.Key.replace(new RegExp(`^${prefix}`), '')}))
|
|
|
- langDataSource.value[lang] = s3ContentsToTree(filtered, {}, (r, label, i, a, thisContent) => ({
|
|
|
- isDir: a.length !== i + 1,
|
|
|
- ...( a.length === i + 1 ? thisContent : {} )
|
|
|
- }));
|
|
|
- })
|
|
|
+ const allLangs: ("zh-CN" | "zh-HK" | "en-US")[] = ["zh-CN", "zh-HK", "en-US"];
|
|
|
+ await Promise.all(
|
|
|
+ allLangs.map(async (lang: "zh-CN" | "zh-HK" | "en-US") => {
|
|
|
+ const prefix = {
|
|
|
+ "zh-CN": "docs/",
|
|
|
+ "zh-HK": "zh-HK/docs/",
|
|
|
+ "en-US": "en-US/docs/",
|
|
|
+ }[lang];
|
|
|
+ const sideBarSortMap = await loadS3SideBarSort(lang);
|
|
|
+ const filtered = dataSource.value
|
|
|
+ .filter((cont) => cont.Key.startsWith(prefix))
|
|
|
+ .map((cont) => ({
|
|
|
+ ...cont,
|
|
|
+ Key: cont.Key.replace(new RegExp(`^${prefix}`), ""),
|
|
|
+ }))
|
|
|
+ .sort((a, b) => sideBarSortMap[a.Key] - sideBarSortMap[b.Key]);
|
|
|
+ // TODO sort
|
|
|
+ langDataSource.value[lang] = s3ContentsToTree(
|
|
|
+ filtered,
|
|
|
+ {},
|
|
|
+ (r, label, i, a, thisContent) => ({
|
|
|
+ isDir: a.length !== i + 1,
|
|
|
+ ...(a.length === i + 1 ? thisContent : {}),
|
|
|
+ })
|
|
|
+ );
|
|
|
+ })
|
|
|
+ );
|
|
|
|
|
|
sideLoading.value = false;
|
|
|
};
|
|
@@ -102,32 +134,43 @@ const onNodeClick = async (data: TreeData, node) => {
|
|
|
const text = ref("");
|
|
|
const textLoading = ref(false);
|
|
|
|
|
|
-const onUploadImg = async (files: File[], callback: (urls: string[] | { url: string; alt: string; title: string }[]) => void) => {
|
|
|
- let result = []
|
|
|
+const onUploadImg = async (
|
|
|
+ files: File[],
|
|
|
+ callback: (
|
|
|
+ urls: string[] | { url: string; alt: string; title: string }[]
|
|
|
+ ) => void
|
|
|
+) => {
|
|
|
+ let result = [];
|
|
|
for (const file of files) {
|
|
|
try {
|
|
|
- const key = `${uuid4()}::${file.name}`
|
|
|
+ const key = `${uuid4()}::${file.name}`;
|
|
|
const command = new PutObjectCommand({
|
|
|
Bucket: import.meta.env.DOCS_MEDIA_BUCKET,
|
|
|
Key: key,
|
|
|
Body: file,
|
|
|
- ACL: 'public-read',
|
|
|
+ ACL: "public-read",
|
|
|
});
|
|
|
const res = await s3.send(command);
|
|
|
- result.push({url: `https://${import.meta.env.DOCS_MEDIA_BUCKET}.s3.amazonaws.com/${key}`, alt: file.name, title: file.name})
|
|
|
+ result.push({
|
|
|
+ url: `https://${
|
|
|
+ import.meta.env.DOCS_MEDIA_BUCKET
|
|
|
+ }.s3.amazonaws.com/${key}`,
|
|
|
+ alt: file.name,
|
|
|
+ title: file.name,
|
|
|
+ });
|
|
|
} catch (e) {
|
|
|
- console.error(e)
|
|
|
- ElNotification.error(`${file.name} 上传失败`)
|
|
|
+ console.error(e);
|
|
|
+ ElNotification.error(`${file.name} 上传失败`);
|
|
|
}
|
|
|
}
|
|
|
- callback(result)
|
|
|
+ callback(result);
|
|
|
};
|
|
|
const onSave = async () => {
|
|
|
if (!currentOpenData.value) {
|
|
|
- ElNotification.info('请先选择文件')
|
|
|
+ ElNotification.info("请先选择文件");
|
|
|
return;
|
|
|
}
|
|
|
- textLoading.value = true
|
|
|
+ textLoading.value = true;
|
|
|
try {
|
|
|
const command = new PutObjectCommand({
|
|
|
Bucket: import.meta.env.VITE_DOCS_LIST_BUCKET,
|
|
@@ -135,36 +178,37 @@ const onSave = async () => {
|
|
|
Body: text.value,
|
|
|
});
|
|
|
await s3.send(command);
|
|
|
- ElNotification.success(`${currentOpenData.value?.key} 保存成功`)
|
|
|
+ ElNotification.success(`${currentOpenData.value?.key} 保存成功`);
|
|
|
} catch (e) {
|
|
|
- console.error(e)
|
|
|
- ElNotification.error(`${currentOpenData.value?.key} 保存失败`)
|
|
|
+ console.error(e);
|
|
|
+ ElNotification.error(`${currentOpenData.value?.key} 保存失败`);
|
|
|
} finally {
|
|
|
- textLoading.value = false
|
|
|
+ textLoading.value = false;
|
|
|
}
|
|
|
};
|
|
|
|
|
|
let resolveAppend: Function | undefined;
|
|
|
let rejectAppend: Function | undefined;
|
|
|
const showAppendModal = ref(false);
|
|
|
-const appendLoading = ref(false)
|
|
|
-const appendContextData = ref()
|
|
|
+const appendLoading = ref(false);
|
|
|
+const appendContextData = ref();
|
|
|
const onAppend = async (data: TreeData) => {
|
|
|
const modalConfirmPromise = new Promise((resolve, reject) => {
|
|
|
resolveAppend = resolve;
|
|
|
rejectAppend = reject;
|
|
|
});
|
|
|
- appendContextData.value = data
|
|
|
+ appendContextData.value = data;
|
|
|
showAppendModal.value = true;
|
|
|
- let filename, isDir
|
|
|
+ let filename, isDir;
|
|
|
try {
|
|
|
- ( { filename, isDir } = await modalConfirmPromise);
|
|
|
+ ({ filename, isDir } = await modalConfirmPromise);
|
|
|
} catch (e) {
|
|
|
- return
|
|
|
+ return;
|
|
|
}
|
|
|
try {
|
|
|
- appendLoading.value = true
|
|
|
- const key = `${langKeyPrefix.value}${data.key ? `${data.key}/` : ''}${filename}`
|
|
|
+ appendLoading.value = true;
|
|
|
+ // const key = `${langKeyPrefix.value}${data.key ? `${data.key}/` : ''}${filename}`
|
|
|
+ const key = path.join(langKeyPrefix.value, data.key ?? "", filename);
|
|
|
if (!isDir) {
|
|
|
const command = new PutObjectCommand({
|
|
|
Bucket: import.meta.env.VITE_DOCS_LIST_BUCKET,
|
|
@@ -174,7 +218,7 @@ const onAppend = async (data: TreeData) => {
|
|
|
await s3.send(command);
|
|
|
}
|
|
|
const newChild: TreeData = {
|
|
|
- key: `${data.key ? `${data.key}/` : ''}${filename}`,
|
|
|
+ key: `${data.key ? `${data.key}/` : ""}${filename}`,
|
|
|
label: filename,
|
|
|
children: [],
|
|
|
isDir,
|
|
@@ -183,84 +227,103 @@ const onAppend = async (data: TreeData) => {
|
|
|
data.children = [];
|
|
|
}
|
|
|
data.children.push(newChild);
|
|
|
- // if (!data.key) {
|
|
|
- // loadSideBar()
|
|
|
- // }
|
|
|
} catch (e) {
|
|
|
console.error(e);
|
|
|
- ElNotification.error('添加失败')
|
|
|
+ ElNotification.error("添加失败");
|
|
|
} finally {
|
|
|
- appendLoading.value = false
|
|
|
+ appendLoading.value = false;
|
|
|
showAppendModal.value = false;
|
|
|
}
|
|
|
+ if (!isDir) {
|
|
|
+ saveSideBarSort();
|
|
|
+ }
|
|
|
};
|
|
|
const onRemove = async (node: ETNode, data: TreeData) => {
|
|
|
const parent = node.parent;
|
|
|
const children = parent.data.children || parent.data;
|
|
|
const index = children.findIndex((d) => d.key === data.key);
|
|
|
|
|
|
- sideLoading.value = true
|
|
|
+ sideLoading.value = true;
|
|
|
try {
|
|
|
- const leafDatas = getTreeFlatten(data)
|
|
|
+ const leafDatas = getTreeFlatten(data);
|
|
|
const command = new DeleteObjectsCommand({
|
|
|
Bucket: import.meta.env.VITE_DOCS_LIST_BUCKET,
|
|
|
Delete: {
|
|
|
- Objects: leafDatas.map(d => ({Key: `${langKeyPrefix.value}${d.key}`})),
|
|
|
+ Objects: leafDatas.map((d) => ({
|
|
|
+ Key: `${langKeyPrefix.value}${d.key}`,
|
|
|
+ })),
|
|
|
Quiet: false,
|
|
|
- }
|
|
|
- })
|
|
|
- await s3.send(command)
|
|
|
+ },
|
|
|
+ });
|
|
|
+ await s3.send(command);
|
|
|
children.splice(index, 1);
|
|
|
- // if (parent.level === 0) {
|
|
|
- // nextTick(loadSideBar)
|
|
|
- // }
|
|
|
} catch (e) {
|
|
|
- console.error(e)
|
|
|
- ElNotification.error('删除失败')
|
|
|
+ console.error(e);
|
|
|
+ ElNotification.error("删除失败");
|
|
|
} finally {
|
|
|
- sideLoading.value = false
|
|
|
+ sideLoading.value = false;
|
|
|
}
|
|
|
};
|
|
|
-const saveSideBarSort = () => {
|
|
|
- // TODO
|
|
|
- const sortedNodes = getTreeFlatten( { children: tree$.value!.data } as TreeData, 'preorder', 'all' )
|
|
|
- const sortedKeyMap = sortedNodes.reduce((a, v, i)=> ( {...a, [v.key ?? '']: i} ), {})
|
|
|
-}
|
|
|
+const saveSideBarSort = async () => {
|
|
|
+ sideLoading.value = true;
|
|
|
+ const sortedNodes = getTreeFlatten(
|
|
|
+ { children: tree$.value!.data } as TreeData,
|
|
|
+ "preorder",
|
|
|
+ "all"
|
|
|
+ );
|
|
|
+ const sortedKeyMap = sortedNodes.reduce(
|
|
|
+ (a, v, i) => ({ ...a, [path.join(...v.__path__!)]: i }),
|
|
|
+ {}
|
|
|
+ );
|
|
|
+ const command = new PutObjectCommand({
|
|
|
+ Bucket: import.meta.env.VITE_DOCS_LIST_BUCKET,
|
|
|
+ Key: `${langSelect.value}::${SIDEBAR_SORTED_MAP_KEY}`,
|
|
|
+ Body: JSON.stringify(sortedKeyMap),
|
|
|
+ });
|
|
|
+ await s3.send(command);
|
|
|
+ sideLoading.value = false;
|
|
|
+};
|
|
|
const onDragEnd = async (
|
|
|
draggingNode: ETNode,
|
|
|
dropNode: ETNode,
|
|
|
dropType: NodeDropType,
|
|
|
ev: DragEvents
|
|
|
) => {
|
|
|
- if (dropType === 'none') {
|
|
|
- return
|
|
|
+ if (dropType === "none") {
|
|
|
+ return;
|
|
|
}
|
|
|
- sideLoading.value = true
|
|
|
- const copyTargetPath = path.join(langKeyPrefix.value, dropType === 'inner' ? dropNode.data.key : dropNode.parent.data.key ?? '')
|
|
|
+ sideLoading.value = true;
|
|
|
|
|
|
- const copySourceNodes = getTreeFlatten(draggingNode.data)
|
|
|
- const waitForDelete = []
|
|
|
+ const copySourceNodes = getTreeFlatten(draggingNode.data);
|
|
|
+ const waitForDelete = [];
|
|
|
for (const sourceNode of copySourceNodes) {
|
|
|
if (sourceNode.isDir) {
|
|
|
- continue
|
|
|
+ continue;
|
|
|
}
|
|
|
|
|
|
- const copySource = path.join(langKeyPrefix.value, sourceNode.key)
|
|
|
- const copyTarget = path.join( copyTargetPath, path.basename(draggingNode.data.key), path.relative(draggingNode.data.key, sourceNode.key) )
|
|
|
+ const copySource = path.join(langKeyPrefix.value, sourceNode.key);
|
|
|
+ const copyTarget = path.join(
|
|
|
+ langKeyPrefix.value,
|
|
|
+ dropType === "inner" ? dropNode.data.key : dropNode.parent.data.key ?? "",
|
|
|
+ path.basename(draggingNode.data.key),
|
|
|
+ path.relative(draggingNode.data.key, sourceNode.key)
|
|
|
+ );
|
|
|
if (copySource === copyTarget) {
|
|
|
- continue
|
|
|
+ continue;
|
|
|
}
|
|
|
try {
|
|
|
const command = new CopyObjectCommand({
|
|
|
Bucket: import.meta.env.VITE_DOCS_LIST_BUCKET,
|
|
|
- CopySource: encodeURIComponent(`/${import.meta.env.VITE_DOCS_LIST_BUCKET}/${copySource}`),
|
|
|
- Key: copyTarget
|
|
|
- })
|
|
|
- await s3.send(command)
|
|
|
- waitForDelete.push({ Key: copySource })
|
|
|
+ CopySource: encodeURIComponent(
|
|
|
+ `/${import.meta.env.VITE_DOCS_LIST_BUCKET}/${copySource}`
|
|
|
+ ),
|
|
|
+ Key: copyTarget,
|
|
|
+ });
|
|
|
+ await s3.send(command);
|
|
|
+ waitForDelete.push({ Key: copySource });
|
|
|
} catch (e) {
|
|
|
- console.error(e)
|
|
|
- console.warn(`${ copySource } 复制失败`)
|
|
|
+ console.error(e);
|
|
|
+ console.warn(`${copySource} 复制失败`);
|
|
|
}
|
|
|
}
|
|
|
if (waitForDelete.length) {
|
|
@@ -269,20 +332,20 @@ const onDragEnd = async (
|
|
|
Delete: {
|
|
|
Objects: waitForDelete,
|
|
|
Quiet: false,
|
|
|
- }
|
|
|
- })
|
|
|
- await s3.send(command)
|
|
|
+ },
|
|
|
+ });
|
|
|
+ await s3.send(command);
|
|
|
}
|
|
|
|
|
|
- await loadSideBar()
|
|
|
- sideLoading.value = false
|
|
|
- nextTick(() => {
|
|
|
- saveSideBarSort()
|
|
|
- })
|
|
|
-}
|
|
|
+ sideLoading.value = false;
|
|
|
+ nextTick(async () => {
|
|
|
+ await saveSideBarSort();
|
|
|
+ await loadSideBar();
|
|
|
+ });
|
|
|
+};
|
|
|
const allowDrop = (draggingNode: Node, dropNode: Node, type: AllowDropType) => {
|
|
|
- return !( !dropNode.data.isDir && type === 'inner' )
|
|
|
-}
|
|
|
+ return !(!dropNode.data.isDir && type === "inner");
|
|
|
+};
|
|
|
</script>
|
|
|
<template>
|
|
|
<div class="md-container">
|
|
@@ -295,13 +358,20 @@ const allowDrop = (draggingNode: Node, dropNode: Node, type: AllowDropType) => {
|
|
|
</el-radio-group>
|
|
|
<el-button-group>
|
|
|
<el-button
|
|
|
- @click="onAppend({ key: '', label: '', children: langSelectedDataSource })"
|
|
|
+ @click="
|
|
|
+ onAppend({ key: '', label: '', children: langSelectedDataSource })
|
|
|
+ "
|
|
|
type="primary"
|
|
|
text
|
|
|
:icon="Plus"
|
|
|
>
|
|
|
</el-button>
|
|
|
- <el-button @click="loadSideBar" type="info" text :icon="Refresh"></el-button>
|
|
|
+ <el-button
|
|
|
+ @click="loadSideBar"
|
|
|
+ type="info"
|
|
|
+ text
|
|
|
+ :icon="Refresh"
|
|
|
+ ></el-button>
|
|
|
</el-button-group>
|
|
|
</div>
|
|
|
<el-divider />
|