FilesModal.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600
  1. <template>
  2. <a-modal
  3. :title="title"
  4. :width="840"
  5. :visible="visible"
  6. :isLoading="isLoading"
  7. :maskClosable="false"
  8. :destroyOnClose="false"
  9. @ok="handleSubmit"
  10. @cancel="handleCancel"
  11. >
  12. <a-spin :spinning="isLoading">
  13. <div class="library-box clearfix">
  14. <!-- 分组列表 -->
  15. <div class="file-group">
  16. <div class="group-tree">
  17. <a-directory-tree
  18. v-if="groupListTreeSelect.length"
  19. :treeData="groupListTreeSelect"
  20. :blockNode="true"
  21. :showIcon="false"
  22. @select="onSelectGroup"
  23. ></a-directory-tree>
  24. </div>
  25. <a class="group-add" href="javascript:void(0);" @click="handleAddGroup">新增分组</a>
  26. </div>
  27. <!-- 文件列表 -->
  28. <div class="file-list">
  29. <!-- 头部操作栏 -->
  30. <div class="top-operate clearfix">
  31. <!-- 搜索框 -->
  32. <a-input-search
  33. class="fl-l"
  34. style="width: 200px"
  35. placeholder="搜索文件名称"
  36. v-model="queryParam.fileName"
  37. @search="onSearch"
  38. />
  39. <!-- 上传按钮 -->
  40. <div class="file-upload fl-r">
  41. <span
  42. class="upload-desc"
  43. >{{ fileType === FileTypeEnum.VIDEO.value ? '视频' : '图片' }}大小不能超过{{ uploadSizeLimit }}M</span>
  44. <a-upload
  45. name="iFile"
  46. :accept="accept"
  47. :beforeUpload="beforeUpload"
  48. :customRequest="onUpload"
  49. :multiple="true"
  50. :showUploadList="false"
  51. >
  52. <a-button icon="cloud-upload">上传</a-button>
  53. </a-upload>
  54. </div>
  55. </div>
  56. <div class="file-list-body">
  57. <!-- 文件列表 -->
  58. <ul v-if="fileList.data && fileList.data.length" class="file-list-ul clearfix">
  59. <li
  60. class="file-item"
  61. :class="{ active: selectedIndexs.indexOf(index) > -1 }"
  62. v-for="(item, index) in fileList.data"
  63. :key="index"
  64. @click="onSelectItem(index)"
  65. >
  66. <div
  67. class="img-cover"
  68. :style="{ backgroundImage: `url('${item.preview_url}')`, width: fileType === FileTypeEnum.VIDEO.value ? '55px' : '95px' }"
  69. ></div>
  70. <p class="file-name oneline-hide">{{ item.file_name }}</p>
  71. <div class="select-mask">
  72. <a-icon class="selected-icon" type="check" />
  73. </div>
  74. </li>
  75. </ul>
  76. <!-- 无数据时显示 -->
  77. <a-empty v-else-if="!isLoading" />
  78. <!-- 底部操作栏 -->
  79. <div class="footer-operate clearfix">
  80. <div class="fl-l" v-if="selectedIndexs.length">
  81. <span class="footer-desc">已选择{{ selectedIndexs.length }}项</span>
  82. <a-config-provider :auto-insert-space-in-button="false">
  83. <a-button-group>
  84. <a-button
  85. v-if="inArray('delete', actions)"
  86. class="btn-mini"
  87. size="small"
  88. @click="handleDelete()"
  89. >删除</a-button>
  90. <a-button
  91. v-if="inArray('move', actions)"
  92. class="btn-mini"
  93. size="small"
  94. @click="handleBatchMove()"
  95. >移动</a-button>
  96. <a-button
  97. v-if="inArray('copyIds', actions)"
  98. class="btn-mini"
  99. size="small"
  100. @click="handleCopyIds()"
  101. >复制ID</a-button>
  102. </a-button-group>
  103. </a-config-provider>
  104. </div>
  105. <!-- 分页组件 -->
  106. <a-pagination
  107. class="fl-r"
  108. size="small"
  109. v-model="fileList.current_page"
  110. :total="fileList.total"
  111. :defaultPageSize="15"
  112. hideOnSinglePage
  113. @change="handleNextPage"
  114. />
  115. </div>
  116. </div>
  117. </div>
  118. </div>
  119. </a-spin>
  120. <!-- 新增分组 -->
  121. <AddGroupForm ref="AddGroupForm" :groupList="groupList" @handleSubmit="getGroupList" />
  122. <!-- 移动分组 -->
  123. <MoveGroupForm ref="MoveGroupForm" :groupList="groupListTree" @handleSubmit="handleRefresh" />
  124. </a-modal>
  125. </template>
  126. <script>
  127. import PropTypes from 'ant-design-vue/es/_util/vue-types'
  128. import store from '@/store'
  129. import { debounce, inArray, assignment } from '@/utils/util'
  130. import * as FileApi from '@/api/files'
  131. import * as GroupApi from '@/api/files/group'
  132. import * as UploadApi from '@/api/upload'
  133. import FileTypeEnum from '@/common/enum/file/FileType'
  134. import ChannelEnum from '@/common/enum/file/Channel'
  135. import AddGroupForm from './AddGroupForm'
  136. import MoveGroupForm from './MoveGroupForm'
  137. export default {
  138. name: 'FilesModal',
  139. components: {
  140. AddGroupForm,
  141. MoveGroupForm
  142. },
  143. props: {
  144. // 多选模式, 如果false为单选
  145. multiple: PropTypes.bool.def(false),
  146. // 最大选择的数量限制, multiple模式下有效
  147. maxNum: PropTypes.integer.def(100),
  148. // 已选择的数量
  149. selectedNum: PropTypes.integer.def(0),
  150. // 文件类型 (10图片 30视频)
  151. fileType: PropTypes.integer.def(FileTypeEnum.IMAGE.value),
  152. // 文件操作
  153. actions: PropTypes.array.def(['delete', 'move', 'copyIds']),
  154. },
  155. data () {
  156. return {
  157. // 对话框标题
  158. title: '图片库',
  159. // modal(对话框)是否可见
  160. visible: false,
  161. // 枚举类
  162. FileTypeEnum,
  163. // 后端上传api
  164. uploadUrl: UploadApi.image,
  165. // 上传文件大小限制
  166. uploadSizeLimit: 2,
  167. // 文件上传的格式限制
  168. accept: '',
  169. // 查询参数
  170. queryParam: {
  171. // 文件类型: 图片
  172. fileType: FileTypeEnum.IMAGE.value,
  173. // 上传来源: 商户后台
  174. channel: ChannelEnum.STORE.value,
  175. // 当前页码
  176. page: 1,
  177. // 文件名称
  178. fileName: '',
  179. // 文件分组
  180. groupId: 0,
  181. },
  182. // modal(对话框)确定按钮 loading
  183. isLoading: true,
  184. // 分组列表
  185. groupList: [],
  186. // 文件列表
  187. fileList: [],
  188. // 文件分组列表(树状结构)
  189. groupListTree: [],
  190. // 文件分组列表(用于筛选组件)
  191. groupListTreeSelect: [],
  192. // 选中的文件
  193. selectedIndexs: [],
  194. // 上传中的文件
  195. uploading: []
  196. }
  197. },
  198. created () {
  199. },
  200. beforeCreate () {
  201. assignment(this, { inArray })
  202. },
  203. methods: {
  204. // 显示对话框
  205. show () {
  206. // 显示窗口
  207. this.visible = true
  208. // 初始化文件类型
  209. this.initFileType()
  210. // 获取分组列表
  211. this.getGroupList()
  212. // 获取文件列表
  213. this.getFileList()
  214. },
  215. // 初始化文件类型
  216. initFileType () {
  217. const publicConfig = store.getters.publicConfig
  218. if (this.fileType === FileTypeEnum.IMAGE.value) {
  219. this.title = '图片库'
  220. this.accept = 'image/jpeg,image/png,image/gif,image/webp'
  221. this.uploadUrl = UploadApi.image
  222. this.uploadSizeLimit = publicConfig.uploadImageSize || 2
  223. }
  224. if (this.fileType === FileTypeEnum.VIDEO.value) {
  225. this.title = '视频库'
  226. this.accept = '.mp4'
  227. this.uploadUrl = UploadApi.video
  228. this.uploadSizeLimit = publicConfig.uploadVideoSize || 10
  229. }
  230. this.queryParam.fileType = this.fileType
  231. },
  232. // 获取文件分组列表
  233. getGroupList () {
  234. // this.isLoading = true
  235. GroupApi.list({}).then(result => {
  236. // 记录列表数据
  237. const groupList = result.data.list
  238. this.groupList = groupList
  239. // 格式化分组列表
  240. const groupListTree = this.formatTreeData(groupList)
  241. // 记录 groupListTree
  242. this.groupListTree = groupListTree
  243. // 记录 groupListTreeSelect
  244. this.groupListTreeSelect = [{
  245. title: '全部',
  246. key: -1,
  247. value: -1
  248. }, {
  249. title: '未分组',
  250. key: 0,
  251. value: 0
  252. }].concat(groupListTree)
  253. // this.isLoading = false
  254. })
  255. },
  256. // 获取文件列表
  257. getFileList () {
  258. this.isLoading = true
  259. FileApi.list(this.queryParam)
  260. .then(result => {
  261. this.fileList = result.data.list
  262. this.isLoading = false
  263. })
  264. },
  265. /**
  266. * 格式化分组列表
  267. */
  268. formatTreeData (list, disabled = false) {
  269. const data = []
  270. list.forEach(item => {
  271. // 新的元素
  272. const netItem = {
  273. title: item.name,
  274. key: item.group_id,
  275. value: item.group_id
  276. }
  277. // 递归整理子集
  278. if (item.children && item.children.length) {
  279. netItem['children'] = this.formatTreeData(item['children'], netItem.disabled)
  280. } else {
  281. netItem['isLeaf'] = true
  282. netItem['scopedSlots'] = { icon: 'meh' }
  283. }
  284. data.push(netItem)
  285. })
  286. return data
  287. },
  288. // 记录选中的分组
  289. onSelectGroup (selectedKeys) {
  290. this.queryParam.groupId = selectedKeys[0]
  291. this.handleRefresh(true)
  292. },
  293. // 记录选中的文件
  294. onSelectItem (index) {
  295. const { multiple, maxNum, selectedIndexs } = this
  296. // 记录选中状态
  297. if (!multiple) {
  298. this.selectedIndexs = [index]
  299. return
  300. }
  301. const key = selectedIndexs.indexOf(index)
  302. const selected = key > -1
  303. // 验证数量限制
  304. if (!selected && (selectedIndexs.length + this.selectedNum) >= maxNum) {
  305. this.$message.warning(`最多可选${maxNum}个文件`, 1)
  306. return
  307. }
  308. !selected ? this.selectedIndexs.push(index) : this.selectedIndexs.splice(key, 1)
  309. },
  310. // 新增分组
  311. handleAddGroup () {
  312. this.$refs.AddGroupForm.add()
  313. },
  314. // 搜索文件名称
  315. onSearch () {
  316. this.handleRefresh(true)
  317. },
  318. // 事件: 上传文件之前
  319. beforeUpload (file, fileList) {
  320. // 显示错误提示(防抖处理)
  321. const showErrorMsg = debounce(this.$message.error, 20)
  322. // 验证文件大小
  323. const fileSizeMb = file.size / 1024 / 1024
  324. if (fileSizeMb > this.uploadSizeLimit) {
  325. showErrorMsg(`上传的文件大小不能超出${this.uploadSizeLimit}MB`)
  326. return false
  327. }
  328. // 验证文件上传数量
  329. if (fileList.length > 10) {
  330. showErrorMsg('一次上传的文件数量不能超出10个')
  331. return false
  332. }
  333. return true
  334. },
  335. // 事件: 自定义上传
  336. onUpload (info) {
  337. this.isLoading = true
  338. // 记录上传状态
  339. this.uploading.push(true)
  340. // 构建上传参数
  341. const formData = new FormData()
  342. formData.append('iFile', info.file)
  343. formData.append('groupId', this.queryParam.groupId)
  344. // 开始上传
  345. this.uploadUrl(formData)
  346. .finally(() => {
  347. this.uploading.pop()
  348. if (this.uploading.length === 0) {
  349. this.isLoading = false
  350. this.handleRefresh(true)
  351. }
  352. })
  353. },
  354. // 列表分页事件
  355. handleNextPage (page, pageSize) {
  356. this.queryParam.page = page
  357. this.handleRefresh()
  358. },
  359. // 关闭对话框事件
  360. handleCancel () {
  361. this.visible = false
  362. this.selectedIndexs = []
  363. },
  364. /**
  365. * 刷新文件列表
  366. * @param Boolean bool 强制刷新到第一页
  367. */
  368. handleRefresh (bool = false) {
  369. bool && (this.queryParam.page = 1)
  370. // 清空选中
  371. this.selectedIndexs = []
  372. // 获取文件列表
  373. this.getFileList()
  374. },
  375. // 删除文件
  376. handleDelete (item) {
  377. const that = this
  378. const fileIds = this.getSelectedItemIds()
  379. const modal = this.$confirm({
  380. title: '您确定要删除该文件吗?',
  381. content: '删除后不可恢复,请谨慎操作',
  382. onOk () {
  383. return FileApi.deleted({ fileIds })
  384. .then(result => {
  385. that.$message.success(result.message, 1.5)
  386. that.handleRefresh()
  387. })
  388. .catch(() => true)
  389. .finally(result => modal.destroy())
  390. }
  391. })
  392. },
  393. /**
  394. * 批量移动文件
  395. */
  396. handleBatchMove () {
  397. const fileIds = this.getSelectedItemIds()
  398. this.$refs.MoveGroupForm.show(fileIds)
  399. },
  400. // 复制选中的文件ID集
  401. handleCopyIds () {
  402. const itemIds = this.getSelectedItemIds()
  403. this.$copyText(itemIds.join(',')).then(res => {
  404. this.$message.success('复制成功', 1.5)
  405. })
  406. },
  407. // 获取选中的文件id集
  408. getSelectedItemIds () {
  409. const selectedItems = this.getSelectedItems()
  410. return selectedItems.map(item => item.file_id)
  411. },
  412. // 获取选中的文件
  413. getSelectedItems () {
  414. const selectedItems = []
  415. for (const key in this.selectedIndexs) {
  416. const index = this.selectedIndexs[key]
  417. selectedItems.push(this.fileList.data[index])
  418. }
  419. return selectedItems
  420. },
  421. // 确认按钮
  422. handleSubmit (e) {
  423. e.preventDefault()
  424. // 获取选中的文件
  425. const selectedItems = this.getSelectedItems()
  426. // 通知父端组件提交完成了
  427. this.$emit('handleSubmit', selectedItems)
  428. // 关闭对话框
  429. this.handleCancel()
  430. }
  431. }
  432. }
  433. </script>
  434. <style lang="less" scoped>
  435. /deep/.ant-modal-header,
  436. /deep/.ant-modal-footer {
  437. border: none;
  438. }
  439. /deep/.ant-modal-body {
  440. padding: 6px;
  441. }
  442. /deep/.ant-empty {
  443. padding: 120px 0;
  444. }
  445. /* 文件库 */
  446. .library-box {
  447. user-select: none;
  448. // 文件分组
  449. .file-group {
  450. float: left;
  451. border-right: 1px solid #e6e6e6;
  452. // 分组列表
  453. .group-tree {
  454. width: 150px;
  455. height: 440px;
  456. overflow-y: auto;
  457. overflow-x: auto;
  458. /deep/.ant-tree {
  459. display: inline-block;
  460. min-width: 100%;
  461. max-height: 380px;
  462. width: auto;
  463. }
  464. }
  465. // 新增分组
  466. .group-add {
  467. display: block;
  468. margin-top: 20px;
  469. font-size: @font-size-base;
  470. padding: 0 30px;
  471. }
  472. }
  473. // 文件列表
  474. .file-list {
  475. float: left;
  476. width: 630px;
  477. margin-left: 20px;
  478. // 头部操作区
  479. .top-operate {
  480. margin-bottom: 10px;
  481. .file-upload {
  482. .upload-desc {
  483. font-size: 12px;
  484. padding-right: 10px;
  485. color: #999;
  486. }
  487. }
  488. }
  489. // 文件列表
  490. .file-list-body {
  491. height: 455px;
  492. .file-list-ul {
  493. margin: 0;
  494. padding: 0;
  495. height: 417px;
  496. }
  497. .file-item {
  498. width: 110px;
  499. position: relative;
  500. cursor: pointer;
  501. border-radius: 2px;
  502. padding: 4px;
  503. border: 1px solid rgba(0, 0, 0, 0.05);
  504. float: left;
  505. margin: 8px;
  506. -webkit-transition: All 0.2s ease-in-out;
  507. -moz-transition: All 0.2s ease-in-out;
  508. -o-transition: All 0.2s ease-in-out;
  509. transition: All 0.2s ease-in-out;
  510. &:hover {
  511. border: 1px solid #16bce2;
  512. }
  513. }
  514. .file-item {
  515. // 文件名称
  516. .file-name {
  517. font-size: 12px;
  518. text-align: center;
  519. }
  520. // 预览图
  521. .img-cover {
  522. margin: 0 auto;
  523. width: 95px;
  524. height: 95px;
  525. background: no-repeat center center / 100%;
  526. }
  527. // 遮罩层(选中时)
  528. &.active .select-mask {
  529. display: block;
  530. }
  531. .select-mask {
  532. display: none;
  533. position: absolute;
  534. top: 0;
  535. bottom: 0;
  536. left: 0;
  537. right: 0;
  538. background: rgba(0, 0, 0, 0.41);
  539. text-align: center;
  540. border-radius: 2px;
  541. .selected-icon {
  542. font-size: 26px;
  543. color: #fff;
  544. line-height: 122px;
  545. text-align: center;
  546. }
  547. }
  548. }
  549. // 底部操作栏
  550. .footer-operate {
  551. height: 28px;
  552. margin-top: 10px;
  553. .footer-desc {
  554. color: #999;
  555. margin-right: 10px;
  556. }
  557. .btn-mini {
  558. font-size: @font-size-base;
  559. padding: 0 15px;
  560. height: 28px;
  561. }
  562. }
  563. }
  564. }
  565. }
  566. </style>