diff --git a/backend/app/api/endpoints/admin_settings.py b/backend/app/api/endpoints/admin_settings.py
index 013c69a..ac1548f 100644
--- a/backend/app/api/endpoints/admin_settings.py
+++ b/backend/app/api/endpoints/admin_settings.py
@@ -9,6 +9,7 @@ from app.core.database import get_db_connection
from app.core.response import create_api_response
from app.services.async_transcription_service import AsyncTranscriptionService
from app.services.llm_service import LLMService
+from app.services.system_config_service import SystemConfigService
router = APIRouter()
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)}")
+@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")
async def get_system_config_compat(current_user=Depends(get_current_admin_user)):
"""兼容旧前端的系统配置接口,数据来源为 sys_system_parameters。"""
diff --git a/backend/app/api/endpoints/meetings.py b/backend/app/api/endpoints/meetings.py
index 6956794..7e5405d 100644
--- a/backend/app/api/endpoints/meetings.py
+++ b/backend/app/api/endpoints/meetings.py
@@ -1074,7 +1074,7 @@ def get_meeting_preview_data(meeting_id: int, password: Optional[str] = None):
# 检查会议是否存在,并获取基本信息
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,
p.name as prompt_name, m.access_password
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,))
attendees_data = cursor.fetchall()
attendees = [{'user_id': row['user_id'], 'caption': row['caption']} for row in attendees_data]
+ tags = _process_tags(cursor, meeting.get('tags'))
# 组装返回数据
preview_data = {
@@ -1187,6 +1188,7 @@ def get_meeting_preview_data(meeting_id: int, password: Optional[str] = None):
"prompt_name": meeting['prompt_name'],
"attendees": attendees,
"attendees_count": len(attendees),
+ "tags": tags,
"has_password": bool(meeting.get('access_password')),
"processing_status": progress_info # 附带进度信息供调试
}
diff --git a/backend/app/services/system_config_service.py b/backend/app/services/system_config_service.py
index 52a075f..b576cee 100644
--- a/backend/app/services/system_config_service.py
+++ b/backend/app/services/system_config_service.py
@@ -15,6 +15,15 @@ class SystemConfigService:
DEFAULT_RESET_PASSWORD = 'default_reset_password'
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_MAX_SIZE = 'voiceprint_max_size'
@@ -603,6 +612,18 @@ class SystemConfigService:
except (ValueError, TypeError):
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模型配置获取方法(直接使用通用方法)
@classmethod
def get_llm_model_name(cls, default: str = "qwen-plus") -> str:
diff --git a/frontend/src/components/MainLayout.jsx b/frontend/src/components/MainLayout.jsx
index f74084b..07a1259 100644
--- a/frontend/src/components/MainLayout.jsx
+++ b/frontend/src/components/MainLayout.jsx
@@ -10,6 +10,7 @@ import {
import { useNavigate, useLocation } from 'react-router-dom';
import menuService from '../services/menuService';
import { renderMenuIcon } from '../utils/menuIcons';
+import configService, { DEFAULT_BRANDING_CONFIG } from '../utils/configService';
const { Header, Content, Sider } = Layout;
const { useBreakpoint } = Grid;
@@ -20,6 +21,7 @@ const MainLayout = ({ children, user, onLogout }) => {
const [drawerOpen, setDrawerOpen] = useState(false);
const [openKeys, setOpenKeys] = useState([]);
const [activeMenuKey, setActiveMenuKey] = useState(null);
+ const [branding, setBranding] = useState(DEFAULT_BRANDING_CONFIG);
const navigate = useNavigate();
const location = useLocation();
const screens = useBreakpoint();
@@ -40,6 +42,10 @@ const MainLayout = ({ children, user, onLogout }) => {
fetchMenus();
}, []);
+ useEffect(() => {
+ configService.getBrandingConfig().then(setBranding).catch(() => {});
+ }, []);
+
useEffect(() => {
if (isMobile) {
setCollapsed(false);
@@ -267,8 +273,8 @@ const MainLayout = ({ children, user, onLogout }) => {
{!collapsed && (
-
iMeeting
-
智能会议控制台
+
{branding.app_name}
+
{branding.console_subtitle}
)}
{!isMobile && (
diff --git a/frontend/src/config/api.js b/frontend/src/config/api.js
index 190b6a1..51d6ddd 100644
--- a/frontend/src/config/api.js
+++ b/frontend/src/config/api.js
@@ -65,6 +65,9 @@ const API_CONFIG = {
ITEM_DETAIL: (id) => `/api/admin/hot-word-items/${id}`,
}
},
+ PUBLIC: {
+ SYSTEM_CONFIG: '/api/system-config/public',
+ },
TAGS: {
LIST: '/api/tags'
},
diff --git a/frontend/src/pages/HomePage.jsx b/frontend/src/pages/HomePage.jsx
index 0dd418d..6cf9e24 100644
--- a/frontend/src/pages/HomePage.jsx
+++ b/frontend/src/pages/HomePage.jsx
@@ -1,4 +1,4 @@
-import React, { useState } from 'react';
+import React, { useEffect, useState } from 'react';
import {
Row, Col, Typography, Button,
Form, Input, Space, Tabs, App
@@ -12,13 +12,19 @@ import apiClient from '../utils/apiClient';
import { buildApiUrl, API_ENDPOINTS } from '../config/api';
import menuService from '../services/menuService';
import BrandLogo from '../components/BrandLogo';
+import configService, { DEFAULT_BRANDING_CONFIG } from '../utils/configService';
const { Title, Paragraph, Text } = Typography;
const HomePage = ({ onLogin }) => {
const [loading, setLoading] = useState(false);
+ const [branding, setBranding] = useState(DEFAULT_BRANDING_CONFIG);
const { message } = App.useApp();
+ useEffect(() => {
+ configService.getBrandingConfig().then(setBranding).catch(() => {});
+ }, []);
+
const handleLogin = async (values) => {
setLoading(true);
try {
@@ -63,16 +69,15 @@ const HomePage = ({ onLogin }) => {
justifyContent: 'center'
}}>
-
+
- 智能协作 会议管理平台
+ {branding.home_headline}
- 全流程会议辅助,让每一份交流都产生价值。
- 实时转录、自动总结、知识沉淀。
+ {branding.home_tagline}
@@ -100,7 +105,7 @@ const HomePage = ({ onLogin }) => {
- 欢迎回来,请输入您的登录凭证。
+ {branding.login_welcome}
@@ -150,7 +155,7 @@ const HomePage = ({ onLogin }) => {
- ©2026 iMeeting · 智能会议协作平台
+ {branding.footer_text}
diff --git a/frontend/src/pages/MeetingPreview.jsx b/frontend/src/pages/MeetingPreview.jsx
index 1a6b567..9cc7a3b 100644
--- a/frontend/src/pages/MeetingPreview.jsx
+++ b/frontend/src/pages/MeetingPreview.jsx
@@ -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 {
- Layout, Card, Typography, Space, Button,
- 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 { Layout, Space, Button, App, Tag, Empty, Input, Tabs } from 'antd';
+import { LockOutlined, EyeOutlined, CopyOutlined, ShareAltOutlined, HomeOutlined, UserOutlined, FileTextOutlined, PartitionOutlined, AudioOutlined } from '@ant-design/icons';
import apiClient from '../utils/apiClient';
import { buildApiUrl, API_ENDPOINTS } from '../config/api';
import MarkdownRenderer from '../components/MarkdownRenderer';
-import BrandLogo from '../components/BrandLogo';
+import MindMap from '../components/MindMap';
import tools from '../utils/tools';
+import configService, { DEFAULT_BRANDING_CONFIG } from '../utils/configService';
+import './MeetingPreview.css';
-const { Header, Content } = Layout;
-const { Title, Text, Paragraph } = Typography;
+const { Content } = Layout;
const MeetingPreview = () => {
const { meeting_id } = useParams();
const { message } = App.useApp();
+ const audioRef = useRef(null);
const [meeting, setMeeting] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
@@ -29,9 +23,20 @@ const MeetingPreview = () => {
const [passwordError, setPasswordError] = useState('');
const [passwordRequired, setPasswordRequired] = 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(() => {
setMeeting(null);
+ setTranscript([]);
+ setAudioUrl('');
+ setActiveSegmentIndex(-1);
setError(null);
setPassword('');
setPasswordError('');
@@ -40,6 +45,25 @@ const MeetingPreview = () => {
fetchPreview();
}, [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 = '') => {
setLoading(true);
try {
@@ -51,6 +75,7 @@ const MeetingPreview = () => {
setPasswordRequired(false);
setPasswordError('');
setError(null);
+ await fetchTranscriptAndAudio();
} catch (err) {
const responseCode = String(err?.response?.data?.code || '');
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 = () => {
if (!password) {
message.warning('请输入访问密码');
@@ -79,69 +138,191 @@ const MeetingPreview = () => {
fetchPreview(password);
};
- if (loading && !meeting) return
;
+ if (loading && !meeting) {
+ return (
+
+ );
+ }
- if (error) return }>返回首页} />;
+ if (error) {
+ return (
+
+
+
⚠️
+
加载失败
+
{error}
+
+
+
+ );
+ }
if (passwordRequired && !isAuthorized) {
return (
-
-
-
- 此会议受密码保护
- 请输入访问密码以查看会议纪要
-
- {
- setPassword(e.target.value);
- if (passwordError) {
- setPasswordError('');
- }
- }}
- onPressEnter={handleVerify}
- />
- {passwordError ? {passwordError} : null}
- } onClick={handleVerify}>立即查看
-
-
-
}>返回首页
+
+
+
+
+
+
+
此会议受密码保护
+
请输入访问密码以查看会议纪要
+
+ {
+ setPassword(e.target.value);
+ if (passwordError) {
+ setPasswordError('');
+ }
+ }}
+ onPressEnter={handleVerify}
+ />
+
+ {passwordError ?
{passwordError}
: null}
+
+
+ }>返回首页
+
-
+
);
}
return (
-
-
-
-
-
- {meeting.title}
-
- {meeting.tags?.map(t => {t.name})}
- {tools.formatDateTime(meeting.meeting_time)}
-
-
-
-
-
- {meeting.summary ? (
-
- ) : (
-
- )}
+
+
+
+
{meeting.title}
+
+
+
会议信息
+
创建人:{meeting.creator_username || '未知'}
+
会议时间:{tools.formatDateTime(meeting.meeting_time)}
+
参会人员:{meeting.attendees?.length ? meeting.attendees.map((item) => item.caption).join('、') : '未设置'}
+
计算人数:{meeting.attendees_count || meeting.attendees?.length || 0}人
+
总结模板:{meeting.prompt_name || '默认模板'}
+ {meeting.tags?.length ? (
+
+ 标签:
+
+ {meeting.tags.map((tag) => {tag.name})}
+
+
+ ) : null}
-
+
+
+
+
+
+
+
+
+
+ 会议总结
+
+ ),
+ children: (
+
+ {meeting.summary ? : }
+
+ ),
+ },
+ {
+ key: 'transcript',
+ label: (
+
+
+ 会议转录
+
+ ),
+ children: (
+
+ {audioUrl ? (
+
+ ) : null}
+ {transcript.length ? (
+
+ {transcript.map((segment, index) => (
+
jumpToSegment(segment)}
+ >
+
+
+
+ {segment.speaker_tag || `发言人 ${segment.speaker_id}`}
+
+ {tools.formatDuration((segment.start_time_ms || 0) / 1000)}
+
+
{segment.text_content}
+
+ ))}
+
+ ) : (
+
暂无转录内容
+ )}
+
+ ),
+ },
+ {
+ key: 'mindmap',
+ label: (
+
+
+ 思维导图
+
+ ),
+ children: (
+
+
+
+ ),
+ },
+ ]}
+ />
+
+
+
+ {branding.footer_text}
+
+
);
diff --git a/frontend/src/utils/configService.js b/frontend/src/utils/configService.js
index 65411c7..c80f77e 100644
--- a/frontend/src/utils/configService.js
+++ b/frontend/src/utils/configService.js
@@ -1,11 +1,23 @@
import apiClient from './apiClient';
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 {
constructor() {
this.configs = null;
this.loadPromise = null;
this._pageSize = null;
+ this.brandingConfig = null;
+ this.brandingPromise = null;
}
async getConfigs() {
@@ -64,6 +76,33 @@ class ConfigService {
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) {
if (bytes === 0) return '0 Bytes';
@@ -78,6 +117,8 @@ class ConfigService {
this.configs = null;
this.loadPromise = null;
this._pageSize = null;
+ this.brandingConfig = null;
+ this.brandingPromise = null;
}
}