feat: 优化上传组件样式
parent
751102f4ad
commit
2a55bc0fcc
|
|
@ -1,6 +1,14 @@
|
||||||
<template>
|
<template>
|
||||||
<a-modal v-model:visible="modal.visible" title="上传文件" title-align="start" :width="732">
|
<a-modal
|
||||||
<!-- <a-alert class="mb-2"> 提示:支持大小在 1G 以内,格式为.png、.jpg、.webp、.mp4、.ogg的文件。 </a-alert> -->
|
v-model:visible="visible"
|
||||||
|
title="上传文件"
|
||||||
|
title-align="start"
|
||||||
|
:width="820"
|
||||||
|
:mask-closable="false"
|
||||||
|
:on-before-cancel="onBeforeCancel"
|
||||||
|
@close="onClose"
|
||||||
|
>
|
||||||
|
<div class="mb-2 flex items-center gap-4">
|
||||||
<a-upload
|
<a-upload
|
||||||
ref="uploadRef"
|
ref="uploadRef"
|
||||||
class="upload"
|
class="upload"
|
||||||
|
|
@ -8,93 +16,218 @@
|
||||||
:multiple="true"
|
:multiple="true"
|
||||||
:custom-request="upload"
|
:custom-request="upload"
|
||||||
:auto-upload="false"
|
:auto-upload="false"
|
||||||
|
:show-file-list="false"
|
||||||
|
@success="onUploadSuccess"
|
||||||
>
|
>
|
||||||
<template #upload-button>
|
<template #upload-button>
|
||||||
<div class="mb-2 flex items-center gap-2">
|
<a-button type="outline"> 选择文件 </a-button>
|
||||||
<div class="flex-1 flex gap-4">
|
</template>
|
||||||
<a-button type="outline"> 选择文件... </a-button>
|
</a-upload>
|
||||||
<span class="flex items-center text-gray-400" @click.prevent.stop>
|
<div class="flex-1 flex items-center text-gray-400">
|
||||||
归类为:
|
归类为:
|
||||||
<span>
|
<span>
|
||||||
<a-select v-model="group" :bordered="false" :options="groupOptions"></a-select>
|
<a-select v-model="group" :bordered="false" :options="groupOptions"></a-select>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
|
||||||
<template #upload-item="{ fileItem }">
|
<ul v-if="fileList.length" class="h-[400px] divide-y overflow-hidden p-0 m-0">
|
||||||
<li :key="fileItem.uid" class="flex items-center gap-2 border-b py-3">
|
<li v-for="item in fileList" :key="item.uid" class="flex items-center gap-2 py-3">
|
||||||
<div class="flex-1">
|
<div class="flex-1 overflow-hidden">
|
||||||
<div class="truncate">
|
<div class="truncate text-slate-900">
|
||||||
{{ fileItem.name }}
|
{{ item.name }}
|
||||||
</div>
|
</div>
|
||||||
<a-progress :percent="Math.floor(fileItem.percent * 100) / 100" :show-text="false"></a-progress>
|
<div class="flex items-center justify-between gap-2 text-gray-400 mb-[-4px]">
|
||||||
<div class="flex items-center justify-between gap-2 text-gray-400">
|
<span class="text-xs text-gray-400">
|
||||||
<span class="text-xs">
|
{{ numeral(item.file?.size).format("0 b") }}
|
||||||
{{ numeral(fileItem.file.size).format("0.00 b") }}
|
|
||||||
</span>
|
</span>
|
||||||
<span class="text-xs">
|
<span class="text-xs">
|
||||||
<span v-if="fileItem.status === 'uploading'"> {{ fileItem.percent * 100 }}% </span>
|
<span v-if="item.status === 'init'"> </span>
|
||||||
<span v-if="fileItem.status === 'done'" class="text-green-500">
|
<span v-else-if="item.status === 'uploading'">
|
||||||
<i class="icon-park-outline-check-one"></i>
|
<span class="text-xs">
|
||||||
完成
|
{{ Math.floor((item.percent || 0) * 100) }}%(
|
||||||
|
{{ numeral(itemMap.get(item.uid)?.speed || 0).format("0 b") }}/s )
|
||||||
</span>
|
</span>
|
||||||
<span v-if="fileItem.status === 'error'" class="text-red-500">
|
|
||||||
<i class="icon-park-outline-close-one"></i>
|
|
||||||
失败
|
|
||||||
</span>
|
</span>
|
||||||
|
<span v-else-if="item.status === 'done'" class="text-green-600"> 完成 </span>
|
||||||
|
<span v-else="item.status === 'error'" class="text-red-500"> 失败 </span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<a-progress :percent="Math.floor((item.percent || 0) * 100) / 100" :show-text="false"></a-progress>
|
||||||
</div>
|
</div>
|
||||||
<div v-show="fileItem.status !== 'done'">
|
<div v-show="item.status !== 'done'">
|
||||||
<a-link v-show="fileItem.status === 'uploading'" @click="pauseItem(fileItem)">停止</a-link>
|
<a-link v-show="item.status === 'uploading'" @click="pauseItem(item)">停止</a-link>
|
||||||
<a-link v-show="fileItem.status === 'init'" @click="removeItem(fileItem)" status="danger">删除</a-link>
|
<a-link v-show="item.status === 'error'" @click="retryItem(item)">重试</a-link>
|
||||||
|
<a-link v-show="item.status === 'init' || item.status === 'error'" @click="removeItem(item)">删除</a-link>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</template>
|
</ul>
|
||||||
</a-upload>
|
|
||||||
<!-- <div v-show="!fileList.length" class="h-[426px]">
|
<div v-else class="h-[400px] flex items-center justify-center">
|
||||||
<a-empty></a-empty>
|
<a-empty description="选择文件后显示"></a-empty>
|
||||||
</div> -->
|
</div>
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<div class="flex justify-end gap-2 items-center">
|
<div class="flex justify-between gap-2 items-center">
|
||||||
<a-button>
|
<div class="text-gray-400 text-xs">已上传 {{ successCount }} 项</div>
|
||||||
清空
|
<div class="space-x-2">
|
||||||
|
<a-button
|
||||||
|
type="text"
|
||||||
|
:disabled="!fileList.length || !fileList.some((i) => i.status === 'done')"
|
||||||
|
@click="clearUploaded"
|
||||||
|
>
|
||||||
|
清空已上传
|
||||||
</a-button>
|
</a-button>
|
||||||
<a-button type="primary" @click="startUpload">
|
<a-button
|
||||||
上传
|
type="primary"
|
||||||
|
:disabled="!fileList.length || !fileList.some((i) => i.status === 'init')"
|
||||||
|
@click="startUpload"
|
||||||
|
>
|
||||||
|
开始上传
|
||||||
</a-button>
|
</a-button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</a-modal>
|
</a-modal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { api } from "@/api";
|
import { RequestParams, api } from "@/api";
|
||||||
import { FileItem, RequestOption, UploadInstance } from "@arco-design/web-vue";
|
import { FileItem, Message, RequestOption, UploadInstance } from "@arco-design/web-vue";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import numeral from "numeral";
|
import numeral from "numeral";
|
||||||
|
|
||||||
|
const emit = defineEmits(["success"]);
|
||||||
|
const visible = ref(false);
|
||||||
const uploadRef = ref<UploadInstance | null>(null);
|
const uploadRef = ref<UploadInstance | null>(null);
|
||||||
|
const successCount = ref(0);
|
||||||
|
const fileList = ref<FileItem[]>([]);
|
||||||
|
const itemMap = reactive<Map<string, { lastTime: number; lastLoaded: number; speed: number } | null>>(new Map());
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开始上传
|
||||||
|
*/
|
||||||
const startUpload = () => {
|
const startUpload = () => {
|
||||||
uploadRef.value?.submit();
|
uploadRef.value?.submit();
|
||||||
};
|
};
|
||||||
const pauseItem = (fileItem: FileItem) => {
|
|
||||||
fileItem.status;
|
/**
|
||||||
uploadRef.value?.abort(fileItem);
|
* 中止上传
|
||||||
|
* @param item 文件
|
||||||
|
*/
|
||||||
|
const pauseItem = (item: FileItem) => {
|
||||||
|
uploadRef.value?.abort(item);
|
||||||
};
|
};
|
||||||
const removeItem = (fileItem: FileItem) => {
|
|
||||||
const index = fileList.value.findIndex((i) => i.uid === fileItem.uid);
|
/**
|
||||||
console.log(fileItem, index);
|
* 移除文件
|
||||||
|
* @param item 文件
|
||||||
|
*/
|
||||||
|
const removeItem = (item: FileItem) => {
|
||||||
|
const index = fileList.value.findIndex((i) => i.uid === item.uid);
|
||||||
if (index > -1) {
|
if (index > -1) {
|
||||||
fileList.value.splice(index, 1);
|
fileList.value.splice(index, 1);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const fileList = ref<FileItem[]>([]);
|
/**
|
||||||
|
* 重新上传
|
||||||
|
* @param item 文件
|
||||||
|
*/
|
||||||
|
const retryItem = (item: FileItem) => {
|
||||||
|
uploadRef.value?.submit(item);
|
||||||
|
};
|
||||||
|
|
||||||
const modal = ref({
|
/**
|
||||||
visible: false,
|
* 清空已上传
|
||||||
|
*/
|
||||||
|
const clearUploaded = () => {
|
||||||
|
fileList.value = fileList.value.filter((i) => i.status !== "done");
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上传成功后处理
|
||||||
|
* @param item 文件
|
||||||
|
*/
|
||||||
|
const onUploadSuccess = (item: FileItem) => {
|
||||||
|
successCount.value += 1;
|
||||||
|
emit("success", item);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 关闭前检测
|
||||||
|
*/
|
||||||
|
const onBeforeCancel = () => {
|
||||||
|
if (fileList.value.some((i) => i.status === "uploading")) {
|
||||||
|
Message.warning("提示:文件上传中,请稍后再试!");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 关闭后处理
|
||||||
|
*/
|
||||||
|
const onClose = () => {
|
||||||
|
fileList.value = [];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自定义上传逻辑
|
||||||
|
* @param option
|
||||||
|
*/
|
||||||
|
const upload = (option: RequestOption) => {
|
||||||
|
const { fileItem, onError, onProgress, onSuccess } = option;
|
||||||
|
const source = axios.CancelToken.source();
|
||||||
|
if (!itemMap.has(fileItem.uid)) {
|
||||||
|
itemMap.set(fileItem.uid, {
|
||||||
|
lastTime: Date.now(),
|
||||||
|
lastLoaded: 0,
|
||||||
|
speed: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const item = itemMap.get(fileItem.uid)!;
|
||||||
|
const up = async () => {
|
||||||
|
const data = { file: fileItem.file as any };
|
||||||
|
const params: RequestParams = {
|
||||||
|
onUploadProgress(e) {
|
||||||
|
let percent = 0;
|
||||||
|
const { lastTime, lastLoaded } = item;
|
||||||
|
if (e.total && e.total > 0) {
|
||||||
|
percent = e.loaded / e.total;
|
||||||
|
const nowTime = Date.now();
|
||||||
|
const diff = (e.loaded - lastLoaded) / (nowTime - lastTime);
|
||||||
|
const speed = Math.floor(diff * 1000);
|
||||||
|
item.speed = speed;
|
||||||
|
item.lastLoaded = e.loaded;
|
||||||
|
item.lastTime = nowTime;
|
||||||
|
}
|
||||||
|
onProgress(percent, e as any);
|
||||||
|
},
|
||||||
|
cancelToken: source.token,
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
const res = await api.file.addFile(data, params);
|
||||||
|
itemMap.delete(fileItem.uid);
|
||||||
|
onSuccess(res);
|
||||||
|
} catch (e) {
|
||||||
|
onError(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (fileItem.file) {
|
||||||
|
up();
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
abort() {
|
||||||
|
source.cancel();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
open: () => {
|
||||||
|
visible.value = true;
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const group = ref("default");
|
const group = ref("default");
|
||||||
|
|
@ -108,51 +241,6 @@ const groupOptions = [
|
||||||
value: "video",
|
value: "video",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const upload = (option: RequestOption) => {
|
|
||||||
const { fileItem, onError, onProgress, onSuccess } = option;
|
|
||||||
const source = axios.CancelToken.source();
|
|
||||||
if (fileItem.file) {
|
|
||||||
api.file
|
|
||||||
.addFile(
|
|
||||||
{
|
|
||||||
file: fileItem.file as any,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
onUploadProgress(e) {
|
|
||||||
let percent = 0;
|
|
||||||
if (e.total && e.total > 0) {
|
|
||||||
percent = e.loaded / e.total;
|
|
||||||
}
|
|
||||||
onProgress(percent, e as any);
|
|
||||||
},
|
|
||||||
cancelToken: source.token,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.then((res) => {
|
|
||||||
onSuccess(res);
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
onError(e);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
abort() {
|
|
||||||
source.cancel();
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
defineExpose({
|
|
||||||
open: () => {
|
|
||||||
modal.value.visible = true;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped></style>
|
||||||
.upload :deep(.arco-upload-list) {
|
|
||||||
height: 426px;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,9 @@
|
||||||
</template>
|
</template>
|
||||||
上传
|
上传
|
||||||
</a-button>
|
</a-button>
|
||||||
|
<a-button type="outline" status="danger" :disabled="!selected.length" @click="onDeleteMany">
|
||||||
|
批量删除
|
||||||
|
</a-button>
|
||||||
<ani-upload ref="uploadRef"></ani-upload>
|
<ani-upload ref="uploadRef"></ani-upload>
|
||||||
</template>
|
</template>
|
||||||
</Table>
|
</Table>
|
||||||
|
|
@ -26,12 +29,13 @@ import { Table, createColumn, updateColumn, useTable } from "@/components";
|
||||||
import numeral from "numeral";
|
import numeral from "numeral";
|
||||||
import AniGroup from "./components/group.vue";
|
import AniGroup from "./components/group.vue";
|
||||||
import AniUpload from "./components/upload.vue";
|
import AniUpload from "./components/upload.vue";
|
||||||
|
import { delConfirm } from "@/utils";
|
||||||
|
|
||||||
const visible = ref(false);
|
const visible = ref(false);
|
||||||
const image = ref("");
|
const image = ref("");
|
||||||
|
const selected = ref<number[]>([]);
|
||||||
const preview = (record: any) => {
|
const preview = (record: any) => {
|
||||||
if (!record.mimetype.startsWith("image")) {
|
if (!record.mimetype.startsWith("image")) {
|
||||||
// Message.warning("暂不支持预览该素材");
|
|
||||||
window.open(record.path, "_blank");
|
window.open(record.path, "_blank");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -39,6 +43,10 @@ const preview = (record: any) => {
|
||||||
visible.value = true;
|
visible.value = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onDeleteMany = async () => {
|
||||||
|
await delConfirm();
|
||||||
|
};
|
||||||
|
|
||||||
const uploadRef = ref<InstanceType<typeof AniUpload>>();
|
const uploadRef = ref<InstanceType<typeof AniUpload>>();
|
||||||
|
|
||||||
const getIcon = (mimetype: string) => {
|
const getIcon = (mimetype: string) => {
|
||||||
|
|
@ -61,6 +69,14 @@ const table = useTable({
|
||||||
data: async (model, paging) => {
|
data: async (model, paging) => {
|
||||||
return api.file.getFiles();
|
return api.file.getFiles();
|
||||||
},
|
},
|
||||||
|
tableProps: {
|
||||||
|
rowSelection: {
|
||||||
|
showCheckedAll: true,
|
||||||
|
},
|
||||||
|
onSelectionChange(rowKeys) {
|
||||||
|
selected.value = rowKeys as number[];
|
||||||
|
},
|
||||||
|
},
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
title: "文件名称",
|
title: "文件名称",
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,12 @@
|
||||||
body {
|
body {
|
||||||
// --border-radius-small: 4px;
|
// --border-radius-small: 4px;
|
||||||
|
|
||||||
|
.arco-icon-hover::before {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border-radius: var(--border-radius-small);
|
||||||
|
}
|
||||||
|
|
||||||
div.arco-dropdown {
|
div.arco-dropdown {
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,6 @@ declare module '@vue/runtime-core' {
|
||||||
ACheckbox: typeof import('@arco-design/web-vue')['Checkbox']
|
ACheckbox: typeof import('@arco-design/web-vue')['Checkbox']
|
||||||
ACheckboxGroup: typeof import('@arco-design/web-vue')['CheckboxGroup']
|
ACheckboxGroup: typeof import('@arco-design/web-vue')['CheckboxGroup']
|
||||||
AConfigProvider: typeof import('@arco-design/web-vue')['ConfigProvider']
|
AConfigProvider: typeof import('@arco-design/web-vue')['ConfigProvider']
|
||||||
ADatePicker: typeof import('@arco-design/web-vue')['DatePicker']
|
|
||||||
ADivider: typeof import('@arco-design/web-vue')['Divider']
|
ADivider: typeof import('@arco-design/web-vue')['Divider']
|
||||||
ADoption: typeof import('@arco-design/web-vue')['Doption']
|
ADoption: typeof import('@arco-design/web-vue')['Doption']
|
||||||
ADrawer: typeof import('@arco-design/web-vue')['Drawer']
|
ADrawer: typeof import('@arco-design/web-vue')['Drawer']
|
||||||
|
|
@ -50,10 +49,8 @@ declare module '@vue/runtime-core' {
|
||||||
ARadioGroup: typeof import('@arco-design/web-vue')['RadioGroup']
|
ARadioGroup: typeof import('@arco-design/web-vue')['RadioGroup']
|
||||||
AScrollbar: typeof import('@arco-design/web-vue')['Scrollbar']
|
AScrollbar: typeof import('@arco-design/web-vue')['Scrollbar']
|
||||||
ASelect: typeof import('@arco-design/web-vue')['Select']
|
ASelect: typeof import('@arco-design/web-vue')['Select']
|
||||||
ASlider: typeof import('@arco-design/web-vue')['Slider']
|
|
||||||
ASpace: typeof import('@arco-design/web-vue')['Space']
|
ASpace: typeof import('@arco-design/web-vue')['Space']
|
||||||
ASpin: typeof import('@arco-design/web-vue')['Spin']
|
ASpin: typeof import('@arco-design/web-vue')['Spin']
|
||||||
ASwitch: typeof import('@arco-design/web-vue')['Switch']
|
|
||||||
ATable: typeof import('@arco-design/web-vue')['Table']
|
ATable: typeof import('@arco-design/web-vue')['Table']
|
||||||
ATableColumn: typeof import('@arco-design/web-vue')['TableColumn']
|
ATableColumn: typeof import('@arco-design/web-vue')['TableColumn']
|
||||||
ATabPane: typeof import('@arco-design/web-vue')['TabPane']
|
ATabPane: typeof import('@arco-design/web-vue')['TabPane']
|
||||||
|
|
|
||||||
|
|
@ -20,11 +20,10 @@ const delConfirm = (config: DelOptions = {}) => {
|
||||||
if (typeof config === "string") {
|
if (typeof config === "string") {
|
||||||
config = { content: config };
|
config = { content: config };
|
||||||
}
|
}
|
||||||
const merged = merge(delOptions, config);
|
const merged = merge({}, delOptions, config);
|
||||||
return new Promise<void>((onOk: () => void, onCancel) => {
|
return new Promise<void>((onOk: () => void, onCancel) => {
|
||||||
Modal.open({ ...merged, onOk, onCancel });
|
Modal.open({ ...merged, onOk, onCancel });
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export { delConfirm };
|
export { delConfirm };
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue