index.vue 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260
  1. <script setup lang="ts">
  2. import { ref, onMounted } from "vue";
  3. import {v4 as uuid4} from 'uuid'
  4. import {
  5. S3Client,
  6. PutObjectCommand,
  7. ListObjectsCommand,
  8. GetObjectCommand,
  9. DeleteObjectsCommand,
  10. } from "@aws-sdk/client-s3";
  11. import {
  12. Plus,
  13. Refresh,
  14. } from "@element-plus/icons-vue";
  15. import Node from "./Node.vue";
  16. import AppendModal from "./AppendModal.vue";
  17. import type { TreeData } from "./Tree";
  18. import { ElNotification, type ElTree } from "element-plus";
  19. import { MdEditor } from "md-editor-v3";
  20. import { pathsToTree, getTreeLeafs } from "@/utils/s3Helper";
  21. import "md-editor-v3/lib/style.css";
  22. const s3 = new S3Client({
  23. credentials: {
  24. accessKeyId: import.meta.env.VITE_AWS_S3_ACCESS_KEY_ID,
  25. secretAccessKey: import.meta.env.VITE_AWS_S3_SECRET_ACCESS_KEY,
  26. },
  27. region: import.meta.env.VITE_AWS_S3_REGION,
  28. });
  29. const tree$ = ref<InstanceType<typeof ElTree>>();
  30. const dataSource = ref<TreeData[]>([]);
  31. const sideLoading = ref(false);
  32. const loadS3DocsListObjects = async () => {
  33. const command = new ListObjectsCommand({ Bucket: import.meta.env.VITE_DOCS_LIST_BUCKET });
  34. const result = await s3.send(command);
  35. return result.Contents!.map((item) => item.Key!).slice(0, 200);
  36. };
  37. const loadSideBar = async () => {
  38. sideLoading.value = true;
  39. dataSource.value = pathsToTree(await loadS3DocsListObjects());
  40. sideLoading.value = false;
  41. };
  42. onMounted(() => {
  43. loadSideBar();
  44. });
  45. const currentOpenData = ref<TreeData>();
  46. const onNodeClick = async (data: TreeData, node) => {
  47. if (data.isDir) {
  48. return;
  49. }
  50. textLoading.value = true;
  51. const command = new GetObjectCommand({
  52. Bucket: import.meta.env.VITE_DOCS_LIST_BUCKET,
  53. Key: data.key,
  54. ResponseCacheControl: "no-cache",
  55. });
  56. const file = await s3.send(command);
  57. text.value = await file.Body?.transformToString()!;
  58. currentOpenData.value = data;
  59. textLoading.value = false;
  60. };
  61. const text = ref("");
  62. const textLoading = ref(false);
  63. const onUploadImg = async (files: File[], callback: (urls: string[] | { url: string; alt: string; title: string }[]) => void) => {
  64. let result = []
  65. for (const file of files) {
  66. try {
  67. const key = `${uuid4()}::${file.name}`
  68. const command = new PutObjectCommand({
  69. Bucket: import.meta.env.DOCS_MEDIA_BUCKET,
  70. Key: key,
  71. Body: file,
  72. ACL: 'public-read',
  73. });
  74. const res = await s3.send(command);
  75. result.push({url: `https://${import.meta.env.DOCS_MEDIA_BUCKET}.s3.amazonaws.com/${key}`, alt: file.name, title: file.name})
  76. } catch (e) {
  77. console.error(e)
  78. ElNotification.error(`${file.name} 上传失败`)
  79. }
  80. }
  81. callback(result)
  82. };
  83. const onSave = async () => {
  84. if (!currentOpenData.value) {
  85. ElNotification.info('请先选择文件')
  86. return;
  87. }
  88. textLoading.value = true
  89. try {
  90. const command = new PutObjectCommand({
  91. Bucket: import.meta.env.VITE_DOCS_LIST_BUCKET,
  92. Key: currentOpenData.value?.key,
  93. Body: text.value,
  94. });
  95. await s3.send(command);
  96. ElNotification.success(`${currentOpenData.value?.key} 保存成功`)
  97. } catch (e) {
  98. console.error(e)
  99. ElNotification.error(`${currentOpenData.value?.key} 保存失败`)
  100. } finally {
  101. textLoading.value = false
  102. }
  103. };
  104. let resolveAppend: Function | undefined;
  105. let rejectAppend: Function | undefined;
  106. const showAppendModal = ref(false);
  107. const appendLoading = ref(false)
  108. const appendContextData = ref()
  109. const onAppend = async (data: TreeData) => {
  110. const modalConfirmPromise = new Promise((resolve, reject) => {
  111. resolveAppend = resolve;
  112. rejectAppend = reject;
  113. });
  114. appendContextData.value = data
  115. showAppendModal.value = true;
  116. let filename, isDir
  117. try {
  118. ( { filename, isDir } = await modalConfirmPromise);
  119. } catch (e) {
  120. return
  121. }
  122. try {
  123. appendLoading.value = true
  124. const key = `${data.key ? `${data.key}/` : ''}${filename}`
  125. if (!isDir) {
  126. const command = new PutObjectCommand({
  127. Bucket: import.meta.env.VITE_DOCS_LIST_BUCKET,
  128. Key: key,
  129. Body: "",
  130. });
  131. await s3.send(command);
  132. }
  133. const newChild: TreeData = {
  134. key,
  135. label: filename,
  136. children: [],
  137. isDir,
  138. };
  139. if (!data.children) {
  140. data.children = [];
  141. }
  142. data.children.push(newChild);
  143. } catch (e) {
  144. console.error(e);
  145. ElNotification.error('添加失败')
  146. } finally {
  147. appendLoading.value = false
  148. showAppendModal.value = false;
  149. }
  150. };
  151. const onRemove = async (node: Node, data: TreeData) => {
  152. const parent = node.parent;
  153. const children = parent.data.children || parent.data;
  154. const index = children.findIndex((d) => d.key === data.key);
  155. sideLoading.value = true
  156. try {
  157. const leafDatas = getTreeLeafs(data)
  158. const command = new DeleteObjectsCommand({
  159. Bucket: import.meta.env.VITE_DOCS_LIST_BUCKET,
  160. Delete: {
  161. Objects: leafDatas.map(d => ({Key: d.key})),
  162. Quiet: false,
  163. }
  164. })
  165. await s3.send(command)
  166. // TODO request S3 delete
  167. children.splice(index, 1);
  168. } catch (e) {
  169. console.error(e)
  170. ElNotification.error('删除失败')
  171. } finally {
  172. sideLoading.value = false
  173. }
  174. };
  175. </script>
  176. <template>
  177. <div class="md-container">
  178. <div class="left" v-loading="sideLoading">
  179. <el-button-group>
  180. <el-button
  181. @click="onAppend({ key: '', label: '', children: dataSource })"
  182. type="primary"
  183. :icon="Plus"
  184. >
  185. </el-button>
  186. <el-button @click="loadSideBar" type="info" :icon="Refresh"></el-button>
  187. </el-button-group>
  188. <el-tree
  189. ref="tree$"
  190. :data="dataSource"
  191. node-key="key"
  192. default-expand-all
  193. :expand-on-click-node="false"
  194. @node-click="onNodeClick"
  195. >
  196. <template #default="{ node, data }">
  197. <Node
  198. :node="node"
  199. :data="data"
  200. @append="() => onAppend(data)"
  201. @remove="() => onRemove(node, data)"
  202. />
  203. </template>
  204. </el-tree>
  205. </div>
  206. <MdEditor
  207. v-loading="textLoading"
  208. :disabled="!currentOpenData"
  209. v-model="text"
  210. @save="onSave"
  211. @uploadImg="onUploadImg"
  212. />
  213. </div>
  214. <AppendModal
  215. v-model:show="showAppendModal"
  216. :contextData="appendContextData"
  217. :appendLoading="appendLoading"
  218. @ok="(formData) => resolveAppend?.(formData)"
  219. @cancel="() => rejectAppend?.()"
  220. />
  221. </template>
  222. <style lang="scss" scoped>
  223. .md-container {
  224. width: 100vw;
  225. height: 100dvh;
  226. display: flex;
  227. align-items: stretch;
  228. .md-editor {
  229. flex: 1;
  230. height: 100%;
  231. }
  232. .left {
  233. flex: 0 0 300px;
  234. padding: 10px;
  235. display: flex;
  236. flex-direction: column;
  237. align-items: stretch;
  238. .el-tree {
  239. overflow: auto;
  240. flex: 1;
  241. :deep(.el-tree-node.is-current) {
  242. & > .el-tree-node__content {
  243. background-color: aqua;
  244. }
  245. }
  246. }
  247. }
  248. }
  249. </style>