优化了样式
parent
230a77c3cf
commit
2dc066a2ce
|
|
@ -15,20 +15,20 @@ mkdir -p logs
|
|||
|
||||
# 停止并删除现有容器
|
||||
echo "📦 停止现有容器..."
|
||||
docker-compose -f docker-compose.prod.yml down
|
||||
docker compose -f docker-compose.prod.yml down
|
||||
|
||||
# 构建新镜像
|
||||
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服务..."
|
||||
docker-compose -f docker-compose.prod.yml up -d
|
||||
docker compose -f docker-compose.prod.yml up -d
|
||||
|
||||
# 检查服务状态
|
||||
echo "🔍 检查服务状态..."
|
||||
sleep 15
|
||||
docker-compose -f docker-compose.prod.yml ps
|
||||
docker compose -f docker-compose.prod.yml ps
|
||||
|
||||
# 检查PM2进程状态
|
||||
echo "🔄 检查PM2进程状态..."
|
||||
|
|
@ -51,4 +51,4 @@ echo " ✅ 集群模式(2个实例)"
|
|||
echo " ✅ 自动重启和故障恢复"
|
||||
echo " ✅ 内存限制保护(1GB)"
|
||||
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 {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React, { useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Clock, Users, FileText, Calendar, User, Edit, Trash2, MoreVertical } from 'lucide-react';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { Clock, Users, FileText, User, Edit, Calendar , Trash2, MoreVertical } from 'lucide-react';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import rehypeRaw from 'rehype-raw';
|
||||
|
|
@ -11,7 +11,7 @@ import './MeetingTimeline.css';
|
|||
const MeetingTimeline = ({ meetingsByDate, currentUser, onDeleteMeeting }) => {
|
||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(null);
|
||||
const [showDropdown, setShowDropdown] = useState(null);
|
||||
|
||||
const navigate = useNavigate();
|
||||
// Close dropdown when clicking outside
|
||||
React.useEffect(() => {
|
||||
const handleClickOutside = () => {
|
||||
|
|
@ -66,6 +66,12 @@ const MeetingTimeline = ({ meetingsByDate, currentUser, onDeleteMeeting }) => {
|
|||
return lines.length > maxLines || summary.length > maxLength;
|
||||
};
|
||||
|
||||
const handleEditClick = (meetingId, e) => {
|
||||
e.preventDefault();
|
||||
navigate(`/meetings/edit/${meetingId}`)
|
||||
setShowDropdown(null);
|
||||
};
|
||||
|
||||
const handleDeleteClick = (meetingId, e) => {
|
||||
e.preventDefault();
|
||||
setShowDeleteConfirm(meetingId);
|
||||
|
|
@ -141,15 +147,12 @@ const MeetingTimeline = ({ meetingsByDate, currentUser, onDeleteMeeting }) => {
|
|||
</button>
|
||||
{showDropdown === meeting.meeting_id && (
|
||||
<div className="dropdown-menu" onClick={(e) => e.stopPropagation()}>
|
||||
<Link
|
||||
to={`/meetings/edit/${meeting.meeting_id}`}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
<button className="dropdown-item"
|
||||
onClick={(e) =>handleEditClick(meeting.meeting_id, e)}
|
||||
>
|
||||
<span className="dropdown-item">
|
||||
<Edit size={16} />
|
||||
编辑
|
||||
</span>
|
||||
</Link>
|
||||
<Edit size={16} />
|
||||
编辑
|
||||
</button>
|
||||
<button
|
||||
className="dropdown-item delete-item"
|
||||
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 [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 (
|
||||
<div>
|
||||
<h2>系统配置</h2>
|
||||
<p>系统相关配置项。</p>
|
||||
<div className="system-configuration">
|
||||
<div className="config-header">
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -31,6 +31,12 @@ const API_CONFIG = {
|
|||
UPLOAD_AUDIO: '/api/meetings/upload-audio',
|
||||
UPLOAD_IMAGE: (meetingId) => `/api/meetings/${meetingId}/upload-image`,
|
||||
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 { Link, useNavigate } from 'react-router-dom';
|
||||
import apiClient from '../utils/apiClient';
|
||||
import configService from '../utils/configService';
|
||||
import { ArrowLeft, Upload, Users, Calendar, FileText, X, User, Plus, Tag } from 'lucide-react';
|
||||
import { buildApiUrl, API_ENDPOINTS } from '../config/api';
|
||||
import DateTimePicker from '../components/DateTimePicker';
|
||||
|
|
@ -21,11 +22,22 @@ const CreateMeeting = ({ user }) => {
|
|||
const [audioFile, setAudioFile] = useState(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [maxFileSize, setMaxFileSize] = useState(100 * 1024 * 1024); // 默认100MB
|
||||
|
||||
useEffect(() => {
|
||||
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 () => {
|
||||
try {
|
||||
// 获取所有用户,设置较大的size参数
|
||||
|
|
@ -74,9 +86,10 @@ const CreateMeeting = ({ user }) => {
|
|||
setError('请上传支持的音频格式 (MP3, WAV, M4A)');
|
||||
return;
|
||||
}
|
||||
// Check file size (max 100MB)
|
||||
if (file.size > 100 * 1024 * 1024) {
|
||||
setError('音频文件大小不能超过100MB');
|
||||
// Check file size using dynamic config
|
||||
if (file.size > maxFileSize) {
|
||||
const maxSizeMB = Math.round(maxFileSize / (1024 * 1024));
|
||||
setError(`音频文件大小不能超过${maxSizeMB}MB`);
|
||||
return;
|
||||
}
|
||||
setAudioFile(file);
|
||||
|
|
@ -276,7 +289,7 @@ const CreateMeeting = ({ user }) => {
|
|||
<label htmlFor="audio-file" className="file-upload-label">
|
||||
<Plus size={20} />
|
||||
<span>选择音频文件</span>
|
||||
<small>支持 MP3, WAV, M4A 格式,最大100MB</small>
|
||||
<small>支持 MP3, WAV, M4A 格式</small>
|
||||
</label>
|
||||
{audioFile && (
|
||||
<div className="selected-file">
|
||||
|
|
|
|||
|
|
@ -96,7 +96,7 @@ const Dashboard = ({ user, onLogout }) => {
|
|||
setUserInfo(userResponse.data);
|
||||
|
||||
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);
|
||||
|
||||
} catch (err) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import { Link, useNavigate, useParams } from 'react-router-dom';
|
||||
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 MDEditor, * as commands from '@uiw/react-md-editor';
|
||||
import '@uiw/react-md-editor/markdown-editor.css';
|
||||
|
|
@ -32,6 +33,8 @@ const EditMeeting = ({ user }) => {
|
|||
const [meeting, setMeeting] = useState(null);
|
||||
const [showUploadArea, setShowUploadArea] = 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) => {
|
||||
setFormData(prev => ({ ...prev, summary: value || '' }));
|
||||
|
|
@ -40,8 +43,20 @@ const EditMeeting = ({ user }) => {
|
|||
useEffect(() => {
|
||||
fetchMeetingData();
|
||||
fetchUsers();
|
||||
loadFileSizeConfig();
|
||||
}, [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 () => {
|
||||
try {
|
||||
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.MEETINGS.EDIT(meeting_id)));
|
||||
|
|
@ -117,9 +132,10 @@ const EditMeeting = ({ user }) => {
|
|||
setError('请上传支持的音频格式 (MP3, WAV, M4A)');
|
||||
return;
|
||||
}
|
||||
// Check file size (max 100MB)
|
||||
if (file.size > 100 * 1024 * 1024) {
|
||||
setError('音频文件大小不能超过100MB');
|
||||
// Check file size using dynamic config
|
||||
if (file.size > maxFileSize) {
|
||||
const maxSizeMB = Math.round(maxFileSize / (1024 * 1024));
|
||||
setError(`音频文件大小不能超过${maxSizeMB}MB`);
|
||||
return;
|
||||
}
|
||||
setAudioFile(file);
|
||||
|
|
@ -201,9 +217,10 @@ const EditMeeting = ({ user }) => {
|
|||
return null;
|
||||
}
|
||||
|
||||
// Validate file size (10MB)
|
||||
if (file.size > 10 * 1024 * 1024) {
|
||||
setError('图片大小不能超过10MB');
|
||||
// Validate file size using dynamic config
|
||||
if (file.size > maxImageSize) {
|
||||
const maxSizeMB = Math.round(maxImageSize / (1024 * 1024));
|
||||
setError(`图片大小不能超过${maxSizeMB}MB`);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -506,7 +523,7 @@ const EditMeeting = ({ user }) => {
|
|||
<label htmlFor="audio-file" className="file-upload-label">
|
||||
<Plus size={20} />
|
||||
<span>选择新的音频文件</span>
|
||||
<small>支持 MP3, WAV, M4A 格式,最大100MB</small>
|
||||
<small>支持 MP3, WAV, M4A 格式</small>
|
||||
</label>
|
||||
{audioFile && (
|
||||
<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