@@ -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)"
>
+
-
-
-
上传进度
-
-
{{ progress.name }}
-
+
+
+
+
+
+
![]()
+
{{ file.name }}
+
{{ formatFileSize(file.size) }}
+
移除
+
+
+
+ 开始上传 ({{ pendingFiles.length }} 个文件)
+
@@ -110,9 +133,9 @@
- 确定选择
+ 确定选择{{ props.multiple && selectedIds.size > 0 ? ` (${selectedIds.size})` : '' }}
@@ -122,7 +145,7 @@
@@ -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 {
diff --git a/src/views/system/SettingManage.vue b/src/views/system/SettingManage.vue
index 4a2a1de..bad3442 100644
--- a/src/views/system/SettingManage.vue
+++ b/src/views/system/SettingManage.vue
@@ -22,6 +22,9 @@
新增配置
+
+ 一键导入配置
+
批量删除 ({{ selectedRows.length }})
@@ -130,7 +133,7 @@
-
-
+
编辑
@@ -138,6 +141,12 @@
新增配置
+
+ 一键复制
+
+
+ 一键导入
+
编辑
@@ -340,13 +349,20 @@
-
+
+
+ JSON
+ 格式化
+ 压缩
+
+
+
+
+
+
+
+
+ 粘贴通过「一键复制」导出的内容即可自动导入。系统会自动识别配置组名称,不存在则自动创建。
+ 格式示例:
+ [配置组] 移动端-全局配置
+ | 配置名 | 类型 | 默认值 | 说明 |
+ |--------|------|--------|------|
+ | `移动端_主题主色` | `string` | `#2B7EFB` | 按钮、链接、选中态主色 |
+
+
+
+
+
+
+
+
+
+ {{ batchImportStatusText }}
+
+
+
+ {{ batchImportProgress }} / {{ batchImportTotal }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ batchImportGroupName }}
+
+ 已存在,将导入到此配置组
+ 不存在,将自动创建后导入
+
+ 粘贴内容后自动识别(首行 [配置组] 名称)
+
+
+
+ 导入的配置项是否默认对外公开
+
+
+
+
+
+
+
+
+
+
+ 解析预览(共 {{ batchImportParsed.length }} 条)
+
+
+ 重新解析
+
+
+
+
+
+
+
+ {{ row.name }}
+ 重复
+
+
+
+
+
+ {{ row.type }}
+
+
+
+
+ {{ row.value }}
+
+
+
+
+
+ 移除
+
+
+
+
+
+
+
+ 取消
+
+ 解析预览
+
+
+ 确认导入 ({{ batchImportParsed.length }})
+
+
+
+
@@ -548,6 +703,7 @@
@@ -571,7 +727,7 @@ import {
setSettingOpen,
deleteSetting
} 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'
const route = useRoute()
@@ -673,9 +829,6 @@ const settingRules = {
name: [
{ required: true, message: '请输入配置名称', trigger: 'blur' }
],
- value: [
- { required: true, message: '请输入配置值', trigger: 'blur' }
- ],
type: [
{ required: true, message: '请选择配置类型', trigger: 'change' }
],
@@ -1033,7 +1186,7 @@ const handleAddSetting = () => {
value: '',
type: 'string',
settingGroupID: selectedNode.value?.type === 'group' ? selectedNode.value.data.id : undefined,
- open: false,
+ open: true,
note: ''
})
fileInfo.value = null
@@ -1051,7 +1204,7 @@ const handleAddSettingToGroup = (groupData) => {
value: '',
type: 'string',
settingGroupID: groupData.id,
- open: false,
+ open: true,
note: ''
})
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) => {
fileUploading.value = true
@@ -1551,8 +1750,8 @@ const handleFormFileDrop = (event, dropIndex) => {
editableFormFileList.value = newList
formFileDraggedIndex.value = -1
- // 更新fileListInfo和表单值
- fileListInfo.value = newList
+ // 更新fileListInfo和表单值(使用独立副本,避免引用同一数组)
+ fileListInfo.value = newList.map(item => ({ ...item }))
updateFormFileListValue()
event.target.style.opacity = '1'
@@ -1714,10 +1913,38 @@ const updateFormFileListValue = () => {
}
// 文件预览
-const previewFile = (fileId) => {
- // 这里可以实现文件预览逻辑
- console.log('预览文件:', fileId)
- ElMessage.info('文件预览功能待实现')
+const previewFile = async (fileId) => {
+ if (!fileId) return
+ try {
+ // 先获取文件详情拿到下载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) => {
@@ -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) {
ElMessage.warning('选择的文件无效')
return
@@ -1798,7 +2068,7 @@ const handleImageSelectorConfirm = (selectedFile) => {
size: selectedFile.size || 0
}
} else if (imageSelectorMode.value === 'list') {
- // 文件列表模式
+ // 单选兼容:文件列表模式(单个文件对象)
if (!fileListInfo.value) {
fileListInfo.value = []
}
@@ -1811,21 +2081,45 @@ const handleImageSelectorConfirm = (selectedFile) => {
size: selectedFile.size || 0
}
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') {
// 文件列表中的特定项替换模式
const index = currentImageSelectorFileId.value
- if (fileListInfo.value && fileListInfo.value[index] !== undefined) {
- fileListInfo.value[index] = {
- id: selectedFile.id,
- url: processImageUrl(selectedFile.url || ''),
- realName: selectedFile.realName || '文件',
- saveName: selectedFile.realName || 'file',
- size: selectedFile.size || 0
- }
- updateFileListValue()
- ElMessage.success('文件替换成功')
+ const updatedFile = {
+ id: selectedFile.id,
+ url: processImageUrl(selectedFile.url || ''),
+ realName: selectedFile.realName || '文件',
+ saveName: selectedFile.realName || 'file',
+ size: selectedFile.size || 0
}
+
+ 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
@@ -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(() => {
// 初始化时加载配置组数据
@@ -2456,6 +3034,33 @@ onMounted(() => {
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 {
width: 100%;
diff --git a/src/views/system/SystemFile.vue b/src/views/system/SystemFile.vue
index b5d1f66..e1d2801 100644
--- a/src/views/system/SystemFile.vue
+++ b/src/views/system/SystemFile.vue
@@ -537,23 +537,69 @@ const handleRemoveFile = (file, fileList) => {
uploadFileList.value = fileList
}
-// 提交上传
-const handleSubmitUpload = () => {
+// 提交上传(批量上传:将所有文件合并为一次请求)
+const handleSubmitUpload = async () => {
if (uploadFileList.value.length === 0) {
ElMessage.warning('请至少选择一个文件')
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) {
- ElMessage.info('所有文件已上传完成')
+ ElMessage.info('没有可上传的有效文件')
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) {
ElMessage.warning(`文件 ${file.name} 大小超过 10MB`)
}
- // 允许文件添加到列表,在上传时再进行验证
+ // 允许文件添加到列表,在提交时再进行验证
return true
}
-// 自定义上传方法
+// 自定义上传方法(保留为空壳,实际上传由 handleSubmitUpload 批量处理)
const handleCustomUpload = async (options) => {
- const { file, onSuccess, onError } = options
- console.log('开始上传文件:', file)
-
- // 在上传前进行验证
- 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)
- }
+ // 不做任何操作,所有上传由 handleSubmitUpload 统一批量处理
+ // el-upload 的 auto-upload 已设为 false,此方法不会被自动调用
}
// 上传成功
diff --git a/src/views/ticket/TicketDetail.vue b/src/views/ticket/TicketDetail.vue
index c16178a..d4c060b 100644
--- a/src/views/ticket/TicketDetail.vue
+++ b/src/views/ticket/TicketDetail.vue
@@ -434,10 +434,10 @@ const sendMessage = async () => {
try {
const formData = new FormData()
- // 添加所有文件
+ // 多个 file_names 和 files 条目在同一请求中
inputFiles.forEach((file) => {
- formData.append('files', file)
formData.append('file_names', file.name)
+ formData.append('files', file)
})
// 设置上传类型为工单
@@ -447,11 +447,11 @@ const sendMessage = async () => {
const uploadRes = await uploadFile(formData)
if (uploadRes.data?.code === 200) {
- // 从返回的数据中提取文件ID(字段名是 id)
+ // 从返回的数据中提取文件ID
const data = uploadRes.data.data
if (Array.isArray(data)) {
fileIds = data.map(item => String(item.id))
- } else if (data.id) {
+ } else if (data?.id) {
fileIds = [String(data.id)]
}
@@ -732,9 +732,10 @@ const saveEditMessage = async () => {
try {
const formData = new FormData()
+ // 多个 file_names 和 files 条目在同一请求中
editMessageFiles.value.forEach((file) => {
- formData.append('files', file)
formData.append('file_names', file.name)
+ formData.append('files', file)
})
formData.append('update_type', 'work_order')
@@ -746,7 +747,7 @@ const saveEditMessage = async () => {
const data = uploadRes.data.data
if (Array.isArray(data)) {
newFileIds = data.map(item => String(item.id))
- } else if (data.id) {
+ } else if (data?.id) {
newFileIds = [String(data.id)]
}
} else {
diff --git a/vite.config.js b/vite.config.js
index 5172787..c48cf8f 100644
--- a/vite.config.js
+++ b/vite.config.js
@@ -33,7 +33,7 @@ export default defineConfig(({ mode }) => {
server: {
// 强制绑定 IPv4 回环地址,避免 TUN/VPN 代理模式拦截 IPv6 或通配地址
host: '127.0.0.1',
- port: 5174,
+ port: 5176,
strictPort: false,
proxy: {
'/api': {