diff --git a/src/App.jsx b/src/App.jsx index 30081c1..f1a2910 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -8,8 +8,10 @@ import MeetingDetails from './pages/MeetingDetails'; import CreateMeeting from './pages/CreateMeeting'; import EditMeeting from './pages/EditMeeting'; import AdminManagement from './pages/AdminManagement'; +import PromptManagementPage from './pages/PromptManagementPage'; import KnowledgeBasePage from './pages/KnowledgeBasePage'; import EditKnowledgeBase from './pages/EditKnowledgeBase'; +import ClientDownloadPage from './pages/ClientDownloadPage'; import './App.css'; function App() { @@ -83,12 +85,16 @@ function App() { : } /> + : + } /> : } /> : } /> + } /> diff --git a/src/components/ClientDownloads.css b/src/components/ClientDownloads.css new file mode 100644 index 0000000..d536da0 --- /dev/null +++ b/src/components/ClientDownloads.css @@ -0,0 +1,150 @@ +/* 客户端下载组件样式 */ +.client-downloads-section { + background: white; + border-radius: 12px; + padding: 2rem; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + margin-bottom: 2rem; +} + +.section-header { + margin-bottom: 2rem; + text-align: center; +} + +.section-header h2 { + font-size: 1.75rem; + font-weight: 600; + color: #1e293b; + margin: 0 0 0.5rem 0; +} + +.section-header p { + color: #64748b; + margin: 0; +} + +.downloads-container { + display: flex; + flex-direction: column; + gap: 2rem; +} + +.platform-group { + background: #f8fafc; + border-radius: 10px; + padding: 1.5rem; +} + +.group-header { + display: flex; + align-items: center; + gap: 0.75rem; + margin-bottom: 1.25rem; + color: #667eea; +} + +.group-header h3 { + margin: 0; + font-size: 1.25rem; + font-weight: 600; + color: #1e293b; +} + +.clients-list { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 1rem; +} + +.client-download-card { + display: flex; + align-items: center; + gap: 1rem; + padding: 1.25rem; + background: white; + border: 1px solid #e2e8f0; + border-radius: 10px; + text-decoration: none; + color: inherit; + transition: all 0.2s ease; + cursor: pointer; +} + +.client-download-card:hover { + border-color: #667eea; + box-shadow: 0 4px 12px rgba(102, 126, 234, 0.15); + transform: translateY(-2px); +} + +.card-icon { + flex-shrink: 0; + width: 48px; + height: 48px; + display: flex; + align-items: center; + justify-content: center; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + border-radius: 12px; +} + +.card-info { + flex: 1; + min-width: 0; +} + +.card-info h4 { + margin: 0 0 0.5rem 0; + font-size: 1rem; + font-weight: 600; + color: #1e293b; +} + +.version-info { + display: flex; + align-items: center; + gap: 0.75rem; + margin-bottom: 0.25rem; +} + +.version { + font-size: 0.875rem; + color: #667eea; + font-weight: 500; +} + +.file-size { + font-size: 0.75rem; + color: #94a3b8; +} + +.system-req { + margin: 0; + font-size: 0.75rem; + color: #64748b; +} + +.download-icon { + flex-shrink: 0; + color: #667eea; +} + +.loading-message, +.empty-message { + text-align: center; + padding: 3rem; + color: #94a3b8; + font-size: 1rem; +} + +/* 响应式设计 */ +@media (max-width: 768px) { + .clients-list { + grid-template-columns: 1fr; + } + + .client-download-card { + padding: 1rem; + } +} diff --git a/src/components/ClientDownloads.jsx b/src/components/ClientDownloads.jsx new file mode 100644 index 0000000..fcc67f1 --- /dev/null +++ b/src/components/ClientDownloads.jsx @@ -0,0 +1,170 @@ +import React, { useState, useEffect } from 'react'; +import { Download, Smartphone, Monitor, Apple, ChevronRight } from 'lucide-react'; +import apiClient from '../utils/apiClient'; +import { buildApiUrl, API_ENDPOINTS } from '../config/api'; +import './ClientDownloads.css'; + +const ClientDownloads = () => { + const [clients, setClients] = useState({ + mobile: [], + desktop: [] + }); + const [loading, setLoading] = useState(true); + + useEffect(() => { + fetchLatestClients(); + }, []); + + const fetchLatestClients = async () => { + setLoading(true); + try { + const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.CLIENT_DOWNLOADS.LATEST)); + console.log('Latest clients response:', response); + setClients(response.data || { mobile: [], desktop: [] }); + } catch (error) { + console.error('获取客户端下载失败:', error); + } finally { + setLoading(false); + } + }; + + const getPlatformIcon = (platformName) => { + switch (platformName) { + case 'ios': + return ; + case 'android': + return ; + case 'mac_intel': + case 'mac_m': + return ; + default: + return ; + } + }; + + const getPlatformLabel = (platformName) => { + const labels = { + ios: 'iOS', + android: 'Android', + windows: 'Windows', + mac_intel: 'Mac (Intel)', + mac_m: 'Mac (M系列)', + linux: 'Linux' + }; + return labels[platformName] || platformName; + }; + + const formatFileSize = (bytes) => { + if (!bytes) return ''; + const mb = bytes / (1024 * 1024); + return `${mb.toFixed(0)} MB`; + }; + + if (loading) { + return ( +
+
+

下载客户端

+
+
加载中...
+
+ ); + } + + return ( +
+
+

下载客户端

+

选择适合您设备的版本

+
+ +
+ {/* 移动端 */} + {clients.mobile && clients.mobile.length > 0 && ( + + )} + + {/* 桌面端 */} + {clients.desktop && clients.desktop.length > 0 && ( + + )} +
+ + {!clients.mobile?.length && !clients.desktop?.length && ( +
暂无可用的客户端下载
+ )} +
+ ); +}; + +export default ClientDownloads; diff --git a/src/config/api.js b/src/config/api.js index 8307de5..7ca43e7 100644 --- a/src/config/api.js +++ b/src/config/api.js @@ -55,6 +55,15 @@ const API_CONFIG = { UPDATE: (kbId) => `/api/knowledge-bases/${kbId}`, DELETE: (kbId) => `/api/knowledge-bases/${kbId}`, TASK_STATUS: (taskId) => `/api/knowledge-bases/tasks/${taskId}` + }, + CLIENT_DOWNLOADS: { + LIST: '/api/clients/downloads', + LATEST: '/api/clients/downloads/latest', + LATEST_BY_PLATFORM: (platformName) => `/api/clients/downloads/${platformName}/latest`, + DETAIL: (id) => `/api/clients/downloads/${id}`, + CREATE: '/api/clients/downloads', + UPDATE: (id) => `/api/clients/downloads/${id}`, + DELETE: (id) => `/api/clients/downloads/${id}` } } }; diff --git a/src/pages/AdminManagement.jsx b/src/pages/AdminManagement.jsx index 3c46d03..00742f7 100644 --- a/src/pages/AdminManagement.jsx +++ b/src/pages/AdminManagement.jsx @@ -1,10 +1,10 @@ import React from 'react'; -import { MessageSquare, Settings, Users, BookText } from 'lucide-react'; +import { MessageSquare, Settings, Users, Smartphone } from 'lucide-react'; import { useNavigate } from 'react-router-dom'; import { Tabs } from 'antd'; import UserManagement from './admin/UserManagement'; import SystemConfiguration from './admin/SystemConfiguration'; -import PromptManagement from './admin/PromptManagement'; +import ClientManagement from './ClientManagement'; import './AdminManagement.css'; const { TabPane } = Tabs; @@ -30,23 +30,23 @@ const AdminManagement = () => {
- 用户管理} + 用户管理} key="userManagement" > - 系统配置} + 系统配置} key="systemConfiguration" > - 提示词仓库} - key="promptManagement" + 客户端管理} + key="clientManagement" > - +
diff --git a/src/pages/ClientDownloadPage.css b/src/pages/ClientDownloadPage.css new file mode 100644 index 0000000..670198e --- /dev/null +++ b/src/pages/ClientDownloadPage.css @@ -0,0 +1,61 @@ +/* 客户端下载页面样式 */ +.client-download-page { + min-height: 100vh; + background: #f8fafc; + display: flex; + flex-direction: column; +} + +.download-page-header { + background: white; + border-bottom: 1px solid #e2e8f0; + padding: 1.5rem 2rem; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); +} + +.download-page-header .header-content { + max-width: 1200px; + margin: 0 auto; +} + +.download-page-header .logo { + display: flex; + align-items: center; + gap: 0.75rem; +} + +.download-page-header .logo-icon { + width: 32px; + height: 32px; + color: #667eea; +} + +.download-page-header .logo-text { + font-size: 1.5rem; + font-weight: 700; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.download-page-content { + flex: 1; + max-width: 1200px; + margin: 0 auto; + width: 100%; + padding: 2rem; +} + +.download-page-footer { + background: white; + border-top: 1px solid #e2e8f0; + padding: 2rem; + text-align: center; + color: #64748b; + font-size: 0.875rem; +} + +.download-page-footer p { + margin: 0.25rem 0; +} diff --git a/src/pages/ClientDownloadPage.jsx b/src/pages/ClientDownloadPage.jsx new file mode 100644 index 0000000..04d69ad --- /dev/null +++ b/src/pages/ClientDownloadPage.jsx @@ -0,0 +1,37 @@ +import React from 'react'; +import { Brain } from 'lucide-react'; +import { useNavigate } from 'react-router-dom'; +import ClientDownloads from '../components/ClientDownloads'; +import './ClientDownloadPage.css'; + +const ClientDownloadPage = () => { + const navigate = useNavigate(); + + const handleLogoClick = () => { + navigate('/'); + }; + + return ( +
+
+
+
+ + iMeeting +
+
+
+ +
+ +
+ +
+

© 2025 紫光汇智信息技术有限公司. All rights reserved.

+

备案号:渝ICP备2023007695号-11

+
+
+ ); +}; + +export default ClientDownloadPage; diff --git a/src/pages/ClientManagement.css b/src/pages/ClientManagement.css new file mode 100644 index 0000000..e10e0cd --- /dev/null +++ b/src/pages/ClientManagement.css @@ -0,0 +1,543 @@ +/* 客户端管理页面样式 */ +.client-management-page { + min-height: 100vh; + background: #f8fafc; + padding: 2rem; +} + +.client-management-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 2rem; +} + +.client-management-header h1 { + font-size: 1.75rem; + font-weight: 600; + color: #1e293b; + margin: 0; +} + +.btn-create { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.75rem 1.5rem; + background: #667eea; + color: white; + border: none; + border-radius: 8px; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; +} + +.btn-create:hover { + background: #5568d3; + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3); +} + +/* 过滤器区域 */ +.client-filters { + display: flex; + gap: 1rem; + margin-bottom: 2rem; + flex-wrap: wrap; +} + +.search-box { + flex: 1; + min-width: 300px; + position: relative; + display: flex; + align-items: center; + background: white; + border: 1px solid #e2e8f0; + border-radius: 8px; + padding: 0.75rem 1rem; +} + +.search-box svg:first-child { + color: #94a3b8; + margin-right: 0.75rem; +} + +.search-box input { + flex: 1; + border: none; + outline: none; + font-size: 0.95rem; +} + +.clear-search { + background: none; + border: none; + color: #94a3b8; + cursor: pointer; + padding: 0.25rem; + display: flex; + align-items: center; + transition: color 0.2s ease; +} + +.clear-search:hover { + color: #64748b; +} + +.platform-filters { + display: flex; + gap: 0.5rem; +} + +.filter-btn { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.75rem 1.25rem; + background: white; + border: 1px solid #e2e8f0; + border-radius: 8px; + color: #64748b; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; +} + +.filter-btn:hover { + border-color: #667eea; + color: #667eea; +} + +.filter-btn.active { + background: #667eea; + border-color: #667eea; + color: white; +} + +/* 客户端分组区域 */ +.clients-sections { + display: flex; + flex-direction: column; + gap: 2rem; +} + +.client-section { + background: white; + border-radius: 12px; + padding: 1.5rem; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); +} + +.section-title { + display: flex; + align-items: center; + gap: 0.75rem; + margin: 0 0 1.5rem 0; + font-size: 1.25rem; + font-weight: 600; + color: #1e293b; +} + +.section-title .count { + color: #94a3b8; + font-size: 1rem; + font-weight: 400; +} + +.clients-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(350px, 1fr)); + gap: 1.5rem; +} + +.client-card { + background: #f8fafc; + border: 1px solid #e2e8f0; + border-radius: 10px; + padding: 1.25rem; + transition: all 0.2s ease; +} + +.client-card:hover { + border-color: #667eea; + box-shadow: 0 4px 12px rgba(102, 126, 234, 0.1); +} + +.client-card.inactive { + opacity: 0.6; +} + +.card-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 1rem; + padding-bottom: 1rem; + border-bottom: 1px solid #e2e8f0; +} + +.platform-info { + display: flex; + align-items: center; + gap: 0.75rem; + flex-wrap: wrap; +} + +.platform-info h3 { + margin: 0; + font-size: 1.125rem; + font-weight: 600; + color: #1e293b; +} + +.badge-latest { + padding: 0.25rem 0.75rem; + background: linear-gradient(135deg, #10b981 0%, #059669 100%); + color: white; + font-size: 0.75rem; + border-radius: 12px; + font-weight: 500; +} + +.badge-inactive { + padding: 0.25rem 0.75rem; + background: #ef4444; + color: white; + font-size: 0.75rem; + border-radius: 12px; + font-weight: 500; +} + +.card-actions { + display: flex; + gap: 0.5rem; +} + +.btn-icon { + padding: 0.5rem; + border: none; + border-radius: 6px; + cursor: pointer; + transition: all 0.2s ease; + display: flex; + align-items: center; + justify-content: center; +} + +.btn-edit { + background: #dbeafe; + color: #3b82f6; +} + +.btn-edit:hover { + background: #3b82f6; + color: white; +} + +.btn-delete { + background: #fee2e2; + color: #ef4444; +} + +.btn-delete:hover { + background: #ef4444; + color: white; +} + +.card-body { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.info-row { + display: flex; + justify-content: space-between; + font-size: 0.9rem; +} + +.info-row .label { + color: #64748b; + font-weight: 500; +} + +.info-row .value { + color: #1e293b; + font-weight: 500; +} + +.release-notes { + margin-top: 0.5rem; +} + +.release-notes .notes-header { + display: flex; + align-items: center; + justify-content: space-between; + cursor: pointer; + padding: 0.5rem; + background: white; + border-radius: 6px; + transition: background 0.2s ease; + margin-bottom: 0.5rem; +} + +.release-notes .notes-header:hover { + background: #f8fafc; +} + +.release-notes .label { + display: block; + color: #64748b; + font-weight: 500; + font-size: 0.9rem; +} + +.release-notes p { + margin: 0; + padding: 0.75rem; + background: white; + border-radius: 6px; + color: #475569; + font-size: 0.875rem; + line-height: 1.6; + white-space: pre-wrap; +} + +.download-link a { + display: inline-flex; + align-items: center; + gap: 0.5rem; + color: #667eea; + text-decoration: none; + font-size: 0.875rem; + font-weight: 500; + transition: color 0.2s ease; +} + +.download-link a:hover { + color: #5568d3; + text-decoration: underline; +} + +/* 模态框样式 */ +.modal-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; + backdrop-filter: blur(2px); +} + +.modal-content { + background: white; + border-radius: 12px; + box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1); + width: 90%; + max-width: 700px; + max-height: 90vh; + display: flex; + flex-direction: column; +} + +.modal-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 1.5rem 2rem; + border-bottom: 1px solid #e2e8f0; +} + +.modal-header h2 { + margin: 0; + font-size: 1.25rem; + font-weight: 600; + color: #1e293b; +} + +.close-btn { + padding: 0.5rem; + border: none; + border-radius: 6px; + background: #f1f5f9; + color: #64748b; + cursor: pointer; + font-size: 1.5rem; + line-height: 1; + transition: all 0.2s ease; +} + +.close-btn:hover { + background: #e2e8f0; + color: #1e293b; +} + +.modal-body { + padding: 2rem; + overflow-y: auto; + flex: 1; +} + +.form-row { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 1rem; + margin-bottom: 1.5rem; +} + +.form-group { + margin-bottom: 1.5rem; +} + +.form-group label { + display: block; + margin-bottom: 0.5rem; + font-weight: 500; + color: #1e293b; + font-size: 0.95rem; +} + +.form-group input, +.form-group select, +.form-group textarea { + width: 100%; + padding: 0.75rem; + border: 1px solid #e2e8f0; + border-radius: 8px; + font-family: inherit; + font-size: 0.95rem; + transition: border-color 0.2s ease; + box-sizing: border-box; +} + +/* 数字input保留默认上下调整按钮 */ +.form-group input[type="number"] { + width: 100%; + padding: 0.75rem; + border: 1px solid #e2e8f0; + border-radius: 8px; + font-family: inherit; + font-size: 0.95rem; + transition: border-color 0.2s ease; + box-sizing: border-box; +} + +.form-group input:focus, +.form-group select:focus, +.form-group textarea:focus { + outline: none; + border-color: #667eea; + box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); +} + +.form-group textarea { + resize: vertical; + min-height: 100px; +} + +.checkbox-group label { + display: flex; + align-items: center; + gap: 0.5rem; + cursor: pointer; +} + +.checkbox-group input[type="checkbox"] { + width: auto; + cursor: pointer; +} + +.modal-actions { + padding: 1rem 2rem; + border-top: 1px solid #e2e8f0; + display: flex; + gap: 1rem; + justify-content: flex-end; +} + +.btn-cancel, +.btn-submit { + padding: 0.75rem 1.5rem; + border-radius: 8px; + border: none; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; +} + +.btn-cancel { + background: #f1f5f9; + color: #475569; +} + +.btn-cancel:hover { + background: #e2e8f0; +} + +.btn-submit { + background: #667eea; + color: white; +} + +.btn-submit:hover { + background: #5568d3; + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3); +} + +/* 删除确认模态框 */ +.delete-modal { + background: white; + border-radius: 12px; + padding: 2rem; + max-width: 400px; + width: 90%; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2); +} + +.delete-modal h3 { + margin: 0 0 1rem 0; + color: #1e293b; + font-size: 1.25rem; +} + +.delete-modal p { + margin: 0 0 2rem 0; + color: #64748b; + line-height: 1.6; +} + +.btn-delete { + background: #ef4444; + color: white; +} + +.btn-delete:hover { + background: #dc2626; +} + +/* 空状态 */ +.empty-state { + text-align: center; + padding: 3rem; + color: #94a3b8; + font-size: 1rem; +} + +/* 加载状态 */ +.client-management-loading { + display: flex; + align-items: center; + justify-content: center; + min-height: 100vh; + font-size: 1.125rem; + color: #94a3b8; +} diff --git a/src/pages/ClientManagement.jsx b/src/pages/ClientManagement.jsx new file mode 100644 index 0000000..e6643fd --- /dev/null +++ b/src/pages/ClientManagement.jsx @@ -0,0 +1,599 @@ +import React, { useState, useEffect } from 'react'; +import { + Download, + Plus, + Edit, + Trash2, + Smartphone, + Monitor, + Apple, + Search, + X, + ChevronDown, + ChevronUp +} from 'lucide-react'; +import apiClient from '../utils/apiClient'; +import { buildApiUrl, API_ENDPOINTS } from '../config/api'; +import './ClientManagement.css'; + +const ClientManagement = ({ user }) => { + const [clients, setClients] = useState([]); + const [loading, setLoading] = useState(true); + const [showCreateModal, setShowCreateModal] = useState(false); + const [showEditModal, setShowEditModal] = useState(false); + const [showDeleteModal, setShowDeleteModal] = useState(false); + const [selectedClient, setSelectedClient] = useState(null); + const [filterPlatformType, setFilterPlatformType] = useState(''); + const [searchQuery, setSearchQuery] = useState(''); + const [expandedNotes, setExpandedNotes] = useState({}); + + const [formData, setFormData] = useState({ + platform_type: 'mobile', + platform_name: 'ios', + version: '', + version_code: '', + download_url: '', + file_size: '', + release_notes: '', + is_active: true, + is_latest: false, + 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: } + ] + }; + + useEffect(() => { + fetchClients(); + }, []); + + const fetchClients = async () => { + setLoading(true); + try { + const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.CLIENT_DOWNLOADS.LIST)); + console.log('Client downloads response:', response); + setClients(response.data.clients || []); + } catch (error) { + console.error('获取客户端列表失败:', error); + } finally { + setLoading(false); + } + }; + + const handleCreate = async () => { + try { + // 验证必填字段 + if (!formData.version_code || !formData.version || !formData.download_url) { + alert('请填写所有必填字段'); + return; + } + + const payload = { + ...formData, + version_code: parseInt(formData.version_code, 10), + file_size: formData.file_size ? parseInt(formData.file_size, 10) : null + }; + + // 验证转换后的数字 + if (isNaN(payload.version_code)) { + alert('版本代码必须是有效的数字'); + return; + } + + if (formData.file_size && isNaN(payload.file_size)) { + alert('文件大小必须是有效的数字'); + return; + } + + await apiClient.post(buildApiUrl(API_ENDPOINTS.CLIENT_DOWNLOADS.CREATE), payload); + setShowCreateModal(false); + resetForm(); + fetchClients(); + } catch (error) { + console.error('创建客户端失败:', error); + alert(error.response?.data?.message || '创建失败,请重试'); + } + }; + + const handleUpdate = async () => { + try { + // 验证必填字段 + if (!formData.version_code || !formData.version || !formData.download_url) { + alert('请填写所有必填字段'); + return; + } + + const payload = { + version: formData.version, + version_code: parseInt(formData.version_code, 10), + download_url: formData.download_url, + file_size: formData.file_size ? parseInt(formData.file_size, 10) : null, + release_notes: formData.release_notes, + is_active: formData.is_active, + is_latest: formData.is_latest, + min_system_version: formData.min_system_version + }; + + // 验证转换后的数字 + if (isNaN(payload.version_code)) { + alert('版本代码必须是有效的数字'); + return; + } + + if (formData.file_size && isNaN(payload.file_size)) { + alert('文件大小必须是有效的数字'); + return; + } + + await apiClient.put( + buildApiUrl(API_ENDPOINTS.CLIENT_DOWNLOADS.UPDATE(selectedClient.id)), + payload + ); + setShowEditModal(false); + resetForm(); + fetchClients(); + } catch (error) { + console.error('更新客户端失败:', error); + alert(error.response?.data?.message || '更新失败,请重试'); + } + }; + + const handleDelete = async () => { + try { + await apiClient.delete(buildApiUrl(API_ENDPOINTS.CLIENT_DOWNLOADS.DELETE(selectedClient.id))); + setShowDeleteModal(false); + setSelectedClient(null); + fetchClients(); + } catch (error) { + console.error('删除客户端失败:', error); + alert('删除失败,请重试'); + } + }; + + const openEditModal = (client) => { + setSelectedClient(client); + setFormData({ + platform_type: client.platform_type, + platform_name: client.platform_name, + version: client.version, + version_code: String(client.version_code), + download_url: client.download_url, + file_size: client.file_size ? String(client.file_size) : '', + release_notes: client.release_notes || '', + is_active: client.is_active, + is_latest: client.is_latest, + min_system_version: client.min_system_version || '' + }); + setShowEditModal(true); + }; + + const openDeleteModal = (client) => { + setSelectedClient(client); + setShowDeleteModal(true); + }; + + const resetForm = () => { + setFormData({ + platform_type: 'mobile', + platform_name: 'ios', + version: '', + version_code: '', + download_url: '', + file_size: '', + release_notes: '', + is_active: true, + is_latest: false, + min_system_version: '' + }); + setSelectedClient(null); + }; + + const getPlatformLabel = (platformName) => { + const allOptions = [...platformOptions.mobile, ...platformOptions.desktop]; + const option = allOptions.find(opt => opt.value === platformName); + return option ? option.label : platformName; + }; + + const formatFileSize = (bytes) => { + if (!bytes) return '-'; + const mb = bytes / (1024 * 1024); + return `${mb.toFixed(2)} MB`; + }; + + const toggleNotes = (clientId) => { + setExpandedNotes(prev => ({ + ...prev, + [clientId]: !prev[clientId] + })); + }; + + const filteredClients = clients.filter(client => { + if (filterPlatformType && client.platform_type !== filterPlatformType) { + return false; + } + if (searchQuery) { + const query = searchQuery.toLowerCase(); + return ( + client.version.toLowerCase().includes(query) || + getPlatformLabel(client.platform_name).toLowerCase().includes(query) || + (client.release_notes && client.release_notes.toLowerCase().includes(query)) + ); + } + return true; + }); + + const groupedClients = { + mobile: filteredClients.filter(c => c.platform_type === 'mobile'), + desktop: filteredClients.filter(c => c.platform_type === 'desktop') + }; + + if (loading) { + return
加载中...
; + } + + return ( +
+
+

客户端下载管理

+ +
+ +
+
+ + setSearchQuery(e.target.value)} + /> + {searchQuery && ( + + )} +
+ +
+ + + +
+
+ +
+ {['mobile', 'desktop'].map(type => { + const typeClients = groupedClients[type]; + if (typeClients.length === 0 && filterPlatformType && filterPlatformType !== type) { + return null; + } + + return ( +
+

+ {type === 'mobile' ? : } + {type === 'mobile' ? '移动端' : '桌面端'} + ({typeClients.length}) +

+ + {typeClients.length === 0 ? ( +
暂无客户端
+ ) : ( +
+ {typeClients.map(client => ( +
+
+
+

{getPlatformLabel(client.platform_name)}

+ {client.is_latest && 最新} + {!client.is_active && 未启用} +
+
+ + +
+
+ +
+
+ 版本: + {client.version} +
+
+ 版本代码: + {client.version_code} +
+
+ 文件大小: + {formatFileSize(client.file_size)} +
+ {client.min_system_version && ( +
+ 系统要求: + {client.min_system_version} +
+ )} + {client.release_notes && ( +
+
toggleNotes(client.id)} + style={{ cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }} + > + 更新说明 + {expandedNotes[client.id] ? : } +
+ {expandedNotes[client.id] && ( +

{client.release_notes}

+ )} +
+ )} + +
+
+ ))} +
+ )} +
+ ); + })} +
+ + {/* 创建/编辑模态框 */} + {(showCreateModal || showEditModal) && ( +
{ + setShowCreateModal(false); + setShowEditModal(false); + resetForm(); + }}> +
e.stopPropagation()}> +
+

{showEditModal ? '编辑客户端' : '新增客户端'}

+ +
+ +
+
+
+ + +
+ +
+ + +
+
+ +
+
+ + setFormData({ ...formData, version: e.target.value })} + /> +
+ +
+ + { + const value = e.target.value; + // 只允许空字符串或正整数 + if (value === '' || /^\d+$/.test(value)) { + setFormData({ ...formData, version_code: value }); + } + }} + min="0" + step="1" + /> +
+
+ +
+ + setFormData({ ...formData, download_url: e.target.value })} + /> +
+ +
+
+ + { + const value = e.target.value; + // 只允许空字符串或正整数 + if (value === '' || /^\d+$/.test(value)) { + setFormData({ ...formData, file_size: value }); + } + }} + min="0" + step="1" + /> +
+ +
+ + setFormData({ ...formData, min_system_version: e.target.value })} + /> +
+
+ +
+ +