feat: 增加字典的json导出和导入

This commit is contained in:
piexlMax(奇淼
2025-11-26 15:45:39 +08:00
parent c028ea3d15
commit a1f18e1386
7 changed files with 477 additions and 5 deletions

View File

@@ -135,3 +135,57 @@ func (s *DictionaryApi) GetSysDictionaryList(c *gin.Context) {
}
response.OkWithDetailed(list, "获取成功", c)
}
// ExportSysDictionary
// @Tags SysDictionary
// @Summary 导出字典JSON包含字典详情
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param data query system.SysDictionary true "字典ID"
// @Success 200 {object} response.Response{data=map[string]interface{},msg=string} "导出字典JSON"
// @Router /sysDictionary/exportSysDictionary [get]
func (s *DictionaryApi) ExportSysDictionary(c *gin.Context) {
var dictionary system.SysDictionary
err := c.ShouldBindQuery(&dictionary)
if err != nil {
response.FailWithMessage(err.Error(), c)
return
}
if dictionary.ID == 0 {
response.FailWithMessage("字典ID不能为空", c)
return
}
exportData, err := dictionaryService.ExportSysDictionary(dictionary.ID)
if err != nil {
global.GVA_LOG.Error("导出失败!", zap.Error(err))
response.FailWithMessage("导出失败", c)
return
}
response.OkWithDetailed(exportData, "导出成功", c)
}
// ImportSysDictionary
// @Tags SysDictionary
// @Summary 导入字典JSON包含字典详情
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param data body map[string]interface{} 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)
if err != nil {
response.FailWithMessage(err.Error(), c)
return
}
err = dictionaryService.ImportSysDictionary(importData)
if err != nil {
global.GVA_LOG.Error("导入失败!", zap.Error(err))
response.FailWithMessage("导入失败: "+err.Error(), c)
return
}
response.OkWithMessage("导入成功", c)
}

View File

@@ -14,6 +14,8 @@ func (s *DictionaryRouter) InitSysDictionaryRouter(Router *gin.RouterGroup) {
sysDictionaryRouter.POST("createSysDictionary", dictionaryApi.CreateSysDictionary) // 新建SysDictionary
sysDictionaryRouter.DELETE("deleteSysDictionary", dictionaryApi.DeleteSysDictionary) // 删除SysDictionary
sysDictionaryRouter.PUT("updateSysDictionary", dictionaryApi.UpdateSysDictionary) // 更新SysDictionary
sysDictionaryRouter.POST("importSysDictionary", dictionaryApi.ImportSysDictionary) // 导入SysDictionary
sysDictionaryRouter.GET("exportSysDictionary", dictionaryApi.ExportSysDictionary) // 导出SysDictionary
}
{
sysDictionaryRouterWithoutRecord.GET("findSysDictionary", dictionaryApi.FindSysDictionary) // 根据ID获取SysDictionary

View File

@@ -152,3 +152,166 @@ func (dictionaryService *DictionaryService) checkCircularReference(currentID uin
return nil
}
//@author: [yourname]
//@function: ExportSysDictionary
//@description: 导出字典JSON包含字典详情
//@param: id uint
//@return: exportData map[string]interface{}, err error
func (dictionaryService *DictionaryService) ExportSysDictionary(id uint) (exportData map[string]interface{}, err error) {
var dictionary system.SysDictionary
// 查询字典及其所有详情
err = global.GVA_DB.Where("id = ?", id).Preload("SysDictionaryDetails", func(db *gorm.DB) *gorm.DB {
return db.Order("sort")
}).First(&dictionary).Error
if err != nil {
return nil, err
}
// 构造导出数据
exportData = map[string]interface{}{
"name": dictionary.Name,
"type": dictionary.Type,
"status": dictionary.Status,
"desc": dictionary.Desc,
"details": dictionary.SysDictionaryDetails,
}
return exportData, nil
}
//@author: [yourname]
//@function: ImportSysDictionary
//@description: 导入字典JSON包含字典详情
//@param: importData map[string]interface{}
//@return: err error
func (dictionaryService *DictionaryService) ImportSysDictionary(importData map[string]interface{}) error {
// 解析基本字典信息
name, ok := importData["name"].(string)
if !ok || name == "" {
return errors.New("字典名称不能为空")
}
dictType, ok := importData["type"].(string)
if !ok || dictType == "" {
return errors.New("字典类型不能为空")
}
// 检查字典类型是否已存在
if !errors.Is(global.GVA_DB.First(&system.SysDictionary{}, "type = ?", dictType).Error, gorm.ErrRecordNotFound) {
return errors.New("存在相同的type不允许导入")
}
// 创建字典
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
}
// 开启事务
return global.GVA_DB.Transaction(func(tx *gorm.DB) error {
// 创建字典
if err := tx.Create(&dictionary).Error; err != nil {
return err
}
// 处理字典详情
if details, ok := importData["details"].([]interface{}); ok && len(details) > 0 {
// 创建一个映射来跟踪旧ID到新ID的对应关系
idMap := make(map[uint]uint)
// 第一遍创建所有详情记录不设置parent_id
for _, detail := range details {
detailMap, ok := detail.(map[string]interface{})
if !ok {
continue
}
label, _ := detailMap["label"].(string)
value, _ := detailMap["value"].(string)
if label == "" || value == "" {
continue
}
detailRecord := system.SysDictionaryDetail{
Label: label,
Value: value,
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
}
}
// 第二遍更新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 {
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
}
}
}
}
}
}
}
return nil
})
}

