feat: 新增移动端配置信息
This commit is contained in:
@@ -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%;
|
||||
|
||||
Reference in New Issue
Block a user