This commit is contained in:
2026-06-24 10:04:03 +08:00
parent b103192fac
commit 0f961789dc
538 changed files with 128210 additions and 128008 deletions
@@ -1,155 +1,155 @@
<template>
<el-dialog
v-model="visible"
title="创建文件分组"
width="400px"
@close="handleCancel"
>
<el-form
:model="form"
:rules="rules"
ref="formRef"
label-width="100px"
>
<el-form-item label="分组名称" prop="name">
<el-input
v-model="form.name"
placeholder="请输入文件分组名称"
maxlength="50"
show-word-limit
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="handleCancel">取消</el-button>
<el-button type="primary" @click="handleSubmit" :loading="loading">
确定
</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, watch } from "vue";
import { ElMessage } from "element-plus";
import { createFileCate } from "@/api/file";
const props = defineProps({
modelValue: {
type: Boolean,
default: false,
},
});
const emit = defineEmits(["update:modelValue", "success", "close"]);
const visible = ref(false);
const formRef = ref<any>(null);
const loading = ref(false);
const form = ref({
name: "",
});
// 表单验证规则
const rules = {
name: [
{ required: true, message: "请输入文件分组名称", trigger: "blur" },
{ min: 1, max: 50, message: "名称长度在 1 到 50 个字符", trigger: "blur" },
],
};
// 监听 modelValue 变化
watch(
() => props.modelValue,
(newVal) => {
visible.value = newVal;
if (newVal) {
// 打开对话框时,清空表单数据
form.value.name = "";
// 清除表单验证
if (formRef.value) {
formRef.value.clearValidate();
}
}
}
);
// 监听 visible 变化
watch(visible, (newVal) => {
if (!newVal) {
emit("update:modelValue", false);
}
});
// 取消
const handleCancel = () => {
visible.value = false;
form.value.name = "";
if (formRef.value) {
formRef.value.clearValidate();
}
emit("close");
};
// 提交
const handleSubmit = async () => {
if (!formRef.value) {
return;
}
// 表单验证
try {
await formRef.value.validate();
} catch (error) {
ElMessage.warning("请检查表单填写是否正确");
return;
}
// 验证名称是否为空
if (!form.value.name || form.value.name.trim() === "") {
ElMessage.error("文件分组名称不能为空");
return;
}
loading.value = true;
try {
const res = await createFileCate({
name: form.value.name.trim(),
});
if (res.code === 200) {
ElMessage.success("创建成功");
visible.value = false;
form.value.name = "";
emit("success");
emit("close");
} else {
ElMessage.error(res.msg || "创建失败");
}
} catch (error: any) {
const errorMsg =
error?.response?.data?.message || error?.message || "创建失败";
ElMessage.error(errorMsg);
} finally {
loading.value = false;
}
};
// 暴露方法给父组件
defineExpose({
open: () => {
form.value.name = "";
visible.value = true;
if (formRef.value) {
formRef.value.clearValidate();
}
},
});
</script>
<style scoped>
.create-category-dialog {
width: 400px;
}
</style>
<template>
<el-dialog
v-model="visible"
title="创建文件分组"
width="400px"
@close="handleCancel"
>
<el-form
:model="form"
:rules="rules"
ref="formRef"
label-width="100px"
>
<el-form-item label="分组名称" prop="name">
<el-input
v-model="form.name"
placeholder="请输入文件分组名称"
maxlength="50"
show-word-limit
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="handleCancel">取消</el-button>
<el-button type="primary" @click="handleSubmit" :loading="loading">
确定
</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, watch } from "vue";
import { ElMessage } from "element-plus";
import { createFileCate } from "@/api/file";
const props = defineProps({
modelValue: {
type: Boolean,
default: false,
},
});
const emit = defineEmits(["update:modelValue", "success", "close"]);
const visible = ref(false);
const formRef = ref<any>(null);
const loading = ref(false);
const form = ref({
name: "",
});
// 表单验证规则
const rules = {
name: [
{ required: true, message: "请输入文件分组名称", trigger: "blur" },
{ min: 1, max: 50, message: "名称长度在 1 到 50 个字符", trigger: "blur" },
],
};
// 监听 modelValue 变化
watch(
() => props.modelValue,
(newVal) => {
visible.value = newVal;
if (newVal) {
// 打开对话框时,清空表单数据
form.value.name = "";
// 清除表单验证
if (formRef.value) {
formRef.value.clearValidate();
}
}
}
);
// 监听 visible 变化
watch(visible, (newVal) => {
if (!newVal) {
emit("update:modelValue", false);
}
});
// 取消
const handleCancel = () => {
visible.value = false;
form.value.name = "";
if (formRef.value) {
formRef.value.clearValidate();
}
emit("close");
};
// 提交
const handleSubmit = async () => {
if (!formRef.value) {
return;
}
// 表单验证
try {
await formRef.value.validate();
} catch (error) {
ElMessage.warning("请检查表单填写是否正确");
return;
}
// 验证名称是否为空
if (!form.value.name || form.value.name.trim() === "") {
ElMessage.error("文件分组名称不能为空");
return;
}
loading.value = true;
try {
const res = await createFileCate({
name: form.value.name.trim(),
});
if (res.code === 200) {
ElMessage.success("创建成功");
visible.value = false;
form.value.name = "";
emit("success");
emit("close");
} else {
ElMessage.error(res.msg || "创建失败");
}
} catch (error: any) {
const errorMsg =
error?.response?.data?.message || error?.message || "创建失败";
ElMessage.error(errorMsg);
} finally {
loading.value = false;
}
};
// 暴露方法给父组件
defineExpose({
open: () => {
form.value.name = "";
visible.value = true;
if (formRef.value) {
formRef.value.clearValidate();
}
},
});
</script>
<style scoped>
.create-category-dialog {
width: 400px;
}
</style>
@@ -1,124 +1,124 @@
<template>
<el-dialog
:model-value="visible"
title="移动文件"
width="400px"
@close="handleClose"
@update:model-value="handleClose"
>
<el-form>
<el-form-item label="目标分组">
<el-select
v-model="targetCate"
placeholder="请选择分组"
style="width: 100%"
@change="handleCateChange"
>
<el-option
v-for="cate in filteredCateList"
:key="cate.id"
:label="cate.name"
:value="cate.id"
:disabled="cate.id === props.currentCateId"
/>
</el-select>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="handleClose">取消</el-button>
<el-button type="primary" :loading="loading" @click="handleMove">确定</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, watch, computed } from 'vue'
import { ElMessage } from 'element-plus'
import { moveFile } from '@/api/file'
interface Cate {
id: number
name: string
}
// Props
const props = defineProps<{
visible: boolean
fileId: number | null
currentCateId?: number | null
cateList: Cate[]
}>()
// Emits
const emit = defineEmits(['update:visible', 'moved'])
// Data
const targetCate = ref<number | null>(null)
const loading = ref(false)
// 未分类分组
const uncategorizedOption: Cate = {
id: 0,
name: '未分类'
}
// 计算属性:添加未分类选项,并标记当前分组为禁用
const filteredCateList = computed(() => {
const list = [uncategorizedOption, ...(props.cateList || [])]
return list
})
// 重置
watch(
() => props.visible,
(val) => {
if (val) {
targetCate.value = null
}
}
)
// 关闭对话框
function handleClose() {
emit('update:visible', false)
targetCate.value = null
}
// 选择分组
function handleCateChange(value: number) {
targetCate.value = value
}
// 移动文件
async function handleMove() {
// 校验文件ID和目标分组ID的有效性
if (!props.fileId) {
ElMessage.error('请选择要移动的文件')
return
}
// 修复:0 是有效的分组ID(未分类),需要检查 null 或 undefined
if (targetCate.value === null || targetCate.value === undefined) {
ElMessage.error('请选择目标分组')
return
}
loading.value = true
try {
const res = await moveFile(props.fileId, targetCate.value)
if (res && res.code === 200) {
ElMessage.success('文件移动成功')
handleClose() // 先关闭对话框
emit('moved') // 然后触发移动成功事件,刷新列表
} else {
ElMessage.error(res?.msg || '文件移动失败')
}
} catch (error: any) {
console.error('移动文件失败:', error)
ElMessage.error(error?.message || '文件移动失败,请稍后重试')
} finally {
loading.value = false
}
}
</script>
<template>
<el-dialog
:model-value="visible"
title="移动文件"
width="400px"
@close="handleClose"
@update:model-value="handleClose"
>
<el-form>
<el-form-item label="目标分组">
<el-select
v-model="targetCate"
placeholder="请选择分组"
style="width: 100%"
@change="handleCateChange"
>
<el-option
v-for="cate in filteredCateList"
:key="cate.id"
:label="cate.name"
:value="cate.id"
:disabled="cate.id === props.currentCateId"
/>
</el-select>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="handleClose">取消</el-button>
<el-button type="primary" :loading="loading" @click="handleMove">确定</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, watch, computed } from 'vue'
import { ElMessage } from 'element-plus'
import { moveFile } from '@/api/file'
interface Cate {
id: number
name: string
}
// Props
const props = defineProps<{
visible: boolean
fileId: number | null
currentCateId?: number | null
cateList: Cate[]
}>()
// Emits
const emit = defineEmits(['update:visible', 'moved'])
// Data
const targetCate = ref<number | null>(null)
const loading = ref(false)
// 未分类分组
const uncategorizedOption: Cate = {
id: 0,
name: '未分类'
}
// 计算属性:添加未分类选项,并标记当前分组为禁用
const filteredCateList = computed(() => {
const list = [uncategorizedOption, ...(props.cateList || [])]
return list
})
// 重置
watch(
() => props.visible,
(val) => {
if (val) {
targetCate.value = null
}
}
)
// 关闭对话框
function handleClose() {
emit('update:visible', false)
targetCate.value = null
}
// 选择分组
function handleCateChange(value: number) {
targetCate.value = value
}
// 移动文件
async function handleMove() {
// 校验文件ID和目标分组ID的有效性
if (!props.fileId) {
ElMessage.error('请选择要移动的文件')
return
}
// 修复:0 是有效的分组ID(未分类),需要检查 null 或 undefined
if (targetCate.value === null || targetCate.value === undefined) {
ElMessage.error('请选择目标分组')
return
}
loading.value = true
try {
const res = await moveFile(props.fileId, targetCate.value)
if (res && res.code === 200) {
ElMessage.success('文件移动成功')
handleClose() // 先关闭对话框
emit('moved') // 然后触发移动成功事件,刷新列表
} else {
ElMessage.error(res?.msg || '文件移动失败')
}
} catch (error: any) {
console.error('移动文件失败:', error)
ElMessage.error(error?.message || '文件移动失败,请稍后重试')
} finally {
loading.value = false
}
}
</script>
@@ -1,175 +1,175 @@
<template>
<el-dialog
v-model="visible"
title="重命名文件分组"
width="400px"
@close="handleCancel"
>
<el-form
:model="form"
:rules="rules"
ref="formRef"
label-width="100px"
>
<el-form-item label="分组名称" prop="name">
<el-input
v-model="form.name"
placeholder="请输入文件分组名称"
maxlength="50"
show-word-limit
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="handleCancel">取消</el-button>
<el-button type="primary" @click="handleSubmit" :loading="loading">
确定
</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, watch } from "vue";
import { ElMessage } from "element-plus";
import { renameFileCate } from "@/api/file";
const props = defineProps({
modelValue: {
type: Boolean,
default: false,
},
categoryId: {
type: Number,
default: null,
},
categoryName: {
type: String,
default: "",
},
});
const emit = defineEmits(["update:modelValue", "success", "close"]);
const visible = ref(false);
const formRef = ref<any>(null);
const loading = ref(false);
const form = ref({
name: "",
});
// 表单验证规则
const rules = {
name: [
{ required: true, message: "请输入文件分组名称", trigger: "blur" },
{ min: 1, max: 50, message: "名称长度在 1 到 50 个字符", trigger: "blur" },
],
};
// 监听 modelValue 变化
watch(
() => props.modelValue,
(newVal) => {
visible.value = newVal;
if (newVal) {
// 打开对话框时,初始化表单数据
form.value.name = props.categoryName || "";
// 清除表单验证
if (formRef.value) {
formRef.value.clearValidate();
}
}
}
);
// 监听 visible 变化
watch(visible, (newVal) => {
if (!newVal) {
emit("update:modelValue", false);
}
});
// 取消
const handleCancel = () => {
visible.value = false;
form.value.name = "";
if (formRef.value) {
formRef.value.clearValidate();
}
emit("close");
};
// 提交
const handleSubmit = async () => {
if (!formRef.value) {
return;
}
// 表单验证
try {
await formRef.value.validate();
} catch (error) {
ElMessage.warning("请检查表单填写是否正确");
return;
}
// 验证名称是否为空
if (!form.value.name || form.value.name.trim() === "") {
ElMessage.error("文件分组名称不能为空");
return;
}
// 验证名称是否与原来相同
if (form.value.name.trim() === props.categoryName) {
ElMessage.warning("名称未发生变化");
return;
}
// 验证 categoryId
if (!props.categoryId) {
ElMessage.error("分组ID不能为空");
return;
}
loading.value = true;
try {
const res = await renameFileCate(props.categoryId, {
name: form.value.name.trim(),
});
if (res.code === 200) {
ElMessage.success("重命名成功");
visible.value = false;
form.value.name = "";
emit("success");
emit("close");
} else {
ElMessage.error(res.msg || "重命名失败");
}
} catch (error: any) {
const errorMsg =
error?.response?.data?.message || error?.message || "重命名失败";
ElMessage.error(errorMsg);
} finally {
loading.value = false;
}
};
// 暴露方法给父组件
defineExpose({
open: (categoryId: number, categoryName: string) => {
form.value.name = categoryName || "";
visible.value = true;
if (formRef.value) {
formRef.value.clearValidate();
}
},
});
</script>
<style scoped>
.rename-category-dialog {
width: 400px;
}
</style>
<template>
<el-dialog
v-model="visible"
title="重命名文件分组"
width="400px"
@close="handleCancel"
>
<el-form
:model="form"
:rules="rules"
ref="formRef"
label-width="100px"
>
<el-form-item label="分组名称" prop="name">
<el-input
v-model="form.name"
placeholder="请输入文件分组名称"
maxlength="50"
show-word-limit
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="handleCancel">取消</el-button>
<el-button type="primary" @click="handleSubmit" :loading="loading">
确定
</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, watch } from "vue";
import { ElMessage } from "element-plus";
import { renameFileCate } from "@/api/file";
const props = defineProps({
modelValue: {
type: Boolean,
default: false,
},
categoryId: {
type: Number,
default: null,
},
categoryName: {
type: String,
default: "",
},
});
const emit = defineEmits(["update:modelValue", "success", "close"]);
const visible = ref(false);
const formRef = ref<any>(null);
const loading = ref(false);
const form = ref({
name: "",
});
// 表单验证规则
const rules = {
name: [
{ required: true, message: "请输入文件分组名称", trigger: "blur" },
{ min: 1, max: 50, message: "名称长度在 1 到 50 个字符", trigger: "blur" },
],
};
// 监听 modelValue 变化
watch(
() => props.modelValue,
(newVal) => {
visible.value = newVal;
if (newVal) {
// 打开对话框时,初始化表单数据
form.value.name = props.categoryName || "";
// 清除表单验证
if (formRef.value) {
formRef.value.clearValidate();
}
}
}
);
// 监听 visible 变化
watch(visible, (newVal) => {
if (!newVal) {
emit("update:modelValue", false);
}
});
// 取消
const handleCancel = () => {
visible.value = false;
form.value.name = "";
if (formRef.value) {
formRef.value.clearValidate();
}
emit("close");
};
// 提交
const handleSubmit = async () => {
if (!formRef.value) {
return;
}
// 表单验证
try {
await formRef.value.validate();
} catch (error) {
ElMessage.warning("请检查表单填写是否正确");
return;
}
// 验证名称是否为空
if (!form.value.name || form.value.name.trim() === "") {
ElMessage.error("文件分组名称不能为空");
return;
}
// 验证名称是否与原来相同
if (form.value.name.trim() === props.categoryName) {
ElMessage.warning("名称未发生变化");
return;
}
// 验证 categoryId
if (!props.categoryId) {
ElMessage.error("分组ID不能为空");
return;
}
loading.value = true;
try {
const res = await renameFileCate(props.categoryId, {
name: form.value.name.trim(),
});
if (res.code === 200) {
ElMessage.success("重命名成功");
visible.value = false;
form.value.name = "";
emit("success");
emit("close");
} else {
ElMessage.error(res.msg || "重命名失败");
}
} catch (error: any) {
const errorMsg =
error?.response?.data?.message || error?.message || "重命名失败";
ElMessage.error(errorMsg);
} finally {
loading.value = false;
}
};
// 暴露方法给父组件
defineExpose({
open: (categoryId: number, categoryName: string) => {
form.value.name = categoryName || "";
visible.value = true;
if (formRef.value) {
formRef.value.clearValidate();
}
},
});
</script>
<style scoped>
.rename-category-dialog {
width: 400px;
}
</style>
@@ -1,313 +1,313 @@
<template>
<el-dialog
v-model="visible"
title="上传文件"
width="600px"
@close="handleClose"
>
<el-upload
ref="uploadRef"
:action="uploadAction"
:headers="uploadHeaders"
:data="uploadData"
:file-list="fileList"
:on-change="handleChange"
:on-success="handleSuccess"
:on-error="handleError"
:on-progress="handleProgress"
:on-remove="handleRemove"
:before-upload="beforeUpload"
:auto-upload="false"
drag
multiple
class="upload-container"
>
<el-icon class="el-icon--upload"><UploadFilled /></el-icon>
<div class="el-upload__text">
将文件拖到此处,或<em>点击上传</em>
</div>
<template #tip>
<div class="el-upload__tip">
支持多文件上传,单个文件大小不超过50MB
</div>
</template>
</el-upload>
<template #footer>
<div class="dialog-footer">
<el-button @click="handleClose">取消</el-button>
<el-button type="primary" @click="submitUpload" :loading="uploading">
{{ uploading ? "上传中..." : "开始上传" }}
</el-button>
</div>
</template>
</el-dialog>
</template>
<script lang="ts" setup>
import { ref, computed } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import { UploadFilled } from "@element-plus/icons-vue";
import { uploadFile } from "@/api/file";
import type { UploadInstance, UploadFile, UploadFiles } from "element-plus";
interface Props {
modelValue: boolean;
categoryId?: number;
}
interface Emits {
(e: "update:modelValue", value: boolean): void;
(e: "success"): void;
}
const props = withDefaults(defineProps<Props>(), {
modelValue: false,
categoryId: undefined,
});
const emit = defineEmits<Emits>();
const visible = computed({
get: () => props.modelValue,
set: (val) => emit("update:modelValue", val),
});
const uploadRef = ref<UploadInstance>();
const fileList = ref<UploadFile[]>([]);
const uploading = ref(false);
const uploadAction = ref(""); // 不使用action,手动上传
// 上传请求头
const uploadHeaders = computed(() => {
const token = localStorage.getItem("token");
return {
Authorization: token ? `Bearer ${token}` : "",
};
});
// 上传额外数据
const uploadData = computed(() => ({
cate: props.categoryId || 0,
}));
// 文件变化监听
const handleChange = (file: UploadFile, files: UploadFiles) => {
// 更新文件列表
fileList.value = files;
};
// 文件上传前的验证
const beforeUpload = (file: File) => {
const maxSize = 50 * 1024 * 1024; // 50MB
const isValidType = true; // 可以根据需要限制文件类型
if (file.size > maxSize) {
ElMessage.error(`文件 ${file.name} 大小超过50MB限制`);
return false;
}
return true;
};
// 移除文件
const handleRemove = (file: UploadFile) => {
const index = fileList.value.findIndex((item) => item.uid === file.uid);
if (index > -1) {
fileList.value.splice(index, 1);
}
};
// 上传进度
const handleProgress = (event: any, file: UploadFile) => {
file.status = "uploading";
file.percentage = Math.round(event.percent);
};
// 上传成功
const handleSuccess = (response: any, file: UploadFile) => {
// 只要 code === 200 或 201 都视作已成功(存在或新上传)
if (response.code === 200) {
file.status = "success";
ElMessage.success(`文件 ${file.name} 上传成功`);
} else if (response.code === 201) {
file.status = "success";
ElMessage.info(response.msg || `文件 ${file.name} 已存在`);
} else {
file.status = "fail";
ElMessage.error(response.msg || response.message || `文件 ${file.name} 上传失败`);
}
};
// 上传失败
const handleError = (error: any, file: UploadFile) => {
file.status = "fail";
let msg = error?.message || error?.msg || '';
if (!msg && error?.response && error.response.data) {
msg = error.response.data.msg || error.response.data.message;
}
console.error("上传失败:", error);
ElMessage.error(msg ? `文件 ${file.name} 上传失败:${msg}` : `文件 ${file.name} 上传失败`);
};
// 手动提交上传
const submitUpload = async () => {
if (!uploadRef.value) return;
// 从 upload 组件实例获取文件列表
const uploadFiles = uploadRef.value.uploadFiles || fileList.value;
const filesToUpload = uploadFiles.filter(
(file: UploadFile) => file.status !== "success" && file.raw
);
if (filesToUpload.length === 0) {
ElMessage.warning("请先选择要上传的文件");
return;
}
uploading.value = true;
try {
// 逐个上传文件,显示进度
const uploadPromises = filesToUpload.map(async (file) => {
file.status = "uploading";
file.percentage = 0;
try {
const formData = new FormData();
formData.append("file", file.raw as File);
if (props.categoryId !== undefined && props.categoryId !== null) {
formData.append("cate", String(props.categoryId));
}
// 模拟上传进度
const progressInterval = setInterval(() => {
if (file.percentage < 90) {
file.percentage = Math.min(file.percentage + 10, 90);
}
}, 200);
const res = await uploadFile(formData, { cate: props.categoryId });
clearInterval(progressInterval);
file.percentage = 100;
// code 200 表示新上传成功,code 201 表示文件已存在(也视为成功)
if (res.code === 200 || res.code === 201) {
file.status = "success";
return { success: true, file, res };
} else {
file.status = "fail";
throw new Error(res.msg || "上传失败");
}
} catch (error: any) {
file.status = "fail";
file.percentage = 0;
throw { file, error };
}
});
const results = await Promise.allSettled(uploadPromises);
const successCount = results.filter((r) => r.status === "fulfilled").length;
const failCount = results.filter((r) => r.status === "rejected").length;
// 统计新上传和已存在的文件数量
let newUploadCount = 0;
let existCount = 0;
results.forEach((result) => {
if (result.status === "fulfilled" && result.value?.res) {
if (result.value.res.code === 200) {
newUploadCount++;
} else if (result.value.res.code === 201) {
existCount++;
}
}
});
if (successCount > 0) {
let message = "";
if (newUploadCount > 0 && existCount > 0) {
message = `成功处理 ${successCount} 个文件(${newUploadCount} 个新上传,${existCount} 个已存在)`;
} else if (newUploadCount > 0) {
message = `成功上传 ${newUploadCount} 个文件`;
} else if (existCount > 0) {
message = `${existCount} 个文件已存在`;
} else {
message = `成功处理 ${successCount} 个文件`;
}
ElMessage.success(message);
}
if (failCount > 0) {
ElMessage.warning(`${failCount} 个文件上传失败`);
}
// 如果所有文件都上传成功,关闭对话框并刷新列表
if (failCount === 0) {
emit("success");
handleClose();
}
} catch (error: any) {
console.error("上传失败:", error);
ElMessage.error("上传过程中发生错误");
} finally {
uploading.value = false;
}
};
// 关闭对话框
const handleClose = () => {
fileList.value = [];
uploading.value = false;
visible.value = false;
};
</script>
<style lang="less" scoped>
.upload-container {
:deep(.el-upload) {
width: 100%;
}
:deep(.el-upload-dragger) {
width: 100%;
height: 180px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.el-icon--upload {
font-size: 67px;
color: #c0c4cc;
margin-bottom: 16px;
}
.el-upload__text {
color: #606266;
font-size: 14px;
text-align: center;
em {
color: #3973ff;
font-style: normal;
}
}
.el-upload__tip {
color: #909399;
font-size: 12px;
margin-top: 8px;
text-align: center;
}
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
}
</style>
<template>
<el-dialog
v-model="visible"
title="上传文件"
width="600px"
@close="handleClose"
>
<el-upload
ref="uploadRef"
:action="uploadAction"
:headers="uploadHeaders"
:data="uploadData"
:file-list="fileList"
:on-change="handleChange"
:on-success="handleSuccess"
:on-error="handleError"
:on-progress="handleProgress"
:on-remove="handleRemove"
:before-upload="beforeUpload"
:auto-upload="false"
drag
multiple
class="upload-container"
>
<el-icon class="el-icon--upload"><UploadFilled /></el-icon>
<div class="el-upload__text">
将文件拖到此处,或<em>点击上传</em>
</div>
<template #tip>
<div class="el-upload__tip">
支持多文件上传,单个文件大小不超过50MB
</div>
</template>
</el-upload>
<template #footer>
<div class="dialog-footer">
<el-button @click="handleClose">取消</el-button>
<el-button type="primary" @click="submitUpload" :loading="uploading">
{{ uploading ? "上传中..." : "开始上传" }}
</el-button>
</div>
</template>
</el-dialog>
</template>
<script lang="ts" setup>
import { ref, computed } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import { UploadFilled } from "@element-plus/icons-vue";
import { uploadFile } from "@/api/file";
import type { UploadInstance, UploadFile, UploadFiles } from "element-plus";
interface Props {
modelValue: boolean;
categoryId?: number;
}
interface Emits {
(e: "update:modelValue", value: boolean): void;
(e: "success"): void;
}
const props = withDefaults(defineProps<Props>(), {
modelValue: false,
categoryId: undefined,
});
const emit = defineEmits<Emits>();
const visible = computed({
get: () => props.modelValue,
set: (val) => emit("update:modelValue", val),
});
const uploadRef = ref<UploadInstance>();
const fileList = ref<UploadFile[]>([]);
const uploading = ref(false);
const uploadAction = ref(""); // 不使用action,手动上传
// 上传请求头
const uploadHeaders = computed(() => {
const token = localStorage.getItem("token");
return {
Authorization: token ? `Bearer ${token}` : "",
};
});
// 上传额外数据
const uploadData = computed(() => ({
cate: props.categoryId || 0,
}));
// 文件变化监听
const handleChange = (file: UploadFile, files: UploadFiles) => {
// 更新文件列表
fileList.value = files;
};
// 文件上传前的验证
const beforeUpload = (file: File) => {
const maxSize = 50 * 1024 * 1024; // 50MB
const isValidType = true; // 可以根据需要限制文件类型
if (file.size > maxSize) {
ElMessage.error(`文件 ${file.name} 大小超过50MB限制`);
return false;
}
return true;
};
// 移除文件
const handleRemove = (file: UploadFile) => {
const index = fileList.value.findIndex((item) => item.uid === file.uid);
if (index > -1) {
fileList.value.splice(index, 1);
}
};
// 上传进度
const handleProgress = (event: any, file: UploadFile) => {
file.status = "uploading";
file.percentage = Math.round(event.percent);
};
// 上传成功
const handleSuccess = (response: any, file: UploadFile) => {
// 只要 code === 200 或 201 都视作已成功(存在或新上传)
if (response.code === 200) {
file.status = "success";
ElMessage.success(`文件 ${file.name} 上传成功`);
} else if (response.code === 201) {
file.status = "success";
ElMessage.info(response.msg || `文件 ${file.name} 已存在`);
} else {
file.status = "fail";
ElMessage.error(response.msg || response.message || `文件 ${file.name} 上传失败`);
}
};
// 上传失败
const handleError = (error: any, file: UploadFile) => {
file.status = "fail";
let msg = error?.message || error?.msg || '';
if (!msg && error?.response && error.response.data) {
msg = error.response.data.msg || error.response.data.message;
}
console.error("上传失败:", error);
ElMessage.error(msg ? `文件 ${file.name} 上传失败:${msg}` : `文件 ${file.name} 上传失败`);
};
// 手动提交上传
const submitUpload = async () => {
if (!uploadRef.value) return;
// 从 upload 组件实例获取文件列表
const uploadFiles = uploadRef.value.uploadFiles || fileList.value;
const filesToUpload = uploadFiles.filter(
(file: UploadFile) => file.status !== "success" && file.raw
);
if (filesToUpload.length === 0) {
ElMessage.warning("请先选择要上传的文件");
return;
}
uploading.value = true;
try {
// 逐个上传文件,显示进度
const uploadPromises = filesToUpload.map(async (file) => {
file.status = "uploading";
file.percentage = 0;
try {
const formData = new FormData();
formData.append("file", file.raw as File);
if (props.categoryId !== undefined && props.categoryId !== null) {
formData.append("cate", String(props.categoryId));
}
// 模拟上传进度
const progressInterval = setInterval(() => {
if (file.percentage < 90) {
file.percentage = Math.min(file.percentage + 10, 90);
}
}, 200);
const res = await uploadFile(formData, { cate: props.categoryId });
clearInterval(progressInterval);
file.percentage = 100;
// code 200 表示新上传成功,code 201 表示文件已存在(也视为成功)
if (res.code === 200 || res.code === 201) {
file.status = "success";
return { success: true, file, res };
} else {
file.status = "fail";
throw new Error(res.msg || "上传失败");
}
} catch (error: any) {
file.status = "fail";
file.percentage = 0;
throw { file, error };
}
});
const results = await Promise.allSettled(uploadPromises);
const successCount = results.filter((r) => r.status === "fulfilled").length;
const failCount = results.filter((r) => r.status === "rejected").length;
// 统计新上传和已存在的文件数量
let newUploadCount = 0;
let existCount = 0;
results.forEach((result) => {
if (result.status === "fulfilled" && result.value?.res) {
if (result.value.res.code === 200) {
newUploadCount++;
} else if (result.value.res.code === 201) {
existCount++;
}
}
});
if (successCount > 0) {
let message = "";
if (newUploadCount > 0 && existCount > 0) {
message = `成功处理 ${successCount} 个文件(${newUploadCount} 个新上传,${existCount} 个已存在)`;
} else if (newUploadCount > 0) {
message = `成功上传 ${newUploadCount} 个文件`;
} else if (existCount > 0) {
message = `${existCount} 个文件已存在`;
} else {
message = `成功处理 ${successCount} 个文件`;
}
ElMessage.success(message);
}
if (failCount > 0) {
ElMessage.warning(`${failCount} 个文件上传失败`);
}
// 如果所有文件都上传成功,关闭对话框并刷新列表
if (failCount === 0) {
emit("success");
handleClose();
}
} catch (error: any) {
console.error("上传失败:", error);
ElMessage.error("上传过程中发生错误");
} finally {
uploading.value = false;
}
};
// 关闭对话框
const handleClose = () => {
fileList.value = [];
uploading.value = false;
visible.value = false;
};
</script>
<style lang="less" scoped>
.upload-container {
:deep(.el-upload) {
width: 100%;
}
:deep(.el-upload-dragger) {
width: 100%;
height: 180px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.el-icon--upload {
font-size: 67px;
color: #c0c4cc;
margin-bottom: 16px;
}
.el-upload__text {
color: #606266;
font-size: 14px;
text-align: center;
em {
color: #3973ff;
font-style: normal;
}
}
.el-upload__tip {
color: #909399;
font-size: 12px;
margin-top: 8px;
text-align: center;
}
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
}
</style>
File diff suppressed because it is too large Load Diff