增加了客户端管理模块

main
mula.liu 2025-10-21 17:30:30 +08:00
parent 1977477fd3
commit 493e632c1f
16 changed files with 1935 additions and 46 deletions

View File

@ -8,8 +8,10 @@ import MeetingDetails from './pages/MeetingDetails';
import CreateMeeting from './pages/CreateMeeting';
import EditMeeting from './pages/EditMeeting';
import AdminManagement from './pages/AdminManagement';
import PromptManagementPage from './pages/PromptManagementPage';
import KnowledgeBasePage from './pages/KnowledgeBasePage';
import EditKnowledgeBase from './pages/EditKnowledgeBase';
import ClientDownloadPage from './pages/ClientDownloadPage';
import './App.css';
function App() {
@ -83,12 +85,16 @@ function App() {
<Route path="/admin/management" element={
user && user.role_id === 1 ? <AdminManagement user={user} /> : <Navigate to="/dashboard" />
} />
<Route path="/prompt-management" element={
user ? <PromptManagementPage user={user} /> : <Navigate to="/" />
} />
<Route path="/knowledge-base" element={
user ? <KnowledgeBasePage user={user} /> : <Navigate to="/" />
} />
<Route path="/knowledge-base/edit/:kb_id" element={
user ? <EditKnowledgeBase user={user} /> : <Navigate to="/" />
} />
<Route path="/downloads" element={<ClientDownloadPage />} />
</Routes>
</div>
</Router>

View File

@ -0,0 +1,150 @@
/* 客户端下载组件样式 */
.client-downloads-section {
background: white;
border-radius: 12px;
padding: 2rem;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
margin-bottom: 2rem;
}
.section-header {
margin-bottom: 2rem;
text-align: center;
}
.section-header h2 {
font-size: 1.75rem;
font-weight: 600;
color: #1e293b;
margin: 0 0 0.5rem 0;
}
.section-header p {
color: #64748b;
margin: 0;
}
.downloads-container {
display: flex;
flex-direction: column;
gap: 2rem;
}
.platform-group {
background: #f8fafc;
border-radius: 10px;
padding: 1.5rem;
}
.group-header {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 1.25rem;
color: #667eea;
}
.group-header h3 {
margin: 0;
font-size: 1.25rem;
font-weight: 600;
color: #1e293b;
}
.clients-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 1rem;
}
.client-download-card {
display: flex;
align-items: center;
gap: 1rem;
padding: 1.25rem;
background: white;
border: 1px solid #e2e8f0;
border-radius: 10px;
text-decoration: none;
color: inherit;
transition: all 0.2s ease;
cursor: pointer;
}
.client-download-card:hover {
border-color: #667eea;
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.15);
transform: translateY(-2px);
}
.card-icon {
flex-shrink: 0;
width: 48px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 12px;
}
.card-info {
flex: 1;
min-width: 0;
}
.card-info h4 {
margin: 0 0 0.5rem 0;
font-size: 1rem;
font-weight: 600;
color: #1e293b;
}
.version-info {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 0.25rem;
}
.version {
font-size: 0.875rem;
color: #667eea;
font-weight: 500;
}
.file-size {
font-size: 0.75rem;
color: #94a3b8;
}
.system-req {
margin: 0;
font-size: 0.75rem;
color: #64748b;
}
.download-icon {
flex-shrink: 0;
color: #667eea;
}
.loading-message,
.empty-message {
text-align: center;
padding: 3rem;
color: #94a3b8;
font-size: 1rem;
}
/* 响应式设计 */
@media (max-width: 768px) {
.clients-list {
grid-template-columns: 1fr;
}
.client-download-card {
padding: 1rem;
}
}

View File

