diff --git a/.DS_Store b/.DS_Store index 6e415a7..55de3c7 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/dist.zip b/dist.zip index 0aa4c9f..7aaf261 100644 Binary files a/dist.zip and b/dist.zip differ diff --git a/src/components/FormModal.css b/src/components/FormModal.css new file mode 100644 index 0000000..dd14f01 --- /dev/null +++ b/src/components/FormModal.css @@ -0,0 +1,196 @@ +/* FormModal 通用模态框样式 */ + +/* 遮罩层 */ +.form-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); + animation: fadeIn 0.2s ease; +} + +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +/* 模态框主体 */ +.form-modal-content { + background: white; + border-radius: 12px; + box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04); + width: 90%; + max-height: 85vh; + display: flex; + flex-direction: column; + overflow: hidden; + animation: slideUp 0.3s ease; +} + +@keyframes slideUp { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +/* 尺寸变体 */ +.form-modal-small { + max-width: 500px; +} + +.form-modal-medium { + max-width: 700px; +} + +.form-modal-large { + max-width: 900px; +} + +/* 模态框头部 */ +.form-modal-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 1.5rem 2rem; + border-bottom: 1px solid #e2e8f0; + flex-shrink: 0; +} + +.form-modal-header-left { + display: flex; + align-items: center; + gap: 1.5rem; + flex: 1; + min-width: 0; /* 允许flex项目收缩 */ +} + +.form-modal-header h2 { + margin: 0; + font-size: 1.25rem; + font-weight: 600; + color: #1e293b; + white-space: nowrap; /* 防止标题换行 */ +} + +.form-modal-header-extra { + display: flex; + align-items: center; + gap: 0.75rem; + flex-shrink: 0; +} + +.form-modal-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; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.form-modal-close-btn:hover { + background: #e2e8f0; + color: #1e293b; +} + +/* 模态框主体内容 - 可滚动区域 */ +.form-modal-body { + padding: 1.5rem 2rem; + overflow-y: auto; + flex: 1; + min-height: 0; /* 重要:允许flex项目正确滚动 */ +} + +/* 模态框底部操作区 */ +.form-modal-actions { + padding: 1rem 2rem; + border-top: 1px solid #e2e8f0; + display: flex; + gap: 1rem; + justify-content: flex-end; + flex-shrink: 0; +} + +/* 滚动条样式 */ +.form-modal-body::-webkit-scrollbar { + width: 6px; +} + +.form-modal-body::-webkit-scrollbar-track { + background: #f1f5f9; + border-radius: 3px; +} + +.form-modal-body::-webkit-scrollbar-thumb { + background: #cbd5e1; + border-radius: 3px; +} + +.form-modal-body::-webkit-scrollbar-thumb:hover { + background: #94a3b8; +} + +/* 响应式设计 */ +@media (max-width: 768px) { + .form-modal-content { + width: 95%; + max-height: 90vh; + } + + .form-modal-header { + padding: 1rem 1.5rem; + } + + .form-modal-header h2 { + font-size: 1.125rem; + } + + .form-modal-body { + padding: 1rem 1.5rem; + } + + .form-modal-actions { + padding: 0.75rem 1.5rem; + } + + /* 小屏幕下标题和额外内容垂直排列 */ + .form-modal-header-left { + flex-direction: column; + align-items: flex-start; + gap: 0.75rem; + } +} + +/* 低分辨率优化 */ +@media (max-height: 700px) { + .form-modal-content { + max-height: 90vh; + } + + .form-modal-body { + padding: 1rem 2rem; + } +} diff --git a/src/components/FormModal.jsx b/src/components/FormModal.jsx new file mode 100644 index 0000000..e95c3f4 --- /dev/null +++ b/src/components/FormModal.jsx @@ -0,0 +1,67 @@ +import React from 'react'; +import { X } from 'lucide-react'; +import './FormModal.css'; + +/** + * 通用表单模态框组件 + * @param {boolean} isOpen - 是否显示模态框 + * @param {function} onClose - 关闭回调 + * @param {string} title - 标题 + * @param {React.ReactNode} children - 表单内容 + * @param {React.ReactNode} actions - 底部操作按钮 + * @param {string} size - 尺寸 'small' | 'medium' | 'large' + * @param {React.ReactNode} headerExtra - 标题栏额外内容(如步骤指示器) + * @param {string} className - 自定义类名 + */ +const FormModal = ({ + isOpen, + onClose, + title, + children, + actions, + size = 'medium', + headerExtra = null, + className = '' +}) => { + if (!isOpen) return null; + + const sizeClass = `form-modal-${size}`; + + return ( +
+
e.stopPropagation()} + > + {/* 模态框头部 */} +
+
+

{title}

