yumi-admin/apps/src/views/tools/upload-file.vue
2026-04-11 00:38:52 +08:00

342 lines
7.6 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script lang="ts" setup>
import { ref } from 'vue';
import { Page } from '@vben/common-ui';
import {
OSS_FILE_BUCKETS,
getAccessImgUrl,
randomFilename,
simpleUploadFile,
} from '#/api/legacy/oss';
import { Button, Card, Empty, Space, Spin, Switch, message } from 'antdv-next';
defineOptions({ name: 'ToolsUploadFile' });
interface UploadResultItem {
loading: boolean;
name: string;
showSourceName: boolean;
sourceName: string;
status: number;
url: string;
}
const fileInputRef = ref<HTMLInputElement | null>(null);
const startCustomFilename = ref(true);
const dragging = ref(false);
const fileList = ref<UploadResultItem[]>([]);
const apkMaxSize = 100;
const otherMaxSize = 5;
const accept = 'image/*,.svga,.svg,.apk';
function getFileDir(fileName?: string) {
const name = String(fileName || '').toLowerCase();
if (!name) {
return OSS_FILE_BUCKETS.back;
}
if (name.endsWith('.svga')) {
return OSS_FILE_BUCKETS.svgaCover;
}
if (name.endsWith('.apk')) {
return OSS_FILE_BUCKETS.apk;
}
return OSS_FILE_BUCKETS.back;
}
function validOverflowM(fileSize: number, targetSize: number) {
return fileSize / 1024 / 1024 < targetSize;
}
function beforeUpload(file: File) {
const maxSize = file.name.toLowerCase().endsWith('.apk')
? apkMaxSize
: otherMaxSize;
const fileSizeM = validOverflowM(file.size, maxSize);
if (!fileSizeM) {
message.error(`${file.name} 超出 ${maxSize}MB`);
return false;
}
return true;
}
async function copyText(text?: string) {
const value = String(text || '');
if (!value) {
message.warning('暂无可复制内容');
return;
}
try {
await navigator.clipboard.writeText(value);
message.success('复制成功');
} catch {
const textarea = document.createElement('textarea');
textarea.value = value;
textarea.setAttribute('readonly', 'readonly');
textarea.style.left = '-9999px';
textarea.style.position = 'fixed';
document.body.append(textarea);
textarea.select();
document.execCommand('copy');
textarea.remove();
message.success('复制成功');
}
}
function openFileDialog() {
fileInputRef.value?.click();
}
function handleFileChange(event: Event) {
const target = event.target as HTMLInputElement;
const files = target.files ? [...target.files] : [];
processFiles(files);
target.value = '';
}
function handleDrop(event: DragEvent) {
dragging.value = false;
const files = event.dataTransfer?.files ? [...event.dataTransfer.files] : [];
processFiles(files);
}
function processFiles(files: File[]) {
files.forEach((file) => {
if (!beforeUpload(file)) {
return;
}
void uploadFile(file);
});
}
async function uploadFile(file: File) {
const dir = getFileDir(file.name);
const customFilename = startCustomFilename.value
? file.name
: randomFilename(file.name);
const item: UploadResultItem = {
loading: true,
name: `${dir}/${customFilename}`,
showSourceName: !startCustomFilename.value,
sourceName: file.name,
status: 200,
url: '',
};
fileList.value.push(item);
try {
const result = await simpleUploadFile(file, dir, customFilename);
item.status = 200;
item.name = result.name || item.name;
item.url = getAccessImgUrl(result.name || item.name);
} catch (error) {
console.error(error);
item.status = 500;
} finally {
item.loading = false;
}
}
</script>
<template>
<Page title="文件上传">
<Card>
<div class="toolbar">
<Space align="center" size="middle">
<span class="switch-label">文件命名</span>
<Switch
v-model:checked="startCustomFilename"
checked-children="原文件名"
un-checked-children="系统创建"
/>
</Space>
</div>
<input
ref="fileInputRef"
:accept="accept"
class="hidden-input"
multiple
type="file"
@change="handleFileChange"
/>
<div
class="dropzone"
:class="{ 'dropzone-active': dragging }"
@click="openFileDialog"
@dragenter.prevent="dragging = true"
@dragover.prevent="dragging = true"
@dragleave.prevent="dragging = false"
@drop.prevent="handleDrop"
>
<div class="dropzone-icon">+</div>
<div class="dropzone-title">将文件拖到此处或点击上传</div>
<div class="dropzone-tip">
只支持 imgsvgasvgapkapk 不可超过 {{ apkMaxSize }}M其他文件不可超过
{{ otherMaxSize }}M
</div>
<Button type="primary">选择文件</Button>
</div>
<div class="result-list">
<Empty v-if="fileList.length === 0" description="暂无上传记录" />
<div v-for="(item, index) in fileList" :key="`${item.name}-${index}`" class="result-item">
<div class="result-main">
<div class="result-name">{{ item.name }}</div>
<div v-if="item.showSourceName" class="result-source">{{ item.sourceName }}</div>
</div>
<div class="result-actions">
<Spin v-if="item.loading" size="small" />
<template v-else>
<span v-if="item.status === 200" class="status-success">成功</span>
<span v-else class="status-failed">失败</span>
<Button size="small" type="link" @click="copyText(item.url)">复制链接</Button>
<a v-if="item.url" :href="item.url" class="result-link" rel="noreferrer" target="_blank">
查看文件
</a>
</template>
</div>
</div>
</div>
</Card>
</Page>
</template>
<style scoped>
.toolbar {
margin-bottom: 16px;
}
.switch-label {
color: #475569;
font-size: 14px;
}
.hidden-input {
display: none;
}
.dropzone {
align-items: center;
background:
linear-gradient(180deg, rgb(37 99 235 / 4%), rgb(37 99 235 / 1%)),
#f8fbff;
border: 1px dashed rgb(59 130 246 / 35%);
border-radius: 20px;
cursor: pointer;
display: flex;
flex-direction: column;
gap: 12px;
justify-content: center;
min-height: 260px;
padding: 32px 24px;
text-align: center;
transition:
border-color 0.2s ease,
box-shadow 0.2s ease,
transform 0.2s ease;
}
.dropzone:hover,
.dropzone-active {
border-color: rgb(37 99 235 / 55%);
box-shadow: 0 16px 40px rgb(37 99 235 / 10%);
transform: translateY(-2px);
}
.dropzone-icon {
align-items: center;
background: rgb(37 99 235 / 10%);
border-radius: 999px;
color: #2563eb;
display: flex;
font-size: 28px;
font-weight: 500;
height: 64px;
justify-content: center;
width: 64px;
}
.dropzone-title {
color: #0f172a;
font-size: 18px;
font-weight: 600;
}
.dropzone-tip {
color: #64748b;
line-height: 1.7;
max-width: 620px;
}
.result-list {
margin-top: 20px;
}
.result-item {
align-items: center;
background: #fff;
border: 1px solid rgb(148 163 184 / 18%);
border-radius: 16px;
display: flex;
gap: 16px;
justify-content: space-between;
margin-bottom: 12px;
padding: 16px 18px;
}
.result-main {
min-width: 0;
}
.result-name {
color: #0f172a;
font-weight: 500;
overflow-wrap: anywhere;
}
.result-source {
color: #94a3b8;
font-size: 12px;
margin-top: 6px;
}
.result-actions {
align-items: center;
display: flex;
flex-shrink: 0;
gap: 8px;
}
.status-success {
color: #16a34a;
font-size: 13px;
font-weight: 500;
}
.status-failed {
color: #dc2626;
font-size: 13px;
font-weight: 500;
}
.result-link {
color: #1677ff;
font-size: 14px;
}
@media (width <= 768px) {
.result-item {
align-items: flex-start;
flex-direction: column;
}
.result-actions {
flex-wrap: wrap;
}
}
</style>