@ -0,0 +1,170 @@
import React, { useState, useEffect } from 'react';
import { Download, Smartphone, Monitor, Apple, ChevronRight } from 'lucide-react';
import apiClient from '../utils/apiClient';
import { buildApiUrl, API_ENDPOINTS } from '../config/api';
import './ClientDownloads.css';
const ClientDownloads = () => {
const [clients, setClients] = useState({
mobile: [],
desktop: []
});
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchLatestClients();
}, []);
const fetchLatestClients = async () => {
setLoading(true);
try {
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.CLIENT_DOWNLOADS.LATEST));
console.log('Latest clients response:', response);
setClients(response.data || { mobile: [], desktop: [] });
} catch (error) {
console.error('获取客户端下载失败:', error);
} finally {
setLoading(false);
}
};
const getPlatformIcon = (platformName) => {
switch (platformName) {
case 'ios':
return <Apple size={32} />;
case 'android':
return <Smartphone size={32} />;
case 'mac_intel':
case 'mac_m':
return <Apple size={32} />;
default:
return <Monitor size={32} />;
}
};
const getPlatformLabel = (platformName) => {
const labels = {
ios: 'iOS',
android: 'Android',
windows: 'Windows',
mac_intel: 'Mac (Intel)',
mac_m: 'Mac (M系列)',
linux: 'Linux'
};
return labels[platformName] || platformName;
};
const formatFileSize = (bytes) => {
if (!bytes) return '';
const mb = bytes / (1024 * 1024);
return `${mb.toFixed(0)} MB`;
};
if (loading) {
return (
<div className="client-downloads-section">
<div className="section-header">
<h2>下载客户端</h2>
</div>
<div className="loading-message">加载中...</div>
</div>
);
}
return (
<div className="client-downloads-section">
<div className="section-header">
<h2>下载客户端</h2>
<p>选择适合您设备的版本</p>
</div>
<div className="downloads-container">
{/* 移动端 */}
{clients.mobile && clients.mobile.length > 0 && (
<div className="platform-group">
<div className="group-header">
<Smartphone size={24} />
<h3>移动端</h3>
</div>
<div className="clients-list">
{clients.mobile.map(client => (
<a
key={client.id}
href={client.download_url}
target="_blank"
rel="noopener noreferrer"
className="client-download-card"
>
<div className="card-icon">
{getPlatformIcon(client.platform_name)}
</div>
<div className="card-info">
<h4>{getPlatformLabel(client.platform_name)}</h4>
<div className="version-info">
<span className="version">v{client.version}</span>
{client.file_size && (
<span className="file-size">{formatFileSize(client.file_size)}</span>
)}
</div>
{client.min_system_version && (
<p className="system-req">需要 {client.min_system_version} 或更高版本</p>
)}
</div>
<div className="download-icon">
<ChevronRight size={20} />
</div>
</a>
))}
</div>
</div>
)}
{/* 桌面端 */}
{clients.desktop && clients.desktop.length > 0 && (
<div className="platform-group">
<div className="group-header">
<Monitor size={24} />
<h3>桌面端</h3>
</div>
<div className="clients-list">
{clients.desktop.map(client => (
<a
key={client.id}
href={client.download_url}
target="_blank"
rel="noopener noreferrer"
className="client-download-card"
>
<div className="card-icon">
{getPlatformIcon(client.platform_name)}
</div>
<div className="card-info">
<h4>{getPlatformLabel(client.platform_name)}</h4>
<div className="version-info">
<span className="version">v{client.version}</span>
{client.file_size && (
<span className="file-size">{formatFileSize(client.file_size)}</span>
)}
</div>
{client.min_system_version && (
<p className="system-req">需要 {client.min_system_version} 或更高版本</p>
)}
</div>
<div className="download-icon">
<Download size={20} />
</div>
</a>
))}
</div>
</div>
)}
</div>
{!clients.mobile?.length && !clients.desktop?.length && (
<div className="empty-message">暂无可用的客户端下载</div>
)}
</div>
);
};
export default ClientDownloads;

View File

@ -55,6 +55,15 @@ const API_CONFIG = {
UPDATE: (kbId) => `/api/knowledge-bases/${kbId}`,
DELETE: (kbId) => `/api/knowledge-bases/${kbId}`,
TASK_STATUS: (taskId) => `/api/knowledge-bases/tasks/${taskId}`
},
CLIENT_DOWNLOADS: {
LIST: '/api/clients/downloads',
LATEST: '/api/clients/downloads/latest',
LATEST_BY_PLATFORM: (platformName) => `/api/clients/downloads/${platformName}/latest`,
DETAIL: (id) => `/api/clients/downloads/${id}`,
CREATE: '/api/clients/downloads',
UPDATE: (id) => `/api/clients/downloads/${id}`,
DELETE: (id) => `/api/clients/downloads/${id}`
}
}
};

View File

