index.vue 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318
  1. <script setup lang="ts">
  2. import axios from "axios";
  3. import Sortable from "sortablejs";
  4. import UploadForm from "./form.vue";
  5. import { ref, computed } from "vue";
  6. import { useRouter } from "vue-router";
  7. import { message } from "@/utils/message";
  8. import type { UploadFile } from "element-plus";
  9. import { getKeyList, extractFields, downloadByData } from "@pureadmin/utils";
  10. import EpPlus from "~icons/ep/plus?width=30&height=30";
  11. import Eye from "~icons/ri/eye-line";
  12. import Delete from "~icons/ri/delete-bin-7-line";
  13. defineOptions({
  14. name: "PureUpload"
  15. });
  16. const fileList = ref([]);
  17. const router = useRouter();
  18. const curOpenImgIndex = ref(0);
  19. const dialogVisible = ref(false);
  20. const urlList = computed(() => getKeyList(fileList.value, "url"));
  21. const imgInfos = computed(() => extractFields(fileList.value, "name", "size"));
  22. const getImgUrl = name => new URL(`./imgs/${name}.jpg`, import.meta.url).href;
  23. const srcList = Array.from({ length: 3 }).map((_, index) => {
  24. return getImgUrl(index + 1);
  25. });
  26. /** 上传文件前校验 */
  27. const onBefore = file => {
  28. if (!["image/jpeg", "image/png", "image/gif"].includes(file.type)) {
  29. message("只能上传图片");
  30. return false;
  31. }
  32. const isExceed = file.size / 1024 / 1024 > 2;
  33. if (isExceed) {
  34. message(`单个图片大小不能超过2MB`);
  35. return false;
  36. }
  37. };
  38. /** 超出最大上传数时触发 */
  39. const onExceed = () => {
  40. message("最多上传3张图片,请先删除在上传");
  41. };
  42. /** 移除上传的文件 */
  43. const handleRemove = (file: UploadFile) => {
  44. fileList.value.splice(fileList.value.indexOf(file), 1);
  45. };
  46. /** 大图预览 */
  47. const handlePictureCardPreview = (file: UploadFile) => {
  48. curOpenImgIndex.value = fileList.value.findIndex(img => img.uid === file.uid);
  49. dialogVisible.value = true;
  50. };
  51. const getUploadItem = () => document.querySelectorAll("#pure-upload-item");
  52. /** 缩略图拖拽排序 */
  53. const imgDrop = uid => {
  54. const CLASSNAME = "el-upload-list";
  55. const _curIndex = fileList.value.findIndex(img => img.uid === uid);
  56. getUploadItem()?.[_curIndex]?.classList?.add(`${CLASSNAME}__item-actions`);
  57. const wrapper: HTMLElement = document.querySelector(`.${CLASSNAME}`);
  58. Sortable.create(wrapper, {
  59. handle: `.${CLASSNAME}__item`,
  60. onEnd: ({ newIndex, oldIndex }) => {
  61. const oldFile = fileList.value[oldIndex];
  62. fileList.value.splice(oldIndex, 1);
  63. fileList.value.splice(newIndex, 0, oldFile);
  64. // fix: https://github.com/SortableJS/Sortable/issues/232 (firefox is ok, but chromium is bad. see https://bugs.chromium.org/p/chromium/issues/detail?id=410328)
  65. getUploadItem().forEach(ele => {
  66. ele.classList.remove(`${CLASSNAME}__item-actions`);
  67. });
  68. }
  69. });
  70. };
  71. /** 下载图片 */
  72. const onDownload = () => {
  73. [
  74. { name: "巴旦木.jpeg", type: "img" },
  75. { name: "恭喜发财.png", type: "img" },
  76. { name: "可爱动物.gif", type: "gif" },
  77. { name: "pure-upload.csv", type: "other" },
  78. { name: "pure-upload.txt", type: "other" }
  79. ].forEach(img => {
  80. axios
  81. .get(`https://xiaoxian521.github.io/hyperlink/${img.type}/${img.name}`, {
  82. responseType: "blob"
  83. })
  84. .then(({ data }) => {
  85. downloadByData(data, img.name);
  86. });
  87. });
  88. };
  89. </script>
  90. <template>
  91. <el-card shadow="never">
  92. <template #header>
  93. <div class="card-header">
  94. <el-link
  95. v-tippy="{
  96. content: '点击查看详细文档'
  97. }"
  98. href="https://element-plus.org/zh-CN/component/upload.html"
  99. target="_blank"
  100. style="font-size: 16px; font-weight: 800"
  101. >
  102. 文件上传
  103. </el-link>
  104. </div>
  105. <el-link
  106. class="mt-2"
  107. href="https://github.com/pure-admin/vue-pure-admin/blob/main/src/views/components/upload"
  108. target="_blank"
  109. >
  110. 代码位置 src/views/components/upload
  111. </el-link>
  112. </template>
  113. <el-button class="mb-4!" text bg @click="onDownload">
  114. 点击下载安全文件进行上传测试
  115. </el-button>
  116. <p class="mb-4!">
  117. 综合示例<span class="text-[14px]">
  118. ( <span class="text-[red]">自动上传</span>
  119. 、拖拽上传、拖拽排序、设置请求头、上传进度、大图预览、多选文件、最大文件数量、文件类型限制、文件大小限制、删除文件)
  120. </span>
  121. </p>
  122. <p v-show="fileList.length > 0" class="mb-4!">
  123. {{ imgInfos }}
  124. </p>
  125. <el-upload
  126. v-model:file-list="fileList"
  127. drag
  128. multiple
  129. class="pure-upload"
  130. list-type="picture-card"
  131. accept="image/jpeg,image/png,image/gif"
  132. action="https://run.mocky.io/v3/3aa761d7-b0b3-4a03-96b3-6168d4f7467b"
  133. :limit="3"
  134. :headers="{ Authorization: 'eyJhbGciOiJIUzUxMiJ9.admin' }"
  135. :on-exceed="onExceed"
  136. :before-upload="onBefore"
  137. >
  138. <EpPlus class="m-auto mt-4" />
  139. <template #file="{ file }">
  140. <div
  141. v-if="file.status == 'ready' || file.status == 'uploading'"
  142. class="mt-[35%]! m-auto"
  143. >
  144. <p class="font-medium">文件上传中</p>
  145. <el-progress
  146. class="mt-2!"
  147. :stroke-width="2"
  148. :text-inside="true"
  149. :show-text="false"
  150. :percentage="file.percentage"
  151. />
  152. </div>
  153. <div v-else @mouseenter.stop="imgDrop(file.uid)">
  154. <img
  155. class="el-upload-list__item-thumbnail select-none"
  156. :src="file.url"
  157. />
  158. <span
  159. id="pure-upload-item"
  160. :class="[
  161. 'el-upload-list__item-actions',
  162. fileList.length > 1 && 'cursor-move!'
  163. ]"
  164. >
  165. <span
  166. title="查看"
  167. class="hover:text-primary"
  168. @click="handlePictureCardPreview(file)"
  169. >
  170. <IconifyIconOffline
  171. :icon="Eye"
  172. class="hover:scale-125 duration-100"
  173. />
  174. </span>
  175. <span
  176. class="el-upload-list__item-delete"
  177. @click="handleRemove(file)"
  178. >
  179. <span title="移除" class="hover:text-[var(--el-color-danger)]">
  180. <IconifyIconOffline
  181. :icon="Delete"
  182. class="hover:scale-125 duration-100"
  183. />
  184. </span>
  185. </span>
  186. </span>
  187. </div>
  188. </template>
  189. </el-upload>
  190. <!-- 有时文档没写并不代表没有,多看源码好处多多😝 https://github.com/element-plus/element-plus/tree/dev/packages/components/image-viewer/src (emm...这让我想起刚开始写这个项目时,很多东西只有英文或者没有文档,需要看源码时,想笑🥹。那些美好时光都给这些坑了,giao) -->
  191. <el-image-viewer
  192. v-if="dialogVisible"
  193. :initialIndex="curOpenImgIndex"
  194. :url-list="urlList"
  195. :zoom-rate="1.2"
  196. :max-scale="7"
  197. :min-scale="0.2"
  198. @close="dialogVisible = false"
  199. @switch="index => (curOpenImgIndex = index)"
  200. />
  201. <!-- 将自定义内容插入到body里,有了它在图片预览的时候,想插入个分页器或者别的东东在预览区某个位置就很方便咯(用户需求可以很灵活,开源组件库几乎不可能尽善尽美,很多时候寻找别的解决途径或许更好) -->
  202. <teleport to="body">
  203. <div
  204. v-if="fileList[curOpenImgIndex] && dialogVisible"
  205. effect="dark"
  206. round
  207. size="large"
  208. type="info"
  209. class="img-name"
  210. >
  211. <p class="text-[#fff] dark:text-black">
  212. {{ fileList[curOpenImgIndex].name }}
  213. </p>
  214. </div>
  215. </teleport>
  216. <p class="el-upload__tip">
  217. 可拖拽上传最多3张单个不超过2MB且格式为jpeg/png/gif的图片
  218. </p>
  219. <el-divider />
  220. <p class="my-4!">
  221. 结合表单校验进行<span class="text-[red]">手动上传</span>
  222. <span class="text-[14px]">
  223. (可先打开浏览器控制台找到Network,然后填写表单内容后点击点提交观察请求变化)
  224. </span>
  225. </p>
  226. <div class="flex justify-between">
  227. <UploadForm />
  228. <div>
  229. <p class="text-center">上传接口相关截图</p>
  230. <el-image
  231. class="w-[200px] rounded-md"
  232. :src="srcList[0]"
  233. :preview-src-list="srcList"
  234. fit="cover"
  235. />
  236. </div>
  237. </div>
  238. <el-divider />
  239. <div class="flex flex-wrap">
  240. <p>
  241. 裁剪、上传头像请参考
  242. <span
  243. class="font-bold text-[18x] cursor-pointer hover:text-[red]"
  244. @click="router.push({ name: 'SystemUser' })"
  245. >
  246. 系统管理-用户管理
  247. </span>
  248. 表格操作栏中的上传头像功能
  249. </p>
  250. <p class="text-[red] text-[12px] flex flex-auto items-center justify-end">
  251. 免责声明:上传接口使用免费开源的
  252. <el-link
  253. href="https://designer.mocky.io/"
  254. target="_blank"
  255. style="font-size: 16px; font-weight: 800"
  256. >
  257. &nbsp;Mocky&nbsp;
  258. </el-link>
  259. <span class="font-bold text-[18x]"> 请不要上传重要信息 </span
  260. >,如果造成任何损失,我们概不负责
  261. </p>
  262. </div>
  263. </el-card>
  264. </template>
  265. <style lang="scss" scoped>
  266. :deep(.card-header) {
  267. display: flex;
  268. .header-right {
  269. display: flex;
  270. flex: auto;
  271. align-items: center;
  272. justify-content: flex-end;
  273. font-size: 14px;
  274. }
  275. }
  276. :deep(.pure-upload) {
  277. .el-upload-dragger {
  278. background-color: transparent;
  279. border: none;
  280. }
  281. }
  282. .img-name {
  283. position: absolute;
  284. bottom: 80px;
  285. left: 50%;
  286. z-index: 4000;
  287. padding: 5px 23px;
  288. background-color: var(--el-text-color-regular);
  289. border-radius: 22px;
  290. transform: translateX(-50%);
  291. /** 将下面的 left: 50%; bottom: 80px; transform: translateX(-50%); 注释掉
  292. * 解开下面 left: 40px; top: 40px; 注释,体验不一样的感觉。啊?还是差强人意,自己调整位置吧🥹
  293. */
  294. // left: 40px;
  295. // top: 40px;
  296. }
  297. </style>