mirror of
https://github.com/continew-org/continew-admin-ui.git
synced 2025-09-08 22:57:11 +08:00
feat: 新增文件管理
This commit is contained in:
@@ -18,3 +18,8 @@ export function listRoleDict(query?: { name: string; status: number }) {
|
||||
export function listCommonDict(code: string) {
|
||||
return http.get<LabelValueState[]>(`${BASE_URL}/dict/${code}`)
|
||||
}
|
||||
|
||||
/** @desc 上传文件 */
|
||||
export function uploadFile(data: FormData) {
|
||||
return http.post(`${BASE_URL}/file`, data)
|
||||
}
|
19
src/apis/system/file.ts
Normal file
19
src/apis/system/file.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import http from '@/utils/http'
|
||||
import type * as System from './type'
|
||||
|
||||
const BASE_URL = '/system/file'
|
||||
|
||||
/** @desc 查询文件列表 */
|
||||
export function listFile(query: System.FileQuery) {
|
||||
return http.get<System.FileItem[]>(`${BASE_URL}/list`, query)
|
||||
}
|
||||
|
||||
/** @desc 修改文件 */
|
||||
export function updateFile(data: any, id: string) {
|
||||
return http.put(`${BASE_URL}/${id}`, data)
|
||||
}
|
||||
|
||||
/** @desc 删除文件 */
|
||||
export function deleteFile(ids: string | Array<string>) {
|
||||
return http.del(`${BASE_URL}/${ids}`)
|
||||
}
|
@@ -1,4 +1,5 @@
|
||||
export * from './dept'
|
||||
export * from './log'
|
||||
export * from './dict'
|
||||
export * from './file'
|
||||
export * from './storage'
|
||||
|
@@ -91,6 +91,25 @@ export interface DictItemQuery extends PageQuery {
|
||||
dictId: string
|
||||
}
|
||||
|
||||
/** 系统文件类型 */
|
||||
export type FileItem = {
|
||||
id: string
|
||||
name: string
|
||||
size: number
|
||||
url: string
|
||||
extension: string
|
||||
type: number
|
||||
storageId: string
|
||||
createUserString: string
|
||||
createTime: string
|
||||
updateUserString: string
|
||||
updateTime: string
|
||||
}
|
||||
export interface FileQuery extends PageQuery {
|
||||
name?: string
|
||||
type?: string
|
||||
}
|
||||
|
||||
/** 系统存储类型 */
|
||||
export type StorageResp = {
|
||||
id: string
|
||||
|
1
src/assets/icons/menu-file.svg
Normal file
1
src/assets/icons/menu-file.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg t="1684652847211" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2238" width="200" height="200"><path d="M0 0m372.363636 0l279.272728 0q372.363636 0 372.363636 372.363636l0 279.272728q0 372.363636-372.363636 372.363636l-279.272728 0q-372.363636 0-372.363636-372.363636l0-279.272728q0-372.363636 372.363636-372.363636Z" fill="#F7A647" p-id="2239"></path><path d="M232.727273 303.872C232.727273 290.327273 243.781818 279.272727 257.326545 279.272727h148.631273c5.620364 0 10.984727 2.292364 14.848 6.283637l50.897455 52.805818c1.186909 1.233455 2.210909 2.594909 3.037091 4.049454h259.874909c31.138909 0 56.657455 25.460364 56.657454 56.610909V688.058182c0 31.150545-25.518545 56.669091-56.657454 56.669091H289.396364C258.245818 744.727273 232.727273 719.208727 232.727273 688.058182V303.872zM726.702545 556.218182h-239.825454a10.216727 10.216727 0 0 0-10.205091 10.205091v25.390545c0 5.620364 4.584727 10.205091 10.205091 10.205091h239.825454a10.216727 10.216727 0 0 0 10.205091-10.205091v-25.390545a10.216727 10.216727 0 0 0-10.205091-10.205091z m-0.058181-87.691637h-239.825455a10.216727 10.216727 0 0 0-10.205091 10.205091h-0.069818v25.390546c0 5.632 4.573091 10.216727 10.205091 10.216727h239.895273a10.216727 10.216727 0 0 0 10.205091-10.216727V478.72a10.216727 10.216727 0 0 0-10.205091-10.205091z" fill="#FFFFFF" p-id="2240"></path></svg>
|
After Width: | Height: | Size: 1.4 KiB |
140
src/views/system/file/components/FileAudioModal/ModalContent.vue
Normal file
140
src/views/system/file/components/FileAudioModal/ModalContent.vue
Normal file
@@ -0,0 +1,140 @@
|
||||
<template>
|
||||
<transition name="slide-dynamic-origin">
|
||||
<div class="audio-box" ref="audioRef" :style="audioStyle" v-show="visible">
|
||||
<section style="padding: 10px 14px 14px 14px">
|
||||
<div class="audio-box__header" ref="audioHeadRef">
|
||||
<div class="audio-name">
|
||||
<icon-music :size="16" spin />
|
||||
<span>{{ props.data?.name }}.{{ props.data?.extension }}</span>
|
||||
</div>
|
||||
<div class="close-icon" @click="close">
|
||||
<icon-close :size="12" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 音频组件 -->
|
||||
<audio class="audio" :src="audioSrc" controls autoplay></audio>
|
||||
</section>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useDraggable, useWindowSize, useElementSize } from '@vueuse/core'
|
||||
import type { FileItem } from '@/apis'
|
||||
|
||||
interface Props {
|
||||
data: FileItem
|
||||
onClose: () => void
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), {})
|
||||
|
||||
const visible = ref(false)
|
||||
const audioRef = ref<HTMLElement | null>(null)
|
||||
const audioHeadRef = ref<HTMLElement | null>(null)
|
||||
|
||||
const audioSrc = computed(() => {
|
||||
return props.data?.url || ''
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
visible.value = true
|
||||
})
|
||||
|
||||
const { width: windowWidth, height: windowHeight } = useWindowSize()
|
||||
const { width: boxWidth, height: boxHeight } = useElementSize(audioRef)
|
||||
|
||||
const axis = ref({ top: 40, left: windowWidth.value - boxWidth.value })
|
||||
const obj = JSON.parse(sessionStorage.getItem('AudioDialogXY') as string)
|
||||
if (obj && obj.top && obj.left) {
|
||||
axis.value.top = obj.top
|
||||
axis.value.left = obj.left
|
||||
}
|
||||
const { x, y } = useDraggable(audioRef, {
|
||||
initialValue: { x: axis.value.left - boxWidth.value, y: axis.value.top }
|
||||
})
|
||||
|
||||
const audioStyle = computed(() => {
|
||||
let left: number | string = x.value
|
||||
let top: number | string = y.value
|
||||
if (x.value > windowWidth.value - boxWidth.value) {
|
||||
left = windowWidth.value - boxWidth.value
|
||||
}
|
||||
if (x.value < 0) {
|
||||
left = 0
|
||||
}
|
||||
if (y.value > windowHeight.value - boxHeight.value) {
|
||||
top = windowHeight.value - boxHeight.value
|
||||
}
|
||||
if (y.value < 0) {
|
||||
top = 0
|
||||
}
|
||||
sessionStorage.setItem('AudioDialogXY', JSON.stringify({ top, left }))
|
||||
return {
|
||||
left: left + 'px',
|
||||
top: top + 'px'
|
||||
}
|
||||
})
|
||||
|
||||
const close = () => {
|
||||
visible.value = false
|
||||
props.onClose && props.onClose()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.audio-box {
|
||||
width: 300px;
|
||||
position: fixed;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||
background: linear-gradient(to right, $color-theme, rgb(var(--primary-2)));
|
||||
z-index: 9999;
|
||||
&__header {
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
margin-bottom: 10px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
cursor: move;
|
||||
user-select: none;
|
||||
&:active {
|
||||
cursor: move;
|
||||
}
|
||||
.audio-name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
> span {
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
.close-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 50%;
|
||||
background: rgba(0, 0, 0, 0);
|
||||
transition: all 0.2s;
|
||||
cursor: pointer;
|
||||
svg {
|
||||
transition: all 0.2s;
|
||||
}
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
svg {
|
||||
transform: scale(1.3);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.audio {
|
||||
width: 100%;
|
||||
&::-webkit-media-controls-enclosure {
|
||||
background: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
45
src/views/system/file/components/FileAudioModal/index.ts
Normal file
45
src/views/system/file/components/FileAudioModal/index.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import type { Component } from 'vue'
|
||||
import { createApp } from 'vue'
|
||||
import ArcoVueIcon from '@arco-design/web-vue/es/icon'
|
||||
import ArcoVue from '@arco-design/web-vue'
|
||||
import type { FileItem } from '@/apis'
|
||||
|
||||
import ModalContent from './ModalContent.vue'
|
||||
|
||||
function createModal<T extends { callback?: () => void }>(component: Component, options?: T) {
|
||||
// 创建一个挂载容器
|
||||
const el: HTMLElement = document.createElement('div')
|
||||
// 挂载组件
|
||||
document.body.appendChild(el)
|
||||
|
||||
// 实例化组件, createApp 第二个参数是 props
|
||||
const instance = createApp(component, {
|
||||
...options,
|
||||
onClose: () => {
|
||||
setTimeout(() => {
|
||||
instance.unmount()
|
||||
document.body.removeChild(el)
|
||||
options?.callback && options?.callback()
|
||||
}, 350)
|
||||
}
|
||||
})
|
||||
|
||||
instance.use(ArcoVue)
|
||||
instance.use(ArcoVueIcon)
|
||||
instance.mount(el)
|
||||
}
|
||||
type TFileOptions = { data: FileItem; callback?: () => void }
|
||||
|
||||
/** 预览 音频文件 弹窗 */
|
||||
let fileAudioId = ''
|
||||
export function previewFileAudioModal(data: FileItem) {
|
||||
if (fileAudioId) return // 防止重复打开
|
||||
fileAudioId = data.id
|
||||
return createModal<TFileOptions>(ModalContent, {
|
||||
data: data,
|
||||
// 关闭的回调
|
||||
callback: () => {
|
||||
fileAudioId = ''
|
||||
}
|
||||
})
|
||||
}
|
@@ -0,0 +1,48 @@
|
||||
<template>
|
||||
<a-row justify="center" align="center">
|
||||
<div style="height: 100px">
|
||||
<FileImage :data="data" style="border-radius: 5px" />
|
||||
</div>
|
||||
</a-row>
|
||||
<a-row style="margin-top: 15px">
|
||||
<a-descriptions :column="1" title="详细信息" layout="inline-vertical">
|
||||
<a-descriptions-item :label="data.name">{{ formatFileSize(data.size) }}</a-descriptions-item>
|
||||
<a-descriptions-item label="创建时间">{{ data.createTime }}</a-descriptions-item>
|
||||
<a-descriptions-item label="修改时间">{{ data.updateTime }}</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</a-row>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { FileItem } from '@/apis'
|
||||
import FileImage from '../../main/FileMain/FileImage.vue'
|
||||
import { formatFileSize } from '@/utils'
|
||||
|
||||
interface Props {
|
||||
data: FileItem
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.label {
|
||||
color: var(--color-text-2);
|
||||
}
|
||||
:deep(.arco-form-item) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
:deep(.arco-form-item-label-col > label) {
|
||||
white-space: nowrap;
|
||||
}
|
||||
:deep(.arco-descriptions-title) {
|
||||
font-size: 14px;
|
||||
}
|
||||
:deep(.arco-descriptions-item-label-inline) {
|
||||
font-size: 12px;
|
||||
}
|
||||
:deep(.arco-descriptions-item-value-inline) {
|
||||
font-size: 12px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
</style>
|
17
src/views/system/file/components/FileDetailModal/index.ts
Normal file
17
src/views/system/file/components/FileDetailModal/index.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import type { FileItem } from '@/apis'
|
||||
import { h } from 'vue'
|
||||
import { Modal } from '@arco-design/web-vue'
|
||||
import ModalContent from './ModalContent.vue'
|
||||
|
||||
/** 打开 详情 弹窗 */
|
||||
export function openFileDetailModal(fileItem: FileItem) {
|
||||
return Modal.open({
|
||||
title: fileItem.extension ? `${fileItem.name}.${fileItem.extension}` : `${fileItem.name}`,
|
||||
titleAlign: 'start',
|
||||
modalAnimationName: 'el-fade',
|
||||
modalStyle: { maxWidth: '320px' },
|
||||
width: '90%',
|
||||
footer: false,
|
||||
content: () => h(ModalContent, { data: fileItem })
|
||||
})
|
||||
}
|
@@ -0,0 +1,27 @@
|
||||
<template>
|
||||
<a-row justify="center" align="center" style="padding: 0 5%">
|
||||
<a-form ref="formRef" :model="form" auto-label-width class="w-full">
|
||||
<a-form-item
|
||||
label="文件名称"
|
||||
field="name"
|
||||
:rules="[{ required: true, message: '请输入文件名称' }]"
|
||||
style="margin-bottom: 0"
|
||||
>
|
||||
<a-input v-model="form.name" placeholder="请输入文件名称" allow-clear />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-row>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { FormInstance } from '@arco-design/web-vue'
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
const form = reactive({
|
||||
name: ''
|
||||
})
|
||||
|
||||
defineExpose({ formRef })
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
27
src/views/system/file/components/FileRenameModal/index.ts
Normal file
27
src/views/system/file/components/FileRenameModal/index.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { updateFile, type FileItem } from '@/apis'
|
||||
import { ref, h } from 'vue'
|
||||
import { Modal, Message } from '@arco-design/web-vue'
|
||||
import ModalContent from './ModalContent.vue'
|
||||
|
||||
export function openFileRenameModal(data: FileItem) {
|
||||
const ModalContentRef = ref<InstanceType<typeof ModalContent>>()
|
||||
return Modal.open({
|
||||
title: '重命名',
|
||||
modalAnimationName: 'el-fade',
|
||||
modalStyle: { maxWidth: '450px' },
|
||||
width: '90%',
|
||||
content: () =>
|
||||
h(ModalContent, {
|
||||
ref: (e) => {
|
||||
ModalContentRef.value = e as any
|
||||
}
|
||||
}),
|
||||
onBeforeOk: async () => {
|
||||
const isInvalid = await ModalContentRef.value?.formRef?.validate()
|
||||
if (isInvalid) return false
|
||||
await updateFile({ name: data.name }, data.id)
|
||||
Message.success('重命名成功')
|
||||
return true
|
||||
}
|
||||
})
|
||||
}
|
@@ -0,0 +1,26 @@
|
||||
<template>
|
||||
<div id="videoId"></div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import Player from 'xgplayer'
|
||||
import type { FileItem } from '@/apis'
|
||||
|
||||
interface Props {
|
||||
data: FileItem
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), {})
|
||||
|
||||
onMounted(() => {
|
||||
new Player({
|
||||
id: 'videoId',
|
||||
url: props.data?.url ?? '',
|
||||
lang: 'zh-cn',
|
||||
autoplay: true,
|
||||
closeVideoClick: true,
|
||||
videoInit: true
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
13
src/views/system/file/components/FileVideoModal/index.ts
Normal file
13
src/views/system/file/components/FileVideoModal/index.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { h } from 'vue'
|
||||
import { Modal } from '@arco-design/web-vue'
|
||||
import ModalContent from './ModalContent.vue'
|
||||
import type { FileItem } from '@/apis'
|
||||
|
||||
export function previewFileVideoModal(data: FileItem) {
|
||||
return Modal.open({
|
||||
title: '视频播放',
|
||||
width: 'auto',
|
||||
modalStyle: {},
|
||||
content: () => h(ModalContent, { data: data })
|
||||
})
|
||||
}
|
4
src/views/system/file/components/index.ts
Normal file
4
src/views/system/file/components/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from '../components/FileDetailModal/index'
|
||||
export * from '../components/FileRenameModal/index'
|
||||
export * from '../components/FileVideoModal/index'
|
||||
export * from '../components/FileAudioModal/index'
|
25
src/views/system/file/index.vue
Normal file
25
src/views/system/file/index.vue
Normal file
@@ -0,0 +1,25 @@
|
||||
<template>
|
||||
<a-row align="stretch" :gutter="14" class="file-manage">
|
||||
<a-col :xs="0" :sm="8" :md="7" :lg="6" :xl="5" :xxl="4" flex="220px" class="h-full ov-hidden">
|
||||
<FileAside></FileAside>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="16" :md="17" :lg="18" :xl="19" :xxl="20" flex="1" class="h-full ov-hidden">
|
||||
<FileMain></FileMain>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import FileAside from './main/FileAside.vue'
|
||||
import FileMain from './main/FileMain/index.vue'
|
||||
|
||||
defineOptions({ name: 'File' })
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.file-manage {
|
||||
flex: 1;
|
||||
padding: $margin;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
104
src/views/system/file/main/FileAside.vue
Normal file
104
src/views/system/file/main/FileAside.vue
Normal file
@@ -0,0 +1,104 @@
|
||||
<template>
|
||||
<div>
|
||||
<a-card title="文件管理" :bordered="false" :body-style="{ padding: 0 }">
|
||||
<a-menu :default-open-keys="['0']" :selected-keys="[selectedKey]">
|
||||
<a-sub-menu key="0">
|
||||
<template #icon>
|
||||
<icon-apps></icon-apps>
|
||||
</template>
|
||||
<template #title>文件类型</template>
|
||||
<a-menu-item v-for="item in FileTypeList" :key="item.value.toString()" @click="onClickItem(item)">
|
||||
<template #icon>
|
||||
<GiSvgIcon :size="28" :name="item.menuIcon"></GiSvgIcon>
|
||||
</template>
|
||||
<span>{{ item.name }}</span>
|
||||
</a-menu-item>
|
||||
</a-sub-menu>
|
||||
</a-menu>
|
||||
</a-card>
|
||||
|
||||
<section class="percent">
|
||||
<a-row justify="space-between">
|
||||
<a-statistic title="总存储量" :value="512" :value-style="{ color: '#5856D6' }">
|
||||
<template #prefix>
|
||||
<icon-cloud />
|
||||
</template>
|
||||
<template #suffix>GB</template>
|
||||
</a-statistic>
|
||||
</a-row>
|
||||
|
||||
<a-space size="mini" fill direction="vertical" :key="selectedKey" class="gi_mt">
|
||||
<a-progress
|
||||
v-for="i in filePercentList"
|
||||
:key="i.label"
|
||||
:percent="i.value"
|
||||
:stroke-width="8"
|
||||
:color="i.color"
|
||||
:animation="true"
|
||||
>
|
||||
<template #text>{{ i.label }}</template>
|
||||
</a-progress>
|
||||
</a-space>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { FileTypeList, type FileTypeListItem } from '@/constant/file'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const selectedKey = ref('0')
|
||||
const filePercentList = [
|
||||
{ label: '图片', value: 0.7, color: '#4F6BF6' },
|
||||
{ label: '文档', value: 0.3, color: '#FFA000' },
|
||||
{ label: '视频', value: 0.4, color: '#A15FDE' },
|
||||
{ label: '音频', value: 0.2, color: '#FD6112' },
|
||||
{ label: '其他', value: 0.5, color: '#52B852' }
|
||||
]
|
||||
|
||||
// 监听路由变化
|
||||
watch(
|
||||
() => route.query,
|
||||
() => {
|
||||
if (route.query.type) {
|
||||
selectedKey.value = route.query.type as string
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true
|
||||
}
|
||||
)
|
||||
|
||||
// 点击事件
|
||||
const onClickItem = (item: FileTypeListItem) => {
|
||||
router.push({ name: 'File', query: { type: item.value } })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.arco-card) {
|
||||
.arco-card-header {
|
||||
border-bottom-style: dashed;
|
||||
margin: 0 16px;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.arco-progress) {
|
||||
.arco-progress-line,
|
||||
.arco-progress-line-bar-buffer,
|
||||
.arco-progress-line-bar {
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.percent {
|
||||
margin-top: 10px;
|
||||
padding: 14px 12px;
|
||||
box-sizing: border-box;
|
||||
background-color: var(--color-bg-1);
|
||||
}
|
||||
</style>
|
170
src/views/system/file/main/FileMain/FileGrid.vue
Normal file
170
src/views/system/file/main/FileMain/FileGrid.vue
Normal file
@@ -0,0 +1,170 @@
|
||||
<template>
|
||||
<div class="file-grid">
|
||||
<a-grid :cols="{ xs: 4, sm: 4, md: 5, lg: 7, xl: 8, xxl: 9 }" :col-gap="12" :row-gap="12">
|
||||
<a-trigger
|
||||
v-for="item in data"
|
||||
:key="item.id"
|
||||
trigger="contextMenu"
|
||||
align-point
|
||||
animation-name="slide-dynamic-origin"
|
||||
auto-fit-transform-origin
|
||||
position="bl"
|
||||
update-at-scroll
|
||||
scroll-to-close
|
||||
>
|
||||
<a-grid-item>
|
||||
<div class="file-grid-item" @click.stop="handleClickFile(item)">
|
||||
<section class="file-grid-item__wrapper">
|
||||
<div class="file-icon">
|
||||
<FileImage :data="item" :title="item.name"></FileImage>
|
||||
</div>
|
||||
<p class="gi_line_1 file-name">{{ getFileName(item) }}</p>
|
||||
</section>
|
||||
<!-- 勾选模式 -->
|
||||
<section
|
||||
v-show="props.isBatchMode"
|
||||
class="check-mode"
|
||||
:class="{ checked: props.selectedFileIds.includes(item.id) }"
|
||||
@click.stop="handleCheckFile(item)"
|
||||
>
|
||||
<a-checkbox
|
||||
class="checkbox"
|
||||
:model-value="props.selectedFileIds.includes(item.id)"
|
||||
@change="handleCheckFile(item)"
|
||||
></a-checkbox>
|
||||
</section>
|
||||
</div>
|
||||
</a-grid-item>
|
||||
<template #content>
|
||||
<FileRightMenu :data="item" @click="handleRightMenuClick($event, item)"></FileRightMenu>
|
||||
</template>
|
||||
</a-trigger>
|
||||
</a-grid>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import FileImage from './FileImage.vue'
|
||||
import FileRightMenu from './FileRightMenu.vue'
|
||||
import type { FileItem } from '@/apis'
|
||||
|
||||
interface Props {
|
||||
data?: FileItem[]
|
||||
selectedFileIds?: string[]
|
||||
isBatchMode?: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
data: () => [], // 文件数据
|
||||
selectedFileIds: () => [], // 批量模式下选中的文件id数组
|
||||
isBatchMode: false // 是否是批量模式
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'click', record: FileItem): void
|
||||
(e: 'select', record: FileItem): void
|
||||
(e: 'right-menu-click', mode: string, item: FileItem): void
|
||||
}>()
|
||||
|
||||
// 文件名称带后缀
|
||||
const getFileName = (item: FileItem) => {
|
||||
return `${item.name}${item.extension ? `.${item.extension}` : ''}`
|
||||
}
|
||||
|
||||
// 点击事件
|
||||
const handleClickFile = (item: FileItem) => {
|
||||
emit('click', item)
|
||||
}
|
||||
|
||||
// 选中事件
|
||||
const handleCheckFile = (item: FileItem) => {
|
||||
emit('select', item)
|
||||
}
|
||||
|
||||
// 右键菜单点击事件
|
||||
const handleRightMenuClick = (mode: string, item: FileItem) => {
|
||||
emit('right-menu-click', mode, item)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.file-grid {
|
||||
flex: 1;
|
||||
margin-top: 12px;
|
||||
overflow: scroll;
|
||||
background: var(--color-bg-2);
|
||||
}
|
||||
|
||||
.file-grid-item {
|
||||
width: 100%;
|
||||
height: 100px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
background: var(--color-primary-light-1);
|
||||
}
|
||||
&:active {
|
||||
svg,
|
||||
img {
|
||||
transform: scale(0.9);
|
||||
}
|
||||
}
|
||||
.check-mode {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
z-index: 9;
|
||||
&.checked {
|
||||
background: none;
|
||||
}
|
||||
.checkbox {
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
left: 5px;
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
&__wrapper {
|
||||
width: 76%;
|
||||
max-width: 100px;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
.file-icon {
|
||||
width: 100%;
|
||||
height: 60px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
> img {
|
||||
width: auto;
|
||||
height: 100%;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
> svg {
|
||||
height: 100%;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
}
|
||||
.file-name {
|
||||
width: 100%;
|
||||
font-size: 12px;
|
||||
margin-top: 8px;
|
||||
padding: 0 5px;
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
41
src/views/system/file/main/FileMain/FileImage.vue
Normal file
41
src/views/system/file/main/FileMain/FileImage.vue
Normal file
@@ -0,0 +1,41 @@
|
||||
<template>
|
||||
<img v-if="isImage" class="file-image" :src="props.data.url" alt="" />
|
||||
<GiSvgIcon v-else size="100%" :name="getFileImg"></GiSvgIcon>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { FileIcon, ImageTypes } from '@/constant/file'
|
||||
import type { FileItem } from '@/apis'
|
||||
|
||||
interface Props {
|
||||
data: FileItem
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {})
|
||||
|
||||
// 是否是图片类型文件
|
||||
const isImage = computed(() => {
|
||||
const extension = props.data.extension.toLowerCase()
|
||||
return ImageTypes.includes(extension)
|
||||
})
|
||||
|
||||
// 获取文件图标,如果是图片就显示图片
|
||||
const getFileImg = computed<string>(() => {
|
||||
const extension = props.data.extension.toLowerCase()
|
||||
if (ImageTypes.includes(extension)) {
|
||||
return props.data.url || ''
|
||||
}
|
||||
if (!Object.keys(FileIcon).includes(extension)) {
|
||||
return FileIcon['other']
|
||||
}
|
||||
return FileIcon[extension]
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.file-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
</style>
|
135
src/views/system/file/main/FileMain/FileList.vue
Normal file
135
src/views/system/file/main/FileMain/FileList.vue
Normal file
@@ -0,0 +1,135 @@
|
||||
<template>
|
||||
<div class="file-list">
|
||||
<a-table
|
||||
row-key="id"
|
||||
:scroll="{ x: '100%', y: '100%', minWidth: 800 }"
|
||||
:data="props.data"
|
||||
:bordered="false"
|
||||
:pagination="false"
|
||||
:row-selection="isBatchMode ? rowSelection : undefined"
|
||||
:selected-keys="selectedFileIds"
|
||||
@select="select"
|
||||
@row-click="handleRowClick"
|
||||
>
|
||||
<template #columns>
|
||||
<a-table-column title="名称">
|
||||
<template #cell="{ record }">
|
||||
<a-trigger
|
||||
trigger="contextMenu"
|
||||
align-point
|
||||
animation-name="slide-dynamic-origin"
|
||||
auto-fit-transform-origin
|
||||
position="bl"
|
||||
update-at-scroll
|
||||
scroll-to-close
|
||||
>
|
||||
<section class="file-name">
|
||||
<div class="file-image">
|
||||
<FileImage :data="record"></FileImage>
|
||||
</div>
|
||||
<span>{{ record.name }}</span>
|
||||
</section>
|
||||
<template #content>
|
||||
<FileRightMenu :data="record" @click="handleRightMenuClick($event, record)"></FileRightMenu>
|
||||
</template>
|
||||
</a-trigger>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="文件大小" data-index="size" :width="150">
|
||||
<template #cell="{ record }">{{ formatFileSize(record.size) }}</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="扩展名" data-index="extension" :width="100">
|
||||
<template #cell="{ record }">
|
||||
<a-tag v-if="record.extension" size="small" color="purple">{{ record.extension }}</a-tag>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="修改时间" data-index="updateTime" :width="200"></a-table-column>
|
||||
<a-table-column title="操作" :width="120" align="center">
|
||||
<template #cell="{ record }">
|
||||
<a-popover trigger="click" position="bottom" :content-style="{ padding: 0, 'margin-top': 0 }">
|
||||
<a-button type="text" @click.stop><icon-more :size="16" /></a-button>
|
||||
<template #content>
|
||||
<FileRightMenu
|
||||
:file-info="record"
|
||||
:shadow="false"
|
||||
@click="handleRightMenuClick($event, record)"
|
||||
></FileRightMenu>
|
||||
</template>
|
||||
</a-popover>
|
||||
</template>
|
||||
</a-table-column>
|
||||
</template>
|
||||
</a-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { FileItem } from '@/apis'
|
||||
import type { TableRowSelection, TableInstance } from '@arco-design/web-vue'
|
||||
import FileImage from './FileImage.vue'
|
||||
import FileRightMenu from './FileRightMenu.vue'
|
||||
import { formatFileSize } from '@/utils'
|
||||
|
||||
interface Props {
|
||||
data?: FileItem[]
|
||||
selectedFileIds?: string[]
|
||||
isBatchMode?: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
data: () => [], // 文件数据
|
||||
selectedFileIds: () => [],
|
||||
isBatchMode: false // 是否是批量模式
|
||||
})
|
||||
|
||||
const rowSelection: TableRowSelection = reactive({
|
||||
type: 'checkbox',
|
||||
showCheckedAll: true
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'click', record: FileItem): void
|
||||
(e: 'select', record: FileItem): void
|
||||
(e: 'right-menu-click', mode: string, item: FileItem): void
|
||||
}>()
|
||||
|
||||
// 多选
|
||||
const select: TableInstance['onSelect'] = (rowKeys, rowKey, record) => {
|
||||
emit('select', record as unknown as FileItem)
|
||||
}
|
||||
|
||||
// 行点击事件
|
||||
const handleRowClick: TableInstance['onRowClick'] = (record) => {
|
||||
emit('click', record as unknown as FileItem)
|
||||
}
|
||||
|
||||
// 右键菜单点击事件
|
||||
const handleRightMenuClick = (mode: string, item: FileItem) => {
|
||||
emit('right-menu-click', mode, item)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.arco-table-td .arco-table-cell) {
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
.file-list {
|
||||
width: 100%;
|
||||
padding-top: 12px;
|
||||
overflow: hidden;
|
||||
.file-name {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-top: 6px;
|
||||
padding-bottom: 6px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.file-image {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
42
src/views/system/file/main/FileMain/FileRightMenu.vue
Normal file
42
src/views/system/file/main/FileMain/FileRightMenu.vue
Normal file
@@ -0,0 +1,42 @@
|
||||
<template>
|
||||
<GiOption :class="{ shadow: props.shadow }">
|
||||
<GiOptionItem label="重命名" icon="menu-edit" @click="onClickItem('rename')"> </GiOptionItem>
|
||||
<GiOptionItem label="详情" icon="menu-detail" @click="onClickItem('detail')"> </GiOptionItem>
|
||||
<GiOptionItem label="下载" icon="menu-download" @click="onClickItem('download')"></GiOptionItem>
|
||||
<GiOptionItem label="删除" icon="menu-delete" @click="onClickItem('delete')"> </GiOptionItem>
|
||||
</GiOption>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import GiOption from '@/components/GiOption/index.vue'
|
||||
import GiOptionItem from '@/components/GiOptionItem/index.vue'
|
||||
import type { FileItem } from '@/apis'
|
||||
|
||||
interface Props {
|
||||
data?: FileItem
|
||||
shadow?: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
shadow: true
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'click', mode: string): void
|
||||
}>()
|
||||
|
||||
const onClickItem = (mode: string) => {
|
||||
emit('click', mode)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.shadow {
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.12), 0 0 6px rgba(0, 0, 0, 0.04);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--color-border-2);
|
||||
box-sizing: border-box;
|
||||
background: var(--color-bg-popup);
|
||||
}
|
||||
</style>
|
261
src/views/system/file/main/FileMain/index.vue
Normal file
261
src/views/system/file/main/FileMain/index.vue
Normal file
@@ -0,0 +1,261 @@
|
||||
<template>
|
||||
<div class="file-main">
|
||||
<a-row justify="space-between" class="file-main__search">
|
||||
<!-- 左侧区域 -->
|
||||
<a-space wrap>
|
||||
<a-dropdown>
|
||||
<a-upload :show-file-list="false" :custom-request="handleUpload">
|
||||
<template #upload-button>
|
||||
<a-button type="primary" shape="round">
|
||||
<template #icon>
|
||||
<icon-upload />
|
||||
</template>
|
||||
<template #default>上传</template>
|
||||
</a-button>
|
||||
</template>
|
||||
</a-upload>
|
||||
</a-dropdown>
|
||||
|
||||
<a-input-group>
|
||||
<a-input v-model="queryForm.name" placeholder="请输入文件名" allow-clear @change="search" />
|
||||
<a-button type="primary" @click="search">
|
||||
<template #icon>
|
||||
<icon-search />
|
||||
</template>
|
||||
<template #default>查询</template>
|
||||
</a-button>
|
||||
</a-input-group>
|
||||
</a-space>
|
||||
|
||||
<!-- 右侧区域 -->
|
||||
<a-space wrap>
|
||||
<a-button
|
||||
v-if="isBatchMode"
|
||||
:disabled="!selectedFileIds.length"
|
||||
type="primary"
|
||||
status="danger"
|
||||
@click="handleMulDelete"
|
||||
>
|
||||
<template #icon>
|
||||
<icon-delete />
|
||||
</template>
|
||||
</a-button>
|
||||
<a-button type="primary" @click="isBatchMode = !isBatchMode">
|
||||
<template #icon>
|
||||
<icon-select-all />
|
||||
</template>
|
||||
<template #default>{{ isBatchMode ? '取消批量' : '批量操作' }}</template>
|
||||
</a-button>
|
||||
<a-button-group>
|
||||
<a-tooltip content="视图" position="bottom">
|
||||
<a-button @click="toggleMode">
|
||||
<template #icon>
|
||||
<icon-apps v-if="mode === 'grid'" />
|
||||
<icon-list v-else />
|
||||
</template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
</a-button-group>
|
||||
</a-space>
|
||||
</a-row>
|
||||
|
||||
<!-- 文件列表-宫格模式 -->
|
||||
<a-spin class="file-main__list" :loading="loading">
|
||||
<FileGrid
|
||||
v-show="fileList.length && mode == 'grid'"
|
||||
:data="fileList"
|
||||
:is-batch-mode="isBatchMode"
|
||||
:selected-file-ids="selectedFileIds"
|
||||
@click="handleClickFile"
|
||||
@select="handleSelectFile"
|
||||
@right-menu-click="handleRightMenuClick"
|
||||
></FileGrid>
|
||||
|
||||
<!-- 文件列表-列表模式 -->
|
||||
<FileList
|
||||
v-show="fileList.length && mode == 'list'"
|
||||
:data="fileList"
|
||||
:is-batch-mode="isBatchMode"
|
||||
:selected-file-ids="selectedFileIds"
|
||||
@click="handleClickFile"
|
||||
@select="handleSelectFile"
|
||||
@right-menu-click="handleRightMenuClick"
|
||||
></FileList>
|
||||
|
||||
<a-empty v-show="!fileList.length"></a-empty>
|
||||
</a-spin>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { listFile, uploadFile, deleteFile, type FileItem, type FileQuery } from '@/apis'
|
||||
import { Message, Modal, type RequestOption } from '@arco-design/web-vue'
|
||||
import FileGrid from './FileGrid.vue'
|
||||
import {
|
||||
openFileDetailModal,
|
||||
openFileRenameModal,
|
||||
previewFileVideoModal,
|
||||
previewFileAudioModal
|
||||
} from '../../components/index'
|
||||
import useFileManage from './useFileManage'
|
||||
import { ImageTypes } from '@/constant/file'
|
||||
import { api as viewerApi } from 'v-viewer'
|
||||
import 'viewerjs/dist/viewer.css'
|
||||
|
||||
const FileList = defineAsyncComponent(() => import('./FileList.vue'))
|
||||
|
||||
const route = useRoute()
|
||||
const { mode, selectedFileIds, toggleMode, addSelectedFileItem } = useFileManage()
|
||||
|
||||
const queryForm = reactive({
|
||||
name: undefined,
|
||||
type: route.query.type?.toString() || undefined,
|
||||
sort: ['updateTime,desc']
|
||||
})
|
||||
|
||||
const fileList = ref<FileItem[]>([])
|
||||
const isBatchMode = ref(false)
|
||||
const loading = ref(false)
|
||||
// 查询文件列表
|
||||
const getFileList = async (query: FileQuery = { ...queryForm, page: 1, size: 50 }) => {
|
||||
try {
|
||||
loading.value = true
|
||||
isBatchMode.value = false
|
||||
query.type = query.type === '0' ? undefined : query.type
|
||||
const res = await listFile(query)
|
||||
fileList.value = res.data
|
||||
} catch (error) {
|
||||
return error
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 点击文件
|
||||
const handleClickFile = (item: FileItem) => {
|
||||
if (ImageTypes.includes(item.extension)) {
|
||||
if (item.url) {
|
||||
const imgList: string[] = fileList.value.filter((i) => ImageTypes.includes(i.extension)).map((a) => a.url || '')
|
||||
const index = imgList.findIndex((i) => i === item.url)
|
||||
if (imgList.length) {
|
||||
viewerApi({
|
||||
options: {
|
||||
initialViewIndex: index
|
||||
},
|
||||
images: imgList
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
if (item.extension === 'mp4') {
|
||||
previewFileVideoModal(item)
|
||||
}
|
||||
if (item.extension === 'mp3') {
|
||||
previewFileAudioModal(item)
|
||||
}
|
||||
}
|
||||
|
||||
// 右键菜单
|
||||
const handleRightMenuClick = (mode: string, fileInfo: FileItem) => {
|
||||
if (mode === 'delete') {
|
||||
Modal.warning({
|
||||
title: '提示',
|
||||
content: `是否确定删除文件 [${fileInfo.name}]?`,
|
||||
hideCancel: false,
|
||||
okButtonProps: { status: 'danger' },
|
||||
onOk: async () => {
|
||||
await deleteFile(fileInfo.id)
|
||||
Message.success('删除成功')
|
||||
search()
|
||||
}
|
||||
})
|
||||
} else if (mode === 'rename') {
|
||||
openFileRenameModal(fileInfo)
|
||||
} else if (mode === 'detail') {
|
||||
openFileDetailModal(fileInfo)
|
||||
}
|
||||
}
|
||||
|
||||
// 勾选文件
|
||||
const handleSelectFile = (item: FileItem) => {
|
||||
addSelectedFileItem(item)
|
||||
}
|
||||
|
||||
// 批量删除
|
||||
const handleMulDelete = () => {
|
||||
Modal.warning({
|
||||
title: '提示',
|
||||
content: `是否确定删除所选的${selectedFileIds.value.length}个文件?`,
|
||||
hideCancel: false,
|
||||
onOk: () => {
|
||||
deleteFile(selectedFileIds.value)
|
||||
Message.success('删除成功')
|
||||
search()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 上传
|
||||
const handleUpload = (options: RequestOption) => {
|
||||
const controller = new AbortController()
|
||||
;(async function requestWrap() {
|
||||
const { onProgress, onError, onSuccess, fileItem, name = 'file' } = options
|
||||
onProgress(20)
|
||||
const formData = new FormData()
|
||||
formData.append(name as string, fileItem.file as Blob)
|
||||
try {
|
||||
const res = uploadFile(formData)
|
||||
Message.success('上传成功')
|
||||
onSuccess(res)
|
||||
search()
|
||||
} catch (error) {
|
||||
onError(error)
|
||||
}
|
||||
})()
|
||||
return {
|
||||
abort() {
|
||||
controller.abort()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 查询
|
||||
const search = () => {
|
||||
getFileList()
|
||||
}
|
||||
|
||||
onBeforeRouteUpdate((to) => {
|
||||
if (!to.query.type) return
|
||||
queryForm.type = to.query.type?.toString()
|
||||
search()
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
search()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.file-main {
|
||||
height: 100%;
|
||||
background: var(--color-bg-1);
|
||||
border-radius: $radius-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
|
||||
&__search {
|
||||
border-bottom: 1px dashed var(--color-border-3);
|
||||
margin: 16px $padding 0;
|
||||
}
|
||||
|
||||
&__list {
|
||||
flex: 1;
|
||||
padding: 0 $padding $padding;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
</style>
|
33
src/views/system/file/main/FileMain/useFileManage.ts
Normal file
33
src/views/system/file/main/FileMain/useFileManage.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { ref, computed } from 'vue'
|
||||
import type { FileItem } from '@/apis'
|
||||
|
||||
type Mode = 'grid' | 'list'
|
||||
|
||||
export default function () {
|
||||
const mode = ref<Mode>('grid')
|
||||
const selectedFileList = ref<FileItem[]>([]) // 已选的文件列表
|
||||
const selectedFileIds = computed(() => selectedFileList.value.map((i) => i.id))
|
||||
|
||||
// 切换视图
|
||||
const toggleMode = () => {
|
||||
mode.value = mode.value === 'grid' ? 'list' : 'grid'
|
||||
}
|
||||
|
||||
// 添加选择的文件
|
||||
const addSelectedFileItem = (item: FileItem) => {
|
||||
if (selectedFileIds.value.includes(item.id)) {
|
||||
const index = selectedFileList.value.findIndex((i) => i.id === item.id)
|
||||
selectedFileList.value.splice(index, 1)
|
||||
} else {
|
||||
selectedFileList.value.push(item)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
mode,
|
||||
selectedFileList,
|
||||
selectedFileIds,
|
||||
toggleMode,
|
||||
addSelectedFileItem
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user