mirror of
https://gitcode.com/flipped-aurora/gin-vue-admin.git
synced 2025-12-30 03:42:26 +00:00
feat: 增加字典的json导出和导入
This commit is contained in:
@@ -171,17 +171,17 @@ func (s *DictionaryApi) ExportSysDictionary(c *gin.Context) {
|
||||
// @Security ApiKeyAuth
|
||||
// @accept application/json
|
||||
// @Produce application/json
|
||||
// @Param data body map[string]interface{} true "字典JSON数据"
|
||||
// @Param data body request.ImportSysDictionaryRequest true "字典JSON数据"
|
||||
// @Success 200 {object} response.Response{msg=string} "导入字典"
|
||||
// @Router /sysDictionary/importSysDictionary [post]
|
||||
func (s *DictionaryApi) ImportSysDictionary(c *gin.Context) {
|
||||
var importData map[string]interface{}
|
||||
err := c.ShouldBindJSON(&importData)
|
||||
var req request.ImportSysDictionaryRequest
|
||||
err := c.ShouldBindJSON(&req)
|
||||
if err != nil {
|
||||
response.FailWithMessage(err.Error(), c)
|
||||
return
|
||||
}
|
||||
err = dictionaryService.ImportSysDictionary(importData)
|
||||
err = dictionaryService.ImportSysDictionary(req.Json)
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("导入失败!", zap.Error(err))
|
||||
response.FailWithMessage("导入失败: "+err.Error(), c)
|
||||
|
||||
@@ -3,3 +3,7 @@ package request
|
||||
type SysDictionarySearch struct {
|
||||
Name string `json:"name" form:"name" gorm:"column:name;comment:字典名(中)"` // 字典名(中)
|
||||
}
|
||||
|
||||
type ImportSysDictionaryRequest struct {
|
||||
Json string `json:"json" binding:"required"` // JSON字符串
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/model/system/request"
|
||||
@@ -153,7 +154,7 @@ func (dictionaryService *DictionaryService) checkCircularReference(currentID uin
|
||||
return nil
|
||||
}
|
||||
|
||||
//@author: [yourname]
|
||||
//@author: [pixelMax]
|
||||
//@function: ExportSysDictionary
|
||||
//@description: 导出字典JSON(包含字典详情)
|
||||
//@param: id uint
|
||||
@@ -169,55 +170,65 @@ func (dictionaryService *DictionaryService) ExportSysDictionary(id uint) (export
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 清空字典详情中的ID、创建时间、更新时间等字段
|
||||
var cleanDetails []map[string]interface{}
|
||||
for _, detail := range dictionary.SysDictionaryDetails {
|
||||
cleanDetail := map[string]interface{}{
|
||||
"label": detail.Label,
|
||||
"value": detail.Value,
|
||||
"extend": detail.Extend,
|
||||
"status": detail.Status,
|
||||
"sort": detail.Sort,
|
||||
"level": detail.Level,
|
||||
"path": detail.Path,
|
||||
}
|
||||
cleanDetails = append(cleanDetails, cleanDetail)
|
||||
}
|
||||
|
||||
// 构造导出数据
|
||||
exportData = map[string]interface{}{
|
||||
"name": dictionary.Name,
|
||||
"type": dictionary.Type,
|
||||
"status": dictionary.Status,
|
||||
"desc": dictionary.Desc,
|
||||
"details": dictionary.SysDictionaryDetails,
|
||||
"name": dictionary.Name,
|
||||
"type": dictionary.Type,
|
||||
"status": dictionary.Status,
|
||||
"desc": dictionary.Desc,
|
||||
"sysDictionaryDetails": cleanDetails,
|
||||
}
|
||||
|
||||
return exportData, nil
|
||||
}
|
||||
|
||||
//@author: [yourname]
|
||||
//@author: [pixelMax]
|
||||
//@function: ImportSysDictionary
|
||||
//@description: 导入字典JSON(包含字典详情)
|
||||
//@param: importData map[string]interface{}
|
||||
//@param: jsonStr string
|
||||
//@return: err error
|
||||
|
||||
func (dictionaryService *DictionaryService) ImportSysDictionary(importData map[string]interface{}) error {
|
||||
// 解析基本字典信息
|
||||
name, ok := importData["name"].(string)
|
||||
if !ok || name == "" {
|
||||
return errors.New("字典名称不能为空")
|
||||
func (dictionaryService *DictionaryService) ImportSysDictionary(jsonStr string) error {
|
||||
// 直接解析到 SysDictionary 结构体
|
||||
var importData system.SysDictionary
|
||||
if err := json.Unmarshal([]byte(jsonStr), &importData); err != nil {
|
||||
return errors.New("JSON 格式错误: " + err.Error())
|
||||
}
|
||||
|
||||
dictType, ok := importData["type"].(string)
|
||||
if !ok || dictType == "" {
|
||||
// 验证必填字段
|
||||
if importData.Name == "" {
|
||||
return errors.New("字典名称不能为空")
|
||||
}
|
||||
if importData.Type == "" {
|
||||
return errors.New("字典类型不能为空")
|
||||
}
|
||||
|
||||
// 检查字典类型是否已存在
|
||||
if !errors.Is(global.GVA_DB.First(&system.SysDictionary{}, "type = ?", dictType).Error, gorm.ErrRecordNotFound) {
|
||||
if !errors.Is(global.GVA_DB.First(&system.SysDictionary{}, "type = ?", importData.Type).Error, gorm.ErrRecordNotFound) {
|
||||
return errors.New("存在相同的type,不允许导入")
|
||||
}
|
||||
|
||||
// 创建字典
|
||||
// 创建字典(清空导入数据的ID和时间戳)
|
||||
dictionary := system.SysDictionary{
|
||||
Name: name,
|
||||
Type: dictType,
|
||||
}
|
||||
|
||||
// 处理status字段
|
||||
if status, ok := importData["status"].(bool); ok {
|
||||
dictionary.Status = &status
|
||||
}
|
||||
|
||||
// 处理desc字段
|
||||
if desc, ok := importData["desc"].(string); ok {
|
||||
dictionary.Desc = desc
|
||||
Name: importData.Name,
|
||||
Type: importData.Type,
|
||||
Status: importData.Status,
|
||||
Desc: importData.Desc,
|
||||
}
|
||||
|
||||
// 开启事务
|
||||
@@ -228,84 +239,53 @@ func (dictionaryService *DictionaryService) ImportSysDictionary(importData map[s
|
||||
}
|
||||
|
||||
// 处理字典详情
|
||||
if details, ok := importData["details"].([]interface{}); ok && len(details) > 0 {
|
||||
if len(importData.SysDictionaryDetails) > 0 {
|
||||
// 创建一个映射来跟踪旧ID到新ID的对应关系
|
||||
idMap := make(map[uint]uint)
|
||||
|
||||
// 第一遍:创建所有详情记录(不设置parent_id)
|
||||
for _, detail := range details {
|
||||
detailMap, ok := detail.(map[string]interface{})
|
||||
if !ok {
|
||||
// 第一遍:创建所有详情记录
|
||||
for _, detail := range importData.SysDictionaryDetails {
|
||||
// 验证必填字段
|
||||
if detail.Label == "" || detail.Value == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
label, _ := detailMap["label"].(string)
|
||||
value, _ := detailMap["value"].(string)
|
||||
|
||||
if label == "" || value == "" {
|
||||
continue
|
||||
}
|
||||
// 记录旧ID
|
||||
oldID := detail.ID
|
||||
|
||||
// 创建新的详情记录(ID会被GORM自动设置)
|
||||
detailRecord := system.SysDictionaryDetail{
|
||||
Label: label,
|
||||
Value: value,
|
||||
Label: detail.Label,
|
||||
Value: detail.Value,
|
||||
Extend: detail.Extend,
|
||||
Status: detail.Status,
|
||||
Sort: detail.Sort,
|
||||
Level: detail.Level,
|
||||
Path: detail.Path,
|
||||
SysDictionaryID: int(dictionary.ID),
|
||||
}
|
||||
|
||||
// 处理extend字段
|
||||
if extend, ok := detailMap["extend"].(string); ok {
|
||||
detailRecord.Extend = extend
|
||||
}
|
||||
|
||||
// 处理status字段
|
||||
if status, ok := detailMap["status"].(bool); ok {
|
||||
detailRecord.Status = &status
|
||||
}
|
||||
|
||||
// 处理sort字段
|
||||
if sort, ok := detailMap["sort"].(float64); ok {
|
||||
detailRecord.Sort = int(sort)
|
||||
}
|
||||
|
||||
// 创建详情记录
|
||||
if err := tx.Create(&detailRecord).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 记录ID映射(如果有原始ID)
|
||||
if oldID, ok := detailMap["ID"].(float64); ok {
|
||||
idMap[uint(oldID)] = detailRecord.ID
|
||||
// 记录旧ID到新ID的映射
|
||||
if oldID > 0 {
|
||||
idMap[oldID] = detailRecord.ID
|
||||
}
|
||||
}
|
||||
|
||||
// 第二遍:更新parent_id关系
|
||||
for i, detail := range details {
|
||||
detailMap, ok := detail.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
// 如果有parentID,更新它
|
||||
if oldParentID, ok := detailMap["parentID"].(float64); ok && oldParentID > 0 {
|
||||
if newParentID, exists := idMap[uint(oldParentID)]; exists {
|
||||
// 获取新创建的记录ID(按顺序)
|
||||
if oldID, ok := detailMap["ID"].(float64); ok {
|
||||
if newID, exists := idMap[uint(oldID)]; exists {
|
||||
if err := tx.Model(&system.SysDictionaryDetail{}).Where("id = ?", newID).Update("parent_id", newParentID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 如果没有ID,使用索引来查找
|
||||
var allDetails []system.SysDictionaryDetail
|
||||
if err := tx.Where("sys_dictionary_id = ?", dictionary.ID).Order("id").Find(&allDetails).Error; err != nil {
|
||||
for _, detail := range importData.SysDictionaryDetails {
|
||||
if detail.ParentID != nil && *detail.ParentID > 0 && detail.ID > 0 {
|
||||
if newID, exists := idMap[detail.ID]; exists {
|
||||
if newParentID, parentExists := idMap[*detail.ParentID]; parentExists {
|
||||
if err := tx.Model(&system.SysDictionaryDetail{}).
|
||||
Where("id = ?", newID).
|
||||
Update("parent_id", newParentID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if i < len(allDetails) {
|
||||
if err := tx.Model(&system.SysDictionaryDetail{}).Where("id = ?", allDetails[i].ID).Update("parent_id", newParentID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
title="获取字典且缓存方法已在前端utils/dictionary 已经封装完成 不必自己书写 使用方法查看文件内注释"
|
||||
/>
|
||||
<el-splitter class="h-full">
|
||||
<el-splitter-panel size="400px" min="200px" max="800px" collapsible>
|
||||
<el-splitter-panel size="300px" min="200px" max="800px" collapsible>
|
||||
<div
|
||||
class="flex-none bg-white text-slate-700 dark:text-slate-400 dark:bg-slate-900 rounded p-4"
|
||||
>
|
||||
@@ -170,34 +170,70 @@
|
||||
</el-form>
|
||||
</el-drawer>
|
||||
|
||||
<!-- 导入字典对话框 -->
|
||||
<el-dialog
|
||||
v-model="importDialogVisible"
|
||||
title="导入字典JSON"
|
||||
width="70%"
|
||||
:close-on-click-modal="false"
|
||||
<!-- 导入字典抽屉 -->
|
||||
<el-drawer
|
||||
v-model="importDrawerVisible"
|
||||
:size="appStore.drawerSize"
|
||||
:show-close="false"
|
||||
:before-close="closeImportDrawer"
|
||||
>
|
||||
<div class="import-dialog-content">
|
||||
<template #header>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-lg">导入字典JSON</span>
|
||||
<div>
|
||||
<el-button @click="closeImportDrawer"> 取 消 </el-button>
|
||||
<el-button type="primary" @click="handleImport" :loading="importing">
|
||||
确认导入
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="import-drawer-content">
|
||||
<div class="mb-4">
|
||||
<el-alert
|
||||
title="请粘贴或编辑字典JSON数据,支持包含字典详情的完整数据结构"
|
||||
title="请粘贴、编辑或拖拽JSON文件到下方区域"
|
||||
type="info"
|
||||
:closable="false"
|
||||
show-icon
|
||||
/>
|
||||
</div>
|
||||
<div class="json-editor-container">
|
||||
|
||||
<!-- 拖拽上传区域 -->
|
||||
<div
|
||||
class="drag-upload-area"
|
||||
:class="{ 'is-dragging': isDragging }"
|
||||
@drop.prevent="handleDrop"
|
||||
@dragover.prevent="handleDragOver"
|
||||
@dragleave.prevent="handleDragLeave"
|
||||
@click="triggerFileInput"
|
||||
>
|
||||
<el-icon class="upload-icon"><Upload /></el-icon>
|
||||
<div class="upload-text">
|
||||
<p>将 JSON 文件拖到此处,或点击选择文件</p>
|
||||
<p class="upload-hint">也可以在下方文本框直接编辑</p>
|
||||
</div>
|
||||
<input
|
||||
ref="fileInputRef"
|
||||
type="file"
|
||||
accept=".json,application/json"
|
||||
style="display: none"
|
||||
@change="handleFileSelect"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="json-editor-container mt-4">
|
||||
<el-input
|
||||
v-model="importJsonText"
|
||||
type="textarea"
|
||||
:rows="20"
|
||||
:rows="15"
|
||||
placeholder='请输入JSON数据,例如:
|
||||
{
|
||||
"name": "性别",
|
||||
"type": "gender",
|
||||
"status": true,
|
||||
"desc": "性别字典",
|
||||
"details": [
|
||||
"sysDictionaryDetails": [
|
||||
{
|
||||
"label": "男",
|
||||
"value": "1",
|
||||
@@ -215,6 +251,7 @@
|
||||
class="json-textarea"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-4" v-if="jsonPreviewError">
|
||||
<el-alert
|
||||
:title="jsonPreviewError"
|
||||
@@ -223,22 +260,10 @@
|
||||
show-icon
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-4" v-if="jsonPreview && !jsonPreviewError">
|
||||
<el-divider content-position="left">JSON预览</el-divider>
|
||||
<div class="json-preview">
|
||||
<pre>{{ jsonPreviewFormatted }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="closeImportDialog">取 消</el-button>
|
||||
<el-button type="primary" @click="handleImport" :loading="importing">
|
||||
确认导入
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -305,11 +330,13 @@
|
||||
const availableParentDictionaries = ref([])
|
||||
|
||||
// 导入相关
|
||||
const importDialogVisible = ref(false)
|
||||
const importDrawerVisible = ref(false)
|
||||
const importJsonText = ref('')
|
||||
const importing = ref(false)
|
||||
const jsonPreviewError = ref('')
|
||||
const jsonPreview = ref(null)
|
||||
const isDragging = ref(false)
|
||||
const fileInputRef = ref(null)
|
||||
|
||||
// 监听JSON文本变化,实时预览
|
||||
watch(importJsonText, (newVal) => {
|
||||
@@ -333,6 +360,7 @@
|
||||
return JSON.stringify(jsonPreview.value, null, 2)
|
||||
})
|
||||
|
||||
|
||||
// 查询
|
||||
const getTableData = async () => {
|
||||
const res = await getSysDictionaryList({
|
||||
@@ -492,20 +520,85 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 打开导入对话框
|
||||
// 打开导入抽屉
|
||||
const openImportDialog = () => {
|
||||
importDialogVisible.value = true
|
||||
importDrawerVisible.value = true
|
||||
importJsonText.value = ''
|
||||
jsonPreview.value = null
|
||||
jsonPreviewError.value = ''
|
||||
isDragging.value = false
|
||||
}
|
||||
|
||||
// 关闭导入对话框
|
||||
const closeImportDialog = () => {
|
||||
importDialogVisible.value = false
|
||||
// 关闭导入抽屉
|
||||
const closeImportDrawer = () => {
|
||||
importDrawerVisible.value = false
|
||||
importJsonText.value = ''
|
||||
jsonPreview.value = null
|
||||
jsonPreviewError.value = ''
|
||||
isDragging.value = false
|
||||
}
|
||||
|
||||
// 处理拖拽进入
|
||||
const handleDragOver = (e) => {
|
||||
isDragging.value = true
|
||||
}
|
||||
|
||||
// 处理拖拽离开
|
||||
const handleDragLeave = (e) => {
|
||||
isDragging.value = false
|
||||
}
|
||||
// 处理文件拖拽
|
||||
const handleDrop = (e) => {
|
||||
isDragging.value = false
|
||||
const files = e.dataTransfer.files
|
||||
if (files.length === 0) return
|
||||
|
||||
const file = files[0]
|
||||
readJsonFile(file)
|
||||
}
|
||||
|
||||
// 触发文件选择
|
||||
const triggerFileInput = () => {
|
||||
fileInputRef.value?.click()
|
||||
}
|
||||
|
||||
// 处理文件选择
|
||||
const handleFileSelect = (e) => {
|
||||
const files = e.target.files
|
||||
if (files.length === 0) return
|
||||
|
||||
const file = files[0]
|
||||
readJsonFile(file)
|
||||
|
||||
// 清空input,以便可以重复选择同一文件
|
||||
e.target.value = ''
|
||||
}
|
||||
|
||||
// 读取JSON文件
|
||||
const readJsonFile = (file) => {
|
||||
// 检查文件类型
|
||||
if (!file.name.endsWith('.json')) {
|
||||
ElMessage.warning('请上传 JSON 文件')
|
||||
return
|
||||
}
|
||||
|
||||
// 读取文件内容
|
||||
const reader = new FileReader()
|
||||
reader.onload = (event) => {
|
||||
try {
|
||||
const content = event.target.result
|
||||
// 验证是否为有效的 JSON
|
||||
JSON.parse(content)
|
||||
importJsonText.value = content
|
||||
ElMessage.success('文件读取成功')
|
||||
} catch (error) {
|
||||
ElMessage.error('文件内容不是有效的 JSON 格式')
|
||||
}
|
||||
}
|
||||
reader.onerror = () => {
|
||||
ElMessage.error('文件读取失败')
|
||||
}
|
||||
reader.readAsText(file)
|
||||
}
|
||||
|
||||
// 处理导入
|
||||
@@ -522,11 +615,10 @@
|
||||
|
||||
try {
|
||||
importing.value = true
|
||||
const jsonData = JSON.parse(importJsonText.value)
|
||||
const res = await importSysDictionary(jsonData)
|
||||
const res = await importSysDictionary({ json: importJsonText.value })
|
||||
if (res.code === 0) {
|
||||
ElMessage.success('导入成功')
|
||||
closeImportDialog()
|
||||
closeImportDrawer()
|
||||
getTableData()
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -547,9 +639,53 @@
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.import-dialog-content {
|
||||
max-height: 70vh;
|
||||
overflow-y: auto;
|
||||
.import-drawer-content {
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
/* 拖拽上传区域 */
|
||||
.drag-upload-area {
|
||||
border: 2px dashed #dcdfe6;
|
||||
border-radius: 8px;
|
||||
padding: 40px 20px;
|
||||
text-align: center;
|
||||
background-color: #fafafa;
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.drag-upload-area:hover {
|
||||
border-color: #409eff;
|
||||
background-color: #ecf5ff;
|
||||
}
|
||||
|
||||
.drag-upload-area.is-dragging {
|
||||
border-color: #409eff;
|
||||
background-color: #ecf5ff;
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
.upload-icon {
|
||||
font-size: 48px;
|
||||
color: #8c939d;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.drag-upload-area.is-dragging .upload-icon {
|
||||
color: #409eff;
|
||||
}
|
||||
|
||||
.upload-text {
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.upload-text p {
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
.upload-hint {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.json-editor-container {
|
||||
@@ -582,6 +718,17 @@
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.dark .drag-upload-area {
|
||||
background-color: #1d1e1f;
|
||||
border-color: #414243;
|
||||
}
|
||||
|
||||
.dark .drag-upload-area:hover,
|
||||
.dark .drag-upload-area.is-dragging {
|
||||
background-color: #1a3a52;
|
||||
border-color: #409eff;
|
||||
}
|
||||
|
||||
.dark .json-preview {
|
||||
background-color: #1d1e1f;
|
||||
border-color: #414243;
|
||||
|
||||
Reference in New Issue
Block a user