优化了样式
parent
230a77c3cf
commit
2dc066a2ce
|
|
@ -15,20 +15,20 @@ mkdir -p logs
|
||||||
|
|
||||||
# 停止并删除现有容器
|
# 停止并删除现有容器
|
||||||
echo "📦 停止现有容器..."
|
echo "📦 停止现有容器..."
|
||||||
docker-compose -f docker-compose.prod.yml down
|
docker compose -f docker-compose.prod.yml down
|
||||||
|
|
||||||
# 构建新镜像
|
# 构建新镜像
|
||||||
echo "🔨 构建Docker镜像..."
|
echo "🔨 构建Docker镜像..."
|
||||||
docker-compose -f docker-compose.prod.yml build --no-cache
|
docker compose --progress=plain -f docker-compose.prod.yml build --no-cache
|
||||||
|
|
||||||
# 启动服务
|
# 启动服务
|
||||||
echo "▶️ 启动PM2服务..."
|
echo "▶️ 启动PM2服务..."
|
||||||
docker-compose -f docker-compose.prod.yml up -d
|
docker compose -f docker-compose.prod.yml up -d
|
||||||
|
|
||||||
# 检查服务状态
|
# 检查服务状态
|
||||||
echo "🔍 检查服务状态..."
|
echo "🔍 检查服务状态..."
|
||||||
sleep 15
|
sleep 15
|
||||||
docker-compose -f docker-compose.prod.yml ps
|
docker compose -f docker-compose.prod.yml ps
|
||||||
|
|
||||||
# 检查PM2进程状态
|
# 检查PM2进程状态
|
||||||
echo "🔄 检查PM2进程状态..."
|
echo "🔄 检查PM2进程状态..."
|
||||||
|
|
@ -51,4 +51,4 @@ echo " ✅ 集群模式(2个实例)"
|
||||||
echo " ✅ 自动重启和故障恢复"
|
echo " ✅ 自动重启和故障恢复"
|
||||||
echo " ✅ 内存限制保护(1GB)"
|
echo " ✅ 内存限制保护(1GB)"
|
||||||
echo " ✅ 详细日志管理"
|
echo " ✅ 详细日志管理"
|
||||||
echo " ✅ 进程监控和健康检查"
|
echo " ✅ 进程监控和健康检查"
|
||||||
|
|
|
||||||
|
|
@ -244,6 +244,92 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Dropdown Menu Styles */
|
||||||
|
.meeting-actions {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-trigger {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0.5rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-trigger:hover {
|
||||||
|
background: #f1f5f9;
|
||||||
|
color: #334155;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-menu {
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
right: 0;
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
padding: 0.5rem;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
min-width: 120px;
|
||||||
|
z-index: 10;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #374151;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 确保Link组件内的dropdown-item正确显示 */
|
||||||
|
.dropdown-menu a {
|
||||||
|
text-decoration: none;
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-menu a .dropdown-item {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item:hover {
|
||||||
|
background: #f3f4f6;
|
||||||
|
color: #111827;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item.delete-item {
|
||||||
|
color: #dc2626;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item.delete-item:hover {
|
||||||
|
background: #fef2f2;
|
||||||
|
color: #b91c1c;
|
||||||
|
}
|
||||||
|
|
||||||
.meeting-content {
|
.meeting-content {
|
||||||
background: white;
|
background: white;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link, useNavigate } from 'react-router-dom';
|
||||||
import { Clock, Users, FileText, Calendar, User, Edit, Trash2, MoreVertical } from 'lucide-react';
|
import { Clock, Users, FileText, User, Edit, Calendar , Trash2, MoreVertical } from 'lucide-react';
|
||||||
import ReactMarkdown from 'react-markdown';
|
import ReactMarkdown from 'react-markdown';
|
||||||
import remarkGfm from 'remark-gfm';
|
import remarkGfm from 'remark-gfm';
|
||||||
import rehypeRaw from 'rehype-raw';
|
import rehypeRaw from 'rehype-raw';
|
||||||
|
|
@ -11,7 +11,7 @@ import './MeetingTimeline.css';
|
||||||
const MeetingTimeline = ({ meetingsByDate, currentUser, onDeleteMeeting }) => {
|
const MeetingTimeline = ({ meetingsByDate, currentUser, onDeleteMeeting }) => {
|
||||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(null);
|
const [showDeleteConfirm, setShowDeleteConfirm] = useState(null);
|
||||||
const [showDropdown, setShowDropdown] = useState(null);
|
const [showDropdown, setShowDropdown] = useState(null);
|
||||||
|
const navigate = useNavigate();
|
||||||
// Close dropdown when clicking outside
|
// Close dropdown when clicking outside
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const handleClickOutside = () => {
|
const handleClickOutside = () => {
|
||||||
|
|
@ -66,6 +66,12 @@ const MeetingTimeline = ({ meetingsByDate, currentUser, onDeleteMeeting }) => {
|
||||||
return lines.length > maxLines || summary.length > maxLength;
|
return lines.length > maxLines || summary.length > maxLength;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleEditClick = (meetingId, e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
navigate(`/meetings/edit/${meetingId}`)
|
||||||
|
setShowDropdown(null);
|
||||||
|
};
|
||||||
|
|
||||||
const handleDeleteClick = (meetingId, e) => {
|
const handleDeleteClick = (meetingId, e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setShowDeleteConfirm(meetingId);
|
setShowDeleteConfirm(meetingId);
|
||||||
|
|
@ -141,15 +147,12 @@ const MeetingTimeline = ({ meetingsByDate, currentUser, onDeleteMeeting }) => {
|
||||||
</button>
|
</button>
|
||||||
{showDropdown === meeting.meeting_id && (
|
{showDropdown === meeting.meeting_id && (
|
||||||
<div className="dropdown-menu" onClick={(e) => e.stopPropagation()}>
|
<div className="dropdown-menu" onClick={(e) => e.stopPropagation()}>
|
||||||
<Link
|
<button className="dropdown-item"
|
||||||
to={`/meetings/edit/${meeting.meeting_id}`}
|
onClick={(e) =>handleEditClick(meeting.meeting_id, e)}
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
>
|
>
|
||||||
<span className="dropdown-item">
|
<Edit size={16} />
|
||||||
<Edit size={16} />
|
编辑
|
||||||
编辑
|
</button>
|
||||||
</span>
|
|
||||||
</Link>
|
|
||||||
<button
|
<button
|
||||||
className="dropdown-item delete-item"
|
className="dropdown-item delete-item"
|
||||||
onClick={(e) => handleDeleteClick(meeting.meeting_id, e)}
|
onClick={(e) => handleDeleteClick(meeting.meeting_id, e)}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,250 @@
|
||||||
|
/* System Configuration Styles */
|
||||||
|
.system-configuration {
|
||||||
|
padding: 2rem;
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-header {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
border-bottom: 2px solid #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-header h2 {
|
||||||
|
margin: 0 0 0.5rem 0;
|
||||||
|
color: #1e293b;
|
||||||
|
font-size: 1.75rem;
|
||||||
|
font-weight: 700;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-header p {
|
||||||
|
margin: 0;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 2rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-block {
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-block-header {
|
||||||
|
padding: 1.5rem;
|
||||||
|
background: linear-gradient(135deg, #f8fafc, #e2e8f0);
|
||||||
|
border-bottom: 1px solid #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-block-header h3 {
|
||||||
|
margin: 0 0 0.5rem 0;
|
||||||
|
color: #334155;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 600;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-block-header p {
|
||||||
|
margin: 0;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-block-content {
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-form-group {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-form-group:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-form-group label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #374151;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-form-group .label-hint {
|
||||||
|
color: #6b7280;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
margin-left: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.75rem;
|
||||||
|
border: 2px solid #e2e8f0;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
box-sizing: border-box;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #3b82f6;
|
||||||
|
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-textarea {
|
||||||
|
min-height: 120px;
|
||||||
|
resize: vertical;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-input-hint {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: #6b7280;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-actions {
|
||||||
|
padding: 1.5rem 2rem;
|
||||||
|
background: #f8fafc;
|
||||||
|
border-top: 1px solid #e2e8f0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
border: none;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-btn-secondary {
|
||||||
|
background: #e2e8f0;
|
||||||
|
color: #475569;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-btn-secondary:hover {
|
||||||
|
background: #cbd5e1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-btn-primary {
|
||||||
|
background: linear-gradient(135deg, #3b82f6, #1e40af);
|
||||||
|
color: white;
|
||||||
|
box-shadow: 0 2px 4px rgba(59, 130, 246, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-btn-primary:hover:not(:disabled) {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 8px rgba(59, 130, 246, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-btn-primary:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spinner {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border: 2px solid transparent;
|
||||||
|
border-top: 2px solid currentColor;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
from { transform: rotate(0deg); }
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-message {
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-message.success {
|
||||||
|
background: #f0f9ff;
|
||||||
|
border: 1px solid #bae6fd;
|
||||||
|
color: #0c4a6e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-message.error {
|
||||||
|
background: #fef2f2;
|
||||||
|
border: 1px solid #fecaca;
|
||||||
|
color: #991b1b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-message.loading {
|
||||||
|
background: #f8fafc;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
color: #475569;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive Design */
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.config-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.system-configuration {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-header h2 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-block-header {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-block-content {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-actions {
|
||||||
|
padding: 1rem;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-btn {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,10 +1,272 @@
|
||||||
import React from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { Settings, Brain, Shield, Save, RotateCcw, CheckCircle, AlertCircle, Loader } from 'lucide-react';
|
||||||
|
import apiClient from '../../utils/apiClient';
|
||||||
|
import { buildApiUrl, API_ENDPOINTS } from '../../config/api';
|
||||||
|
import './SystemConfiguration.css';
|
||||||
|
|
||||||
const SystemConfiguration = () => {
|
const SystemConfiguration = () => {
|
||||||
|
const [configs, setConfigs] = useState({
|
||||||
|
model_name: '',
|
||||||
|
system_prompt: '',
|
||||||
|
DEFAULT_RESET_PASSWORD: '',
|
||||||
|
MAX_FILE_SIZE: 0,
|
||||||
|
MAX_IMAGE_SIZE: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
const [originalConfigs, setOriginalConfigs] = useState({});
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [saving, setSaving] = useState(false);
|
||||||
|
const [message, setMessage] = useState({ type: '', text: '' });
|
||||||
|
|
||||||
|
// 工具函数:字节转MB
|
||||||
|
const bytesToMB = (bytes) => {
|
||||||
|
return Math.round(bytes / (1024 * 1024));
|
||||||
|
};
|
||||||
|
|
||||||
|
// 工具函数:MB转字节
|
||||||
|
const mbToBytes = (mb) => {
|
||||||
|
return mb * 1024 * 1024;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 加载配置数据
|
||||||
|
useEffect(() => {
|
||||||
|
fetchConfigs();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const fetchConfigs = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.ADMIN.SYSTEM_CONFIG));
|
||||||
|
const configData = response.data;
|
||||||
|
setConfigs(configData);
|
||||||
|
setOriginalConfigs(configData);
|
||||||
|
setMessage({ type: '', text: '' });
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to fetch configurations:', err);
|
||||||
|
setMessage({
|
||||||
|
type: 'error',
|
||||||
|
text: '加载配置失败:' + (err.response?.data?.detail || err.message)
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInputChange = (key, value) => {
|
||||||
|
if (key === 'MAX_FILE_SIZE' || key === 'MAX_IMAGE_SIZE') {
|
||||||
|
// 对于文件大小,输入的是MB,需要转换为字节
|
||||||
|
const numValue = parseInt(value) || 0;
|
||||||
|
setConfigs(prev => ({
|
||||||
|
...prev,
|
||||||
|
[key]: mbToBytes(numValue)
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
setConfigs(prev => ({
|
||||||
|
...prev,
|
||||||
|
[key]: value
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
try {
|
||||||
|
setSaving(true);
|
||||||
|
setMessage({ type: 'loading', text: '保存配置中...' });
|
||||||
|
|
||||||
|
await apiClient.put(buildApiUrl(API_ENDPOINTS.ADMIN.SYSTEM_CONFIG), configs);
|
||||||
|
|
||||||
|
setOriginalConfigs(configs);
|
||||||
|
setMessage({ type: 'success', text: '配置保存成功!' });
|
||||||
|
|
||||||
|
// 3秒后清除成功消息
|
||||||
|
setTimeout(() => {
|
||||||
|
setMessage({ type: '', text: '' });
|
||||||
|
}, 3000);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to save configurations:', err);
|
||||||
|
setMessage({
|
||||||
|
type: 'error',
|
||||||
|
text: '保存配置失败:' + (err.response?.data?.detail || err.message)
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setSaving(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleReset = () => {
|
||||||
|
setConfigs(originalConfigs);
|
||||||
|
setMessage({ type: '', text: '' });
|
||||||
|
};
|
||||||
|
|
||||||
|
const hasChanges = JSON.stringify(configs) !== JSON.stringify(originalConfigs);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="system-configuration">
|
||||||
|
<div className="config-message loading">
|
||||||
|
<Loader className="loading-spinner" />
|
||||||
|
加载配置数据中...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="system-configuration">
|
||||||
<h2>系统配置</h2>
|
<div className="config-header">
|
||||||
<p>系统相关配置项。</p>
|
<h2>
|
||||||
|
<Settings />
|
||||||
|
系统配置
|
||||||
|
</h2>
|
||||||
|
<p>管理系统的核心配置参数,包括模型配置和管理员设置。</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{message.text && (
|
||||||
|
<div className={`config-message ${message.type}`}>
|
||||||
|
{message.type === 'success' && <CheckCircle size={16} />}
|
||||||
|
{message.type === 'error' && <AlertCircle size={16} />}
|
||||||
|
{message.type === 'loading' && <Loader className="loading-spinner" size={16} />}
|
||||||
|
{message.text}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="config-grid">
|
||||||
|
{/* 模型配置块 */}
|
||||||
|
<div className="config-block">
|
||||||
|
<div className="config-block-header">
|
||||||
|
<h3>
|
||||||
|
<Brain />
|
||||||
|
模型配置
|
||||||
|
</h3>
|
||||||
|
<p>配置AI模型相关参数</p>
|
||||||
|
</div>
|
||||||
|
<div className="config-block-content">
|
||||||
|
<div className="config-form-group">
|
||||||
|
<label>
|
||||||
|
模型名称
|
||||||
|
<span className="label-hint">(model_name)</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="config-input"
|
||||||
|
value={configs.model_name}
|
||||||
|
onChange={(e) => handleInputChange('model_name', e.target.value)}
|
||||||
|
placeholder="请输入模型名称"
|
||||||
|
/>
|
||||||
|
<div className="config-input-hint">
|
||||||
|
指定要使用的AI模型名称,例如:gpt-4, claude-3-sonnet等
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="config-form-group">
|
||||||
|
<label>
|
||||||
|
系统提示词
|
||||||
|
<span className="label-hint">(system_prompt)</span>
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
className="config-input config-textarea"
|
||||||
|
value={configs.system_prompt}
|
||||||
|
onChange={(e) => handleInputChange('system_prompt', e.target.value)}
|
||||||
|
placeholder="请输入系统提示词"
|
||||||
|
rows={6}
|
||||||
|
/>
|
||||||
|
<div className="config-input-hint">
|
||||||
|
定义AI助手的行为和回答风格,这将影响所有AI生成的内容
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 管理配置块 */}
|
||||||
|
<div className="config-block">
|
||||||
|
<div className="config-block-header">
|
||||||
|
<h3>
|
||||||
|
<Shield />
|
||||||
|
管理配置
|
||||||
|
</h3>
|
||||||
|
<p>系统管理相关设置</p>
|
||||||
|
</div>
|
||||||
|
<div className="config-block-content">
|
||||||
|
<div className="config-form-group">
|
||||||
|
<label>
|
||||||
|
默认重置密码
|
||||||
|
<span className="label-hint">(DEFAULT_RESET_PASSWORD)</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="config-input"
|
||||||
|
value={configs.DEFAULT_RESET_PASSWORD}
|
||||||
|
onChange={(e) => handleInputChange('DEFAULT_RESET_PASSWORD', e.target.value)}
|
||||||
|
placeholder="请输入默认密码"
|
||||||
|
/>
|
||||||
|
<div className="config-input-hint">
|
||||||
|
管理员重置用户密码时使用的默认密码,建议设置为安全的临时密码
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="config-form-group">
|
||||||
|
<label>
|
||||||
|
最大文件大小
|
||||||
|
<span className="label-hint">(MAX_FILE_SIZE)</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
className="config-input"
|
||||||
|
value={bytesToMB(configs.MAX_FILE_SIZE)}
|
||||||
|
onChange={(e) => handleInputChange('MAX_FILE_SIZE', e.target.value)}
|
||||||
|
placeholder="请输入文件大小限制(MB)"
|
||||||
|
min="1"
|
||||||
|
max="1000"
|
||||||
|
/>
|
||||||
|
<div className="config-input-hint">
|
||||||
|
用户上传音频文件的大小限制,单位为MB,建议设置为50-200MB
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="config-form-group">
|
||||||
|
<label>
|
||||||
|
最大图片大小
|
||||||
|
<span className="label-hint">(MAX_IMAGE_SIZE)</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
className="config-input"
|
||||||
|
value={bytesToMB(configs.MAX_IMAGE_SIZE)}
|
||||||
|
onChange={(e) => handleInputChange('MAX_IMAGE_SIZE', e.target.value)}
|
||||||
|
placeholder="请输入图片大小限制(MB)"
|
||||||
|
min="1"
|
||||||
|
max="100"
|
||||||
|
/>
|
||||||
|
<div className="config-input-hint">
|
||||||
|
用户上传图片的大小限制,单位为MB,建议设置为5-20MB
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="config-actions">
|
||||||
|
<button
|
||||||
|
className="config-btn config-btn-secondary"
|
||||||
|
onClick={handleReset}
|
||||||
|
disabled={!hasChanges || saving}
|
||||||
|
>
|
||||||
|
<RotateCcw size={16} />
|
||||||
|
重置更改
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="config-btn config-btn-primary"
|
||||||
|
onClick={handleSave}
|
||||||
|
disabled={!hasChanges || saving}
|
||||||
|
>
|
||||||
|
{saving ? (
|
||||||
|
<Loader className="loading-spinner" size={16} />
|
||||||
|
) : (
|
||||||
|
<Save size={16} />
|
||||||
|
)}
|
||||||
|
{saving ? '保存中...' : '保存配置'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,12 @@ const API_CONFIG = {
|
||||||
UPLOAD_AUDIO: '/api/meetings/upload-audio',
|
UPLOAD_AUDIO: '/api/meetings/upload-audio',
|
||||||
UPLOAD_IMAGE: (meetingId) => `/api/meetings/${meetingId}/upload-image`,
|
UPLOAD_IMAGE: (meetingId) => `/api/meetings/${meetingId}/upload-image`,
|
||||||
REGENERATE_SUMMARY: (meetingId) => `/api/meetings/${meetingId}/regenerate-summary`
|
REGENERATE_SUMMARY: (meetingId) => `/api/meetings/${meetingId}/regenerate-summary`
|
||||||
|
},
|
||||||
|
ADMIN: {
|
||||||
|
SYSTEM_CONFIG: '/api/admin/system-config'
|
||||||
|
},
|
||||||
|
TAGS: {
|
||||||
|
LIST: '/api/tags'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { Link, useNavigate } from 'react-router-dom';
|
import { Link, useNavigate } from 'react-router-dom';
|
||||||
import apiClient from '../utils/apiClient';
|
import apiClient from '../utils/apiClient';
|
||||||
|
import configService from '../utils/configService';
|
||||||
import { ArrowLeft, Upload, Users, Calendar, FileText, X, User, Plus, Tag } from 'lucide-react';
|
import { ArrowLeft, Upload, Users, Calendar, FileText, X, User, Plus, Tag } from 'lucide-react';
|
||||||
import { buildApiUrl, API_ENDPOINTS } from '../config/api';
|
import { buildApiUrl, API_ENDPOINTS } from '../config/api';
|
||||||
import DateTimePicker from '../components/DateTimePicker';
|
import DateTimePicker from '../components/DateTimePicker';
|
||||||
|
|
@ -21,11 +22,22 @@ const CreateMeeting = ({ user }) => {
|
||||||
const [audioFile, setAudioFile] = useState(null);
|
const [audioFile, setAudioFile] = useState(null);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
|
const [maxFileSize, setMaxFileSize] = useState(100 * 1024 * 1024); // 默认100MB
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchUsers();
|
fetchUsers();
|
||||||
|
loadMaxFileSize();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const loadMaxFileSize = async () => {
|
||||||
|
try {
|
||||||
|
const size = await configService.getMaxFileSize();
|
||||||
|
setMaxFileSize(size);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to load max file size config:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const fetchUsers = async () => {
|
const fetchUsers = async () => {
|
||||||
try {
|
try {
|
||||||
// 获取所有用户,设置较大的size参数
|
// 获取所有用户,设置较大的size参数
|
||||||
|
|
@ -74,9 +86,10 @@ const CreateMeeting = ({ user }) => {
|
||||||
setError('请上传支持的音频格式 (MP3, WAV, M4A)');
|
setError('请上传支持的音频格式 (MP3, WAV, M4A)');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Check file size (max 100MB)
|
// Check file size using dynamic config
|
||||||
if (file.size > 100 * 1024 * 1024) {
|
if (file.size > maxFileSize) {
|
||||||
setError('音频文件大小不能超过100MB');
|
const maxSizeMB = Math.round(maxFileSize / (1024 * 1024));
|
||||||
|
setError(`音频文件大小不能超过${maxSizeMB}MB`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setAudioFile(file);
|
setAudioFile(file);
|
||||||
|
|
@ -276,7 +289,7 @@ const CreateMeeting = ({ user }) => {
|
||||||
<label htmlFor="audio-file" className="file-upload-label">
|
<label htmlFor="audio-file" className="file-upload-label">
|
||||||
<Plus size={20} />
|
<Plus size={20} />
|
||||||
<span>选择音频文件</span>
|
<span>选择音频文件</span>
|
||||||
<small>支持 MP3, WAV, M4A 格式,最大100MB</small>
|
<small>支持 MP3, WAV, M4A 格式</small>
|
||||||
</label>
|
</label>
|
||||||
{audioFile && (
|
{audioFile && (
|
||||||
<div className="selected-file">
|
<div className="selected-file">
|
||||||
|
|
|
||||||
|
|
@ -96,7 +96,7 @@ const Dashboard = ({ user, onLogout }) => {
|
||||||
setUserInfo(userResponse.data);
|
setUserInfo(userResponse.data);
|
||||||
|
|
||||||
const meetingsResponse = await apiClient.get(buildApiUrl(`${API_ENDPOINTS.MEETINGS.LIST}?user_id=${user.user_id}`));
|
const meetingsResponse = await apiClient.get(buildApiUrl(`${API_ENDPOINTS.MEETINGS.LIST}?user_id=${user.user_id}`));
|
||||||
console.log('Meetings response:', meetingsResponse.data);
|
//console.log('Meetings response:', meetingsResponse.data);
|
||||||
setMeetings(meetingsResponse.data);
|
setMeetings(meetingsResponse.data);
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
||||||
import { Link, useNavigate, useParams } from 'react-router-dom';
|
import { Link, useNavigate, useParams } from 'react-router-dom';
|
||||||
import apiClient from '../utils/apiClient';
|
import apiClient from '../utils/apiClient';
|
||||||
|
import configService from '../utils/configService';
|
||||||
import { ArrowLeft, Users, Calendar, FileText, X, User, Save, Upload, Plus, Image, Tag } from 'lucide-react';
|
import { ArrowLeft, Users, Calendar, FileText, X, User, Save, Upload, Plus, Image, Tag } from 'lucide-react';
|
||||||
import MDEditor, * as commands from '@uiw/react-md-editor';
|
import MDEditor, * as commands from '@uiw/react-md-editor';
|
||||||
import '@uiw/react-md-editor/markdown-editor.css';
|
import '@uiw/react-md-editor/markdown-editor.css';
|
||||||
|
|
@ -32,6 +33,8 @@ const EditMeeting = ({ user }) => {
|
||||||
const [meeting, setMeeting] = useState(null);
|
const [meeting, setMeeting] = useState(null);
|
||||||
const [showUploadArea, setShowUploadArea] = useState(false);
|
const [showUploadArea, setShowUploadArea] = useState(false);
|
||||||
const [showUploadConfirm, setShowUploadConfirm] = useState(false);
|
const [showUploadConfirm, setShowUploadConfirm] = useState(false);
|
||||||
|
const [maxFileSize, setMaxFileSize] = useState(100 * 1024 * 1024); // 默认100MB
|
||||||
|
const [maxImageSize, setMaxImageSize] = useState(10 * 1024 * 1024); // 默认10MB
|
||||||
|
|
||||||
const handleSummaryChange = useCallback((value) => {
|
const handleSummaryChange = useCallback((value) => {
|
||||||
setFormData(prev => ({ ...prev, summary: value || '' }));
|
setFormData(prev => ({ ...prev, summary: value || '' }));
|
||||||
|
|
@ -40,8 +43,20 @@ const EditMeeting = ({ user }) => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchMeetingData();
|
fetchMeetingData();
|
||||||
fetchUsers();
|
fetchUsers();
|
||||||
|
loadFileSizeConfig();
|
||||||
}, [meeting_id]);
|
}, [meeting_id]);
|
||||||
|
|
||||||
|
const loadFileSizeConfig = async () => {
|
||||||
|
try {
|
||||||
|
const fileSize = await configService.getMaxFileSize();
|
||||||
|
const imageSize = await configService.getMaxImageSize();
|
||||||
|
setMaxFileSize(fileSize);
|
||||||
|
setMaxImageSize(imageSize);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to load file size config:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const fetchMeetingData = async () => {
|
const fetchMeetingData = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.MEETINGS.EDIT(meeting_id)));
|
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.MEETINGS.EDIT(meeting_id)));
|
||||||
|
|
@ -117,9 +132,10 @@ const EditMeeting = ({ user }) => {
|
||||||
setError('请上传支持的音频格式 (MP3, WAV, M4A)');
|
setError('请上传支持的音频格式 (MP3, WAV, M4A)');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Check file size (max 100MB)
|
// Check file size using dynamic config
|
||||||
if (file.size > 100 * 1024 * 1024) {
|
if (file.size > maxFileSize) {
|
||||||
setError('音频文件大小不能超过100MB');
|
const maxSizeMB = Math.round(maxFileSize / (1024 * 1024));
|
||||||
|
setError(`音频文件大小不能超过${maxSizeMB}MB`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setAudioFile(file);
|
setAudioFile(file);
|
||||||
|
|
@ -201,9 +217,10 @@ const EditMeeting = ({ user }) => {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate file size (10MB)
|
// Validate file size using dynamic config
|
||||||
if (file.size > 10 * 1024 * 1024) {
|
if (file.size > maxImageSize) {
|
||||||
setError('图片大小不能超过10MB');
|
const maxSizeMB = Math.round(maxImageSize / (1024 * 1024));
|
||||||
|
setError(`图片大小不能超过${maxSizeMB}MB`);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -506,7 +523,7 @@ const EditMeeting = ({ user }) => {
|
||||||
<label htmlFor="audio-file" className="file-upload-label">
|
<label htmlFor="audio-file" className="file-upload-label">
|
||||||
<Plus size={20} />
|
<Plus size={20} />
|
||||||
<span>选择新的音频文件</span>
|
<span>选择新的音频文件</span>
|
||||||
<small>支持 MP3, WAV, M4A 格式,最大100MB</small>
|
<small>支持 MP3, WAV, M4A 格式</small>
|
||||||
</label>
|
</label>
|
||||||
{audioFile && (
|
{audioFile && (
|
||||||
<div className="selected-file">
|
<div className="selected-file">
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
import apiClient from './apiClient';
|
||||||
|
import { buildApiUrl, API_ENDPOINTS } from '../config/api';
|
||||||
|
|
||||||
|
class ConfigService {
|
||||||
|
constructor() {
|
||||||
|
this.configs = null;
|
||||||
|
this.loadPromise = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getConfigs() {
|
||||||
|
if (this.configs) {
|
||||||
|
return this.configs;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.loadPromise) {
|
||||||
|
return this.loadPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loadPromise = this.loadConfigsFromServer();
|
||||||
|
this.configs = await this.loadPromise;
|
||||||
|
return this.configs;
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadConfigsFromServer() {
|
||||||
|
try {
|
||||||
|
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.ADMIN.SYSTEM_CONFIG));
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to load system configs, using defaults:', error);
|
||||||
|
// 返回默认配置
|
||||||
|
return {
|
||||||
|
MAX_FILE_SIZE: 100 * 1024 * 1024, // 100MB
|
||||||
|
MAX_IMAGE_SIZE: 10 * 1024 * 1024 // 10MB
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getMaxFileSize() {
|
||||||
|
const configs = await this.getConfigs();
|
||||||
|
return configs.MAX_FILE_SIZE || 100 * 1024 * 1024;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getMaxImageSize() {
|
||||||
|
const configs = await this.getConfigs();
|
||||||
|
return configs.MAX_IMAGE_SIZE || 10 * 1024 * 1024;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化文件大小为可读格式
|
||||||
|
formatFileSize(bytes) {
|
||||||
|
if (bytes === 0) return '0 Bytes';
|
||||||
|
const k = 1024;
|
||||||
|
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清除缓存的配置(用于配置更新后)
|
||||||
|
clearCache() {
|
||||||
|
this.configs = null;
|
||||||
|
this.loadPromise = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const configService = new ConfigService();
|
||||||
|
export default configService;
|
||||||
Loading…
Reference in New Issue