🎨 新增批量上传文章功能
This commit is contained in:
@@ -48,3 +48,12 @@ export const getTechers = (params) => {
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
//BulkUpload
|
||||
export const bulkUpload = (data) => {
|
||||
return service({
|
||||
url: '/article/bulk',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
@@ -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>
|
||||
|
Reference in New Issue
Block a user