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
+1
View File
@@ -270,6 +270,7 @@ import { closeAllMessage } from '../../utils/message'
formData.append('files', file) formData.append('files', file)
formData.append('file_names', file.name) formData.append('file_names', file.name)
formData.append('update_type', 'cover') formData.append('update_type', 'cover')
formData.append('open_down', 'true')
try { try {
const res = await uploadFile(formData) const res = await uploadFile(formData)
+282 -103
View File
@@ -13,9 +13,14 @@
<div class="file-list-container"> <div class="file-list-container">
<div class="file-list-header"> <div class="file-list-header">
<h4>图片文件库</h4> <h4>图片文件库</h4>
<el-button type="primary" @click="switchToUpload" :icon="Upload"> <div class="header-actions">
上传新图片 <span v-if="props.multiple && selectedIds.size > 0" class="selected-count">
</el-button> 已选 {{ selectedIds.size }} 个文件
</span>
<el-button type="primary" @click="switchToUpload" :icon="Upload">
上传新图片
</el-button>
</div>
</div> </div>
<!-- 搜索过滤 --> <!-- 搜索过滤 -->
@@ -35,9 +40,12 @@
v-for="file in filteredFileList" v-for="file in filteredFileList"
:key="file.id" :key="file.id"
class="file-item" class="file-item"
:class="{ 'selected': selectedId === file.id }" :class="{ 'selected': props.multiple ? selectedIds.has(file.id) : selectedId === file.id }"
@click="selectFile(file)" @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"> <div class="file-preview">
<img <img
:src="processImageUrl(file.url)" :src="processImageUrl(file.url)"
@@ -73,9 +81,9 @@
<el-tab-pane label="上传图片" name="upload"> <el-tab-pane label="上传图片" name="upload">
<div class="upload-section"> <div class="upload-section">
<el-upload <el-upload
:http-request="handleUpload" :auto-upload="false"
:before-upload="beforeUpload"
:show-file-list="false" :show-file-list="false"
:on-change="handleFileChange"
accept="image/*" accept="image/*"
multiple multiple
drag drag
@@ -91,13 +99,28 @@
</template> </template>
</el-upload> </el-upload>
<!-- 上传进度 --> <!-- 上传文件列表 -->
<div v-if="uploadProgress.length > 0" class="upload-progress"> <div v-if="pendingFiles.length > 0" class="pending-files">
<h4>上传进度</h4> <div class="pending-header">
<div v-for="progress in uploadProgress" :key="progress.id" class="progress-item"> <h4>待上传文件 ({{ pendingFiles.length }})</h4>
<span>{{ progress.name }}</span> <el-button type="danger" link @click="pendingFiles = []">清空</el-button>
<el-progress :percentage="progress.percentage" />
</div> </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>
</div> </div>
</el-tab-pane> </el-tab-pane>
@@ -110,9 +133,9 @@
<el-button <el-button
type="primary" type="primary"
@click="handleConfirm" @click="handleConfirm"
:disabled="!selectedId" :disabled="props.multiple ? selectedIds.size === 0 : !selectedId"
> >
确定选择 确定选择{{ props.multiple && selectedIds.size > 0 ? ` (${selectedIds.size})` : '' }}
</el-button> </el-button>
</div> </div>
</template> </template>
@@ -122,7 +145,7 @@
<script setup> <script setup>
import { ref, watch, computed } from 'vue' import { ref, watch, computed } from 'vue'
import { ElMessage } from 'element-plus' 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' import { getFileList, getFileDetail, uploadFile } from '@/api/admin/file'
// Props // Props
@@ -150,17 +173,21 @@ const activeTab = ref('fileLibrary')
const fileList = ref([]) const fileList = ref([])
const loading = ref(false) const loading = ref(false)
const selectedId = ref('') const selectedId = ref('')
const selectedIds = ref(new Set()) // 多选模式下选中的文件ID集合
const currentPage = ref(1) const currentPage = ref(1)
const pageSize = ref(12) const pageSize = ref(12)
const total = ref(0) const total = ref(0)
const searchKeyword = ref('') const searchKeyword = ref('')
const uploadProgress = ref([]) const pendingFiles = ref([]) // 待上传文件列表
const uploading = ref(false) // 批量上传中
let fetchVersion = 0 // 防止 fetchFileList 竞态条件
// 监听 modelValue 变化 // 监听 modelValue 变化
watch(() => props.modelValue, (newVal) => { watch(() => props.modelValue, (newVal) => {
visible.value = newVal visible.value = newVal
if (newVal) { if (newVal) {
selectedId.value = props.currentFileId selectedId.value = props.currentFileId
selectedIds.value = new Set()
currentPage.value = 1 currentPage.value = 1
searchKeyword.value = '' searchKeyword.value = ''
fetchFileList() fetchFileList()
@@ -191,10 +218,10 @@ const processImageUrl = (url) => {
return decodeURIComponent(processedUrl) return decodeURIComponent(processedUrl)
} }
// 获取文件列表 // 获取文件列表(带版本号防止竞态条件)
const fetchFileList = async () => { const fetchFileList = async () => {
const currentFetchVersion = ++fetchVersion
loading.value = true loading.value = true
fileList.value = []
try { try {
const res = await getFileList({ const res = await getFileList({
@@ -202,32 +229,49 @@ const fetchFileList = async () => {
count: pageSize.value count: pageSize.value
}) })
// 如果有更新的请求发起,丢弃当前结果
if (currentFetchVersion !== fetchVersion) return
if (res.data.code === 200) { if (res.data.code === 200) {
const list = res.data.data.list || [] const list = res.data.data.list || []
total.value = res.data.data.all_count || 0 total.value = res.data.data.all_count || 0
// 获取每个文件详情 // 并行获取所有文件详情(替代逐个串行,大幅提升速度)
for (let i = 0; i < list.length; i++) { const detailPromises = list.map(item =>
try { getFileDetail({ file_id: item.id })
const res2 = await getFileDetail({ file_id: list[i].id }) .then(res2 => {
if (res2.data.code === 200) { if (res2.data.code === 200) {
fileList.value.push({ return {
id: res2.data.data.data.id, id: res2.data.data.data.id,
url: res2.data.data.url, url: res2.data.data.url,
size: res2.data.data.data.size, size: res2.data.data.data.size,
realName: res2.data.data.data.realName realName: res2.data.data.data.realName
}) }
} }
} catch (error) { return null
console.error('获取文件详情失败:', error) })
} .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) { } catch (error) {
console.error('获取文件列表失败:', error) if (currentFetchVersion === fetchVersion) {
ElMessage.error('获取文件列表失败') console.error('获取文件列表失败:', error)
ElMessage.error('获取文件列表失败')
}
} finally { } finally {
loading.value = false if (currentFetchVersion === fetchVersion) {
loading.value = false
}
} }
} }
@@ -279,65 +323,102 @@ const formatFileSize = (size) => {
// 选择文件 // 选择文件
const selectFile = (file) => { 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 handleFileChange = (file) => {
const isImage = file.type.startsWith('image/') const rawFile = file.raw
const isLt5M = file.size / 1024 / 1024 < 5 if (!rawFile) return
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 progressId = Date.now() + Math.random() const isImage = rawFile.type.startsWith('image/')
uploadProgress.value.push({ if (!isImage) {
id: progressId, ElMessage.error(`${rawFile.name} 不是图片文件,已跳过`)
name: file.name, return
percentage: 0 }
// 验证文件大小
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 { try {
const res = await uploadFile(formData) const res = await uploadFile(formData)
// 移除进度跟踪
uploadProgress.value = uploadProgress.value.filter(p => p.id !== progressId)
if (res.data.code === 200) { 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 currentPage.value = 1
await fetchFileList() await fetchFileList()
// 切换到文件库标签页
activeTab.value = 'fileLibrary' activeTab.value = 'fileLibrary'
// 自动选择新上传的文件
if (res.data.data?.id) {
selectedId.value = res.data.data.id
}
} else { } else {
ElMessage.error(res.data.msg || '上传失败') ElMessage.error(res.data.msg || '上传失败')
} }
} catch (error) { } catch (error) {
// 移除进度跟踪 console.error('批量上传失败:', error)
uploadProgress.value = uploadProgress.value.filter(p => p.id !== progressId) ElMessage.error('上传失败,请重试')
console.error('上传失败:', error) } finally {
ElMessage.error('上传失败') uploading.value = false
} }
} }
@@ -350,23 +431,43 @@ const handleImageError = (event) => {
const handleClose = () => { const handleClose = () => {
visible.value = false visible.value = false
selectedId.value = '' selectedId.value = ''
selectedIds.value = new Set()
fileList.value = [] fileList.value = []
currentPage.value = 1 currentPage.value = 1
total.value = 0 total.value = 0
searchKeyword.value = '' searchKeyword.value = ''
uploadProgress.value = [] // 清理待上传文件的预览URL
pendingFiles.value.forEach(f => {
if (f.previewUrl) URL.revokeObjectURL(f.previewUrl)
})
pendingFiles.value = []
} }
// 确认选择 // 确认选择
const handleConfirm = () => { const handleConfirm = () => {
if (selectedId.value) { if (props.multiple) {
const selectedFile = fileList.value.find(file => file.id === selectedId.value) // 多选模式:返回选中的文件数组
emit('confirm', { if (selectedIds.value.size === 0) return
id: selectedId.value, const selectedFiles = fileList.value
url: selectedFile?.url || '', .filter(file => selectedIds.value.has(file.id))
realName: selectedFile?.realName || '' .map(file => ({
}) id: file.id,
url: file.url || '',
realName: file.realName || ''
}))
emit('confirm', selectedFiles)
handleClose() 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> </script>
@@ -426,6 +527,39 @@ const handleConfirm = () => {
background-color: #f0f9ff; 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 { .file-preview {
width: 100px; width: 100px;
height: 100px; height: 100px;
@@ -466,29 +600,74 @@ const handleConfirm = () => {
} }
.upload-section { .upload-section {
padding: 40px 20px; padding: 20px;
text-align: center; text-align: center;
} }
.upload-progress { /* 待上传文件列表 */
margin-top: 30px; .pending-files {
margin-top: 20px;
text-align: left; text-align: left;
} }
.upload-progress h4 { .pending-header {
margin-bottom: 15px; display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.pending-header h4 {
margin: 0;
color: #303133; color: #303133;
}
.progress-item {
margin-bottom: 10px;
}
.progress-item span {
display: block;
margin-bottom: 5px;
font-size: 14px; 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 { .pagination-container {
+638 -33
View File
@@ -22,6 +22,9 @@
<el-button type="primary" @click="handleAddSetting"> <el-button type="primary" @click="handleAddSetting">
<el-icon><Plus /></el-icon>新增配置 <el-icon><Plus /></el-icon>新增配置
</el-button> </el-button>
<el-button type="success" @click="handleBatchImport">
<el-icon><UploadFilled /></el-icon>一键导入配置
</el-button>
<el-button type="danger" :disabled="selectedRows.length === 0" @click="handleBatchDelete"> <el-button type="danger" :disabled="selectedRows.length === 0" @click="handleBatchDelete">
<el-icon><Delete /></el-icon>批量删除 ({{ selectedRows.length }}) <el-icon><Delete /></el-icon>批量删除 ({{ selectedRows.length }})
</el-button> </el-button>
@@ -130,7 +133,7 @@
<span v-else>-</span> <span v-else>-</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="操作" width="200" fixed="right"> <el-table-column label="操作" width="320" fixed="right">
<template #default="{ row }"> <template #default="{ row }">
<el-button v-if="row.type === 'group'" type="primary" link size="small" @click="handleEditGroup(row.data)"> <el-button v-if="row.type === 'group'" type="primary" link size="small" @click="handleEditGroup(row.data)">
编辑 编辑
@@ -138,6 +141,12 @@
<el-button v-if="row.type === 'group'" type="success" link size="small" @click="handleAddSettingToGroup(row.data)"> <el-button v-if="row.type === 'group'" type="success" link size="small" @click="handleAddSettingToGroup(row.data)">
新增配置 新增配置
</el-button> </el-button>
<el-button v-if="row.type === 'group'" type="warning" link size="small" @click="handleCopyGroupSettings(row)">
一键复制
</el-button>
<el-button v-if="row.type === 'group'" type="success" link size="small" @click="handleImportToGroup(row.data)">
一键导入
</el-button>
<el-button v-if="row.type === 'setting'" type="primary" link size="small" @click="handleEditSetting(row.data)"> <el-button v-if="row.type === 'setting'" type="primary" link size="small" @click="handleEditSetting(row.data)">
编辑 编辑
</el-button> </el-button>
@@ -340,13 +349,20 @@
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="值" prop="value"> <el-form-item label="值" prop="value">
<el-input <div v-if="settingForm.type === 'string'" style="width: 100%">
v-if="settingForm.type === 'string'" <div v-if="isJsonValue" class="json-toolbar">
v-model="settingForm.value" <el-tag size="small" type="success">JSON</el-tag>
type="textarea" <el-button size="small" @click="formatJson">格式化</el-button>
:rows="3" <el-button size="small" @click="compressJson">压缩</el-button>
placeholder="请输入配置值" </div>
/> <el-input
v-model="settingForm.value"
type="textarea"
:rows="isJsonValue ? 12 : 3"
placeholder="请输入配置值"
:input-style="isJsonValue ? { fontFamily: 'Consolas, Monaco, monospace', fontSize: '13px', lineHeight: '1.5' } : {}"
/>
</div>
<el-input-number <el-input-number
v-else-if="settingForm.type === 'int'" v-else-if="settingForm.type === 'int'"
v-model="settingForm.value" v-model="settingForm.value"
@@ -539,6 +555,145 @@
</template> </template>
</el-dialog> </el-dialog>
<!-- 一键导入配置对话框 -->
<el-dialog
v-model="batchImportDialogVisible"
title="一键导入配置"
width="900px"
destroy-on-close
class="dialog-scrollable"
:close-on-click-modal="!batchImportLoading"
:close-on-press-escape="!batchImportLoading"
:show-close="!batchImportLoading"
>
<el-alert
title="使用说明"
type="info"
:closable="false"
show-icon
style="margin-bottom: 16px"
>
<template #default>
<div style="line-height: 1.8">
粘贴通过一键复制导出的内容即可自动导入系统会自动识别配置组名称不存在则自动创建<br />
格式示例<br />
<code>[配置组] 移动端-全局配置</code><br />
<code>| 配置名 | 类型 | 默认值 | 说明 |</code><br />
<code>|--------|------|--------|------|</code><br />
<code>| `移动端_主题主色` | `string` | `#2B7EFB` | 按钮链接选中态主色 |</code>
</div>
</template>
</el-alert>
<!-- 导入进度面板 -->
<div v-if="batchImportLoading" class="import-progress-panel">
<div class="progress-header">
<el-icon class="is-loading" :size="20" color="#409eff"><Loading /></el-icon>
<span style="font-weight: 600; font-size: 15px; color: #303133">正在导入...</span>
</div>
<div class="progress-info">
<div v-if="batchImportStatusText" style="margin-bottom: 12px; color: #606266; font-size: 13px;">
{{ batchImportStatusText }}
</div>
<el-progress
:percentage="batchImportTotal > 0 ? Math.round(batchImportProgress / batchImportTotal * 100) : 0"
:stroke-width="18"
:text-inside="true"
striped
striped-flow
/>
<div style="margin-top: 8px; color: #909399; font-size: 12px; text-align: center;">
{{ batchImportProgress }} / {{ batchImportTotal }}
</div>
</div>
</div>
<!-- 非导入中时显示表单 -->
<template v-if="!batchImportLoading">
<el-form label-width="100px">
<!-- 自动识别的配置组 -->
<el-form-item label="配置组">
<div v-if="batchImportGroupName" style="display: flex; align-items: center; gap: 8px; width: 100%;">
<el-tag :type="batchImportGroupExists ? 'success' : 'warning'" size="large" style="font-size: 14px;">
<el-icon style="margin-right: 4px;"><Folder /></el-icon>
{{ batchImportGroupName }}
</el-tag>
<span v-if="batchImportGroupExists" style="color: #67c23a; font-size: 12px;">已存在将导入到此配置组</span>
<span v-else style="color: #e6a23c; font-size: 12px;">不存在将自动创建后导入</span>
</div>
<span v-else style="color: #c0c4cc; font-size: 13px;">粘贴内容后自动识别首行 <code>[配置组] 名称</code></span>
</el-form-item>
<el-form-item label="默认公开">
<el-switch v-model="batchImportOpen" />
<span style="margin-left: 8px; color: #909399; font-size: 13px">导入的配置项是否默认对外公开</span>
</el-form-item>
<el-form-item label="导入内容">
<el-input
v-model="batchImportText"
type="textarea"
:rows="10"
placeholder="粘贴通过「一键复制」导出的内容..."
style="font-family: monospace"
/>
</el-form-item>
</el-form>
<!-- 解析预览 -->
<div v-if="batchImportParsed.length > 0" style="margin-top: 8px">
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px">
<span style="font-weight: 600; font-size: 14px; color: #303133">
解析预览 {{ batchImportParsed.length }}
</span>
<el-button type="primary" link @click="parseBatchImportText">
<el-icon><Search /></el-icon>重新解析
</el-button>
</div>
<el-table :data="batchImportParsed" border size="small" max-height="300">
<el-table-column type="index" label="#" width="50" />
<el-table-column prop="name" label="配置名" min-width="180">
<template #default="{ row }">
<span :style="{ color: row._duplicate ? '#F56C6C' : '' }">
{{ row.name }}
<el-tag v-if="row._duplicate" type="danger" size="small" style="margin-left: 4px">重复</el-tag>
</span>
</template>
</el-table-column>
<el-table-column prop="type" label="类型" width="120">
<template #default="{ row }">
<el-tag size="small">{{ row.type }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="value" label="默认值" min-width="160">
<template #default="{ row }">
<span class="text-value">{{ row.value }}</span>
</template>
</el-table-column>
<el-table-column prop="note" label="说明" min-width="200" />
<el-table-column label="操作" width="70" fixed="right">
<template #default="{ $index }">
<el-button link type="danger" size="small" @click="batchImportParsed.splice($index, 1)">移除</el-button>
</template>
</el-table-column>
</el-table>
</div>
</template>
<template #footer>
<el-button @click="batchImportDialogVisible = false" :disabled="batchImportLoading">取消</el-button>
<el-button type="primary" @click="parseBatchImportText" :disabled="!batchImportText.trim() || batchImportLoading">
解析预览
</el-button>
<el-button
type="success"
:loading="batchImportLoading"
:disabled="batchImportParsed.length === 0 || !batchImportGroupName"
@click="submitBatchImport"
>
确认导入 ({{ batchImportParsed.length }})
</el-button>
</template>
</el-dialog>
<!-- 图片查看器 --> <!-- 图片查看器 -->
<el-dialog v-model="imageViewerVisible" width="auto" destroy-on-close> <el-dialog v-model="imageViewerVisible" width="auto" destroy-on-close>
<img :src="currentViewImage" style="max-width: 100%; max-height: 80vh;" /> <img :src="currentViewImage" style="max-width: 100%; max-height: 80vh;" />
@@ -548,6 +703,7 @@
<ImageSelector <ImageSelector
v-model="imageSelectorVisible" v-model="imageSelectorVisible"
:current-file-id="currentImageSelectorFileId" :current-file-id="currentImageSelectorFileId"
:multiple="imageSelectorMode === 'list'"
@confirm="handleImageSelectorConfirm" @confirm="handleImageSelectorConfirm"
/> />
</div> </div>
@@ -571,7 +727,7 @@ import {
setSettingOpen, setSettingOpen,
deleteSetting deleteSetting
} from '@/api/admin/setting' } from '@/api/admin/setting'
import { uploadFile } from '@/api/admin/file' import { uploadFile, getFileDetail, downloadFile } from '@/api/admin/file'
import ImageSelector from '@/components/admin/ImageSelector.vue' import ImageSelector from '@/components/admin/ImageSelector.vue'
const route = useRoute() const route = useRoute()
@@ -673,9 +829,6 @@ const settingRules = {
name: [ name: [
{ required: true, message: '请输入配置名称', trigger: 'blur' } { required: true, message: '请输入配置名称', trigger: 'blur' }
], ],
value: [
{ required: true, message: '请输入配置值', trigger: 'blur' }
],
type: [ type: [
{ required: true, message: '请选择配置类型', trigger: 'change' } { required: true, message: '请选择配置类型', trigger: 'change' }
], ],
@@ -1033,7 +1186,7 @@ const handleAddSetting = () => {
value: '', value: '',
type: 'string', type: 'string',
settingGroupID: selectedNode.value?.type === 'group' ? selectedNode.value.data.id : undefined, settingGroupID: selectedNode.value?.type === 'group' ? selectedNode.value.data.id : undefined,
open: false, open: true,
note: '' note: ''
}) })
fileInfo.value = null fileInfo.value = null
@@ -1051,7 +1204,7 @@ const handleAddSettingToGroup = (groupData) => {
value: '', value: '',
type: 'string', type: 'string',
settingGroupID: groupData.id, settingGroupID: groupData.id,
open: false, open: true,
note: '' note: ''
}) })
fileInfo.value = null fileInfo.value = null
@@ -1178,6 +1331,52 @@ const fetchAllGroupList = async () => {
} }
} }
// JSON 值检测与格式化
const isJsonValue = computed(() => {
if (settingForm.type !== 'string' || !settingForm.value) return false
const v = settingForm.value.trim()
return (v.startsWith('{') && v.endsWith('}')) || (v.startsWith('[') && v.endsWith(']'))
})
const formatJson = () => {
try {
const parsed = JSON.parse(settingForm.value)
settingForm.value = JSON.stringify(parsed, null, 2)
} catch (e) {
ElMessage.warning('JSON 格式不合法,无法格式化')
}
}
const compressJson = () => {
try {
const parsed = JSON.parse(settingForm.value)
settingForm.value = JSON.stringify(parsed)
} catch (e) {
ElMessage.warning('JSON 格式不合法,无法压缩')
}
}
// 类型切换处理 - 重置值和相关状态
const handleTypeChange = (newType) => {
// 重置表单值为对应类型的默认值
if (newType === 'bool') {
settingForm.value = false
} else if (newType === 'int') {
settingForm.value = 0
} else if (newType === 'float') {
settingForm.value = 0.0
} else {
settingForm.value = ''
}
// 重置所有相关状态
fileInfo.value = null
fileListInfo.value = []
editableFormStringList.value = []
editableFormFileList.value = []
newStringItem.value = ''
}
// 文件相关函数 // 文件相关函数
const handleFileChange = async (file) => { const handleFileChange = async (file) => {
fileUploading.value = true fileUploading.value = true
@@ -1551,8 +1750,8 @@ const handleFormFileDrop = (event, dropIndex) => {
editableFormFileList.value = newList editableFormFileList.value = newList
formFileDraggedIndex.value = -1 formFileDraggedIndex.value = -1
// 更新fileListInfo和表单值 // 更新fileListInfo和表单值(使用独立副本,避免引用同一数组)
fileListInfo.value = newList fileListInfo.value = newList.map(item => ({ ...item }))
updateFormFileListValue() updateFormFileListValue()
event.target.style.opacity = '1' event.target.style.opacity = '1'
@@ -1714,10 +1913,38 @@ const updateFormFileListValue = () => {
} }
// 文件预览 // 文件预览
const previewFile = (fileId) => { const previewFile = async (fileId) => {
// 这里可以实现文件预览逻辑 if (!fileId) return
console.log('预览文件:', fileId) try {
ElMessage.info('文件预览功能待实现') // 先获取文件详情拿到下载URL
const res = await getFileDetail({ file_id: fileId })
if (res.data.code === 200 && res.data.data?.url) {
const url = processImageUrl(res.data.data.url)
const fileName = res.data.data.data?.realName || ''
// 判断是否为图片
const imageExts = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg']
const ext = fileName.toLowerCase().substring(fileName.lastIndexOf('.'))
if (imageExts.includes(ext) || url.match(/\.(jpg|jpeg|png|gif|bmp|webp|svg)/i)) {
// 图片类型:使用内置图片查看器
currentViewImage.value = url
imageViewerVisible.value = true
} else {
// 非图片类型:在新窗口打开
window.open(url, '_blank')
}
} else {
// 降级:尝试用下载接口
const downRes = await downloadFile({ file_id: fileId })
if (downRes.data.code === 200 && downRes.data.data?.url) {
window.open(processImageUrl(downRes.data.data.url), '_blank')
} else {
ElMessage.error('获取文件预览失败')
}
}
} catch (error) {
console.error('文件预览失败:', error)
ElMessage.error('文件预览失败')
}
} }
const previewUrl = (url) => { const previewUrl = (url) => {
@@ -1781,7 +2008,50 @@ const openImageSelectorForListItem = (index) => {
} }
// 处理图像选择器确认 // 处理图像选择器确认
const handleImageSelectorConfirm = (selectedFile) => { const handleImageSelectorConfirm = (selectedFileOrFiles) => {
if (imageSelectorMode.value === 'list' && Array.isArray(selectedFileOrFiles)) {
// 多选文件列表模式 - 接收文件数组
if (selectedFileOrFiles.length === 0) {
ElMessage.warning('未选择任何文件')
return
}
if (!fileListInfo.value) {
fileListInfo.value = []
}
for (const selectedFile of selectedFileOrFiles) {
if (!selectedFile || !selectedFile.id) continue
const newFile = {
id: selectedFile.id,
url: processImageUrl(selectedFile.url || ''),
realName: selectedFile.realName || '文件',
saveName: selectedFile.realName || 'file',
size: selectedFile.size || 0
}
fileListInfo.value.push(newFile)
// 同步更新 editableFormFileList(表单UI读取的数据源)
editableFormFileList.value.push({
id: selectedFile.id,
url: processImageUrl(selectedFile.url || ''),
localUrl: '',
realName: selectedFile.realName || '文件',
saveName: selectedFile.realName || 'file',
size: selectedFile.size || 0,
uploading: false
})
}
updateFormFileListValue()
ElMessage.success(`已添加 ${selectedFileOrFiles.length} 个文件`)
imageSelectorVisible.value = false
return
}
// 以下为单选模式处理
const selectedFile = selectedFileOrFiles
if (!selectedFile || !selectedFile.id) { if (!selectedFile || !selectedFile.id) {
ElMessage.warning('选择的文件无效') ElMessage.warning('选择的文件无效')
return return
@@ -1798,7 +2068,7 @@ const handleImageSelectorConfirm = (selectedFile) => {
size: selectedFile.size || 0 size: selectedFile.size || 0
} }
} else if (imageSelectorMode.value === 'list') { } else if (imageSelectorMode.value === 'list') {
// 文件列表模式 // 单选兼容:文件列表模式(单个文件对象)
if (!fileListInfo.value) { if (!fileListInfo.value) {
fileListInfo.value = [] fileListInfo.value = []
} }
@@ -1811,21 +2081,45 @@ const handleImageSelectorConfirm = (selectedFile) => {
size: selectedFile.size || 0 size: selectedFile.size || 0
} }
fileListInfo.value.push(newFile) fileListInfo.value.push(newFile)
updateFileListValue()
// 同步更新 editableFormFileList(表单UI读取的数据源)
editableFormFileList.value.push({
id: selectedFile.id,
url: processImageUrl(selectedFile.url || ''),
localUrl: '',
realName: selectedFile.realName || '文件',
saveName: selectedFile.realName || 'file',
size: selectedFile.size || 0,
uploading: false
})
updateFormFileListValue()
} else if (imageSelectorMode.value === 'list-item') { } else if (imageSelectorMode.value === 'list-item') {
// 文件列表中的特定项替换模式 // 文件列表中的特定项替换模式
const index = currentImageSelectorFileId.value const index = currentImageSelectorFileId.value
if (fileListInfo.value && fileListInfo.value[index] !== undefined) { const updatedFile = {
fileListInfo.value[index] = { id: selectedFile.id,
id: selectedFile.id, url: processImageUrl(selectedFile.url || ''),
url: processImageUrl(selectedFile.url || ''), realName: selectedFile.realName || '文件',
realName: selectedFile.realName || '文件', saveName: selectedFile.realName || 'file',
saveName: selectedFile.realName || 'file', size: selectedFile.size || 0
size: selectedFile.size || 0
}
updateFileListValue()
ElMessage.success('文件替换成功')
} }
if (fileListInfo.value && fileListInfo.value[index] !== undefined) {
fileListInfo.value[index] = updatedFile
}
// 同步更新 editableFormFileList
if (editableFormFileList.value && editableFormFileList.value[index] !== undefined) {
editableFormFileList.value[index] = {
...updatedFile,
localUrl: '',
uploading: false
}
}
updateFormFileListValue()
ElMessage.success('文件替换成功')
} }
imageSelectorVisible.value = false imageSelectorVisible.value = false
@@ -2030,6 +2324,290 @@ const submitSettingForm = async () => {
} }
} }
// ==================== 一键导入配置 ====================
const batchImportDialogVisible = ref(false)
const batchImportText = ref('')
const batchImportParsed = ref([])
const batchImportGroupId = ref(undefined)
const batchImportGroupName = ref('')
const batchImportGroupExists = ref(false)
const batchImportOpen = ref(true)
const batchImportLoading = ref(false)
const batchImportProgress = ref(0)
const batchImportTotal = ref(0)
const batchImportStatusText = ref('')
const handleBatchImport = async () => {
batchImportText.value = ''
batchImportParsed.value = []
batchImportGroupId.value = undefined
batchImportGroupName.value = ''
batchImportGroupExists.value = false
batchImportOpen.value = true
batchImportProgress.value = 0
batchImportTotal.value = 0
batchImportStatusText.value = ''
batchImportDialogVisible.value = true
try {
const clipText = await navigator.clipboard.readText()
if (clipText && clipText.trim()) {
batchImportText.value = clipText.trim()
}
} catch (e) {
console.warn('读取剪贴板失败(可能需要用户授权):', e)
}
}
/**
* 解析 Markdown 表格文本为配置项数组
* 支持格式:
* | 配置名 | 类型 | 默认值 | 说明 |
* |--------|------|--------|------|
* | `移动端_主题主色` | `string` | `#2B7EFB` | 按钮、链接、选中态主色 |
*/
const parseBatchImportText = () => {
const text = batchImportText.value.trim()
if (!text) {
ElMessage.warning('请先粘贴导入内容')
return
}
const lines = text.split('\n').map(l => l.trim()).filter(l => l.length > 0)
const validTypes = ['string', 'int', 'float', 'bool', 'file', 'file_list', 'string_list']
const results = []
const stripBackticks = (str) => str.replace(/`/g, '').trim()
// 识别 [配置组] 行
let detectedGroupName = ''
for (const line of lines) {
const groupMatch = line.match(/^\[配置组\]\s*(.+)$/)
if (groupMatch) {
detectedGroupName = groupMatch[1].trim()
continue
}
if (/^[\s|:\-]+$/.test(line)) continue
if (/配置名|类型.*默认值|名称.*类型/.test(line)) continue
const parts = line.split('|').map(s => s.trim()).filter(s => s.length > 0)
if (parts.length < 3) continue
const name = stripBackticks(parts[0])
const type = stripBackticks(parts[1]).toLowerCase()
const value = stripBackticks(parts[2])
const note = parts.length >= 4 ? stripBackticks(parts[3]) : ''
if (!name) continue
if (!validTypes.includes(type)) {
console.warn(`跳过无效类型 "${type}" (配置名: ${name})`)
continue
}
results.push({ name, type, value, note, _duplicate: false })
}
// 更新配置组识别结果
if (detectedGroupName) {
batchImportGroupName.value = detectedGroupName
const existingGroup = allGroupList.value.find(g => g.name === detectedGroupName)
if (existingGroup) {
batchImportGroupId.value = existingGroup.id
batchImportGroupExists.value = true
} else {
batchImportGroupId.value = undefined
batchImportGroupExists.value = false
}
}
// 标记重复项
const nameSet = new Set()
results.forEach(item => {
if (nameSet.has(item.name)) {
item._duplicate = true
} else {
nameSet.add(item.name)
}
})
batchImportParsed.value = results
if (results.length === 0) {
ElMessage.warning('未能解析到有效的配置项,请检查格式')
} else {
const groupInfo = detectedGroupName ? `,配置组:${detectedGroupName}` : ''
ElMessage.success(`成功解析 ${results.length} 条配置项${groupInfo}`)
}
}
// 监听文本变化自动解析
watch(batchImportText, (val) => {
if (val && val.trim().split('\n').length >= 2) {
parseBatchImportText()
}
})
const submitBatchImport = async () => {
if (!batchImportGroupName.value) {
ElMessage.warning('未识别到配置组名称,请检查内容格式')
return
}
const items = batchImportParsed.value.filter(i => !i._duplicate)
if (items.length === 0) {
ElMessage.warning('没有可导入的配置项')
return
}
batchImportLoading.value = true
batchImportProgress.value = 0
batchImportTotal.value = items.length
let successCount = 0
let failCount = 0
const errors = []
// 步骤 1:确保配置组存在
let targetGroupId = batchImportGroupId.value
if (!targetGroupId) {
batchImportStatusText.value = `正在创建配置组「${batchImportGroupName.value}」...`
try {
const res = await createSettingGroup({ name: batchImportGroupName.value, note: '' })
if (res.data.code === 200) {
targetGroupId = res.data.data?.id || res.data.data?.ID
batchImportGroupId.value = targetGroupId
batchImportGroupExists.value = true
} else {
batchImportLoading.value = false
batchImportStatusText.value = ''
ElMessage.error(`创建配置组失败:${res.data.message || '未知错误'}`)
return
}
} catch (error) {
batchImportLoading.value = false
batchImportStatusText.value = ''
ElMessage.error(`创建配置组失败:${error.response?.data?.message || error.message}`)
return
}
}
// 步骤 2:逐条导入配置项
for (let i = 0; i < items.length; i++) {
const item = items[i]
batchImportStatusText.value = `正在导入:${item.name}`
batchImportProgress.value = i
try {
const res = await createSetting({
name: item.name,
value: item.value,
type: item.type,
setting_group_id: targetGroupId,
open: batchImportOpen.value,
note: item.note
})
if (res.data.code === 200) {
if (batchImportOpen.value && res.data.data?.id) {
try {
await setSettingOpen({ id: res.data.data.id, open: true })
} catch (e) {
console.warn('设置公开状态失败:', item.name, e)
}
}
successCount++
} else {
failCount++
errors.push(`${item.name}: ${res.data.message || '未知错误'}`)
}
} catch (error) {
failCount++
errors.push(`${item.name}: ${error.response?.data?.message || error.message || '请求失败'}`)
}
}
batchImportProgress.value = items.length
batchImportStatusText.value = '导入完成'
batchImportLoading.value = false
if (failCount === 0) {
ElMessage.success(`全部导入成功!共 ${successCount} 条,配置组:${batchImportGroupName.value}`)
batchImportDialogVisible.value = false
} else {
ElMessage.warning(`导入完成:成功 ${successCount} 条,失败 ${failCount}`)
if (errors.length > 0) {
console.error('批量导入失败详情:', errors)
}
}
// 刷新配置组树
await loadGroups()
await nextTick()
const groupNode = treeData.value.find(item =>
item.type === 'group' && item.data.id === targetGroupId
)
if (groupNode) {
groupNode._children = []
groupNode._expanded = false
await toggleExpand(groupNode)
}
}
// 一键复制:将配置组的所有配置项格式化为 Markdown 批量导入表格并复制到剪贴板
const handleCopyGroupSettings = async (row) => {
const groupId = row.data.id
const groupName = row.data.name
try {
let settings = []
if (row._expanded && row._children && row._children.length > 0) {
settings = row._children.map(child => child.data)
} else {
const res = await getSettingList({ group_id: groupId, page: 1, count: 100 })
if (res.data.code === 200) {
settings = res.data.data.data || []
}
}
if (settings.length === 0) {
ElMessage.warning(`配置组「${groupName}」下没有配置项`)
return
}
const lines = [
`[配置组] ${groupName}`,
'',
'| 配置名 | 类型 | 默认值 | 说明 |',
'|--------|------|--------|------|'
]
settings.forEach(s => {
const name = s.name || ''
const type = s.type || 'string'
const value = (s.value != null ? String(s.value) : '').replace(/\|/g, '\\|')
const note = (s.note || '-').replace(/\|/g, '\\|')
lines.push(`| \`${name}\` | \`${type}\` | \`${value}\` | ${note} |`)
})
const text = lines.join('\n')
await navigator.clipboard.writeText(text)
ElMessage.success(`已复制「${groupName}」的 ${settings.length} 条配置项到剪贴板`)
} catch (error) {
console.error('一键复制失败:', error)
ElMessage.error('复制失败,请重试')
}
}
// 一键导入:打开批量导入弹窗并预选当前配置组
const handleImportToGroup = (groupData) => {
batchImportText.value = ''
batchImportParsed.value = []
batchImportGroupId.value = groupData.id
batchImportGroupName.value = groupData.name
batchImportGroupExists.value = true
batchImportOpen.value = true
batchImportProgress.value = 0
batchImportTotal.value = 0
batchImportStatusText.value = ''
batchImportDialogVisible.value = true
}
// 初始化 // 初始化
onMounted(() => { onMounted(() => {
// 初始化时加载配置组数据 // 初始化时加载配置组数据
@@ -2456,6 +3034,33 @@ onMounted(() => {
background-color: #2c3e50; background-color: #2c3e50;
} }
/* JSON 编辑工具栏 */
.json-toolbar {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
}
/* 导入进度面板 */
.import-progress-panel {
padding: 40px 20px;
text-align: center;
}
.progress-header {
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
margin-bottom: 24px;
}
.progress-info {
max-width: 500px;
margin: 0 auto;
}
/* 文件上传相关样式 */ /* 文件上传相关样式 */
.file-upload-section { .file-upload-section {
width: 100%; width: 100%;
+60 -73
View File
@@ -537,23 +537,69 @@ const handleRemoveFile = (file, fileList) => {
uploadFileList.value = fileList uploadFileList.value = fileList
} }
// 提交上传 // 提交上传(批量上传:将所有文件合并为一次请求)
const handleSubmitUpload = () => { const handleSubmitUpload = async () => {
if (uploadFileList.value.length === 0) { if (uploadFileList.value.length === 0) {
ElMessage.warning('请至少选择一个文件') ElMessage.warning('请至少选择一个文件')
return return
} }
// 触发所有待上传文件的上传
const filesToUpload = uploadFileList.value.filter(file => // 筛选待上传的有效文件
file.status !== 'success' && file.status !== 'uploading' const validTypes = ['image/jpeg', 'image/png', 'image/gif', 'application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document']
) const filesToUpload = uploadFileList.value.filter(file => {
if (file.status === 'success') return false
const raw = file.raw
if (!raw) return false
const isValidType = validTypes.includes(raw.type)
const isLt10M = raw.size / 1024 / 1024 < 10
if (!isValidType) {
ElMessage.warning(`文件 ${raw.name} 格式不符合要求,已跳过`)
return false
}
if (!isLt10M) {
ElMessage.warning(`文件 ${raw.name} 大小超过 10MB,已跳过`)
return false
}
return true
})
if (filesToUpload.length === 0) { if (filesToUpload.length === 0) {
ElMessage.info('所有文件已上传完成') ElMessage.info('没有可上传的有效文件')
return return
} }
// 逐个提交文件
uploadRef.value?.submit() // 构建 FormData,多个 file_names 和 files 条目在同一请求中
const formData = new FormData()
filesToUpload.forEach(file => {
formData.append('file_names', file.raw.name)
formData.append('files', file.raw)
})
// 添加上传类型
if (uploadForm.update_type) {
formData.append('update_type', uploadForm.update_type)
}
// 添加是否开放下载
formData.append('open_down', uploadForm.open_down ? 'true' : 'false')
try {
const res = await uploadFile(formData)
if (res && res.data && res.data.code === 200) {
ElMessage.success(`成功上传 ${filesToUpload.length} 个文件`)
setTimeout(() => {
uploadDialogVisible.value = false
uploadFileList.value = []
fetchFileList()
}, 500)
} else {
const errorMsg = res?.data?.message || res?.data?.msg || '上传失败'
ElMessage.error(errorMsg)
}
} catch (error) {
console.error('批量上传失败:', error)
ElMessage.error(error?.response?.data?.message || error?.message || '上传失败,请重试')
}
} }
// 上传前检查(只做提示,不阻止文件添加到列表) // 上传前检查(只做提示,不阻止文件添加到列表)
@@ -568,73 +614,14 @@ const beforeUpload = (file) => {
if (!isLt10M) { if (!isLt10M) {
ElMessage.warning(`文件 ${file.name} 大小超过 10MB`) ElMessage.warning(`文件 ${file.name} 大小超过 10MB`)
} }
// 允许文件添加到列表,在上传时再进行验证 // 允许文件添加到列表,在提交时再进行验证
return true return true
} }
// 自定义上传方法 // 自定义上传方法(保留为空壳,实际上传由 handleSubmitUpload 批量处理)
const handleCustomUpload = async (options) => { const handleCustomUpload = async (options) => {
const { file, onSuccess, onError } = options // 不做任何操作,所有上传由 handleSubmitUpload 统一批量处理
console.log('开始上传文件:', file) // el-upload 的 auto-upload 已设为 false,此方法不会被自动调用
// 在上传前进行验证
const isValidType = ['image/jpeg', 'image/png', 'image/gif', 'application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'].includes(file.type)
const isLt10M = file.size / 1024 / 1024 < 10
if (!isValidType) {
const error = new Error(`文件 ${file.name} 格式不符合要求(仅支持 JPG/PNG/GIF/PDF/DOC/DOCX`)
// 标记为校验类错误,on-error 中不再弹 error 提示
error.isValidation = true
onError(error, file)
return
}
if (!isLt10M) {
const error = new Error(`文件 ${file.name} 大小超过 10MB`)
error.isValidation = true
onError(error, file)
return
}
try {
const formData = new FormData()
// 根据 API 文档,字段名应该是 files(复数)
formData.append('files', file)
// 添加文件名列表(虽然 API 文档说是数组,但实际传递时直接传字符串)
formData.append('file_names', file.name)
// 添加上传类型
if (uploadForm.update_type) {
formData.append('update_type', uploadForm.update_type)
}
// 添加是否开放下载
formData.append('open_down', uploadForm.open_down ? '1' : '0')
console.log('上传参数:', {
files: file.name,
file_names: [file.name],
update_type: uploadForm.update_type,
open_down: uploadForm.open_down
})
const res = await uploadFile(formData)
console.log('上传响应:', res)
// 根据返回码严格区分成功和失败
if (res && res.data && res.data.code === 200) {
console.log("上传成功")
} else {
const errorMsg = res?.data?.message || res?.data?.msg || '上传失败'
const error = new Error(errorMsg)
onError(error, file)
}
} catch (error) {
console.error('上传文件失败:', error)
const err = new Error(error?.response?.data?.message || error?.message || '上传失败')
onError(err, file)
}
} }
// 上传成功 // 上传成功
+7 -6
View File
@@ -434,10 +434,10 @@ const sendMessage = async () => {
try { try {
const formData = new FormData() const formData = new FormData()
// 添加所有文件 // 多个 file_names 和 files 条目在同一请求中
inputFiles.forEach((file) => { inputFiles.forEach((file) => {
formData.append('files', file)
formData.append('file_names', file.name) formData.append('file_names', file.name)
formData.append('files', file)
}) })
// 设置上传类型为工单 // 设置上传类型为工单
@@ -447,11 +447,11 @@ const sendMessage = async () => {
const uploadRes = await uploadFile(formData) const uploadRes = await uploadFile(formData)
if (uploadRes.data?.code === 200) { if (uploadRes.data?.code === 200) {
// 从返回的数据中提取文件ID(字段名是 id // 从返回的数据中提取文件ID
const data = uploadRes.data.data const data = uploadRes.data.data
if (Array.isArray(data)) { if (Array.isArray(data)) {
fileIds = data.map(item => String(item.id)) fileIds = data.map(item => String(item.id))
} else if (data.id) { } else if (data?.id) {
fileIds = [String(data.id)] fileIds = [String(data.id)]
} }
@@ -732,9 +732,10 @@ const saveEditMessage = async () => {
try { try {
const formData = new FormData() const formData = new FormData()
// 多个 file_names 和 files 条目在同一请求中
editMessageFiles.value.forEach((file) => { editMessageFiles.value.forEach((file) => {
formData.append('files', file)
formData.append('file_names', file.name) formData.append('file_names', file.name)
formData.append('files', file)
}) })
formData.append('update_type', 'work_order') formData.append('update_type', 'work_order')
@@ -746,7 +747,7 @@ const saveEditMessage = async () => {
const data = uploadRes.data.data const data = uploadRes.data.data
if (Array.isArray(data)) { if (Array.isArray(data)) {
newFileIds = data.map(item => String(item.id)) newFileIds = data.map(item => String(item.id))
} else if (data.id) { } else if (data?.id) {
newFileIds = [String(data.id)] newFileIds = [String(data.id)]
} }
} else { } else {
+1 -1
View File
@@ -33,7 +33,7 @@ export default defineConfig(({ mode }) => {
server: { server: {
// 强制绑定 IPv4 回环地址,避免 TUN/VPN 代理模式拦截 IPv6 或通配地址 // 强制绑定 IPv4 回环地址,避免 TUN/VPN 代理模式拦截 IPv6 或通配地址
host: '127.0.0.1', host: '127.0.0.1',
port: 5174, port: 5176,
strictPort: false, strictPort: false,
proxy: { proxy: {
'/api': { '/api': {