View File

@@ -149,6 +149,8 @@ func (i *initApi) InitializeData(ctx context.Context) (context.Context, error) {
{ApiGroup: "系统字典", Method: "PUT", Path: "/sysDictionary/updateSysDictionary", Description: "更新字典"},
{ApiGroup: "系统字典", Method: "GET", Path: "/sysDictionary/findSysDictionary", Description: "根据ID获取字典建议选择"},
{ApiGroup: "系统字典", Method: "GET", Path: "/sysDictionary/getSysDictionaryList", Description: "获取字典列表"},
{ApiGroup: "系统字典", Method: "POST", Path: "/sysDictionary/importSysDictionary", Description: "导入字典JSON"},
{ApiGroup: "系统字典", Method: "GET", Path: "/sysDictionary/exportSysDictionary", Description: "导出字典JSON"},
{ApiGroup: "操作记录", Method: "POST", Path: "/sysOperationRecord/createSysOperationRecord", Description: "新增操作记录"},
{ApiGroup: "操作记录", Method: "GET", Path: "/sysOperationRecord/findSysOperationRecord", Description: "根据ID获取操作记录"},

View File

@@ -149,6 +149,8 @@ func (i *initCasbin) InitializeData(ctx context.Context) (context.Context, error
{Ptype: "p", V0: "888", V1: "/sysDictionary/getSysDictionaryList", V2: "GET"},
{Ptype: "p", V0: "888", V1: "/sysDictionary/createSysDictionary", V2: "POST"},
{Ptype: "p", V0: "888", V1: "/sysDictionary/deleteSysDictionary", V2: "DELETE"},
{Ptype: "p", V0: "888", V1: "/sysDictionary/importSysDictionary", V2: "POST"},
{Ptype: "p", V0: "888", V1: "/sysDictionary/exportSysDictionary", V2: "GET"},
{Ptype: "p", V0: "888", V1: "/sysOperationRecord/findSysOperationRecord", V2: "GET"},
{Ptype: "p", V0: "888", V1: "/sysOperationRecord/updateSysOperationRecord", V2: "PUT"},

View File

@@ -78,3 +78,35 @@ export const getSysDictionaryList = (params) => {
params
})
}
// @Tags SysDictionary
// @Summary 导出字典JSON包含字典详情
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param data query model.SysDictionary true "字典ID"
// @Success 200 {string} string "{"success":true,"data":{},"msg":"导出成功"}"
// @Router /sysDictionary/exportSysDictionary [get]
export const exportSysDictionary = (params) => {
return service({
url: '/sysDictionary/exportSysDictionary',
method: 'get',
params
})
}
// @Tags SysDictionary
// @Summary 导入字典JSON包含字典详情
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param data body object true "字典JSON数据"
// @Success 200 {string} string "{"success":true,"data":{},"msg":"导入成功"}"
// @Router /sysDictionary/importSysDictionary [post]
export const importSysDictionary = (data) => {
return service({
url: '/sysDictionary/importSysDictionary',
method: 'post',
data
})
}

View File

