index.vue 6.8 KB

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