diff --git a/.DS_Store b/.DS_Store index ebd5b5e..d6f4d35 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/dist.zip b/dist.zip new file mode 100644 index 0000000..fceaac0 Binary files /dev/null and b/dist.zip differ diff --git a/src/components/ClientDownloads.jsx b/src/components/ClientDownloads.jsx index db5ef46..bb4f8c1 100644 --- a/src/components/ClientDownloads.jsx +++ b/src/components/ClientDownloads.jsx @@ -29,42 +29,24 @@ const ClientDownloads = () => { } }; - const getPlatformIcon = (platformName) => { - switch (platformName) { - case 'ios': - return ; - case 'android': - return ; - case 'mac_intel': - case 'mac_m': - return ; - case 'mcu': - return ; - default: - return ; + const getPlatformIcon = (platformCode) => { + const code = (platformCode || '').toUpperCase(); + + // 根据 platform_code 判断图标 + if (code.includes('IOS') || code.includes('MAC')) { + return ; + } else if (code.includes('ANDROID')) { + return ; + } else if (code.includes('TERM') || code.includes('MCU')) { + return ; + } else { + return ; } }; const getPlatformLabel = (client) => { - const platformName = client.platform_name; - const platformType = client.platform_type; - - // 根据平台类型和平台名称组合判断 - if (platformType === 'terminal') { - if (platformName === 'android') return 'Android终端'; - if (platformName === 'mcu') return '单片机'; - } - - // 默认标签 - const labels = { - ios: 'iOS', - android: 'Android', - windows: 'Windows', - mac_intel: 'Mac (Intel)', - mac_m: 'Mac (M系列)', - linux: 'Linux' - }; - return labels[platformName] || platformName; + // 优先使用 dict_data 的中文标签 + return client.label_cn || client.platform_code || '未知平台'; }; const formatFileSize = (bytes) => { @@ -109,7 +91,7 @@ const ClientDownloads = () => { className="client-download-card" >
- {getPlatformIcon(client.platform_name)} + {getPlatformIcon(client.platform_code)}

{getPlatformLabel(client)}

@@ -149,7 +131,7 @@ const ClientDownloads = () => { className="client-download-card" >
- {getPlatformIcon(client.platform_name)} + {getPlatformIcon(client.platform_code)}

{getPlatformLabel(client)}

@@ -189,7 +171,7 @@ const ClientDownloads = () => { className="client-download-card" >
- {getPlatformIcon(client.platform_name)} + {getPlatformIcon(client.platform_code)}

{getPlatformLabel(client)}

diff --git a/src/config/api.js b/src/config/api.js index 6564c27..b13d19a 100644 --- a/src/config/api.js +++ b/src/config/api.js @@ -67,11 +67,20 @@ const API_CONFIG = { CLIENT_DOWNLOADS: { LIST: '/api/clients', LATEST: '/api/clients/latest', - LATEST_BY_PLATFORM: '/api/clients/latest/by-platform', + LATEST_BY_PLATFORM: '/api/clients/latest/by-platform', // 支持旧版(platform_type+platform_name)和新版(platform_code) DETAIL: (id) => `/api/clients/${id}`, CREATE: '/api/clients', UPDATE: (id) => `/api/clients/${id}`, - DELETE: (id) => `/api/clients/${id}` + DELETE: (id) => `/api/clients/${id}`, + UPLOAD: '/api/clients/upload' + }, + DICT_DATA: { + TYPES: '/api/dict/types', + BY_TYPE: (dictType) => `/api/dict/${dictType}`, + BY_CODE: (dictType, dictCode) => `/api/dict/${dictType}/${dictCode}`, + CREATE: '/api/dict', + UPDATE: (id) => `/api/dict/${id}`, + DELETE: (id) => `/api/dict/${id}` }, VOICEPRINT: { STATUS: (userId) => `/api/voiceprint/${userId}`, diff --git a/src/pages/AdminManagement.css b/src/pages/AdminManagement.css index d717d3c..11d1c52 100644 --- a/src/pages/AdminManagement.css +++ b/src/pages/AdminManagement.css @@ -1,10 +1,9 @@ /* AdminManagement.css */ .admin-management-page { - height: 100vh; + min-height: 100vh; background: #f8fafc; display: flex; flex-direction: column; - overflow: hidden; } /* Content */ @@ -17,14 +16,15 @@ display: flex; flex-direction: column; gap: 2rem; - overflow-y: auto; } .admin-wrapper { background: white; border-radius: 16px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05); - overflow: hidden; + display: flex; + flex-direction: column; + min-height: 0; } /* Old Tabs styles - can be removed or kept for reference */ @@ -78,10 +78,18 @@ } /* New AntD Tabs Styles */ +.admin-tabs { + display: flex; + flex-direction: column; + flex: 1; + min-height: 0; +} + .admin-tabs .ant-tabs-nav { padding: 0 2rem; margin-bottom: 0 !important; border-bottom: 1px solid #e2e8f0; + flex-shrink: 0; } .admin-tabs .ant-tabs-tab { @@ -109,7 +117,11 @@ .admin-tabs .ant-tabs-content-holder { padding: 2rem; - min-height: 60vh; + overflow-y: auto; +} + +.admin-tabs .ant-tabs-content { + height: 100%; } /* Responsive Design */ diff --git a/src/pages/AdminManagement.jsx b/src/pages/AdminManagement.jsx index be34110..44465d2 100644 --- a/src/pages/AdminManagement.jsx +++ b/src/pages/AdminManagement.jsx @@ -1,10 +1,11 @@ import React from 'react'; -import { Settings, Users, Smartphone, Shield } from 'lucide-react'; +import { Settings, Users, Smartphone, Shield, BookText } from 'lucide-react'; import { Tabs } from 'antd'; import UserManagement from './admin/UserManagement'; import SystemConfiguration from './admin/SystemConfiguration'; import ClientManagement from './ClientManagement'; import PermissionManagement from './admin/PermissionManagement'; +import DictManagement from './admin/DictManagement'; import Breadcrumb from '../components/Breadcrumb'; import './AdminManagement.css'; @@ -30,6 +31,12 @@ const AdminManagement = () => { > + 字典管理} + key="dictManagement" + > + + 系统配置} key="systemConfiguration" diff --git a/src/pages/ClientDownloadPage.css b/src/pages/ClientDownloadPage.css index d9343d3..2fd2822 100644 --- a/src/pages/ClientDownloadPage.css +++ b/src/pages/ClientDownloadPage.css @@ -13,7 +13,7 @@ } .download-page-header .header-content { - max-width: 1200px; + max-width: 1400px; margin: 0 auto; padding: 1rem 2rem; } @@ -41,7 +41,7 @@ .download-page-content { flex: 1; - max-width: 1200px; + max-width: 1400px; margin: 0 auto; width: 100%; padding: 2rem; diff --git a/src/pages/ClientManagement.css b/src/pages/ClientManagement.css index e10e0cd..4392009 100644 --- a/src/pages/ClientManagement.css +++ b/src/pages/ClientManagement.css @@ -216,14 +216,26 @@ } .btn-icon { - padding: 0.5rem; + width: 40px; + height: 40px; + min-width: 40px; + min-height: 40px; + padding: 0; border: none; border-radius: 6px; cursor: pointer; transition: all 0.2s ease; - display: flex; + display: inline-flex; align-items: center; justify-content: center; + flex-shrink: 0; +} + +.btn-icon svg { + display: block; + width: 16px !important; + height: 16px !important; + flex-shrink: 0; } .btn-edit { @@ -455,6 +467,60 @@ cursor: pointer; } +/* 文件上传区域 */ +.upload-area { + border: 2px dashed #e2e8f0; + border-radius: 8px; + padding: 1.5rem; + text-align: center; + transition: all 0.2s ease; +} + +.upload-area:hover { + border-color: #667eea; + background: #f8fafc; +} + +.upload-label { + display: inline-flex; + flex-direction: column; + align-items: center; + gap: 0.5rem; + padding: 1rem 2rem; + background: #667eea; + color: white; + border-radius: 8px; + cursor: pointer; + transition: all 0.2s ease; + font-weight: 500; +} + +.upload-label:hover { + background: #5568d3; + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3); +} + +.upload-label.disabled { + background: #94a3b8; + cursor: not-allowed; + transform: none; + box-shadow: none; +} + +.upload-label.disabled:hover { + background: #94a3b8; + transform: none; + box-shadow: none; +} + +.upload-hint { + margin-top: 0.75rem; + font-size: 0.875rem; + color: #64748b; + margin-bottom: 0; +} + .modal-actions { padding: 1rem 2rem; border-top: 1px solid #e2e8f0; @@ -515,15 +581,6 @@ line-height: 1.6; } -.btn-delete { - background: #ef4444; - color: white; -} - -.btn-delete:hover { - background: #dc2626; -} - /* 空状态 */ .empty-state { text-align: center; diff --git a/src/pages/ClientManagement.jsx b/src/pages/ClientManagement.jsx index d4e2027..fdaf4d1 100644 --- a/src/pages/ClientManagement.jsx +++ b/src/pages/ClientManagement.jsx @@ -16,7 +16,8 @@ import { Link, FileText, HardDrive, - Cpu + Cpu, + Upload } from 'lucide-react'; import apiClient from '../utils/apiClient'; import { buildApiUrl, API_ENDPOINTS } from '../config/api'; @@ -36,10 +37,14 @@ const ClientManagement = ({ user }) => { const [searchQuery, setSearchQuery] = useState(''); const [expandedNotes, setExpandedNotes] = useState({}); const [toasts, setToasts] = useState([]); + const [uploadingFile, setUploadingFile] = useState(false); + + // 码表数据 + const [platforms, setPlatforms] = useState({ tree: [], items: [] }); + const [platformsMap, setPlatformsMap] = useState({}); const [formData, setFormData] = useState({ - platform_type: 'mobile', - platform_name: 'ios', + platform_code: '', version: '', version_code: '', download_url: '', @@ -50,23 +55,6 @@ const ClientManagement = ({ user }) => { min_system_version: '' }); - const platformOptions = { - mobile: [ - { value: 'ios', label: 'iOS', icon: }, - { value: 'android', label: 'Android', icon: } - ], - desktop: [ - { value: 'windows', label: 'Windows', icon: }, - { value: 'mac_intel', label: 'Mac (Intel)', icon: }, - { value: 'mac_m', label: 'Mac (M系列)', icon: }, - { value: 'linux', label: 'Linux', icon: } - ], - terminal: [ - { value: 'android', label: 'Android终端', icon: }, - { value: 'mcu', label: '单片机', icon: } - ] - }; - // Toast helper functions const showToast = (message, type = 'info') => { const id = Date.now(); @@ -78,9 +66,28 @@ const ClientManagement = ({ user }) => { }; useEffect(() => { + fetchPlatforms(); fetchClients(); }, []); + const fetchPlatforms = async () => { + try { + const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.DICT_DATA.BY_TYPE('client_platform'))); + const { tree, items } = response.data; + setPlatforms({ tree, items }); + + // 构建快速查找map + const map = {}; + items.forEach(item => { + map[item.dict_code] = item; + }); + setPlatformsMap(map); + } catch (error) { + console.error('获取平台列表失败:', error); + showToast('获取平台列表失败', 'error'); + } + }; + const fetchClients = async () => { setLoading(true); try { @@ -97,13 +104,32 @@ const ClientManagement = ({ user }) => { const handleCreate = async () => { try { // 验证必填字段 - if (!formData.version_code || !formData.version || !formData.download_url) { + if (!formData.platform_code || !formData.version_code || !formData.version || !formData.download_url) { showToast('请填写所有必填字段', 'warning'); return; } + // 根据platform_code映射到旧字段platform_type和platform_name以保持向后兼容 + const platformInfo = platformsMap[formData.platform_code]; + const parentCode = platformInfo?.parent_code; + const parentInfo = parentCode && parentCode !== 'ROOT' ? platformsMap[parentCode] : null; + + // 映射platform_type + let platform_type = 'desktop'; // 默认 + if (parentInfo) { + const parentCodeUpper = parentCode.toUpperCase(); + if (parentCodeUpper === 'MOBILE') platform_type = 'mobile'; + else if (parentCodeUpper === 'DESKTOP') platform_type = 'desktop'; + else if (parentCodeUpper === 'TERMINAL') platform_type = 'terminal'; + } + + // 映射platform_name (简化映射,用小写的dict_code) + const platform_name = formData.platform_code.toLowerCase(); + const payload = { ...formData, + platform_type, + platform_name, version_code: parseInt(formData.version_code, 10), file_size: formData.file_size ? parseInt(formData.file_size, 10) : null }; @@ -132,12 +158,13 @@ const ClientManagement = ({ user }) => { const handleUpdate = async () => { try { // 验证必填字段 - if (!formData.version_code || !formData.version || !formData.download_url) { + if (!formData.platform_code || !formData.version_code || !formData.version || !formData.download_url) { showToast('请填写所有必填字段', 'warning'); return; } const payload = { + platform_code: formData.platform_code, version: formData.version, version_code: parseInt(formData.version_code, 10), download_url: formData.download_url, @@ -191,8 +218,7 @@ const ClientManagement = ({ user }) => { setIsEditing(true); setSelectedClient(client); setFormData({ - platform_type: client.platform_type, - platform_name: client.platform_name, + platform_code: client.platform_code || '', version: client.version, version_code: String(client.version_code), download_url: client.download_url, @@ -205,9 +231,12 @@ const ClientManagement = ({ user }) => { } else { setIsEditing(false); setSelectedClient(null); + // 默认选择第一个可用的平台 + const defaultPlatformCode = platforms.items.length > 0 && platforms.items[0].parent_code !== 'ROOT' + ? platforms.items[0].dict_code + : ''; setFormData({ - platform_type: 'mobile', - platform_name: 'ios', + platform_code: defaultPlatformCode, version: '', version_code: '', download_url: '', @@ -240,6 +269,54 @@ const ClientManagement = ({ user }) => { setFormData(prev => ({ ...prev, [field]: value })); }; + const handleFileUpload = async (event) => { + const file = event.target.files[0]; + if (!file) return; + + if (!formData.platform_code) { + showToast('请先选择平台', 'warning'); + event.target.value = ''; + return; + } + + setUploadingFile(true); + + try { + const uploadFormData = new FormData(); + uploadFormData.append('file', file); + uploadFormData.append('platform_code', formData.platform_code); + + const response = await apiClient.post( + buildApiUrl(API_ENDPOINTS.CLIENT_DOWNLOADS.UPLOAD), + uploadFormData, + { + headers: { + 'Content-Type': 'multipart/form-data' + } + } + ); + + const { file_size, download_url, version_code, version_name } = response.data; + + // 自动填充表单 + setFormData(prev => ({ + ...prev, + file_size: file_size ? String(file_size) : prev.file_size, + download_url: download_url || prev.download_url, + version_code: version_code ? String(version_code) : prev.version_code, + version: version_name || prev.version + })); + + showToast('文件上传成功,已自动填充相关字段', 'success'); + } catch (error) { + console.error('文件上传失败:', error); + showToast(error.response?.data?.message || '文件上传失败', 'error'); + } finally { + setUploadingFile(false); + event.target.value = ''; + } + }; + const openEditModal = (client) => { handleOpenModal(client); }; @@ -247,15 +324,17 @@ const ClientManagement = ({ user }) => { const openDeleteModal = (client) => { setDeleteConfirmInfo({ id: client.id, - platform_name: getPlatformLabel(client.platform_name), + platform_name: getPlatformLabel(client.platform_code), version: client.version }); }; const resetForm = () => { + const defaultPlatformCode = platforms.items.length > 0 && platforms.items[0].parent_code !== 'ROOT' + ? platforms.items[0].dict_code + : ''; setFormData({ - platform_type: 'mobile', - platform_name: 'ios', + platform_code: defaultPlatformCode, version: '', version_code: '', download_url: '', @@ -268,10 +347,9 @@ const ClientManagement = ({ user }) => { setSelectedClient(null); }; - const getPlatformLabel = (platformName) => { - const allOptions = [...platformOptions.mobile, ...platformOptions.desktop, ...platformOptions.terminal]; - const option = allOptions.find(opt => opt.value === platformName); - return option ? option.label : platformName; + const getPlatformLabel = (platformCode) => { + const platform = platformsMap[platformCode]; + return platform ? platform.label_cn : platformCode; }; const formatFileSize = (bytes) => { @@ -295,7 +373,7 @@ const ClientManagement = ({ user }) => { const query = searchQuery.toLowerCase(); return ( client.version.toLowerCase().includes(query) || - getPlatformLabel(client.platform_name).toLowerCase().includes(query) || + getPlatformLabel(client.platform_code).toLowerCase().includes(query) || (client.release_notes && client.release_notes.toLowerCase().includes(query)) ); } @@ -398,9 +476,9 @@ const ClientManagement = ({ user }) => {
-

{getPlatformLabel(client.platform_name)}

- {client.is_latest && 最新} - {!client.is_active && 未启用} +

{getPlatformLabel(client.platform_code)}

+ {client.is_latest === true && 最新} + {client.is_active === false && 未启用}
- {client.min_system_version && ( -
- 系统要求: - {client.min_system_version} -
- )} {client.release_notes && (
{ {formData && ( <>
-
- +
+ -
- -
- -
+ {/* 文件上传区域 */} +
+ +
+ + +

+ {!formData.platform_code + ? '请先选择平台' + : 'APK文件将自动读取版本信息,其他文件只读取文件大小'} +

+
+
+
diff --git a/src/pages/admin/DictManagement.css b/src/pages/admin/DictManagement.css new file mode 100644 index 0000000..6906a4c --- /dev/null +++ b/src/pages/admin/DictManagement.css @@ -0,0 +1,331 @@ +/* 字典管理页面样式 - 左右布局 */ +.dict-management { + padding: 1.5rem; + background: #f8fafc; + min-height: 100%; + display: flex; + flex-direction: column; +} + +/* 头部 */ +.dict-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1.5rem; + padding: 1.5rem; + background: white; + border-radius: 12px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); +} + +.dict-header-left { + display: flex; + align-items: center; + gap: 1rem; +} + +.dict-header-left svg { + color: #667eea; +} + +.dict-header-left h2 { + margin: 0; + font-size: 1.5rem; + font-weight: 600; + color: #1e293b; +} + +.dict-header-left p { + margin: 0.25rem 0 0 0; + font-size: 0.875rem; + color: #64748b; +} + +/* 主布局 - 左右分栏 */ +.dict-main-layout { + display: grid; + grid-template-columns: 380px 1fr; + gap: 1.5rem; + flex: 1; + min-height: 0; +} + +/* 左侧面板 */ +.dict-left-panel { + display: flex; + flex-direction: column; + min-height: 0; +} + +.dict-tree-card { + flex: 1; + display: flex; + flex-direction: column; + min-height: 0; +} + +.dict-tree-card .ant-card-body { + flex: 1; + display: flex; + flex-direction: column; + min-height: 0; + padding: 1rem; +} + +/* 字典类型选择器 */ +.dict-type-selector { + display: flex; + align-items: center; + gap: 0.75rem; + margin-bottom: 1rem; + padding-bottom: 1rem; + border-bottom: 1px solid #e2e8f0; +} + +.dict-type-selector label { + font-weight: 500; + color: #475569; + white-space: nowrap; +} + +/* 树形容器 */ +.dict-tree-container { + flex: 1; + overflow-y: auto; + min-height: 0; +} + +.dict-tree-container .ant-tree { + background: transparent; +} + +.dict-tree-container .ant-tree-node-content-wrapper { + padding: 4px 8px; + border-radius: 4px; + transition: all 0.2s ease; +} + +.dict-tree-container .ant-tree-node-content-wrapper:hover { + background: #f1f5f9; +} + +.dict-tree-container .ant-tree-node-selected .ant-tree-node-content-wrapper { + background: #e0e7ff !important; + color: #667eea; +} + +/* 树节点标题样式 */ +.tree-node-title { + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 0.9rem; +} + +.tree-node-title svg { + color: #64748b; + flex-shrink: 0; +} + +.tree-node-title span { + flex-shrink: 0; +} + +.tree-node-code { + color: #94a3b8; + font-size: 0.8rem; + margin-left: 0.25rem; +} + +/* 右侧面板 */ +.dict-right-panel { + display: flex; + flex-direction: column; + min-height: 0; +} + +.dict-form-card { + flex: 1; + display: flex; + flex-direction: column; + min-height: 0; +} + +.dict-form-card .ant-card-body { + flex: 1; + overflow-y: auto; + min-height: 0; + padding: 1.5rem; +} + +/* Card 头部样式 */ +.panel-header { + display: flex; + align-items: center; + gap: 0.5rem; + font-weight: 600; + color: #1e293b; +} + +.panel-header svg { + color: #667eea; +} + +/* Card 样式 */ +.dict-management .ant-card { + border-radius: 12px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); +} + +.dict-management .ant-card-head { + border-bottom: 1px solid #e2e8f0; + padding: 1rem 1.5rem; + min-height: auto; +} + +.dict-management .ant-card-head-title { + padding: 0; +} + +.dict-management .ant-card-extra { + padding: 0; +} + +/* 按钮样式 */ +.dict-management .ant-btn-primary { + background: #667eea; + border-color: #667eea; +} + +.dict-management .ant-btn-primary:hover { + background: #5568d3; + border-color: #5568d3; +} + +/* Form 样式 */ +.dict-management .ant-form-item-label > label { + font-weight: 500; + color: #475569; +} + +.dict-management .ant-input, +.dict-management .ant-input-number, +.dict-management .ant-select-selector { + border-radius: 6px; +} + +.dict-management .ant-input:focus, +.dict-management .ant-input-number:focus, +.dict-management .ant-select-focused .ant-select-selector { + border-color: #667eea; + box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.1); +} + +.dict-management .ant-input:disabled, +.dict-management .ant-select-disabled .ant-select-selector { + background: #f8fafc; + color: #94a3b8; +} + +/* 行内表单组 */ +.form-inline-group { + display: flex; + gap: 2rem; + align-items: center; + padding: 1rem; + background: #f8fafc; + border-radius: 8px; + margin-top: 0.5rem; +} + +.form-inline-item { + display: flex; + align-items: center; + gap: 0.75rem; +} + +.form-inline-item label { + font-weight: 500; + color: #475569; + white-space: nowrap; + margin: 0; +} + +/* Select 样式 */ +.dict-management .ant-select-dropdown { + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); +} + +/* Switch 样式 */ +.dict-management .ant-switch-checked { + background-color: #10b981; +} + +/* Empty 样式 */ +.dict-management .ant-empty { + margin: 2rem 0; +} + +/* Popconfirm 样式 */ +.dict-management .ant-popover-inner { + border-radius: 8px; +} + +/* 滚动条样式 */ +.dict-tree-container::-webkit-scrollbar, +.dict-form-card .ant-card-body::-webkit-scrollbar { + width: 6px; +} + +.dict-tree-container::-webkit-scrollbar-track, +.dict-form-card .ant-card-body::-webkit-scrollbar-track { + background: #f1f5f9; + border-radius: 3px; +} + +.dict-tree-container::-webkit-scrollbar-thumb, +.dict-form-card .ant-card-body::-webkit-scrollbar-thumb { + background: #cbd5e1; + border-radius: 3px; +} + +.dict-tree-container::-webkit-scrollbar-thumb:hover, +.dict-form-card .ant-card-body::-webkit-scrollbar-thumb:hover { + background: #94a3b8; +} + +/* 响应式设计 */ +@media (max-width: 1200px) { + .dict-main-layout { + grid-template-columns: 320px 1fr; + } +} + +@media (max-width: 968px) { + .dict-main-layout { + grid-template-columns: 1fr; + grid-template-rows: auto 1fr; + } + + .dict-left-panel { + max-height: 400px; + } +} + +@media (max-width: 768px) { + .dict-management { + padding: 1rem; + } + + .dict-header { + flex-direction: column; + align-items: flex-start; + gap: 1rem; + } + + .dict-main-layout { + gap: 1rem; + } +} diff --git a/src/pages/admin/DictManagement.jsx b/src/pages/admin/DictManagement.jsx new file mode 100644 index 0000000..66f11e2 --- /dev/null +++ b/src/pages/admin/DictManagement.jsx @@ -0,0 +1,407 @@ +import React, { useState, useEffect } from 'react'; +import { Tree, Button, Form, Input, InputNumber, Select, Switch, Space, message, Card, Empty, Popconfirm } from 'antd'; +import { BookText, Plus, Save, Trash2, FolderTree, FileText, X } from 'lucide-react'; +import apiClient from '../../utils/apiClient'; +import { buildApiUrl, API_ENDPOINTS } from '../../config/api'; +import './DictManagement.css'; + +const { Option } = Select; +const { TextArea } = Input; + +const DictManagement = () => { + const [loading, setLoading] = useState(false); + const [dictTypes, setDictTypes] = useState([]); // 字典类型列表 + const [selectedDictType, setSelectedDictType] = useState('client_platform'); // 当前选中的字典类型 + const [dictData, setDictData] = useState([]); // 当前字典类型的数据 + const [treeData, setTreeData] = useState([]); // 树形结构数据 + const [selectedNode, setSelectedNode] = useState(null); // 当前选中的节点 + const [isEditing, setIsEditing] = useState(false); // 是否处于编辑状态 + const [form] = Form.useForm(); + + // 获取所有字典类型 + const fetchDictTypes = async () => { + try { + const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.DICT_DATA.TYPES)); + if (response.code === '200') { + setDictTypes(response.data.types); + } + } catch (error) { + message.error('获取字典类型失败'); + } + }; + + // 获取指定类型的字典数据 + const fetchDictData = async (dictType) => { + setLoading(true); + try { + const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.DICT_DATA.BY_TYPE(dictType))); + if (response.code === '200') { + setDictData(response.data.items); + + // 转换为 antd Tree 需要的格式 + const antdTreeData = buildAntdTreeData(response.data.tree); + setTreeData(antdTreeData); + + // 清空选中节点 + setSelectedNode(null); + setIsEditing(false); + form.resetFields(); + } + } catch (error) { + message.error('获取字典数据失败'); + } finally { + setLoading(false); + } + }; + + // 将树形数据转换为 antd Tree 组件格式 + const buildAntdTreeData = (tree) => { + return tree.map(node => ({ + title: ( +
+ {node.parent_code === 'ROOT' ? : } + {node.label_cn} + ({node.dict_code}) +
+ ), + key: node.dict_code, + data: node, + children: node.children && node.children.length > 0 ? buildAntdTreeData(node.children) : [] + })); + }; + + useEffect(() => { + fetchDictTypes(); + }, []); + + useEffect(() => { + if (selectedDictType) { + fetchDictData(selectedDictType); + } + }, [selectedDictType]); + + // 选中树节点 + const handleSelectNode = (selectedKeys, info) => { + if (selectedKeys.length > 0) { + const nodeData = info.node.data; + setSelectedNode(nodeData); + setIsEditing(true); + + // 填充表单 + form.setFieldsValue({ + dict_type: nodeData.dict_type, + dict_code: nodeData.dict_code, + parent_code: nodeData.parent_code, + label_cn: nodeData.label_cn, + label_en: nodeData.label_en, + sort_order: nodeData.sort_order, + extension_attr: nodeData.extension_attr ? JSON.stringify(nodeData.extension_attr, null, 2) : '', + is_default: nodeData.is_default === 1, + status: nodeData.status + }); + } + }; + + // 新增节点 + const handleAddNode = () => { + setSelectedNode(null); + setIsEditing(true); + form.resetFields(); + form.setFieldsValue({ + dict_type: selectedDictType, + parent_code: 'ROOT', + sort_order: 0, + status: 1, + is_default: false + }); + }; + + // 保存 + const handleSave = async () => { + try { + const values = await form.validateFields(); + + // 解析 extension_attr JSON + if (values.extension_attr) { + try { + values.extension_attr = JSON.parse(values.extension_attr); + } catch (e) { + message.error('扩展属性 JSON 格式错误'); + return; + } + } + + // 转换 is_default 为数字 + values.is_default = values.is_default ? 1 : 0; + + if (selectedNode) { + // 更新 + await apiClient.put( + buildApiUrl(API_ENDPOINTS.DICT_DATA.UPDATE(selectedNode.id)), + values + ); + message.success('更新成功'); + } else { + // 新增 + await apiClient.post(buildApiUrl(API_ENDPOINTS.DICT_DATA.CREATE), values); + message.success('创建成功'); + } + + // 重新加载数据 + fetchDictData(selectedDictType); + } catch (error) { + if (error.errorFields) { + // 表单验证错误 + return; + } + message.error(selectedNode ? '更新失败' : '创建失败'); + } + }; + + // 删除 + const handleDelete = async () => { + if (!selectedNode) return; + + try { + await apiClient.delete(buildApiUrl(API_ENDPOINTS.DICT_DATA.DELETE(selectedNode.id))); + message.success('删除成功'); + + // 重新加载数据 + setSelectedNode(null); + setIsEditing(false); + form.resetFields(); + fetchDictData(selectedDictType); + } catch (error) { + message.error('删除失败:' + (error.message || '未知错误')); + } + }; + + // 取消编辑 + const handleCancel = () => { + setIsEditing(false); + setSelectedNode(null); + form.resetFields(); + }; + + // 获取父级选项(用于新增/编辑时选择父级) + const getParentOptions = () => { + const options = [{ label: 'ROOT(顶级)', value: 'ROOT' }]; + dictData.forEach(item => { + if (item.parent_code === 'ROOT') { + options.push({ label: `${item.label_cn} (${item.dict_code})`, value: item.dict_code }); + } + }); + return options; + }; + + return ( +
+
+
+ +
+

字典管理

+

管理系统中的码表数据(树形结构)

+
+
+
+ +
+ {/* 左侧面板 */} +
+ + + 字典树 +
+ } + extra={ + + } + bordered={false} + className="dict-tree-card" + > +
+ + +
+ +
+ {treeData.length > 0 ? ( + + ) : ( + + )} +
+ +
+ + {/* 右侧面板 */} +
+ + + {selectedNode ? '编辑字典项' : isEditing ? '新增字典项' : '字典详情'} +
+ } + extra={ + isEditing && ( + + {selectedNode && ( + + + + )} + + + + ) + } + bordered={false} + className="dict-form-card" + > + {isEditing ? ( +
+ + + + + + + + + + + + + + + + + + + + + + + + + +