index.vue 6.8 KB

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