@@ -35,6 +35,8 @@
:icon="Search"
@click="showSearchInputHandler"
></el-button>
<el-button type="success" @click="openImportDialog" :icon="Upload">
</el-button>
<el-button type="primary" @click="openDrawer" :icon="Plus">
</el-button>
</div>
@@ -61,7 +63,14 @@
<span class="mr-auto text-sm">{{ dictionary.type }}</span>
</div>
<div class="min-w-[40px]">
<div class="min-w-[60px] flex items-center gap-2">
<el-icon
class="text-green-500"
@click.stop="exportDictionary(dictionary)"
title="导出字典"
>
<Download />
</el-icon>
<el-icon
class="text-blue-500"
@click.stop="updateSysDictionaryFunc(dictionary)"
@@ -160,6 +169,76 @@
</el-form-item>
</el-form>
</el-drawer>
<!-- 导入字典对话框 -->
<el-dialog
v-model="importDialogVisible"
title="导入字典JSON"
width="70%"
:close-on-click-modal="false"
>
<div class="import-dialog-content">
<div class="mb-4">
<el-alert
title="请粘贴或编辑字典JSON数据支持包含字典详情的完整数据结构"
type="info"
:closable="false"
show-icon
/>
</div>
<div class="json-editor-container">
<el-input
v-model="importJsonText"
type="textarea"
:rows="20"
placeholder='请输入JSON数据例如
{
"name": "性别",
"type": "gender",
"status": true,
"desc": "性别字典",
"details": [
{
"label": "男",
"value": "1",
"status": true,
"sort": 1
},
{
"label": "女",
"value": "2",
"status": true,
"sort": 2
}
]
}'
class="json-textarea"
/>
</div>
<div class="mt-4" v-if="jsonPreviewError">
<el-alert
:title="jsonPreviewError"
type="error"
:closable="false"
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>
</div>
</template>
@@ -169,14 +248,16 @@
deleteSysDictionary,
updateSysDictionary,
findSysDictionary,
getSysDictionaryList
getSysDictionaryList,
exportSysDictionary,
importSysDictionary
} from '@/api/sysDictionary' // 此处请自行替换地址
import WarningBar from '@/components/warningBar/warningBar.vue'
import { ref } from 'vue'
import { ref, computed, watch } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import sysDictionaryDetail from './sysDictionaryDetail.vue'
import { Edit, Plus, Search } from '@element-plus/icons-vue'
import { Edit, Plus, Search, Download, Upload } from '@element-plus/icons-vue'
import { useAppStore } from '@/pinia'
defineOptions({
@@ -223,6 +304,35 @@
const dictionaryData = ref([])
const availableParentDictionaries = ref([])
// 导入相关
const importDialogVisible = ref(false)
const importJsonText = ref('')
const importing = ref(false)
const jsonPreviewError = ref('')
const jsonPreview = ref(null)
// 监听JSON文本变化实时预览
watch(importJsonText, (newVal) => {
if (!newVal.trim()) {
jsonPreview.value = null
jsonPreviewError.value = ''
return
}
try {
jsonPreview.value = JSON.parse(newVal)
jsonPreviewError.value = ''
} catch (e) {
jsonPreviewError.value = 'JSON格式错误: ' + e.message
jsonPreview.value = null
}
})
// 格式化JSON预览
const jsonPreviewFormatted = computed(() => {
if (!jsonPreview.value) return ''
return JSON.stringify(jsonPreview.value, null, 2)
})
// 查询
const getTableData = async () => {
const res = await getSysDictionaryList({
@@ -358,9 +468,76 @@
getTableData()
}
}
// 导出字典
const exportDictionary = async (row) => {
try {
const res = await exportSysDictionary({ ID: row.ID })
if (res.code === 0) {
// 将JSON数据转换为字符串并下载
const jsonStr = JSON.stringify(res.data, null, 2)
const blob = new Blob([jsonStr], { type: 'application/json' })
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = `${row.type}_${row.name}_dictionary.json`
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
URL.revokeObjectURL(url)
ElMessage.success('导出成功')
}
} catch (error) {
ElMessage.error('导出失败: ' + error.message)
}
}
// 打开导入对话框
const openImportDialog = () => {
importDialogVisible.value = true
importJsonText.value = ''
jsonPreview.value = null
jsonPreviewError.value = ''
}
// 关闭导入对话框
const closeImportDialog = () => {
importDialogVisible.value = false
importJsonText.value = ''
jsonPreview.value = null
jsonPreviewError.value = ''
}
// 处理导入
const handleImport = async () => {
if (!importJsonText.value.trim()) {
ElMessage.warning('请输入JSON数据')
return
}
if (jsonPreviewError.value) {
ElMessage.error('JSON格式错误请检查后重试')
return
}
try {
importing.value = true
const jsonData = JSON.parse(importJsonText.value)
const res = await importSysDictionary(jsonData)
if (res.code === 0) {
ElMessage.success('导入成功')
closeImportDialog()
getTableData()
}
} catch (error) {
ElMessage.error('导入失败: ' + error.message)
} finally {
importing.value = false
}
}
</script>
<style>
<style scoped>
.dict-box {
height: calc(100vh - 240px);
}
@@ -369,4 +546,44 @@
background-color: var(--el-color-primary) !important;
color: #fff;
}
.import-dialog-content {
max-height: 70vh;
overflow-y: auto;
}
.json-editor-container {
border: 1px solid #dcdfe6;
border-radius: 4px;
overflow: hidden;
}
.json-textarea :deep(.el-textarea__inner) {
font-family: 'Courier New', Courier, monospace;
font-size: 13px;
line-height: 1.5;
}
.json-preview {
background-color: #f5f7fa;
border: 1px solid #dcdfe6;
border-radius: 4px;
padding: 16px;
max-height: 400px;
overflow: auto;
}
.json-preview pre {
margin: 0;
font-family: 'Courier New', Courier, monospace;
font-size: 13px;
line-height: 1.5;
white-space: pre-wrap;
word-wrap: break-word;
}
.dark .json-preview {
background-color: #1d1e1f;
border-color: #414243;
}
</style>