增加了客户端管理模块
parent
1977477fd3
commit
493e632c1f
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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}`
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
Loading…
Reference in New Issue