@ -1,10 +1,10 @@
import React from 'react';
import { MessageSquare, Settings, Users, BookText } from 'lucide-react';
import { MessageSquare, Settings, Users, Smartphone } from 'lucide-react';
import { useNavigate } from 'react-router-dom';
import { Tabs } from 'antd';
import UserManagement from './admin/UserManagement';
import SystemConfiguration from './admin/SystemConfiguration';
import PromptManagement from './admin/PromptManagement';
import ClientManagement from './ClientManagement';
import './AdminManagement.css';
const { TabPane } = Tabs;
@ -30,23 +30,23 @@ const AdminManagement = () => {
<div className="admin-content">
<div className="admin-wrapper">
<Tabs defaultActiveKey="userManagement" className="admin-tabs">
<TabPane
tab={<span><Users size={16} /> 用户管理</span>}
<TabPane
tab={<span><Users size={16} /> 用户管理</span>}
key="userManagement"
>
<UserManagement />
</TabPane>
<TabPane
tab={<span><Settings size={16} /> 系统配置</span>}
<TabPane
tab={<span><Settings size={16} /> 系统配置</span>}
key="systemConfiguration"
>
<SystemConfiguration />
</TabPane>
<TabPane
tab={<span><BookText size={16} /> 提示词仓库</span>}
key="promptManagement"
<TabPane
tab={<span><Smartphone size={16} /> 客户端管理</span>}
key="clientManagement"
>
<PromptManagement />
<ClientManagement />
</TabPane>
</Tabs>
</div>

View File

@ -0,0 +1,61 @@
/* 客户端下载页面样式 */
.client-download-page {
min-height: 100vh;
background: #f8fafc;
display: flex;
flex-direction: column;
}
.download-page-header {
background: white;
border-bottom: 1px solid #e2e8f0;
padding: 1.5rem 2rem;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.download-page-header .header-content {
max-width: 1200px;
margin: 0 auto;
}
.download-page-header .logo {
display: flex;
align-items: center;
gap: 0.75rem;
}
.download-page-header .logo-icon {
width: 32px;
height: 32px;
color: #667eea;
}
.download-page-header .logo-text {
font-size: 1.5rem;
font-weight: 700;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.download-page-content {
flex: 1;
max-width: 1200px;
margin: 0 auto;
width: 100%;
padding: 2rem;
}
.download-page-footer {
background: white;
border-top: 1px solid #e2e8f0;
padding: 2rem;
text-align: center;
color: #64748b;
font-size: 0.875rem;
}
.download-page-footer p {
margin: 0.25rem 0;
}

View File

@ -0,0 +1,37 @@
import React from 'react';
import { Brain } from 'lucide-react';
import { useNavigate } from 'react-router-dom';
import ClientDownloads from '../components/ClientDownloads';
import './ClientDownloadPage.css';
const ClientDownloadPage = () => {
const navigate = useNavigate();
const handleLogoClick = () => {
navigate('/');
};
return (
<div className="client-download-page">
<header className="download-page-header">
<div className="header-content">
<div className="logo" onClick={handleLogoClick} style={{ cursor: 'pointer' }}>
<Brain className="logo-icon" />
<span className="logo-text">iMeeting</span>
</div>
</div>
</header>
<div className="download-page-content">
<ClientDownloads />
</div>
<footer className="download-page-footer">
<p>© 2025 紫光汇智信息技术有限公司. All rights reserved.</p>
<p>备案号渝ICP备2023007695号-11</p>
</footer>
</div>
);
};
export default ClientDownloadPage;

View File

@ -0,0 +1,543 @@
/* 客户端管理页面样式 */
.client-management-page {
min-height: 100vh;
background: #f8fafc;
padding: 2rem;
}
.client-management-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
}
.client-management-header h1 {
font-size: 1.75rem;
font-weight: 600;
color: #1e293b;
margin: 0;
}
.btn-create {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.75rem 1.5rem;
background: #667eea;
color: white;
border: none;
border-radius: 8px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
}
.btn-create:hover {
background: #5568d3;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
}
/* 过滤器区域 */
.client-filters {
display: flex;
gap: 1rem;
margin-bottom: 2rem;
flex-wrap: wrap;
}
.search-box {
flex: 1;
min-width: 300px;
position: relative;
display: flex;
align-items: center;
background: white;
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 0.75rem 1rem;
}
.search-box svg:first-child {
color: #94a3b8;
margin-right: 0.75rem;
}
.search-box input {
flex: 1;
border: none;
outline: none;
font-size: 0.95rem;
}
.clear-search {
background: none;
border: none;
color: #94a3b8;
cursor: pointer;
padding: 0.25rem;
display: flex;
align-items: center;
transition: color 0.2s ease;
}
.clear-search:hover {
color: #64748b;
}
.platform-filters {
display: flex;
gap: 0.5rem;
}
.filter-btn {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.75rem 1.25rem;
background: white;
border: 1px solid #e2e8f0;
border-radius: 8px;
color: #64748b;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
}
.filter-btn:hover {
border-color: #667eea;
color: #667eea;
}
.filter-btn.active {
background: #667eea;
border-color: #667eea;
color: white;
}
/* 客户端分组区域 */
.clients-sections {
display: flex;
flex-direction: column;
gap: 2rem;
}
.client-section {
background: white;
border-radius: 12px;
padding: 1.5rem;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.section-title {
display: flex;
align-items: center;
gap: 0.75rem;
margin: 0 0 1.5rem 0;
font-size: 1.25rem;
font-weight: 600;
color: #1e293b;
}
.section-title .count {
color: #94a3b8;
font-size: 1rem;
font-weight: 400;
}
.clients-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
gap: 1.5rem;
}
.client-card {
background: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 10px;
padding: 1.25rem;
transition: all 0.2s ease;
}
.client-card:hover {
border-color: #667eea;
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.1);
}
.client-card.inactive {
opacity: 0.6;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 1rem;
padding-bottom: 1rem;
border-bottom: 1px solid #e2e8f0;
}
.platform-info {
display: flex;
align-items: center;
gap: 0.75rem;
flex-wrap: wrap;
}
.platform-info h3 {
margin: 0;
font-size: 1.125rem;
font-weight: 600;
color: #1e293b;
}
.badge-latest {
padding: 0.25rem 0.75rem;
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
color: white;
font-size: 0.75rem;
border-radius: 12px;
font-weight: 500;
}
.badge-inactive {
padding: 0.25rem 0.75rem;
background: #ef4444;
color: white;
font-size: 0.75rem;
border-radius: 12px;
font-weight: 500;
}
.card-actions {
display: flex;
gap: 0.5rem;
}
.btn-icon {
padding: 0.5rem;
border: none;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
}
.btn-edit {
background: #dbeafe;
color: #3b82f6;
}
.btn-edit:hover {
background: #3b82f6;
color: white;
}
.btn-delete {
background: #fee2e2;
color: #ef4444;
}
.btn-delete:hover {
background: #ef4444;
color: white;
}
.card-body {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.info-row {
display: flex;
justify-content: space-between;
font-size: 0.9rem;
}
.info-row .label {
color: #64748b;
font-weight: 500;
}
.info-row .value {
color: #1e293b;
font-weight: 500;
}
.release-notes {
margin-top: 0.5rem;
}
.release-notes .notes-header {
display: flex;
align-items: center;
justify-content: space-between;
cursor: pointer;
padding: 0.5rem;
background: white;
border-radius: 6px;
transition: background 0.2s ease;
margin-bottom: 0.5rem;
}
.release-notes .notes-header:hover {
background: #f8fafc;
}
.release-notes .label {
display: block;
color: #64748b;
font-weight: 500;
font-size: 0.9rem;
}
.release-notes p {
margin: 0;
padding: 0.75rem;
background: white;
border-radius: 6px;
color: #475569;
font-size: 0.875rem;
line-height: 1.6;
white-space: pre-wrap;
}
.download-link a {
display: inline-flex;
align-items: center;
gap: 0.5rem;
color: #667eea;
text-decoration: none;
font-size: 0.875rem;
font-weight: 500;
transition: color 0.2s ease;
}
.download-link a:hover {
color: #5568d3;
text-decoration: underline;
}
/* 模态框样式 */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
backdrop-filter: blur(2px);
}
.modal-content {
background: white;
border-radius: 12px;
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
width: 90%;
max-width: 700px;
max-height: 90vh;
display: flex;
flex-direction: column;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.5rem 2rem;
border-bottom: 1px solid #e2e8f0;
}
.modal-header h2 {
margin: 0;
font-size: 1.25rem;
font-weight: 600;
color: #1e293b;
}
.close-btn {
padding: 0.5rem;
border: none;
border-radius: 6px;
background: #f1f5f9;
color: #64748b;
cursor: pointer;
font-size: 1.5rem;
line-height: 1;
transition: all 0.2s ease;
}
.close-btn:hover {
background: #e2e8f0;
color: #1e293b;
}
.modal-body {
padding: 2rem;
overflow-y: auto;
flex: 1;
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
margin-bottom: 1.5rem;
}
.form-group {
margin-bottom: 1.5rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
color: #1e293b;
font-size: 0.95rem;
}
.form-group input,
.form-group select,
.form-group textarea {
width: 100%;
padding: 0.75rem;
border: 1px solid #e2e8f0;
border-radius: 8px;
font-family: inherit;
font-size: 0.95rem;
transition: border-color 0.2s ease;
box-sizing: border-box;
}
/* 数字input保留默认上下调整按钮 */
.form-group input[type="number"] {
width: 100%;
padding: 0.75rem;
border: 1px solid #e2e8f0;
border-radius: 8px;
font-family: inherit;
font-size: 0.95rem;
transition: border-color 0.2s ease;
box-sizing: border-box;
}
.form-group input:focus,
.form-group select:focus,
.form-group textarea:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.form-group textarea {
resize: vertical;
min-height: 100px;
}
.checkbox-group label {
display: flex;
align-items: center;
gap: 0.5rem;
cursor: pointer;
}
.checkbox-group input[type="checkbox"] {
width: auto;
cursor: pointer;
}
.modal-actions {
padding: 1rem 2rem;
border-top: 1px solid #e2e8f0;
display: flex;
gap: 1rem;
justify-content: flex-end;
}
.btn-cancel,
.btn-submit {
padding: 0.75rem 1.5rem;
border-radius: 8px;
border: none;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
}
.btn-cancel {
background: #f1f5f9;
color: #475569;
}
.btn-cancel:hover {
background: #e2e8f0;
}
.btn-submit {
background: #667eea;
color: white;
}
.btn-submit:hover {
background: #5568d3;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
}
/* 删除确认模态框 */
.delete-modal {
background: white;
border-radius: 12px;
padding: 2rem;
max-width: 400px;
width: 90%;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
}
.delete-modal h3 {
margin: 0 0 1rem 0;
color: #1e293b;
font-size: 1.25rem;
}
.delete-modal p {
margin: 0 0 2rem 0;
color: #64748b;
line-height: 1.6;
}
.btn-delete {
background: #ef4444;
color: white;
}
.btn-delete:hover {
background: #dc2626;
}
/* 空状态 */
.empty-state {
text-align: center;
padding: 3rem;
color: #94a3b8;
font-size: 1rem;
}
/* 加载状态 */
.client-management-loading {
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
font-size: 1.125rem;
color: #94a3b8;
}

View File

@ -0,0 +1,599 @@
import React, { useState, useEffect } from 'react';
import {
Download,
Plus,
Edit,
Trash2,
Smartphone,
Monitor,
Apple,
Search,
X,
ChevronDown,
ChevronUp
} from 'lucide-react';
import apiClient from '../utils/apiClient';
import { buildApiUrl, API_ENDPOINTS } from '../config/api';
import './ClientManagement.css';
const ClientManagement = ({ user }) => {
const [clients, setClients] = useState([]);
const [loading, setLoading] = useState(true);
const [showCreateModal, setShowCreateModal] = useState(false);
const [showEditModal, setShowEditModal] = useState(false);
const [showDeleteModal, setShowDeleteModal] = useState(false);
const [selectedClient, setSelectedClient] = useState(null);
const [filterPlatformType, setFilterPlatformType] = useState('');
const [searchQuery, setSearchQuery] = useState('');
const [expandedNotes, setExpandedNotes] = useState({});
const [formData, setFormData] = useState({
platform_type: 'mobile',
platform_name: 'ios',
version: '',
version_code: '',
download_url: '',
file_size: '',
release_notes: '',
is_active: true,
is_latest: false,
min_system_version: ''
});
const platformOptions = {
mobile: [
{ value: 'ios', label: 'iOS', icon: <Apple size={16} /> },
{ value: 'android', label: 'Android', icon: <Smartphone size={16} /> }
],
desktop: [
{ value: 'windows', label: 'Windows', icon: <Monitor size={16} /> },
{ value: 'mac_intel', label: 'Mac (Intel)', icon: <Apple size={16} /> },
{ value: 'mac_m', label: 'Mac (M系列)', icon: <Apple size={16} /> },
{ value: 'linux', label: 'Linux', icon: <Monitor size={16} /> }
]
};
useEffect(() => {
fetchClients();
}, []);
const fetchClients = async () => {
setLoading(true);
try {
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.CLIENT_DOWNLOADS.LIST));
console.log('Client downloads response:', response);
setClients(response.data.clients || []);
} catch (error) {
console.error('获取客户端列表失败:', error);
} finally {
setLoading(false);
}
};
const handleCreate = async () => {
try {
//
if (!formData.version_code || !formData.version || !formData.download_url) {
alert('请填写所有必填字段');
return;
}
const payload = {
...formData,
version_code: parseInt(formData.version_code, 10),
file_size: formData.file_size ? parseInt(formData.file_size, 10) : null
};
//
if (isNaN(payload.version_code)) {
alert('版本代码必须是有效的数字');
return;
}
if (formData.file_size && isNaN(payload.file_size)) {
alert('文件大小必须是有效的数字');
return;
}
await apiClient.post(buildApiUrl(API_ENDPOINTS.CLIENT_DOWNLOADS.CREATE), payload);
setShowCreateModal(false);
resetForm();
fetchClients();
} catch (error) {
console.error('创建客户端失败:', error);
alert(error.response?.data?.message || '创建失败,请重试');
}
};
const handleUpdate = async () => {
try {
//
if (!formData.version_code || !formData.version || !formData.download_url) {
alert('请填写所有必填字段');
return;
}
const payload = {
version: formData.version,
version_code: parseInt(formData.version_code, 10),
download_url: formData.download_url,
file_size: formData.file_size ? parseInt(formData.file_size, 10) : null,
release_notes: formData.release_notes,
is_active: formData.is_active,
is_latest: formData.is_latest,
min_system_version: formData.min_system_version
};
//
if (isNaN(payload.version_code)) {
alert('版本代码必须是有效的数字');
return;
}
if (formData.file_size && isNaN(payload.file_size)) {
alert('文件大小必须是有效的数字');
return;
}
await apiClient.put(
buildApiUrl(API_ENDPOINTS.CLIENT_DOWNLOADS.UPDATE(selectedClient.id)),
payload
);
setShowEditModal(false);
resetForm();
fetchClients();
} catch (error) {
console.error('更新客户端失败:', error);
alert(error.response?.data?.message || '更新失败,请重试');
}
};
const handleDelete = async () => {
try {
await apiClient.delete(buildApiUrl(API_ENDPOINTS.CLIENT_DOWNLOADS.DELETE(selectedClient.id)));
setShowDeleteModal(false);
setSelectedClient(null);
fetchClients();
} catch (error) {
console.error('删除客户端失败:', error);
alert('删除失败,请重试');
}
};
const openEditModal = (client) => {
setSelectedClient(client);
setFormData({
platform_type: client.platform_type,
platform_name: client.platform_name,
version: client.version,
version_code: String(client.version_code),
download_url: client.download_url,
file_size: client.file_size ? String(client.file_size) : '',
release_notes: client.release_notes || '',
is_active: client.is_active,
is_latest: client.is_latest,
min_system_version: client.min_system_version || ''
});
setShowEditModal(true);
};
const openDeleteModal = (client) => {
setSelectedClient(client);
setShowDeleteModal(true);
};
const resetForm = () => {
setFormData({
platform_type: 'mobile',
platform_name: 'ios',
version: '',
version_code: '',
download_url: '',
file_size: '',
release_notes: '',
is_active: true,
is_latest: false,
min_system_version: ''
});
setSelectedClient(null);
};
const getPlatformLabel = (platformName) => {
const allOptions = [...platformOptions.mobile, ...platformOptions.desktop];
const option = allOptions.find(opt => opt.value === platformName);
return option ? option.label : platformName;
};
const formatFileSize = (bytes) => {
if (!bytes) return '-';
const mb = bytes / (1024 * 1024);
return `${mb.toFixed(2)} MB`;
};
const toggleNotes = (clientId) => {
setExpandedNotes(prev => ({
...prev,
[clientId]: !prev[clientId]
}));
};
const filteredClients = clients.filter(client => {
if (filterPlatformType && client.platform_type !== filterPlatformType) {
return false;
}
if (searchQuery) {
const query = searchQuery.toLowerCase();
return (
client.version.toLowerCase().includes(query) ||
getPlatformLabel(client.platform_name).toLowerCase().includes(query) ||
(client.release_notes && client.release_notes.toLowerCase().includes(query))
);
}
return true;
});
const groupedClients = {
mobile: filteredClients.filter(c => c.platform_type === 'mobile'),
desktop: filteredClients.filter(c => c.platform_type === 'desktop')
};
if (loading) {
return <div className="client-management-loading">加载中...</div>;
}
return (
<div className="client-management-page">
<div className="client-management-header">
<h1>客户端下载管理</h1>
<button className="btn-create" onClick={() => setShowCreateModal(true)}>
<Plus size={18} />
<span>新增客户端</span>
</button>
</div>
<div className="client-filters">
<div className="search-box">
<Search size={18} />
<input
type="text"
placeholder="搜索版本号、平台或更新说明..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
{searchQuery && (
<button className="clear-search" onClick={() => setSearchQuery('')}>
<X size={16} />
</button>
)}
</div>
<div className="platform-filters">
<button
className={`filter-btn ${filterPlatformType === '' ? 'active' : ''}`}
onClick={() => setFilterPlatformType('')}
>
全部
</button>
<button
className={`filter-btn ${filterPlatformType === 'mobile' ? 'active' : ''}`}
onClick={() => setFilterPlatformType('mobile')}
>
<Smartphone size={16} />
<span>移动端</span>
</button>
<button
className={`filter-btn ${filterPlatformType === 'desktop' ? 'active' : ''}`}
onClick={() => setFilterPlatformType('desktop')}
>
<Monitor size={16} />
<span>桌面端</span>
</button>
</div>
</div>
<div className="clients-sections">
{['mobile', 'desktop'].map(type => {
const typeClients = groupedClients[type];
if (typeClients.length === 0 && filterPlatformType && filterPlatformType !== type) {
return null;
}
return (
<div key={type} className="client-section">
<h2 className="section-title">
{type === 'mobile' ? <Smartphone size={20} /> : <Monitor size={20} />}
<span>{type === 'mobile' ? '移动端' : '桌面端'}</span>
<span className="count">({typeClients.length})</span>
</h2>
{typeClients.length === 0 ? (
<div className="empty-state">暂无客户端</div>
) : (
<div className="clients-grid">
{typeClients.map(client => (
<div key={client.id} className={`client-card ${!client.is_active ? 'inactive' : ''}`}>
<div className="card-header">
<div className="platform-info">
<h3>{getPlatformLabel(client.platform_name)}</h3>
{client.is_latest && <span className="badge-latest">最新</span>}
{!client.is_active && <span className="badge-inactive">未启用</span>}
</div>
<div className="card-actions">
<button
className="btn-icon btn-edit"
onClick={() => openEditModal(client)}
title="编辑"
>
<Edit size={16} />
</button>
<button
className="btn-icon btn-delete"
onClick={() => openDeleteModal(client)}
title="删除"
>
<Trash2 size={16} />
</button>
</div>
</div>
<div className="card-body">
<div className="info-row">
<span className="label">版本:</span>
<span className="value">{client.version}</span>
</div>
<div className="info-row">
<span className="label">版本代码:</span>
<span className="value">{client.version_code}</span>
</div>
<div className="info-row">
<span className="label">文件大小:</span>
<span className="value">{formatFileSize(client.file_size)}</span>
</div>
{client.min_system_version && (
<div className="info-row">
<span className="label">系统要求:</span>
<span className="value">{client.min_system_version}</span>
</div>
)}
{client.release_notes && (
<div className="release-notes">
<div
className="notes-header"
onClick={() => toggleNotes(client.id)}
style={{ cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}
>
<span className="label">更新说明</span>
{expandedNotes[client.id] ? <ChevronUp size={16} /> : <ChevronDown size={16} />}
</div>
{expandedNotes[client.id] && (
<p>{client.release_notes}</p>
)}
</div>
)}
<div className="download-link">
<a href={client.download_url} target="_blank" rel="noopener noreferrer">
<Download size={14} />
<span>查看下载链接</span>
</a>
</div>
</div>
</div>
))}
</div>
)}
</div>
);
})}
</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>
</div>
<div className="form-group">
<label>下载链接 *</label>
<input
type="url"
placeholder="https://..."
value={formData.download_url}
onChange={(e) => setFormData({ ...formData, download_url: 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 })}
/>
</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>
</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>
</div>
</div>
)}
{/* 删除确认模态框 */}
{showDeleteModal && selectedClient && (
<div className="modal-overlay" onClick={() => setShowDeleteModal(false)}>
<div className="delete-modal" onClick={(e) => e.stopPropagation()}>
<h3>确认删除</h3>
<p>
确定要删除 <strong>{getPlatformLabel(selectedClient.platform_name)}</strong> 版本{' '}
<strong>{selectedClient.version}</strong> 此操作无法撤销
</p>
<div className="modal-actions">
<button className="btn-cancel" onClick={() => setShowDeleteModal(false)}>
取消
</button>
<button className="btn-delete" onClick={handleDelete}>
确定删除
</button>
</div>
</div>
</div>
)}
</div>
);
};
export default ClientManagement;

