🎨 新增批量上传文章功能

This commit is contained in:
2025-09-12 23:55:12 +08:00
parent 94a061691c
commit 04db549012
2 changed files with 488 additions and 0 deletions

View File

@@ -48,3 +48,12 @@ export const getTechers = (params) => {
params params
}) })
} }
//BulkUpload
export const bulkUpload = (data) => {
return service({
url: '/article/bulk',
method: 'post',
data
})
}

View File

@@ -0,0 +1,479 @@
<template>
<div class="container-wrapper">
<el-form
ref="ruleFormRef"
:model="formData"
:rules="rules"
label-width="auto"
style="margin-top: 0.5rem;"
>
<ColumnItem title="批量上传文章">
<!-- 文件上传区域 -->
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="上传文件" prop="files">
<el-upload
ref="uploadRef"
:action="`${getBaseUrl()}/fileUploadAndDownload/upload`"
:before-upload="checkFile"
:on-error="uploadError"
:on-success="uploadSuccess"
:on-remove="removeFile"
:file-list="fileList"
:data="{'classId': 3}"
:headers="{'x-token': token}"
multiple
drag
class="upload-dragger"
accept="image/*"
>
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
<div class="el-upload__text">
将文件拖到此处<em>点击上传</em>
</div>
<template #tip>
<div class="el-upload__tip">
支持批量上传图片文件单个文件不超过5MB
</div>
</template>
</el-upload>
</el-form-item>
</el-col>
</el-row>
<!-- 已上传文件列表 -->
<el-row :gutter="20" v-if="uploadedFiles.length > 0">
<el-col :span="24">
<el-form-item label="已上传文件">
<div class="file-list">
<div
v-for="(file, index) in uploadedFiles"
:key="index"
class="file-item"
>
<el-image
:src="file.url"
:preview-src-list="uploadedFiles.map(f => f.url)"
fit="cover"
class="file-preview"
/>
<div class="file-info">
<div class="file-name">{{ file.name }}</div>
<div class="file-url">{{ file.url }}</div>
</div>
<el-button
type="danger"
size="small"
@click="removeUploadedFile(index)"
class="remove-btn"
>
删除
</el-button>
</div>
</div>
</el-form-item>
</el-col>
</el-row>
<!-- 文章信息表单 -->
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="标题" prop="title">
<el-input v-model="formData.title" placeholder="请输入文章标题" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="分类" prop="categoryId">
<el-select v-model="formData.categoryId" placeholder="请选择文章分类" clearable style="width: 100%">
<el-option
v-for="item in articleCategoryList"
:key="item.ID"
:label="item.name"
:value="item.ID"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="是否免费" prop="isFree">
<el-radio-group v-model="formData.isFree">
<el-radio :label="1">免费</el-radio>
<el-radio :label="0">付费</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12" v-if="formData.isFree === 0">
<el-form-item label="价格()" prop="price">
<el-input v-model="formData.price" type="number" placeholder="请输入价格单位" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="定时发布" prop="publishTime">
<el-date-picker
v-model="formData.publishTime"
type="datetime"
placeholder="选择发布时间可选"
format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DD HH:mm:ss"
style="width: 100%"
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="文章描述" prop="desc">
<el-input
v-model="formData.desc"
type="textarea"
:rows="3"
placeholder="请输入文章描述"
/>
</el-form-item>
</el-col>
</el-row>
</ColumnItem>
</el-form>
<div class="gva-table-box footer-box">
<el-button @click="$router.back()">返回</el-button>
<el-button
type="primary"
@click="submit(ruleFormRef)"
:loading="submitLoading"
:disabled="uploadedFiles.length === 0"
>
批量提交 ({{ uploadedFiles.length }} 个文件)
</el-button>
</div>
</div>
</template>
<script setup>
import { onMounted, ref, computed } from 'vue'
import { useRouter } from 'vue-router'
import { bulkUpload } from '@/api/goods/index.js'
import { getCategoryList } from '@/api/category/category.js'
import ColumnItem from '@/components/columnItem/ColumnItem.vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { UploadFilled } from '@element-plus/icons-vue'
import { getBaseUrl } from '@/utils/format'
import { useUserStore } from "@/pinia"
const router = useRouter()
const userStore = useUserStore()
const token = userStore.token
const ruleFormRef = ref(null)
const uploadRef = ref(null)
const submitLoading = ref(false)
const fileList = ref([])
const uploadedFiles = ref([])
const categoryList = ref([])
const formData = ref({
title: '',
desc: '',
price: 0,
categoryId: 0,
publishTime: '',
isFree: 0
})
const rules = ref({
title: [
{ required: true, message: '请输入文章标题', trigger: 'blur' }
],
categoryId: [
{ required: true, message: '请选择文章分类', trigger: 'change' }
],
isFree: [
{ required: true, message: '请选择是否免费', trigger: 'change' }
],
price: [
{
required: true,
message: '请输入价格',
trigger: 'blur',
validator: (rule, value, callback) => {
if (formData.value.isFree === 0 && (!value || value <= 0)) {
callback(new Error('付费文章请输入大于0的价格'))
} else {
callback()
}
}
}
]
})
// 计算属性:筛选出文章分类
const articleCategoryList = computed(() => {
return categoryList.value.filter(item => !item.url || item.url === '')
})
onMounted(() => {
getCategoryData()
})
// 获取分类数据
async function getCategoryData() {
try {
const res = await getCategoryList()
if (res.code === 0) {
categoryList.value = res.data.list || []
}
} catch (error) {
console.error('获取分类数据失败:', error)
}
}
// 文件上传前检查
const checkFile = (file) => {
const isLt5M = file.size / 1024 / 1024 < 5
const isImage = file.type.startsWith('image/')
if (!isImage) {
ElMessage.error('只能上传图片文件!')
return false
}
if (!isLt5M) {
ElMessage.error('上传图片大小不能超过 5MB!')
return false
}
return true
}
// 文件上传成功
const uploadSuccess = (res, file) => {
if (res.code === 0 && res.data.file) {
uploadedFiles.value.push({
name: file.name,
url: res.data.file.url
})
ElMessage.success(`${file.name} 上传成功`)
} else {
ElMessage.error('上传失败')
}
}
// 文件上传失败
const uploadError = () => {
ElMessage.error('上传失败')
}
// 移除文件
const removeFile = (file) => {
const index = fileList.value.findIndex(item => item.uid === file.uid)
if (index > -1) {
uploadedFiles.value.splice(index, 1)
}
}
// 移除已上传文件
const removeUploadedFile = (index) => {
uploadedFiles.value.splice(index, 1)
// 同时移除fileList中对应的文件
if (fileList.value[index]) {
fileList.value.splice(index, 1)
}
}
// 提交表单
function submit(formRef) {
if (!formRef) return
if (uploadedFiles.value.length === 0) {
ElMessage.warning('请先上传文件')
return
}
// 校验
formRef.validate(async (valid) => {
if (!valid) return
submitLoading.value = true
try {
// 处理价格字段:用户输入元,接口需要分
let price = 0
if (formData.value.isFree === 0 && formData.value.price) {
price = Math.round(Number(formData.value.price) * 100)
}
const submitData = {
files: uploadedFiles.value.map(file => file.url),
title: formData.value.title,
desc: formData.value.desc,
price: price,
categoryId: formData.value.categoryId,
publishTime: formData.value.publishTime || '',
isFree: formData.value.isFree
}
const res = await bulkUpload(submitData)
if (res.code === 0) {
ElMessage.success('批量上传成功!')
router.back()
} else if (res.code === 5) {
// 部分文件上传失败,处理失败的文件
handlePartialFailure(res)
} else {
ElMessage.error(res.msg || '批量上传失败')
}
} catch (error) {
console.error('批量上传失败:', error)
ElMessage.error('批量上传失败')
} finally {
submitLoading.value = false
}
})
}
// 处理部分上传失败的情况
function handlePartialFailure(res) {
try {
// 解析失败的文件URL列表
const failedUrls = res.data.replace(/[[\]]/g, '').split(' ').filter(url => url.trim())
if (failedUrls.length === 0) {
ElMessage.error('解析失败文件列表出错')
return
}
// 从已上传文件中移除成功的文件,只保留失败的文件
const originalCount = uploadedFiles.value.length
uploadedFiles.value = uploadedFiles.value.filter(file =>
failedUrls.includes(file.url)
)
// 同时更新fileList
fileList.value = fileList.value.filter(file =>
failedUrls.some(url => file.url === url)
)
const successCount = originalCount - failedUrls.length
const failedCount = failedUrls.length
ElMessage.warning({
message: `部分文件上传失败!成功:${successCount}个,失败:${failedCount}个。请重新上传失败的文件。`,
duration: 5000
})
// 显示失败文件的详细信息
ElMessageBox.alert(
`失败的文件:\n${failedUrls.join('\n')}`,
'上传失败详情',
{
confirmButtonText: '确定',
type: 'warning'
}
)
} catch (error) {
console.error('处理部分失败时出错:', error)
ElMessage.error('处理上传结果时出错')
}
}
</script>
<style lang="scss" scoped>
.container-wrapper {
padding: 20px;
}
.upload-dragger {
width: 100%;
}
.file-list {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin-top: 10px;
}
.file-item {
display: flex;
align-items: center;
padding: 8px;
border: 1px solid #e4e7ed;
border-radius: 4px;
background: #fafafa;
min-width: 200px;
}
.file-preview {
width: 40px;
height: 40px;
border-radius: 4px;
margin-right: 8px;
}
.file-info {
flex: 1;
min-width: 0;
}
.file-name {
font-size: 12px;
color: #606266;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-bottom: 4px;
}
.file-url {
font-size: 10px;
color: #909399;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.remove-btn {
margin-left: 8px;
}
.footer-box {
display: flex;
justify-content: flex-end;
gap: 12px;
margin-top: 20px;
padding: 20px;
background: white;
border-radius: 4px;
}
:deep(.el-row) {
margin-bottom: 20px;
}
:deep(.el-form) {
flex: 1;
background: white;
overflow: hidden;
}
:deep(.el-form-item) {
margin-bottom: 18px;
}
:deep(.el-form-item__label) {
font-weight: 500;
color: #606266;
}
:deep(.el-upload-dragger) {
width: 100%;
height: 120px;
}
</style>