From a2a7644a9f347f030d9a2d5a2491e27336db0bd2 Mon Sep 17 00:00:00 2001 From: 2256907009 <2256907009@qq.com> Date: Tue, 10 Mar 2026 19:25:43 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E8=AE=BE=E7=BD=AE=E5=8A=A8=E6=80=81?= =?UTF-8?q?=E7=BC=96=E8=BE=91=E9=85=8D=E7=BD=AE=E5=9B=BE=E7=89=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/admin/ImageSelector.vue | 505 +++++++++ src/views/system/SettingManage.vue | 1303 +++++++++++++++++++++--- 2 files changed, 1663 insertions(+), 145 deletions(-) create mode 100644 src/components/admin/ImageSelector.vue diff --git a/src/components/admin/ImageSelector.vue b/src/components/admin/ImageSelector.vue new file mode 100644 index 0000000..b04cd89 --- /dev/null +++ b/src/components/admin/ImageSelector.vue @@ -0,0 +1,505 @@ + + + + + diff --git a/src/views/system/SettingManage.vue b/src/views/system/SettingManage.vue index 3b17697..6677a7d 100644 --- a/src/views/system/SettingManage.vue +++ b/src/views/system/SettingManage.vue @@ -96,8 +96,9 @@ type="primary" @click="previewFile(fileId)" class="file-link" + :title="`文件${index + 1}: ${fileId}`" > - 文件{{ index + 1 }}: {{ fileId }} + {{ truncateFileName(`文件${index + 1}: ${fileId}`, 25) }} - @@ -110,13 +111,17 @@ type="primary" size="small" class="string-tag" + :title="item" > - {{ item }} + {{ truncateFileName(item, 25) }} - - {{ row.data.value }} + + {{ truncateFileName(row.data.value, 25) }} + {{ truncateFileName(row.data.value, 15) }} + @@ -173,43 +178,74 @@ {{ selectedNode.data.type }} {{ selectedNode.data.settingGroupID || '-' }} - - {{ selectedNode.data.value ? '是' : '否' }} - - - 文件ID: {{ selectedNode.data.value }} - - - - - -
- - 文件{{ index + 1 }}: {{ fileId }} + +
+ {{ selectedNode.data.value ? '是' : '否' }} + + + 文件ID: {{ selectedNode.data.value }} -
- - - - -
- - {{ item }} - -
- - -
- {{ selectedNode.data.value }} + - + + +
+ + {{ truncateFileName(`文件${index + 1}: ${fileId}`, 25) }} + +
+ - +
+ +
+
+ 字符串列表 ({{ getStringList(selectedNode.data.value).length }} 项) + 添加项目 +
+
+
+
+ +
+
+ + + {{ truncateFileName(item.value, 40) }} + +
+
+ + +
+
+
+
+
+ + {{ selectedNode.data.value }} + +
@@ -261,7 +297,7 @@ @@ -324,10 +360,15 @@ />
-
+
-
- +
+ + +
+ + 上传中... +
@@ -338,7 +379,7 @@
- {{ fileInfo.realName || fileInfo.saveName }} + {{ truncateFileName(fileInfo.realName || fileInfo.saveName) }} 文件ID: {{ settingForm.value }} {{ formatFileSize(fileInfo.size) }} @@ -351,97 +392,151 @@
- - -
- 将文件拖到此处,或点击上传 +
+
+ + +
+ 将文件拖到此处,或点击上传 +
+ +
+ +
+ + + 从文件库选择图片 +
- - +
-
-
-
- -
- +
+
+ 文件列表 ({{ getEditableFormFileList().length }} 项) +
+ + 从文件库选择 + + + + 上传文件 + + +
+
+
+
+
+
- -
-
+
+ +
+
+ + 上传中... +
- - -
- {{ fileInfo.realName || fileInfo.saveName }} - 文件ID: {{ fileInfo.id }} - - +
+
+ {{ truncateFileName(fileInfo.realName || fileInfo.saveName, 30) }} +
+
ID: {{ fileInfo.id || '上传中' }}
+
{{ formatFileSize(fileInfo.size) }}
- -
- 删除 + + + +
- - -
- 将文件拖到此处,或点击上传 -
- -
-
-
- - {{ item }} - +
+
+ 字符串列表 ({{ getEditableFormStringList().length }} 项) + 添加项目 +
+
+
+
+ +
+
+ + + {{ truncateFileName(item.value, 40) }} + +
+
+ + +
+
-
-
- - 添加
+ + +
@@ -482,7 +584,7 @@ import { ref, reactive, onMounted, watch, nextTick, computed } from 'vue' import { useRoute } from 'vue-router' import { ElMessage, ElMessageBox } from 'element-plus' -import { Search, Plus, Delete, UploadFilled, Folder, Document, ArrowRight, Loading } from '@element-plus/icons-vue' +import { Search, Plus, Delete, UploadFilled, Folder, Document, ArrowRight, Loading, Picture, Edit, Rank } from '@element-plus/icons-vue' import { getSettingGroupList, getSettingGroupInfo, @@ -497,6 +599,7 @@ import { deleteSetting } from '@/api/admin/setting' import { uploadFile } from '@/api/admin/file' +import ImageSelector from '@/components/admin/ImageSelector.vue' const route = useRoute() @@ -506,6 +609,14 @@ const treeData = ref([]) const allGroupList = ref([]) // 存储所有已加载的配置组 const treeDataMap = ref(new Map()) // 存储树形数据,key为group_id const selectedNode = ref(null) +const editableStringList = ref([]) // 弹窗中可编辑的字符串列表 +const draggedIndex = ref(-1) // 拖拽的索引 + +// 表单相关可编辑列表 +const editableFormStringList = ref([]) // 表单中可编辑的字符串列表 +const editableFormFileList = ref([]) // 表单中可编辑的文件列表 +const formDraggedIndex = ref(-1) // 表单拖拽索引 +const formFileDraggedIndex = ref(-1) // 表单文件拖拽索引 // 查询参数 const queryParams = reactive({ @@ -556,7 +667,6 @@ const groupRules = { const groupLoading = ref(false) const groupList = ref([]) const groupTotal = ref(0) -const selectedRows = ref([]) const groupDialogVisible = ref(false) const groupDialogTitle = ref('新增配置组') const groupFormRef = ref(null) @@ -614,6 +724,12 @@ const fileListInfo = ref([]) const newStringItem = ref('') const imageViewerVisible = ref(false) const currentViewImage = ref('') +const imageSelectorVisible = ref(false) +const currentImageSelectorFileId = ref('') +const imageSelectorMode = ref('single') // 'single' 或 'list' + +// 批量选择相关 +const selectedRows = ref([]) // 格式化日期时间 const formatDate = (dateString) => { @@ -709,6 +825,11 @@ const loadGroups = async () => { // 节点点击事件 const handleNodeClick = (row) => { selectedNode.value = row + + // 如果是字符串列表类型,初始化可编辑列表 + if (row.data.type === 'string_list') { + initEditableStringList() + } } // 查询处理 @@ -964,6 +1085,17 @@ const handleEditSetting = async (row) => { } else { fileInfo.value = null } + } else if (data.type === 'string_list') { + // 处理字符串列表类型,对每个字符串进行截断 + if (data.parsedValue && Array.isArray(data.parsedValue)) { + const truncatedValues = data.parsedValue.map(item => truncateFileName(item, 25)) + settingForm.value = truncatedValues.join(',') + initEditableFormStringList() // 初始化表单可编辑字符串列表 + } else { + settingForm.value = '' + editableFormStringList.value = [] + } + newStringItem.value = '' } else if (data.type === 'file_list') { // 处理文件列表类型,使用parsedValue来获取文件信息 if (data.parsedValue && Array.isArray(data.parsedValue)) { @@ -977,11 +1109,14 @@ const handleEditSetting = async (row) => { size: 0 } }) + // 确保在设置fileListInfo后再初始化表单可编辑文件列表 + nextTick(() => { + initEditableFormFileList() + }) } else { fileListInfo.value = [] + editableFormFileList.value = [] } - } else if (data.type === 'string_list') { - newStringItem.value = '' } settingDialogVisible.value = true } @@ -1036,6 +1171,21 @@ const fetchAllGroupList = async () => { // 文件相关函数 const handleFileChange = async (file) => { fileUploading.value = true + + // 创建本地预览URL + const localUrl = URL.createObjectURL(file.raw) + const isImage = file.raw.type.startsWith('image/') + + // 立即显示本地预览 + fileInfo.value = { + id: '', // 暂时为空,上传成功后会更新 + url: isImage ? localUrl : '', + realName: file.name, + saveName: file.name, + size: file.size, + isLocal: true // 标记为本地文件 + } + try { const formData = new FormData() formData.append('file_names', file.name) @@ -1044,18 +1194,30 @@ const handleFileChange = async (file) => { formData.append('open_down','true') const res = await uploadFile(formData) - if (res.data.code === 200 && res.data.data.length > 0) { + if (res.data.code === 200 && res.data.data && res.data.data.length > 0) { const uploadedFile = res.data.data[0] - settingForm.value = String(uploadedFile.id) + settingForm.value = String(uploadedFile.id || '') - // 确保上传的文件URL也经过处理 + // 释放本地URL(暂时不释放,保留用于渲染) + // if (fileInfo.value?.isLocal) { + // URL.revokeObjectURL(fileInfo.value.url) + // } + + // 更新为服务器返回的文件信息,但保留本地URL用于渲染 fileInfo.value = { - ...uploadedFile, - url: processImageUrl(uploadedFile.url || uploadedFile.realName) + id: uploadedFile.id || '', + url: processImageUrl(uploadedFile.url || uploadedFile.realName || ''), + localUrl: fileInfo.value?.url || '', // 保留本地URL用于渲染 + realName: uploadedFile.realName || '文件', + saveName: uploadedFile.saveName || 'file', + size: uploadedFile.size || 0, + isLocal: false // 标记为已上传,但保留本地渲染 } ElMessage.success('文件上传成功') } else { + // 上传失败,保留本地预览,但显示错误消息 ElMessage.error(res.data.message || '文件上传失败') + // 注意:不清理本地预览,让用户可以重新上传或删除 } } catch (error) { console.error('文件上传失败:', error) @@ -1066,6 +1228,14 @@ const handleFileChange = async (file) => { } const clearFile = () => { + // 释放本地URL + if (fileInfo.value?.localUrl) { + URL.revokeObjectURL(fileInfo.value.localUrl) + } + if (fileInfo.value?.isLocal && fileInfo.value?.url) { + URL.revokeObjectURL(fileInfo.value.url) + } + settingForm.value = '' fileInfo.value = null } @@ -1078,6 +1248,462 @@ const formatFileSize = (bytes) => { return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i] } +const truncateFileName = (fileName, maxLength = 25) => { + if (!fileName) return '' + if (fileName.length <= maxLength) return fileName + + const extension = fileName.includes('.') ? '.' + fileName.split('.').pop() : '' + const nameWithoutExt = fileName.substring(0, fileName.lastIndexOf('.')) + + if (nameWithoutExt.length <= maxLength) { + return fileName + } + + const truncatedName = nameWithoutExt.substring(0, maxLength - 3) // 留3个字符给"...和扩展名" + console.log('111',truncatedName + '...' + extension) + return truncatedName + '...' + extension +} + +// ==================== 可编辑字符串列表功能 ==================== + +// 获取可编辑的字符串列表 +const getEditableStringList = () => { + if (!selectedNode.value || selectedNode.value.data.type !== 'string_list') { + return [] + } + return editableStringList.value +} + +// 初始化可编辑字符串列表 +const initEditableStringList = () => { + if (!selectedNode.value || selectedNode.value.data.type !== 'string_list') { + editableStringList.value = [] + return + } + + const stringItems = getStringList(selectedNode.value.data.value) + editableStringList.value = stringItems.map(item => ({ + value: item, + editing: false + })) +} + +// 开始拖拽 +const handleDragStart = (event, index) => { + draggedIndex.value = index + event.dataTransfer.effectAllowed = 'move' + event.dataTransfer.setData('text/html', event.target.outerHTML) + event.target.style.opacity = '0.5' +} + +// 拖拽放下 +const handleDrop = (event, dropIndex) => { + event.preventDefault() + const dragIndex = draggedIndex.value + + if (dragIndex === dropIndex) return + + // 重新排列数组 + const newList = [...editableStringList.value] + const [draggedItem] = newList.splice(dragIndex, 1) + newList.splice(dropIndex, 0, draggedItem) + + editableStringList.value = newList + draggedIndex.value = -1 + + // 更新selectedNode中的数据 + const updatedValues = newList.map(item => item.value) + selectedNode.value.data.value = updatedValues.join(',') + + event.target.style.opacity = '1' +} + +// 添加新字符串项目 +const addEditableStringItem = () => { + const newItem = { + value: '', + editing: true + } + editableStringList.value.push(newItem) + + // 更新selectedNode中的数据 + const updatedValues = editableStringList.value.map(item => item.value) + selectedNode.value.data.value = updatedValues.join(',') + + // 聚焦到新添加的输入框 + nextTick(() => { + const inputs = document.querySelectorAll('.string-list-item input') + if (inputs.length > 0) { + inputs[inputs.length - 1].focus() + } + }) +} + +// 开始编辑项目 +const startEditItem = (index) => { + editableStringList.value[index].editing = true + + nextTick(() => { + const inputs = document.querySelectorAll('.string-list-item input') + if (inputs[index]) { + inputs[index].focus() + inputs[index].select() + } + }) +} + +// 完成编辑项目 +const finishEditItem = (index) => { + editableStringList.value[index].editing = false + + // 更新selectedNode中的数据 + const updatedValues = editableStringList.value.map(item => item.value) + selectedNode.value.data.value = updatedValues.join(',') +} + +// 删除字符串项目 +const removeEditableStringItem = (index) => { + ElMessageBox.confirm('确定要删除这个项目吗?', '提示', { + confirmButtonText: '确定', + cancelButtonText: '取消', + type: 'warning' + }).then(() => { + editableStringList.value.splice(index, 1) + + // 更新selectedNode中的数据 + const updatedValues = editableStringList.value.map(item => item.value) + selectedNode.value.data.value = updatedValues.join(',') + + ElMessage.success('删除成功') + }) +} + +// 聚焦输入框 +const focusEditInput = () => { + // 这个方法会被 @mounted 调用 +} + +// ==================== 表单可编辑字符串列表功能 ==================== + +// 获取表单可编辑的字符串列表 +const getEditableFormStringList = () => { + return editableFormStringList.value +} + +// 初始化表单可编辑字符串列表 +const initEditableFormStringList = () => { + const stringItems = getStringList(settingForm.value) + editableFormStringList.value = stringItems.map(item => ({ + value: item, + editing: false + })) +} + +// 开始表单拖拽 +const handleFormDragStart = (event, index) => { + formDraggedIndex.value = index + event.dataTransfer.effectAllowed = 'move' + event.dataTransfer.setData('text/html', event.target.outerHTML) + event.target.style.opacity = '0.5' +} + +// 表单拖拽放下 +const handleFormDrop = (event, dropIndex) => { + event.preventDefault() + const dragIndex = formDraggedIndex.value + + if (dragIndex === dropIndex) return + + // 重新排列数组 + const newList = [...editableFormStringList.value] + const [draggedItem] = newList.splice(dragIndex, 1) + newList.splice(dropIndex, 0, draggedItem) + + editableFormStringList.value = newList + formDraggedIndex.value = -1 + + // 更新表单值 + const updatedValues = newList.map(item => item.value) + settingForm.value = updatedValues.join(',') + + event.target.style.opacity = '1' +} + +// 添加表单新字符串项目 +const addFormEditableStringItem = () => { + const newItem = { + value: '', + editing: true + } + editableFormStringList.value.push(newItem) + + // 更新表单值 + const updatedValues = editableFormStringList.value.map(item => item.value) + settingForm.value = updatedValues.join(',') + + // 聚焦到新添加的输入框 + nextTick(() => { + const inputs = document.querySelectorAll('.string-list-item input') + if (inputs.length > 0) { + inputs[inputs.length - 1].focus() + } + }) +} + +// 开始编辑表单项目 +const startFormEditItem = (index) => { + editableFormStringList.value[index].editing = true + + nextTick(() => { + const inputs = document.querySelectorAll('.string-list-item input') + if (inputs[index]) { + inputs[index].focus() + inputs[index].select() + } + }) +} + +// 完成编辑表单项目 +const finishFormEditItem = (index) => { + editableFormStringList.value[index].editing = false + + // 更新表单值 + const updatedValues = editableFormStringList.value.map(item => item.value) + settingForm.value = updatedValues.join(',') +} + +// 删除表单字符串项目 +const removeFormEditableStringItem = (index) => { + ElMessageBox.confirm('确定要删除这个项目吗?', '提示', { + confirmButtonText: '确定', + cancelButtonText: '取消', + type: 'warning' + }).then(() => { + editableFormStringList.value.splice(index, 1) + + // 更新表单值 + const updatedValues = editableFormStringList.value.map(item => item.value) + settingForm.value = updatedValues.join(',') + + ElMessage.success('删除成功') + }) +} + +// 聚焦表单输入框 +const focusFormEditInput = () => { + // 这个方法会被 @mounted 调用 +} + +// ==================== 表单可编辑文件列表功能 ==================== + +// 获取表单可编辑的文件列表 +const getEditableFormFileList = () => { + return editableFormFileList.value +} + +// 初始化表单可编辑文件列表 +const initEditableFormFileList = () => { + if (settingForm.type === 'file_list' && fileListInfo.value && fileListInfo.value.length > 0) { + editableFormFileList.value = fileListInfo.value.map(file => ({ + id: file.id || '', + url: file.url || '', + localUrl: file.localUrl || '', // 保留本地URL字段 + realName: file.realName || '文件', + saveName: file.saveName || 'file', + size: file.size || 0, + uploading: false + })) + } else { + editableFormFileList.value = [] + } +} + +// 开始表单文件拖拽 +const handleFormFileDragStart = (event, index) => { + formFileDraggedIndex.value = index + event.dataTransfer.effectAllowed = 'move' + event.dataTransfer.setData('text/html', event.target.outerHTML) + event.target.style.opacity = '0.5' +} + +// 表单文件拖拽放下 +const handleFormFileDrop = (event, dropIndex) => { + event.preventDefault() + const dragIndex = formFileDraggedIndex.value + + if (dragIndex === dropIndex) return + + // 重新排列数组 + const newList = [...editableFormFileList.value] + const [draggedItem] = newList.splice(dragIndex, 1) + newList.splice(dropIndex, 0, draggedItem) + + editableFormFileList.value = newList + formFileDraggedIndex.value = -1 + + // 更新fileListInfo和表单值 + fileListInfo.value = newList + updateFormFileListValue() + + event.target.style.opacity = '1' +} + +// 表单文件列表变更 +const handleFormFileListChange = async (file) => { + // 创建本地预览URL + const localUrl = URL.createObjectURL(file.raw) + const isImage = file.raw.type.startsWith('image/') + + // 立即添加到表单文件列表 + const tempFile = { + id: '', // 暂时为空,上传成功后会更新 + url: isImage ? localUrl : '', + realName: file.name, + saveName: file.name, + size: file.size, + isLocal: true, + uploading: true + } + + editableFormFileList.value.push(tempFile) + + try { + const formData = new FormData() + formData.append('file_names', file.name) + formData.append('files', file.raw) + formData.append('update_type','cover') + formData.append('open_down','true') + + const res = await uploadFile(formData) + if (res.data.code === 200 && res.data.data && res.data.data.length > 0) { + const uploadedFile = res.data.data[0] + + // 找到对应的本地文件并更新 + const index = editableFormFileList.value.findIndex(f => f.isLocal && f.realName === file.name) + if (index !== -1) { + editableFormFileList.value[index] = { + id: uploadedFile.id || '', + url: processImageUrl(uploadedFile.url || uploadedFile.realName || ''), + localUrl: localUrl, // 保留本地URL用于渲染 + realName: uploadedFile.realName || '文件', + saveName: uploadedFile.saveName || 'file', + size: uploadedFile.size || 0, + isLocal: false, + uploading: false + } + } + + updateFormFileListValue() + ElMessage.success('文件上传成功') + } else { + // 上传失败,移除临时文件 + const index = editableFormFileList.value.findIndex(f => f.isLocal && f.realName === file.name) + if (index !== -1) { + editableFormFileList.value.splice(index, 1) + } + ElMessage.error(res.data.message || '文件上传失败') + } + } catch (error) { + console.error('文件上传失败:', error) + // 上传失败,移除临时文件 + const index = editableFormFileList.value.findIndex(f => f.isLocal && f.realName === file.name) + if (index !== -1) { + editableFormFileList.value.splice(index, 1) + } + ElMessage.error('文件上传失败') + } +} + +// 表单文件替换 +const handleFormFileReplace = async (file, index) => { + if (!editableFormFileList.value[index]) return + + // 创建本地预览URL + const localUrl = URL.createObjectURL(file.raw) + const isImage = file.raw.type.startsWith('image/') + + // 更新文件信息 + editableFormFileList.value[index] = { + ...editableFormFileList.value[index], + url: isImage ? localUrl : '', + realName: file.name, + saveName: file.name, + size: file.size, + isLocal: true, + uploading: true + } + + try { + const formData = new FormData() + formData.append('file_names', file.name) + formData.append('files', file.raw) + formData.append('update_type','cover') + formData.append('open_down','true') + + const res = await uploadFile(formData) + if (res.data.code === 200 && res.data.data && res.data.data.length > 0) { + const uploadedFile = res.data.data[0] + + // 更新文件信息 + editableFormFileList.value[index] = { + id: uploadedFile.id || '', + url: processImageUrl(uploadedFile.url || uploadedFile.realName || ''), + localUrl: localUrl, // 保留本地URL用于渲染 + realName: uploadedFile.realName || '文件', + saveName: uploadedFile.saveName || 'file', + size: uploadedFile.size || 0, + isLocal: false, + uploading: false + } + + updateFormFileListValue() + ElMessage.success('文件替换成功') + } else { + // 上传失败,恢复原文件信息 + ElMessage.error(res.data.message || '文件替换失败') + } + } catch (error) { + console.error('文件替换失败:', error) + ElMessage.error('文件替换失败') + } +} + +// 删除表单文件项目 +const removeFormEditableFileItem = (index) => { + ElMessageBox.confirm('确定要删除这个文件吗?', '提示', { + confirmButtonText: '确定', + cancelButtonText: '取消', + type: 'warning' + }).then(() => { + // 释放本地URL + if (editableFormFileList.value[index].localUrl) { + URL.revokeObjectURL(editableFormFileList.value[index].localUrl) + } + if (editableFormFileList.value[index].isLocal && editableFormFileList.value[index].url) { + URL.revokeObjectURL(editableFormFileList.value[index].url) + } + + editableFormFileList.value.splice(index, 1) + + // 更新fileListInfo和表单值 + fileListInfo.value = editableFormFileList.value + updateFormFileListValue() + + ElMessage.success('删除成功') + }) +} + +// 更新表单文件列表值 +const updateFormFileListValue = () => { + if (!editableFormFileList.value || !Array.isArray(editableFormFileList.value)) { + return + } + + const fileIds = editableFormFileList.value.map(file => file.id).filter(id => id) + settingForm.value = fileIds.join(',') +} + +// 文件预览 const previewFile = (fileId) => { // 这里可以实现文件预览逻辑 console.log('预览文件:', fileId) @@ -1117,22 +1743,96 @@ const previewImage = (url) => { // 处理图片加载失败 const handleImageError = (event) => { // 图片加载失败时,可以隐藏图片或显示占位符 - console.log('图片加载失败:', event.target.src) + console.error('图片加载失败:', event.target.src) // 这里可以添加更多的错误处理逻辑 } +// ==================== 图像选择器相关 ==================== + +// 打开图像选择器(单个文件) +const openImageSelector = () => { + imageSelectorMode.value = 'single' + currentImageSelectorFileId.value = settingForm.value || '' + imageSelectorVisible.value = true +} + +// 打开图像选择器(文件列表) +const openImageSelectorForList = () => { + imageSelectorMode.value = 'list' + currentImageSelectorFileId.value = '' + imageSelectorVisible.value = true +} + +// 处理图像选择器确认 +const handleImageSelectorConfirm = (selectedFile) => { + if (!selectedFile || !selectedFile.id) { + ElMessage.warning('选择的文件无效') + return + } + + if (imageSelectorMode.value === 'single') { + // 单个文件模式 + settingForm.value = selectedFile.id + fileInfo.value = { + id: selectedFile.id, + url: processImageUrl(selectedFile.url || ''), + realName: selectedFile.realName || '文件', + saveName: selectedFile.realName || 'file', + size: selectedFile.size || 0 + } + } else if (imageSelectorMode.value === 'list') { + // 文件列表模式 + if (!fileListInfo.value) { + fileListInfo.value = [] + } + + const newFile = { + id: selectedFile.id, + url: processImageUrl(selectedFile.url || ''), + realName: selectedFile.realName || '文件', + saveName: selectedFile.realName || 'file', + size: selectedFile.size || 0 + } + fileListInfo.value.push(newFile) + updateFileListValue() + } + + imageSelectorVisible.value = false +} + const getFileList = (value) => { - if (!value) return [] - return value.split(',').filter(id => id.trim()) + if (!value || typeof value !== 'string') return [] + return value.split(',').filter(id => id && id.trim()) } const getStringList = (value) => { - if (!value) return [] - return value.split(',').filter(str => str.trim()) + if (!value || typeof value !== 'string') return [] + return value.split(',').filter(str => str && str.trim()) } const handleFileListChange = async (file) => { fileUploading.value = true + + // 创建本地预览URL + const localUrl = URL.createObjectURL(file.raw) + const isImage = file.raw.type.startsWith('image/') + + // 立即添加本地预览到列表 + if (!fileListInfo.value) { + fileListInfo.value = [] + } + + const tempFile = { + id: '', // 暂时为空,上传成功后会更新 + url: isImage ? localUrl : '', + realName: file.name, + saveName: file.name, + size: file.size, + isLocal: true // 标记为本地文件 + } + + fileListInfo.value.push(tempFile) + try { const formData = new FormData() formData.append('file_names', file.name) @@ -1141,24 +1841,41 @@ const handleFileListChange = async (file) => { formData.append('open_down','true') const res = await uploadFile(formData) - if (res.data.code === 200 && res.data.data.length > 0) { + if (res.data.code === 200 && res.data.data && res.data.data.length > 0) { const uploadedFile = res.data.data[0] const currentFileIds = getFileList(settingForm.value) - currentFileIds.push(String(uploadedFile.id)) + currentFileIds.push(String(uploadedFile.id || '')) settingForm.value = currentFileIds.join(',') - // 确保新上传的文件URL也经过处理 - const processedFile = { - ...uploadedFile, - url: processImageUrl(uploadedFile.url || uploadedFile.realName) + // 找到对应的本地文件并更新 + const index = fileListInfo.value.findIndex(f => f.isLocal && f.realName === file.name) + if (index !== -1) { + // 释放本地URL(暂时不释放,保留用于渲染) + // if (fileListInfo.value[index].isLocal) { + // URL.revokeObjectURL(fileListInfo.value[index].url) + // } + + // 更新为服务器返回的文件信息,但保留本地URL用于渲染 + fileListInfo.value[index] = { + id: uploadedFile.id || '', + url: processImageUrl(uploadedFile.url || uploadedFile.realName || ''), + localUrl: fileListInfo.value[index]?.url || '', // 保留本地URL用于渲染 + realName: uploadedFile.realName || '文件', + saveName: uploadedFile.saveName || 'file', + size: uploadedFile.size || 0, + isLocal: false // 标记为已上传,但保留本地渲染 + } } - fileListInfo.value.push(processedFile) + + updateFileListValue() ElMessage.success('文件上传成功') } else { + // 上传失败,不清理本地预览,让用户可以重新上传或删除 ElMessage.error(res.data.message || '文件上传失败') } } catch (error) { console.error('文件上传失败:', error) + // 上传失败,不清理本地预览,让用户可以重新上传或删除 ElMessage.error('文件上传失败') } finally { fileUploading.value = false @@ -1166,12 +1883,34 @@ const handleFileListChange = async (file) => { } const removeFile = (index) => { - const currentFileIds = getFileList(settingForm.value) + if (!fileListInfo.value || index < 0 || index >= fileListInfo.value.length) { + return + } + + // 释放本地URL + if (fileListInfo.value[index].localUrl) { + URL.revokeObjectURL(fileListInfo.value[index].localUrl) + } + if (fileListInfo.value[index].isLocal && fileListInfo.value[index].url) { + URL.revokeObjectURL(fileListInfo.value[index].url) + } + + const currentFileIds = getFileList(settingForm.value) || [] currentFileIds.splice(index, 1) settingForm.value = currentFileIds.join(',') fileListInfo.value.splice(index, 1) } +// 更新文件列表值 +const updateFileListValue = () => { + if (!fileListInfo.value || !Array.isArray(fileListInfo.value)) { + return + } + + const fileIds = fileListInfo.value.map(file => file.id).filter(id => id) + settingForm.value = fileIds.join(',') +} + const addStringItem = () => { if (newStringItem.value.trim()) { const currentItems = getStringList(settingForm.value) @@ -1324,6 +2063,8 @@ onMounted(() => { /* 文件预览样式 */ .file-preview { margin-top: 8px; + position: relative; + flex-shrink: 0; } .preview-image { @@ -1356,10 +2097,6 @@ onMounted(() => { gap: 12px; } -.file-preview { - flex-shrink: 0; -} - .file-placeholder { width: 80px; height: 80px; @@ -1662,6 +2399,45 @@ onMounted(() => { width: 100%; } +.file-upload-options { + width: 100%; +} + +.upload-methods { + display: flex; + flex-direction: column; + gap: 16px; + align-items: center; +} + +.divider { + position: relative; + text-align: center; + color: #909399; + font-size: 14px; + margin: 8px 0; +} + +.divider::before { + content: ''; + position: absolute; + top: 50%; + left: 0; + right: 0; + height: 1px; + background: #dcdfe6; +} + +.divider { + background: #fff; + padding: 0 16px; +} + +.image-selector-btn { + width: 200px; + height: 40px; +} + :deep(.file-uploader .el-upload) { width: 100%; } @@ -1764,6 +2540,37 @@ onMounted(() => { color: #409eff; } +/* 上传状态指示器样式 */ +.file-item.uploading { + border-color: #409eff; + background-color: #f0f9ff; +} + +.upload-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.6); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + color: white; + font-size: 12px; + border-radius: 4px; +} + +.upload-overlay .el-icon { + font-size: 20px; + margin-bottom: 4px; +} + +.upload-overlay span { + font-size: 11px; +} + /* 字符串列表相关样式 */ .string-list-section { width: 100%; @@ -1823,6 +2630,212 @@ onMounted(() => { display: inline-block; } +/* 可编辑字符串列表样式 */ +.editable-string-list { + border: 1px solid #e4e7ed; + border-radius: 4px; + padding: 16px; + background-color: #fafafa; +} + +.string-list-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 12px; + padding-bottom: 8px; + border-bottom: 1px solid #ebeef5; +} + +.list-title { + font-weight: 500; + color: #303133; + font-size: 14px; +} + +.string-list-items { + display: flex; + flex-direction: column; + gap: 8px; +} + +.string-list-item { + display: flex; + align-items: center; + gap: 12px; + padding: 8px 12px; + background-color: #ffffff; + border: 1px solid #e4e7ed; + border-radius: 4px; + transition: all 0.3s ease; + cursor: move; +} + +.string-list-item:hover { + border-color: #c0c4cc; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.drag-handle { + color: #909399; + cursor: grab; + flex-shrink: 0; +} + +.drag-handle:hover { + color: #606266; +} + +.item-content { + flex: 1; + min-width: 0; +} + +.item-text { + padding: 4px 8px; + border-radius: 4px; + transition: background-color 0.3s ease; + cursor: pointer; + word-break: break-word; + white-space: normal; + line-height: 1.4; +} + +.item-text:hover { + background-color: #f5f7fa; +} + +.item-actions { + display: flex; + gap: 4px; + flex-shrink: 0; +} + +.danger-btn { + color: #f56c6c; +} + +.danger-btn:hover { + color: #f78989; +} + +/* 可编辑文件列表样式 */ +.editable-file-list { + border: 1px solid #e4e7ed; + border-radius: 4px; + padding: 16px; + background-color: #fafafa; +} + +.file-list-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 12px; + padding-bottom: 8px; + border-bottom: 1px solid #ebeef5; +} + +.header-actions { + display: flex; + gap: 8px; +} + +.file-list-items { + display: flex; + flex-direction: column; + gap: 12px; +} + +.file-list-item { + display: flex; + align-items: center; + gap: 12px; + padding: 12px; + background-color: #ffffff; + border: 1px solid #e4e7ed; + border-radius: 4px; + transition: all 0.3s ease; + cursor: move; +} + +.file-list-item:hover { + border-color: #c0c4cc; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.file-list-item .file-preview { + width: 60px; + height: 60px; + border-radius: 4px; + overflow: hidden; + border: 1px solid #e4e7ed; + flex-shrink: 0; + position: relative; +} + +.file-list-item .preview-image { + width: 100%; + height: 100%; + object-fit: cover; + cursor: pointer; +} + +.file-list-item .file-placeholder { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + background-color: #f5f7fa; + color: #909399; +} + +.file-list-item .file-info { + flex: 1; + min-width: 0; +} + +.file-list-item .file-name { + font-weight: 500; + color: #303133; + margin-bottom: 4px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} + +.file-list-item .file-id { + font-size: 12px; + color: #909399; + margin-bottom: 2px; +} + +.file-list-item .file-size { + font-size: 12px; + color: #909399; +} + +.file-list-item .file-actions { + display: flex; + gap: 4px; + flex-shrink: 0; +} + +/* 弹窗中值内容的省略号样式 */ +.value-content { + max-width: 100%; +} + +.value-content .text-value { + display: block; + max-width: 100%; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + word-break: break-all; +} + /* 响应式调整 */ @media (max-width: 768px) { .file-name {