index.vue 6.8 KB

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