feat: 新增移动端配置信息
Build and Deploy Vue3 / build (push) Successful in 1m33s
Build and Deploy Vue3 / deploy (push) Successful in 1m15s

This commit is contained in:
2026-03-17 18:40:12 +08:00
parent f4dbf17ce9
commit cd16ec17ae
6 changed files with 989 additions and 216 deletions
+638 -33
View File
@@ -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%;