修复了前端部分显示

main
mula.liu 2025-11-12 15:29:05 +08:00
parent e5b04ed2d6
commit 6c549eca15
13 changed files with 1063 additions and 514 deletions

BIN
.DS_Store vendored

Binary file not shown.

BIN
dist.zip

Binary file not shown.

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
import React, { useState, useEffect, useRef } from 'react'; import React, { useState, useEffect, useRef } from 'react';
import { Tag, X, Plus } from 'lucide-react'; import { Tag, X, Plus } from 'lucide-react';
import apiClient from '../utils/apiClient'; import apiClient from '../utils/apiClient';
import { buildApiUrl } from '../config/api'; import { buildApiUrl, API_ENDPOINTS } from '../config/api';
import './TagEditor.css'; import './TagEditor.css';
const TagEditor = ({ const TagEditor = ({
@ -49,7 +49,7 @@ const TagEditor = ({
const fetchAvailableTags = async () => { const fetchAvailableTags = async () => {
try { 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)); setAvailableTags(response.data.map(tag => tag.name));
} catch (err) { } catch (err) {
console.error('Error fetching tags:', err); console.error('Error fetching tags:', err);

View File

@ -10,19 +10,25 @@ import {
Search, Search,
X, X,
ChevronDown, ChevronDown,
ChevronUp ChevronUp,
Package,
Hash,
Link,
FileText,
HardDrive
} from 'lucide-react'; } from 'lucide-react';
import apiClient from '../utils/apiClient'; import apiClient from '../utils/apiClient';
import { buildApiUrl, API_ENDPOINTS } from '../config/api'; import { buildApiUrl, API_ENDPOINTS } from '../config/api';
import ConfirmDialog from '../components/ConfirmDialog'; import ConfirmDialog from '../components/ConfirmDialog';
import FormModal from '../components/FormModal';
import Toast from '../components/Toast'; import Toast from '../components/Toast';
import './ClientManagement.css'; import './ClientManagement.css';
const ClientManagement = ({ user }) => { const ClientManagement = ({ user }) => {
const [clients, setClients] = useState([]); const [clients, setClients] = useState([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [showCreateModal, setShowCreateModal] = useState(false); const [showClientModal, setShowClientModal] = useState(false);
const [showEditModal, setShowEditModal] = useState(false); const [isEditing, setIsEditing] = useState(false);
const [deleteConfirmInfo, setDeleteConfirmInfo] = useState(null); const [deleteConfirmInfo, setDeleteConfirmInfo] = useState(null);
const [selectedClient, setSelectedClient] = useState(null); const [selectedClient, setSelectedClient] = useState(null);
const [filterPlatformType, setFilterPlatformType] = useState(''); const [filterPlatformType, setFilterPlatformType] = useState('');
@ -109,9 +115,9 @@ const ClientManagement = ({ user }) => {
} }
await apiClient.post(buildApiUrl(API_ENDPOINTS.CLIENT_DOWNLOADS.CREATE), payload); await apiClient.post(buildApiUrl(API_ENDPOINTS.CLIENT_DOWNLOADS.CREATE), payload);
setShowCreateModal(false); handleCloseModal();
resetForm();
fetchClients(); fetchClients();
showToast('客户端创建成功', 'success');
} catch (error) { } catch (error) {
console.error('创建客户端失败:', error); console.error('创建客户端失败:', error);
showToast(error.response?.data?.message || '创建失败,请重试', 'error'); showToast(error.response?.data?.message || '创建失败,请重试', 'error');
@ -152,9 +158,9 @@ const ClientManagement = ({ user }) => {
buildApiUrl(API_ENDPOINTS.CLIENT_DOWNLOADS.UPDATE(selectedClient.id)), buildApiUrl(API_ENDPOINTS.CLIENT_DOWNLOADS.UPDATE(selectedClient.id)),
payload payload
); );
setShowEditModal(false); handleCloseModal();
resetForm();
fetchClients(); fetchClients();
showToast('客户端更新成功', 'success');
} catch (error) { } catch (error) {
console.error('更新客户端失败:', error); console.error('更新客户端失败:', error);
showToast(error.response?.data?.message || '更新失败,请重试', '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) => { const openEditModal = (client) => {
setSelectedClient(client); handleOpenModal(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) => { const openDeleteModal = (client) => {
@ -263,7 +310,7 @@ const ClientManagement = ({ user }) => {
<div className="client-management-page"> <div className="client-management-page">
<div className="client-management-header"> <div className="client-management-header">
<h1>客户端下载管理</h1> <h1>客户端下载管理</h1>
<button className="btn-create" onClick={() => setShowCreateModal(true)}> <button className="btn-create" onClick={() => handleOpenModal()}>
<Plus size={18} /> <Plus size={18} />
<span>新增客户端</span> <span>新增客户端</span>
</button> </button>
@ -404,191 +451,162 @@ const ClientManagement = ({ user }) => {
})} })}
</div> </div>
{/* 创建/编辑模态框 */} {/* 客户端表单模态框 */}
{(showCreateModal || showEditModal) && ( <FormModal
<div className="modal-overlay" onClick={() => { isOpen={showClientModal}
setShowCreateModal(false); onClose={handleCloseModal}
setShowEditModal(false); title={isEditing ? '编辑客户端' : '新增客户端'}
resetForm(); size="large"
}}> actions={
<div className="modal-content" onClick={(e) => e.stopPropagation()}> <>
<div className="modal-header"> <button type="button" className="btn btn-secondary" onClick={handleCloseModal}>
<h2>{showEditModal ? '编辑客户端' : '新增客户端'}</h2> 取消
<button </button>
className="close-btn" <button type="button" className="btn btn-primary" onClick={handleSave}>
onClick={() => { {isEditing ? '保存' : '创建'}
setShowCreateModal(false); </button>
setShowEditModal(false); </>
resetForm(); }
}} >
> {formData && (
× <>
</button> <div className="form-row">
</div> <div className="form-group">
<label><Monitor size={16} /> 平台类型 *</label>
<div className="modal-body"> <select
<div className="form-row"> value={formData.platform_type}
<div className="form-group"> onChange={(e) => {
<label>平台类型 *</label> const newType = e.target.value;
<select handleInputChange('platform_type', newType);
value={formData.platform_type} handleInputChange('platform_name', platformOptions[newType][0].value);
onChange={(e) => { }}
const newType = e.target.value; disabled={isEditing}
setFormData({ >
...formData, <option value="mobile">移动端</option>
platform_type: newType, <option value="desktop">桌面端</option>
platform_name: platformOptions[newType][0].value </select>
});
}}
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>
</div> </div>
<div className="form-group"> <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 <input
type="url" type="text"
placeholder="https://..." placeholder="例如: 1.0.0"
value={formData.download_url} value={formData.version}
onChange={(e) => setFormData({ ...formData, download_url: e.target.value })} onChange={(e) => handleInputChange('version', e.target.value)}
/> />
</div> </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"> <div className="form-group">
<label>更新说明</label> <label><Hash size={16} /> 版本代码 *</label>
<textarea <input
rows={6} type="number"
placeholder="请输入更新说明..." placeholder="例如: 1000"
value={formData.release_notes} value={formData.version_code}
onChange={(e) => setFormData({ ...formData, release_notes: e.target.value })} 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>
<div className="form-row"> <div className="form-group">
<div className="form-group checkbox-group"> <label><Monitor size={16} /> 最低系统版本</label>
<label> <input
<input type="text"
type="checkbox" placeholder="例如: iOS 13.0"
checked={formData.is_active} value={formData.min_system_version}
onChange={(e) => setFormData({ ...formData, is_active: e.target.checked })} onChange={(e) => handleInputChange('min_system_version', e.target.value)}
/> />
<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> </div>
</div> </div>
<div className="modal-actions"> <div className="form-group">
<button <label><FileText size={16} /> 更新说明</label>
className="btn-cancel" <textarea
onClick={() => { rows={6}
setShowCreateModal(false); placeholder="请输入更新说明..."
setShowEditModal(false); value={formData.release_notes}
resetForm(); onChange={(e) => handleInputChange('release_notes', e.target.value)}
}} />
>
取消
</button>
<button
className="btn-submit"
onClick={showEditModal ? handleUpdate : handleCreate}
>
{showEditModal ? '保存' : '创建'}
</button>
</div> </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 <ConfirmDialog

View File

@ -777,8 +777,7 @@
} }
.meeting-list { .meeting-list {
max-height: calc(85vh - 380px); /* 动态计算高度,适配不同分辨率 */ height: 380px; /* 固定高度 */
min-height: 200px; /* 设置最小高度 */
overflow-y: auto; overflow-y: auto;
border: 1px solid #e2e8f0; border: 1px solid #e2e8f0;
border-radius: 8px; border-radius: 8px;

View File

@ -7,6 +7,8 @@ import ContentViewer from '../components/ContentViewer';
import TagDisplay from '../components/TagDisplay'; import TagDisplay from '../components/TagDisplay';
import Toast from '../components/Toast'; import Toast from '../components/Toast';
import ConfirmDialog from '../components/ConfirmDialog'; import ConfirmDialog from '../components/ConfirmDialog';
import FormModal from '../components/FormModal';
import StepIndicator from '../components/StepIndicator';
import SimpleSearchInput from '../components/SimpleSearchInput'; import SimpleSearchInput from '../components/SimpleSearchInput';
import remarkGfm from 'remark-gfm'; import remarkGfm from 'remark-gfm';
import rehypeRaw from 'rehype-raw'; import rehypeRaw from 'rehype-raw';
@ -238,6 +240,36 @@ const KnowledgeBasePage = ({ user }) => {
setSelectedTags([]); 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) => { const handleDelete = async (kb) => {
setDeleteConfirmInfo({ kb_id: kb.kb_id, title: kb.title }); setDeleteConfirmInfo({ kb_id: kb.kb_id, title: kb.title });
}; };
@ -564,7 +596,7 @@ const KnowledgeBasePage = ({ user }) => {
{!sidebarCollapsed && ( {!sidebarCollapsed && (
<button <button
className="btn-new-kb" className="btn-new-kb"
onClick={() => setShowCreateForm(true)} onClick={handleOpenCreateModal}
title="新增知识条目" title="新增知识条目"
> >
<Plus size={18} /> <Plus size={18} />
@ -803,212 +835,187 @@ const KnowledgeBasePage = ({ user }) => {
</div> </div>
{/* 新增知识库表单弹窗 */} {/* 新增知识库表单弹窗 */}
{showCreateForm && ( <FormModal
<div className="modal-overlay"> isOpen={showCreateForm}
<div className="modal-content create-kb-modal"> onClose={handleCloseCreateModal}
<div className="modal-header"> title="新增知识库"
<div className="modal-header-left"> size="large"
<h2>新增知识库</h2> headerExtra={
{/* 步骤指示器集成到标题栏 */} <StepIndicator
<div className="header-step-indicator"> steps={['选择会议', '自定义提示词']}
<span className={`step-tag ${createStep === 1 ? 'active' : 'completed'}`}> currentStep={createStep}
{createStep > 1 ? '✓' : '1'} 选择会议 />
</span> }
<span className="step-arrow"></span> actions={
<span className={`step-tag ${createStep === 2 ? 'active' : ''}`}> <>
2 自定义提示词 {createStep === 1 ? (
</span> <>
</div> <button type="button" className="btn btn-secondary" onClick={handleCloseCreateModal}>
</div> 取消
<button onClick={() => { </button>
setShowCreateForm(false); <button
setCreateStep(1); type="button"
setSelectedMeetings([]); className="btn btn-primary"
setUserPrompt(''); onClick={handleNextStep}
setSearchQuery(''); disabled={selectedMeetings.length === 0}
setSelectedTags([]); >
}} className="close-btn">×</button> 下一步
</div> </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"> {availableTags.length > 0 && (
{/* 步骤 1: 选择会议 */} <div className="tag-filter-section">
{createStep === 1 && ( <div className="tag-filter-chips">
<div className="form-step"> {availableTags.map(tag => (
<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) && (
<button <button
key={tag}
type="button" type="button"
className="clear-filters-btn" className={`tag-chip ${selectedTags.includes(tag) ? 'selected' : ''}`}
onClick={clearFilters} onClick={() => handleTagToggle(tag)}
> >
<X size={14} /> {tag}
清除筛选
</button> </button>
)} ))}
</div> </div>
</div>
)}
<div className="meeting-list"> {(searchQuery || selectedTags.length > 0) && (
{loadingMeetings ? ( <button
<div className="loading-state"> type="button"
<p>加载中...</p> className="clear-filters-btn"
</div> onClick={clearFilters}
) : meetings.length === 0 ? ( >
<div className="empty-state"> <X size={14} />
<p>未找到匹配的会议</p> 清除筛选
</div> </button>
) : ( )}
meetings.map(meeting => ( </div>
<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>
{/* 分页按钮 */} <div className="meeting-list">
{!loadingMeetings && meetings.length > 0 && ( {loadingMeetings ? (
<div className="pagination-controls"> <div className="loading-state">
<button <p>加载中...</p>
className="pagination-btn" </div>
onClick={() => handlePageChange(meetingsPagination.page - 1)} ) : meetings.length === 0 ? (
disabled={meetingsPagination.page === 1} <div className="empty-state">
> <p>未找到匹配的会议</p>
<ChevronLeft size={16} /> </div>
上一页 ) : (
</button> meetings.map(meeting => (
<span className="pagination-info"> <div
{meetingsPagination.page} · {meetingsPagination.total} key={meeting.meeting_id}
</span> className={`meeting-item ${selectedMeetings.includes(meeting.meeting_id) ? 'selected' : ''}`}
<button onClick={() => toggleMeetingSelection(meeting.meeting_id)}
className="pagination-btn" >
onClick={() => handlePageChange(meetingsPagination.page + 1)} <input
disabled={!meetingsPagination.has_more} type="checkbox"
> checked={selectedMeetings.includes(meeting.meeting_id)}
下一页 onChange={(e) => {
<ChevronRight size={16} /> e.stopPropagation();
</button> 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>
</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> ))
<div className="form-group"> )}
<label>用户提示词可选</label> </div>
<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 className="modal-actions"> {/* 分页按钮 */}
{createStep === 1 ? ( {!loadingMeetings && meetings.length > 0 && (
<> <div className="pagination-controls">
<button className="btn-cancel" onClick={() => {
setShowCreateForm(false);
setCreateStep(1);
setSelectedMeetings([]);
setUserPrompt('');
setSearchQuery('');
setSelectedTags([]);
}}>取消</button>
<button <button
className="btn-primary" className="pagination-btn"
onClick={() => { onClick={() => handlePageChange(meetingsPagination.page - 1)}
if (selectedMeetings.length === 0) { disabled={meetingsPagination.page === 1}
showToast('请至少选择一个会议', 'warning');
return;
}
setCreateStep(2);
}}
disabled={selectedMeetings.length === 0}
> >
下一步 <ChevronLeft size={16} />
上一页
</button> </button>
</> <span className="pagination-info">
) : ( {meetingsPagination.page} · {meetingsPagination.total}
<> </span>
<button className="btn-secondary" onClick={() => setCreateStep(1)}>上一步</button>
<button <button
className="btn-primary" className="pagination-btn"
onClick={handleGenerate} onClick={() => handlePageChange(meetingsPagination.page + 1)}
disabled={generating} disabled={!meetingsPagination.has_more}
> >
{generating ? `生成中... ${progress}%` : '生成知识库'} 下一页
<ChevronRight size={16} />
</button> </button>
</> </div>
)} )}
</div> </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 <ConfirmDialog

View File

@ -157,16 +157,106 @@
flex-grow: 1; flex-grow: 1;
} }
/* Reusing existing modal and form styles */ /* Form group styles - 配合 FormModal 使用 */
/* Ensure these are defined globally or copy them here if needed */ .form-group {
.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); } margin-bottom: 1.5rem;
.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:last-child {
.form-group { margin-bottom: 1rem; } margin-bottom: 0;
.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 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 { .pagination {
display: flex; display: flex;

View File

@ -3,8 +3,10 @@ import apiClient from '../../utils/apiClient';
import { buildApiUrl, API_ENDPOINTS } from '../../config/api'; import { buildApiUrl, API_ENDPOINTS } from '../../config/api';
import { Plus, MoreVertical, Edit, Trash2, BookText, Tag, FileText } from 'lucide-react'; import { Plus, MoreVertical, Edit, Trash2, BookText, Tag, FileText } from 'lucide-react';
import './PromptManagement.css'; import './PromptManagement.css';
import TagEditor from '../../components/TagEditor'; // Reusing the TagEditor component import TagEditor from '../../components/TagEditor';
import ConfirmDialog from '../../components/ConfirmDialog'; import ConfirmDialog from '../../components/ConfirmDialog';
import FormModal from '../../components/FormModal';
import Toast from '../../components/Toast';
const PromptManagement = () => { const PromptManagement = () => {
const [prompts, setPrompts] = useState([]); const [prompts, setPrompts] = useState([]);
@ -16,10 +18,21 @@ const PromptManagement = () => {
const [showModal, setShowModal] = useState(false); const [showModal, setShowModal] = useState(false);
const [isEditing, setIsEditing] = useState(false); const [isEditing, setIsEditing] = useState(false);
const [currentPrompt, setCurrentPrompt] = useState(null); const [currentPrompt, setCurrentPrompt] = useState(null);
const [activeMenu, setActiveMenu] = useState(null); // For dropdown menu const [activeMenu, setActiveMenu] = useState(null);
const [deleteConfirmInfo, setDeleteConfirmInfo] = useState(null); const [deleteConfirmInfo, setDeleteConfirmInfo] = useState(null);
const [toasts, setToasts] = useState([]);
const menuRef = useRef(null); 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(() => { useEffect(() => {
fetchPrompts(); fetchPrompts();
}, [page, pageSize]); }, [page, pageSize]);
@ -72,11 +85,13 @@ const PromptManagement = () => {
try { try {
if (isEditing) { if (isEditing) {
await apiClient.put(buildApiUrl(API_ENDPOINTS.PROMPTS.UPDATE(currentPrompt.id)), currentPrompt); await apiClient.put(buildApiUrl(API_ENDPOINTS.PROMPTS.UPDATE(currentPrompt.id)), currentPrompt);
showToast('提示词更新成功', 'success');
} else { } else {
await apiClient.post(buildApiUrl(API_ENDPOINTS.PROMPTS.CREATE), currentPrompt); await apiClient.post(buildApiUrl(API_ENDPOINTS.PROMPTS.CREATE), currentPrompt);
showToast('提示词创建成功', 'success');
} }
handleCloseModal(); handleCloseModal();
fetchPrompts(); // Refresh list fetchPrompts();
} catch (err) { } catch (err) {
setError(err.response?.data?.message || '保存失败'); setError(err.response?.data?.message || '保存失败');
} }
@ -93,11 +108,14 @@ const PromptManagement = () => {
const handleConfirmDelete = async () => { const handleConfirmDelete = async () => {
try { try {
await apiClient.delete(buildApiUrl(API_ENDPOINTS.PROMPTS.DELETE(deleteConfirmInfo.id))); await apiClient.delete(buildApiUrl(API_ENDPOINTS.PROMPTS.DELETE(deleteConfirmInfo.id)));
fetchPrompts(); // Refresh list setDeleteConfirmInfo(null);
fetchPrompts();
showToast('提示词删除成功', 'success');
} catch (err) { } catch (err) {
setError(err.response?.data?.message || '删除失败'); setError(err.response?.data?.message || '删除失败');
showToast('删除失败,请重试', 'error');
setDeleteConfirmInfo(null);
} }
setDeleteConfirmInfo(null);
}; };
const handleInputChange = (field, value) => { const handleInputChange = (field, value) => {
@ -153,30 +171,53 @@ const PromptManagement = () => {
</> </>
)} )}
{showModal && ( <FormModal
<div className="modal-overlay"> isOpen={showModal}
<div className="modal-content"> onClose={handleCloseModal}
<h2>{isEditing ? '编辑提示词' : '新增提示词'}</h2> title={isEditing ? '编辑提示词' : '新增提示词'}
{error && <div className="error-message">{error}</div>} 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"> <div className="form-group">
<label><FileText size={16} /> 名称</label> <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>
<div className="form-group"> <div className="form-group">
<label><Tag size={16} /> 标签</label> <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>
<div className="form-group"> <div className="form-group">
<label><BookText size={16} /> 内容</label> <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>
<div className="modal-actions"> </>
<button type="button" className="btn btn-secondary" onClick={handleCloseModal}>取消</button> )}
<button type="button" className="btn btn-primary" onClick={handleSave}>保存</button> </FormModal>
</div>
</div>
</div>
)}
{/* 删除提示词确认对话框 */} {/* 删除提示词确认对话框 */}
<ConfirmDialog <ConfirmDialog
@ -189,6 +230,16 @@ const PromptManagement = () => {
cancelText="取消" cancelText="取消"
type="danger" type="danger"
/> />
{/* Toast notifications */}
{toasts.map(toast => (
<Toast
key={toast.id}
message={toast.message}
type={toast.type}
onClose={() => removeToast(toast.id)}
/>
))}
</div> </div>
); );
}; };

View File

@ -1,8 +1,9 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import apiClient from '../../utils/apiClient'; import apiClient from '../../utils/apiClient';
import { buildApiUrl, API_ENDPOINTS } from '../../config/api'; 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 ConfirmDialog from '../../components/ConfirmDialog';
import FormModal from '../../components/FormModal';
import Toast from '../../components/Toast'; import Toast from '../../components/Toast';
import './UserManagement.css'; import './UserManagement.css';
@ -13,12 +14,11 @@ const UserManagement = () => {
const [pageSize, setPageSize] = useState(10); const [pageSize, setPageSize] = useState(10);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState(''); const [error, setError] = useState('');
const [showAddUserModal, setShowAddUserModal] = useState(false); const [showUserModal, setShowUserModal] = useState(false);
const [showEditUserModal, setShowEditUserModal] = useState(false); const [isEditing, setIsEditing] = useState(false);
const [currentUser, setCurrentUser] = useState(null);
const [deleteConfirmInfo, setDeleteConfirmInfo] = useState(null); const [deleteConfirmInfo, setDeleteConfirmInfo] = useState(null);
const [resetConfirmInfo, setResetConfirmInfo] = 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 [roles, setRoles] = useState([]);
const [toasts, setToasts] = useState([]); const [toasts, setToasts] = useState([]);
@ -67,11 +67,11 @@ const UserManagement = () => {
const handleAddUser = async (e) => { const handleAddUser = async (e) => {
e.preventDefault(); e.preventDefault();
try { try {
await apiClient.post(buildApiUrl(API_ENDPOINTS.USERS.CREATE), newUser); await apiClient.post(buildApiUrl(API_ENDPOINTS.USERS.CREATE), currentUser);
setShowAddUserModal(false); handleCloseModal();
setNewUser({ username: '', caption: '', email: '', role_id: 2 }); setError('');
setError(''); // Clear any previous errors fetchUsers();
fetchUsers(); // Refresh user list showToast('用户添加成功', 'success');
} catch (err) { } catch (err) {
console.error('Error adding user:', err); console.error('Error adding user:', err);
setError(err.response?.data?.message || '新增用户失败'); setError(err.response?.data?.message || '新增用户失败');
@ -81,27 +81,23 @@ const UserManagement = () => {
const handleUpdateUser = async (e) => { const handleUpdateUser = async (e) => {
e.preventDefault(); e.preventDefault();
try { try {
//
const updateData = { const updateData = {
caption: editingUser.caption, caption: currentUser.caption,
email: editingUser.email, email: currentUser.email,
role_id: editingUser.role_id role_id: currentUser.role_id
}; };
// if (currentUser.username && currentUser.username.trim()) {
if (editingUser.username && editingUser.username.trim()) { updateData.username = currentUser.username;
updateData.username = editingUser.username;
} }
console.log('Sending update data:', updateData); // await apiClient.put(buildApiUrl(API_ENDPOINTS.USERS.UPDATE(currentUser.user_id)), updateData);
handleCloseModal();
await apiClient.put(buildApiUrl(API_ENDPOINTS.USERS.UPDATE(editingUser.user_id)), updateData); setError('');
setShowEditUserModal(false); fetchUsers();
setError(''); // Clear any previous errors showToast('用户修改成功', 'success');
fetchUsers(); // Refresh user list
} catch (err) { } catch (err) {
console.error('Error updating user:', err); console.error('Error updating user:', err);
console.error('Error response:', err.response?.data); //
setError(err.response?.data?.message || '修改用户失败'); setError(err.response?.data?.message || '修改用户失败');
} }
}; };
@ -111,7 +107,7 @@ const UserManagement = () => {
await apiClient.delete(buildApiUrl(API_ENDPOINTS.USERS.DELETE(deleteConfirmInfo.user_id))); await apiClient.delete(buildApiUrl(API_ENDPOINTS.USERS.DELETE(deleteConfirmInfo.user_id)));
setDeleteConfirmInfo(null); setDeleteConfirmInfo(null);
showToast('用户删除成功', 'success'); showToast('用户删除成功', 'success');
fetchUsers(); // Refresh user list fetchUsers();
} catch (err) { } catch (err) {
console.error('Error deleting user:', err); console.error('Error deleting user:', err);
showToast('删除用户失败,请重试', 'error'); showToast('删除用户失败,请重试', 'error');
@ -131,9 +127,34 @@ const UserManagement = () => {
} }
}; };
const openEditModal = (user) => { const handleOpenModal = (user = null) => {
setEditingUser({ ...user }); if (user) {
setShowEditUserModal(true); 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) => { const openDeleteConfirm = (user) => {
@ -148,7 +169,7 @@ const UserManagement = () => {
<div className="user-management"> <div className="user-management">
<div className="toolbar"> <div className="toolbar">
<h2>用户列表</h2> <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> </div>
{loading && <p>加载中...</p>} {loading && <p>加载中...</p>}
{error && <p className="error-message">{error}</p>} {error && <p className="error-message">{error}</p>}
@ -176,7 +197,7 @@ const UserManagement = () => {
<td>{user.role_name}</td> <td>{user.role_name}</td>
<td>{new Date(user.created_at).toLocaleString()}</td> <td>{new Date(user.created_at).toLocaleString()}</td>
<td className="action-cell"> <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-danger" onClick={() => openDeleteConfirm(user)} title="删除"><Trash2 size={16} />删除</button>
<button className="action-btn btn-warning" onClick={() => openResetConfirm(user)} title="重置密码"><KeyRound size={16} />重置</button> <button className="action-btn btn-warning" onClick={() => openResetConfirm(user)} title="重置密码"><KeyRound size={16} />重置</button>
</td> </td>
@ -195,78 +216,75 @@ const UserManagement = () => {
</> </>
)} )}
{showAddUserModal && ( {/* 用户表单模态框 */}
<div className="modal-overlay"> <FormModal
<div className="modal-content"> isOpen={showUserModal}
<form onSubmit={handleAddUser}> onClose={handleCloseModal}
<h2>新增用户</h2> title={isEditing ? '编辑用户' : '新增用户'}
{error && <div className="error-message">{error}</div>} size="medium"
<div className="form-group"> actions={
<label>用户名</label> <>
<input className="form-input" type="text" value={newUser.username} onChange={(e) => setNewUser({...newUser, username: e.target.value})} required /> <button type="button" className="btn btn-secondary" onClick={handleCloseModal}>
</div> 取消
<div className="form-group"> </button>
<label>姓名</label> <button type="button" className="btn btn-primary" onClick={handleSave}>
<input className="form-input" type="text" value={newUser.caption} onChange={(e) => setNewUser({...newUser, caption: e.target.value})} required /> {isEditing ? '确认修改' : '确认新增'}
</div> </button>
<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> {error && <div className="error-message">{error}</div>}
<div className="form-group"> {currentUser && (
<label>角色</label> <>
<select className="form-input" value={newUser.role_id} onChange={(e) => setNewUser({...newUser, role_id: parseInt(e.target.value)})}> <div className="form-group">
{roles.map(role => ( <label><User size={16} /> 用户名</label>
<option key={role.role_id} value={role.role_id}>{role.role_name}</option> <input
))} type="text"
</select> value={currentUser.username}
</div> 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"> <div className="info-note">
<p>新用户的默认密码为系统配置的默认密码用户可登录后自行修改</p> <p>新用户的默认密码为系统配置的默认密码用户可登录后自行修改</p>
</div> </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> </FormModal>
</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>
)}
{/* 删除用户确认对话框 */} {/* 删除用户确认对话框 */}
<ConfirmDialog <ConfirmDialog