+ {headerExtra &&
{headerExtra}
} +
+ +
+ + {/* 模态框主体内容 */} +
+ {children} +
+ + {/* 模态框底部操作区 */} + {actions && ( +
+ {actions} +
+ )} +
+
+ ); +}; + +export default FormModal; diff --git a/src/components/StepIndicator.css b/src/components/StepIndicator.css new file mode 100644 index 0000000..7a683fd --- /dev/null +++ b/src/components/StepIndicator.css @@ -0,0 +1,81 @@ +.step-indicator { + display: flex; + align-items: center; + gap: 12px; + padding: 0; + margin: 0; +} + +.step-item { + display: flex; + align-items: center; + gap: 6px; + transition: all 0.3s ease; +} + +.step-number { + display: inline-flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + border-radius: 50%; + background-color: #e2e8f0; + color: #64748b; + font-size: 12px; + font-weight: 600; + transition: all 0.3s ease; +} + +.step-label { + font-size: 14px; + color: #64748b; + font-weight: 500; + transition: all 0.3s ease; +} + +.step-item.active .step-number { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; +} + +.step-item.active .step-label { + color: #667eea; + font-weight: 600; +} + +.step-item.completed .step-number { + background-color: #10b981; + color: white; +} + +.step-item.completed .step-label { + color: #10b981; +} + +.step-arrow { + color: #cbd5e1; + font-size: 16px; + font-weight: 300; +} + +/* 响应式设计 */ +@media (max-width: 768px) { + .step-indicator { + gap: 8px; + } + + .step-number { + width: 20px; + height: 20px; + font-size: 11px; + } + + .step-label { + font-size: 12px; + } + + .step-arrow { + font-size: 14px; + } +} diff --git a/src/components/StepIndicator.jsx b/src/components/StepIndicator.jsx new file mode 100644 index 0000000..270478c --- /dev/null +++ b/src/components/StepIndicator.jsx @@ -0,0 +1,22 @@ +import React from 'react'; +import './StepIndicator.css'; + +const StepIndicator = ({ steps, currentStep }) => { + return ( +
+ {steps.map((step, index) => ( + +
index + 1 ? 'completed' : ''}`}> + {index + 1} + {step} +
+ {index < steps.length - 1 && ( +
+ )} +
+ ))} +
+ ); +}; + +export default StepIndicator; diff --git a/src/components/TagEditor.jsx b/src/components/TagEditor.jsx index fe91b4f..9d6b5cf 100644 --- a/src/components/TagEditor.jsx +++ b/src/components/TagEditor.jsx @@ -1,7 +1,7 @@ import React, { useState, useEffect, useRef } from 'react'; import { Tag, X, Plus } from 'lucide-react'; import apiClient from '../utils/apiClient'; -import { buildApiUrl } from '../config/api'; +import { buildApiUrl, API_ENDPOINTS } from '../config/api'; import './TagEditor.css'; const TagEditor = ({ @@ -49,7 +49,7 @@ const TagEditor = ({ const fetchAvailableTags = async () => { try { - const response = await apiClient.get(buildApiUrl('/api/tags/')); + const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.TAGS.LIST)); setAvailableTags(response.data.map(tag => tag.name)); } catch (err) { console.error('Error fetching tags:', err); diff --git a/src/pages/ClientManagement.jsx b/src/pages/ClientManagement.jsx index c6d9e6c..600615b 100644 --- a/src/pages/ClientManagement.jsx +++ b/src/pages/ClientManagement.jsx @@ -10,19 +10,25 @@ import { Search, X, ChevronDown, - ChevronUp + ChevronUp, + Package, + Hash, + Link, + FileText, + HardDrive } from 'lucide-react'; import apiClient from '../utils/apiClient'; import { buildApiUrl, API_ENDPOINTS } from '../config/api'; import ConfirmDialog from '../components/ConfirmDialog'; +import FormModal from '../components/FormModal'; import Toast from '../components/Toast'; 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 [showClientModal, setShowClientModal] = useState(false); + const [isEditing, setIsEditing] = useState(false); const [deleteConfirmInfo, setDeleteConfirmInfo] = useState(null); const [selectedClient, setSelectedClient] = useState(null); const [filterPlatformType, setFilterPlatformType] = useState(''); @@ -109,9 +115,9 @@ const ClientManagement = ({ user }) => { } await apiClient.post(buildApiUrl(API_ENDPOINTS.CLIENT_DOWNLOADS.CREATE), payload); - setShowCreateModal(false); - resetForm(); + handleCloseModal(); fetchClients(); + showToast('客户端创建成功', 'success'); } catch (error) { console.error('创建客户端失败:', error); showToast(error.response?.data?.message || '创建失败,请重试', 'error'); @@ -152,9 +158,9 @@ const ClientManagement = ({ user }) => { buildApiUrl(API_ENDPOINTS.CLIENT_DOWNLOADS.UPDATE(selectedClient.id)), payload ); - setShowEditModal(false); - resetForm(); + handleCloseModal(); fetchClients(); + showToast('客户端更新成功', 'success'); } catch (error) { console.error('更新客户端失败:', error); showToast(error.response?.data?.message || '更新失败,请重试', 'error'); @@ -175,21 +181,62 @@ const ClientManagement = ({ user }) => { } }; + const handleOpenModal = (client = null) => { + if (client) { + setIsEditing(true); + 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 || '' + }); + } else { + setIsEditing(false); + setSelectedClient(null); + 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: '' + }); + } + setShowClientModal(true); + }; + + const handleCloseModal = () => { + setShowClientModal(false); + setIsEditing(false); + setSelectedClient(null); + resetForm(); + }; + + const handleSave = async () => { + if (isEditing) { + await handleUpdate(); + } else { + await handleCreate(); + } + }; + + const handleInputChange = (field, value) => { + setFormData(prev => ({ ...prev, [field]: value })); + }; + 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); + handleOpenModal(client); }; const openDeleteModal = (client) => { @@ -263,7 +310,7 @@ const ClientManagement = ({ user }) => {

客户端下载管理

- @@ -404,191 +451,162 @@ const ClientManagement = ({ user }) => { })}
- {/* 创建/编辑模态框 */} - {(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" - /> -
+ {/* 客户端表单模态框 */} + + + + + } + > + {formData && ( + <> +
+
+ +
- + + +
+
+ +
+
+ setFormData({ ...formData, download_url: e.target.value })} + type="text" + placeholder="例如: 1.0.0" + value={formData.version} + onChange={(e) => handleInputChange('version', 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 })} - /> -
-
-
- -