diff --git a/server/api/v1/system/sys_dictionary.go b/server/api/v1/system/sys_dictionary.go index 65833dee4..9673e7f28 100644 --- a/server/api/v1/system/sys_dictionary.go +++ b/server/api/v1/system/sys_dictionary.go @@ -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) diff --git a/server/model/system/request/sys_dictionary.go b/server/model/system/request/sys_dictionary.go index 2b5fa1a9d..5a84796ec 100644 --- a/server/model/system/request/sys_dictionary.go +++ b/server/model/system/request/sys_dictionary.go @@ -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字符串 +} diff --git a/server/service/system/sys_dictionary.go b/server/service/system/sys_dictionary.go index 90750d847..d5d068d91 100644 --- a/server/service/system/sys_dictionary.go +++ b/server/service/system/sys_dictionary.go @@ -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 - } - } } } } diff --git a/web/src/view/superAdmin/dictionary/sysDictionary.vue b/web/src/view/superAdmin/dictionary/sysDictionary.vue index 672150cba..7c1bd8a4a 100644 --- a/web/src/view/superAdmin/dictionary/sysDictionary.vue +++ b/web/src/view/superAdmin/dictionary/sysDictionary.vue @@ -4,7 +4,7 @@ title="获取字典且缓存方法已在前端utils/dictionary 已经封装完成 不必自己书写 使用方法查看文件内注释" /> - +
@@ -170,34 +170,70 @@ - - + -
+ + +
-
+ + +
+ +
+

将 JSON 文件拖到此处,或点击选择文件

+

也可以在下方文本框直接编辑

+
+ +
+ +
{ @@ -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;