Explorar o código

feat: dragable sidebar

Carson hai 10 meses
pai
achega
403aa7f520

+ 6 - 0
.vitepress/config.mts

@@ -16,6 +16,9 @@ import path from "node:path";
 import { exec } from "child_process";
 import { buildSideBar } from "../utils/sideBar";
 
+import inject from '@rollup/plugin-inject'
+
+
 const DOC_BASE_PATH = "pages";
 
 if (process.env.NODE_ENV === "production") {
@@ -163,6 +166,9 @@ export default defineConfig({
           ),
         },
       }),
+      inject({
+        process: 'process/browser'
+      })
     ],
     resolve: {
       alias: [

+ 83 - 23
components/Edit/index.vue

@@ -1,5 +1,6 @@
 <script setup lang="ts">
 import { ref, onMounted, computed, nextTick } from "vue";
+import _ from 'lodash'
 import {v4 as uuid4} from 'uuid'
 import {
   S3Client,
@@ -7,6 +8,7 @@ import {
   ListObjectsCommand,
   GetObjectCommand,
   DeleteObjectsCommand,
+  CopyObjectCommand,
 } from "@aws-sdk/client-s3";
 import {
   Plus,
@@ -17,7 +19,7 @@ import AppendModal from "./AppendModal.vue";
 import type { TreeData } from "./Tree";
 import { ElNotification, type ElTree } from "element-plus";
 import { MdEditor } from "md-editor-v3";
-import { s3ContentsToTree, getTreeLeafs } from "@/utils/s3Helper";
+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'
@@ -25,6 +27,7 @@ import type {
   AllowDropType,
   NodeDropType,
 } from 'element-plus/es/components/tree/src/tree.type'
+import path from "path-browserify";
 
 const s3 = new S3Client({
   credentials: {
@@ -36,7 +39,7 @@ const s3 = new S3Client({
 
 const tree$ = ref<InstanceType<typeof ElTree>>();
 
-const langSelect = ref('zh-CN')
+const langSelect = ref<'zh-CN'|'zh-HK'|'en-US'>('zh-CN')
 const langKeyPrefix = computed(() => {
   return {
     'zh-CN': 'docs/',
@@ -44,14 +47,11 @@ const langKeyPrefix = computed(() => {
     'en-US': 'en-US/docs/',
   }[langSelect.value]
 })
-const langDataSource = computed<TreeData[]>(() => {
-  const filtered = dataSource.value.filter(cont => cont.Key.startsWith(langKeyPrefix.value)).map(cont => ({...cont, Key: cont.Key.replace(new RegExp(`^${langKeyPrefix.value}`), '')}))
-  return s3ContentsToTree(filtered, {}, (r, label, i, a, thisContent) => ({
-    isDir: a.length !== i + 1,
-    ...( a.length === i + 1 ? thisContent : {} )
-  }));
+const langDataSource = ref<Partial<Record<'zh-CN'|'zh-HK'|'en-US', TreeData[]>>>({})
+const langSelectedDataSource = computed(() => {
+  return langDataSource.value[langSelect.value]
 })
-const dataSource = ref([]);
+const dataSource = ref<unknown[]>([]);
 const sideLoading = ref(false);
 const loadS3DocsListObjects = async () => {
   const command = new ListObjectsCommand({ Bucket: import.meta.env.VITE_DOCS_LIST_BUCKET });
@@ -61,6 +61,19 @@ const loadS3DocsListObjects = async () => {
 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 : {} )
+    }));
+  })
+
   sideLoading.value = false;
 };
 onMounted(() => {
@@ -161,7 +174,7 @@ const onAppend = async (data: TreeData) => {
       await s3.send(command);
     }
     const newChild: TreeData = {
-      key,
+      key: `${data.key ? `${data.key}/` : ''}${filename}`,
       label: filename,
       children: [],
       isDir,
@@ -170,9 +183,9 @@ const onAppend = async (data: TreeData) => {
       data.children = [];
     }
     data.children.push(newChild);
-    if (!data.key) {
-      loadSideBar()
-    }
+    // if (!data.key) {
+    //   loadSideBar()
+    // }
   } catch (e) {
     console.error(e);
     ElNotification.error('添加失败')
@@ -188,7 +201,7 @@ const onRemove = async (node: ETNode, data: TreeData) => {
 
   sideLoading.value = true
   try {
-    const leafDatas = getTreeLeafs(data)
+    const leafDatas = getTreeFlatten(data)
     const command = new DeleteObjectsCommand({
       Bucket: import.meta.env.VITE_DOCS_LIST_BUCKET,
       Delete: {
@@ -198,9 +211,9 @@ const onRemove = async (node: ETNode, data: TreeData) => {
     })
     await s3.send(command)
     children.splice(index, 1);
-    if (parent.level === 0) {
-      nextTick(loadSideBar)
-    }
+    // if (parent.level === 0) {
+    //   nextTick(loadSideBar)
+    // }
   } catch (e) {
     console.error(e)
     ElNotification.error('删除失败')
@@ -208,17 +221,64 @@ const onRemove = async (node: ETNode, data: TreeData) => {
     sideLoading.value = false
   }
 };
-const onDragEnd = (
+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 onDragEnd = async (
   draggingNode: ETNode,
   dropNode: ETNode,
   dropType: NodeDropType,
   ev: DragEvents
 ) => {
+  if (dropType === 'none') {
+    return
+  }
   sideLoading.value = true
-  console.log('tree drag end:', draggingNode, dropNode, dropType)
-  // TODO copy object
-  // TODO save redis
+  const copyTargetPath = path.join(langKeyPrefix.value, dropType === 'inner' ? dropNode.data.key : dropNode.parent.data.key ?? '')
+
+  const copySourceNodes = getTreeFlatten(draggingNode.data)
+  const waitForDelete = []
+  for (const sourceNode of copySourceNodes) {
+    if (sourceNode.isDir) {
+      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) )
+    if (copySource === copyTarget) {
+      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 })
+    } catch (e) {
+      console.error(e)
+      console.warn(`${ copySource } 复制失败`)
+    }
+  }
+  if (waitForDelete.length) {
+    const command = new DeleteObjectsCommand({
+      Bucket: import.meta.env.VITE_DOCS_LIST_BUCKET,
+      Delete: {
+        Objects: waitForDelete,
+        Quiet: false,
+      }
+    })
+    await s3.send(command)
+  }
+
+  await loadSideBar()
   sideLoading.value = false
+  nextTick(() => {
+    saveSideBarSort()
+  })
 }
 const allowDrop = (draggingNode: Node, dropNode: Node, type: AllowDropType) => {
    return !( !dropNode.data.isDir && type === 'inner' )
@@ -235,7 +295,7 @@ const allowDrop = (draggingNode: Node, dropNode: Node, type: AllowDropType) => {
         </el-radio-group>
         <el-button-group>
           <el-button
-            @click="onAppend({ key: '', label: '', children: langDataSource })"
+            @click="onAppend({ key: '', label: '', children: langSelectedDataSource })"
             type="primary"
             text
             :icon="Plus"
@@ -247,7 +307,7 @@ const allowDrop = (draggingNode: Node, dropNode: Node, type: AllowDropType) => {
       <el-divider />
       <el-tree
         ref="tree$"
-        :data="langDataSource"
+        :data="langSelectedDataSource"
         node-key="key"
         default-expand-all
         :expand-on-click-node="false"

+ 44 - 0
package-lock.json

@@ -7,15 +7,19 @@
       "dependencies": {
         "@aws-sdk/client-s3": "^3.600.0",
         "@iconify/tools": "^4.0.4",
+        "@types/path-browserify": "^1.0.2",
         "@vueuse/core": "^10.11.0",
         "element-plus": "^2.7.5",
         "lodash": "^4.17.21",
         "md-editor-v3": "^4.17.0",
+        "path-browserify": "^1.0.1",
+        "process": "^0.11.10",
         "uuid": "^10.0.0",
         "vue-i18n": "^9.13.1"
       },
       "devDependencies": {
         "@intlify/unplugin-vue-i18n": "^4.0.0",
+        "@rollup/plugin-inject": "^5.0.5",
         "@types/node": "^20.14.2",
         "@vue/compiler-sfc": "^3.4.29",
         "child_process": "^1.0.2",
@@ -2297,6 +2301,28 @@
         "url": "https://opencollective.com/popperjs"
       }
     },
+    "node_modules/@rollup/plugin-inject": {
+      "version": "5.0.5",
+      "resolved": "https://registry.npmmirror.com/@rollup/plugin-inject/-/plugin-inject-5.0.5.tgz",
+      "integrity": "sha512-2+DEJbNBoPROPkgTDNe8/1YXWcqxbN5DTjASVIOx8HS+pITXushyNiBV56RB08zuptzz8gT3YfkqriTBVycepg==",
+      "dev": true,
+      "dependencies": {
+        "@rollup/pluginutils": "^5.0.1",
+        "estree-walker": "^2.0.2",
+        "magic-string": "^0.30.3"
+      },
+      "engines": {
+        "node": ">=14.0.0"
+      },
+      "peerDependencies": {
+        "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
+      },
+      "peerDependenciesMeta": {
+        "rollup": {
+          "optional": true
+        }
+      }
+    },
     "node_modules/@rollup/pluginutils": {
       "version": "5.1.0",
       "resolved": "https://registry.npmmirror.com/@rollup/pluginutils/-/pluginutils-5.1.0.tgz",
@@ -3245,6 +3271,11 @@
         "undici-types": "~5.26.4"
       }
     },
+    "node_modules/@types/path-browserify": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/@types/path-browserify/-/path-browserify-1.0.2.tgz",
+      "integrity": "sha512-ZkC5IUqqIFPXx3ASTTybTzmQdwHwe2C0u3eL75ldQ6T9E9IWFJodn6hIfbZGab73DfyiHN4Xw15gNxUq2FbvBA=="
+    },
     "node_modules/@types/tar": {
       "version": "6.1.13",
       "resolved": "https://registry.npmmirror.com/@types/tar/-/tar-6.1.13.tgz",
@@ -5016,6 +5047,11 @@
         "url": "https://github.com/inikulin/parse5?sponsor=1"
       }
     },
+    "node_modules/path-browserify": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/path-browserify/-/path-browserify-1.0.1.tgz",
+      "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="
+    },
     "node_modules/path-exists": {
       "version": "4.0.0",
       "resolved": "https://registry.npmmirror.com/path-exists/-/path-exists-4.0.0.tgz",
@@ -5112,6 +5148,14 @@
         "url": "https://opencollective.com/preact"
       }
     },
+    "node_modules/process": {
+      "version": "0.11.10",
+      "resolved": "https://registry.npmmirror.com/process/-/process-0.11.10.tgz",
+      "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
+      "engines": {
+        "node": ">= 0.6.0"
+      }
+    },
     "node_modules/proxy-from-env": {
       "version": "1.1.0",
       "resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz",

+ 4 - 0
package.json

@@ -6,6 +6,7 @@
   },
   "devDependencies": {
     "@intlify/unplugin-vue-i18n": "^4.0.0",
+    "@rollup/plugin-inject": "^5.0.5",
     "@types/node": "^20.14.2",
     "@vue/compiler-sfc": "^3.4.29",
     "child_process": "^1.0.2",
@@ -17,10 +18,13 @@
   "dependencies": {
     "@aws-sdk/client-s3": "^3.600.0",
     "@iconify/tools": "^4.0.4",
+    "@types/path-browserify": "^1.0.2",
     "@vueuse/core": "^10.11.0",
     "element-plus": "^2.7.5",
     "lodash": "^4.17.21",
     "md-editor-v3": "^4.17.0",
+    "path-browserify": "^1.0.1",
+    "process": "^0.11.10",
     "uuid": "^10.0.0",
     "vue-i18n": "^9.13.1"
   }

+ 0 - 0
pages/docs/dir1/f1-1


+ 0 - 0
pages/docs/dir2/dir2-1/f2-1-1


+ 0 - 0
pages/docs/dir2/dir2-1/f2-1-2


+ 0 - 0
pages/docs/dir2/f2-1


+ 1 - 0
pages/en-US/docs/index.md

@@ -0,0 +1 @@
+qqa

+ 0 - 0
pages/zh-HK/docs/dir1/f1-1


+ 17 - 5
utils/s3Helper.ts

@@ -36,17 +36,29 @@ export const s3ContentsToTree = (
   return __result;
 };
 
-export const trimTreeForSideBar = (tree) => {
-
-}
+export const trimTreeForSideBar = (tree) => {};
 
 type TreeNodeLike = {
   children?: TreeNodeLike[];
 };
 
-export const getTreeLeafs = <T extends TreeNodeLike>(treeNode: T): T[] => {
+export const getTreeFlatten = <T extends TreeNodeLike>(
+  treeNode: T,
+  order: "preorder" | "postorder" = "preorder",
+  mode: "all" | "leaf" = "leaf"
+): T[] => {
   if (treeNode.children?.length) {
-    return _.flatMap<T, T>(treeNode.children as T[], getTreeLeafs);
+    const result = _.flatMap<T, T>(treeNode.children as T[], (node) =>
+      getTreeFlatten(node, order, mode)
+    )
+    if (mode === 'all') {
+      if (order === 'preorder') {
+        result.unshift(treeNode)
+      } else if (order === 'postorder') {
+        result.push(treeNode)
+      }
+    }
+    return result
   }
   return [treeNode];
 };