修复了前端部分显示
parent
e5b04ed2d6
commit
6c549eca15
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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 (
|
||||
<div className="form-modal-overlay" onClick={onClose}>
|
||||
<div
|
||||
className={`form-modal-content ${sizeClass} ${className}`}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* 模态框头部 */}
|
||||
<div className="form-modal-header">
|
||||
<div className="form-modal-header-left">
|
||||
<h2>{title}</h2>
|
||||
{headerExtra && <div className="form-modal-header-extra">{headerExtra}</div>}
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="form-modal-close-btn"
|
||||
aria-label="关闭"
|
||||
>
|
||||
<X size={20} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 模态框主体内容 */}
|
||||
<div className="form-modal-body">
|
||||
{children}
|
||||
</div>
|
||||
|
||||
{/* 模态框底部操作区 */}
|
||||
{actions && (
|
||||
<div className="form-modal-actions">
|
||||
{actions}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FormModal;
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
import React from 'react';
|
||||
import './StepIndicator.css';
|
||||
|
||||
const StepIndicator = ({ steps, currentStep }) => {
|
||||
return (
|
||||
<div className="step-indicator">
|
||||
{steps.map((step, index) => (
|
||||
<React.Fragment key={index}>
|
||||
<div className={`step-item ${currentStep === index + 1 ? 'active' : ''} ${currentStep > index + 1 ? 'completed' : ''}`}>
|
||||
<span className="step-number">{index + 1}</span>
|
||||
<span className="step-label">{step}</span>
|
||||
</div>
|
||||
{index < steps.length - 1 && (
|
||||
<div className="step-arrow">→</div>
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default StepIndicator;
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 }) => {
|
|||
<div className="client-management-page">
|
||||
<div className="client-management-header">
|
||||
<h1>客户端下载管理</h1>
|
||||
<button className="btn-create" onClick={() => setShowCreateModal(true)}>
|
||||
<button className="btn-create" onClick={() => handleOpenModal()}>
|
||||
<Plus size={18} />
|
||||
<span>新增客户端</span>
|
||||
</button>
|
||||
|
|
@ -404,191 +451,162 @@ const ClientManagement = ({ user }) => {
|
|||
})}
|
||||
</div>
|
||||
|
||||
{/* 创建/编辑模态框 */}
|
||||
{(showCreateModal || showEditModal) && (
|
||||
<div className="modal-overlay" onClick={() => {
|
||||
setShowCreateModal(false);
|
||||
setShowEditModal(false);
|
||||
resetForm();
|
||||
}}>
|
||||
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="modal-header">
|
||||
<h2>{showEditModal ? '编辑客户端' : '新增客户端'}</h2>
|
||||
<button
|
||||
className="close-btn"
|
||||
onClick={() => {
|
||||
setShowCreateModal(false);
|
||||
setShowEditModal(false);
|
||||
resetForm();
|
||||
}}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="modal-body">
|
||||
<div className="form-row">
|
||||
<div className="form-group">
|
||||
<label>平台类型 *</label>
|
||||
<select
|
||||
value={formData.platform_type}
|
||||
onChange={(e) => {
|
||||
const newType = e.target.value;
|
||||
setFormData({
|
||||
...formData,
|
||||
platform_type: newType,
|
||||
platform_name: platformOptions[newType][0].value
|
||||
});
|
||||
}}
|
||||
disabled={showEditModal}
|
||||
>
|
||||
<option value="mobile">移动端</option>
|
||||
<option value="desktop">桌面端</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label>具体平台 *</label>
|
||||
<select
|
||||
value={formData.platform_name}
|
||||
onChange={(e) => setFormData({ ...formData, platform_name: e.target.value })}
|
||||
disabled={showEditModal}
|
||||
>
|
||||
{platformOptions[formData.platform_type].map(option => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-row">
|
||||
<div className="form-group">
|
||||
<label>版本号 *</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="例如: 1.0.0"
|
||||
value={formData.version}
|
||||
onChange={(e) => setFormData({ ...formData, version: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label>版本代码 *</label>
|
||||
<input
|
||||
type="number"
|
||||
placeholder="例如: 1000"
|
||||
value={formData.version_code}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
// 只允许空字符串或正整数
|
||||
if (value === '' || /^\d+$/.test(value)) {
|
||||
setFormData({ ...formData, version_code: value });
|
||||
}
|
||||
}}
|
||||
min="0"
|
||||
step="1"
|
||||
/>
|
||||
</div>
|
||||
{/* 客户端表单模态框 */}
|
||||
<FormModal
|
||||
isOpen={showClientModal}
|
||||
onClose={handleCloseModal}
|
||||
title={isEditing ? '编辑客户端' : '新增客户端'}
|
||||
size="large"
|
||||
actions={
|
||||
<>
|
||||
<button type="button" className="btn btn-secondary" onClick={handleCloseModal}>
|
||||
取消
|
||||
</button>
|
||||
<button type="button" className="btn btn-primary" onClick={handleSave}>
|
||||
{isEditing ? '保存' : '创建'}
|
||||
</button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
{formData && (
|
||||
<>
|
||||
<div className="form-row">
|
||||
<div className="form-group">
|
||||
<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);
|
||||
}}
|
||||
disabled={isEditing}
|
||||
>
|
||||
<option value="mobile">移动端</option>
|
||||
<option value="desktop">桌面端</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label>下载链接 *</label>
|
||||
<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>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-row">
|
||||
<div className="form-group">
|
||||
<label><Package size={16} /> 版本号 *</label>
|
||||
<input
|
||||
type="url"
|
||||
placeholder="https://..."
|
||||
value={formData.download_url}
|
||||
onChange={(e) => setFormData({ ...formData, download_url: e.target.value })}
|
||||
type="text"
|
||||
placeholder="例如: 1.0.0"
|
||||
value={formData.version}
|
||||
onChange={(e) => handleInputChange('version', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-row">
|
||||
<div className="form-group">
|
||||
<label>文件大小 (字节)</label>
|
||||
<input
|
||||
type="number"
|
||||
placeholder="例如: 52428800"
|
||||
value={formData.file_size}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
// 只允许空字符串或正整数
|
||||
if (value === '' || /^\d+$/.test(value)) {
|
||||
setFormData({ ...formData, file_size: value });
|
||||
}
|
||||
}}
|
||||
min="0"
|
||||
step="1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label>最低系统版本</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="例如: iOS 13.0"
|
||||
value={formData.min_system_version}
|
||||
onChange={(e) => setFormData({ ...formData, min_system_version: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label>更新说明</label>
|
||||
<textarea
|
||||
rows={6}
|
||||
placeholder="请输入更新说明..."
|
||||
value={formData.release_notes}
|
||||
onChange={(e) => setFormData({ ...formData, release_notes: e.target.value })}
|
||||
<label><Hash size={16} /> 版本代码 *</label>
|
||||
<input
|
||||
type="number"
|
||||
placeholder="例如: 1000"
|
||||
value={formData.version_code}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
if (value === '' || /^\d+$/.test(value)) {
|
||||
handleInputChange('version_code', value);
|
||||
}
|
||||
}}
|
||||
min="0"
|
||||
step="1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label><Link size={16} /> 下载链接 *</label>
|
||||
<input
|
||||
type="url"
|
||||
placeholder="https://..."
|
||||
value={formData.download_url}
|
||||
onChange={(e) => handleInputChange('download_url', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-row">
|
||||
<div className="form-group">
|
||||
<label><HardDrive size={16} /> 文件大小 (字节)</label>
|
||||
<input
|
||||
type="number"
|
||||
placeholder="例如: 52428800"
|
||||
value={formData.file_size}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
if (value === '' || /^\d+$/.test(value)) {
|
||||
handleInputChange('file_size', value);
|
||||
}
|
||||
}}
|
||||
min="0"
|
||||
step="1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-row">
|
||||
<div className="form-group checkbox-group">
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.is_active}
|
||||
onChange={(e) => setFormData({ ...formData, is_active: e.target.checked })}
|
||||
/>
|
||||
<span>启用</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="form-group checkbox-group">
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.is_latest}
|
||||
onChange={(e) => setFormData({ ...formData, is_latest: e.target.checked })}
|
||||
/>
|
||||
<span>设为最新版本</span>
|
||||
</label>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label><Monitor size={16} /> 最低系统版本</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="例如: iOS 13.0"
|
||||
value={formData.min_system_version}
|
||||
onChange={(e) => handleInputChange('min_system_version', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="modal-actions">
|
||||
<button
|
||||
className="btn-cancel"
|
||||
onClick={() => {
|
||||
setShowCreateModal(false);
|
||||
setShowEditModal(false);
|
||||
resetForm();
|
||||
}}
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
className="btn-submit"
|
||||
onClick={showEditModal ? handleUpdate : handleCreate}
|
||||
>
|
||||
{showEditModal ? '保存' : '创建'}
|
||||
</button>
|
||||
<div className="form-group">
|
||||
<label><FileText size={16} /> 更新说明</label>
|
||||
<textarea
|
||||
rows={6}
|
||||
placeholder="请输入更新说明..."
|
||||
value={formData.release_notes}
|
||||
onChange={(e) => handleInputChange('release_notes', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="form-row">
|
||||
<div className="form-group checkbox-group">
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.is_active}
|
||||
onChange={(e) => handleInputChange('is_active', e.target.checked)}
|
||||
/>
|
||||
<span>启用</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="form-group checkbox-group">
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.is_latest}
|
||||
onChange={(e) => handleInputChange('is_latest', e.target.checked)}
|
||||
/>
|
||||
<span>设为最新版本</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</FormModal>
|
||||
|
||||
{/* 删除确认对话框 */}
|
||||
<ConfirmDialog
|
||||
|
|
|
|||
|
|
@ -777,8 +777,7 @@
|
|||
}
|
||||
|
||||
.meeting-list {
|
||||
max-height: calc(85vh - 380px); /* 动态计算高度,适配不同分辨率 */
|
||||
min-height: 200px; /* 设置最小高度 */
|
||||
height: 380px; /* 固定高度 */
|
||||
overflow-y: auto;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ import ContentViewer from '../components/ContentViewer';
|
|||
import TagDisplay from '../components/TagDisplay';
|
||||
import Toast from '../components/Toast';
|
||||
import ConfirmDialog from '../components/ConfirmDialog';
|
||||
import FormModal from '../components/FormModal';
|
||||
import StepIndicator from '../components/StepIndicator';
|
||||
import SimpleSearchInput from '../components/SimpleSearchInput';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import rehypeRaw from 'rehype-raw';
|
||||
|
|
@ -238,6 +240,36 @@ const KnowledgeBasePage = ({ user }) => {
|
|||
setSelectedTags([]);
|
||||
};
|
||||
|
||||
const handleOpenCreateModal = () => {
|
||||
setCreateStep(1);
|
||||
setSelectedMeetings([]);
|
||||
setUserPrompt('');
|
||||
setSearchQuery('');
|
||||
setSelectedTags([]);
|
||||
setShowCreateForm(true);
|
||||
};
|
||||
|
||||
const handleCloseCreateModal = () => {
|
||||
setShowCreateForm(false);
|
||||
setCreateStep(1);
|
||||
setSelectedMeetings([]);
|
||||
setUserPrompt('');
|
||||
setSearchQuery('');
|
||||
setSelectedTags([]);
|
||||
};
|
||||
|
||||
const handleNextStep = () => {
|
||||
if (selectedMeetings.length === 0) {
|
||||
showToast('请至少选择一个会议', 'warning');
|
||||
return;
|
||||
}
|
||||
setCreateStep(2);
|
||||
};
|
||||
|
||||
const handlePrevStep = () => {
|
||||
setCreateStep(1);
|
||||
};
|
||||
|
||||
const handleDelete = async (kb) => {
|
||||
setDeleteConfirmInfo({ kb_id: kb.kb_id, title: kb.title });
|
||||
};
|
||||
|
|
@ -564,7 +596,7 @@ const KnowledgeBasePage = ({ user }) => {
|
|||
{!sidebarCollapsed && (
|
||||
<button
|
||||
className="btn-new-kb"
|
||||
onClick={() => setShowCreateForm(true)}
|
||||
onClick={handleOpenCreateModal}
|
||||
title="新增知识条目"
|
||||
>
|
||||
<Plus size={18} />
|
||||
|
|
@ -803,212 +835,187 @@ const KnowledgeBasePage = ({ user }) => {
|
|||
</div>
|
||||
|
||||
{/* 新增知识库表单弹窗 */}
|
||||
{showCreateForm && (
|
||||
<div className="modal-overlay">
|
||||
<div className="modal-content create-kb-modal">
|
||||
<div className="modal-header">
|
||||
<div className="modal-header-left">
|
||||
<h2>新增知识库</h2>
|
||||
{/* 步骤指示器集成到标题栏 */}
|
||||
<div className="header-step-indicator">
|
||||
<span className={`step-tag ${createStep === 1 ? 'active' : 'completed'}`}>
|
||||
{createStep > 1 ? '✓' : '1'} 选择会议
|
||||
</span>
|
||||
<span className="step-arrow">→</span>
|
||||
<span className={`step-tag ${createStep === 2 ? 'active' : ''}`}>
|
||||
2 自定义提示词
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<button onClick={() => {
|
||||
setShowCreateForm(false);
|
||||
setCreateStep(1);
|
||||
setSelectedMeetings([]);
|
||||
setUserPrompt('');
|
||||
setSearchQuery('');
|
||||
setSelectedTags([]);
|
||||
}} className="close-btn">×</button>
|
||||
</div>
|
||||
<FormModal
|
||||
isOpen={showCreateForm}
|
||||
onClose={handleCloseCreateModal}
|
||||
title="新增知识库"
|
||||
size="large"
|
||||
headerExtra={
|
||||
<StepIndicator
|
||||
steps={['选择会议', '自定义提示词']}
|
||||
currentStep={createStep}
|
||||
/>
|
||||
}
|
||||
actions={
|
||||
<>
|
||||
{createStep === 1 ? (
|
||||
<>
|
||||
<button type="button" className="btn btn-secondary" onClick={handleCloseCreateModal}>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-primary"
|
||||
onClick={handleNextStep}
|
||||
disabled={selectedMeetings.length === 0}
|
||||
>
|
||||
下一步
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<button type="button" className="btn btn-secondary" onClick={handlePrevStep}>
|
||||
上一步
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-primary"
|
||||
onClick={handleGenerate}
|
||||
disabled={generating}
|
||||
>
|
||||
{generating ? `生成中... ${progress}%` : '生成知识库'}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
>
|
||||
{/* 步骤 1: 选择会议 */}
|
||||
{createStep === 1 && (
|
||||
<div className="form-step">
|
||||
<div className="form-group">
|
||||
{/* 紧凑的搜索和过滤区 */}
|
||||
<div className="search-filter-area">
|
||||
<SimpleSearchInput
|
||||
value={searchQuery}
|
||||
onChange={setSearchQuery}
|
||||
placeholder="搜索会议名称或创建人..."
|
||||
realTimeSearch={true}
|
||||
debounceDelay={500}
|
||||
/>
|
||||
|
||||
<div className="modal-body">
|
||||
{/* 步骤 1: 选择会议 */}
|
||||
{createStep === 1 && (
|
||||
<div className="form-step">
|
||||
<div className="form-group">
|
||||
{/* 紧凑的搜索和过滤区 */}
|
||||
<div className="search-filter-area">
|
||||
<SimpleSearchInput
|
||||
value={searchQuery}
|
||||
onChange={setSearchQuery}
|
||||
placeholder="搜索会议名称或创建人..."
|
||||
realTimeSearch={true}
|
||||
debounceDelay={500}
|
||||
/>
|
||||
|
||||
{availableTags.length > 0 && (
|
||||
<div className="tag-filter-section">
|
||||
<div className="tag-filter-chips">
|
||||
{availableTags.map(tag => (
|
||||
<button
|
||||
key={tag}
|
||||
type="button"
|
||||
className={`tag-chip ${selectedTags.includes(tag) ? 'selected' : ''}`}
|
||||
onClick={() => handleTagToggle(tag)}
|
||||
>
|
||||
{tag}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(searchQuery || selectedTags.length > 0) && (
|
||||
{availableTags.length > 0 && (
|
||||
<div className="tag-filter-section">
|
||||
<div className="tag-filter-chips">
|
||||
{availableTags.map(tag => (
|
||||
<button
|
||||
key={tag}
|
||||
type="button"
|
||||
className="clear-filters-btn"
|
||||
onClick={clearFilters}
|
||||
className={`tag-chip ${selectedTags.includes(tag) ? 'selected' : ''}`}
|
||||
onClick={() => handleTagToggle(tag)}
|
||||
>
|
||||
<X size={14} />
|
||||
清除筛选
|
||||
{tag}
|
||||
</button>
|
||||
)}
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="meeting-list">
|
||||
{loadingMeetings ? (
|
||||
<div className="loading-state">
|
||||
<p>加载中...</p>
|
||||
</div>
|
||||
) : meetings.length === 0 ? (
|
||||
<div className="empty-state">
|
||||
<p>未找到匹配的会议</p>
|
||||
</div>
|
||||
) : (
|
||||
meetings.map(meeting => (
|
||||
<div
|
||||
key={meeting.meeting_id}
|
||||
className={`meeting-item ${selectedMeetings.includes(meeting.meeting_id) ? 'selected' : ''}`}
|
||||
onClick={() => toggleMeetingSelection(meeting.meeting_id)}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedMeetings.includes(meeting.meeting_id)}
|
||||
onChange={(e) => {
|
||||
e.stopPropagation();
|
||||
toggleMeetingSelection(meeting.meeting_id);
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
<div className="meeting-item-content">
|
||||
<div className="meeting-item-title">{meeting.title}</div>
|
||||
<div className="meeting-item-meta">
|
||||
{meeting.creator_username && (
|
||||
<span className="meeting-item-creator">创建人: {meeting.creator_username}</span>
|
||||
)}
|
||||
{meeting.created_at && (
|
||||
<span className="meeting-item-date">创建时间: {formatMeetingDate(meeting.created_at)}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
{(searchQuery || selectedTags.length > 0) && (
|
||||
<button
|
||||
type="button"
|
||||
className="clear-filters-btn"
|
||||
onClick={clearFilters}
|
||||
>
|
||||
<X size={14} />
|
||||
清除筛选
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 分页按钮 */}
|
||||
{!loadingMeetings && meetings.length > 0 && (
|
||||
<div className="pagination-controls">
|
||||
<button
|
||||
className="pagination-btn"
|
||||
onClick={() => handlePageChange(meetingsPagination.page - 1)}
|
||||
disabled={meetingsPagination.page === 1}
|
||||
>
|
||||
<ChevronLeft size={16} />
|
||||
上一页
|
||||
</button>
|
||||
<span className="pagination-info">
|
||||
第 {meetingsPagination.page} 页 · 共 {meetingsPagination.total} 条
|
||||
</span>
|
||||
<button
|
||||
className="pagination-btn"
|
||||
onClick={() => handlePageChange(meetingsPagination.page + 1)}
|
||||
disabled={!meetingsPagination.has_more}
|
||||
>
|
||||
下一页
|
||||
<ChevronRight size={16} />
|
||||
</button>
|
||||
<div className="meeting-list">
|
||||
{loadingMeetings ? (
|
||||
<div className="loading-state">
|
||||
<p>加载中...</p>
|
||||
</div>
|
||||
) : meetings.length === 0 ? (
|
||||
<div className="empty-state">
|
||||
<p>未找到匹配的会议</p>
|
||||
</div>
|
||||
) : (
|
||||
meetings.map(meeting => (
|
||||
<div
|
||||
key={meeting.meeting_id}
|
||||
className={`meeting-item ${selectedMeetings.includes(meeting.meeting_id) ? 'selected' : ''}`}
|
||||
onClick={() => toggleMeetingSelection(meeting.meeting_id)}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedMeetings.includes(meeting.meeting_id)}
|
||||
onChange={(e) => {
|
||||
e.stopPropagation();
|
||||
toggleMeetingSelection(meeting.meeting_id);
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
<div className="meeting-item-content">
|
||||
<div className="meeting-item-title">{meeting.title}</div>
|
||||
<div className="meeting-item-meta">
|
||||
{meeting.creator_username && (
|
||||
<span className="meeting-item-creator">创建人: {meeting.creator_username}</span>
|
||||
)}
|
||||
{meeting.created_at && (
|
||||
<span className="meeting-item-date">创建时间: {formatMeetingDate(meeting.created_at)}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 步骤 2: 输入提示词 */}
|
||||
{createStep === 2 && (
|
||||
<div className="form-step">
|
||||
<div className="step-summary">
|
||||
<div className="summary-item">
|
||||
<span className="summary-label">已选择会议:</span>
|
||||
<span className="summary-value">{selectedMeetings.length} 个</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>用户提示词(可选)</label>
|
||||
<p className="field-hint">您可以添加额外的要求来定制知识库生成内容,例如重点关注某个主题、提取特定信息等。如不填写,系统将使用默认提示词。</p>
|
||||
<textarea
|
||||
placeholder="例如:请重点关注会议中的决策事项和待办任务..."
|
||||
value={userPrompt}
|
||||
onChange={(e) => setUserPrompt(e.target.value)}
|
||||
className="kb-prompt-input"
|
||||
rows={8}
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="modal-actions">
|
||||
{createStep === 1 ? (
|
||||
<>
|
||||
<button className="btn-cancel" onClick={() => {
|
||||
setShowCreateForm(false);
|
||||
setCreateStep(1);
|
||||
setSelectedMeetings([]);
|
||||
setUserPrompt('');
|
||||
setSearchQuery('');
|
||||
setSelectedTags([]);
|
||||
}}>取消</button>
|
||||
{/* 分页按钮 */}
|
||||
{!loadingMeetings && meetings.length > 0 && (
|
||||
<div className="pagination-controls">
|
||||
<button
|
||||
className="btn-primary"
|
||||
onClick={() => {
|
||||
if (selectedMeetings.length === 0) {
|
||||
showToast('请至少选择一个会议', 'warning');
|
||||
return;
|
||||
}
|
||||
setCreateStep(2);
|
||||
}}
|
||||
disabled={selectedMeetings.length === 0}
|
||||
className="pagination-btn"
|
||||
onClick={() => handlePageChange(meetingsPagination.page - 1)}
|
||||
disabled={meetingsPagination.page === 1}
|
||||
>
|
||||
下一步
|
||||
<ChevronLeft size={16} />
|
||||
上一页
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<button className="btn-secondary" onClick={() => setCreateStep(1)}>上一步</button>
|
||||
<span className="pagination-info">
|
||||
第 {meetingsPagination.page} 页 · 共 {meetingsPagination.total} 条
|
||||
</span>
|
||||
<button
|
||||
className="btn-primary"
|
||||
onClick={handleGenerate}
|
||||
disabled={generating}
|
||||
className="pagination-btn"
|
||||
onClick={() => handlePageChange(meetingsPagination.page + 1)}
|
||||
disabled={!meetingsPagination.has_more}
|
||||
>
|
||||
{generating ? `生成中... ${progress}%` : '生成知识库'}
|
||||
下一页
|
||||
<ChevronRight size={16} />
|
||||
</button>
|
||||
</>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
|
||||
{/* 步骤 2: 输入提示词 */}
|
||||
{createStep === 2 && (
|
||||
<div className="form-step">
|
||||
<div className="step-summary">
|
||||
<div className="summary-item">
|
||||
<span className="summary-label">已选择会议:</span>
|
||||
<span className="summary-value">{selectedMeetings.length} 个</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>用户提示词(可选)</label>
|
||||
<p className="field-hint">您可以添加额外的要求来定制知识库生成内容,例如重点关注某个主题、提取特定信息等。如不填写,系统将使用默认提示词。</p>
|
||||
<textarea
|
||||
placeholder="例如:请重点关注会议中的决策事项和待办任务..."
|
||||
value={userPrompt}
|
||||
onChange={(e) => setUserPrompt(e.target.value)}
|
||||
className="kb-prompt-input"
|
||||
rows={8}
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</FormModal>
|
||||
|
||||
{/* 删除确认对话框 */}
|
||||
<ConfirmDialog
|
||||
|
|
|
|||
|
|
@ -157,16 +157,106 @@
|
|||
flex-grow: 1;
|
||||
}
|
||||
|
||||
/* Reusing existing modal and form styles */
|
||||
/* Ensure these are defined globally or copy them here if needed */
|
||||
.modal-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.6); display: flex; align-items: center; justify-content: center; z-index: 1000; backdrop-filter: blur(5px); }
|
||||
.modal-content { background: white; color: #333; padding: 2rem; border-radius: 12px; width: 90%; max-width: 600px; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2); }
|
||||
.modal-content h2 { margin-top: 0; margin-bottom: 1.5rem; }
|
||||
.modal-actions { display: flex; justify-content: flex-end; gap: 1rem; margin-top: 1.5rem; }
|
||||
.form-group { margin-bottom: 1rem; }
|
||||
.form-group label { display: flex; align-items: center; gap: 0.5rem; font-weight: 500; margin-bottom: 0.5rem; }
|
||||
.form-group input[type="text"], .form-group textarea { width: 100%; padding: 0.75rem; border: 1px solid #ced4da; border-radius: 6px; font-size: 1rem; }
|
||||
.form-group textarea { resize: vertical; }
|
||||
/* Form group styles - 配合 FormModal 使用 */
|
||||
.form-group {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.form-group:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-weight: 500;
|
||||
margin-bottom: 0.5rem;
|
||||
color: #1e293b;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.form-group input[type="text"] {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
font-size: 1rem;
|
||||
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.form-group input[type="text"]:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
||||
}
|
||||
|
||||
.form-group textarea {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
font-size: 1rem;
|
||||
font-family: inherit;
|
||||
resize: vertical;
|
||||
min-height: 200px;
|
||||
max-height: 400px;
|
||||
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.form-group textarea:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
||||
}
|
||||
|
||||
/* Error message styling */
|
||||
.error-message {
|
||||
background-color: #fee2e2;
|
||||
color: #dc2626;
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 6px;
|
||||
border-left: 3px solid #ef4444;
|
||||
margin-bottom: 1rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Button styles */
|
||||
.btn {
|
||||
padding: 0.65rem 1.25rem;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
font-size: 0.95rem;
|
||||
transition: all 0.2s ease;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: #667eea;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: #5568d3;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background-color: white;
|
||||
color: #475569;
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background-color: #f8fafc;
|
||||
border-color: #cbd5e1;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
display: flex;
|
||||
|
|
|
|||
|
|
@ -3,8 +3,10 @@ import apiClient from '../../utils/apiClient';
|
|||
import { buildApiUrl, API_ENDPOINTS } from '../../config/api';
|
||||
import { Plus, MoreVertical, Edit, Trash2, BookText, Tag, FileText } from 'lucide-react';
|
||||
import './PromptManagement.css';
|
||||
import TagEditor from '../../components/TagEditor'; // Reusing the TagEditor component
|
||||
import TagEditor from '../../components/TagEditor';
|
||||
import ConfirmDialog from '../../components/ConfirmDialog';
|
||||
import FormModal from '../../components/FormModal';
|
||||
import Toast from '../../components/Toast';
|
||||
|
||||
const PromptManagement = () => {
|
||||
const [prompts, setPrompts] = useState([]);
|
||||
|
|
@ -16,10 +18,21 @@ const PromptManagement = () => {
|
|||
const [showModal, setShowModal] = useState(false);
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [currentPrompt, setCurrentPrompt] = useState(null);
|
||||
const [activeMenu, setActiveMenu] = useState(null); // For dropdown menu
|
||||
const [activeMenu, setActiveMenu] = useState(null);
|
||||
const [deleteConfirmInfo, setDeleteConfirmInfo] = useState(null);
|
||||
const [toasts, setToasts] = useState([]);
|
||||
const menuRef = useRef(null);
|
||||
|
||||
// Toast helper functions
|
||||
const showToast = (message, type = 'info') => {
|
||||
const id = Date.now();
|
||||
setToasts(prev => [...prev, { id, message, type }]);
|
||||
};
|
||||
|
||||
const removeToast = (id) => {
|
||||
setToasts(prev => prev.filter(toast => toast.id !== id));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchPrompts();
|
||||
}, [page, pageSize]);
|
||||
|
|
@ -72,11 +85,13 @@ const PromptManagement = () => {
|
|||
try {
|
||||
if (isEditing) {
|
||||
await apiClient.put(buildApiUrl(API_ENDPOINTS.PROMPTS.UPDATE(currentPrompt.id)), currentPrompt);
|
||||
showToast('提示词更新成功', 'success');
|
||||
} else {
|
||||
await apiClient.post(buildApiUrl(API_ENDPOINTS.PROMPTS.CREATE), currentPrompt);
|
||||
showToast('提示词创建成功', 'success');
|
||||
}
|
||||
handleCloseModal();
|
||||
fetchPrompts(); // Refresh list
|
||||
fetchPrompts();
|
||||
} catch (err) {
|
||||
setError(err.response?.data?.message || '保存失败');
|
||||
}
|
||||
|
|
@ -93,11 +108,14 @@ const PromptManagement = () => {
|
|||
const handleConfirmDelete = async () => {
|
||||
try {
|
||||
await apiClient.delete(buildApiUrl(API_ENDPOINTS.PROMPTS.DELETE(deleteConfirmInfo.id)));
|
||||
fetchPrompts(); // Refresh list
|
||||
setDeleteConfirmInfo(null);
|
||||
fetchPrompts();
|
||||
showToast('提示词删除成功', 'success');
|
||||
} catch (err) {
|
||||
setError(err.response?.data?.message || '删除失败');
|
||||
showToast('删除失败,请重试', 'error');
|
||||
setDeleteConfirmInfo(null);
|
||||
}
|
||||
setDeleteConfirmInfo(null);
|
||||
};
|
||||
|
||||
const handleInputChange = (field, value) => {
|
||||
|
|
@ -153,30 +171,53 @@ const PromptManagement = () => {
|
|||
</>
|
||||
)}
|
||||
|
||||
{showModal && (
|
||||
<div className="modal-overlay">
|
||||
<div className="modal-content">
|
||||
<h2>{isEditing ? '编辑提示词' : '新增提示词'}</h2>
|
||||
{error && <div className="error-message">{error}</div>}
|
||||
<FormModal
|
||||
isOpen={showModal}
|
||||
onClose={handleCloseModal}
|
||||
title={isEditing ? '编辑提示词' : '新增提示词'}
|
||||
size="medium"
|
||||
actions={
|
||||
<>
|
||||
<button type="button" className="btn btn-secondary" onClick={handleCloseModal}>
|
||||
取消
|
||||
</button>
|
||||
<button type="button" className="btn btn-primary" onClick={handleSave}>
|
||||
保存
|
||||
</button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
{error && <div className="error-message">{error}</div>}
|
||||
{currentPrompt && (
|
||||
<>
|
||||
<div className="form-group">
|
||||
<label><FileText size={16} /> 名称</label>
|
||||
<input type="text" value={currentPrompt.name} onChange={(e) => handleInputChange('name', e.target.value)} />
|
||||
<input
|
||||
type="text"
|
||||
value={currentPrompt.name}
|
||||
onChange={(e) => handleInputChange('name', e.target.value)}
|
||||
placeholder="请输入提示词名称"
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label><Tag size={16} /> 标签</label>
|
||||
<TagEditor value={currentPrompt.tags} onChange={(value) => handleInputChange('tags', value)} />
|
||||
<TagEditor
|
||||
value={currentPrompt.tags}
|
||||
onChange={(value) => handleInputChange('tags', value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label><BookText size={16} /> 内容</label>
|
||||
<textarea rows="10" value={currentPrompt.content} onChange={(e) => handleInputChange('content', e.target.value)} />
|
||||
<textarea
|
||||
rows="10"
|
||||
value={currentPrompt.content}
|
||||
onChange={(e) => handleInputChange('content', e.target.value)}
|
||||
placeholder="请输入提示词内容"
|
||||
/>
|
||||
</div>
|
||||
<div className="modal-actions">
|
||||
<button type="button" className="btn btn-secondary" onClick={handleCloseModal}>取消</button>
|
||||
<button type="button" className="btn btn-primary" onClick={handleSave}>保存</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</FormModal>
|
||||
|
||||
{/* 删除提示词确认对话框 */}
|
||||
<ConfirmDialog
|
||||
|
|
@ -189,6 +230,16 @@ const PromptManagement = () => {
|
|||
cancelText="取消"
|
||||
type="danger"
|
||||
/>
|
||||
|
||||
{/* Toast notifications */}
|
||||
{toasts.map(toast => (
|
||||
<Toast
|
||||
key={toast.id}
|
||||
message={toast.message}
|
||||
type={toast.type}
|
||||
onClose={() => removeToast(toast.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import apiClient from '../../utils/apiClient';
|
||||
import { buildApiUrl, API_ENDPOINTS } from '../../config/api';
|
||||
import { Plus, Edit, Trash2, KeyRound } from 'lucide-react';
|
||||
import { Plus, Edit, Trash2, KeyRound, User, Mail, Shield } from 'lucide-react';
|
||||
import ConfirmDialog from '../../components/ConfirmDialog';
|
||||
import FormModal from '../../components/FormModal';
|
||||
import Toast from '../../components/Toast';
|
||||
import './UserManagement.css';
|
||||
|
||||
|
|
@ -13,12 +14,11 @@ const UserManagement = () => {
|
|||
const [pageSize, setPageSize] = useState(10);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState('');
|
||||
const [showAddUserModal, setShowAddUserModal] = useState(false);
|
||||
const [showEditUserModal, setShowEditUserModal] = useState(false);
|
||||
const [showUserModal, setShowUserModal] = useState(false);
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [currentUser, setCurrentUser] = useState(null);
|
||||
const [deleteConfirmInfo, setDeleteConfirmInfo] = useState(null);
|
||||
const [resetConfirmInfo, setResetConfirmInfo] = useState(null);
|
||||
const [newUser, setNewUser] = useState({ username: '', caption: '', email: '', role_id: 2 });
|
||||
const [editingUser, setEditingUser] = useState(null);
|
||||
const [roles, setRoles] = useState([]);
|
||||
const [toasts, setToasts] = useState([]);
|
||||
|
||||
|
|
@ -67,11 +67,11 @@ const UserManagement = () => {
|
|||
const handleAddUser = async (e) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
await apiClient.post(buildApiUrl(API_ENDPOINTS.USERS.CREATE), newUser);
|
||||
setShowAddUserModal(false);
|
||||
setNewUser({ username: '', caption: '', email: '', role_id: 2 });
|
||||
setError(''); // Clear any previous errors
|
||||
fetchUsers(); // Refresh user list
|
||||
await apiClient.post(buildApiUrl(API_ENDPOINTS.USERS.CREATE), currentUser);
|
||||
handleCloseModal();
|
||||
setError('');
|
||||
fetchUsers();
|
||||
showToast('用户添加成功', 'success');
|
||||
} catch (err) {
|
||||
console.error('Error adding user:', err);
|
||||
setError(err.response?.data?.message || '新增用户失败');
|
||||
|
|
@ -81,27 +81,23 @@ const UserManagement = () => {
|
|||
const handleUpdateUser = async (e) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
// 只发送模型期望的字段
|
||||
const updateData = {
|
||||
caption: editingUser.caption,
|
||||
email: editingUser.email,
|
||||
role_id: editingUser.role_id
|
||||
caption: currentUser.caption,
|
||||
email: currentUser.email,
|
||||
role_id: currentUser.role_id
|
||||
};
|
||||
|
||||
// 只有当用户名被修改时才发送
|
||||
if (editingUser.username && editingUser.username.trim()) {
|
||||
updateData.username = editingUser.username;
|
||||
|
||||
if (currentUser.username && currentUser.username.trim()) {
|
||||
updateData.username = currentUser.username;
|
||||
}
|
||||
|
||||
console.log('Sending update data:', updateData); // 调试用
|
||||
|
||||
await apiClient.put(buildApiUrl(API_ENDPOINTS.USERS.UPDATE(editingUser.user_id)), updateData);
|
||||
setShowEditUserModal(false);
|
||||
setError(''); // Clear any previous errors
|
||||
fetchUsers(); // Refresh user list
|
||||
|
||||
await apiClient.put(buildApiUrl(API_ENDPOINTS.USERS.UPDATE(currentUser.user_id)), updateData);
|
||||
handleCloseModal();
|
||||
setError('');
|
||||
fetchUsers();
|
||||
showToast('用户修改成功', 'success');
|
||||
} catch (err) {
|
||||
console.error('Error updating user:', err);
|
||||
console.error('Error response:', err.response?.data); // 调试用
|
||||
setError(err.response?.data?.message || '修改用户失败');
|
||||
}
|
||||
};
|
||||
|
|
@ -111,7 +107,7 @@ const UserManagement = () => {
|
|||
await apiClient.delete(buildApiUrl(API_ENDPOINTS.USERS.DELETE(deleteConfirmInfo.user_id)));
|
||||
setDeleteConfirmInfo(null);
|
||||
showToast('用户删除成功', 'success');
|
||||
fetchUsers(); // Refresh user list
|
||||
fetchUsers();
|
||||
} catch (err) {
|
||||
console.error('Error deleting user:', err);
|
||||
showToast('删除用户失败,请重试', 'error');
|
||||
|
|
@ -131,9 +127,34 @@ const UserManagement = () => {
|
|||
}
|
||||
};
|
||||
|
||||
const openEditModal = (user) => {
|
||||
setEditingUser({ ...user });
|
||||
setShowEditUserModal(true);
|
||||
const handleOpenModal = (user = null) => {
|
||||
if (user) {
|
||||
setIsEditing(true);
|
||||
setCurrentUser({ ...user });
|
||||
} else {
|
||||
setIsEditing(false);
|
||||
setCurrentUser({ username: '', caption: '', email: '', role_id: 2 });
|
||||
}
|
||||
setError('');
|
||||
setShowUserModal(true);
|
||||
};
|
||||
|
||||
const handleCloseModal = () => {
|
||||
setShowUserModal(false);
|
||||
setCurrentUser(null);
|
||||
setError('');
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
if (isEditing) {
|
||||
await handleUpdateUser({ preventDefault: () => {} });
|
||||
} else {
|
||||
await handleAddUser({ preventDefault: () => {} });
|
||||
}
|
||||
};
|
||||
|
||||
const handleInputChange = (field, value) => {
|
||||
setCurrentUser(prev => ({ ...prev, [field]: value }));
|
||||
};
|
||||
|
||||
const openDeleteConfirm = (user) => {
|
||||
|
|
@ -148,7 +169,7 @@ const UserManagement = () => {
|
|||
<div className="user-management">
|
||||
<div className="toolbar">
|
||||
<h2>用户列表</h2>
|
||||
<button className="btn btn-primary" onClick={() => setShowAddUserModal(true)}><Plus size={16} /> 新增用户</button>
|
||||
<button className="btn btn-primary" onClick={() => handleOpenModal()}><Plus size={16} /> 新增用户</button>
|
||||
</div>
|
||||
{loading && <p>加载中...</p>}
|
||||
{error && <p className="error-message">{error}</p>}
|
||||
|
|
@ -176,7 +197,7 @@ const UserManagement = () => {
|
|||
<td>{user.role_name}</td>
|
||||
<td>{new Date(user.created_at).toLocaleString()}</td>
|
||||
<td className="action-cell">
|
||||
<button className="action-btn" onClick={() => openEditModal(user)} title="修改"><Edit size={16} />修改</button>
|
||||
<button className="action-btn" onClick={() => handleOpenModal(user)} title="修改"><Edit size={16} />修改</button>
|
||||
<button className="action-btn btn-danger" onClick={() => openDeleteConfirm(user)} title="删除"><Trash2 size={16} />删除</button>
|
||||
<button className="action-btn btn-warning" onClick={() => openResetConfirm(user)} title="重置密码"><KeyRound size={16} />重置</button>
|
||||
</td>
|
||||
|
|
@ -195,78 +216,75 @@ const UserManagement = () => {
|
|||
</>
|
||||
)}
|
||||
|
||||
{showAddUserModal && (
|
||||
<div className="modal-overlay">
|
||||
<div className="modal-content">
|
||||
<form onSubmit={handleAddUser}>
|
||||
<h2>新增用户</h2>
|
||||
{error && <div className="error-message">{error}</div>}
|
||||
<div className="form-group">
|
||||
<label>用户名</label>
|
||||
<input className="form-input" type="text" value={newUser.username} onChange={(e) => setNewUser({...newUser, username: e.target.value})} required />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>姓名</label>
|
||||
<input className="form-input" type="text" value={newUser.caption} onChange={(e) => setNewUser({...newUser, caption: e.target.value})} required />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>邮箱</label>
|
||||
<input className="form-input" type="email" value={newUser.email} onChange={(e) => setNewUser({...newUser, email: e.target.value})} required />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>角色</label>
|
||||
<select className="form-input" value={newUser.role_id} onChange={(e) => setNewUser({...newUser, role_id: parseInt(e.target.value)})}>
|
||||
{roles.map(role => (
|
||||
<option key={role.role_id} value={role.role_id}>{role.role_name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
{/* 用户表单模态框 */}
|
||||
<FormModal
|
||||
isOpen={showUserModal}
|
||||
onClose={handleCloseModal}
|
||||
title={isEditing ? '编辑用户' : '新增用户'}
|
||||
size="medium"
|
||||
actions={
|
||||
<>
|
||||
<button type="button" className="btn btn-secondary" onClick={handleCloseModal}>
|
||||
取消
|
||||
</button>
|
||||
<button type="button" className="btn btn-primary" onClick={handleSave}>
|
||||
{isEditing ? '确认修改' : '确认新增'}
|
||||
</button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
{error && <div className="error-message">{error}</div>}
|
||||
{currentUser && (
|
||||
<>
|
||||
<div className="form-group">
|
||||
<label><User size={16} /> 用户名</label>
|
||||
<input
|
||||
type="text"
|
||||
value={currentUser.username}
|
||||
onChange={(e) => handleInputChange('username', e.target.value)}
|
||||
placeholder="请输入用户名"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label><User size={16} /> 姓名</label>
|
||||
<input
|
||||
type="text"
|
||||
value={currentUser.caption}
|
||||
onChange={(e) => handleInputChange('caption', e.target.value)}
|
||||
placeholder="请输入姓名"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label><Mail size={16} /> 邮箱</label>
|
||||
<input
|
||||
type="email"
|
||||
value={currentUser.email}
|
||||
onChange={(e) => handleInputChange('email', e.target.value)}
|
||||
placeholder="请输入邮箱"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label><Shield size={16} /> 角色</label>
|
||||
<select
|
||||
value={currentUser.role_id}
|
||||
onChange={(e) => handleInputChange('role_id', parseInt(e.target.value))}
|
||||
>
|
||||
{roles.map(role => (
|
||||
<option key={role.role_id} value={role.role_id}>{role.role_name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
{!isEditing && (
|
||||
<div className="info-note">
|
||||
<p>注:新用户的默认密码为系统配置的默认密码,用户可登录后自行修改</p>
|
||||
</div>
|
||||
<div className="modal-actions">
|
||||
<button type="button" className="btn btn-secondary" onClick={() => {setShowAddUserModal(false); setError('');}}>取消</button>
|
||||
<button type="submit" className="btn btn-primary">确认新增</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showEditUserModal && editingUser && (
|
||||
<div className="modal-overlay">
|
||||
<div className="modal-content">
|
||||
<form onSubmit={handleUpdateUser}>
|
||||
<h2>修改用户</h2>
|
||||
{error && <div className="error-message">{error}</div>}
|
||||
<div className="form-group">
|
||||
<label>用户名</label>
|
||||
<input className="form-input" type="text" value={editingUser.username} onChange={(e) => setEditingUser({...editingUser, username: e.target.value})} required />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>姓名</label>
|
||||
<input className="form-input" type="text" value={editingUser.caption} onChange={(e) => setEditingUser({...editingUser, caption: e.target.value})} required />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>邮箱</label>
|
||||
<input className="form-input" type="email" value={editingUser.email} onChange={(e) => setEditingUser({...editingUser, email: e.target.value})} required />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>角色</label>
|
||||
<select className="form-input" value={editingUser.role_id} onChange={(e) => setEditingUser({...editingUser, role_id: parseInt(e.target.value)})}>
|
||||
{roles.map(role => (
|
||||
<option key={role.role_id} value={role.role_id}>{role.role_name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="modal-actions">
|
||||
<button type="button" className="btn btn-secondary" onClick={() => {setShowEditUserModal(false); setError('');}}>取消</button>
|
||||
<button type="submit" className="btn btn-primary">确认修改</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</FormModal>
|
||||
|
||||
{/* 删除用户确认对话框 */}
|
||||
<ConfirmDialog
|
||||
|
|
|
|||
Loading…
Reference in New Issue