feat: 优化上传组件样式

master
luoer 2023-10-31 17:30:01 +08:00
parent 751102f4ad
commit 2a55bc0fcc
5 changed files with 229 additions and 123 deletions

View File

@ -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>

View File

@ -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: "文件名称",

View File

@ -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;
} }

View File

@ -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']

View File

@ -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 };