增加了字段管理

main
mula.liu 2025-12-18 19:57:56 +08:00
parent a708031347
commit 86f58c854f
11 changed files with 1016 additions and 124 deletions

BIN
.DS_Store vendored

Binary file not shown.

BIN
dist.zip 100644

Binary file not shown.

View File

@ -29,42 +29,24 @@ const ClientDownloads = () => {
}
};
const getPlatformIcon = (platformName) => {
switch (platformName) {
case 'ios':
return <Apple size={32} />;
case 'android':
return <Smartphone size={32} />;
case 'mac_intel':
case 'mac_m':
return <Apple size={32} />;
case 'mcu':
return <Cpu size={32} />;
default:
return <Monitor size={32} />;
const getPlatformIcon = (platformCode) => {
const code = (platformCode || '').toUpperCase();
// platform_code
if (code.includes('IOS') || code.includes('MAC')) {
return <Apple size={32} />;
} else if (code.includes('ANDROID')) {
return <Smartphone size={32} />;
} else if (code.includes('TERM') || code.includes('MCU')) {
return <Cpu size={32} />;
} else {
return <Monitor size={32} />;
}
};
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"
>
<div className="card-icon">
{getPlatformIcon(client.platform_name)}
{getPlatformIcon(client.platform_code)}
</div>
<div className="card-info">
<h4>{getPlatformLabel(client)}</h4>
@ -149,7 +131,7 @@ const ClientDownloads = () => {
className="client-download-card"
>
<div className="card-icon">
{getPlatformIcon(client.platform_name)}
{getPlatformIcon(client.platform_code)}
</div>
<div className="card-info">
<h4>{getPlatformLabel(client)}</h4>
@ -189,7 +171,7 @@ const ClientDownloads = () => {
className="client-download-card"
>
<div className="card-icon">
{getPlatformIcon(client.platform_name)}
{getPlatformIcon(client.platform_code)}
</div>
<div className="card-info">
<h4>{getPlatformLabel(client)}</h4>

View File

@ -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}`,

View File

@ -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 */

View File

@ -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 = () => {
>
<PermissionManagement />
</TabPane>
<TabPane
tab={<span><BookText size={16} /> 字典管理</span>}
key="dictManagement"
>
<DictManagement />
</TabPane>
<TabPane
tab={<span><Settings size={16} /> 系统配置</span>}
key="systemConfiguration"

View File

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

View File

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

View File

@ -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: <Apple size={16} /> },
{ value: 'android', label: 'Android', icon: <Smartphone size={16} /> }
],
desktop: [
{ value: 'windows', label: 'Windows', icon: <Monitor size={16} /> },
{ value: 'mac_intel', label: 'Mac (Intel)', icon: <Apple size={16} /> },
{ value: 'mac_m', label: 'Mac (M系列)', icon: <Apple size={16} /> },
{ value: 'linux', label: 'Linux', icon: <Monitor size={16} /> }
],
terminal: [
{ value: 'android', label: 'Android终端', icon: <Smartphone size={16} /> },
{ value: 'mcu', label: '单片机', icon: <Cpu size={16} /> }
]
};
// 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_codeplatform_typeplatform_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 }) => {
<div key={client.id} className={`client-card ${!client.is_active ? 'inactive' : ''}`}>
<div className="card-header">
<div className="platform-info">
<h3>{getPlatformLabel(client.platform_name)}</h3>
{client.is_latest && <span className="badge-latest">最新</span>}
{!client.is_active && <span className="badge-inactive">未启用</span>}
<h3>{getPlatformLabel(client.platform_code)}</h3>
{client.is_latest === true && <span className="badge-latest">最新</span>}
{client.is_active === false && <span className="badge-inactive">未启用</span>}
</div>
<div className="card-actions">
<button
@ -433,12 +511,6 @@ const ClientManagement = ({ user }) => {
<span className="label">文件大小:</span>
<span className="value">{formatFileSize(client.file_size)}</span>
</div>
{client.min_system_version && (
<div className="info-row">
<span className="label">系统要求:</span>
<span className="value">{client.min_system_version}</span>
</div>
)}
{client.release_notes && (
<div className="release-notes">
<div
@ -490,39 +562,54 @@ const ClientManagement = ({ user }) => {
{formData && (
<>
<div className="form-row">
<div className="form-group">
<label><Monitor size={16} /> 平台类型 *</label>
<div className="form-group" style={{ flex: 1 }}>
<label><Monitor size={16} /> 选择平台 *</label>
<select
value={formData.platform_type}
onChange={(e) => {
const newType = e.target.value;
handleInputChange('platform_type', newType);
handleInputChange('platform_name', platformOptions[newType][0].value);
}}
value={formData.platform_code}
onChange={(e) => handleInputChange('platform_code', e.target.value)}
disabled={isEditing}
>
<option value="mobile">移动端</option>
<option value="desktop">桌面端</option>
<option value="terminal">专用终端</option>
</select>
</div>
<div className="form-group">
<label><Smartphone size={16} /> 具体平台 *</label>
<select
value={formData.platform_name}
onChange={(e) => handleInputChange('platform_name', e.target.value)}
disabled={isEditing}
>
{platformOptions[formData.platform_type].map(option => (
<option key={option.value} value={option.value}>
{option.label}
</option>
<option value="">请选择平台</option>
{platforms.tree.map(parentNode => (
<optgroup key={parentNode.dict_code} label={parentNode.label_cn}>
{parentNode.children && parentNode.children.map(childNode => (
<option key={childNode.dict_code} value={childNode.dict_code}>
{childNode.label_cn}
</option>
))}
</optgroup>
))}
</select>
</div>
</div>
{/* 文件上传区域 */}
<div className="form-group">
<label><Upload size={16} /> 上传安装包</label>
<div className="upload-area">
<input
type="file"
id="client-file-upload"
accept=".apk,.exe,.dmg,.deb,.rpm,.pkg,.msi,.zip,.tar.gz"
onChange={handleFileUpload}
disabled={uploadingFile || !formData.platform_code}
style={{ display: 'none' }}
/>
<label
htmlFor="client-file-upload"
className={`upload-label ${uploadingFile || !formData.platform_code ? 'disabled' : ''}`}
>
<Upload size={20} />
<span>{uploadingFile ? '上传中...' : '选择文件'}</span>
</label>
<p className="upload-hint">
{!formData.platform_code
? '请先选择平台'
: 'APK文件将自动读取版本信息其他文件只读取文件大小'}
</p>
</div>
</div>
<div className="form-row">
<div className="form-group">
<label><Package size={16} /> 版本号 *</label>

View File

@ -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;
}
}

View File

@ -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: (
<div className="tree-node-title">
{node.parent_code === 'ROOT' ? <FolderTree size={14} /> : <FileText size={14} />}
<span>{node.label_cn}</span>
<span className="tree-node-code">({node.dict_code})</span>
</div>
),
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 (
<div className="dict-management">
<div className="dict-header">
<div className="dict-header-left">
<BookText size={24} />
<div>
<h2>字典管理</h2>
<p>管理系统中的码表数据树形结构</p>
</div>
</div>
</div>
<div className="dict-main-layout">
{/* 左侧面板 */}
<div className="dict-left-panel">
<Card
title={
<div className="panel-header">
<FolderTree size={18} />
<span>字典树</span>
</div>
}
extra={
<Button
type="primary"
size="small"
icon={<Plus size={14} />}
onClick={handleAddNode}
>
新增
</Button>
}
bordered={false}
className="dict-tree-card"
>
<div className="dict-type-selector">
<label>字典类型</label>
<Select
value={selectedDictType}
onChange={setSelectedDictType}
style={{ flex: 1 }}
placeholder="选择字典类型"
>
{dictTypes.map(type => (
<Option key={type} value={type}>{type}</Option>
))}
</Select>
</div>
<div className="dict-tree-container">
{treeData.length > 0 ? (
<Tree
showLine
showIcon={false}
treeData={treeData}
onSelect={handleSelectNode}
selectedKeys={selectedNode ? [selectedNode.dict_code] : []}
defaultExpandAll
/>
) : (
<Empty
description="暂无数据"
image={Empty.PRESENTED_IMAGE_SIMPLE}
/>
)}
</div>
</Card>
</div>
{/* 右侧面板 */}
<div className="dict-right-panel">
<Card
title={
<div className="panel-header">
<FileText size={18} />
<span>{selectedNode ? '编辑字典项' : isEditing ? '新增字典项' : '字典详情'}</span>
</div>
}
extra={
isEditing && (
<Space>
{selectedNode && (
<Popconfirm
title="确定要删除此项吗?"
description="删除后将无法恢复"
onConfirm={handleDelete}
okText="确定"
cancelText="取消"
>
<Button
danger
size="small"
icon={<Trash2 size={14} />}
>
删除
</Button>
</Popconfirm>
)}
<Button size="small" icon={<X size={14} />} onClick={handleCancel}>
取消
</Button>
<Button
type="primary"
size="small"
icon={<Save size={14} />}
onClick={handleSave}
>
保存
</Button>
</Space>
)
}
bordered={false}
className="dict-form-card"
>
{isEditing ? (
<Form
form={form}
layout="vertical"
initialValues={{
dict_type: selectedDictType,
parent_code: 'ROOT',
sort_order: 0,
status: 1,
is_default: false
}}
>
<Form.Item
label="字典类型"
name="dict_type"
rules={[{ required: true, message: '请输入字典类型' }]}
>
<Input disabled={!!selectedNode} placeholder="如: client_platform" />
</Form.Item>
<Form.Item
label="编码"
name="dict_code"
rules={[{ required: true, message: '请输入编码' }]}
>
<Input disabled={!!selectedNode} placeholder="如: WIN, MAC, ANDROID" />
</Form.Item>
<Form.Item
label="中文名称"
name="label_cn"
rules={[{ required: true, message: '请输入中文名称' }]}
>
<Input placeholder="如: Windows" />
</Form.Item>
<Form.Item label="英文名称" name="label_en">
<Input placeholder="如: Windows" />
</Form.Item>
<Form.Item
label="父级编码"
name="parent_code"
rules={[{ required: true, message: '请选择父级' }]}
>
<Select placeholder="选择父级">
{getParentOptions().map(opt => (
<Option key={opt.value} value={opt.value}>{opt.label}</Option>
))}
</Select>
</Form.Item>
<Form.Item label="排序" name="sort_order">
<InputNumber min={0} style={{ width: '100%' }} />
</Form.Item>
<Form.Item label="扩展属性JSON格式" name="extension_attr">
<TextArea
rows={4}
placeholder='如: {"suffix": ".exe", "arch_support": ["x86", "x64"]}'
/>
</Form.Item>
<div className="form-inline-group">
<div className="form-inline-item">
<label>是否默认</label>
<Form.Item name="is_default" valuePropName="checked" noStyle>
<Switch checkedChildren="是" unCheckedChildren="否" />
</Form.Item>
</div>
<div className="form-inline-item">
<label>状态</label>
<Form.Item name="status" noStyle>
<Select style={{ width: 120 }}>
<Option value={1}>正常</Option>
<Option value={0}>停用</Option>
</Select>
</Form.Item>
</div>
</div>
</Form>
) : (
<Empty
description="请从左侧树中选择一个节点进行编辑,或点击新增按钮创建新节点"
image={Empty.PRESENTED_IMAGE_SIMPLE}
/>
)}
</Card>
</div>
</div>
</div>
);
};
export default DictManagement;