View File

@ -1,5 +1,5 @@
import React, { useState, useEffect, useRef } from 'react';
import { LogOut, User, Calendar, Users, TrendingUp, Clock, MessageSquare, Plus, ChevronDown, KeyRound, Shield, Filter, X, Library } from 'lucide-react';
import { LogOut, User, Calendar, Users, TrendingUp, Clock, MessageSquare, Plus, ChevronDown, KeyRound, Shield, Filter, X, Library, BookText } from 'lucide-react';
import apiClient from '../utils/apiClient';
import { Link } from 'react-router-dom';
import { buildApiUrl, API_ENDPOINTS } from '../config/api';
@ -218,10 +218,11 @@ const Dashboard = ({ user, onLogout }) => {
{dropdownOpen && (
<div className="dropdown-menu">
<button onClick={() => { setShowChangePasswordModal(true); setDropdownOpen(false); }}><KeyRound size={16} /> 修改密码</button>
<Link to="/prompt-management" onClick={() => setDropdownOpen(false)}><BookText size={16} /> 提示词仓库</Link>
{user.role_id === 1 && (
<Link to="/admin/management" onClick={() => setDropdownOpen(false)}><Shield size={16} /> 平台管理</Link>
)}
<button onClick={onLogout}><LogOut size={16} /> 退出</button>
<button onClick={onLogout}><LogOut size={16} /> 退出登录</button>
</div>
)}
</div>

View File

@ -67,16 +67,54 @@
color: var(--text-dark);
}
.header-actions {
display: flex;
align-items: center;
gap: 1rem;
}
.download-link,
.login-btn {
background-color: var(--accent-color);
border: none;
color: white;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
padding: 0.6rem 1.6rem;
border: 2px solid var(--accent-color);
border-radius: 25px;
cursor: pointer;
transition: all 0.3s ease;
font-size: 0.9rem;
font-weight: 500;
line-height: 1;
transition: all 0.3s ease;
vertical-align: middle;
box-sizing: border-box;
height: auto;
margin: 0;
font-family: inherit;
}
.download-link {
color: var(--accent-color);
background-color: transparent;
text-decoration: none;
}
.download-link svg,
.login-btn svg {
flex-shrink: 0;
}
.download-link:hover {
background-color: var(--accent-color);
color: white;
transform: translateY(-2px);
box-shadow: 0 4px 15px rgba(111, 66, 193, 0.2);
}
.login-btn {
background-color: var(--accent-color);
color: white;
cursor: pointer;
}
.login-btn:hover {

View File

@ -1,5 +1,6 @@
import React, { useState } from 'react';
import { Brain, Users, Calendar, TrendingUp, X, User, Lock, Library, Download } from 'lucide-react';
import { Brain, Users, Calendar, TrendingUp, X, User, Lock, Library, Download, LogIn } from 'lucide-react';
import { Link } from 'react-router-dom';
import apiClient from '../utils/apiClient';
import { buildApiUrl, API_ENDPOINTS } from '../config/api';
import './HomePage.css';
@ -48,12 +49,19 @@ const HomePage = ({ onLogin }) => {
<Brain className="logo-icon" />
<span className="logo-text">iMeeting</span>
</div>
<button
className="login-btn"
onClick={() => setShowLoginModal(true)}
>
登录
</button>
<div className="header-actions">
<Link to="/downloads" className="download-link">
<Download size={18} />
<span>下载客户端</span>
</Link>
<button
className="login-btn"
onClick={() => setShowLoginModal(true)}
>
<LogIn size={18} />
<span>登录</span>
</button>
</div>
</div>
</header>

View File

@ -575,7 +575,7 @@
}
.modal-body {
padding: 2rem;
padding: 1rem;
overflow-y: auto;
flex: 1;
}
@ -625,6 +625,86 @@
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
/* 紧凑的搜索和过滤区 */
.search-filter-area {
margin-bottom: 1rem;
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.compact-search-input {
width: 100%;
padding: 0.625rem 0.75rem;
border: 1px solid #e2e8f0;
border-radius: 6px;
font-size: 0.875rem;
transition: border-color 0.2s ease;
}
.compact-search-input:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.1);
}
.tag-filter-section {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.tag-filter-chips {
display: flex;
flex-wrap: nowrap;
gap: 0.5rem;
overflow: visible;
padding: 0.25rem;
}
.tag-chip {
padding: 0.375rem 0.75rem;
border: 1px solid #e2e8f0;
border-radius: 16px;
background: #f8fafc;
color: #64748b;
font-size: 0.8rem;
cursor: pointer;
transition: all 0.2s ease;
white-space: nowrap;
}
.tag-chip:hover {
background: #e2e8f0;
border-color: #cbd5e1;
}
.tag-chip.selected {
background: #667eea;
color: white;
border-color: #667eea;
}
.clear-filters-btn {
align-self: flex-start;
padding: 0.375rem 0.75rem;
border: none;
border-radius: 6px;
background: #f1f5f9;
color: #64748b;
font-size: 0.8rem;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
gap: 0.375rem;
}
.clear-filters-btn:hover {
background: #e2e8f0;
color: #475569;
}
.meeting-list {
max-height: 300px;
overflow-y: auto;

View File

@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react';
import { MessageSquare, ChevronLeft, ChevronRight, Plus, Calendar, Database, Trash2, Edit, FileText, Image } from 'lucide-react';
import { MessageSquare, ChevronLeft, ChevronRight, Plus, Calendar, Database, Trash2, Edit, FileText, Image, X } from 'lucide-react';
import { useNavigate } from 'react-router-dom';
import apiClient from '../utils/apiClient';
import { buildApiUrl, API_ENDPOINTS } from '../config/api';
@ -22,6 +22,8 @@ const KnowledgeBasePage = ({ user }) => {
const [selectedMeetings, setSelectedMeetings] = useState([]);
const [userPrompt, setUserPrompt] = useState('');
const [searchQuery, setSearchQuery] = useState('');
const [selectedTags, setSelectedTags] = useState([]);
const [availableTags, setAvailableTags] = useState([]);
const [generating, setGenerating] = useState(false);
const [taskId, setTaskId] = useState(null);
const [progress, setProgress] = useState(0);
@ -94,7 +96,29 @@ const KnowledgeBasePage = ({ user }) => {
const fetchMeetings = () => {
apiClient.get(buildApiUrl(API_ENDPOINTS.MEETINGS.LIST, { user_id: user.user_id }))
.then(response => {
setMeetings(response.data);
const meetingsData = response.data;
setMeetings(meetingsData);
// 使
const tagFrequency = {};
meetingsData.forEach(meeting => {
if (meeting.tags && Array.isArray(meeting.tags)) {
meeting.tags.forEach(tag => {
const tagName = typeof tag === 'string' ? tag : tag.name;
if (tagName) {
tagFrequency[tagName] = (tagFrequency[tagName] || 0) + 1;
}
});
}
});
// 使,5
const topTags = Object.entries(tagFrequency)
.sort((a, b) => b[1] - a[1])
.slice(0, 6)
.map(([tag]) => tag);
setAvailableTags(topTags);
})
.catch(error => {
console.error("Error fetching meetings:", error);
@ -143,10 +167,48 @@ const KnowledgeBasePage = ({ user }) => {
};
const filteredMeetings = (meetings || []).filter(meeting => {
if (!searchQuery) return true;
return meeting.title.toLowerCase().includes(searchQuery.toLowerCase());
//
if (searchQuery) {
const query = searchQuery.toLowerCase();
const matchTitle = meeting.title?.toLowerCase().includes(query);
const matchCreator = meeting.creator_username?.toLowerCase().includes(query);
if (!matchTitle && !matchCreator) {
return false;
}
}
//
if (selectedTags.length > 0) {
if (!meeting.tags || meeting.tags.length === 0) {
return false;
}
const meetingTags = meeting.tags.map(tag =>
typeof tag === 'string' ? tag : tag.name
);
const hasAllTags = selectedTags.every(selectedTag =>
meetingTags.includes(selectedTag)
);
if (!hasAllTags) {
return false;
}
}
return true;
});
const handleTagToggle = (tag) => {
setSelectedTags(prev =>
prev.includes(tag)
? prev.filter(t => t !== tag)
: [...prev, tag]
);
};
const clearFilters = () => {
setSearchQuery('');
setSelectedTags([]);
};
const handleDelete = async (kb) => {
setDeletingKb(kb);
setShowDeleteConfirm(true);
@ -712,25 +774,48 @@ const KnowledgeBasePage = ({ user }) => {
<button onClick={() => setShowCreateForm(false)} className="close-btn">×</button>
</div>
<div className="modal-body">
<div className="form-group">
<label>用户提示词可选</label>
<textarea
placeholder="请输入您的提示词..."
value={userPrompt}
onChange={(e) => setUserPrompt(e.target.value)}
className="kb-prompt-input"
rows={4}
/>
</div>
<div className="form-group">
<label>选择会议数据源</label>
<input
type="text"
placeholder="搜索会议名称..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="search-input"
/>
{/* 紧凑的搜索和过滤区 */}
<div className="search-filter-area">
<input
type="text"
placeholder="搜索会议名称或创建人..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="compact-search-input"
/>
{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
type="button"
className="clear-filters-btn"
onClick={clearFilters}
>
<X size={14} />
清除筛选
</button>
)}
</div>
<div className="meeting-list">
{filteredMeetings.length === 0 ? (
<div className="empty-state">
@ -768,6 +853,16 @@ const KnowledgeBasePage = ({ user }) => {
已选择 {selectedMeetings.length} 个会议
</div>
)}
<div className="form-group">
<label>用户提示词可选</label>
<textarea
placeholder="请输入您的提示词..."
value={userPrompt}
onChange={(e) => setUserPrompt(e.target.value)}
className="kb-prompt-input"
rows={4}
/>
</div>
</div>
<div className="modal-actions">
<button className="btn-cancel" onClick={() => setShowCreateForm(false)}>取消</button>

View File

@ -0,0 +1,58 @@
/* 提示词管理页面样式 */
.prompt-management-page {
min-height: 100vh;
background: #f8fafc;
}
.prompt-header {
background: white;
border-bottom: 1px solid #e2e8f0;
padding: 1rem 2rem;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.prompt-header .header-content {
max-width: 1200px;
margin: 0 auto;
display: flex;
align-items: center;
gap: 2rem;
}
.prompt-header .logo {
display: flex;
align-items: center;
gap: 0.75rem;
cursor: pointer;
}
.prompt-header .logo-icon {
width: 32px;
height: 32px;
color: #667eea;
}
.prompt-header .logo-text {
font-size: 1.5rem;
font-weight: 700;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.prompt-header h1 {
font-size: 1.5rem;
font-weight: 600;
color: #1e293b;
margin: 0;
}
.prompt-content {
padding: 2rem;
}
.prompt-wrapper {
max-width: 1200px;
margin: 0 auto;
}

View File

@ -0,0 +1,34 @@
import React from 'react';
import { MessageSquare } from 'lucide-react';
import { useNavigate } from 'react-router-dom';
import PromptManagement from './admin/PromptManagement';
import './PromptManagementPage.css';
const PromptManagementPage = () => {
const navigate = useNavigate();
const handleLogoClick = () => {
navigate('/dashboard');
};
return (
<div className="prompt-management-page">
<header className="prompt-header">
<div className="header-content">
<div className="logo" onClick={handleLogoClick} style={{ cursor: 'pointer' }}>
<MessageSquare className="logo-icon" />
<span className="logo-text">iMeeting</span>
</div>
<h1>提示词仓库</h1>
</div>
</header>
<div className="prompt-content">
<div className="prompt-wrapper">
<PromptManagement />
</div>
</div>
</div>
);
};
export default PromptManagementPage;