更新分享页面样式与道路页面描述
parent
a99cc389c8
commit
181f4565b4
|
|
@ -9,6 +9,7 @@ from app.core.database import get_db_connection
|
||||||
from app.core.response import create_api_response
|
from app.core.response import create_api_response
|
||||||
from app.services.async_transcription_service import AsyncTranscriptionService
|
from app.services.async_transcription_service import AsyncTranscriptionService
|
||||||
from app.services.llm_service import LLMService
|
from app.services.llm_service import LLMService
|
||||||
|
from app.services.system_config_service import SystemConfigService
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
llm_service = LLMService()
|
llm_service = LLMService()
|
||||||
|
|
@ -800,6 +801,18 @@ async def test_audio_model_config(request: AudioModelTestRequest, current_user=D
|
||||||
return create_api_response(code="500", message=f"音频模型测试失败: {str(e)}")
|
return create_api_response(code="500", message=f"音频模型测试失败: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/system-config/public")
|
||||||
|
async def get_public_system_config():
|
||||||
|
try:
|
||||||
|
return create_api_response(
|
||||||
|
code="200",
|
||||||
|
message="获取公开配置成功",
|
||||||
|
data=SystemConfigService.get_branding_config()
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
return create_api_response(code="500", message=f"获取公开配置失败: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
@router.get("/admin/system-config")
|
@router.get("/admin/system-config")
|
||||||
async def get_system_config_compat(current_user=Depends(get_current_admin_user)):
|
async def get_system_config_compat(current_user=Depends(get_current_admin_user)):
|
||||||
"""兼容旧前端的系统配置接口,数据来源为 sys_system_parameters。"""
|
"""兼容旧前端的系统配置接口,数据来源为 sys_system_parameters。"""
|
||||||
|
|
|
||||||
|
|
@ -1074,7 +1074,7 @@ def get_meeting_preview_data(meeting_id: int, password: Optional[str] = None):
|
||||||
|
|
||||||
# 检查会议是否存在,并获取基本信息
|
# 检查会议是否存在,并获取基本信息
|
||||||
query = '''
|
query = '''
|
||||||
SELECT m.meeting_id, m.title, m.meeting_time, m.summary, m.updated_at, m.prompt_id,
|
SELECT m.meeting_id, m.title, m.meeting_time, m.summary, m.updated_at, m.prompt_id, m.tags,
|
||||||
m.user_id as creator_id, u.caption as creator_username,
|
m.user_id as creator_id, u.caption as creator_username,
|
||||||
p.name as prompt_name, m.access_password
|
p.name as prompt_name, m.access_password
|
||||||
FROM meetings m
|
FROM meetings m
|
||||||
|
|
@ -1175,6 +1175,7 @@ def get_meeting_preview_data(meeting_id: int, password: Optional[str] = None):
|
||||||
cursor.execute(attendees_query, (meeting_id,))
|
cursor.execute(attendees_query, (meeting_id,))
|
||||||
attendees_data = cursor.fetchall()
|
attendees_data = cursor.fetchall()
|
||||||
attendees = [{'user_id': row['user_id'], 'caption': row['caption']} for row in attendees_data]
|
attendees = [{'user_id': row['user_id'], 'caption': row['caption']} for row in attendees_data]
|
||||||
|
tags = _process_tags(cursor, meeting.get('tags'))
|
||||||
|
|
||||||
# 组装返回数据
|
# 组装返回数据
|
||||||
preview_data = {
|
preview_data = {
|
||||||
|
|
@ -1187,6 +1188,7 @@ def get_meeting_preview_data(meeting_id: int, password: Optional[str] = None):
|
||||||
"prompt_name": meeting['prompt_name'],
|
"prompt_name": meeting['prompt_name'],
|
||||||
"attendees": attendees,
|
"attendees": attendees,
|
||||||
"attendees_count": len(attendees),
|
"attendees_count": len(attendees),
|
||||||
|
"tags": tags,
|
||||||
"has_password": bool(meeting.get('access_password')),
|
"has_password": bool(meeting.get('access_password')),
|
||||||
"processing_status": progress_info # 附带进度信息供调试
|
"processing_status": progress_info # 附带进度信息供调试
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,15 @@ class SystemConfigService:
|
||||||
DEFAULT_RESET_PASSWORD = 'default_reset_password'
|
DEFAULT_RESET_PASSWORD = 'default_reset_password'
|
||||||
MAX_AUDIO_SIZE = 'max_audio_size'
|
MAX_AUDIO_SIZE = 'max_audio_size'
|
||||||
|
|
||||||
|
# 品牌配置
|
||||||
|
BRANDING_APP_NAME = 'branding_app_name'
|
||||||
|
BRANDING_HOME_HEADLINE = 'branding_home_headline'
|
||||||
|
BRANDING_HOME_TAGLINE = 'branding_home_tagline'
|
||||||
|
BRANDING_CONSOLE_SUBTITLE = 'branding_console_subtitle'
|
||||||
|
BRANDING_PREVIEW_TITLE = 'branding_preview_title'
|
||||||
|
BRANDING_LOGIN_WELCOME = 'branding_login_welcome'
|
||||||
|
BRANDING_FOOTER_TEXT = 'branding_footer_text'
|
||||||
|
|
||||||
# 声纹配置
|
# 声纹配置
|
||||||
VOICEPRINT_TEMPLATE_TEXT = 'voiceprint_template_text'
|
VOICEPRINT_TEMPLATE_TEXT = 'voiceprint_template_text'
|
||||||
VOICEPRINT_MAX_SIZE = 'voiceprint_max_size'
|
VOICEPRINT_MAX_SIZE = 'voiceprint_max_size'
|
||||||
|
|
@ -603,6 +612,18 @@ class SystemConfigService:
|
||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
return default
|
return default
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_branding_config(cls) -> Dict[str, str]:
|
||||||
|
return {
|
||||||
|
"app_name": str(cls.get_config(cls.BRANDING_APP_NAME, "智听云平台") or "智听云平台"),
|
||||||
|
"home_headline": str(cls.get_config(cls.BRANDING_HOME_HEADLINE, "智听云平台") or "智听云平台"),
|
||||||
|
"home_tagline": str(cls.get_config(cls.BRANDING_HOME_TAGLINE, "让每一次谈话都产生价值。") or "让每一次谈话都产生价值。"),
|
||||||
|
"console_subtitle": str(cls.get_config(cls.BRANDING_CONSOLE_SUBTITLE, "智能会议控制台") or "智能会议控制台"),
|
||||||
|
"preview_title": str(cls.get_config(cls.BRANDING_PREVIEW_TITLE, "会议预览") or "会议预览"),
|
||||||
|
"login_welcome": str(cls.get_config(cls.BRANDING_LOGIN_WELCOME, "欢迎回来,请输入您的登录凭证。") or "欢迎回来,请输入您的登录凭证。"),
|
||||||
|
"footer_text": str(cls.get_config(cls.BRANDING_FOOTER_TEXT, "©2026 智听云平台") or "©2026 智听云平台"),
|
||||||
|
}
|
||||||
|
|
||||||
# LLM模型配置获取方法(直接使用通用方法)
|
# LLM模型配置获取方法(直接使用通用方法)
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_llm_model_name(cls, default: str = "qwen-plus") -> str:
|
def get_llm_model_name(cls, default: str = "qwen-plus") -> str:
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import {
|
||||||
import { useNavigate, useLocation } from 'react-router-dom';
|
import { useNavigate, useLocation } from 'react-router-dom';
|
||||||
import menuService from '../services/menuService';
|
import menuService from '../services/menuService';
|
||||||
import { renderMenuIcon } from '../utils/menuIcons';
|
import { renderMenuIcon } from '../utils/menuIcons';
|
||||||
|
import configService, { DEFAULT_BRANDING_CONFIG } from '../utils/configService';
|
||||||
|
|
||||||
const { Header, Content, Sider } = Layout;
|
const { Header, Content, Sider } = Layout;
|
||||||
const { useBreakpoint } = Grid;
|
const { useBreakpoint } = Grid;
|
||||||
|
|
@ -20,6 +21,7 @@ const MainLayout = ({ children, user, onLogout }) => {
|
||||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||||
const [openKeys, setOpenKeys] = useState([]);
|
const [openKeys, setOpenKeys] = useState([]);
|
||||||
const [activeMenuKey, setActiveMenuKey] = useState(null);
|
const [activeMenuKey, setActiveMenuKey] = useState(null);
|
||||||
|
const [branding, setBranding] = useState(DEFAULT_BRANDING_CONFIG);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const screens = useBreakpoint();
|
const screens = useBreakpoint();
|
||||||
|
|
@ -40,6 +42,10 @@ const MainLayout = ({ children, user, onLogout }) => {
|
||||||
fetchMenus();
|
fetchMenus();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
configService.getBrandingConfig().then(setBranding).catch(() => {});
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isMobile) {
|
if (isMobile) {
|
||||||
setCollapsed(false);
|
setCollapsed(false);
|
||||||
|
|
@ -267,8 +273,8 @@ const MainLayout = ({ children, user, onLogout }) => {
|
||||||
</span>
|
</span>
|
||||||
{!collapsed && (
|
{!collapsed && (
|
||||||
<div>
|
<div>
|
||||||
<div className="main-layout-brand-title">iMeeting</div>
|
<div className="main-layout-brand-title">{branding.app_name}</div>
|
||||||
<div className="main-layout-brand-subtitle">智能会议控制台</div>
|
<div className="main-layout-brand-subtitle">{branding.console_subtitle}</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!isMobile && (
|
{!isMobile && (
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,9 @@ const API_CONFIG = {
|
||||||
ITEM_DETAIL: (id) => `/api/admin/hot-word-items/${id}`,
|
ITEM_DETAIL: (id) => `/api/admin/hot-word-items/${id}`,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
PUBLIC: {
|
||||||
|
SYSTEM_CONFIG: '/api/system-config/public',
|
||||||
|
},
|
||||||
TAGS: {
|
TAGS: {
|
||||||
LIST: '/api/tags'
|
LIST: '/api/tags'
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
Row, Col, Typography, Button,
|
Row, Col, Typography, Button,
|
||||||
Form, Input, Space, Tabs, App
|
Form, Input, Space, Tabs, App
|
||||||
|
|
@ -12,13 +12,19 @@ import apiClient from '../utils/apiClient';
|
||||||
import { buildApiUrl, API_ENDPOINTS } from '../config/api';
|
import { buildApiUrl, API_ENDPOINTS } from '../config/api';
|
||||||
import menuService from '../services/menuService';
|
import menuService from '../services/menuService';
|
||||||
import BrandLogo from '../components/BrandLogo';
|
import BrandLogo from '../components/BrandLogo';
|
||||||
|
import configService, { DEFAULT_BRANDING_CONFIG } from '../utils/configService';
|
||||||
|
|
||||||
const { Title, Paragraph, Text } = Typography;
|
const { Title, Paragraph, Text } = Typography;
|
||||||
|
|
||||||
const HomePage = ({ onLogin }) => {
|
const HomePage = ({ onLogin }) => {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [branding, setBranding] = useState(DEFAULT_BRANDING_CONFIG);
|
||||||
const { message } = App.useApp();
|
const { message } = App.useApp();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
configService.getBrandingConfig().then(setBranding).catch(() => {});
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleLogin = async (values) => {
|
const handleLogin = async (values) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
|
|
@ -63,16 +69,15 @@ const HomePage = ({ onLogin }) => {
|
||||||
justifyContent: 'center'
|
justifyContent: 'center'
|
||||||
}}>
|
}}>
|
||||||
<div style={{ marginBottom: 80 }}>
|
<div style={{ marginBottom: 80 }}>
|
||||||
<BrandLogo title="iMeeting" size={48} titleSize={24} gap={12} titleColor="#6f42c1" />
|
<BrandLogo title={branding.app_name} size={48} titleSize={24} gap={12} titleColor="#6f42c1" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ maxWidth: 600 }}>
|
<div style={{ maxWidth: 600 }}>
|
||||||
<Title style={{ fontSize: 56, marginBottom: 24, fontWeight: 800 }}>
|
<Title style={{ fontSize: 56, marginBottom: 24, fontWeight: 800 }}>
|
||||||
智能协作 <span style={{ color: '#1677ff' }}>会议管理平台</span>
|
<span style={{ color: '#1677ff' }}>{branding.home_headline}</span>
|
||||||
</Title>
|
</Title>
|
||||||
<Paragraph style={{ fontSize: 20, color: '#4e5969', lineHeight: 1.6 }}>
|
<Paragraph style={{ fontSize: 20, color: '#4e5969', lineHeight: 1.6 }}>
|
||||||
全流程会议辅助,让每一份交流都产生价值。
|
{branding.home_tagline}
|
||||||
实时转录、自动总结、知识沉淀。
|
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
</div>
|
</div>
|
||||||
</Col>
|
</Col>
|
||||||
|
|
@ -100,7 +105,7 @@ const HomePage = ({ onLogin }) => {
|
||||||
|
|
||||||
<div style={{ marginBottom: 40 }}>
|
<div style={{ marginBottom: 40 }}>
|
||||||
<Paragraph type="secondary" style={{ fontSize: 16 }}>
|
<Paragraph type="secondary" style={{ fontSize: 16 }}>
|
||||||
欢迎回来,请输入您的登录凭证。
|
{branding.login_welcome}
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -150,7 +155,7 @@ const HomePage = ({ onLogin }) => {
|
||||||
|
|
||||||
<div style={{ marginTop: 100, textAlign: 'center' }}>
|
<div style={{ marginTop: 100, textAlign: 'center' }}>
|
||||||
<Text type="secondary" style={{ fontSize: 13 }}>
|
<Text type="secondary" style={{ fontSize: 13 }}>
|
||||||
©2026 iMeeting · 智能会议协作平台
|
{branding.footer_text}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,27 +1,21 @@
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
import { useParams, Link } from 'react-router-dom';
|
import { useParams, Link } from 'react-router-dom';
|
||||||
import {
|
import { Layout, Space, Button, App, Tag, Empty, Input, Tabs } from 'antd';
|
||||||
Layout, Card, Typography, Space, Button,
|
import { LockOutlined, EyeOutlined, CopyOutlined, ShareAltOutlined, HomeOutlined, UserOutlined, FileTextOutlined, PartitionOutlined, AudioOutlined } from '@ant-design/icons';
|
||||||
Result, Spin, App, Tag, Divider, Empty, Input
|
|
||||||
} from 'antd';
|
|
||||||
import {
|
|
||||||
LockOutlined, EyeOutlined, EyeInvisibleOutlined,
|
|
||||||
CopyOutlined, CheckCircleOutlined, ShareAltOutlined,
|
|
||||||
PlayCircleFilled, PauseCircleFilled,
|
|
||||||
HomeOutlined, CalendarOutlined, LoginOutlined
|
|
||||||
} from '@ant-design/icons';
|
|
||||||
import apiClient from '../utils/apiClient';
|
import apiClient from '../utils/apiClient';
|
||||||
import { buildApiUrl, API_ENDPOINTS } from '../config/api';
|
import { buildApiUrl, API_ENDPOINTS } from '../config/api';
|
||||||
import MarkdownRenderer from '../components/MarkdownRenderer';
|
import MarkdownRenderer from '../components/MarkdownRenderer';
|
||||||
import BrandLogo from '../components/BrandLogo';
|
import MindMap from '../components/MindMap';
|
||||||
import tools from '../utils/tools';
|
import tools from '../utils/tools';
|
||||||
|
import configService, { DEFAULT_BRANDING_CONFIG } from '../utils/configService';
|
||||||
|
import './MeetingPreview.css';
|
||||||
|
|
||||||
const { Header, Content } = Layout;
|
const { Content } = Layout;
|
||||||
const { Title, Text, Paragraph } = Typography;
|
|
||||||
|
|
||||||
const MeetingPreview = () => {
|
const MeetingPreview = () => {
|
||||||
const { meeting_id } = useParams();
|
const { meeting_id } = useParams();
|
||||||
const { message } = App.useApp();
|
const { message } = App.useApp();
|
||||||
|
const audioRef = useRef(null);
|
||||||
const [meeting, setMeeting] = useState(null);
|
const [meeting, setMeeting] = useState(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
|
|
@ -29,9 +23,20 @@ const MeetingPreview = () => {
|
||||||
const [passwordError, setPasswordError] = useState('');
|
const [passwordError, setPasswordError] = useState('');
|
||||||
const [passwordRequired, setPasswordRequired] = useState(false);
|
const [passwordRequired, setPasswordRequired] = useState(false);
|
||||||
const [isAuthorized, setIsAuthorized] = useState(false);
|
const [isAuthorized, setIsAuthorized] = useState(false);
|
||||||
|
const [branding, setBranding] = useState(DEFAULT_BRANDING_CONFIG);
|
||||||
|
const [transcript, setTranscript] = useState([]);
|
||||||
|
const [audioUrl, setAudioUrl] = useState('');
|
||||||
|
const [activeSegmentIndex, setActiveSegmentIndex] = useState(-1);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
configService.getBrandingConfig().then(setBranding).catch(() => {});
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setMeeting(null);
|
setMeeting(null);
|
||||||
|
setTranscript([]);
|
||||||
|
setAudioUrl('');
|
||||||
|
setActiveSegmentIndex(-1);
|
||||||
setError(null);
|
setError(null);
|
||||||
setPassword('');
|
setPassword('');
|
||||||
setPasswordError('');
|
setPasswordError('');
|
||||||
|
|
@ -40,6 +45,25 @@ const MeetingPreview = () => {
|
||||||
fetchPreview();
|
fetchPreview();
|
||||||
}, [meeting_id]);
|
}, [meeting_id]);
|
||||||
|
|
||||||
|
const fetchTranscriptAndAudio = async () => {
|
||||||
|
const [transcriptRes, audioRes] = await Promise.allSettled([
|
||||||
|
apiClient.get(buildApiUrl(API_ENDPOINTS.MEETINGS.TRANSCRIPT(meeting_id))),
|
||||||
|
apiClient.get(buildApiUrl(API_ENDPOINTS.MEETINGS.AUDIO(meeting_id))),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (transcriptRes.status === 'fulfilled') {
|
||||||
|
setTranscript(Array.isArray(transcriptRes.value.data) ? transcriptRes.value.data : []);
|
||||||
|
} else {
|
||||||
|
setTranscript([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (audioRes.status === 'fulfilled') {
|
||||||
|
setAudioUrl(buildApiUrl(`${API_ENDPOINTS.MEETINGS.AUDIO(meeting_id)}/stream`));
|
||||||
|
} else {
|
||||||
|
setAudioUrl('');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const fetchPreview = async (pwd = '') => {
|
const fetchPreview = async (pwd = '') => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
|
|
@ -51,6 +75,7 @@ const MeetingPreview = () => {
|
||||||
setPasswordRequired(false);
|
setPasswordRequired(false);
|
||||||
setPasswordError('');
|
setPasswordError('');
|
||||||
setError(null);
|
setError(null);
|
||||||
|
await fetchTranscriptAndAudio();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const responseCode = String(err?.response?.data?.code || '');
|
const responseCode = String(err?.response?.data?.code || '');
|
||||||
if (responseCode === '401') {
|
if (responseCode === '401') {
|
||||||
|
|
@ -71,6 +96,40 @@ const MeetingPreview = () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleCopyLink = async () => {
|
||||||
|
await navigator.clipboard.writeText(window.location.href);
|
||||||
|
message.success('分享链接已复制');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleShare = async () => {
|
||||||
|
if (navigator.share) {
|
||||||
|
try {
|
||||||
|
await navigator.share({ title: meeting?.title || branding.preview_title, url: window.location.href });
|
||||||
|
return;
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
await handleCopyLink();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTimeUpdate = () => {
|
||||||
|
if (!audioRef.current || !transcript.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const currentMs = audioRef.current.currentTime * 1000;
|
||||||
|
const index = transcript.findIndex(
|
||||||
|
(item) => currentMs >= item.start_time_ms && currentMs <= item.end_time_ms,
|
||||||
|
);
|
||||||
|
setActiveSegmentIndex(index);
|
||||||
|
};
|
||||||
|
|
||||||
|
const jumpToSegment = (segment) => {
|
||||||
|
if (!audioRef.current || !segment) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
audioRef.current.currentTime = (segment.start_time_ms || 0) / 1000;
|
||||||
|
audioRef.current.play().catch(() => {});
|
||||||
|
};
|
||||||
|
|
||||||
const handleVerify = () => {
|
const handleVerify = () => {
|
||||||
if (!password) {
|
if (!password) {
|
||||||
message.warning('请输入访问密码');
|
message.warning('请输入访问密码');
|
||||||
|
|
@ -79,69 +138,191 @@ const MeetingPreview = () => {
|
||||||
fetchPreview(password);
|
fetchPreview(password);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (loading && !meeting) return <div style={{ textAlign: 'center', padding: '100px' }}><Spin size="large" /></div>;
|
if (loading && !meeting) {
|
||||||
|
return (
|
||||||
|
<div className="preview-container">
|
||||||
|
<div className="preview-loading">
|
||||||
|
<div className="loading-spinner" />
|
||||||
|
<p>正在加载会议预览...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (error) return <Result status="error" title={error} extra={<Link to="/"><Button type="primary" icon={<HomeOutlined />}>返回首页</Button></Link>} />;
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div className="preview-container">
|
||||||
|
<div className="preview-error">
|
||||||
|
<div className="error-icon">⚠️</div>
|
||||||
|
<h2>加载失败</h2>
|
||||||
|
<p>{error}</p>
|
||||||
|
<button type="button" className="error-retry-btn" onClick={() => fetchPreview(password)}>
|
||||||
|
重试
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (passwordRequired && !isAuthorized) {
|
if (passwordRequired && !isAuthorized) {
|
||||||
return (
|
return (
|
||||||
<div style={{ minHeight: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center', background: '#f0f2f5' }}>
|
<div className="preview-container">
|
||||||
<Card style={{ width: 400, borderRadius: 16, textAlign: 'center', boxShadow: '0 8px 24px rgba(0,0,0,0.05)' }}>
|
<div className="password-protection-modal">
|
||||||
<LockOutlined style={{ fontSize: 48, color: '#1677ff', marginBottom: 24 }} />
|
<div className="password-modal-content">
|
||||||
<Title level={3}>此会议受密码保护</Title>
|
<div className="password-icon-large">
|
||||||
<Paragraph type="secondary">请输入访问密码以查看会议纪要</Paragraph>
|
<LockOutlined style={{ fontSize: 36 }} />
|
||||||
<Space direction="vertical" style={{ width: '100%' }} size="large">
|
</div>
|
||||||
<Input.Password
|
<h2>此会议受密码保护</h2>
|
||||||
size="large"
|
<p>请输入访问密码以查看会议纪要</p>
|
||||||
placeholder="访问密码"
|
<div className="password-input-group">
|
||||||
value={password}
|
<Input.Password
|
||||||
status={passwordError ? 'error' : undefined}
|
className={`password-input${passwordError ? ' error' : ''}`}
|
||||||
onChange={e => {
|
placeholder="访问密码"
|
||||||
setPassword(e.target.value);
|
value={password}
|
||||||
if (passwordError) {
|
onChange={(e) => {
|
||||||
setPasswordError('');
|
setPassword(e.target.value);
|
||||||
}
|
if (passwordError) {
|
||||||
}}
|
setPasswordError('');
|
||||||
onPressEnter={handleVerify}
|
}
|
||||||
/>
|
}}
|
||||||
{passwordError ? <Text type="danger">{passwordError}</Text> : null}
|
onPressEnter={handleVerify}
|
||||||
<Button type="primary" size="large" block icon={<EyeOutlined />} onClick={handleVerify}>立即查看</Button>
|
/>
|
||||||
</Space>
|
</div>
|
||||||
<div style={{ marginTop: 24 }}>
|
{passwordError ? <div className="password-error-message">{passwordError}</div> : null}
|
||||||
<Link to="/"><Button type="link" icon={<HomeOutlined />}>返回首页</Button></Link>
|
<button type="button" className="password-verify-btn" onClick={handleVerify}>
|
||||||
|
<EyeOutlined style={{ marginRight: 8 }} />
|
||||||
|
立即查看
|
||||||
|
</button>
|
||||||
|
<div style={{ marginTop: 24 }}>
|
||||||
|
<Link to="/"><Button type="link" icon={<HomeOutlined />}>返回首页</Button></Link>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout style={{ minHeight: '100vh', background: '#f0f2f5' }}>
|
<Layout style={{ minHeight: '100vh' }}>
|
||||||
<Header style={{ background: '#fff', padding: '0 24px', display: 'flex', alignItems: 'center', justifyContent: 'space-between', boxShadow: '0 2px 8px rgba(0,0,0,0.05)' }}>
|
<Content className="preview-container">
|
||||||
<Space>
|
<div className="preview-content">
|
||||||
<BrandLogo title="iMeeting 会议预览" size={28} titleSize={18} gap={10} />
|
<h1 className="preview-title">{meeting.title}</h1>
|
||||||
</Space>
|
|
||||||
<Link to="/"><Button icon={<LoginOutlined />}>登录系统</Button></Link>
|
<div className="meeting-info-section">
|
||||||
</Header>
|
<h2 className="section-title">会议信息</h2>
|
||||||
|
<div className="info-item"><strong>创建人:</strong>{meeting.creator_username || '未知'}</div>
|
||||||
<Content style={{ padding: '40px 20px', maxWidth: 900, margin: '0 auto', width: '100%' }}>
|
<div className="info-item"><strong>会议时间:</strong>{tools.formatDateTime(meeting.meeting_time)}</div>
|
||||||
<Card variant="borderless" style={{ borderRadius: 16 }}>
|
<div className="info-item"><strong>参会人员:</strong>{meeting.attendees?.length ? meeting.attendees.map((item) => item.caption).join('、') : '未设置'}</div>
|
||||||
<Title level={2}>{meeting.title}</Title>
|
<div className="info-item"><strong>计算人数:</strong>{meeting.attendees_count || meeting.attendees?.length || 0}人</div>
|
||||||
<Space wrap style={{ marginBottom: 20 }}>
|
<div className="info-item"><strong>总结模板:</strong>{meeting.prompt_name || '默认模板'}</div>
|
||||||
{meeting.tags?.map(t => <Tag key={t.id} color="blue">{t.name}</Tag>)}
|
{meeting.tags?.length ? (
|
||||||
<Text type="secondary"><CalendarOutlined /> {tools.formatDateTime(meeting.meeting_time)}</Text>
|
<div className="info-item">
|
||||||
</Space>
|
<strong>标签:</strong>
|
||||||
|
<Space wrap style={{ marginLeft: 8 }}>
|
||||||
<Divider />
|
{meeting.tags.map((tag) => <Tag key={tag.id || tag.name} color="blue">{tag.name}</Tag>)}
|
||||||
|
</Space>
|
||||||
<div className="preview-content">
|
</div>
|
||||||
{meeting.summary ? (
|
) : null}
|
||||||
<MarkdownRenderer content={meeting.summary} />
|
|
||||||
) : (
|
|
||||||
<Empty description="暂无会议总结内容" />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
|
||||||
|
<div className="action-buttons">
|
||||||
|
<button type="button" className="action-btn copy-btn" onClick={handleCopyLink}>
|
||||||
|
<CopyOutlined />
|
||||||
|
复制链接
|
||||||
|
</button>
|
||||||
|
<button type="button" className="action-btn share-btn" onClick={handleShare}>
|
||||||
|
<ShareAltOutlined />
|
||||||
|
立即分享
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="summary-section">
|
||||||
|
<Tabs
|
||||||
|
className="preview-tabs"
|
||||||
|
items={[
|
||||||
|
{
|
||||||
|
key: 'summary',
|
||||||
|
label: (
|
||||||
|
<Space size={8}>
|
||||||
|
<FileTextOutlined />
|
||||||
|
<span>会议总结</span>
|
||||||
|
</Space>
|
||||||
|
),
|
||||||
|
children: (
|
||||||
|
<div className="summary-content">
|
||||||
|
{meeting.summary ? <MarkdownRenderer content={meeting.summary} /> : <Empty description="暂无会议总结内容" />}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'transcript',
|
||||||
|
label: (
|
||||||
|
<Space size={8}>
|
||||||
|
<AudioOutlined />
|
||||||
|
<span>会议转录</span>
|
||||||
|
</Space>
|
||||||
|
),
|
||||||
|
children: (
|
||||||
|
<div className="transcript-wrapper">
|
||||||
|
{audioUrl ? (
|
||||||
|
<div className="preview-audio-player">
|
||||||
|
<audio
|
||||||
|
ref={audioRef}
|
||||||
|
src={audioUrl}
|
||||||
|
controls
|
||||||
|
controlsList="nodownload noplaybackrate"
|
||||||
|
onTimeUpdate={handleTimeUpdate}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{transcript.length ? (
|
||||||
|
<div className="transcript-list">
|
||||||
|
{transcript.map((segment, index) => (
|
||||||
|
<div
|
||||||
|
key={segment.segment_id}
|
||||||
|
className={`transcript-segment${activeSegmentIndex === index ? ' active' : ''}`}
|
||||||
|
onClick={() => jumpToSegment(segment)}
|
||||||
|
>
|
||||||
|
<div className="segment-header">
|
||||||
|
<span className="speaker-name">
|
||||||
|
<UserOutlined style={{ marginRight: 6 }} />
|
||||||
|
{segment.speaker_tag || `发言人 ${segment.speaker_id}`}
|
||||||
|
</span>
|
||||||
|
<span className="segment-time">{tools.formatDuration((segment.start_time_ms || 0) / 1000)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="segment-text">{segment.text_content}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="empty-transcript">暂无转录内容</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'mindmap',
|
||||||
|
label: (
|
||||||
|
<Space size={8}>
|
||||||
|
<PartitionOutlined />
|
||||||
|
<span>思维导图</span>
|
||||||
|
</Space>
|
||||||
|
),
|
||||||
|
children: (
|
||||||
|
<div className="mindmap-wrapper">
|
||||||
|
<MindMap content={meeting.summary} title={meeting.title} />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="preview-footer">
|
||||||
|
{branding.footer_text}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</Content>
|
</Content>
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,23 @@
|
||||||
import apiClient from './apiClient';
|
import apiClient from './apiClient';
|
||||||
import { buildApiUrl, API_ENDPOINTS } from '../config/api';
|
import { buildApiUrl, API_ENDPOINTS } from '../config/api';
|
||||||
|
|
||||||
|
export const DEFAULT_BRANDING_CONFIG = {
|
||||||
|
app_name: '智听云平台',
|
||||||
|
home_headline: '智听云平台',
|
||||||
|
home_tagline: '让每一次谈话都产生价值。',
|
||||||
|
console_subtitle: '智能会议控制台',
|
||||||
|
preview_title: '会议预览',
|
||||||
|
login_welcome: '欢迎回来,请输入您的登录凭证。',
|
||||||
|
footer_text: '©2026 智听云平台',
|
||||||
|
};
|
||||||
|
|
||||||
class ConfigService {
|
class ConfigService {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.configs = null;
|
this.configs = null;
|
||||||
this.loadPromise = null;
|
this.loadPromise = null;
|
||||||
this._pageSize = null;
|
this._pageSize = null;
|
||||||
|
this.brandingConfig = null;
|
||||||
|
this.brandingPromise = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getConfigs() {
|
async getConfigs() {
|
||||||
|
|
@ -64,6 +76,33 @@ class ConfigService {
|
||||||
return 10;
|
return 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getBrandingConfig() {
|
||||||
|
if (this.brandingConfig) {
|
||||||
|
return this.brandingConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.brandingPromise) {
|
||||||
|
return this.brandingPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.brandingPromise = this.loadBrandingConfigFromServer();
|
||||||
|
this.brandingConfig = await this.brandingPromise;
|
||||||
|
return this.brandingConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadBrandingConfigFromServer() {
|
||||||
|
try {
|
||||||
|
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.PUBLIC.SYSTEM_CONFIG));
|
||||||
|
return {
|
||||||
|
...DEFAULT_BRANDING_CONFIG,
|
||||||
|
...(response.data || {}),
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to load branding configs, using defaults:', error);
|
||||||
|
return { ...DEFAULT_BRANDING_CONFIG };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 格式化文件大小为可读格式
|
// 格式化文件大小为可读格式
|
||||||
formatFileSize(bytes) {
|
formatFileSize(bytes) {
|
||||||
if (bytes === 0) return '0 Bytes';
|
if (bytes === 0) return '0 Bytes';
|
||||||
|
|
@ -78,6 +117,8 @@ class ConfigService {
|
||||||
this.configs = null;
|
this.configs = null;
|
||||||
this.loadPromise = null;
|
this.loadPromise = null;
|
||||||
this._pageSize = null;
|
this._pageSize = null;
|
||||||
|
this.brandingConfig = null;
|
||||||
|
this.brandingPromise = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue