3740 lines
108 KiB
Vue
3740 lines
108 KiB
Vue
<template>
|
||
<div class="setting-manage-container">
|
||
<el-card class="main-container" shadow="never">
|
||
<!-- 搜索和操作栏 -->
|
||
<div class="filter-section">
|
||
<div class="filter-content">
|
||
<el-form :inline="true" :model="queryParams" class="search-form">
|
||
<el-form-item label="关键词筛选">
|
||
<el-input v-model="queryParams.key" placeholder="请输入关键词" clearable style="width: 200px" />
|
||
</el-form-item>
|
||
<el-form-item>
|
||
<el-button type="primary" @click="handleQuery">
|
||
<el-icon><Search /></el-icon>查询
|
||
</el-button>
|
||
<el-button @click="resetQuery">重置</el-button>
|
||
</el-form-item>
|
||
</el-form>
|
||
<div class="action-bar">
|
||
<el-button type="primary" @click="handleAddGroup">
|
||
<el-icon><Plus /></el-icon>新增配置组
|
||
</el-button>
|
||
<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>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 树状图显示区域 -->
|
||
<div class="tree-section">
|
||
<!-- 树形表格视图 -->
|
||
<el-table
|
||
ref="treeTableRef"
|
||
:data="treeDisplayData"
|
||
style="width: 100%"
|
||
row-key="id"
|
||
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
|
||
class="setting-tree-table"
|
||
@selection-change="handleSelectionChange"
|
||
>
|
||
<el-table-column type="selection" width="55" />
|
||
<el-table-column label="名称" min-width="300">
|
||
<template #default="{ row }">
|
||
<div class="tree-item-content" :style="{ paddingLeft: (row.level - 1) * 24 + 'px' }">
|
||
<!-- 展开/收起按钮 -->
|
||
<span
|
||
v-if="row.type === 'group' && row.hasChildren"
|
||
class="expand-icon"
|
||
@click="toggleExpand(row)"
|
||
>
|
||
<el-icon v-if="row._loading"><Loading /></el-icon>
|
||
<el-icon v-else :class="{ 'is-expanded': row._expanded }"><ArrowRight /></el-icon>
|
||
</span>
|
||
<span v-else class="expand-placeholder"></span>
|
||
<div class="tree-node">
|
||
<el-icon class="tree-icon" v-if="row.type === 'group'">
|
||
<Folder />
|
||
</el-icon>
|
||
<el-icon class="tree-icon" v-else-if="row.type === 'setting'">
|
||
<Document />
|
||
</el-icon>
|
||
<span class="tree-label">{{ row.label }}</span>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="类型" width="100">
|
||
<template #default="{ row }">
|
||
<el-tag v-if="row.type === 'group'" type="primary" size="small">配置组</el-tag>
|
||
<el-tag v-else-if="row.type === 'setting'" :type="getTypeColor(row.data.type)" size="small">
|
||
{{ row.data.type }}
|
||
</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="值" min-width="200">
|
||
<template #default="{ row }">
|
||
<span v-if="row.type === 'group'" style="cursor: default;">{{ row.data.note || '-' }}</span>
|
||
<span v-else-if="row.type === 'setting'" style="cursor: pointer;" @click="handleEditSetting(row.data)">
|
||
<span v-if="row.data.type === 'bool'">{{ row.data.value ? '是' : '否' }}</span>
|
||
<span v-else-if="row.data.type === 'file'">
|
||
<el-link v-if="row.data.value" type="primary" @click.stop="previewFile(row.data.value)">
|
||
文件ID: {{ row.data.value }}
|
||
</el-link>
|
||
<span v-else>-</span>
|
||
</span>
|
||
<span v-else-if="row.data.type === 'file_list'">
|
||
<div v-if="row.data.value" class="file-list-summary">
|
||
<el-tag type="primary" size="small" @click.stop="handleEditSetting(row.data)" style="cursor: pointer;">
|
||
<el-icon><Folder /></el-icon>
|
||
共 {{ getFileList(row.data.value).length }} 个文件
|
||
</el-tag>
|
||
</div>
|
||
<span v-else>-</span>
|
||
</span>
|
||
<span v-else-if="row.data.type === 'string_list'">
|
||
<div v-if="row.data.value" class="string-list-summary">
|
||
<el-tag type="success" size="small" @click.stop="handleEditSetting(row.data)" style="cursor: pointer;">
|
||
<el-icon><Document /></el-icon>
|
||
共 {{ getStringList(row.data.value).length }} 条字符串
|
||
</el-tag>
|
||
</div>
|
||
<span v-else>-</span>
|
||
</span>
|
||
<span v-else>
|
||
<span class="text-value" :title="row.data.value">{{ truncateText(row.data.value, 30) }}</span>
|
||
</span>
|
||
</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="状态" width="100">
|
||
<template #default="{ row }">
|
||
<el-tag v-if="row.type === 'group'" type="success" size="small">正常</el-tag>
|
||
<el-tag v-else-if="row.type === 'setting'" :type="row.data.open ? 'success' : 'info'" size="small">
|
||
{{ row.data.open ? '开放' : '私有' }}
|
||
</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="创建时间" width="180">
|
||
<template #default="{ row }">
|
||
{{ formatDate(row.data.CreatedAt) }}
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="备注" min-width="150" show-overflow-tooltip>
|
||
<template #default="{ row }">
|
||
<span v-if="row.type === 'group'">{{ row.data.note || '-' }}</span>
|
||
<span v-else-if="row.type === 'setting'">{{ row.data.note || '-' }}</span>
|
||
<span v-else>-</span>
|
||
</template>
|
||
</el-table-column>
|
||
<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)">
|
||
编辑
|
||
</el-button>
|
||
<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>
|
||
<el-button type="danger" link size="small" @click="handleDelete(row)">
|
||
删除
|
||
</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</div>
|
||
|
||
<!-- 详细信息面板 -->
|
||
<div class="detail-section" v-if="selectedNode">
|
||
<el-divider content-position="left">{{ selectedNode.type === 'group' ? '配置组详情' : '配置详情' }}</el-divider>
|
||
<div class="detail-content">
|
||
<template v-if="selectedNode.type === 'group'">
|
||
<el-descriptions :column="2" border>
|
||
<el-descriptions-item label="ID">{{ selectedNode.data.id }}</el-descriptions-item>
|
||
<el-descriptions-item label="名称">{{ selectedNode.data.name }}</el-descriptions-item>
|
||
<el-descriptions-item label="备注" :span="2">{{ selectedNode.data.note || '-' }}</el-descriptions-item>
|
||
<el-descriptions-item label="创建时间">{{ formatDate(selectedNode.data.CreatedAt) }}</el-descriptions-item>
|
||
<el-descriptions-item label="更新时间">{{ formatDate(selectedNode.data.UpdatedAt) }}</el-descriptions-item>
|
||
</el-descriptions>
|
||
</template>
|
||
<template v-else-if="selectedNode.type === 'setting'">
|
||
<el-descriptions :column="2" border>
|
||
<el-descriptions-item label="ID">{{ selectedNode.data.id }}</el-descriptions-item>
|
||
<el-descriptions-item label="名称">{{ selectedNode.data.name }}</el-descriptions-item>
|
||
<el-descriptions-item label="类型">
|
||
<el-tag :type="getTypeColor(selectedNode.data.type)">{{ selectedNode.data.type }}</el-tag>
|
||
</el-descriptions-item>
|
||
<el-descriptions-item label="配置组">{{ selectedNode.data.settingGroupID || '-' }}</el-descriptions-item>
|
||
<el-descriptions-item label="值" :span="2" class="value-descriptions-item">
|
||
<div class="value-content">
|
||
<span v-if="selectedNode.data.type === 'bool'">{{ selectedNode.data.value ? '是' : '否' }}</span>
|
||
<span v-else-if="selectedNode.data.type === 'file'">
|
||
<el-link v-if="selectedNode.data.value" type="primary" @click="previewFile(selectedNode.data.value)">
|
||
文件ID: {{ selectedNode.data.value }}
|
||
</el-link>
|
||
<span v-else>-</span>
|
||
</span>
|
||
<span v-else-if="selectedNode.data.type === 'file_list'">
|
||
<div v-if="selectedNode.data.value" class="table-file-list">
|
||
<div
|
||
v-for="(fileId, index) in getFileList(selectedNode.data.value)"
|
||
:key="fileId"
|
||
class="table-file-item"
|
||
>
|
||
<div class="file-preview-mini">
|
||
<img
|
||
v-if="selectedNode.data.parsedValue && selectedNode.data.parsedValue[index]"
|
||
:src="processImageUrl(selectedNode.data.parsedValue[index])"
|
||
:alt="`文件${index + 1}`"
|
||
class="preview-mini-image"
|
||
@click="previewImage(selectedNode.data.parsedValue[index])"
|
||
@error="handleImageError"
|
||
/>
|
||
<div v-else class="file-placeholder-mini">
|
||
<el-icon><Document /></el-icon>
|
||
</div>
|
||
</div>
|
||
<div class="file-info-mini">
|
||
<div class="file-name-mini" :title="`文件${index + 1}: ${fileId}`">
|
||
文件{{ index + 1 }}
|
||
</div>
|
||
<div class="file-id-mini">ID: {{ fileId }}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<span v-else>-</span>
|
||
</span>
|
||
<span v-else-if="selectedNode.data.type === 'string_list'">
|
||
<div class="editable-string-list">
|
||
<div class="string-list-header">
|
||
<span class="list-title">字符串列表 ({{ getStringList(selectedNode.data.value).length }} 项)</span>
|
||
<el-button type="primary" size="small" @click="addEditableStringItem">添加项目</el-button>
|
||
</div>
|
||
<div class="string-list-items">
|
||
<div
|
||
v-for="(item, index) in getEditableStringList()"
|
||
:key="`editable-${index}`"
|
||
class="string-list-item"
|
||
draggable="true"
|
||
@dragstart="handleDragStart($event, index)"
|
||
@dragover.prevent
|
||
@drop="handleDrop($event, index)"
|
||
>
|
||
<div class="drag-handle">
|
||
<el-icon><Rank /></el-icon>
|
||
</div>
|
||
<div class="item-content">
|
||
<el-input
|
||
v-if="item.editing"
|
||
v-model="item.value"
|
||
size="small"
|
||
@blur="finishEditItem(index)"
|
||
@keyup.enter="finishEditItem(index)"
|
||
ref="editInput"
|
||
@mounted="focusEditInput"
|
||
/>
|
||
<span v-else class="item-text" @dblclick="startEditItem(index)">
|
||
{{ truncateFileName(item.value, 40) }}
|
||
</span>
|
||
</div>
|
||
<div class="item-actions">
|
||
<el-button type="text" size="small" @click="startEditItem(index)" :icon="Edit" />
|
||
<el-button type="text" size="small" @click="removeEditableStringItem(index)" :icon="Delete" class="danger-btn" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</span>
|
||
<span v-else class="text-value">
|
||
{{ selectedNode.data.value }}
|
||
</span>
|
||
</div>
|
||
</el-descriptions-item>
|
||
<el-descriptions-item label="是否开放">
|
||
<el-tag :type="selectedNode.data.open ? 'success' : 'info'">
|
||
{{ selectedNode.data.open ? '开放' : '私有' }}
|
||
</el-tag>
|
||
</el-descriptions-item>
|
||
<el-descriptions-item label="备注" :span="2">{{ selectedNode.data.note || '-' }}</el-descriptions-item>
|
||
<el-descriptions-item label="创建时间">{{ formatDate(selectedNode.data.CreatedAt) }}</el-descriptions-item>
|
||
<el-descriptions-item label="更新时间">{{ formatDate(selectedNode.data.UpdatedAt) }}</el-descriptions-item>
|
||
</el-descriptions>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
</el-card>
|
||
|
||
<!-- 配置组表单对话框 -->
|
||
<el-dialog
|
||
v-model="groupDialogVisible"
|
||
:title="groupDialogTitle"
|
||
width="500px"
|
||
destroy-on-close
|
||
class="dialog-scrollable"
|
||
>
|
||
<el-form
|
||
ref="groupFormRef"
|
||
:model="groupForm"
|
||
:rules="groupRules"
|
||
label-width="100px"
|
||
>
|
||
<el-form-item label="名称" prop="name">
|
||
<el-input v-model="groupForm.name" placeholder="请输入配置组名称" />
|
||
</el-form-item>
|
||
<el-form-item label="备注" prop="note">
|
||
<el-input
|
||
v-model="groupForm.note"
|
||
type="textarea"
|
||
:rows="3"
|
||
placeholder="请输入备注信息"
|
||
/>
|
||
</el-form-item>
|
||
</el-form>
|
||
<template #footer>
|
||
<el-button @click="groupDialogVisible = false">取消</el-button>
|
||
<el-button type="primary" @click="submitGroupForm">确定</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- 配置表单对话框 -->
|
||
<el-dialog
|
||
v-model="settingDialogVisible"
|
||
:title="settingDialogTitle"
|
||
width="1000px"
|
||
destroy-on-close
|
||
class="dialog-scrollable"
|
||
>
|
||
<el-form
|
||
ref="settingFormRef"
|
||
:model="settingForm"
|
||
:rules="settingRules"
|
||
label-width="120px"
|
||
>
|
||
<el-form-item label="配置组" prop="settingGroupID">
|
||
<el-select v-model="settingForm.settingGroupID" placeholder="请选择配置组" style="width: 100%">
|
||
<el-option
|
||
v-for="group in allGroupList"
|
||
:key="group.id"
|
||
:label="group.name"
|
||
:value="group.id"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="名称" prop="name">
|
||
<el-input v-model="settingForm.name" placeholder="请输入配置名称" />
|
||
</el-form-item>
|
||
<el-form-item label="类型" prop="type">
|
||
<el-select v-model="settingForm.type" placeholder="请选择类型" style="width: 100%" @change="handleTypeChange">
|
||
<el-option label="字符串 (string)" value="string" />
|
||
<el-option label="整数 (int)" value="int" />
|
||
<el-option label="浮点数 (float)" value="float" />
|
||
<el-option label="布尔值 (bool)" value="bool" />
|
||
<el-option label="文件 (file)" value="file" />
|
||
<el-option label="多文件 (file_list)" value="file_list" />
|
||
<el-option label="字符串列表 (string_list)" value="string_list" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="值" prop="value">
|
||
<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"
|
||
:controls="false"
|
||
placeholder="请输入整数"
|
||
style="width: 100%"
|
||
/>
|
||
<el-input-number
|
||
v-else-if="settingForm.type === 'float'"
|
||
v-model="settingForm.value"
|
||
:controls="false"
|
||
:precision="2"
|
||
placeholder="请输入浮点数"
|
||
style="width: 100%"
|
||
/>
|
||
<el-switch
|
||
v-else-if="settingForm.type === 'bool'"
|
||
v-model="settingForm.value"
|
||
/>
|
||
<div v-else-if="settingForm.type === 'file'" class="file-upload-section">
|
||
<div class="file-info-display" v-if="settingForm.value && fileInfo">
|
||
<div class="file-item" :class="{ 'uploading': fileUploading && fileInfo.isLocal }">
|
||
<!-- 图片预览 -->
|
||
<div class="file-preview" v-if="fileInfo.url || fileInfo.localUrl" @click="openImageSelector" style="cursor: pointer;">
|
||
<img :src="fileInfo.localUrl || processImageUrl(fileInfo.url)" :alt="fileInfo.realName" class="preview-image" @click.stop="previewImage(fileInfo.localUrl || fileInfo.url)" @error="handleImageError" />
|
||
<!-- 上传状态指示器 -->
|
||
<div v-if="fileUploading && fileInfo.isLocal" class="upload-overlay">
|
||
<el-icon class="is-loading"><Loading /></el-icon>
|
||
<span>上传中...</span>
|
||
</div>
|
||
</div>
|
||
<!-- 图片加载失败占位符 -->
|
||
<div class="file-preview" v-else @click="openImageSelector" style="cursor: pointer;">
|
||
<div class="file-placeholder">
|
||
<el-icon><Document /></el-icon>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 文件信息 -->
|
||
<div class="file-details">
|
||
<span class="file-name" :title="fileInfo.realName || fileInfo.saveName">{{ truncateFileName(fileInfo.realName || fileInfo.saveName) }}</span>
|
||
<span class="file-id">文件ID: {{ settingForm.value }}</span>
|
||
<span class="file-size">{{ formatFileSize(fileInfo.size) }}</span>
|
||
<!-- 非图片文件预览链接 -->
|
||
|
||
</div>
|
||
|
||
<!-- 操作按钮 -->
|
||
<div class="file-actions">
|
||
<el-button type="primary" size="small" @click="openImageSelector" :icon="Picture">
|
||
修改图片
|
||
</el-button>
|
||
<el-button type="danger" size="small" @click="clearFile">删除</el-button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div v-else class="file-upload-options">
|
||
<div class="upload-methods">
|
||
<el-button
|
||
type="primary"
|
||
@click="openImageSelector"
|
||
:icon="Picture"
|
||
class="image-selector-btn"
|
||
>
|
||
从文件库选择图片
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div v-else-if="settingForm.type === 'file_list'" class="file-list-section">
|
||
<div class="editable-file-list">
|
||
<div class="file-list-header">
|
||
<span class="list-title">文件列表 ({{ getEditableFormFileList().length }} 项)</span>
|
||
<div class="header-actions">
|
||
<el-button size="small" @click="openImageSelectorForList" :icon="Picture">
|
||
从文件库选择
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
<div class="file-list-items">
|
||
<div
|
||
v-for="(fileInfo, index) in getEditableFormFileList()"
|
||
:key="`form-file-${index}`"
|
||
class="file-list-item"
|
||
draggable="true"
|
||
@dragstart="handleFormFileDragStart($event, index)"
|
||
@dragover.prevent
|
||
@drop="handleFormFileDrop($event, index)"
|
||
>
|
||
<div class="drag-handle">
|
||
<el-icon><Rank /></el-icon>
|
||
</div>
|
||
<div class="file-preview">
|
||
<img
|
||
v-if="fileInfo.url || fileInfo.localUrl"
|
||
:src="fileInfo.localUrl || processImageUrl(fileInfo.url)"
|
||
:alt="fileInfo.realName"
|
||
class="preview-image"
|
||
@click="previewImage(fileInfo.localUrl || fileInfo.url)"
|
||
@error="handleImageError"
|
||
/>
|
||
<div v-else class="file-placeholder">
|
||
<el-icon><Document /></el-icon>
|
||
</div>
|
||
<div v-if="fileInfo.uploading" class="upload-overlay">
|
||
<el-icon class="is-loading"><Loading /></el-icon>
|
||
<span>上传中...</span>
|
||
</div>
|
||
</div>
|
||
<div class="file-info">
|
||
<div class="file-name" :title="fileInfo.realName || fileInfo.saveName">
|
||
{{ truncateFileName(fileInfo.realName || fileInfo.saveName, 30) }}
|
||
</div>
|
||
<div class="file-id">ID: {{ fileInfo.id || '上传中' }}</div>
|
||
<div class="file-size">{{ formatFileSize(fileInfo.size) }}</div>
|
||
</div>
|
||
<div class="file-actions">
|
||
<el-button type="text" size="small" @click="openImageSelectorForListItem(index)" :icon="Edit" title="替换文件" />
|
||
<el-button type="text" size="small" @click="removeFormEditableFileItem(index)" :icon="Delete" class="danger-btn" title="删除文件" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div v-else-if="settingForm.type === 'string_list'" class="string-list-section">
|
||
<div class="editable-string-list">
|
||
<div class="string-list-header">
|
||
<span class="list-title">字符串列表 ({{ getEditableFormStringList().length }} 项)</span>
|
||
<el-button type="primary" size="small" @click="addFormEditableStringItem">添加项目</el-button>
|
||
</div>
|
||
<div class="string-list-items">
|
||
<div
|
||
v-for="(item, index) in getEditableFormStringList()"
|
||
:key="`form-editable-${index}`"
|
||
class="string-list-item"
|
||
draggable="true"
|
||
@dragstart="handleFormDragStart($event, index)"
|
||
@dragover.prevent
|
||
@drop="handleFormDrop($event, index)"
|
||
>
|
||
<div class="drag-handle">
|
||
<el-icon><Rank /></el-icon>
|
||
</div>
|
||
<div class="item-content">
|
||
<el-input
|
||
v-if="item.editing"
|
||
v-model="item.value"
|
||
size="small"
|
||
@blur="finishFormEditItem(index)"
|
||
@keyup.enter="finishFormEditItem(index)"
|
||
ref="formEditInput"
|
||
@mounted="focusFormEditInput"
|
||
/>
|
||
<span v-else class="item-text" @dblclick="startFormEditItem(index)">
|
||
{{ truncateFileName(item.value, 40) }}
|
||
</span>
|
||
</div>
|
||
<div class="item-actions">
|
||
<el-button type="text" size="small" @click="startFormEditItem(index)" :icon="Edit" />
|
||
<el-button type="text" size="small" @click="removeFormEditableStringItem(index)" :icon="Delete" class="danger-btn" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<el-input
|
||
v-else
|
||
v-model="settingForm.value"
|
||
placeholder="请输入配置值"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="是否开放访问">
|
||
<el-switch v-model="settingForm.open" />
|
||
<span style="margin-left: 10px; color: #909399; font-size: 12px;">
|
||
开启后允许公开访问
|
||
</span>
|
||
</el-form-item>
|
||
<el-form-item label="备注" prop="note">
|
||
<el-input
|
||
v-model="settingForm.note"
|
||
type="textarea"
|
||
:rows="3"
|
||
placeholder="请输入备注信息"
|
||
/>
|
||
</el-form-item>
|
||
</el-form>
|
||
<template #footer>
|
||
<el-button @click="settingDialogVisible = false">取消</el-button>
|
||
<el-button type="primary" @click="submitSettingForm">确定</el-button>
|
||
</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;" />
|
||
</el-dialog>
|
||
|
||
<!-- 图片选择器 -->
|
||
<ImageSelector
|
||
v-model="imageSelectorVisible"
|
||
:current-file-id="currentImageSelectorFileId"
|
||
:multiple="imageSelectorMode === 'list'"
|
||
@confirm="handleImageSelectorConfirm"
|
||
/>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, reactive, onMounted, watch, nextTick, computed } from 'vue'
|
||
import { useRoute } from 'vue-router'
|
||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||
import { Search, Plus, Delete, UploadFilled, Folder, Document, ArrowRight, Loading, Picture, Edit, Rank } from '@element-plus/icons-vue'
|
||
import {
|
||
getSettingGroupList,
|
||
getSettingGroupInfo,
|
||
createSettingGroup,
|
||
updateSettingGroup,
|
||
deleteSettingGroup,
|
||
getSettingList,
|
||
getSettingInfo,
|
||
createSetting,
|
||
updateSetting,
|
||
setSettingOpen,
|
||
deleteSetting
|
||
} from '@/api/admin/setting'
|
||
import { uploadFile, getFileDetail, downloadFile } from '@/api/admin/file'
|
||
import ImageSelector from '@/components/admin/ImageSelector.vue'
|
||
|
||
const route = useRoute()
|
||
|
||
// 树状图相关状态
|
||
const treeTableRef = ref(null)
|
||
const treeData = ref([])
|
||
const allGroupList = ref([]) // 存储所有已加载的配置组
|
||
const treeDataMap = ref(new Map()) // 存储树形数据,key为group_id
|
||
const selectedNode = ref(null)
|
||
const editableStringList = ref([]) // 弹窗中可编辑的字符串列表
|
||
const draggedIndex = ref(-1) // 拖拽的索引
|
||
|
||
// 表单相关可编辑列表
|
||
const editableFormStringList = ref([]) // 表单中可编辑的字符串列表
|
||
const editableFormFileList = ref([]) // 表单中可编辑的文件列表
|
||
const formDraggedIndex = ref(-1) // 表单拖拽索引
|
||
const formFileDraggedIndex = ref(-1) // 表单文件拖拽索引
|
||
|
||
// 查询参数
|
||
const queryParams = reactive({
|
||
key: '',
|
||
page: 1,
|
||
count: 10
|
||
})
|
||
|
||
// 树状图配置
|
||
const treeProps = {
|
||
children: 'children',
|
||
label: 'label',
|
||
hasChildren: 'hasChildren'
|
||
}
|
||
|
||
// 树形显示数据(扁平化用于表格显示)
|
||
const treeDisplayData = computed(() => {
|
||
const result = []
|
||
const rootItems = treeData.value || []
|
||
|
||
const flatten = (items, parentExpanded = true) => {
|
||
if (!parentExpanded) return
|
||
items.forEach(item => {
|
||
result.push(item)
|
||
if (item._expanded && item._children && item._children.length > 0) {
|
||
flatten(item._children, true)
|
||
}
|
||
})
|
||
}
|
||
|
||
flatten(rootItems)
|
||
return result
|
||
})
|
||
|
||
// ==================== 配置组相关 ====================
|
||
const groupForm = reactive({
|
||
id: undefined,
|
||
name: '',
|
||
note: ''
|
||
})
|
||
|
||
const groupRules = {
|
||
name: [
|
||
{ required: true, message: '请输入配置组名称', trigger: 'blur' }
|
||
]
|
||
}
|
||
|
||
const groupLoading = ref(false)
|
||
const groupList = ref([])
|
||
const groupTotal = ref(0)
|
||
const groupDialogVisible = ref(false)
|
||
const groupDialogTitle = ref('新增配置组')
|
||
const groupFormRef = ref(null)
|
||
|
||
// 添加缺失的变量
|
||
const groupQueryParams = reactive({
|
||
key: '',
|
||
page: 1,
|
||
count: 10
|
||
})
|
||
|
||
const settingQueryParams = reactive({
|
||
group_id: undefined,
|
||
key: '',
|
||
page: 1,
|
||
count: 10
|
||
})
|
||
|
||
// ==================== 配置相关 ====================
|
||
const settingForm = reactive({
|
||
id: undefined,
|
||
name: '',
|
||
value: '',
|
||
type: 'string',
|
||
settingGroupID: undefined,
|
||
open: false,
|
||
note: ''
|
||
})
|
||
|
||
const settingRules = {
|
||
name: [
|
||
{ required: true, message: '请输入配置名称', trigger: 'blur' }
|
||
],
|
||
type: [
|
||
{ required: true, message: '请选择配置类型', trigger: 'change' }
|
||
],
|
||
settingGroupID: [
|
||
{ required: true, message: '请选择配置组', trigger: 'change' }
|
||
]
|
||
}
|
||
|
||
const settingLoading = ref(false)
|
||
const settingList = ref([])
|
||
const settingTotal = ref(0)
|
||
const settingDialogVisible = ref(false)
|
||
const settingDialogTitle = ref('新增配置')
|
||
const settingFormRef = ref(null)
|
||
const toggleLoading = ref(null)
|
||
const fileInfo = ref(null)
|
||
const fileUploading = ref(false)
|
||
const fileListInfo = ref([])
|
||
const newStringItem = ref('')
|
||
const imageViewerVisible = ref(false)
|
||
const currentViewImage = ref('')
|
||
const imageSelectorVisible = ref(false)
|
||
const currentImageSelectorFileId = ref('')
|
||
const imageSelectorMode = ref('single') // 'single' 或 'list'
|
||
|
||
// 批量选择相关
|
||
const selectedRows = ref([])
|
||
|
||
// 格式化日期时间
|
||
const formatDate = (dateString) => {
|
||
if (!dateString) return '-'
|
||
const date = new Date(dateString)
|
||
const year = date.getFullYear()
|
||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||
const day = String(date.getDate()).padStart(2, '0')
|
||
const hours = String(date.getHours()).padStart(2, '0')
|
||
const minutes = String(date.getMinutes()).padStart(2, '0')
|
||
const seconds = String(date.getSeconds()).padStart(2, '0')
|
||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
|
||
}
|
||
|
||
// ==================== 树状图方法 ====================
|
||
|
||
// 相近字排序函数
|
||
const sortBySimilarity = (settings) => {
|
||
if (!settings || settings.length <= 1) return settings
|
||
|
||
// 提取所有配置项的名称
|
||
const names = settings.map(item => item.name || '')
|
||
|
||
// 按名称排序,相近的名称会排在一起
|
||
return settings.sort((a, b) => {
|
||
const nameA = (a.name || '').toLowerCase()
|
||
const nameB = (b.name || '').toLowerCase()
|
||
|
||
// 先按前缀分组排序
|
||
const prefixA = nameA.substring(0, 2)
|
||
const prefixB = nameB.substring(0, 2)
|
||
|
||
if (prefixA !== prefixB) {
|
||
return prefixA.localeCompare(prefixB, 'zh-CN')
|
||
}
|
||
|
||
// 前缀相同则按完整名称排序
|
||
return nameA.localeCompare(nameB, 'zh-CN')
|
||
})
|
||
}
|
||
|
||
// 加载配置项子节点
|
||
const loadChildren = async (row) => {
|
||
if (!row || row.type !== 'group') return
|
||
|
||
row._loading = true
|
||
try {
|
||
const groupId = row.data.id
|
||
console.log('Loading children for group:', groupId)
|
||
const res = await getSettingList({ group_id: groupId, page: 1, count: 10 })
|
||
console.log('getSettingList response:', res.data)
|
||
if (res.data.code === 200) {
|
||
let settings = res.data.data.data || []
|
||
console.log('settings loaded:', settings)
|
||
|
||
// 应用相近字排序
|
||
settings = sortBySimilarity(settings)
|
||
console.log('settings after sorting:', settings)
|
||
|
||
const settingNodes = settings.map(setting => ({
|
||
id: `setting_${setting.id}`,
|
||
label: setting.name,
|
||
type: 'setting',
|
||
data: setting,
|
||
level: row.level + 1,
|
||
hasChildren: false, // 配置项没有子节点
|
||
_expanded: false,
|
||
_children: [],
|
||
_loading: false
|
||
}))
|
||
console.log('settingNodes created:', settingNodes)
|
||
|
||
row._children = settingNodes
|
||
console.log('updated row with children:', row)
|
||
}
|
||
} catch (error) {
|
||
console.error('加载配置列表失败:', error)
|
||
ElMessage.error('加载配置列表失败')
|
||
} finally {
|
||
row._loading = false
|
||
}
|
||
}
|
||
|
||
// 切换展开状态
|
||
const toggleExpand = async (row) => {
|
||
if (row._loading) return
|
||
|
||
if (row._expanded) {
|
||
// 收起
|
||
row._expanded = false
|
||
} else {
|
||
// 展开 - 如果还没加载子级,先加载
|
||
if (row.type === 'group' && (!row._children || row._children.length === 0)) {
|
||
await loadChildren(row)
|
||
}
|
||
row._expanded = true
|
||
}
|
||
}
|
||
|
||
// 初始化加载配置组
|
||
const loadGroups = async () => {
|
||
try {
|
||
const res = await getSettingGroupList({ page: 1, count: 10 })
|
||
console.log('getSettingGroupList response:', res.data)
|
||
if (res.data.code === 200) {
|
||
const groups = res.data.data.data || []
|
||
console.log('groups loaded:', groups)
|
||
const treeNodes = groups.map(group => ({
|
||
id: `group_${group.id}`,
|
||
label: group.name,
|
||
type: 'group',
|
||
data: group,
|
||
level: 1,
|
||
hasChildren: true, // 标记有子节点
|
||
_expanded: false,
|
||
_children: [],
|
||
_loading: false
|
||
}))
|
||
console.log('treeNodes created:', treeNodes)
|
||
treeData.value = treeNodes
|
||
allGroupList.value = groups
|
||
console.log('data set to reactive refs')
|
||
}
|
||
} catch (error) {
|
||
console.error('加载配置组失败:', error)
|
||
ElMessage.error('加载配置组失败')
|
||
}
|
||
}
|
||
|
||
// 节点点击事件
|
||
const handleNodeClick = (row) => {
|
||
selectedNode.value = row
|
||
|
||
// 如果是字符串列表类型,初始化可编辑列表
|
||
if (row.data.type === 'string_list') {
|
||
initEditableStringList()
|
||
}
|
||
}
|
||
|
||
// 查询处理
|
||
const handleQuery = () => {
|
||
// 重新加载树
|
||
treeData.value = []
|
||
selectedNode.value = null
|
||
|
||
// 重新加载配置组
|
||
loadGroups()
|
||
}
|
||
|
||
// 重置查询
|
||
const resetQuery = () => {
|
||
queryParams.key = ''
|
||
handleQuery()
|
||
}
|
||
|
||
// 批量删除
|
||
const handleBatchDelete = () => {
|
||
if (selectedRows.value.length === 0) {
|
||
ElMessage.warning('请先选择要删除的项目')
|
||
return
|
||
}
|
||
|
||
// 分离配置组和配置项
|
||
const groups = selectedRows.value.filter(row => row.type === 'group')
|
||
const settings = selectedRows.value.filter(row => row.type === 'setting')
|
||
|
||
let confirmMessage = ''
|
||
if (groups.length > 0 && settings.length > 0) {
|
||
confirmMessage = `确认删除选中的 ${groups.length} 个配置组和 ${settings.length} 个配置项吗?删除配置组将同时删除其下的所有配置项。`
|
||
} else if (groups.length > 0) {
|
||
confirmMessage = `确认删除选中的 ${groups.length} 个配置组吗?删除配置组将同时删除其下的所有配置项。`
|
||
} else {
|
||
confirmMessage = `确认删除选中的 ${settings.length} 个配置项吗?`
|
||
}
|
||
|
||
ElMessageBox.confirm(confirmMessage, '批量删除警告', {
|
||
confirmButtonText: '确定',
|
||
cancelButtonText: '取消',
|
||
type: 'warning'
|
||
}).then(async () => {
|
||
const deletePromises = []
|
||
|
||
// 删除配置组
|
||
groups.forEach(row => {
|
||
deletePromises.push(deleteSettingGroup({ setting_group_id: row.data.id }))
|
||
})
|
||
|
||
// 删除配置项
|
||
settings.forEach(row => {
|
||
deletePromises.push(deleteSetting({ id: row.data.id }))
|
||
})
|
||
|
||
try {
|
||
const results = await Promise.all(deletePromises)
|
||
const successCount = results.filter(res => res.data.code === 200).length
|
||
|
||
if (successCount === selectedRows.value.length) {
|
||
ElMessage.success('批量删除成功')
|
||
} else {
|
||
ElMessage.warning(`成功删除 ${successCount} 项,失败 ${selectedRows.value.length - successCount} 项`)
|
||
}
|
||
|
||
// 清空选择
|
||
selectedRows.value = []
|
||
handleQuery()
|
||
} catch (error) {
|
||
console.error('批量删除失败:', error)
|
||
ElMessage.error('批量删除失败')
|
||
}
|
||
}).catch(() => {})
|
||
}
|
||
|
||
// 删除节点
|
||
const handleDelete = (data) => {
|
||
const type = data.type
|
||
const rowData = data.data
|
||
|
||
if (type === 'group') {
|
||
ElMessageBox.confirm(`确认删除配置组 "${rowData.name}" 吗?`, '警告', {
|
||
confirmButtonText: '确定',
|
||
cancelButtonText: '取消',
|
||
type: 'warning'
|
||
}).then(async () => {
|
||
try {
|
||
const res = await deleteSettingGroup({ setting_group_id: rowData.id })
|
||
if (res.data.code === 200) {
|
||
ElMessage.success('删除成功')
|
||
handleQuery()
|
||
}
|
||
} catch (error) {
|
||
console.error('删除失败:', error)
|
||
ElMessage.error(error.response?.data?.message || '删除失败')
|
||
}
|
||
}).catch(() => {})
|
||
} else if (type === 'setting') {
|
||
ElMessageBox.confirm(`确认删除配置 "${rowData.name}" 吗?`, '警告', {
|
||
confirmButtonText: '确定',
|
||
cancelButtonText: '取消',
|
||
type: 'warning'
|
||
}).then(async () => {
|
||
try {
|
||
const res = await deleteSetting({ id: rowData.id })
|
||
if (res.data.code === 200) {
|
||
ElMessage.success('删除成功')
|
||
handleQuery()
|
||
}
|
||
} catch (error) {
|
||
console.error('删除失败:', error)
|
||
ElMessage.error(error.response?.data?.message || '删除失败')
|
||
}
|
||
}).catch(() => {})
|
||
}
|
||
}
|
||
|
||
// 处理表格选择变化
|
||
const handleSelectionChange = (selection) => {
|
||
selectedRows.value = selection
|
||
}
|
||
|
||
// ==================== 配置组方法 ====================
|
||
|
||
// 新增配置组
|
||
const handleAddGroup = () => {
|
||
groupDialogTitle.value = '新增配置组'
|
||
Object.assign(groupForm, {
|
||
id: undefined,
|
||
name: '',
|
||
note: ''
|
||
})
|
||
groupDialogVisible.value = true
|
||
}
|
||
|
||
// 编辑配置组
|
||
const handleEditGroup = async (row) => {
|
||
groupDialogTitle.value = '编辑配置组'
|
||
try {
|
||
const res = await getSettingGroupInfo({ setting_group_id: row.id })
|
||
if (res.data.code === 200) {
|
||
Object.assign(groupForm, {
|
||
id: res.data.data.id,
|
||
name: res.data.data.name || '',
|
||
note: res.data.data.note || ''
|
||
})
|
||
groupDialogVisible.value = true
|
||
}
|
||
} catch (error) {
|
||
console.error('获取配置组详情失败:', error)
|
||
ElMessage.error('获取配置组详情失败')
|
||
}
|
||
}
|
||
|
||
// 提交配置组表单
|
||
const submitGroupForm = () => {
|
||
groupFormRef.value?.validate(async (valid) => {
|
||
if (valid) {
|
||
try {
|
||
const submitData = {
|
||
name: groupForm.name,
|
||
note: groupForm.note
|
||
}
|
||
if (groupForm.id) {
|
||
submitData.id = groupForm.id
|
||
}
|
||
const res = groupForm.id
|
||
? await updateSettingGroup(submitData)
|
||
: await createSettingGroup(submitData)
|
||
if (res.data.code === 200) {
|
||
ElMessage.success(groupForm.id ? '修改成功' : '创建成功')
|
||
groupDialogVisible.value = false
|
||
handleQuery()
|
||
}
|
||
} catch (error) {
|
||
console.error('提交失败:', error)
|
||
ElMessage.error(error.response?.data?.message || '提交失败')
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
// ==================== 配置方法 ====================
|
||
|
||
// 新增配置
|
||
const handleAddSetting = () => {
|
||
settingDialogTitle.value = '新增配置'
|
||
Object.assign(settingForm, {
|
||
id: undefined,
|
||
name: '',
|
||
value: '',
|
||
type: 'string',
|
||
settingGroupID: selectedNode.value?.type === 'group' ? selectedNode.value.data.id : undefined,
|
||
open: true,
|
||
note: ''
|
||
})
|
||
fileInfo.value = null
|
||
fileListInfo.value = []
|
||
newStringItem.value = ''
|
||
settingDialogVisible.value = true
|
||
}
|
||
|
||
// 为指定配置组新增配置
|
||
const handleAddSettingToGroup = (groupData) => {
|
||
settingDialogTitle.value = '新增配置'
|
||
Object.assign(settingForm, {
|
||
id: undefined,
|
||
name: '',
|
||
value: '',
|
||
type: 'string',
|
||
settingGroupID: groupData.id,
|
||
open: true,
|
||
note: ''
|
||
})
|
||
fileInfo.value = null
|
||
fileListInfo.value = []
|
||
newStringItem.value = ''
|
||
settingDialogVisible.value = true
|
||
}
|
||
|
||
// 编辑配置
|
||
const handleEditSetting = async (row) => {
|
||
settingDialogTitle.value = '编辑配置'
|
||
try {
|
||
const res = await getSettingInfo({ id: row.id })
|
||
if (res.data.code === 200) {
|
||
const data = res.data.data
|
||
Object.assign(settingForm, {
|
||
id: data.id,
|
||
name: data.name || '',
|
||
value: data.value,
|
||
type: data.type || 'string',
|
||
settingGroupID: data.settingGroupID,
|
||
open: data.open || false,
|
||
note: data.note || ''
|
||
})
|
||
if (data.type === 'bool') {
|
||
settingForm.value = data.value === true || data.value === 'true' || data.value === 1
|
||
} else if (data.type === 'int') {
|
||
settingForm.value = parseInt(data.value) || 0
|
||
} else if (data.type === 'float') {
|
||
settingForm.value = parseFloat(data.value) || 0
|
||
} else if (data.type === 'file') {
|
||
// 处理单个文件类型,获取文件信息
|
||
if (data.parsedValue && typeof data.parsedValue === 'string') {
|
||
fileInfo.value = {
|
||
id: data.value,
|
||
url: processImageUrl(data.parsedValue), // 使用processImageUrl处理URL
|
||
realName: '文件',
|
||
saveName: 'file',
|
||
size: 0
|
||
}
|
||
} else {
|
||
fileInfo.value = null
|
||
}
|
||
} else if (data.type === 'string_list') {
|
||
// 处理字符串列表类型,对每个字符串进行截断
|
||
if (data.parsedValue && Array.isArray(data.parsedValue)) {
|
||
const truncatedValues = data.parsedValue.map(item => truncateFileName(item, 25))
|
||
settingForm.value = truncatedValues.join(',')
|
||
initEditableFormStringList() // 初始化表单可编辑字符串列表
|
||
} else {
|
||
settingForm.value = ''
|
||
editableFormStringList.value = []
|
||
}
|
||
newStringItem.value = ''
|
||
} else if (data.type === 'file_list') {
|
||
// 处理文件列表类型,使用parsedValue来获取文件信息
|
||
if (data.parsedValue && Array.isArray(data.parsedValue)) {
|
||
fileListInfo.value = data.parsedValue.map((url, index) => {
|
||
const fileIds = getFileList(data.value)
|
||
return {
|
||
id: fileIds[index] || `file_${index}`,
|
||
url: processImageUrl(url), // 使用processImageUrl处理URL
|
||
realName: `文件${index + 1}`,
|
||
saveName: `file_${index}`,
|
||
size: 0
|
||
}
|
||
})
|
||
// 确保在设置fileListInfo后再初始化表单可编辑文件列表
|
||
nextTick(() => {
|
||
initEditableFormFileList()
|
||
})
|
||
} else {
|
||
fileListInfo.value = []
|
||
editableFormFileList.value = []
|
||
}
|
||
}
|
||
settingDialogVisible.value = true
|
||
}
|
||
} catch (error) {
|
||
console.error('获取配置详情失败:', error)
|
||
ElMessage.error('获取配置详情失败')
|
||
}
|
||
}
|
||
|
||
// 获取类型颜色
|
||
const getTypeColor = (type) => {
|
||
const colorMap = {
|
||
'string': 'primary',
|
||
'int': 'success',
|
||
'float': 'warning',
|
||
'bool': 'info',
|
||
'file': 'danger',
|
||
'file_list': 'danger',
|
||
'string_list': 'primary'
|
||
}
|
||
return colorMap[type] || ''
|
||
}
|
||
|
||
// 添加缺失的辅助函数
|
||
const fetchGroupList = async () => {
|
||
groupLoading.value = true
|
||
try {
|
||
const res = await getSettingGroupList(groupQueryParams)
|
||
if (res.data.code === 200) {
|
||
groupList.value = res.data.data.data || []
|
||
groupTotal.value = res.data.data.all_count || 0
|
||
}
|
||
} catch (error) {
|
||
console.error('获取配置组列表失败:', error)
|
||
ElMessage.error('获取配置组列表失败')
|
||
} finally {
|
||
groupLoading.value = false
|
||
}
|
||
}
|
||
|
||
const fetchAllGroupList = async () => {
|
||
try {
|
||
const res = await getSettingGroupList({ page: 1, count: 10 })
|
||
if (res.data.code === 200) {
|
||
allGroupList.value = res.data.data.data || []
|
||
}
|
||
} catch (error) {
|
||
console.error('获取配置组列表失败:', error)
|
||
}
|
||
}
|
||
|
||
// 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
|
||
|
||
// 创建本地预览URL
|
||
const localUrl = URL.createObjectURL(file.raw)
|
||
const isImage = file.raw.type.startsWith('image/')
|
||
|
||
// 立即显示本地预览
|
||
fileInfo.value = {
|
||
id: '', // 暂时为空,上传成功后会更新
|
||
url: isImage ? localUrl : '',
|
||
realName: file.name,
|
||
saveName: file.name,
|
||
size: file.size,
|
||
isLocal: true // 标记为本地文件
|
||
}
|
||
|
||
try {
|
||
const formData = new FormData()
|
||
formData.append('file_names', file.name)
|
||
formData.append('files', file.raw)
|
||
formData.append('update_type','cover')
|
||
formData.append('open_down','true')
|
||
|
||
const res = await uploadFile(formData)
|
||
if (res.data.code === 200 && res.data.data && res.data.data.length > 0) {
|
||
const uploadedFile = res.data.data[0]
|
||
settingForm.value = String(uploadedFile.id || '')
|
||
|
||
// 释放本地URL(暂时不释放,保留用于渲染)
|
||
// if (fileInfo.value?.isLocal) {
|
||
// URL.revokeObjectURL(fileInfo.value.url)
|
||
// }
|
||
|
||
// 更新为服务器返回的文件信息,但保留本地URL用于渲染
|
||
fileInfo.value = {
|
||
id: uploadedFile.id || '',
|
||
url: processImageUrl(uploadedFile.url || uploadedFile.realName || ''),
|
||
localUrl: fileInfo.value?.url || '', // 保留本地URL用于渲染
|
||
realName: uploadedFile.realName || '文件',
|
||
saveName: uploadedFile.saveName || 'file',
|
||
size: uploadedFile.size || 0,
|
||
isLocal: false // 标记为已上传,但保留本地渲染
|
||
}
|
||
ElMessage.success('文件上传成功')
|
||
} else {
|
||
// 上传失败,保留本地预览,但显示错误消息
|
||
ElMessage.error(res.data.message || '文件上传失败')
|
||
// 注意:不清理本地预览,让用户可以重新上传或删除
|
||
}
|
||
} catch (error) {
|
||
console.error('文件上传失败:', error)
|
||
ElMessage.error('文件上传失败')
|
||
} finally {
|
||
fileUploading.value = false
|
||
}
|
||
}
|
||
|
||
const clearFile = () => {
|
||
// 释放本地URL
|
||
if (fileInfo.value?.localUrl) {
|
||
URL.revokeObjectURL(fileInfo.value.localUrl)
|
||
}
|
||
if (fileInfo.value?.isLocal && fileInfo.value?.url) {
|
||
URL.revokeObjectURL(fileInfo.value.url)
|
||
}
|
||
|
||
settingForm.value = ''
|
||
fileInfo.value = null
|
||
}
|
||
|
||
const formatFileSize = (bytes) => {
|
||
if (!bytes) return '0 B'
|
||
const k = 1024
|
||
const sizes = ['B', 'KB', 'MB', 'GB']
|
||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
|
||
}
|
||
|
||
const truncateFileName = (fileName, maxLength = 25) => {
|
||
if (!fileName) return ''
|
||
if (fileName.length <= maxLength) return fileName
|
||
|
||
const extension = fileName.includes('.') ? '.' + fileName.split('.').pop() : ''
|
||
const nameWithoutExt = fileName.substring(0, fileName.lastIndexOf('.'))
|
||
|
||
if (nameWithoutExt.length <= maxLength) {
|
||
return fileName
|
||
}
|
||
|
||
const truncatedName = nameWithoutExt.substring(0, maxLength - 3) // 留3个字符给"...和扩展名"
|
||
console.log('111',truncatedName + '...' + extension)
|
||
return truncatedName + '...' + extension
|
||
}
|
||
|
||
// ==================== 可编辑字符串列表功能 ====================
|
||
|
||
// 获取可编辑的字符串列表
|
||
const getEditableStringList = () => {
|
||
if (!selectedNode.value || selectedNode.value.data.type !== 'string_list') {
|
||
return []
|
||
}
|
||
return editableStringList.value
|
||
}
|
||
|
||
// 初始化可编辑字符串列表
|
||
const initEditableStringList = () => {
|
||
if (!selectedNode.value || selectedNode.value.data.type !== 'string_list') {
|
||
editableStringList.value = []
|
||
return
|
||
}
|
||
|
||
const stringItems = getStringList(selectedNode.value.data.value)
|
||
editableStringList.value = stringItems.map(item => ({
|
||
value: item,
|
||
editing: false
|
||
}))
|
||
}
|
||
|
||
// 开始拖拽
|
||
const handleDragStart = (event, index) => {
|
||
draggedIndex.value = index
|
||
event.dataTransfer.effectAllowed = 'move'
|
||
event.dataTransfer.setData('text/html', event.target.outerHTML)
|
||
event.target.style.opacity = '0.5'
|
||
}
|
||
|
||
// 拖拽放下
|
||
const handleDrop = (event, dropIndex) => {
|
||
event.preventDefault()
|
||
const dragIndex = draggedIndex.value
|
||
|
||
if (dragIndex === dropIndex) return
|
||
|
||
// 重新排列数组
|
||
const newList = [...editableStringList.value]
|
||
const [draggedItem] = newList.splice(dragIndex, 1)
|
||
newList.splice(dropIndex, 0, draggedItem)
|
||
|
||
editableStringList.value = newList
|
||
draggedIndex.value = -1
|
||
|
||
// 更新selectedNode中的数据
|
||
const updatedValues = newList.map(item => item.value)
|
||
selectedNode.value.data.value = updatedValues.join(',')
|
||
|
||
event.target.style.opacity = '1'
|
||
}
|
||
|
||
// 添加新字符串项目
|
||
const addEditableStringItem = () => {
|
||
const newItem = {
|
||
value: '',
|
||
editing: true
|
||
}
|
||
editableStringList.value.push(newItem)
|
||
|
||
// 更新selectedNode中的数据
|
||
const updatedValues = editableStringList.value.map(item => item.value)
|
||
selectedNode.value.data.value = updatedValues.join(',')
|
||
|
||
// 聚焦到新添加的输入框
|
||
nextTick(() => {
|
||
const inputs = document.querySelectorAll('.string-list-item input')
|
||
if (inputs.length > 0) {
|
||
inputs[inputs.length - 1].focus()
|
||
}
|
||
})
|
||
}
|
||
|
||
// 开始编辑项目
|
||
const startEditItem = (index) => {
|
||
editableStringList.value[index].editing = true
|
||
|
||
nextTick(() => {
|
||
const inputs = document.querySelectorAll('.string-list-item input')
|
||
if (inputs[index]) {
|
||
inputs[index].focus()
|
||
inputs[index].select()
|
||
}
|
||
})
|
||
}
|
||
|
||
// 完成编辑项目
|
||
const finishEditItem = (index) => {
|
||
editableStringList.value[index].editing = false
|
||
|
||
// 更新selectedNode中的数据
|
||
const updatedValues = editableStringList.value.map(item => item.value)
|
||
selectedNode.value.data.value = updatedValues.join(',')
|
||
}
|
||
|
||
// 删除字符串项目
|
||
const removeEditableStringItem = (index) => {
|
||
ElMessageBox.confirm('确定要删除这个项目吗?', '提示', {
|
||
confirmButtonText: '确定',
|
||
cancelButtonText: '取消',
|
||
type: 'warning'
|
||
}).then(() => {
|
||
editableStringList.value.splice(index, 1)
|
||
|
||
// 更新selectedNode中的数据
|
||
const updatedValues = editableStringList.value.map(item => item.value)
|
||
selectedNode.value.data.value = updatedValues.join(',')
|
||
|
||
ElMessage.success('删除成功')
|
||
})
|
||
}
|
||
|
||
// 聚焦输入框
|
||
const focusEditInput = () => {
|
||
// 这个方法会被 @mounted 调用
|
||
}
|
||
|
||
// ==================== 表单可编辑字符串列表功能 ====================
|
||
|
||
// 获取表单可编辑的字符串列表
|
||
const getEditableFormStringList = () => {
|
||
return editableFormStringList.value
|
||
}
|
||
|
||
// 初始化表单可编辑字符串列表
|
||
const initEditableFormStringList = () => {
|
||
const stringItems = getStringList(settingForm.value)
|
||
editableFormStringList.value = stringItems.map(item => ({
|
||
value: item,
|
||
editing: false
|
||
}))
|
||
}
|
||
|
||
// 开始表单拖拽
|
||
const handleFormDragStart = (event, index) => {
|
||
formDraggedIndex.value = index
|
||
event.dataTransfer.effectAllowed = 'move'
|
||
event.dataTransfer.setData('text/html', event.target.outerHTML)
|
||
event.target.style.opacity = '0.5'
|
||
}
|
||
|
||
// 表单拖拽放下
|
||
const handleFormDrop = (event, dropIndex) => {
|
||
event.preventDefault()
|
||
const dragIndex = formDraggedIndex.value
|
||
|
||
if (dragIndex === dropIndex) return
|
||
|
||
// 重新排列数组
|
||
const newList = [...editableFormStringList.value]
|
||
const [draggedItem] = newList.splice(dragIndex, 1)
|
||
newList.splice(dropIndex, 0, draggedItem)
|
||
|
||
editableFormStringList.value = newList
|
||
formDraggedIndex.value = -1
|
||
|
||
// 更新表单值
|
||
const updatedValues = newList.map(item => item.value)
|
||
settingForm.value = updatedValues.join(',')
|
||
|
||
event.target.style.opacity = '1'
|
||
}
|
||
|
||
// 添加表单新字符串项目
|
||
const addFormEditableStringItem = () => {
|
||
const newItem = {
|
||
value: '',
|
||
editing: true
|
||
}
|
||
editableFormStringList.value.push(newItem)
|
||
|
||
// 更新表单值
|
||
const updatedValues = editableFormStringList.value.map(item => item.value)
|
||
settingForm.value = updatedValues.join(',')
|
||
|
||
// 聚焦到新添加的输入框
|
||
nextTick(() => {
|
||
const inputs = document.querySelectorAll('.string-list-item input')
|
||
if (inputs.length > 0) {
|
||
inputs[inputs.length - 1].focus()
|
||
}
|
||
})
|
||
}
|
||
|
||
// 开始编辑表单项目
|
||
const startFormEditItem = (index) => {
|
||
editableFormStringList.value[index].editing = true
|
||
|
||
nextTick(() => {
|
||
const inputs = document.querySelectorAll('.string-list-item input')
|
||
if (inputs[index]) {
|
||
inputs[index].focus()
|
||
inputs[index].select()
|
||
}
|
||
})
|
||
}
|
||
|
||
// 完成编辑表单项目
|
||
const finishFormEditItem = (index) => {
|
||
editableFormStringList.value[index].editing = false
|
||
|
||
// 更新表单值
|
||
const updatedValues = editableFormStringList.value.map(item => item.value)
|
||
settingForm.value = updatedValues.join(',')
|
||
}
|
||
|
||
// 删除表单字符串项目
|
||
const removeFormEditableStringItem = (index) => {
|
||
ElMessageBox.confirm('确定要删除这个项目吗?', '提示', {
|
||
confirmButtonText: '确定',
|
||
cancelButtonText: '取消',
|
||
type: 'warning'
|
||
}).then(() => {
|
||
editableFormStringList.value.splice(index, 1)
|
||
|
||
// 更新表单值
|
||
const updatedValues = editableFormStringList.value.map(item => item.value)
|
||
settingForm.value = updatedValues.join(',')
|
||
|
||
ElMessage.success('删除成功')
|
||
})
|
||
}
|
||
|
||
// 聚焦表单输入框
|
||
const focusFormEditInput = () => {
|
||
// 这个方法会被 @mounted 调用
|
||
}
|
||
|
||
// ==================== 表单可编辑文件列表功能 ====================
|
||
|
||
// 获取表单可编辑的文件列表
|
||
const getEditableFormFileList = () => {
|
||
return editableFormFileList.value
|
||
}
|
||
|
||
// 初始化表单可编辑文件列表
|
||
const initEditableFormFileList = () => {
|
||
if (settingForm.type === 'file_list' && fileListInfo.value && fileListInfo.value.length > 0) {
|
||
editableFormFileList.value = fileListInfo.value.map(file => ({
|
||
id: file.id || '',
|
||
url: file.url || '',
|
||
localUrl: file.localUrl || '', // 保留本地URL字段
|
||
realName: file.realName || '文件',
|
||
saveName: file.saveName || 'file',
|
||
size: file.size || 0,
|
||
uploading: false
|
||
}))
|
||
} else {
|
||
editableFormFileList.value = []
|
||
}
|
||
}
|
||
|
||
// 开始表单文件拖拽
|
||
const handleFormFileDragStart = (event, index) => {
|
||
formFileDraggedIndex.value = index
|
||
event.dataTransfer.effectAllowed = 'move'
|
||
event.dataTransfer.setData('text/html', event.target.outerHTML)
|
||
event.target.style.opacity = '0.5'
|
||
}
|
||
|
||
// 表单文件拖拽放下
|
||
const handleFormFileDrop = (event, dropIndex) => {
|
||
event.preventDefault()
|
||
const dragIndex = formFileDraggedIndex.value
|
||
|
||
if (dragIndex === dropIndex) return
|
||
|
||
// 重新排列数组
|
||
const newList = [...editableFormFileList.value]
|
||
const [draggedItem] = newList.splice(dragIndex, 1)
|
||
newList.splice(dropIndex, 0, draggedItem)
|
||
|
||
editableFormFileList.value = newList
|
||
formFileDraggedIndex.value = -1
|
||
|
||
// 更新fileListInfo和表单值(使用独立副本,避免引用同一数组)
|
||
fileListInfo.value = newList.map(item => ({ ...item }))
|
||
updateFormFileListValue()
|
||
|
||
event.target.style.opacity = '1'
|
||
}
|
||
|
||
// 表单文件列表变更
|
||
const handleFormFileListChange = async (file) => {
|
||
// 创建本地预览URL
|
||
const localUrl = URL.createObjectURL(file.raw)
|
||
const isImage = file.raw.type.startsWith('image/')
|
||
|
||
// 立即添加到表单文件列表
|
||
const tempFile = {
|
||
id: '', // 暂时为空,上传成功后会更新
|
||
url: isImage ? localUrl : '',
|
||
realName: file.name,
|
||
saveName: file.name,
|
||
size: file.size,
|
||
isLocal: true,
|
||
uploading: true
|
||
}
|
||
|
||
editableFormFileList.value.push(tempFile)
|
||
|
||
try {
|
||
const formData = new FormData()
|
||
formData.append('file_names', file.name)
|
||
formData.append('files', file.raw)
|
||
formData.append('update_type','cover')
|
||
formData.append('open_down','true')
|
||
|
||
const res = await uploadFile(formData)
|
||
if (res.data.code === 200 && res.data.data && res.data.data.length > 0) {
|
||
const uploadedFile = res.data.data[0]
|
||
|
||
// 找到对应的本地文件并更新
|
||
const index = editableFormFileList.value.findIndex(f => f.isLocal && f.realName === file.name)
|
||
if (index !== -1) {
|
||
editableFormFileList.value[index] = {
|
||
id: uploadedFile.id || '',
|
||
url: processImageUrl(uploadedFile.url || uploadedFile.realName || ''),
|
||
localUrl: localUrl, // 保留本地URL用于渲染
|
||
realName: uploadedFile.realName || '文件',
|
||
saveName: uploadedFile.saveName || 'file',
|
||
size: uploadedFile.size || 0,
|
||
isLocal: false,
|
||
uploading: false
|
||
}
|
||
}
|
||
|
||
updateFormFileListValue()
|
||
ElMessage.success('文件上传成功')
|
||
} else {
|
||
// 上传失败,移除临时文件
|
||
const index = editableFormFileList.value.findIndex(f => f.isLocal && f.realName === file.name)
|
||
if (index !== -1) {
|
||
editableFormFileList.value.splice(index, 1)
|
||
}
|
||
ElMessage.error(res.data.message || '文件上传失败')
|
||
}
|
||
} catch (error) {
|
||
console.error('文件上传失败:', error)
|
||
// 上传失败,移除临时文件
|
||
const index = editableFormFileList.value.findIndex(f => f.isLocal && f.realName === file.name)
|
||
if (index !== -1) {
|
||
editableFormFileList.value.splice(index, 1)
|
||
}
|
||
ElMessage.error('文件上传失败')
|
||
}
|
||
}
|
||
|
||
// 表单文件替换
|
||
const handleFormFileReplace = async (file, index) => {
|
||
if (!editableFormFileList.value[index]) return
|
||
|
||
// 创建本地预览URL
|
||
const localUrl = URL.createObjectURL(file.raw)
|
||
const isImage = file.raw.type.startsWith('image/')
|
||
|
||
// 更新文件信息
|
||
editableFormFileList.value[index] = {
|
||
...editableFormFileList.value[index],
|
||
url: isImage ? localUrl : '',
|
||
realName: file.name,
|
||
saveName: file.name,
|
||
size: file.size,
|
||
isLocal: true,
|
||
uploading: true
|
||
}
|
||
|
||
try {
|
||
const formData = new FormData()
|
||
formData.append('file_names', file.name)
|
||
formData.append('files', file.raw)
|
||
formData.append('update_type','cover')
|
||
formData.append('open_down','true')
|
||
|
||
const res = await uploadFile(formData)
|
||
if (res.data.code === 200 && res.data.data && res.data.data.length > 0) {
|
||
const uploadedFile = res.data.data[0]
|
||
|
||
// 更新文件信息
|
||
editableFormFileList.value[index] = {
|
||
id: uploadedFile.id || '',
|
||
url: processImageUrl(uploadedFile.url || uploadedFile.realName || ''),
|
||
localUrl: localUrl, // 保留本地URL用于渲染
|
||
realName: uploadedFile.realName || '文件',
|
||
saveName: uploadedFile.saveName || 'file',
|
||
size: uploadedFile.size || 0,
|
||
isLocal: false,
|
||
uploading: false
|
||
}
|
||
|
||
updateFormFileListValue()
|
||
ElMessage.success('文件替换成功')
|
||
} else {
|
||
// 上传失败,恢复原文件信息
|
||
ElMessage.error(res.data.message || '文件替换失败')
|
||
}
|
||
} catch (error) {
|
||
console.error('文件替换失败:', error)
|
||
ElMessage.error('文件替换失败')
|
||
}
|
||
}
|
||
|
||
// 删除表单文件项目
|
||
const removeFormEditableFileItem = (index) => {
|
||
ElMessageBox.confirm('确定要删除这个文件吗?', '提示', {
|
||
confirmButtonText: '确定',
|
||
cancelButtonText: '取消',
|
||
type: 'warning'
|
||
}).then(() => {
|
||
// 释放本地URL
|
||
if (editableFormFileList.value[index].localUrl) {
|
||
URL.revokeObjectURL(editableFormFileList.value[index].localUrl)
|
||
}
|
||
if (editableFormFileList.value[index].isLocal && editableFormFileList.value[index].url) {
|
||
URL.revokeObjectURL(editableFormFileList.value[index].url)
|
||
}
|
||
|
||
editableFormFileList.value.splice(index, 1)
|
||
|
||
// 更新fileListInfo和表单值
|
||
fileListInfo.value = editableFormFileList.value
|
||
updateFormFileListValue()
|
||
|
||
ElMessage.success('删除成功')
|
||
})
|
||
}
|
||
|
||
// 更新表单文件列表值
|
||
const updateFormFileListValue = () => {
|
||
if (!editableFormFileList.value || !Array.isArray(editableFormFileList.value)) {
|
||
return
|
||
}
|
||
|
||
const fileIds = editableFormFileList.value.map(file => file.id).filter(id => id)
|
||
settingForm.value = fileIds.join(',')
|
||
}
|
||
|
||
// 文件预览
|
||
const previewFile = 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) => {
|
||
window.open(url, '_blank')
|
||
}
|
||
|
||
// 检查是否为图片文件
|
||
const isImageFile = (url) => {
|
||
if (!url) return false
|
||
const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg']
|
||
// 使用相同的URL处理逻辑
|
||
const processedUrl = processImageUrl(url)
|
||
const extension = processedUrl.toLowerCase().substring(processedUrl.lastIndexOf('.'))
|
||
return imageExtensions.includes(extension)
|
||
}
|
||
|
||
// 处理图片URL,确保正确显示
|
||
const processImageUrl = (url) => {
|
||
if (!url) return ''
|
||
// 先处理转义字符:将 \u0026 替换为 &
|
||
let processedUrl = url.replace(/\\u0026/g, '&')
|
||
// 再进行URL解码
|
||
return decodeURIComponent(processedUrl)
|
||
}
|
||
|
||
// 图片预览
|
||
const previewImage = (url) => {
|
||
// 使用图片查看器对话框
|
||
currentViewImage.value = processImageUrl(url)
|
||
imageViewerVisible.value = true
|
||
}
|
||
|
||
// 处理图片加载失败
|
||
const handleImageError = (event) => {
|
||
// 图片加载失败时,可以隐藏图片或显示占位符
|
||
console.error('图片加载失败:', event.target.src)
|
||
// 这里可以添加更多的错误处理逻辑
|
||
}
|
||
|
||
// ==================== 图像选择器相关 ====================
|
||
|
||
// 打开图像选择器(单个文件)
|
||
const openImageSelector = () => {
|
||
imageSelectorMode.value = 'single'
|
||
currentImageSelectorFileId.value = settingForm.value || ''
|
||
imageSelectorVisible.value = true
|
||
}
|
||
|
||
// 打开图像选择器(文件列表)
|
||
const openImageSelectorForList = () => {
|
||
imageSelectorMode.value = 'list'
|
||
currentImageSelectorFileId.value = ''
|
||
imageSelectorVisible.value = true
|
||
}
|
||
|
||
// 打开图像选择器(文件列表中的特定项)
|
||
const openImageSelectorForListItem = (index) => {
|
||
imageSelectorMode.value = 'list-item'
|
||
currentImageSelectorFileId.value = index
|
||
imageSelectorVisible.value = true
|
||
}
|
||
|
||
// 处理图像选择器确认
|
||
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
|
||
}
|
||
|
||
if (imageSelectorMode.value === 'single') {
|
||
// 单个文件模式
|
||
settingForm.value = selectedFile.id
|
||
fileInfo.value = {
|
||
id: selectedFile.id,
|
||
url: processImageUrl(selectedFile.url || ''),
|
||
realName: selectedFile.realName || '文件',
|
||
saveName: selectedFile.realName || 'file',
|
||
size: selectedFile.size || 0
|
||
}
|
||
} else if (imageSelectorMode.value === 'list') {
|
||
// 单选兼容:文件列表模式(单个文件对象)
|
||
if (!fileListInfo.value) {
|
||
fileListInfo.value = []
|
||
}
|
||
|
||
const newFile = {
|
||
id: selectedFile.id,
|
||
url: processImageUrl(selectedFile.url || ''),
|
||
realName: selectedFile.realName || '文件',
|
||
saveName: selectedFile.realName || 'file',
|
||
size: selectedFile.size || 0
|
||
}
|
||
fileListInfo.value.push(newFile)
|
||
|
||
// 同步更新 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
|
||
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
|
||
}
|
||
|
||
const getFileList = (value) => {
|
||
if (!value || typeof value !== 'string') return []
|
||
return value.split(',').filter(id => id && id.trim())
|
||
}
|
||
|
||
const getStringList = (value) => {
|
||
if (!value || typeof value !== 'string') return []
|
||
return value.split(',').filter(str => str && str.trim())
|
||
}
|
||
|
||
const truncateText = (text, maxLength = 30) => {
|
||
if (!text) return ''
|
||
if (text.length <= maxLength) return text
|
||
return text.substring(0, maxLength) + '...'
|
||
}
|
||
|
||
const handleFileListChange = async (file) => {
|
||
fileUploading.value = true
|
||
|
||
// 创建本地预览URL
|
||
const localUrl = URL.createObjectURL(file.raw)
|
||
const isImage = file.raw.type.startsWith('image/')
|
||
|
||
// 立即添加本地预览到列表
|
||
if (!fileListInfo.value) {
|
||
fileListInfo.value = []
|
||
}
|
||
|
||
const tempFile = {
|
||
id: '', // 暂时为空,上传成功后会更新
|
||
url: isImage ? localUrl : '',
|
||
realName: file.name,
|
||
saveName: file.name,
|
||
size: file.size,
|
||
isLocal: true // 标记为本地文件
|
||
}
|
||
|
||
fileListInfo.value.push(tempFile)
|
||
|
||
try {
|
||
const formData = new FormData()
|
||
formData.append('file_names', file.name)
|
||
formData.append('files', file.raw)
|
||
formData.append('update_type','cover')
|
||
formData.append('open_down','true')
|
||
|
||
const res = await uploadFile(formData)
|
||
if (res.data.code === 200 && res.data.data && res.data.data.length > 0) {
|
||
const uploadedFile = res.data.data[0]
|
||
const currentFileIds = getFileList(settingForm.value)
|
||
currentFileIds.push(String(uploadedFile.id || ''))
|
||
settingForm.value = currentFileIds.join(',')
|
||
|
||
// 找到对应的本地文件并更新
|
||
const index = fileListInfo.value.findIndex(f => f.isLocal && f.realName === file.name)
|
||
if (index !== -1) {
|
||
// 释放本地URL(暂时不释放,保留用于渲染)
|
||
// if (fileListInfo.value[index].isLocal) {
|
||
// URL.revokeObjectURL(fileListInfo.value[index].url)
|
||
// }
|
||
|
||
// 更新为服务器返回的文件信息,但保留本地URL用于渲染
|
||
fileListInfo.value[index] = {
|
||
id: uploadedFile.id || '',
|
||
url: processImageUrl(uploadedFile.url || uploadedFile.realName || ''),
|
||
localUrl: fileListInfo.value[index]?.url || '', // 保留本地URL用于渲染
|
||
realName: uploadedFile.realName || '文件',
|
||
saveName: uploadedFile.saveName || 'file',
|
||
size: uploadedFile.size || 0,
|
||
isLocal: false // 标记为已上传,但保留本地渲染
|
||
}
|
||
}
|
||
|
||
updateFileListValue()
|
||
ElMessage.success('文件上传成功')
|
||
} else {
|
||
// 上传失败,不清理本地预览,让用户可以重新上传或删除
|
||
ElMessage.error(res.data.message || '文件上传失败')
|
||
}
|
||
} catch (error) {
|
||
console.error('文件上传失败:', error)
|
||
// 上传失败,不清理本地预览,让用户可以重新上传或删除
|
||
ElMessage.error('文件上传失败')
|
||
} finally {
|
||
fileUploading.value = false
|
||
}
|
||
}
|
||
|
||
const removeFile = (index) => {
|
||
if (!fileListInfo.value || index < 0 || index >= fileListInfo.value.length) {
|
||
return
|
||
}
|
||
|
||
// 释放本地URL
|
||
if (fileListInfo.value[index].localUrl) {
|
||
URL.revokeObjectURL(fileListInfo.value[index].localUrl)
|
||
}
|
||
if (fileListInfo.value[index].isLocal && fileListInfo.value[index].url) {
|
||
URL.revokeObjectURL(fileListInfo.value[index].url)
|
||
}
|
||
|
||
const currentFileIds = getFileList(settingForm.value) || []
|
||
currentFileIds.splice(index, 1)
|
||
settingForm.value = currentFileIds.join(',')
|
||
fileListInfo.value.splice(index, 1)
|
||
}
|
||
|
||
// 更新文件列表值
|
||
const updateFileListValue = () => {
|
||
if (!fileListInfo.value || !Array.isArray(fileListInfo.value)) {
|
||
return
|
||
}
|
||
|
||
const fileIds = fileListInfo.value.map(file => file.id).filter(id => id)
|
||
settingForm.value = fileIds.join(',')
|
||
}
|
||
|
||
const addStringItem = () => {
|
||
if (newStringItem.value.trim()) {
|
||
const currentItems = getStringList(settingForm.value)
|
||
currentItems.push(newStringItem.value.trim())
|
||
settingForm.value = currentItems.join(',')
|
||
newStringItem.value = ''
|
||
}
|
||
}
|
||
|
||
const removeStringItem = (index) => {
|
||
const currentItems = getStringList(settingForm.value)
|
||
currentItems.splice(index, 1)
|
||
settingForm.value = currentItems.join(',')
|
||
}
|
||
|
||
// 提交配置表单
|
||
const submitSettingForm = async () => {
|
||
if (!settingFormRef.value) return
|
||
|
||
try {
|
||
await settingFormRef.value.validate()
|
||
|
||
const submitData = {
|
||
id: settingForm.id,
|
||
name: settingForm.name,
|
||
value: settingForm.value,
|
||
type: settingForm.type,
|
||
setting_group_id: settingForm.settingGroupID,
|
||
open: settingForm.open,
|
||
note: settingForm.note
|
||
}
|
||
|
||
// 保存原始的开放状态用于比较
|
||
const originalOpen = selectedNode.value?.data?.open
|
||
const newOpen = settingForm.open
|
||
|
||
const res = settingForm.id
|
||
? await updateSetting(submitData)
|
||
: await createSetting(submitData)
|
||
|
||
if (res.data.code === 200) {
|
||
// 如果是修改配置且开放状态有变化,调用setSettingOpen
|
||
if (settingForm.id && originalOpen !== newOpen) {
|
||
try {
|
||
await setSettingOpen({
|
||
id: settingForm.id,
|
||
open: newOpen
|
||
})
|
||
console.log('配置开放状态已更新:', { id: settingForm.id, open: newOpen })
|
||
} catch (openError) {
|
||
console.error('更新开放状态失败:', openError)
|
||
ElMessage.warning('配置已更新,但开放状态更新失败')
|
||
}
|
||
}
|
||
|
||
ElMessage.success(settingForm.id ? '修改成功' : '创建成功')
|
||
settingDialogVisible.value = false
|
||
|
||
// 重新加载数据
|
||
if (settingForm.settingGroupID) {
|
||
// 找到对应的配置组并重新加载其配置项
|
||
const groupNode = treeData.value.find(item =>
|
||
item.type === 'group' && item.data.id === settingForm.settingGroupID
|
||
)
|
||
if (groupNode) {
|
||
groupNode._children = []
|
||
groupNode._expanded = false
|
||
await toggleExpand(groupNode)
|
||
}
|
||
} else {
|
||
// 重新加载所有数据
|
||
loadGroups()
|
||
}
|
||
} else {
|
||
ElMessage.error(res.data.message || '操作失败')
|
||
}
|
||
} catch (error) {
|
||
console.error('提交配置失败:', error)
|
||
ElMessage.error('操作失败')
|
||
}
|
||
}
|
||
|
||
// ==================== 一键导入配置 ====================
|
||
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: 10 })
|
||
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(() => {
|
||
// 初始化时加载配置组数据
|
||
loadGroups()
|
||
})
|
||
</script>
|
||
|
||
<style scoped>
|
||
.setting-manage-container {
|
||
padding: 0;
|
||
}
|
||
|
||
.main-container {
|
||
border: 1px solid #e1e8ed;
|
||
background: #ffffff;
|
||
}
|
||
|
||
/* 树状图样式 */
|
||
.tree-section {
|
||
padding: 20px;
|
||
min-height: 400px;
|
||
max-height: 500px;
|
||
overflow-y: auto;
|
||
/* 隐藏滚动条 */
|
||
scrollbar-width: none; /* Firefox */
|
||
-ms-overflow-style: none; /* IE and Edge */
|
||
}
|
||
|
||
.tree-section::-webkit-scrollbar {
|
||
display: none; /* Chrome, Safari, Opera */
|
||
}
|
||
|
||
.setting-tree-table {
|
||
background: white;
|
||
}
|
||
|
||
/* 树状层级样式 */
|
||
.tree-item-content {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 8px 0;
|
||
}
|
||
|
||
.expand-icon {
|
||
width: 20px;
|
||
height: 20px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
cursor: pointer;
|
||
color: #606266;
|
||
transition: transform 0.2s ease;
|
||
}
|
||
|
||
.expand-icon:hover {
|
||
color: #409eff;
|
||
}
|
||
|
||
.expand-icon .is-expanded {
|
||
transform: rotate(90deg);
|
||
}
|
||
|
||
.expand-placeholder {
|
||
width: 20px;
|
||
height: 20px;
|
||
}
|
||
|
||
.tree-node {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.tree-icon {
|
||
color: #409eff;
|
||
font-size: 16px;
|
||
flex-shrink: 0;
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
.tree-label {
|
||
font-weight: 500;
|
||
color: #303133;
|
||
font-size: 14px;
|
||
flex: 1;
|
||
line-height: 16px; /* 与图标高度保持一致 */
|
||
}
|
||
|
||
/* 表格中文字值样式 */
|
||
.text-value {
|
||
display: inline-block;
|
||
max-width: 200px;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
vertical-align: middle;
|
||
}
|
||
|
||
/* 文件预览样式 */
|
||
.file-preview {
|
||
margin-top: 8px;
|
||
position: relative;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.preview-image {
|
||
width: 80px;
|
||
height: 80px;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
transition: transform 0.2s ease;
|
||
border: 1px solid #e1e8ed;
|
||
object-fit: cover;
|
||
}
|
||
|
||
.preview-image:hover {
|
||
transform: scale(1.1);
|
||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||
}
|
||
|
||
.file-preview-link {
|
||
margin-top: 8px;
|
||
}
|
||
|
||
.file-item {
|
||
border: 1px solid #e1e8ed;
|
||
border-radius: 4px;
|
||
padding: 12px;
|
||
margin-bottom: 8px;
|
||
background: #fafbfc;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
}
|
||
|
||
.file-placeholder {
|
||
width: 80px;
|
||
height: 80px;
|
||
border-radius: 4px;
|
||
border: 1px solid #e1e8ed;
|
||
background: #f5f7fa;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: #909399;
|
||
font-size: 24px;
|
||
}
|
||
|
||
.file-details {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
flex: 1;
|
||
}
|
||
|
||
.file-actions {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
align-items: flex-end;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.file-name {
|
||
font-weight: 500;
|
||
color: #303133;
|
||
}
|
||
|
||
.file-id {
|
||
font-size: 12px;
|
||
color: #909399;
|
||
}
|
||
|
||
.file-size {
|
||
font-size: 12px;
|
||
color: #909399;
|
||
}
|
||
|
||
/* 表格行样式 */
|
||
:deep(.setting-tree-table .el-table__row--level-1) {
|
||
background-color: #fafbfc;
|
||
}
|
||
|
||
:deep(.setting-tree-table .el-table__row--level-2) {
|
||
background-color: #f5f7fa;
|
||
}
|
||
|
||
/* 表格样式优化 */
|
||
:deep(.setting-tree-table .el-table) {
|
||
border: none;
|
||
color: #2c3e50;
|
||
}
|
||
|
||
:deep(.setting-tree-table .el-table__header) {
|
||
background: #f8f9fa;
|
||
}
|
||
|
||
:deep(.setting-tree-table .el-table th) {
|
||
background: #f8f9fa !important;
|
||
border-bottom: 2px solid #e1e8ed;
|
||
color: #2c3e50;
|
||
font-weight: 600;
|
||
font-size: 13px;
|
||
}
|
||
|
||
:deep(.setting-tree-table .el-table td) {
|
||
border-bottom: 1px solid #f0f2f5;
|
||
color: #34495e;
|
||
}
|
||
|
||
:deep(.setting-tree-table .el-table tr:hover > td) {
|
||
background-color: #f8f9fa !important;
|
||
}
|
||
|
||
/* 详细信息面板样式 */
|
||
.detail-section {
|
||
padding: 20px;
|
||
background: #fafbfc;
|
||
border-top: 1px solid #e1e8ed;
|
||
}
|
||
|
||
.detail-content {
|
||
background: white;
|
||
border-radius: 8px;
|
||
padding: 20px;
|
||
}
|
||
|
||
.filter-section {
|
||
padding: 0;
|
||
border-bottom: 1px solid #e1e8ed;
|
||
background: #fafbfc;
|
||
margin: 0 -20px;
|
||
}
|
||
|
||
.filter-content {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 16px 20px;
|
||
gap: 20px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.search-form {
|
||
margin: 0;
|
||
flex: 1;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.search-form :deep(.el-form-item) {
|
||
margin-bottom: 0;
|
||
margin-right: 12px;
|
||
}
|
||
|
||
.action-bar {
|
||
display: flex;
|
||
gap: 12px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.table-section {
|
||
padding: 20px 0;
|
||
}
|
||
|
||
.pagination {
|
||
margin-top: 20px;
|
||
padding: 16px 0;
|
||
border-top: 1px solid #e1e8ed;
|
||
justify-content: flex-end;
|
||
}
|
||
|
||
/* 表格样式优化 */
|
||
:deep(.el-table) {
|
||
border: none;
|
||
color: #2c3e50;
|
||
}
|
||
|
||
:deep(.el-table__header) {
|
||
background: #f8f9fa;
|
||
}
|
||
|
||
:deep(.el-table th) {
|
||
background: #f8f9fa !important;
|
||
border-bottom: 2px solid #e1e8ed;
|
||
color: #2c3e50;
|
||
font-weight: 600;
|
||
font-size: 13px;
|
||
}
|
||
|
||
:deep(.el-table td) {
|
||
border-bottom: 1px solid #f0f2f5;
|
||
color: #34495e;
|
||
}
|
||
|
||
:deep(.el-table tr:hover > td) {
|
||
background-color: #f8f9fa !important;
|
||
}
|
||
|
||
:deep(.dialog-scrollable .el-dialog) {
|
||
max-height: 90vh;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
:deep(.dialog-scrollable .el-dialog__body) {
|
||
max-height: calc(90vh - 120px);
|
||
overflow-y: auto;
|
||
padding: 20px;
|
||
scrollbar-width: none; /* Firefox */
|
||
-ms-overflow-style: none; /* IE and Edge */
|
||
}
|
||
|
||
/* 完全隐藏滚动条但保持滚动功能 */
|
||
:deep(.dialog-scrollable .el-dialog__body)::-webkit-scrollbar {
|
||
display: none; /* Chrome, Safari, Opera */
|
||
}
|
||
|
||
/* 确保弹窗内容区域正确布局 */
|
||
:deep(.dialog-scrollable .el-dialog__header) {
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
:deep(.dialog-scrollable .el-dialog__footer) {
|
||
flex-shrink: 0;
|
||
padding: 20px;
|
||
border-top: 1px solid #e4e7ed;
|
||
}
|
||
|
||
/* 移动端适配 */
|
||
@media (max-width: 768px) {
|
||
:deep(.dialog-scrollable .el-dialog) {
|
||
max-height: 95vh;
|
||
width: 95vw !important;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
:deep(.dialog-scrollable .el-dialog__body) {
|
||
max-height: calc(95vh - 140px);
|
||
padding: 15px;
|
||
}
|
||
|
||
:deep(.dialog-scrollable .el-dialog__footer) {
|
||
padding: 15px;
|
||
}
|
||
|
||
.tree-section {
|
||
max-height: 300px;
|
||
min-height: 300px;
|
||
padding: 10px;
|
||
}
|
||
|
||
.tree-item-content {
|
||
padding: 4px 0;
|
||
}
|
||
|
||
.tree-label {
|
||
font-size: 13px;
|
||
line-height: 14px; /* 移动端调整行高 */
|
||
}
|
||
|
||
.tree-icon {
|
||
font-size: 14px;
|
||
}
|
||
|
||
:deep(.setting-tree-table .el-table th) {
|
||
font-size: 12px;
|
||
padding: 8px 4px;
|
||
}
|
||
|
||
:deep(.setting-tree-table .el-table td) {
|
||
font-size: 12px;
|
||
padding: 8px 4px;
|
||
}
|
||
}
|
||
|
||
/* 确保表单在弹窗中正确显示 */
|
||
:deep(.dialog-scrollable .el-form) {
|
||
margin: 0;
|
||
}
|
||
|
||
:deep(.dialog-scrollable .el-form-item) {
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
/* 文件上传组件在弹窗中的样式优化 */
|
||
:deep(.dialog-scrollable .file-uploader) {
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
:deep(.dialog-scrollable .file-info-display),
|
||
:deep(.dialog-scrollable .file-list-info-display) {
|
||
margin-bottom: 15px;
|
||
}
|
||
|
||
:deep(.el-tabs__header) {
|
||
margin: 0;
|
||
padding: 0 0 0 0;
|
||
border-bottom: 1px solid #e1e8ed;
|
||
}
|
||
|
||
:deep(.el-tabs__nav-wrap::after) {
|
||
display: none;
|
||
}
|
||
|
||
:deep(.el-tabs__item) {
|
||
padding: 0 20px;
|
||
height: 50px;
|
||
line-height: 50px;
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
}
|
||
|
||
:deep(.el-tabs__item.is-active) {
|
||
color: #2c3e50;
|
||
font-weight: 600;
|
||
}
|
||
|
||
:deep(.el-tabs__active-bar) {
|
||
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%;
|
||
}
|
||
|
||
.file-uploader {
|
||
width: 100%;
|
||
}
|
||
|
||
.file-upload-options {
|
||
width: 100%;
|
||
}
|
||
|
||
.upload-methods {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 16px;
|
||
align-items: center;
|
||
}
|
||
|
||
.divider {
|
||
position: relative;
|
||
text-align: center;
|
||
color: #909399;
|
||
font-size: 14px;
|
||
margin: 8px 0;
|
||
}
|
||
|
||
.divider::before {
|
||
content: '';
|
||
position: absolute;
|
||
top: 50%;
|
||
left: 0;
|
||
right: 0;
|
||
height: 1px;
|
||
background: #dcdfe6;
|
||
}
|
||
|
||
.divider {
|
||
background: #fff;
|
||
padding: 0 16px;
|
||
}
|
||
|
||
.image-selector-btn {
|
||
width: 200px;
|
||
height: 40px;
|
||
}
|
||
|
||
:deep(.file-uploader .el-upload) {
|
||
width: 100%;
|
||
}
|
||
|
||
:deep(.file-uploader .el-upload-dragger) {
|
||
width: 100%;
|
||
border: 1px dashed #d9d9d9;
|
||
border-radius: 6px;
|
||
cursor: pointer;
|
||
position: relative;
|
||
overflow: hidden;
|
||
transition: border-color 0.3s;
|
||
}
|
||
|
||
:deep(.file-uploader .el-upload-dragger:hover) {
|
||
border-color: #409eff;
|
||
}
|
||
|
||
.file-info-display {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 12px;
|
||
border: 1px solid #e4e7ed;
|
||
border-radius: 6px;
|
||
background-color: #f5f7fa;
|
||
}
|
||
|
||
.file-details {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
}
|
||
|
||
.file-name {
|
||
font-weight: 500;
|
||
color: #303133;
|
||
max-width: 200px;
|
||
overflow: hidden;
|
||
white-space: nowrap;
|
||
text-overflow: ellipsis;
|
||
display: inline-block;
|
||
}
|
||
|
||
.file-id {
|
||
font-size: 12px;
|
||
color: #909399;
|
||
}
|
||
|
||
.file-size {
|
||
font-size: 12px;
|
||
color: #909399;
|
||
}
|
||
|
||
/* 文件列表相关样式 */
|
||
.file-list-section {
|
||
width: 100%;
|
||
}
|
||
|
||
.file-list-info-display {
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.file-list-details {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
}
|
||
|
||
.file-item {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 12px;
|
||
border: 1px solid #e4e7ed;
|
||
border-radius: 6px;
|
||
background-color: #f5f7fa;
|
||
}
|
||
|
||
.file-item .file-details {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
flex: 1;
|
||
}
|
||
|
||
.file-item .file-name {
|
||
font-weight: 500;
|
||
color: #303133;
|
||
max-width: 180px;
|
||
overflow: hidden;
|
||
white-space: nowrap;
|
||
text-overflow: ellipsis;
|
||
display: inline-block;
|
||
cursor: pointer;
|
||
transition: color 0.3s;
|
||
}
|
||
|
||
.file-item .file-name:hover {
|
||
color: #409eff;
|
||
}
|
||
|
||
/* 上传状态指示器样式 */
|
||
.file-item.uploading {
|
||
border-color: #409eff;
|
||
background-color: #f0f9ff;
|
||
}
|
||
|
||
.upload-overlay {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background-color: rgba(0, 0, 0, 0.6);
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: white;
|
||
font-size: 12px;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.upload-overlay .el-icon {
|
||
font-size: 20px;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.upload-overlay span {
|
||
font-size: 11px;
|
||
}
|
||
|
||
/* 字符串列表相关样式 */
|
||
.string-list-section {
|
||
width: 100%;
|
||
}
|
||
|
||
.string-list-display {
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.string-list-items {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.string-item {
|
||
margin: 0;
|
||
}
|
||
|
||
.string-list-input {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
}
|
||
|
||
/* 文件列表摘要样式 */
|
||
.file-list-summary {
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
.file-list-summary .el-tag {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
cursor: pointer;
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.file-list-summary .el-tag:hover {
|
||
transform: translateY(-1px);
|
||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
/* 字符串列表摘要样式 */
|
||
.string-list-summary {
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
.string-list-summary .el-tag {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
cursor: pointer;
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.string-list-summary .el-tag:hover {
|
||
transform: translateY(-1px);
|
||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
/* 表格中的文件列表和字符串列表样式 */
|
||
.table-file-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
max-height: 120px;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.table-file-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 4px 6px;
|
||
background-color: #f8f9fa;
|
||
border-radius: 4px;
|
||
border: 1px solid #e9ecef;
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.table-file-item:hover {
|
||
background-color: #e9ecef;
|
||
border-color: #dee2e6;
|
||
}
|
||
|
||
.file-preview-mini {
|
||
width: 24px;
|
||
height: 24px;
|
||
border-radius: 3px;
|
||
overflow: hidden;
|
||
flex-shrink: 0;
|
||
border: 1px solid #dee2e6;
|
||
}
|
||
|
||
.preview-mini-image {
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: cover;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.file-placeholder-mini {
|
||
width: 100%;
|
||
height: 100%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background-color: #f8f9fa;
|
||
color: #6c757d;
|
||
font-size: 10px;
|
||
}
|
||
|
||
.file-info-mini {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
|
||
.file-name-mini {
|
||
font-size: 12px;
|
||
font-weight: 500;
|
||
color: #495057;
|
||
margin-bottom: 2px;
|
||
overflow: hidden;
|
||
white-space: nowrap;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
.file-id-mini {
|
||
font-size: 10px;
|
||
color: #6c757d;
|
||
overflow: hidden;
|
||
white-space: nowrap;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
.table-string-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
max-height: 120px;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.table-string-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
padding: 3px 6px;
|
||
background-color: #f8f9fa;
|
||
border-radius: 3px;
|
||
border-left: 3px solid #007bff;
|
||
transition: all 0.2s ease;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.table-string-item:hover {
|
||
background-color: #e9ecef;
|
||
border-left-color: #0056b3;
|
||
}
|
||
|
||
.string-index {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 18px;
|
||
height: 18px;
|
||
background-color: #007bff;
|
||
color: white;
|
||
font-size: 10px;
|
||
font-weight: bold;
|
||
border-radius: 50%;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.string-content {
|
||
font-size: 12px;
|
||
color: #495057;
|
||
overflow: hidden;
|
||
white-space: nowrap;
|
||
text-overflow: ellipsis;
|
||
flex: 1;
|
||
}
|
||
|
||
.file-list-display {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
}
|
||
|
||
.file-link {
|
||
font-size: 12px;
|
||
margin-right: 8px;
|
||
max-width: 150px;
|
||
overflow: hidden;
|
||
white-space: nowrap;
|
||
text-overflow: ellipsis;
|
||
display: inline-block;
|
||
vertical-align: middle;
|
||
}
|
||
|
||
.string-list-display {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 4px;
|
||
}
|
||
|
||
.string-tag {
|
||
margin: 0;
|
||
max-width: 120px;
|
||
overflow: hidden;
|
||
white-space: nowrap;
|
||
text-overflow: ellipsis;
|
||
display: inline-block;
|
||
}
|
||
|
||
/* 可编辑字符串列表样式 */
|
||
.editable-string-list {
|
||
border: 1px solid #e4e7ed;
|
||
border-radius: 4px;
|
||
padding: 16px;
|
||
background-color: #fafafa;
|
||
}
|
||
|
||
.string-list-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 12px;
|
||
padding-bottom: 8px;
|
||
border-bottom: 1px solid #ebeef5;
|
||
}
|
||
|
||
.list-title {
|
||
font-weight: 500;
|
||
color: #303133;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.string-list-items {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
}
|
||
|
||
.string-list-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
padding: 8px 12px;
|
||
background-color: #ffffff;
|
||
border: 1px solid #e4e7ed;
|
||
border-radius: 4px;
|
||
transition: all 0.3s ease;
|
||
cursor: move;
|
||
}
|
||
|
||
.string-list-item:hover {
|
||
border-color: #c0c4cc;
|
||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.drag-handle {
|
||
color: #909399;
|
||
cursor: grab;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.drag-handle:hover {
|
||
color: #606266;
|
||
}
|
||
|
||
.item-content {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
|
||
.item-text {
|
||
padding: 4px 8px;
|
||
border-radius: 4px;
|
||
transition: background-color 0.3s ease;
|
||
cursor: pointer;
|
||
word-break: break-word;
|
||
white-space: normal;
|
||
line-height: 1.4;
|
||
}
|
||
|
||
.item-text:hover {
|
||
background-color: #f5f7fa;
|
||
}
|
||
|
||
.item-actions {
|
||
display: flex;
|
||
gap: 4px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.danger-btn {
|
||
color: #f56c6c;
|
||
}
|
||
|
||
.danger-btn:hover {
|
||
color: #f78989;
|
||
}
|
||
|
||
/* 可编辑文件列表样式 */
|
||
.editable-file-list {
|
||
border: 1px solid #e4e7ed;
|
||
border-radius: 4px;
|
||
padding: 16px;
|
||
background-color: #fafafa;
|
||
}
|
||
|
||
.file-list-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 12px;
|
||
padding-bottom: 8px;
|
||
border-bottom: 1px solid #ebeef5;
|
||
}
|
||
|
||
.header-actions {
|
||
display: flex;
|
||
gap: 8px;
|
||
}
|
||
|
||
.file-list-items {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
}
|
||
|
||
.file-list-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
padding: 12px;
|
||
background-color: #ffffff;
|
||
border: 1px solid #e4e7ed;
|
||
border-radius: 4px;
|
||
transition: all 0.3s ease;
|
||
cursor: move;
|
||
}
|
||
|
||
.file-list-item:hover {
|
||
border-color: #c0c4cc;
|
||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.file-list-item .file-preview {
|
||
width: 60px;
|
||
height: 60px;
|
||
border-radius: 4px;
|
||
overflow: hidden;
|
||
border: 1px solid #e4e7ed;
|
||
flex-shrink: 0;
|
||
position: relative;
|
||
}
|
||
|
||
.file-list-item .preview-image {
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: cover;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.file-list-item .file-placeholder {
|
||
width: 100%;
|
||
height: 100%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background-color: #f5f7fa;
|
||
color: #909399;
|
||
}
|
||
|
||
.file-list-item .file-info {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
|
||
.file-list-item .file-name {
|
||
font-weight: 500;
|
||
color: #303133;
|
||
margin-bottom: 4px;
|
||
overflow: hidden;
|
||
white-space: nowrap;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
.file-list-item .file-id {
|
||
font-size: 12px;
|
||
color: #909399;
|
||
margin-bottom: 2px;
|
||
}
|
||
|
||
.file-list-item .file-size {
|
||
font-size: 12px;
|
||
color: #909399;
|
||
}
|
||
|
||
.file-list-item .file-actions {
|
||
display: flex;
|
||
gap: 4px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
/* 弹窗中值内容的省略号样式 */
|
||
.value-content {
|
||
max-width: 100%;
|
||
}
|
||
|
||
.value-content .text-value {
|
||
display: block;
|
||
max-width: 100%;
|
||
overflow: hidden;
|
||
white-space: nowrap;
|
||
text-overflow: ellipsis;
|
||
word-break: break-all;
|
||
}
|
||
|
||
/* 响应式样式 */
|
||
@media (max-width: 768px) {
|
||
.table-file-list {
|
||
max-height: 80px;
|
||
}
|
||
|
||
.table-string-list {
|
||
max-height: 80px;
|
||
}
|
||
|
||
.file-preview-mini {
|
||
width: 20px;
|
||
height: 20px;
|
||
}
|
||
|
||
.file-name-mini {
|
||
font-size: 11px;
|
||
}
|
||
|
||
.file-id-mini {
|
||
font-size: 9px;
|
||
}
|
||
|
||
.string-index {
|
||
width: 16px;
|
||
height: 16px;
|
||
font-size: 9px;
|
||
}
|
||
|
||
.string-content {
|
||
font-size: 11px;
|
||
}
|
||
|
||
.file-link {
|
||
max-width: 100px !important;
|
||
}
|
||
|
||
.string-tag {
|
||
max-width: 80px !important;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 1200px) {
|
||
.table-file-list {
|
||
max-height: 100px;
|
||
}
|
||
|
||
.table-string-list {
|
||
max-height: 100px;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.file-name {
|
||
max-width: 120px !important;
|
||
}
|
||
|
||
.file-item .file-name {
|
||
max-width: 100px !important;
|
||
}
|
||
|
||
.file-link {
|
||
max-width: 100px !important;
|
||
}
|
||
|
||
.string-tag {
|
||
max-width: 80px !important;
|
||
}
|
||
}
|
||
</style>
|