feat: 新增移动端配置信息
Build and Deploy Vue3 / build (push) Successful in 1m33s
Build and Deploy Vue3 / deploy (push) Successful in 1m15s

This commit is contained in:
2026-03-17 18:40:12 +08:00
parent f4dbf17ce9
commit cd16ec17ae
6 changed files with 989 additions and 216 deletions
+282 -103
View File
@@ -13,9 +13,14 @@
<div class="file-list-container">
<div class="file-list-header">
<h4>图片文件库</h4>
<el-button type="primary" @click="switchToUpload" :icon="Upload">
上传新图片
</el-button>
<div class="header-actions">
<span v-if="props.multiple && selectedIds.size > 0" class="selected-count">
已选 {{ selectedIds.size }} 个文件
</span>
<el-button type="primary" @click="switchToUpload" :icon="Upload">
上传新图片
</el-button>
</div>
</div>
<!-- 搜索过滤 -->
@@ -35,9 +40,12 @@
v-for="file in filteredFileList"
:key="file.id"
class="file-item"
:class="{ 'selected': selectedId === file.id }"
:class="{ 'selected': props.multiple ? selectedIds.has(file.id) : selectedId === file.id }"
@click="selectFile(file)"
>
<div class="file-check-badge" v-if="props.multiple && selectedIds.has(file.id)">
<el-icon><Select /></el-icon>
</div>
<div class="file-preview">
<img
:src="processImageUrl(file.url)"
@@ -73,9 +81,9 @@
<el-tab-pane label="上传图片" name="upload">
<div class="upload-section">
<el-upload
:http-request="handleUpload"
:before-upload="beforeUpload"
:auto-upload="false"
:show-file-list="false"
:on-change="handleFileChange"
accept="image/*"
multiple
drag
@@ -91,13 +99,28 @@
</template>
</el-upload>
<!-- 上传进度 -->
<div v-if="uploadProgress.length > 0" class="upload-progress">
<h4>上传进度</h4>
<div v-for="progress in uploadProgress" :key="progress.id" class="progress-item">
<span>{{ progress.name }}</span>
<el-progress :percentage="progress.percentage" />
<!-- 上传文件列表 -->
<div v-if="pendingFiles.length > 0" class="pending-files">
<div class="pending-header">
<h4>待上传文件 ({{ pendingFiles.length }})</h4>
<el-button type="danger" link @click="pendingFiles = []">清空</el-button>
</div>
<div class="pending-list">
<div v-for="(file, index) in pendingFiles" :key="index" class="pending-item">
<img :src="file.previewUrl" class="pending-preview" />
<span class="pending-name" :title="file.name">{{ file.name }}</span>
<span class="pending-size">{{ formatFileSize(file.size) }}</span>
<el-button type="danger" link size="small" @click="removePendingFile(index)">移除</el-button>
</div>
</div>
<el-button
type="primary"
@click="handleBatchUpload"
:loading="uploading"
style="margin-top: 16px; width: 100%;"
>
开始上传 ({{ pendingFiles.length }} 个文件)
</el-button>
</div>
</div>
</el-tab-pane>
@@ -110,9 +133,9 @@
<el-button
type="primary"
@click="handleConfirm"
:disabled="!selectedId"
:disabled="props.multiple ? selectedIds.size === 0 : !selectedId"
>
确定选择
确定选择{{ props.multiple && selectedIds.size > 0 ? ` (${selectedIds.size})` : '' }}
</el-button>
</div>
</template>
@@ -122,7 +145,7 @@
<script setup>
import { ref, watch, computed } from 'vue'
import { ElMessage } from 'element-plus'
import { Upload, UploadFilled, Search } from '@element-plus/icons-vue'
import { Upload, UploadFilled, Search, Select, Delete } from '@element-plus/icons-vue'
import { getFileList, getFileDetail, uploadFile } from '@/api/admin/file'
// Props
@@ -150,17 +173,21 @@ const activeTab = ref('fileLibrary')
const fileList = ref([])
const loading = ref(false)
const selectedId = ref('')
const selectedIds = ref(new Set()) // 多选模式下选中的文件ID集合
const currentPage = ref(1)
const pageSize = ref(12)
const total = ref(0)
const searchKeyword = ref('')
const uploadProgress = ref([])
const pendingFiles = ref([]) // 待上传文件列表
const uploading = ref(false) // 批量上传中
let fetchVersion = 0 // 防止 fetchFileList 竞态条件
// 监听 modelValue 变化
watch(() => props.modelValue, (newVal) => {
visible.value = newVal
if (newVal) {
selectedId.value = props.currentFileId
selectedIds.value = new Set()
currentPage.value = 1
searchKeyword.value = ''
fetchFileList()
@@ -191,10 +218,10 @@ const processImageUrl = (url) => {
return decodeURIComponent(processedUrl)
}
// 获取文件列表
// 获取文件列表(带版本号防止竞态条件)
const fetchFileList = async () => {
const currentFetchVersion = ++fetchVersion
loading.value = true
fileList.value = []
try {
const res = await getFileList({
@@ -202,32 +229,49 @@ const fetchFileList = async () => {
count: pageSize.value
})
// 如果有更新的请求发起,丢弃当前结果
if (currentFetchVersion !== fetchVersion) return
if (res.data.code === 200) {
const list = res.data.data.list || []
total.value = res.data.data.all_count || 0
// 获取每个文件详情
for (let i = 0; i < list.length; i++) {
try {
const res2 = await getFileDetail({ file_id: list[i].id })
if (res2.data.code === 200) {
fileList.value.push({
id: res2.data.data.data.id,
url: res2.data.data.url,
size: res2.data.data.data.size,
realName: res2.data.data.data.realName
})
}
} catch (error) {
console.error('获取文件详情失败:', error)
}
}
// 并行获取所有文件详情(替代逐个串行,大幅提升速度)
const detailPromises = list.map(item =>
getFileDetail({ file_id: item.id })
.then(res2 => {
if (res2.data.code === 200) {
return {
id: res2.data.data.data.id,
url: res2.data.data.url,
size: res2.data.data.data.size,
realName: res2.data.data.data.realName
}
}
return null
})
.catch(error => {
console.error('获取文件详情失败:', error)
return null
})
)
const results = await Promise.all(detailPromises)
// 再次检查版本号,防止旧结果覆盖新结果
if (currentFetchVersion !== fetchVersion) return
fileList.value = results.filter(item => item !== null)
}
} catch (error) {
console.error('获取文件列表失败:', error)
ElMessage.error('获取文件列表失败')
if (currentFetchVersion === fetchVersion) {
console.error('获取文件列表失败:', error)
ElMessage.error('获取文件列表失败')
}
} finally {
loading.value = false
if (currentFetchVersion === fetchVersion) {
loading.value = false
}
}
}
@@ -279,65 +323,102 @@ const formatFileSize = (size) => {
// 选择文件
const selectFile = (file) => {
selectedId.value = file.id
if (props.multiple) {
// 多选模式:切换选中状态
const newSet = new Set(selectedIds.value)
if (newSet.has(file.id)) {
newSet.delete(file.id)
} else {
newSet.add(file.id)
}
selectedIds.value = newSet
} else {
selectedId.value = file.id
}
}
// 上传前验证
const beforeUpload = (file) => {
const isImage = file.type.startsWith('image/')
const isLt5M = file.size / 1024 / 1024 < 5
if (!isImage) {
ElMessage.error('只能上传图片文件!')
return false
}
if (!isLt5M) {
ElMessage.error('图片大小不能超过 5MB!')
return false
}
return true
}
// 自定义上传
const handleUpload = async (options) => {
const { file } = options
const formData = new FormData()
formData.append('files', file)
formData.append('file_names', file.name)
// 文件选择变化(收集待上传文件)
const handleFileChange = (file) => {
const rawFile = file.raw
if (!rawFile) return
// 添加上传进度跟踪
const progressId = Date.now() + Math.random()
uploadProgress.value.push({
id: progressId,
name: file.name,
percentage: 0
// 验证文件类型
const isImage = rawFile.type.startsWith('image/')
if (!isImage) {
ElMessage.error(`${rawFile.name} 不是图片文件,已跳过`)
return
}
// 验证文件大小
const isLt5M = rawFile.size / 1024 / 1024 < 5
if (!isLt5M) {
ElMessage.error(`${rawFile.name} 超过 5MB,已跳过`)
return
}
// 检查是否重复添加
const exists = pendingFiles.value.some(f => f.name === rawFile.name && f.size === rawFile.size)
if (exists) return
// 添加到待上传列表,生成本地预览URL
pendingFiles.value.push({
raw: rawFile,
name: rawFile.name,
size: rawFile.size,
previewUrl: URL.createObjectURL(rawFile)
})
}
// 移除待上传文件
const removePendingFile = (index) => {
const file = pendingFiles.value[index]
if (file?.previewUrl) {
URL.revokeObjectURL(file.previewUrl)
}
pendingFiles.value.splice(index, 1)
}
// 批量上传(所有文件合并为一次请求,多个 file_names 和 files 条目)
const handleBatchUpload = async () => {
if (pendingFiles.value.length === 0) {
ElMessage.warning('请先选择要上传的文件')
return
}
uploading.value = true
const formData = new FormData()
pendingFiles.value.forEach(file => {
formData.append('file_names', file.name)
formData.append('files', file.raw)
})
formData.append('update_type', 'cover')
formData.append('open_down', 'true')
try {
const res = await uploadFile(formData)
// 移除进度跟踪
uploadProgress.value = uploadProgress.value.filter(p => p.id !== progressId)
if (res.data.code === 200) {
ElMessage.success("上传成功")
// 重置到第一页并刷新文件列表
const count = pendingFiles.value.length
// 释放所有预览URL
pendingFiles.value.forEach(f => {
if (f.previewUrl) URL.revokeObjectURL(f.previewUrl)
})
pendingFiles.value = []
ElMessage.success(`成功上传 ${count} 个文件`)
// 刷新文件列表并切换到文件库
currentPage.value = 1
await fetchFileList()
// 切换到文件库标签页
activeTab.value = 'fileLibrary'
// 自动选择新上传的文件
if (res.data.data?.id) {
selectedId.value = res.data.data.id
}
} else {
ElMessage.error(res.data.msg || '上传失败')
}
} catch (error) {
// 移除进度跟踪
uploadProgress.value = uploadProgress.value.filter(p => p.id !== progressId)
console.error('上传失败:', error)
ElMessage.error('上传失败')
console.error('批量上传失败:', error)
ElMessage.error('上传失败,请重试')
} finally {
uploading.value = false
}
}
@@ -350,23 +431,43 @@ const handleImageError = (event) => {
const handleClose = () => {
visible.value = false
selectedId.value = ''
selectedIds.value = new Set()
fileList.value = []
currentPage.value = 1
total.value = 0
searchKeyword.value = ''
uploadProgress.value = []
// 清理待上传文件的预览URL
pendingFiles.value.forEach(f => {
if (f.previewUrl) URL.revokeObjectURL(f.previewUrl)
})
pendingFiles.value = []
}
// 确认选择
const handleConfirm = () => {
if (selectedId.value) {
const selectedFile = fileList.value.find(file => file.id === selectedId.value)
emit('confirm', {
id: selectedId.value,
url: selectedFile?.url || '',
realName: selectedFile?.realName || ''
})
if (props.multiple) {
// 多选模式:返回选中的文件数组
if (selectedIds.value.size === 0) return
const selectedFiles = fileList.value
.filter(file => selectedIds.value.has(file.id))
.map(file => ({
id: file.id,
url: file.url || '',
realName: file.realName || ''
}))
emit('confirm', selectedFiles)
handleClose()
} else {
// 单选模式:返回单个文件对象
if (selectedId.value) {
const selectedFile = fileList.value.find(file => file.id === selectedId.value)
emit('confirm', {
id: selectedId.value,
url: selectedFile?.url || '',
realName: selectedFile?.realName || ''
})
handleClose()
}
}
}
</script>
@@ -426,6 +527,39 @@ const handleConfirm = () => {
background-color: #f0f9ff;
}
.file-item {
position: relative;
}
.file-check-badge {
position: absolute;
top: 6px;
right: 6px;
width: 22px;
height: 22px;
background-color: #409EFF;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-size: 14px;
z-index: 1;
box-shadow: 0 2px 4px rgba(64, 158, 255, 0.4);
}
.selected-count {
color: #409EFF;
font-weight: 600;
font-size: 14px;
margin-right: 12px;
}
.header-actions {
display: flex;
align-items: center;
}
.file-preview {
width: 100px;
height: 100px;
@@ -466,29 +600,74 @@ const handleConfirm = () => {
}
.upload-section {
padding: 40px 20px;
padding: 20px;
text-align: center;
}
.upload-progress {
margin-top: 30px;
/* 待上传文件列表 */
.pending-files {
margin-top: 20px;
text-align: left;
}
.upload-progress h4 {
margin-bottom: 15px;
.pending-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.pending-header h4 {
margin: 0;
color: #303133;
}
.progress-item {
margin-bottom: 10px;
}
.progress-item span {
display: block;
margin-bottom: 5px;
font-size: 14px;
color: #606266;
}
.pending-list {
max-height: 240px;
overflow-y: auto;
border: 1px solid #ebeef5;
border-radius: 6px;
}
.pending-item {
display: flex;
align-items: center;
padding: 8px 12px;
border-bottom: 1px solid #f0f0f0;
gap: 10px;
}
.pending-item:last-child {
border-bottom: none;
}
.pending-item:hover {
background-color: #fafafa;
}
.pending-preview {
width: 40px;
height: 40px;
border-radius: 4px;
object-fit: cover;
flex-shrink: 0;
border: 1px solid #ebeef5;
}
.pending-name {
flex: 1;
font-size: 13px;
color: #303133;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.pending-size {
font-size: 12px;
color: #909399;
flex-shrink: 0;
}
.pagination-container {