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