🎨 新增批量上传文章功能
This commit is contained in:
@@ -48,3 +48,12 @@ export const getTechers = (params) => {
|
|||||||
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