Compare commits
3 Commits
cc1817078a
...
ac9c2f5fd4
| Author | SHA1 | Date |
|---|---|---|
|
|
ac9c2f5fd4 | |
|
|
cec4f98d42 | |
|
|
df61cd870d |
|
|
@ -12,10 +12,11 @@ from app.services.system_config_service import SystemConfigService
|
|||
from app.utils.audio_parser import get_audio_duration
|
||||
from app.core.auth import get_current_user, get_optional_current_user
|
||||
from app.core.response import create_api_response
|
||||
from typing import List, Optional
|
||||
from typing import Any, Dict, List, Optional
|
||||
from datetime import datetime
|
||||
from pydantic import BaseModel
|
||||
from urllib.parse import quote
|
||||
from collections import defaultdict
|
||||
import os
|
||||
import uuid
|
||||
import shutil
|
||||
|
|
@ -32,13 +33,17 @@ class GenerateSummaryRequest(BaseModel):
|
|||
model_code: Optional[str] = None # LLM模型编码,如果不指定则使用默认模型
|
||||
|
||||
|
||||
def _split_tag_names(tag_string: Optional[str]) -> List[str]:
|
||||
if not tag_string:
|
||||
return []
|
||||
return [name.strip() for name in str(tag_string).split(',') if name and name.strip()]
|
||||
|
||||
|
||||
def _process_tags(cursor, tag_string: Optional[str], creator_id: Optional[int] = None) -> List[Tag]:
|
||||
"""
|
||||
处理标签:查询已存在的标签,如果提供了 creator_id 则创建不存在的标签
|
||||
"""
|
||||
if not tag_string:
|
||||
return []
|
||||
tag_names = [name.strip() for name in tag_string.split(',') if name.strip()]
|
||||
tag_names = _split_tag_names(tag_string)
|
||||
if not tag_names:
|
||||
return []
|
||||
|
||||
|
|
@ -66,6 +71,209 @@ def _sync_attendees(cursor, meeting_id: int, attendee_ids: Optional[List[int]])
|
|||
[(meeting_id, user_id) for user_id in attendee_id_list]
|
||||
)
|
||||
|
||||
|
||||
def _serialize_task_record(task_record: Optional[Dict[str, Any]], meeting_id: int) -> Optional[Dict[str, Any]]:
|
||||
if not task_record:
|
||||
return None
|
||||
|
||||
created_at = task_record.get('created_at')
|
||||
completed_at = task_record.get('completed_at')
|
||||
|
||||
return {
|
||||
'task_id': task_record.get('task_id'),
|
||||
'status': task_record.get('status', 'pending') or 'pending',
|
||||
'progress': int(task_record.get('progress') or 0),
|
||||
'meeting_id': meeting_id,
|
||||
'created_at': created_at.isoformat() if hasattr(created_at, 'isoformat') else created_at,
|
||||
'completed_at': completed_at.isoformat() if hasattr(completed_at, 'isoformat') else completed_at,
|
||||
'error_message': task_record.get('error_message')
|
||||
}
|
||||
|
||||
|
||||
def _build_meeting_overall_status(
|
||||
transcription_status: Optional[Dict[str, Any]] = None,
|
||||
llm_status: Optional[Dict[str, Any]] = None
|
||||
) -> Dict[str, Any]:
|
||||
trans_data = {
|
||||
"status": transcription_status.get('status', 'pending') if transcription_status else 'pending',
|
||||
"progress": transcription_status.get('progress', 0) if transcription_status else 0,
|
||||
"task_id": transcription_status.get('task_id') if transcription_status else None,
|
||||
"error_message": transcription_status.get('error_message') if transcription_status else None,
|
||||
"created_at": transcription_status.get('created_at') if transcription_status else None
|
||||
}
|
||||
|
||||
llm_data = {
|
||||
"status": llm_status.get('status', 'pending') if llm_status else 'pending',
|
||||
"progress": llm_status.get('progress', 0) if llm_status else 0,
|
||||
"task_id": llm_status.get('task_id') if llm_status else None,
|
||||
"error_message": llm_status.get('error_message') if llm_status else None,
|
||||
"created_at": llm_status.get('created_at') if llm_status else None
|
||||
}
|
||||
|
||||
trans_status_val = trans_data["status"]
|
||||
llm_status_val = llm_data["status"]
|
||||
|
||||
if trans_status_val == 'failed':
|
||||
overall_status = "failed"
|
||||
current_stage = "transcription"
|
||||
overall_progress = 0
|
||||
elif llm_status_val == 'failed':
|
||||
overall_status = "failed"
|
||||
current_stage = "llm"
|
||||
overall_progress = 50
|
||||
elif trans_status_val == 'completed' and llm_status_val == 'completed':
|
||||
overall_status = "completed"
|
||||
current_stage = "completed"
|
||||
overall_progress = 100
|
||||
elif trans_status_val == 'completed':
|
||||
if llm_status_val in ['pending', 'processing']:
|
||||
overall_status = "summarizing"
|
||||
current_stage = "llm"
|
||||
overall_progress = 50 + int(llm_data["progress"] * 0.5)
|
||||
else:
|
||||
overall_status = "summarizing"
|
||||
current_stage = "llm"
|
||||
overall_progress = 50
|
||||
else:
|
||||
if trans_status_val in ['pending', 'processing']:
|
||||
overall_status = "transcribing"
|
||||
current_stage = "transcription"
|
||||
overall_progress = int(trans_data["progress"] * 0.5)
|
||||
else:
|
||||
overall_status = "pending"
|
||||
current_stage = "transcription"
|
||||
overall_progress = 0
|
||||
|
||||
return {
|
||||
"overall_status": overall_status,
|
||||
"overall_progress": overall_progress,
|
||||
"current_stage": current_stage,
|
||||
"transcription": trans_data,
|
||||
"llm": llm_data
|
||||
}
|
||||
|
||||
|
||||
def _load_attendees_map(cursor, meeting_ids: List[int]) -> Dict[int, List[Dict[str, Any]]]:
|
||||
if not meeting_ids:
|
||||
return {}
|
||||
|
||||
format_strings = ', '.join(['%s'] * len(meeting_ids))
|
||||
query = f'''
|
||||
SELECT a.meeting_id, u.user_id, u.caption
|
||||
FROM attendees a
|
||||
JOIN sys_users u ON a.user_id = u.user_id
|
||||
WHERE a.meeting_id IN ({format_strings})
|
||||
ORDER BY a.meeting_id ASC, a.attendee_id ASC
|
||||
'''
|
||||
cursor.execute(query, tuple(meeting_ids))
|
||||
|
||||
attendees_map: Dict[int, List[Dict[str, Any]]] = defaultdict(list)
|
||||
for row in cursor.fetchall():
|
||||
attendees_map[row['meeting_id']].append({
|
||||
'user_id': row['user_id'],
|
||||
'caption': row['caption']
|
||||
})
|
||||
|
||||
return dict(attendees_map)
|
||||
|
||||
|
||||
def _load_tags_map(cursor, meetings: List[Dict[str, Any]]) -> Dict[int, List[Tag]]:
|
||||
meeting_tag_names: Dict[int, List[str]] = {}
|
||||
all_tag_names: List[str] = []
|
||||
|
||||
for meeting in meetings:
|
||||
tag_names = _split_tag_names(meeting.get('tags'))
|
||||
meeting_tag_names[meeting['meeting_id']] = tag_names
|
||||
all_tag_names.extend(tag_names)
|
||||
|
||||
if not all_tag_names:
|
||||
return {}
|
||||
|
||||
unique_tag_names = list(dict.fromkeys(all_tag_names))
|
||||
format_strings = ', '.join(['%s'] * len(unique_tag_names))
|
||||
cursor.execute(
|
||||
f"SELECT id, name, color FROM tags WHERE name IN ({format_strings})",
|
||||
tuple(unique_tag_names)
|
||||
)
|
||||
name_to_tag = {row['name']: Tag(**row) for row in cursor.fetchall()}
|
||||
|
||||
return {
|
||||
meeting_id: [name_to_tag[name] for name in tag_names if name in name_to_tag]
|
||||
for meeting_id, tag_names in meeting_tag_names.items()
|
||||
}
|
||||
|
||||
|
||||
def _load_latest_task_map(cursor, meeting_ids: List[int], task_type: str) -> Dict[int, Dict[str, Any]]:
|
||||
if not meeting_ids:
|
||||
return {}
|
||||
|
||||
table_name = 'transcript_tasks' if task_type == 'transcription' else 'llm_tasks'
|
||||
format_strings = ', '.join(['%s'] * len(meeting_ids))
|
||||
query = f'''
|
||||
SELECT meeting_id, task_id, status, progress, created_at, completed_at, error_message
|
||||
FROM {table_name}
|
||||
WHERE meeting_id IN ({format_strings})
|
||||
ORDER BY meeting_id ASC, created_at DESC, task_id DESC
|
||||
'''
|
||||
cursor.execute(query, tuple(meeting_ids))
|
||||
|
||||
latest_task_map: Dict[int, Dict[str, Any]] = {}
|
||||
for row in cursor.fetchall():
|
||||
meeting_id = row['meeting_id']
|
||||
if meeting_id not in latest_task_map:
|
||||
latest_task_map[meeting_id] = _serialize_task_record(row, meeting_id)
|
||||
|
||||
return latest_task_map
|
||||
|
||||
|
||||
def _load_latest_task_record(cursor, meeting_id: int, task_type: str) -> Optional[Dict[str, Any]]:
|
||||
table_name = 'transcript_tasks' if task_type == 'transcription' else 'llm_tasks'
|
||||
query = f'''
|
||||
SELECT task_id, status, progress, created_at, completed_at, error_message
|
||||
FROM {table_name}
|
||||
WHERE meeting_id = %s
|
||||
ORDER BY created_at DESC, task_id DESC
|
||||
LIMIT 1
|
||||
'''
|
||||
cursor.execute(query, (meeting_id,))
|
||||
return _serialize_task_record(cursor.fetchone(), meeting_id)
|
||||
|
||||
|
||||
def _load_overall_status_map(cursor, meeting_ids: List[int]) -> Dict[int, Dict[str, Any]]:
|
||||
if not meeting_ids:
|
||||
return {}
|
||||
|
||||
transcription_map = _load_latest_task_map(cursor, meeting_ids, 'transcription')
|
||||
llm_map = _load_latest_task_map(cursor, meeting_ids, 'llm')
|
||||
|
||||
return {
|
||||
meeting_id: _build_meeting_overall_status(
|
||||
transcription_map.get(meeting_id),
|
||||
llm_map.get(meeting_id)
|
||||
)
|
||||
for meeting_id in meeting_ids
|
||||
}
|
||||
|
||||
|
||||
def _build_task_status_model(task_record: Optional[Dict[str, Any]]) -> Optional[TranscriptionTaskStatus]:
|
||||
if not task_record:
|
||||
return None
|
||||
|
||||
return TranscriptionTaskStatus(
|
||||
task_id=task_record.get('task_id'),
|
||||
status=task_record.get('status', 'pending') or 'pending',
|
||||
progress=int(task_record.get('progress') or 0)
|
||||
)
|
||||
|
||||
def _verify_meeting_owner(cursor, meeting_id: int, current_user_id: int):
|
||||
cursor.execute("SELECT meeting_id, user_id FROM meetings WHERE meeting_id = %s", (meeting_id,))
|
||||
meeting = cursor.fetchone()
|
||||
if not meeting:
|
||||
return None, create_api_response(code="404", message="Meeting not found")
|
||||
if meeting['user_id'] != current_user_id:
|
||||
return None, create_api_response(code="403", message="仅会议创建人可执行此操作")
|
||||
return meeting, None
|
||||
|
||||
def _get_meeting_overall_status(meeting_id: int) -> dict:
|
||||
"""
|
||||
获取会议的整体进度状态(包含转译和LLM两个阶段)
|
||||
|
|
@ -79,74 +287,9 @@ def _get_meeting_overall_status(meeting_id: int) -> dict:
|
|||
"llm": {status, progress, task_id, error_message, created_at}
|
||||
}
|
||||
"""
|
||||
# 获取转译状态
|
||||
transcription_status = transcription_service.get_meeting_transcription_status(meeting_id)
|
||||
trans_data = {
|
||||
"status": transcription_status.get('status', 'pending') if transcription_status else 'pending',
|
||||
"progress": transcription_status.get('progress', 0) if transcription_status else 0,
|
||||
"task_id": transcription_status.get('task_id') if transcription_status else None,
|
||||
"error_message": transcription_status.get('error_message') if transcription_status else None,
|
||||
"created_at": transcription_status.get('created_at') if transcription_status else None
|
||||
}
|
||||
|
||||
# 获取LLM状态
|
||||
llm_status = async_meeting_service.get_meeting_llm_status(meeting_id)
|
||||
llm_data = {
|
||||
"status": llm_status.get('status', 'pending') if llm_status else 'pending',
|
||||
"progress": llm_status.get('progress', 0) if llm_status else 0,
|
||||
"task_id": llm_status.get('task_id') if llm_status else None,
|
||||
"error_message": llm_status.get('error_message') if llm_status else None,
|
||||
"created_at": llm_status.get('created_at') if llm_status else None
|
||||
}
|
||||
|
||||
# 计算整体状态和进度
|
||||
trans_status_val = trans_data["status"]
|
||||
llm_status_val = llm_data["status"]
|
||||
|
||||
# 判断是否有失败
|
||||
if trans_status_val == 'failed':
|
||||
overall_status = "failed"
|
||||
current_stage = "transcription"
|
||||
overall_progress = 0
|
||||
elif llm_status_val == 'failed':
|
||||
overall_status = "failed"
|
||||
current_stage = "llm"
|
||||
overall_progress = 50 # 转译已完成
|
||||
# 判断当前阶段
|
||||
elif trans_status_val == 'completed' and llm_status_val == 'completed':
|
||||
overall_status = "completed"
|
||||
current_stage = "completed"
|
||||
overall_progress = 100
|
||||
elif trans_status_val == 'completed':
|
||||
# 转译完成,进入LLM阶段
|
||||
if llm_status_val in ['pending', 'processing']:
|
||||
overall_status = "summarizing"
|
||||
current_stage = "llm"
|
||||
overall_progress = 50 + int(llm_data["progress"] * 0.5)
|
||||
else:
|
||||
# llm还未开始
|
||||
overall_status = "summarizing"
|
||||
current_stage = "llm"
|
||||
overall_progress = 50
|
||||
else:
|
||||
# 还在转译阶段
|
||||
if trans_status_val in ['pending', 'processing']:
|
||||
overall_status = "transcribing"
|
||||
current_stage = "transcription"
|
||||
overall_progress = int(trans_data["progress"] * 0.5)
|
||||
else:
|
||||
# 转译还未开始
|
||||
overall_status = "pending"
|
||||
current_stage = "transcription"
|
||||
overall_progress = 0
|
||||
|
||||
return {
|
||||
"overall_status": overall_status,
|
||||
"overall_progress": overall_progress,
|
||||
"current_stage": current_stage,
|
||||
"transcription": trans_data,
|
||||
"llm": llm_data
|
||||
}
|
||||
return _build_meeting_overall_status(transcription_status, llm_status)
|
||||
|
||||
@router.get("/meetings")
|
||||
def get_meetings(
|
||||
|
|
@ -209,7 +352,12 @@ def get_meetings(
|
|||
|
||||
# 构建基础查询
|
||||
base_query = '''
|
||||
SELECT m.meeting_id, m.title, m.meeting_time, m.summary, m.created_at, m.tags, m.access_password,
|
||||
SELECT m.meeting_id, m.title, m.meeting_time,
|
||||
CASE
|
||||
WHEN m.summary IS NULL THEN NULL
|
||||
ELSE LEFT(m.summary, 240)
|
||||
END as summary,
|
||||
m.created_at, m.tags, m.access_password,
|
||||
m.user_id as creator_id, u.caption as creator_username, MAX(af.file_path) as audio_file_path
|
||||
FROM meetings m
|
||||
JOIN sys_users u ON m.user_id = u.user_id
|
||||
|
|
@ -250,18 +398,20 @@ def get_meetings(
|
|||
cursor.execute(query, params)
|
||||
meetings = cursor.fetchall()
|
||||
|
||||
meeting_ids = [meeting['meeting_id'] for meeting in meetings]
|
||||
attendees_map = _load_attendees_map(cursor, meeting_ids)
|
||||
tags_map = _load_tags_map(cursor, meetings)
|
||||
status_map = _load_overall_status_map(cursor, meeting_ids)
|
||||
|
||||
meeting_list = []
|
||||
for meeting in meetings:
|
||||
attendees_query = 'SELECT u.user_id, u.caption FROM attendees a JOIN sys_users u ON a.user_id = u.user_id WHERE a.meeting_id = %s'
|
||||
cursor.execute(attendees_query, (meeting['meeting_id'],))
|
||||
attendees_data = cursor.fetchall()
|
||||
attendees = [{'user_id': row['user_id'], 'caption': row['caption']} for row in attendees_data]
|
||||
tags_list = _process_tags(cursor, meeting.get('tags'))
|
||||
progress_info = _get_meeting_overall_status(meeting['meeting_id'])
|
||||
attendees = attendees_map.get(meeting['meeting_id'], [])
|
||||
tags_list = tags_map.get(meeting['meeting_id'], [])
|
||||
progress_info = status_map.get(meeting['meeting_id']) or _build_meeting_overall_status()
|
||||
meeting_list.append(Meeting(
|
||||
meeting_id=meeting['meeting_id'], title=meeting['title'], meeting_time=meeting['meeting_time'],
|
||||
summary=meeting['summary'], created_at=meeting['created_at'], audio_file_path=meeting['audio_file_path'],
|
||||
attendees=attendees, attendee_ids=[row['user_id'] for row in attendees_data], creator_id=meeting['creator_id'], creator_username=meeting['creator_username'], tags=tags_list,
|
||||
attendees=attendees, attendee_ids=[row['user_id'] for row in attendees], creator_id=meeting['creator_id'], creator_username=meeting['creator_username'], tags=tags_list,
|
||||
access_password=meeting.get('access_password'),
|
||||
overall_status=progress_info.get('overall_status'),
|
||||
overall_progress=progress_info.get('overall_progress'),
|
||||
|
|
@ -345,12 +495,15 @@ def get_meeting_details(meeting_id: int, current_user: dict = Depends(get_curren
|
|||
meeting = cursor.fetchone()
|
||||
if not meeting:
|
||||
return create_api_response(code="404", message="Meeting not found")
|
||||
attendees_query = 'SELECT u.user_id, u.caption FROM attendees a JOIN sys_users u ON a.user_id = u.user_id WHERE a.meeting_id = %s'
|
||||
cursor.execute(attendees_query, (meeting['meeting_id'],))
|
||||
attendees_data = cursor.fetchall()
|
||||
|
||||
attendees_data = _load_attendees_map(cursor, [meeting_id]).get(meeting_id, [])
|
||||
attendees = [{'user_id': row['user_id'], 'caption': row['caption']} for row in attendees_data]
|
||||
tags = _process_tags(cursor, meeting.get('tags'))
|
||||
tags = _load_tags_map(cursor, [meeting]).get(meeting_id, [])
|
||||
transcription_task = _load_latest_task_record(cursor, meeting_id, 'transcription')
|
||||
llm_task = _load_latest_task_record(cursor, meeting_id, 'llm')
|
||||
overall_status = _build_meeting_overall_status(transcription_task, llm_task)
|
||||
cursor.close()
|
||||
|
||||
meeting_data = Meeting(
|
||||
meeting_id=meeting['meeting_id'], title=meeting['title'], meeting_time=meeting['meeting_time'],
|
||||
summary=meeting['summary'], created_at=meeting['created_at'], attendees=attendees,
|
||||
|
|
@ -358,24 +511,19 @@ def get_meeting_details(meeting_id: int, current_user: dict = Depends(get_curren
|
|||
creator_id=meeting['creator_id'], creator_username=meeting['creator_username'], tags=tags,
|
||||
prompt_id=meeting.get('prompt_id'),
|
||||
prompt_name=meeting.get('prompt_name'),
|
||||
overall_status=overall_status.get('overall_status'),
|
||||
overall_progress=overall_status.get('overall_progress'),
|
||||
current_stage=overall_status.get('current_stage'),
|
||||
access_password=meeting.get('access_password')
|
||||
)
|
||||
# 只有路径长度大于5(排除空串或占位符)才认为有录音
|
||||
if meeting.get('audio_file_path') and len(meeting['audio_file_path']) > 5:
|
||||
meeting_data.audio_file_path = meeting['audio_file_path']
|
||||
meeting_data.audio_duration = meeting['audio_duration']
|
||||
try:
|
||||
transcription_status_data = transcription_service.get_meeting_transcription_status(meeting_id)
|
||||
if transcription_status_data:
|
||||
meeting_data.transcription_status = TranscriptionTaskStatus(**transcription_status_data)
|
||||
except Exception as e:
|
||||
print(f"Warning: Failed to get transcription status for meeting {meeting_id}: {e}")
|
||||
try:
|
||||
llm_status_data = async_meeting_service.get_meeting_llm_status(meeting_id)
|
||||
if llm_status_data:
|
||||
meeting_data.llm_status = TranscriptionTaskStatus(**llm_status_data)
|
||||
except Exception as e:
|
||||
print(f"Warning: Failed to get llm status for meeting {meeting_id}: {e}")
|
||||
|
||||
meeting_data.transcription_status = _build_task_status_model(transcription_task)
|
||||
meeting_data.llm_status = _build_task_status_model(llm_task)
|
||||
|
||||
return create_api_response(code="200", message="获取会议详情成功", data=meeting_data)
|
||||
|
||||
@router.get("/meetings/{meeting_id}/transcript")
|
||||
|
|
@ -861,7 +1009,10 @@ async def upload_image(meeting_id: int, image_file: UploadFile = File(...), curr
|
|||
def update_speaker_tag(meeting_id: int, request: SpeakerTagUpdateRequest, current_user: dict = Depends(get_current_user)):
|
||||
try:
|
||||
with get_db_connection() as connection:
|
||||
cursor = connection.cursor()
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
_, error_response = _verify_meeting_owner(cursor, meeting_id, current_user['user_id'])
|
||||
if error_response:
|
||||
return error_response
|
||||
update_query = 'UPDATE transcript_segments SET speaker_tag = %s WHERE meeting_id = %s AND speaker_id = %s'
|
||||
cursor.execute(update_query, (request.new_tag, meeting_id, request.speaker_id))
|
||||
if cursor.rowcount == 0:
|
||||
|
|
@ -875,7 +1026,10 @@ def update_speaker_tag(meeting_id: int, request: SpeakerTagUpdateRequest, curren
|
|||
def batch_update_speaker_tags(meeting_id: int, request: BatchSpeakerTagUpdateRequest, current_user: dict = Depends(get_current_user)):
|
||||
try:
|
||||
with get_db_connection() as connection:
|
||||
cursor = connection.cursor()
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
_, error_response = _verify_meeting_owner(cursor, meeting_id, current_user['user_id'])
|
||||
if error_response:
|
||||
return error_response
|
||||
total_updated = 0
|
||||
for update_item in request.updates:
|
||||
update_query = 'UPDATE transcript_segments SET speaker_tag = %s WHERE meeting_id = %s AND speaker_id = %s'
|
||||
|
|
@ -890,7 +1044,10 @@ def batch_update_speaker_tags(meeting_id: int, request: BatchSpeakerTagUpdateReq
|
|||
def batch_update_transcript(meeting_id: int, request: BatchTranscriptUpdateRequest, current_user: dict = Depends(get_current_user)):
|
||||
try:
|
||||
with get_db_connection() as connection:
|
||||
cursor = connection.cursor()
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
_, error_response = _verify_meeting_owner(cursor, meeting_id, current_user['user_id'])
|
||||
if error_response:
|
||||
return error_response
|
||||
total_updated = 0
|
||||
for update_item in request.updates:
|
||||
cursor.execute("SELECT segment_id FROM transcript_segments WHERE segment_id = %s AND meeting_id = %s", (update_item.segment_id, meeting_id))
|
||||
|
|
@ -936,9 +1093,9 @@ def generate_meeting_summary_async(meeting_id: int, request: GenerateSummaryRequ
|
|||
try:
|
||||
with get_db_connection() as connection:
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
cursor.execute("SELECT meeting_id FROM meetings WHERE meeting_id = %s", (meeting_id,))
|
||||
if not cursor.fetchone():
|
||||
return create_api_response(code="404", message="Meeting not found")
|
||||
_, error_response = _verify_meeting_owner(cursor, meeting_id, current_user['user_id'])
|
||||
if error_response:
|
||||
return error_response
|
||||
transcription_status = transcription_service.get_meeting_transcription_status(meeting_id)
|
||||
if transcription_status and transcription_status.get('status') in ['pending', 'processing']:
|
||||
return create_api_response(code="409", message="转录进行中,暂不允许重新总结", data={
|
||||
|
|
@ -1265,6 +1422,12 @@ def update_meeting_access_password(
|
|||
API响应,包含操作结果
|
||||
"""
|
||||
try:
|
||||
normalized_password = None
|
||||
if request.password is not None:
|
||||
normalized_password = request.password.strip()
|
||||
if not normalized_password:
|
||||
return create_api_response(code="400", message="访问密码不能为空")
|
||||
|
||||
with get_db_connection() as connection:
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
|
||||
|
|
@ -1284,15 +1447,15 @@ def update_meeting_access_password(
|
|||
# 更新访问密码
|
||||
cursor.execute(
|
||||
"UPDATE meetings SET access_password = %s WHERE meeting_id = %s",
|
||||
(request.password, meeting_id)
|
||||
(normalized_password, meeting_id)
|
||||
)
|
||||
connection.commit()
|
||||
|
||||
if request.password:
|
||||
if normalized_password:
|
||||
return create_api_response(
|
||||
code="200",
|
||||
message="访问密码已设置",
|
||||
data={"password": request.password}
|
||||
data={"password": normalized_password}
|
||||
)
|
||||
else:
|
||||
return create_api_response(
|
||||
|
|
|
|||
|
|
@ -617,8 +617,8 @@ class SystemConfigService:
|
|||
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 "智能会议控制台"),
|
||||
"home_tagline": str(cls.get_config(cls.BRANDING_HOME_TAGLINE, "让每一次谈话都产生价值") or "让每一次谈话都产生价值"),
|
||||
"console_subtitle": str(cls.get_config(cls.BRANDING_CONSOLE_SUBTITLE, "iMeeting控制台") or "iMeeting控制台"),
|
||||
"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 智听云平台"),
|
||||
|
|
|
|||
|
|
@ -156,11 +156,8 @@ SELECT
|
|||
FROM `sys_menus` m
|
||||
WHERE m.`menu_code` = 'platform_admin';
|
||||
|
||||
-- role 1 gets full access
|
||||
INSERT IGNORE INTO `sys_role_menu_permissions` (`role_id`, `menu_id`, `granted_by`)
|
||||
SELECT 1, m.menu_id, 1
|
||||
FROM sys_menus m
|
||||
WHERE m.menu_code IN ('parameter_management', 'model_management');
|
||||
-- Keep existing role-menu permissions unchanged.
|
||||
-- New menus are added to sys_menus only; authorization is assigned manually.
|
||||
|
||||
-- backfill menu tree metadata for newly inserted rows
|
||||
UPDATE `sys_menus` c
|
||||
|
|
|
|||
|
|
@ -61,27 +61,8 @@ SET c.`menu_level` = p.`menu_level` + 1,
|
|||
c.`tree_path` = CONCAT(p.`tree_path`, '/', c.`menu_id`)
|
||||
WHERE c.`tree_path` IS NULL;
|
||||
|
||||
-- 5) Align permissions: role 1 owns all active menus
|
||||
INSERT IGNORE INTO `sys_role_menu_permissions` (`role_id`, `menu_id`, `granted_by`)
|
||||
SELECT 1, m.`menu_id`, 1
|
||||
FROM `sys_menus` m
|
||||
WHERE m.`is_active` = 1;
|
||||
|
||||
-- role 2 excludes platform admin tree
|
||||
DELETE rmp
|
||||
FROM `sys_role_menu_permissions` rmp
|
||||
JOIN `sys_menus` m ON m.`menu_id` = rmp.`menu_id`
|
||||
WHERE rmp.`role_id` = 2
|
||||
AND m.`menu_code` IN (
|
||||
'platform_admin',
|
||||
'user_management',
|
||||
'permission_management',
|
||||
'dict_management',
|
||||
'hot_word_management',
|
||||
'client_management',
|
||||
'external_app_management',
|
||||
'terminal_management',
|
||||
'permission_menu_tree'
|
||||
);
|
||||
-- 5) Keep existing role-menu permissions unchanged.
|
||||
-- Permission assignment is managed explicitly in the application/admin UI.
|
||||
-- Re-running this migration must not backfill or overwrite production grants.
|
||||
|
||||
COMMIT;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,81 @@
|
|||
-- Migration: optimize meeting loading performance
|
||||
-- Created at: 2026-04-03
|
||||
|
||||
BEGIN;
|
||||
|
||||
SET @sql := IF(
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.statistics
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'audio_files'
|
||||
AND index_name = 'idx_audio_files_meeting_id'
|
||||
),
|
||||
'SELECT 1',
|
||||
'ALTER TABLE `audio_files` ADD KEY `idx_audio_files_meeting_id` (`meeting_id`)'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @sql := IF(
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.statistics
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'transcript_segments'
|
||||
AND index_name = 'idx_transcript_segments_meeting_time'
|
||||
),
|
||||
'SELECT 1',
|
||||
'ALTER TABLE `transcript_segments` ADD KEY `idx_transcript_segments_meeting_time` (`meeting_id`, `start_time_ms`)'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @sql := IF(
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.statistics
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'transcript_tasks'
|
||||
AND index_name = 'idx_transcript_tasks_meeting_created'
|
||||
),
|
||||
'SELECT 1',
|
||||
'ALTER TABLE `transcript_tasks` ADD KEY `idx_transcript_tasks_meeting_created` (`meeting_id`, `created_at`)'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @sql := IF(
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.statistics
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'llm_tasks'
|
||||
AND index_name = 'idx_llm_tasks_meeting_created'
|
||||
),
|
||||
'SELECT 1',
|
||||
'ALTER TABLE `llm_tasks` ADD KEY `idx_llm_tasks_meeting_created` (`meeting_id`, `created_at`)'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @sql := IF(
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.statistics
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'meetings'
|
||||
AND index_name = 'idx_meetings_user_time_created'
|
||||
),
|
||||
'SELECT 1',
|
||||
'ALTER TABLE `meetings` ADD KEY `idx_meetings_user_time_created` (`user_id`, `meeting_time`, `created_at`)'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
COMMIT;
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -97,17 +97,7 @@ SET c.menu_level = p.menu_level + 1,
|
|||
c.tree_path = CONCAT(p.tree_path, '/', c.menu_id)
|
||||
WHERE c.menu_code = 'prompt_config';
|
||||
|
||||
-- permissions:
|
||||
-- admin gets both
|
||||
INSERT IGNORE INTO sys_role_menu_permissions (role_id, menu_id)
|
||||
SELECT 1, m.menu_id
|
||||
FROM sys_menus m
|
||||
WHERE m.menu_code IN ('platform_admin', 'prompt_management', 'prompt_config');
|
||||
|
||||
-- normal user gets platform_admin + prompt_management + prompt_config
|
||||
INSERT IGNORE INTO sys_role_menu_permissions (role_id, menu_id)
|
||||
SELECT 2, m.menu_id
|
||||
FROM sys_menus m
|
||||
WHERE m.menu_code IN ('platform_admin', 'prompt_management', 'prompt_config');
|
||||
-- Keep existing role-menu permissions unchanged.
|
||||
-- This migration only creates/adjusts menu definitions.
|
||||
|
||||
COMMIT;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,222 @@
|
|||
# iMeeting 按钮统一规范
|
||||
|
||||
## 目标
|
||||
|
||||
统一平台内业务操作按钮的视觉语言、尺寸层级和使用方式,避免同一场景中出现不同形态的编辑、删除、查看、复制等操作按钮。
|
||||
|
||||
当前平台按钮分为两套标准:
|
||||
|
||||
1. 业务操作按钮:统一通过 [ActionButton](/Users/jiliu/工作/projects/imeeting/frontend/src/components/ActionButton.jsx) 承载。
|
||||
2. 表单操作条按钮:统一使用 Ant Design `Button` 家族,不与 `ActionButton` 混用。
|
||||
|
||||
## 设计原则
|
||||
|
||||
1. 语义和尺寸分离。
|
||||
语义只表达动作类型,例如查看、编辑、删除;尺寸只表达场景层级,例如列表操作、面板次级操作。
|
||||
|
||||
2. 同一场景只选一种尺寸档。
|
||||
不要在同一条记录、同一个卡片头部、同一个面板操作区里混用大图标、小图标、大文字、小文字。
|
||||
|
||||
3. 删除按钮只保留颜色差异。
|
||||
删除按钮和其它按钮使用同一套圆角、边框、阴影和尺寸,不再使用额外的危险态造型。
|
||||
|
||||
4. 业务操作、表单操作、功能控件分开。
|
||||
- 业务操作统一走 `ActionButton`
|
||||
- 表单操作条统一走标准 `Button`
|
||||
- 编辑器工具栏、播放器控制等功能控件不强行复用这套规范
|
||||
|
||||
5. 同一操作条内外框语言必须一致。
|
||||
如果一组按钮里已经采用普通 `Button`,就不要混入胶囊式 `ActionButton`;如果一组按钮采用 `ActionButton`,就不要再塞入默认 `Button`。
|
||||
|
||||
## 标准尺寸
|
||||
|
||||
### 1. 带文字(大)
|
||||
|
||||
- 组件参数:`variant="textLg"`
|
||||
- 典型场景:面板内次级操作、详情弹窗中的补充动作、信息面板中的“下载结果 / 复制链接 / 编辑当前菜单”
|
||||
- 示例:`编辑`、`删除`、`下载转录结果 (JSON)`、`复制链接`
|
||||
|
||||
### 2. 带文字(小)
|
||||
|
||||
- 组件参数:`variant="textSm"`
|
||||
- 典型场景:卡片头部轻量动作、模块内局部切换动作
|
||||
- 示例:`标签`、`编辑`、`查看详情`、`清空已选`
|
||||
|
||||
### 3. 仅图标(大)
|
||||
|
||||
- 组件参数:`variant="iconLg"`
|
||||
- 典型场景:输入框组合控件右侧操作、需要更高点击面的紧凑操作
|
||||
- 示例:分享密码区的 `复制 / 保存`
|
||||
|
||||
### 4. 仅图标(小)
|
||||
|
||||
- 组件参数:`variant="iconSm"`
|
||||
- 典型场景:列表行操作、卡片角落操作、悬浮于内容边缘的轻量动作
|
||||
- 示例:`编辑 / 删除 / 查看 / 同步 / 更多`
|
||||
|
||||
## 标准语义
|
||||
|
||||
### `tone="view"`
|
||||
|
||||
- 用于查看、打开、复制、下载、预览
|
||||
- 颜色语义:蓝色系
|
||||
|
||||
### `tone="neutral"`
|
||||
|
||||
- 用于更多、清空、重置筛选、非强调型辅助动作
|
||||
- 颜色语义:中性色
|
||||
|
||||
### `tone="edit"`
|
||||
|
||||
- 用于编辑、设置、修改
|
||||
- 颜色语义:蓝色系,与查看同体系但文义偏编辑
|
||||
|
||||
### `tone="accent"`
|
||||
|
||||
- 用于需要强调但非危险的特殊动作
|
||||
- 颜色语义:强调色
|
||||
- 当前典型场景:`重置密码`
|
||||
|
||||
### `tone="delete"`
|
||||
|
||||
- 用于删除、移除、关闭会话等不可逆动作
|
||||
- 颜色语义:红色系
|
||||
|
||||
## 场景映射
|
||||
|
||||
### 列表行操作
|
||||
|
||||
- 一律使用 `iconSm`
|
||||
- 常见组合:`编辑 / 删除`、`查看 / 编辑 / 删除`
|
||||
- 示例页面:
|
||||
- 热词管理
|
||||
- 用户管理
|
||||
- 参数管理
|
||||
- 模型管理
|
||||
- 客户端管理
|
||||
- 外部应用管理
|
||||
|
||||
### 卡片角落操作
|
||||
|
||||
- 一律使用 `iconSm`
|
||||
- 常见组合:`编辑 / 删除`、`更多`
|
||||
- 示例页面:
|
||||
- 会议中心卡片
|
||||
- 时间线卡片
|
||||
- 知识库左侧列表
|
||||
|
||||
### 卡片或模块头部的轻量动作
|
||||
|
||||
- 一律使用 `textSm`
|
||||
- 常见组合:`标签`、`编辑`、`查看详情`
|
||||
|
||||
### 面板或 Drawer 的次级操作
|
||||
|
||||
- 一律使用 `textLg`
|
||||
- 常见组合:`编辑`、`删除`、`复制链接`、`下载转录结果`
|
||||
|
||||
### 表单操作条
|
||||
|
||||
- 一律使用标准 `Button`
|
||||
- 常见组合:`删除 / 取消 / 保存`
|
||||
- 视觉规范:
|
||||
- `删除`:`className="btn-soft-red"`
|
||||
- `取消`:默认 `Button`
|
||||
- `保存`:`type="primary"`
|
||||
- 说明:
|
||||
- 这类按钮属于提交与撤销控制,不使用 `ActionButton`
|
||||
- 同一条内不要出现 `ActionButton tone="delete"` 与普通 `Button` 并排
|
||||
|
||||
### 输入框附属操作
|
||||
|
||||
- 一律使用 `iconLg`
|
||||
- 常见组合:输入框后缀的 `复制 / 保存`
|
||||
|
||||
## 禁止项
|
||||
|
||||
1. 不要在业务操作中直接裸用 `EditOutlined`、`DeleteOutlined` 作为可点击入口。
|
||||
|
||||
2. 不要在同一场景混用:
|
||||
- `shape="circle"` 图标按钮
|
||||
- 旧的 `btn-icon-soft-*`
|
||||
- 默认 `danger` 样式按钮
|
||||
- 新的 `ActionButton`
|
||||
- 普通 `Button` 和 `ActionButton` 的不同外框语言
|
||||
|
||||
3. 不要为了“删除更危险”单独给删除按钮换不同形状、尺寸或阴影。
|
||||
|
||||
4. 不要在业务操作中继续手写 `Tooltip + Button + btn-text-* + btn-action-*`。
|
||||
优先使用 `ActionButton`。
|
||||
|
||||
## 推荐写法
|
||||
|
||||
```jsx
|
||||
import ActionButton from '../components/ActionButton';
|
||||
|
||||
<ActionButton
|
||||
tone="edit"
|
||||
variant="iconSm"
|
||||
tooltip="编辑"
|
||||
icon={<EditOutlined />}
|
||||
onClick={handleEdit}
|
||||
/>
|
||||
```
|
||||
|
||||
```jsx
|
||||
<ActionButton
|
||||
tone="delete"
|
||||
variant="textLg"
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={handleDelete}
|
||||
>
|
||||
删除
|
||||
</ActionButton>
|
||||
```
|
||||
|
||||
```jsx
|
||||
<ActionButton
|
||||
tone="view"
|
||||
variant="iconLg"
|
||||
tooltip="复制密码"
|
||||
icon={<CopyOutlined />}
|
||||
onClick={handleCopy}
|
||||
/>
|
||||
```
|
||||
|
||||
```jsx
|
||||
<Space>
|
||||
<Button className="btn-soft-red" icon={<DeleteOutlined />} onClick={handleDelete}>
|
||||
删除
|
||||
</Button>
|
||||
<Button icon={<CloseOutlined />} onClick={handleCancel}>
|
||||
取消
|
||||
</Button>
|
||||
<Button type="primary" icon={<SaveOutlined />} onClick={handleSave}>
|
||||
保存
|
||||
</Button>
|
||||
</Space>
|
||||
```
|
||||
|
||||
## 落地约束
|
||||
|
||||
1. 新增按钮时,先判断是不是业务动作还是表单提交动作。
|
||||
- 业务动作优先使用 `ActionButton`
|
||||
- 表单操作条优先使用标准 `Button`
|
||||
|
||||
2. 新页面开发时,先确定场景,再选尺寸档。
|
||||
不要先凭感觉挑样式。
|
||||
|
||||
3. 旧页面调整时,如果涉及编辑、删除、查看、更多、复制、下载等业务动作,顺手迁移到 `ActionButton`。
|
||||
|
||||
4. 如果删除按钮和取消、保存并排出现,优先按表单操作条处理,不要单独给删除套胶囊业务按钮。
|
||||
|
||||
## 现状说明
|
||||
|
||||
当前平台中的主要业务操作按钮已基本迁移到 `ActionButton`,主要表单操作条也开始统一为 `删除 / 取消 / 保存` 的标准按钮组合。
|
||||
|
||||
未纳入本规范的主要是两类控件:
|
||||
|
||||
- Markdown 编辑器工具栏按钮
|
||||
- 会议详情中的音频工具栏按钮
|
||||
|
||||
这两类属于功能控件,不按业务操作按钮规范处理。
|
||||
|
|
@ -0,0 +1,129 @@
|
|||
# iMeeting `imeeting_qy` 数据库升级报告
|
||||
|
||||
## 1. 升级目标
|
||||
|
||||
- 源旧库:`imeeting_qy`
|
||||
- 对标最新结构库:`imeeting`
|
||||
- 升级日期:`2026-04-03`
|
||||
- 执行方式:先在临时测试库 `imeeting_qy_upgrade_test` 演练,再正式执行到 `imeeting_qy`
|
||||
|
||||
## 2. 备份信息
|
||||
|
||||
- 正式升级前已创建完整备份库:`imeeting_qy_backup_20260403_004354`
|
||||
- 备份方式:同服务器整库复制全部 `BASE TABLE`
|
||||
|
||||
## 3. 本次执行内容
|
||||
|
||||
本次升级使用迁移脚本:
|
||||
|
||||
- `backend/sql/migrations/upgrade_imeeting_qy_to_latest.sql`
|
||||
|
||||
核心动作如下:
|
||||
|
||||
1. 将旧系统表标准化为 `sys_*` 体系:
|
||||
- `users -> sys_users`
|
||||
- `roles -> sys_roles`
|
||||
- `menus -> sys_menus`
|
||||
- `role_menu_permissions -> sys_role_menu_permissions`
|
||||
- `dict_data -> sys_dict_data`
|
||||
|
||||
2. 重建兼容视图:
|
||||
- `users`
|
||||
- `roles`
|
||||
- `menus`
|
||||
- `role_menu_permissions`
|
||||
- `dict_data`
|
||||
- `system_parameters`
|
||||
|
||||
3. 对齐旧系统表字段与索引:
|
||||
- `sys_users` 补齐 `idx_role_id`
|
||||
- `sys_roles` 补齐 `uk_role_name`
|
||||
- `sys_menus` 补齐 `menu_level/tree_path/is_visible` 及相关索引
|
||||
- `sys_role_menu_permissions` 补齐 `granted_by/granted_at` 及相关索引
|
||||
- `prompts` 补齐 `is_system` 字段及组合索引
|
||||
- `terminals.current_user_id` 字段注释对齐到最新结构
|
||||
|
||||
4. 新增并初始化最新配置表:
|
||||
- `sys_system_parameters`
|
||||
- `ai_model_configs`
|
||||
- `llm_model_config`
|
||||
- `audio_model_config`
|
||||
- `hot_word_group`
|
||||
- `hot_word_item`
|
||||
- `prompt_config`
|
||||
- `sys_user_mcp`
|
||||
|
||||
5. 迁移旧配置数据:
|
||||
- 从 `sys_dict_data(dict_type='system_config')` 迁移系统参数到 `sys_system_parameters`
|
||||
- 迁移 LLM / ASR / 声纹配置到 `ai_model_configs`
|
||||
- 拆分生成 `llm_model_config`、`audio_model_config`
|
||||
- 从旧 `hot_words` 迁移到 `hot_word_group` / `hot_word_item`
|
||||
|
||||
6. 重建最新菜单树与角色授权模型:
|
||||
- 新增 `dashboard`、`desktop`、`meeting_manage`、`system_management` 等最新菜单结构
|
||||
- 规范平台管理、系统管理、会议管理三套菜单层级
|
||||
- 管理员角色授予全部启用菜单
|
||||
- 普通用户保留 `desktop/meeting_manage/meeting_center/prompt_config`
|
||||
|
||||
## 4. 升级结果
|
||||
|
||||
升级后关键表数据如下:
|
||||
|
||||
| 表名 | 行数 |
|
||||
|---|---:|
|
||||
| `sys_users` | 44 |
|
||||
| `sys_roles` | 2 |
|
||||
| `sys_menus` | 19 |
|
||||
| `sys_role_menu_permissions` | 22 |
|
||||
| `sys_system_parameters` | 4 |
|
||||
| `ai_model_configs` | 3 |
|
||||
| `llm_model_config` | 1 |
|
||||
| `audio_model_config` | 2 |
|
||||
| `hot_word_group` | 1 |
|
||||
| `hot_word_item` | 20 |
|
||||
| `prompt_config` | 0 |
|
||||
| `sys_user_mcp` | 0 |
|
||||
|
||||
迁移后的系统参数:
|
||||
|
||||
| 参数键 | 参数值 |
|
||||
|---|---|
|
||||
| `asr_vocabulary_id` | `vocab-imeeting-734e93f5bd8a4f3bb665dd526d584516` |
|
||||
| `default_reset_password` | `123456` |
|
||||
| `max_audio_size` | `500` |
|
||||
| `timeline_pagesize` | `20` |
|
||||
|
||||
迁移后的模型配置:
|
||||
|
||||
- `llm_model_config`:1 条默认模型,`model_code=llm_model`
|
||||
- `audio_model_config`:2 条配置
|
||||
- `audio_model` / `asr`
|
||||
- `voiceprint_model` / `voiceprint`
|
||||
|
||||
迁移后的热词配置:
|
||||
|
||||
- `hot_word_group`:1 个默认热词组
|
||||
- `hot_word_item`:20 条热词条目
|
||||
|
||||
## 5. 角色菜单结果
|
||||
|
||||
- 平台管理员:
|
||||
- `dashboard, hot_word_management, user_management, meeting_center, desktop, meeting_manage, model_management, permission_management, prompt_config, prompt_management, platform_admin, dict_management, system_management, client_management, external_app_management, terminal_management, parameter_management, permission_menu_tree`
|
||||
|
||||
- 普通用户:
|
||||
- `meeting_center, desktop, meeting_manage, prompt_config`
|
||||
|
||||
## 6. 结构校验结论
|
||||
|
||||
对 `imeeting_qy` 与 `imeeting` 进行了 `information_schema.tables` + `information_schema.columns` 级别的最终校验,结果如下:
|
||||
|
||||
- 缺失表:`0`
|
||||
- 多余表:`0`
|
||||
- 表类型差异:`0`
|
||||
- 字段差异:`0`
|
||||
|
||||
结论:
|
||||
|
||||
- `imeeting_qy` 已完成升级
|
||||
- 当前库结构已与 `imeeting` 对齐
|
||||
- 本次升级为“结构对齐 + 必要配置数据迁移”,未删除旧业务数据
|
||||
|
|
@ -28,16 +28,16 @@ body {
|
|||
border-radius: 12px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.01em;
|
||||
transition: transform 0.18s ease, box-shadow 0.22s ease, border-color 0.22s ease, background 0.22s ease, color 0.22s ease;
|
||||
transition: box-shadow 0.22s ease, border-color 0.22s ease, background 0.22s ease, color 0.22s ease;
|
||||
box-shadow: 0 6px 16px rgba(15, 23, 42, 0.06);
|
||||
}
|
||||
|
||||
.ant-btn:hover {
|
||||
transform: translateY(-1px);
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.ant-btn:active {
|
||||
transform: translateY(0);
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.ant-btn .anticon {
|
||||
|
|
@ -56,7 +56,7 @@ body {
|
|||
background: #ffffff;
|
||||
border-color: rgba(59, 130, 246, 0.28);
|
||||
color: #1d4ed8;
|
||||
box-shadow: 0 10px 24px rgba(59, 130, 246, 0.12);
|
||||
box-shadow: 0 8px 18px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
.ant-btn.ant-btn-primary {
|
||||
|
|
@ -67,7 +67,7 @@ body {
|
|||
|
||||
.ant-btn.ant-btn-primary:hover {
|
||||
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 48%, #1d4ed8 100%);
|
||||
box-shadow: 0 14px 28px rgba(37, 99, 235, 0.24);
|
||||
box-shadow: 0 10px 20px rgba(37, 99, 235, 0.18);
|
||||
}
|
||||
|
||||
.ant-btn.ant-btn-primary.ant-btn-dangerous,
|
||||
|
|
@ -79,7 +79,7 @@ body {
|
|||
|
||||
.ant-btn.ant-btn-primary.ant-btn-dangerous:hover,
|
||||
.ant-btn.ant-btn-dangerous:not(.ant-btn-link):not(.ant-btn-text):hover {
|
||||
box-shadow: 0 14px 28px rgba(220, 38, 38, 0.24);
|
||||
box-shadow: 0 10px 20px rgba(220, 38, 38, 0.18);
|
||||
}
|
||||
|
||||
.ant-btn.ant-btn-link,
|
||||
|
|
@ -122,7 +122,7 @@ body {
|
|||
background: linear-gradient(180deg, #eff6ff 0%, #dbeafe 100%);
|
||||
border-color: #93c5fd;
|
||||
color: #1d4ed8;
|
||||
box-shadow: 0 14px 26px rgba(59, 130, 246, 0.18);
|
||||
box-shadow: 0 10px 20px rgba(59, 130, 246, 0.14);
|
||||
}
|
||||
|
||||
.ant-btn.btn-soft-violet,
|
||||
|
|
@ -138,7 +138,7 @@ body {
|
|||
background: linear-gradient(180deg, #f3e8ff 0%, #e9d5ff 100%);
|
||||
border-color: #c084fc;
|
||||
color: #6d28d9;
|
||||
box-shadow: 0 14px 26px rgba(124, 58, 237, 0.18);
|
||||
box-shadow: 0 10px 20px rgba(124, 58, 237, 0.14);
|
||||
}
|
||||
|
||||
.ant-btn.btn-soft-green,
|
||||
|
|
@ -154,67 +154,135 @@ body {
|
|||
background: linear-gradient(180deg, #dcfce7 0%, #bbf7d0 100%);
|
||||
border-color: #4ade80;
|
||||
color: #166534;
|
||||
box-shadow: 0 14px 26px rgba(34, 197, 94, 0.18);
|
||||
box-shadow: 0 10px 20px rgba(34, 197, 94, 0.14);
|
||||
}
|
||||
|
||||
.ant-btn.btn-icon-soft-blue {
|
||||
background: #eff6ff;
|
||||
border-color: #bfdbfe;
|
||||
color: #1d4ed8;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.ant-btn.btn-icon-soft-blue:hover {
|
||||
background: #dbeafe;
|
||||
border-color: #93c5fd;
|
||||
color: #1d4ed8;
|
||||
}
|
||||
|
||||
.ant-btn.btn-icon-soft-red,
|
||||
.ant-btn.ant-btn-dangerous.btn-icon-soft-red {
|
||||
background: #fff1f2;
|
||||
.ant-btn.btn-soft-red,
|
||||
.ant-btn.ant-btn-primary.btn-soft-red {
|
||||
background: linear-gradient(180deg, #fff7f7 0%, #fff1f2 100%);
|
||||
border-color: #fecdd3;
|
||||
color: #dc2626;
|
||||
box-shadow: 0 10px 22px rgba(239, 68, 68, 0.1);
|
||||
}
|
||||
|
||||
.ant-btn.btn-soft-red:hover,
|
||||
.ant-btn.ant-btn-primary.btn-soft-red:hover {
|
||||
background: linear-gradient(180deg, #fff1f2 0%, #ffe4e6 100%);
|
||||
border-color: #fda4af;
|
||||
color: #b91c1c;
|
||||
box-shadow: 0 10px 20px rgba(239, 68, 68, 0.12);
|
||||
}
|
||||
|
||||
.ant-btn.btn-pill-primary {
|
||||
height: 40px;
|
||||
padding-inline: 16px;
|
||||
border-radius: 999px;
|
||||
box-shadow: 0 10px 18px rgba(37, 99, 235, 0.18);
|
||||
}
|
||||
|
||||
.ant-btn.ant-btn-lg.btn-pill-primary {
|
||||
height: 44px;
|
||||
padding-inline: 18px;
|
||||
}
|
||||
|
||||
.ant-btn.btn-pill-secondary {
|
||||
height: 40px;
|
||||
padding-inline: 16px;
|
||||
border-radius: 999px;
|
||||
border-color: #dbe3ef;
|
||||
background: #fff;
|
||||
color: #49627f;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.ant-btn.btn-icon-soft-red:hover,
|
||||
.ant-btn.ant-btn-dangerous.btn-icon-soft-red:hover {
|
||||
background: #ffe4e6;
|
||||
border-color: #fda4af;
|
||||
color: #b91c1c;
|
||||
.ant-btn.btn-pill-secondary:hover,
|
||||
.ant-btn.btn-pill-secondary:focus {
|
||||
border-color: #c7d4e4;
|
||||
background: #f8fafc;
|
||||
color: #355171;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.ant-btn.ant-btn-lg.btn-pill-secondary {
|
||||
height: 44px;
|
||||
padding-inline: 18px;
|
||||
}
|
||||
|
||||
.ant-card-hoverable {
|
||||
transition: box-shadow 0.22s ease, border-color 0.22s ease, background 0.22s ease, transform 0.22s ease;
|
||||
}
|
||||
|
||||
.ant-card-hoverable:hover {
|
||||
transform: none !important;
|
||||
box-shadow: 0 12px 24px rgba(31, 78, 146, 0.12);
|
||||
}
|
||||
|
||||
.ant-btn.ant-btn-link.btn-text-view,
|
||||
.ant-btn.ant-btn-text.btn-text-view,
|
||||
.ant-btn.ant-btn-link.btn-text-neutral,
|
||||
.ant-btn.ant-btn-text.btn-text-neutral,
|
||||
.ant-btn.ant-btn-link.btn-text-edit,
|
||||
.ant-btn.ant-btn-text.btn-text-edit,
|
||||
.ant-btn.ant-btn-link.btn-text-accent,
|
||||
.ant-btn.ant-btn-text.btn-text-accent,
|
||||
.ant-btn.ant-btn-link.btn-text-delete,
|
||||
.ant-btn.ant-btn-text.btn-text-delete,
|
||||
.ant-btn.ant-btn-link.ant-btn-dangerous.btn-text-delete,
|
||||
.ant-btn.ant-btn-text.ant-btn-dangerous.btn-text-delete {
|
||||
border: 1px solid #d7e4f6;
|
||||
background: linear-gradient(180deg, #ffffff 0%, #f8fbff 100%);
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.88), 0 2px 8px rgba(25, 63, 121, 0.06);
|
||||
}
|
||||
|
||||
.ant-btn.ant-btn-link.btn-text-view,
|
||||
.ant-btn.ant-btn-text.btn-text-view {
|
||||
color: #2563eb;
|
||||
border-color: #c9ddfb;
|
||||
}
|
||||
|
||||
.ant-btn.ant-btn-link.btn-text-view:hover,
|
||||
.ant-btn.ant-btn-text.btn-text-view:hover {
|
||||
background: rgba(37, 99, 235, 0.1);
|
||||
background: linear-gradient(180deg, #f8fbff 0%, #eef5ff 100%);
|
||||
border-color: #a9c9fa;
|
||||
color: #1d4ed8;
|
||||
}
|
||||
|
||||
.ant-btn.ant-btn-link.btn-text-neutral,
|
||||
.ant-btn.ant-btn-text.btn-text-neutral {
|
||||
color: #5f7392;
|
||||
border-color: #d7e4f6;
|
||||
}
|
||||
|
||||
.ant-btn.ant-btn-link.btn-text-neutral:hover,
|
||||
.ant-btn.ant-btn-text.btn-text-neutral:hover {
|
||||
background: linear-gradient(180deg, #fbfdff 0%, #f1f6fc 100%);
|
||||
border-color: #bfd0e6;
|
||||
color: #355171;
|
||||
}
|
||||
|
||||
.ant-btn.ant-btn-link.btn-text-edit,
|
||||
.ant-btn.ant-btn-text.btn-text-edit {
|
||||
color: #0f766e;
|
||||
color: #1d4ed8;
|
||||
border-color: #c9ddfb;
|
||||
}
|
||||
|
||||
.ant-btn.ant-btn-link.btn-text-edit:hover,
|
||||
.ant-btn.ant-btn-text.btn-text-edit:hover {
|
||||
background: rgba(13, 148, 136, 0.1);
|
||||
color: #0f766e;
|
||||
background: linear-gradient(180deg, #f8fbff 0%, #eef5ff 100%);
|
||||
border-color: #a9c9fa;
|
||||
color: #1d4ed8;
|
||||
}
|
||||
|
||||
.ant-btn.ant-btn-link.btn-text-accent,
|
||||
.ant-btn.ant-btn-text.btn-text-accent {
|
||||
color: #7c3aed;
|
||||
border-color: #dbc8fb;
|
||||
}
|
||||
|
||||
.ant-btn.ant-btn-link.btn-text-accent:hover,
|
||||
.ant-btn.ant-btn-text.btn-text-accent:hover {
|
||||
background: rgba(124, 58, 237, 0.1);
|
||||
background: linear-gradient(180deg, #fbf8ff 0%, #f3ebff 100%);
|
||||
border-color: #caa6fb;
|
||||
color: #6d28d9;
|
||||
}
|
||||
|
||||
|
|
@ -223,13 +291,15 @@ body {
|
|||
.ant-btn.ant-btn-link.ant-btn-dangerous.btn-text-delete,
|
||||
.ant-btn.ant-btn-text.ant-btn-dangerous.btn-text-delete {
|
||||
color: #dc2626;
|
||||
border-color: #fecdd3;
|
||||
}
|
||||
|
||||
.ant-btn.ant-btn-link.btn-text-delete:hover,
|
||||
.ant-btn.ant-btn-text.btn-text-delete:hover,
|
||||
.ant-btn.ant-btn-link.ant-btn-dangerous.btn-text-delete:hover,
|
||||
.ant-btn.ant-btn-text.ant-btn-dangerous.btn-text-delete:hover {
|
||||
background: rgba(220, 38, 38, 0.1);
|
||||
background: linear-gradient(180deg, #ffffff 0%, #fff5f5 100%);
|
||||
border-color: #fda4af;
|
||||
color: #b91c1c;
|
||||
}
|
||||
|
||||
|
|
@ -242,6 +312,55 @@ body {
|
|||
min-width: 40px;
|
||||
}
|
||||
|
||||
.ant-btn.btn-action-text-lg {
|
||||
min-height: 32px;
|
||||
padding-inline: 12px;
|
||||
border-radius: 999px;
|
||||
font-size: 13px;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.ant-btn.btn-action-text-sm,
|
||||
.ant-btn.btn-action-inline {
|
||||
min-height: 26px;
|
||||
padding-inline: 10px;
|
||||
border-radius: 999px;
|
||||
font-size: 12px;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.ant-btn.btn-action-icon-lg,
|
||||
.ant-btn.btn-action-icon-sm,
|
||||
.ant-btn.btn-action-compact {
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.ant-btn.btn-action-icon-lg.ant-btn-icon-only {
|
||||
width: 32px;
|
||||
min-width: 32px;
|
||||
height: 32px;
|
||||
padding-inline: 0;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.ant-btn.btn-action-icon-sm.ant-btn-icon-only,
|
||||
.ant-btn.btn-action-compact.ant-btn-icon-only {
|
||||
width: 26px;
|
||||
min-width: 26px;
|
||||
height: 26px;
|
||||
padding-inline: 0;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.ant-btn.btn-action-icon-lg .anticon {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.ant-btn.btn-action-icon-sm .anticon,
|
||||
.ant-btn.btn-action-compact .anticon {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.app-loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
@ -303,7 +422,7 @@ body {
|
|||
}
|
||||
|
||||
.btn:hover {
|
||||
transform: translateY(-1px);
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,56 @@
|
|||
import React from 'react';
|
||||
import { Button, Tooltip } from 'antd';
|
||||
|
||||
const TONE_CLASS_MAP = {
|
||||
view: 'btn-text-view',
|
||||
neutral: 'btn-text-neutral',
|
||||
edit: 'btn-text-edit',
|
||||
accent: 'btn-text-accent',
|
||||
delete: 'btn-text-delete',
|
||||
};
|
||||
|
||||
const VARIANT_CLASS_MAP = {
|
||||
textLg: 'btn-action-text-lg',
|
||||
textSm: 'btn-action-text-sm',
|
||||
iconLg: 'btn-action-icon-lg',
|
||||
iconSm: 'btn-action-icon-sm',
|
||||
};
|
||||
|
||||
const ActionButton = ({
|
||||
tone = 'neutral',
|
||||
variant = 'iconSm',
|
||||
tooltip,
|
||||
tooltipProps,
|
||||
className,
|
||||
size,
|
||||
children,
|
||||
...buttonProps
|
||||
}) => {
|
||||
const toneClass = TONE_CLASS_MAP[tone] || TONE_CLASS_MAP.neutral;
|
||||
const variantClass = VARIANT_CLASS_MAP[variant] || VARIANT_CLASS_MAP.iconSm;
|
||||
const resolvedSize = size ?? (variant === 'textSm' || variant === 'iconSm' ? 'small' : undefined);
|
||||
const mergedClassName = [toneClass, variantClass, className].filter(Boolean).join(' ');
|
||||
|
||||
const buttonNode = (
|
||||
<Button
|
||||
type="text"
|
||||
size={resolvedSize}
|
||||
className={mergedClassName}
|
||||
{...buttonProps}
|
||||
>
|
||||
{children}
|
||||
</Button>
|
||||
);
|
||||
|
||||
if (!tooltip) {
|
||||
return buttonNode;
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip title={tooltip} {...tooltipProps}>
|
||||
<span style={{ display: 'inline-flex' }}>{buttonNode}</span>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
export default ActionButton;
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
.shared-meeting-card.ant-card {
|
||||
height: 224px;
|
||||
border-radius: 20px;
|
||||
border: 1px solid #d7e4f4;
|
||||
background: linear-gradient(180deg, #ffffff 0%, #f7fbff 100%);
|
||||
box-shadow: 0 8px 22px rgba(31, 78, 146, 0.08);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.shared-meeting-card.ant-card:hover {
|
||||
border-color: #bfd5f6;
|
||||
background: linear-gradient(180deg, #ffffff 0%, #f2f8ff 100%);
|
||||
box-shadow: 0 14px 28px rgba(31, 78, 146, 0.12);
|
||||
}
|
||||
|
||||
.shared-meeting-card .ant-card-body {
|
||||
padding: 16px 16px 14px;
|
||||
}
|
||||
|
||||
.shared-meeting-card-shell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.shared-meeting-card-top {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.shared-meeting-card-title-block {
|
||||
min-height: 26px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.shared-meeting-card-title.ant-typography {
|
||||
margin: 0 !important;
|
||||
font-size: 18px;
|
||||
line-height: 1.3;
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.shared-meeting-card-summary {
|
||||
min-height: 42px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.shared-meeting-card-summary-content.ant-typography {
|
||||
margin: 0 !important;
|
||||
color: #7184a0;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.shared-meeting-card-summary.is-empty .shared-meeting-card-summary-content.ant-typography {
|
||||
color: #b1bfd1;
|
||||
}
|
||||
|
||||
.shared-meeting-card-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 0;
|
||||
margin-top: auto;
|
||||
color: #6b7a90;
|
||||
}
|
||||
|
||||
.shared-meeting-card-meta-icon {
|
||||
font-size: 15px;
|
||||
color: #70819b;
|
||||
}
|
||||
|
||||
.shared-meeting-card-meta-text {
|
||||
color: #64748b;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.shared-meeting-card-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.shared-meeting-card-footer-user.ant-typography {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.shared-meeting-card-footer-link {
|
||||
border-radius: 999px;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.shared-meeting-card.ant-card {
|
||||
height: 216px;
|
||||
}
|
||||
|
||||
.shared-meeting-card-title.ant-typography {
|
||||
font-size: 17px;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +1,10 @@
|
|||
import React from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
App,
|
||||
Avatar,
|
||||
Button,
|
||||
Card,
|
||||
Divider,
|
||||
Dropdown,
|
||||
Space,
|
||||
Tag,
|
||||
Timeline,
|
||||
|
|
@ -18,15 +16,48 @@ import {
|
|||
ClockCircleOutlined,
|
||||
DeleteOutlined,
|
||||
EditOutlined,
|
||||
FileTextOutlined,
|
||||
MoreOutlined,
|
||||
TeamOutlined,
|
||||
UserOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import MarkdownRenderer from './MarkdownRenderer';
|
||||
import ActionButton from './ActionButton';
|
||||
import tools from '../utils/tools';
|
||||
import './MeetingInfoCard.css';
|
||||
|
||||
const { Title, Text, Paragraph } = Typography;
|
||||
const { Text, Paragraph } = Typography;
|
||||
|
||||
const ROLE_META = {
|
||||
creator: {
|
||||
label: '我发起',
|
||||
tagClass: 'console-tag-soft-blue',
|
||||
},
|
||||
attendee: {
|
||||
label: '我参与',
|
||||
tagClass: 'console-tag-soft-green',
|
||||
},
|
||||
};
|
||||
|
||||
const STATUS_META = {
|
||||
completed: {
|
||||
label: '已完成',
|
||||
tagClass: 'console-tag-soft-green',
|
||||
},
|
||||
failed: {
|
||||
label: '失败',
|
||||
tagClass: 'console-tag-soft-red',
|
||||
},
|
||||
transcribing: {
|
||||
label: '转写中',
|
||||
tagClass: 'console-tag-soft-blue',
|
||||
},
|
||||
summarizing: {
|
||||
label: '总结中',
|
||||
tagClass: 'console-tag-soft-orange',
|
||||
},
|
||||
pending: {
|
||||
label: '待处理',
|
||||
tagClass: 'console-tag-soft-default',
|
||||
},
|
||||
};
|
||||
|
||||
const formatDateMeta = (date) => {
|
||||
const parsed = new Date(date);
|
||||
|
|
@ -39,6 +70,12 @@ const formatDateMeta = (date) => {
|
|||
};
|
||||
};
|
||||
|
||||
const getStatusMeta = (meeting) => STATUS_META[meeting?.overall_status || 'pending'] || STATUS_META.pending;
|
||||
const getSummaryPreview = (summary) => {
|
||||
if (!summary) return '';
|
||||
return tools.stripMarkdown(summary).replace(/\s+/g, ' ').trim();
|
||||
};
|
||||
|
||||
const MeetingTimeline = ({
|
||||
meetingsByDate,
|
||||
currentUser,
|
||||
|
|
@ -50,7 +87,6 @@ const MeetingTimeline = ({
|
|||
searchQuery = '',
|
||||
selectedTags = [],
|
||||
}) => {
|
||||
const { modal } = App.useApp();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleEditClick = (event, meetingId) => {
|
||||
|
|
@ -62,13 +98,7 @@ const MeetingTimeline = ({
|
|||
const handleDeleteClick = (event, meeting) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
modal.confirm({
|
||||
title: '删除会议',
|
||||
content: `确定要删除会议“${meeting.title}”吗?此操作无法撤销。`,
|
||||
okText: '删除',
|
||||
okType: 'danger',
|
||||
onOk: () => onDeleteMeeting(meeting.meeting_id),
|
||||
});
|
||||
onDeleteMeeting(meeting.meeting_id);
|
||||
};
|
||||
|
||||
const sortedDates = Object.keys(meetingsByDate).sort((a, b) => new Date(b) - new Date(a));
|
||||
|
|
@ -82,69 +112,97 @@ const MeetingTimeline = ({
|
|||
{dateMeta.sub ? <Text className="timeline-date-sub">{dateMeta.sub}</Text> : null}
|
||||
</div>
|
||||
),
|
||||
dot: <span className="timeline-date-dot" />,
|
||||
children: (
|
||||
<div className="timeline-date-group">
|
||||
{meetingsByDate[date].map((meeting) => {
|
||||
const isCreator = String(meeting.creator_id) === String(currentUser.user_id);
|
||||
const menuItems = [
|
||||
{ key: 'edit', label: '编辑', icon: <EditOutlined />, onClick: ({ domEvent }) => handleEditClick(domEvent, meeting.meeting_id) },
|
||||
{ key: 'delete', label: '删除', icon: <DeleteOutlined />, danger: true, onClick: ({ domEvent }) => handleDeleteClick(domEvent, meeting) },
|
||||
];
|
||||
const roleMeta = isCreator ? ROLE_META.creator : ROLE_META.attendee;
|
||||
const statusMeta = getStatusMeta(meeting);
|
||||
const summaryPreview = getSummaryPreview(meeting.summary);
|
||||
const hasSummary = Boolean(summaryPreview);
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={meeting.meeting_id}
|
||||
hoverable
|
||||
className="timeline-meeting-card"
|
||||
style={{ borderLeft: isCreator ? '4px solid #1677ff' : '4px solid #52c41a' }}
|
||||
variant="borderless"
|
||||
className="timeline-meeting-card console-surface shared-meeting-card"
|
||||
onClick={() => navigate(`/meetings/${meeting.meeting_id}`, {
|
||||
state: { filterContext: { filterType, searchQuery, selectedTags } },
|
||||
})}
|
||||
>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: 14, gap: 16 }}>
|
||||
<Space direction="vertical" size={6} style={{ flex: 1 }}>
|
||||
<Title level={4} style={{ margin: 0, fontSize: 20 }}>{meeting.title}</Title>
|
||||
<Space size={12} split={<Divider type="vertical" />} wrap>
|
||||
<Text type="secondary"><ClockCircleOutlined /> {tools.formatTime(meeting.meeting_time)}</Text>
|
||||
<Text type="secondary"><TeamOutlined /> {meeting.attendees?.length || 0} 人</Text>
|
||||
<Space size={[6, 6]} wrap>
|
||||
{meeting.tags?.slice(0, 4).map((tag) => (
|
||||
<Tag key={tag.id} color="blue" bordered={false} style={{ fontSize: 12, borderRadius: 999 }}>
|
||||
{tag.name}
|
||||
</Tag>
|
||||
))}
|
||||
<div className="shared-meeting-card-shell">
|
||||
<div className="shared-meeting-card-top">
|
||||
<Space size={10} wrap>
|
||||
<Tag bordered={false} className={`${roleMeta.tagClass} console-tag-large`}>
|
||||
{roleMeta.label}
|
||||
</Tag>
|
||||
<Tag bordered={false} className={`${statusMeta.tagClass} console-tag-large`}>
|
||||
{statusMeta.label}
|
||||
</Tag>
|
||||
</Space>
|
||||
{isCreator ? (
|
||||
<Space size={8}>
|
||||
<ActionButton
|
||||
tone="edit"
|
||||
variant="iconSm"
|
||||
tooltip="编辑"
|
||||
icon={<EditOutlined />}
|
||||
onClick={(event) => handleEditClick(event, meeting.meeting_id)}
|
||||
/>
|
||||
<ActionButton
|
||||
tone="delete"
|
||||
variant="iconSm"
|
||||
tooltip="删除"
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={(event) => handleDeleteClick(event, meeting)}
|
||||
/>
|
||||
</Space>
|
||||
</Space>
|
||||
</Space>
|
||||
{isCreator ? (
|
||||
<Dropdown menu={{ items: menuItems }} placement="bottomRight" arrow trigger={['click']}>
|
||||
<Button type="text" icon={<MoreOutlined />} className="timeline-action-trigger" onClick={(event) => event.stopPropagation()} />
|
||||
</Dropdown>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{meeting.summary ? (
|
||||
<div className="timeline-summary-box">
|
||||
<Space size={8} style={{ marginBottom: 8, display: 'flex' }}>
|
||||
<FileTextOutlined style={{ color: '#1677ff' }} />
|
||||
<Text strong>会议摘要</Text>
|
||||
</Space>
|
||||
<div className="timeline-summary-content">
|
||||
<Paragraph ellipsis={{ rows: 2 }} type="secondary" style={{ margin: 0, fontSize: 13 }}>
|
||||
<MarkdownRenderer content={tools.truncateSummary(meeting.summary)} />
|
||||
</Paragraph>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 12, flexWrap: 'wrap' }}>
|
||||
<Space>
|
||||
<Avatar size="small" src={meeting.creator_avatar_url} icon={<UserOutlined />} />
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>{meeting.creator_username}</Text>
|
||||
</Space>
|
||||
<Button type="text" size="small" icon={<ArrowRightOutlined />} className="timeline-footer-link">
|
||||
查看详情
|
||||
</Button>
|
||||
<div className="shared-meeting-card-title-block">
|
||||
<Paragraph
|
||||
className="shared-meeting-card-title"
|
||||
ellipsis={{ rows: 2, tooltip: meeting.title }}
|
||||
>
|
||||
{meeting.title}
|
||||
</Paragraph>
|
||||
</div>
|
||||
|
||||
<div className={`shared-meeting-card-summary ${hasSummary ? '' : 'is-empty'}`}>
|
||||
<Paragraph
|
||||
className="shared-meeting-card-summary-content"
|
||||
ellipsis={hasSummary ? { rows: 2, tooltip: summaryPreview } : false}
|
||||
>
|
||||
{hasSummary ? summaryPreview : '暂无摘要'}
|
||||
</Paragraph>
|
||||
</div>
|
||||
|
||||
<div className="shared-meeting-card-meta">
|
||||
<Space size={10}>
|
||||
<ClockCircleOutlined className="shared-meeting-card-meta-icon" />
|
||||
<Text className="shared-meeting-card-meta-text">
|
||||
{tools.formatTime(meeting.meeting_time || meeting.created_at)}
|
||||
</Text>
|
||||
</Space>
|
||||
<Divider type="vertical" />
|
||||
<Space size={10}>
|
||||
<TeamOutlined className="shared-meeting-card-meta-icon" />
|
||||
<Text className="shared-meeting-card-meta-text">{meeting.attendees?.length || 0} 人</Text>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<div className="shared-meeting-card-footer">
|
||||
<Space>
|
||||
<Avatar size="small" src={meeting.creator_avatar_url} icon={<UserOutlined />} />
|
||||
<Text type="secondary" className="shared-meeting-card-footer-user">{meeting.creator_username}</Text>
|
||||
</Space>
|
||||
<ActionButton tone="view" variant="textSm" icon={<ArrowRightOutlined />} className="shared-meeting-card-footer-link">
|
||||
查看详情
|
||||
</ActionButton>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
|
|
@ -157,15 +215,15 @@ const MeetingTimeline = ({
|
|||
return (
|
||||
<div className="modern-timeline">
|
||||
<Timeline mode="left" items={timelineItems} />
|
||||
<div style={{ textAlign: 'center', marginTop: 28 }}>
|
||||
{hasMore ? (
|
||||
<Button onClick={onLoadMore} loading={loadingMore} icon={<CalendarOutlined />}>
|
||||
加载更多
|
||||
</Button>
|
||||
) : (
|
||||
<Divider plain><Text type="secondary">已加载全部会议</Text></Divider>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ textAlign: 'center', marginTop: 28 }}>
|
||||
{hasMore ? (
|
||||
<Button className="btn-pill-secondary" onClick={onLoadMore} loading={loadingMore} icon={<CalendarOutlined />}>
|
||||
加载更多
|
||||
</Button>
|
||||
) : (
|
||||
<Divider plain><Text type="secondary">已加载全部会议</Text></Divider>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import React from 'react';
|
|||
import { Modal, Typography, Space, Button, App } from 'antd';
|
||||
import { QrcodeOutlined, CopyOutlined, CheckOutlined } from '@ant-design/icons';
|
||||
import { QRCodeSVG } from 'qrcode.react';
|
||||
import ActionButton from './ActionButton';
|
||||
|
||||
const { Text, Paragraph } = Typography;
|
||||
|
||||
|
|
@ -19,7 +20,7 @@ const QRCodeModal = ({ open, onClose, url, title = "扫描二维码分享", chil
|
|||
open={open}
|
||||
onCancel={onClose}
|
||||
footer={[
|
||||
<Button key="copy" icon={<CopyOutlined />} onClick={handleCopy}>复制链接</Button>,
|
||||
<ActionButton key="copy" tone="view" variant="textLg" icon={<CopyOutlined />} onClick={handleCopy}>复制链接</ActionButton>,
|
||||
<Button key="close" type="primary" icon={<CheckOutlined />} className="btn-soft-green" onClick={onClose}>完成</Button>
|
||||
]}
|
||||
width={400}
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ const TagCloud = ({
|
|||
}
|
||||
};
|
||||
|
||||
const handleTagClick = (tag, checked) => {
|
||||
const handleTagClick = (tag) => {
|
||||
if (onTagClick) {
|
||||
onTagClick(tag.name);
|
||||
}
|
||||
|
|
@ -66,13 +66,8 @@ const TagCloud = ({
|
|||
<CheckableTag
|
||||
key={tag.id}
|
||||
checked={selectedTags.includes(tag.name)}
|
||||
onChange={(checked) => handleTagClick(tag, checked)}
|
||||
style={{
|
||||
border: '1px solid #d9d9d9',
|
||||
borderRadius: '4px',
|
||||
padding: '2px 10px',
|
||||
fontSize: '13px'
|
||||
}}
|
||||
onChange={() => handleTagClick(tag)}
|
||||
className="console-tag-large"
|
||||
>
|
||||
{tag.name}
|
||||
</CheckableTag>
|
||||
|
|
@ -89,7 +84,6 @@ const TagCloud = ({
|
|||
closable
|
||||
color="blue"
|
||||
onClose={() => onTagClick(tag)}
|
||||
style={{ borderRadius: 4 }}
|
||||
>
|
||||
{tag}
|
||||
</Tag>
|
||||
|
|
|
|||
|
|
@ -18,13 +18,18 @@ const TagDisplay = ({
|
|||
<Space size={4} wrap className={className}>
|
||||
{showIcon && <TagOutlined style={{ color: '#8c8c8c', fontSize: size === 'small' ? 12 : 14 }} />}
|
||||
{displayTags.map((tag, index) => (
|
||||
<Tag key={index} color="blue" bordered={false} style={{ margin: 0, fontSize: size === 'small' ? 11 : 12 }}>
|
||||
<Tag
|
||||
key={index}
|
||||
color="blue"
|
||||
bordered={false}
|
||||
className={size === 'small' ? 'console-tag-compact' : ''}
|
||||
>
|
||||
{tag}
|
||||
</Tag>
|
||||
))}
|
||||
{remainingCount > 0 && (
|
||||
<Tooltip title={tags.slice(maxDisplay).join('、')}>
|
||||
<Tag bordered={false} style={{ margin: 0, fontSize: size === 'small' ? 11 : 12 }}>
|
||||
<Tag bordered={false} className={size === 'small' ? 'console-tag-compact' : ''}>
|
||||
+{remainingCount}
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
|
|
|
|||
|
|
@ -39,7 +39,6 @@ const TagEditor = ({ tags = [], onTagsChange }) => {
|
|||
closable
|
||||
onClose={() => handleClose(tag)}
|
||||
color="blue"
|
||||
style={{ borderRadius: 4 }}
|
||||
>
|
||||
{tag}
|
||||
</Tag>
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import {
|
|||
import apiClient from '../utils/apiClient';
|
||||
import { API_ENDPOINTS, buildApiUrl } from '../config/api';
|
||||
import StatusTag from '../components/StatusTag';
|
||||
import ActionButton from '../components/ActionButton';
|
||||
import './AccountSettings.css';
|
||||
|
||||
const { Title, Paragraph, Text } = Typography;
|
||||
|
|
@ -391,9 +392,7 @@ const AccountSettings = ({ user, onUpdateUser }) => {
|
|||
<Text className="account-settings-mcp-field-label">X-BOT-ID</Text>
|
||||
<div className="account-settings-mcp-field-value">{mcpConfig?.bot_id || '-'}</div>
|
||||
<div style={{ marginTop: 16 }}>
|
||||
<Button icon={<CopyOutlined />} onClick={() => copyToClipboard(mcpConfig?.bot_id, 'Bot ID')}>
|
||||
复制 Bot ID
|
||||
</Button>
|
||||
<ActionButton tone="view" variant="textLg" icon={<CopyOutlined />} onClick={() => copyToClipboard(mcpConfig?.bot_id, 'Bot ID')}>复制 Bot ID</ActionButton>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
|
|
@ -402,9 +401,7 @@ const AccountSettings = ({ user, onUpdateUser }) => {
|
|||
<Text className="account-settings-mcp-field-label">X-BOT-SECRET</Text>
|
||||
<div className="account-settings-mcp-field-value">{mcpConfig?.bot_secret || '-'}</div>
|
||||
<Space style={{ marginTop: 16 }} wrap>
|
||||
<Button icon={<CopyOutlined />} onClick={() => copyToClipboard(mcpConfig?.bot_secret, 'Secret')}>
|
||||
复制 Secret
|
||||
</Button>
|
||||
<ActionButton tone="view" variant="textLg" icon={<CopyOutlined />} onClick={() => copyToClipboard(mcpConfig?.bot_secret, 'Secret')}>复制 Secret</ActionButton>
|
||||
<Text className="account-settings-muted">变更后旧 Secret 立即失效</Text>
|
||||
</Space>
|
||||
</Card>
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ import {
|
|||
} from '@ant-design/icons';
|
||||
import apiClient from '../utils/apiClient';
|
||||
import { buildApiUrl, API_ENDPOINTS } from '../config/api';
|
||||
import ActionButton from '../components/ActionButton';
|
||||
import './AdminDashboard.css';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
|
@ -262,7 +263,7 @@ const AdminDashboard = () => {
|
|||
key: 'action',
|
||||
align: 'center',
|
||||
render: (_, record) => (
|
||||
<Button type="text" danger className="btn-text-delete" icon={<UserDeleteOutlined />} onClick={() => handleKickUser(record)} />
|
||||
<ActionButton tone="delete" variant="iconSm" tooltip="踢出会话" icon={<UserDeleteOutlined />} onClick={() => handleKickUser(record)} />
|
||||
),
|
||||
},
|
||||
];
|
||||
|
|
@ -281,11 +282,9 @@ const AdminDashboard = () => {
|
|||
key: 'meeting_title',
|
||||
render: (text, record) => (
|
||||
<Space>
|
||||
{record.meeting_id && (
|
||||
<Tooltip title="查看详情">
|
||||
<Button type="link" size="small" icon={<SearchOutlined />} onClick={() => handleViewMeeting(record.meeting_id)} />
|
||||
</Tooltip>
|
||||
)}
|
||||
{record.meeting_id ? (
|
||||
<ActionButton tone="view" variant="iconSm" tooltip="查看详情" icon={<SearchOutlined />} onClick={() => handleViewMeeting(record.meeting_id)} />
|
||||
) : null}
|
||||
<span>{text || '-'}</span>
|
||||
</Space>
|
||||
),
|
||||
|
|
@ -517,9 +516,9 @@ const AdminDashboard = () => {
|
|||
: '无时长信息'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="操作">
|
||||
<Button type="link" icon={<FileTextOutlined />} onClick={() => handleDownloadTranscript(meetingDetails.meeting_id)}>
|
||||
<ActionButton tone="view" variant="textLg" icon={<FileTextOutlined />} onClick={() => handleDownloadTranscript(meetingDetails.meeting_id)}>
|
||||
下载转录结果 (JSON)
|
||||
</Button>
|
||||
</ActionButton>
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ import {
|
|||
import apiClient from '../utils/apiClient';
|
||||
import { buildApiUrl, API_ENDPOINTS } from '../config/api';
|
||||
import AdminModuleShell from '../components/AdminModuleShell';
|
||||
import ActionButton from '../components/ActionButton';
|
||||
|
||||
const { Text } = Typography;
|
||||
const { TextArea } = Input;
|
||||
|
|
@ -362,16 +363,10 @@ const ClientManagement = () => {
|
|||
fixed: 'right',
|
||||
width: 140,
|
||||
render: (_, record) => (
|
||||
<Space size="middle">
|
||||
<Tooltip title="编辑">
|
||||
<Button type="text" className="btn-text-edit" icon={<EditOutlined />} onClick={() => openModal(record)} />
|
||||
</Tooltip>
|
||||
<Tooltip title="下载">
|
||||
<Button type="text" className="btn-text-view" icon={<DownloadOutlined />} href={record.download_url} target="_blank" />
|
||||
</Tooltip>
|
||||
<Tooltip title="删除">
|
||||
<Button type="text" danger className="btn-text-delete" icon={<DeleteOutlined />} onClick={() => handleDelete(record)} />
|
||||
</Tooltip>
|
||||
<Space size={6}>
|
||||
<ActionButton tone="edit" variant="iconSm" tooltip="编辑" icon={<EditOutlined />} onClick={() => openModal(record)} />
|
||||
<ActionButton tone="view" variant="iconSm" tooltip="下载" icon={<DownloadOutlined />} href={record.download_url} target="_blank" />
|
||||
<ActionButton tone="delete" variant="iconSm" tooltip="删除" icon={<DeleteOutlined />} onClick={() => handleDelete(record)} />
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
|
|
|
|||
|
|
@ -313,8 +313,8 @@
|
|||
}
|
||||
|
||||
.btn-submit:hover:not(:disabled) {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 16px rgba(59, 130, 246, 0.4);
|
||||
transform: none;
|
||||
box-shadow: 0 3px 12px rgba(59, 130, 246, 0.32);
|
||||
}
|
||||
|
||||
.btn-submit:disabled {
|
||||
|
|
@ -348,4 +348,4 @@
|
|||
font-size: 0.8rem;
|
||||
padding: 0.4rem 0.6rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,15 @@
|
|||
.dashboard-v3 {
|
||||
padding-bottom: 40px;
|
||||
min-height: calc(100vh - 128px);
|
||||
padding: 10px 0 28px;
|
||||
}
|
||||
|
||||
.dashboard-hero-card,
|
||||
.dashboard-main-card {
|
||||
border-radius: 24px;
|
||||
border: 1px solid #dbe6f2;
|
||||
box-shadow: 0 18px 38px rgba(31, 78, 146, 0.08);
|
||||
background: linear-gradient(180deg, rgba(255, 255, 255, 0.98) 0%, #f8fbff 100%);
|
||||
border-radius: 18px;
|
||||
}
|
||||
|
||||
.dashboard-hero-card .ant-card-body {
|
||||
padding: 20px 24px;
|
||||
}
|
||||
|
||||
.dashboard-user-block {
|
||||
|
|
@ -49,8 +51,8 @@
|
|||
}
|
||||
|
||||
.dashboard-stat-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 18px 34px rgba(31, 78, 146, 0.12);
|
||||
transform: none;
|
||||
box-shadow: 0 14px 28px rgba(31, 78, 146, 0.11);
|
||||
}
|
||||
|
||||
.dashboard-stat-card.active {
|
||||
|
|
@ -134,20 +136,8 @@
|
|||
}
|
||||
|
||||
.dashboard-icon-button.active {
|
||||
border-color: #93c5fd;
|
||||
background: #dbeafe;
|
||||
color: #1d4ed8;
|
||||
}
|
||||
|
||||
.dashboard-search-row .dashboard-icon-button:nth-of-type(2) {
|
||||
border-color: #bfdbfe;
|
||||
background: #eff6ff;
|
||||
color: #2563eb;
|
||||
}
|
||||
|
||||
.dashboard-search-row .dashboard-icon-button:nth-of-type(2):hover {
|
||||
background: #dbeafe;
|
||||
border-color: #93c5fd;
|
||||
border-color: #c9ddfb;
|
||||
background: linear-gradient(180deg, #f8fbff 0%, #eef5ff 100%);
|
||||
color: #1d4ed8;
|
||||
}
|
||||
|
||||
|
|
@ -176,6 +166,9 @@
|
|||
|
||||
.dashboard-main-content {
|
||||
padding: 24px 22px 28px;
|
||||
min-height: 520px;
|
||||
background: #f7fafc;
|
||||
border-radius: 0 0 20px 20px;
|
||||
}
|
||||
|
||||
.dashboard-timeline-wrap {
|
||||
|
|
@ -198,96 +191,57 @@
|
|||
}
|
||||
|
||||
.modern-timeline .ant-timeline-item {
|
||||
padding-bottom: 28px;
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
|
||||
.modern-timeline .ant-timeline-item-label {
|
||||
width: 118px !important;
|
||||
width: 72px !important;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.modern-timeline .ant-timeline-item-tail,
|
||||
.modern-timeline .ant-timeline-item-head {
|
||||
inset-inline-start: 136px !important;
|
||||
inset-inline-start: 84px !important;
|
||||
}
|
||||
|
||||
.modern-timeline .ant-timeline-item-content {
|
||||
inset-inline-start: 160px !important;
|
||||
width: calc(100% - 178px) !important;
|
||||
inset-inline-start: 96px !important;
|
||||
width: calc(100% - 112px) !important;
|
||||
}
|
||||
|
||||
.timeline-date-label {
|
||||
padding-right: 12px;
|
||||
padding-right: 10px;
|
||||
text-align: right;
|
||||
opacity: 0.78;
|
||||
opacity: 0.88;
|
||||
}
|
||||
|
||||
.timeline-date-main {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
color: #6b84a6;
|
||||
color: #5f7595;
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.timeline-date-sub {
|
||||
display: block;
|
||||
margin-top: 2px;
|
||||
color: #a0b2c9;
|
||||
color: #91a5bf;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.timeline-date-dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(180deg, #2563eb 0%, #38bdf8 100%);
|
||||
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.14);
|
||||
}
|
||||
|
||||
.timeline-date-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.timeline-meeting-card {
|
||||
margin-bottom: 12px;
|
||||
border-radius: 20px;
|
||||
border: 1px solid #dbe5f2;
|
||||
box-shadow: 0 14px 30px rgba(15, 23, 42, 0.06);
|
||||
}
|
||||
|
||||
.timeline-meeting-card .ant-card-body {
|
||||
padding: 16px 18px;
|
||||
}
|
||||
|
||||
.timeline-summary-box {
|
||||
background: linear-gradient(180deg, #f8fbff 0%, #f2f7ff 100%);
|
||||
padding: 12px 13px;
|
||||
border-radius: 14px;
|
||||
border: 1px solid #e2ebf6;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.timeline-summary-content {
|
||||
min-height: 44px;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.timeline-action-trigger {
|
||||
color: #8ba0bb;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.timeline-action-trigger:hover {
|
||||
background: #f5f8fc;
|
||||
color: #375a8c;
|
||||
}
|
||||
|
||||
.timeline-footer-link {
|
||||
color: #7b93b2;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.timeline-footer-link:hover {
|
||||
background: #f5f8fc;
|
||||
color: #1d4ed8;
|
||||
}
|
||||
|
||||
.timeline-footer-link {
|
||||
padding-right: 0;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
|
|
@ -304,17 +258,17 @@
|
|||
}
|
||||
|
||||
.modern-timeline .ant-timeline-item-label {
|
||||
width: 82px !important;
|
||||
width: 72px !important;
|
||||
}
|
||||
|
||||
.modern-timeline .ant-timeline-item-tail,
|
||||
.modern-timeline .ant-timeline-item-head {
|
||||
inset-inline-start: 100px !important;
|
||||
inset-inline-start: 84px !important;
|
||||
}
|
||||
|
||||
.modern-timeline .ant-timeline-item-content {
|
||||
inset-inline-start: 118px !important;
|
||||
width: calc(100% - 132px) !important;
|
||||
inset-inline-start: 96px !important;
|
||||
width: calc(100% - 112px) !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -341,6 +295,10 @@
|
|||
width: calc(100% - 28px) !important;
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
.timeline-card-title.ant-typography {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.prompt-config-segmented.ant-segmented {
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ import {
|
|||
import { useNavigate } from 'react-router-dom';
|
||||
import apiClient from '../utils/apiClient';
|
||||
import { API_ENDPOINTS, buildApiUrl } from '../config/api';
|
||||
import ActionButton from '../components/ActionButton';
|
||||
import MeetingTimeline from '../components/MeetingTimeline';
|
||||
import TagCloud from '../components/TagCloud';
|
||||
import VoiceprintCollectionModal from '../components/VoiceprintCollectionModal';
|
||||
|
|
@ -204,7 +205,7 @@ const Dashboard = ({ user }) => {
|
|||
try {
|
||||
const userResponse = await apiClient.get(buildApiUrl(API_ENDPOINTS.USERS.DETAIL(user.user_id)));
|
||||
setUserInfo(userResponse.data);
|
||||
} catch (error) {
|
||||
} catch {
|
||||
message.error('获取用户信息失败');
|
||||
}
|
||||
};
|
||||
|
|
@ -266,7 +267,7 @@ const Dashboard = ({ user }) => {
|
|||
|
||||
return (
|
||||
<div className="dashboard-v3">
|
||||
<Card variant="borderless" className="dashboard-hero-card" style={{ marginBottom: 24 }}>
|
||||
<Card variant="borderless" className="console-surface dashboard-hero-card" style={{ marginBottom: 16 }}>
|
||||
<Row gutter={[28, 24]} align="middle">
|
||||
<Col xs={24} xl={8}>
|
||||
<div className="dashboard-user-block">
|
||||
|
|
@ -284,7 +285,7 @@ const Dashboard = ({ user }) => {
|
|||
<Tag
|
||||
color={voiceprintStatus?.has_voiceprint ? 'success' : 'default'}
|
||||
icon={<AudioOutlined />}
|
||||
style={{ cursor: 'pointer', borderRadius: 999, paddingInline: 10 }}
|
||||
className="console-tag-clickable"
|
||||
onClick={voiceprintStatus?.has_voiceprint ? handleDeleteVoiceprint : () => setShowVoiceprintModal(true)}
|
||||
>
|
||||
{voiceprintStatus?.has_voiceprint ? '声纹已采' : '未采声纹'}
|
||||
|
|
@ -327,7 +328,7 @@ const Dashboard = ({ user }) => {
|
|||
</Row>
|
||||
</Card>
|
||||
|
||||
<Card variant="borderless" className="dashboard-main-card" styles={{ body: { padding: 0 } }}>
|
||||
<Card variant="borderless" className="console-surface dashboard-main-card" styles={{ body: { padding: 0 } }}>
|
||||
<div className="dashboard-toolbar">
|
||||
<div className="dashboard-search-row">
|
||||
<Input
|
||||
|
|
@ -340,22 +341,22 @@ const Dashboard = ({ user }) => {
|
|||
onChange={(e) => setSearchInput(e.target.value)}
|
||||
onPressEnter={() => setSearchQuery(searchInput.trim())}
|
||||
/>
|
||||
<Tooltip title="搜索会议">
|
||||
<Button
|
||||
icon={<SearchOutlined />}
|
||||
size="large"
|
||||
onClick={() => setSearchQuery(searchInput.trim())}
|
||||
className="dashboard-icon-button"
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title={selectedTags.length ? `标签过滤(已选 ${selectedTags.length})` : '标签过滤'}>
|
||||
<Button
|
||||
icon={<FilterOutlined />}
|
||||
size="large"
|
||||
onClick={() => setShowTagFilters((prev) => !prev)}
|
||||
className={`dashboard-icon-button ${showTagFilters ? 'active' : ''}`}
|
||||
/>
|
||||
</Tooltip>
|
||||
<ActionButton
|
||||
tone="neutral"
|
||||
variant="iconLg"
|
||||
tooltip="搜索会议"
|
||||
icon={<SearchOutlined />}
|
||||
onClick={() => setSearchQuery(searchInput.trim())}
|
||||
className="dashboard-icon-button"
|
||||
/>
|
||||
<ActionButton
|
||||
tone={showTagFilters ? 'view' : 'neutral'}
|
||||
variant="iconLg"
|
||||
tooltip={selectedTags.length ? `标签过滤(已选 ${selectedTags.length})` : '标签过滤'}
|
||||
icon={<FilterOutlined />}
|
||||
onClick={() => setShowTagFilters((prev) => !prev)}
|
||||
className={`dashboard-icon-button ${showTagFilters ? 'active' : ''}`}
|
||||
/>
|
||||
|
||||
<div className="dashboard-toolbar-actions">
|
||||
<Tooltip title="新建会议">
|
||||
|
|
@ -363,7 +364,7 @@ const Dashboard = ({ user }) => {
|
|||
type="primary"
|
||||
size="large"
|
||||
icon={<PlusOutlined />}
|
||||
className="dashboard-add-button"
|
||||
className="dashboard-add-button btn-pill-primary"
|
||||
onClick={() => navigate('/meetings/center', { state: { openCreate: true } })}
|
||||
>
|
||||
新建会议
|
||||
|
|
@ -380,7 +381,7 @@ const Dashboard = ({ user }) => {
|
|||
<Text type="secondary">点击标签可快速筛选会议时间轴</Text>
|
||||
</Space>
|
||||
{selectedTags.length > 0 ? (
|
||||
<Button type="link" icon={<ClearOutlined />} onClick={() => setSelectedTags([])}>清空已选</Button>
|
||||
<ActionButton tone="neutral" variant="textSm" icon={<ClearOutlined />} onClick={() => setSelectedTags([])}>清空已选</ActionButton>
|
||||
) : null}
|
||||
</div>
|
||||
<TagCloud
|
||||
|
|
@ -419,8 +420,9 @@ const Dashboard = ({ user }) => {
|
|||
<Space direction="vertical">
|
||||
<Text type="secondary">未找到符合条件的会议记录</Text>
|
||||
{(searchQuery || selectedTags.length > 0 || filterType !== 'all') && (
|
||||
<Button
|
||||
type="link"
|
||||
<ActionButton
|
||||
tone="neutral"
|
||||
variant="textLg"
|
||||
icon={<CloseCircleOutlined />}
|
||||
onClick={() => {
|
||||
setSearchInput('');
|
||||
|
|
@ -431,7 +433,7 @@ const Dashboard = ({ user }) => {
|
|||
}}
|
||||
>
|
||||
清除所有过滤
|
||||
</Button>
|
||||
</ActionButton>
|
||||
)}
|
||||
</Space>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -180,8 +180,8 @@
|
|||
}
|
||||
|
||||
.btn-submit:hover:not(:disabled) {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 16px rgba(16, 185, 129, 0.4);
|
||||
transform: none;
|
||||
box-shadow: 0 3px 12px rgba(16, 185, 129, 0.32);
|
||||
}
|
||||
|
||||
.btn-submit:disabled {
|
||||
|
|
|
|||
|
|
@ -116,8 +116,8 @@
|
|||
}
|
||||
|
||||
.regenerate-btn:hover:not(:disabled) {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 8px rgba(245, 158, 11, 0.4);
|
||||
transform: none;
|
||||
box-shadow: 0 3px 8px rgba(245, 158, 11, 0.3);
|
||||
}
|
||||
|
||||
.regenerate-btn:disabled {
|
||||
|
|
@ -336,8 +336,8 @@
|
|||
}
|
||||
|
||||
.btn-submit:hover:not(:disabled) {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 16px rgba(16, 185, 129, 0.4);
|
||||
transform: none;
|
||||
box-shadow: 0 3px 12px rgba(16, 185, 129, 0.32);
|
||||
}
|
||||
|
||||
.btn-submit:disabled {
|
||||
|
|
@ -379,8 +379,8 @@
|
|||
|
||||
.show-upload-btn:hover {
|
||||
background: linear-gradient(135deg, #d97706, #b45309);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 8px rgba(245, 158, 11, 0.4);
|
||||
transform: none;
|
||||
box-shadow: 0 3px 8px rgba(245, 158, 11, 0.3);
|
||||
}
|
||||
|
||||
.upload-header {
|
||||
|
|
@ -489,7 +489,7 @@
|
|||
|
||||
.upload-btn:hover:not(:disabled) {
|
||||
background: #5a67d8;
|
||||
transform: translateY(-1px);
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.upload-btn:disabled {
|
||||
|
|
@ -637,4 +637,4 @@
|
|||
.modal-actions .btn-submit:disabled {
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -107,8 +107,8 @@
|
|||
.download-link:hover {
|
||||
background-color: var(--accent-color);
|
||||
color: white;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 15px rgba(111, 66, 193, 0.2);
|
||||
transform: none;
|
||||
box-shadow: 0 3px 12px rgba(111, 66, 193, 0.16);
|
||||
}
|
||||
|
||||
.login-btn {
|
||||
|
|
@ -119,8 +119,8 @@
|
|||
|
||||
.login-btn:hover {
|
||||
background-color: var(--accent-color-dark);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 15px rgba(111, 66, 193, 0.2);
|
||||
transform: none;
|
||||
box-shadow: 0 3px 12px rgba(111, 66, 193, 0.16);
|
||||
}
|
||||
|
||||
/* Hero Section */
|
||||
|
|
@ -194,8 +194,8 @@
|
|||
}
|
||||
|
||||
.cta-button:hover {
|
||||
transform: translateY(-3px) scale(1.05);
|
||||
box-shadow: 0 7px 25px rgba(0, 0, 0, 0.15);
|
||||
transform: none;
|
||||
box-shadow: 0 6px 22px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
/* Features Section */
|
||||
|
|
@ -234,8 +234,8 @@
|
|||
}
|
||||
|
||||
.feature-card:hover {
|
||||
transform: translateY(-8px);
|
||||
box-shadow: 0 12px 30px rgba(0, 0, 0, 0.1);
|
||||
transform: none;
|
||||
box-shadow: 0 10px 22px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.feature-icon {
|
||||
|
|
@ -314,7 +314,7 @@
|
|||
.error-message-container { min-height: 52px; display: flex; align-items: center; }
|
||||
.error-message { background: #fff5f5; color: #c53030; padding: 0.85rem; border-radius: 8px; border: 1px solid #fed7d7; font-size: 0.9rem; width: 100%; }
|
||||
.submit-btn { background: var(--accent-color); color: white; border: none; padding: 0.85rem; border-radius: 10px; font-size: 1rem; font-weight: 600; cursor: pointer; transition: all 0.3s ease; }
|
||||
.submit-btn:hover:not(:disabled) { background-color: var(--accent-color-dark); transform: translateY(-2px); box-shadow: 0 4px 15px rgba(111, 66, 193, 0.2); }
|
||||
.submit-btn:hover:not(:disabled) { background-color: var(--accent-color-dark); transform: none; box-shadow: 0 3px 12px rgba(111, 66, 193, 0.16); }
|
||||
.submit-btn:disabled { opacity: 0.6; cursor: not-allowed; }
|
||||
.demo-info { margin-top: 1.5rem; padding: 1rem; background: #f8f9fa; border-radius: 8px; font-size: 0.85rem; color: var(--text-light); text-align: center; }
|
||||
.demo-info p { margin: 0.25rem 0; }
|
||||
|
|
@ -325,4 +325,4 @@
|
|||
.hero-subtitle { font-size: 1.1rem; }
|
||||
.features-grid { grid-template-columns: 1fr; }
|
||||
.login-modal { margin: 1rem; padding: 2rem; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,12 @@ import configService, { DEFAULT_BRANDING_CONFIG } from '../utils/configService';
|
|||
|
||||
const { Title, Paragraph, Text } = Typography;
|
||||
|
||||
const BRAND_HIGHLIGHTS = [
|
||||
'智能转录与结构化总结',
|
||||
'时间轴回看与全文检索',
|
||||
'沉淀可追踪的会议资产',
|
||||
];
|
||||
|
||||
const HomePage = ({ onLogin }) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [branding, setBranding] = useState(DEFAULT_BRANDING_CONFIG);
|
||||
|
|
@ -68,17 +74,112 @@ const HomePage = ({ onLogin }) => {
|
|||
flexDirection: 'column',
|
||||
justifyContent: 'center'
|
||||
}}>
|
||||
<div style={{ marginBottom: 80 }}>
|
||||
<BrandLogo title={branding.app_name} size={48} titleSize={24} gap={12} titleColor="#6f42c1" />
|
||||
</div>
|
||||
<div style={{ maxWidth: 640 }}>
|
||||
<div style={{ marginBottom: 64 }}>
|
||||
<BrandLogo title={branding.app_name} size={48} titleSize={24} gap={12} titleColor="#6f42c1" />
|
||||
</div>
|
||||
|
||||
<div style={{ maxWidth: 600 }}>
|
||||
<Title style={{ fontSize: 56, marginBottom: 24, fontWeight: 800 }}>
|
||||
<span style={{ color: '#1677ff' }}>{branding.home_headline}</span>
|
||||
</Title>
|
||||
<Paragraph style={{ fontSize: 20, color: '#4e5969', lineHeight: 1.6 }}>
|
||||
{branding.home_tagline}
|
||||
</Paragraph>
|
||||
<div
|
||||
style={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
padding: '8px 16px',
|
||||
borderRadius: 999,
|
||||
marginBottom: 24,
|
||||
border: '1px solid rgba(37, 99, 235, 0.14)',
|
||||
background: 'rgba(255, 255, 255, 0.55)',
|
||||
boxShadow: 'inset 0 1px 0 rgba(255,255,255,0.75)',
|
||||
}}
|
||||
>
|
||||
<Text style={{ color: '#47658d', fontSize: 13, fontWeight: 700, letterSpacing: '0.08em' }}>
|
||||
INTELLIGENT MEETING WORKSPACE
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<div style={{ maxWidth: 600 }}>
|
||||
<div
|
||||
style={{
|
||||
marginBottom: 24,
|
||||
display: 'flex',
|
||||
alignItems: 'baseline',
|
||||
gap: 18,
|
||||
flexWrap: 'wrap',
|
||||
}}
|
||||
>
|
||||
<Title
|
||||
level={2}
|
||||
style={{
|
||||
margin: 0,
|
||||
fontSize: 38,
|
||||
lineHeight: 1.05,
|
||||
fontWeight: 700,
|
||||
letterSpacing: '0.02em',
|
||||
color: '#365b8d',
|
||||
}}
|
||||
>
|
||||
iMeeting
|
||||
</Title>
|
||||
|
||||
<Title
|
||||
level={1}
|
||||
style={{
|
||||
margin: 0,
|
||||
fontSize: 48,
|
||||
lineHeight: 1.04,
|
||||
fontWeight: 800,
|
||||
letterSpacing: '-0.03em',
|
||||
color: '#1677ff',
|
||||
}}
|
||||
>
|
||||
{branding.home_headline}
|
||||
</Title>
|
||||
</div>
|
||||
|
||||
<Paragraph
|
||||
style={{
|
||||
fontSize: 22,
|
||||
color: '#334e73',
|
||||
lineHeight: 1.65,
|
||||
marginBottom: 18,
|
||||
maxWidth: 560,
|
||||
}}
|
||||
>
|
||||
{branding.home_tagline}
|
||||
</Paragraph>
|
||||
|
||||
<Paragraph
|
||||
style={{
|
||||
fontSize: 16,
|
||||
color: '#5a718f',
|
||||
lineHeight: 1.9,
|
||||
marginBottom: 28,
|
||||
maxWidth: 560,
|
||||
}}
|
||||
>
|
||||
</Paragraph>
|
||||
|
||||
<Space size={[10, 12]} wrap>
|
||||
{BRAND_HIGHLIGHTS.map((item) => (
|
||||
<span
|
||||
key={item}
|
||||
style={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
padding: '9px 14px',
|
||||
borderRadius: 999,
|
||||
border: '1px solid rgba(113, 140, 178, 0.18)',
|
||||
background: 'rgba(255, 255, 255, 0.62)',
|
||||
color: '#4d678b',
|
||||
fontSize: 14,
|
||||
fontWeight: 600,
|
||||
boxShadow: '0 8px 18px rgba(40, 72, 120, 0.06)',
|
||||
}}
|
||||
>
|
||||
{item}
|
||||
</span>
|
||||
))}
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import { useNavigate } from 'react-router-dom';
|
|||
import apiClient from '../utils/apiClient';
|
||||
import { buildApiUrl, API_ENDPOINTS } from '../config/api';
|
||||
import ContentViewer from '../components/ContentViewer';
|
||||
import ActionButton from '../components/ActionButton';
|
||||
import exportService from '../services/exportService';
|
||||
import tools from '../utils/tools';
|
||||
import meetingCacheService from '../services/meetingCacheService';
|
||||
|
|
@ -201,9 +202,9 @@ const KnowledgeBasePage = ({ user }) => {
|
|||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<Text strong ellipsis style={{ width: '80%' }}>{item.title}</Text>
|
||||
{selectedKb?.kb_id === item.kb_id && (
|
||||
<Space>
|
||||
<EditOutlined onClick={(e) => { e.stopPropagation(); navigate(`/knowledge-base/edit/${item.kb_id}`); }} />
|
||||
<DeleteOutlined onClick={(e) => { e.stopPropagation(); handleDelete(item); }} style={{ color: '#ff4d4f' }} />
|
||||
<Space size={6}>
|
||||
<ActionButton tone="edit" variant="iconSm" tooltip="编辑" icon={<EditOutlined />} onClick={(e) => { e.stopPropagation(); navigate(`/knowledge-base/edit/${item.kb_id}`); }} />
|
||||
<ActionButton tone="delete" variant="iconSm" tooltip="删除" icon={<DeleteOutlined />} onClick={(e) => { e.stopPropagation(); handleDelete(item); }} />
|
||||
</Space>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,63 @@
|
|||
.meeting-center-page {
|
||||
min-height: calc(100vh - 128px);
|
||||
padding: 10px 0 28px;
|
||||
}
|
||||
|
||||
.meeting-center-header.ant-space {
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.meeting-center-header .ant-space-item:last-child {
|
||||
margin-inline-start: auto;
|
||||
}
|
||||
|
||||
.meeting-center-header-actions.ant-space {
|
||||
justify-content: flex-end;
|
||||
gap: 12px 12px;
|
||||
}
|
||||
|
||||
.meeting-center-search-input {
|
||||
width: 240px;
|
||||
}
|
||||
|
||||
.meeting-center-content-panel {
|
||||
min-height: 560px;
|
||||
}
|
||||
|
||||
.meeting-center-card.ant-card {
|
||||
height: 224px;
|
||||
}
|
||||
|
||||
.meeting-center-empty {
|
||||
min-height: 460px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
@media (max-width: 991px) {
|
||||
.meeting-center-header .ant-space-item:last-child {
|
||||
margin-inline-start: 0;
|
||||
}
|
||||
|
||||
.meeting-center-header-actions.ant-space {
|
||||
width: 100%;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.meeting-center-search-input {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.meeting-center-page {
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
.meeting-center-card.ant-card {
|
||||
height: 216px;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,30 +1,38 @@
|
|||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import {
|
||||
App,
|
||||
Avatar,
|
||||
Button,
|
||||
Card,
|
||||
Col,
|
||||
Divider,
|
||||
Empty,
|
||||
Input,
|
||||
Row,
|
||||
Segmented,
|
||||
Space,
|
||||
Tag,
|
||||
Tooltip,
|
||||
Typography,
|
||||
} from 'antd';
|
||||
import {
|
||||
CalendarOutlined,
|
||||
ArrowRightOutlined,
|
||||
ClockCircleOutlined,
|
||||
DeleteOutlined,
|
||||
EditOutlined,
|
||||
PlusOutlined,
|
||||
RightOutlined,
|
||||
SearchOutlined,
|
||||
TeamOutlined,
|
||||
UserOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import apiClient from '../utils/apiClient';
|
||||
import { API_ENDPOINTS, buildApiUrl } from '../config/api';
|
||||
import ActionButton from '../components/ActionButton';
|
||||
import CenterPager from '../components/CenterPager';
|
||||
import MeetingFormDrawer from '../components/MeetingFormDrawer';
|
||||
import meetingCacheService from '../services/meetingCacheService';
|
||||
import tools from '../utils/tools';
|
||||
import './MeetingCenterPage.css';
|
||||
import '../components/MeetingInfoCard.css';
|
||||
|
||||
const { Title, Text, Paragraph } = Typography;
|
||||
|
||||
|
|
@ -34,41 +42,42 @@ const FILTER_OPTIONS = [
|
|||
{ label: '我参与', value: 'attended' },
|
||||
];
|
||||
|
||||
const ROLE_META = {
|
||||
creator: {
|
||||
label: '我发起',
|
||||
tagClass: 'console-tag-soft-blue',
|
||||
},
|
||||
attendee: {
|
||||
label: '我参与',
|
||||
tagClass: 'console-tag-soft-green',
|
||||
},
|
||||
};
|
||||
|
||||
const STATUS_META = {
|
||||
completed: {
|
||||
label: '已完成',
|
||||
tagColor: '#52c41a',
|
||||
tagBg: '#f6ffed',
|
||||
tagBorder: '#b7eb8f',
|
||||
tagClass: 'console-tag-soft-green',
|
||||
accent: '#52c41a',
|
||||
},
|
||||
failed: {
|
||||
label: '失败',
|
||||
tagColor: '#ff4d4f',
|
||||
tagBg: '#fff1f0',
|
||||
tagBorder: '#ffccc7',
|
||||
tagClass: 'console-tag-soft-red',
|
||||
accent: '#ff4d4f',
|
||||
},
|
||||
transcribing: {
|
||||
label: '转写中',
|
||||
tagColor: '#1677ff',
|
||||
tagBg: '#eff6ff',
|
||||
tagBorder: '#bfdbfe',
|
||||
tagClass: 'console-tag-soft-blue',
|
||||
accent: '#1677ff',
|
||||
},
|
||||
summarizing: {
|
||||
label: '总结中',
|
||||
tagColor: '#fa8c16',
|
||||
tagBg: '#fff7e6',
|
||||
tagBorder: '#ffd591',
|
||||
tagClass: 'console-tag-soft-orange',
|
||||
accent: '#fa8c16',
|
||||
},
|
||||
pending: {
|
||||
label: '待处理',
|
||||
tagColor: '#8c8c8c',
|
||||
tagBg: '#fafafa',
|
||||
tagBorder: '#d9d9d9',
|
||||
accent: '#d9d9d9',
|
||||
tagClass: 'console-tag-soft-default',
|
||||
accent: '#cbd5e1',
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -77,11 +86,9 @@ const getStatusMeta = (meeting) => {
|
|||
return STATUS_META[status] || STATUS_META.pending;
|
||||
};
|
||||
|
||||
const formatMeetingTime = (value) => {
|
||||
if (!value) return '-';
|
||||
const date = new Date(value);
|
||||
if (Number.isNaN(date.getTime())) return '-';
|
||||
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')} ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`;
|
||||
const getSummaryPreview = (summary) => {
|
||||
if (!summary) return '';
|
||||
return tools.stripMarkdown(summary).replace(/\s+/g, ' ').trim();
|
||||
};
|
||||
|
||||
const MeetingCenterPage = ({ user }) => {
|
||||
|
|
@ -99,7 +106,20 @@ const MeetingCenterPage = ({ user }) => {
|
|||
const [formDrawerOpen, setFormDrawerOpen] = useState(false);
|
||||
const [editingMeetingId, setEditingMeetingId] = useState(null);
|
||||
|
||||
const loadMeetings = async (nextPage = page, nextKeyword = keyword, nextFilter = filterType) => {
|
||||
const loadMeetings = useCallback(async (nextPage = page, nextKeyword = keyword, nextFilter = filterType, options = {}) => {
|
||||
const { forceRefresh = false } = options;
|
||||
const filterKey = meetingCacheService.generateFilterKey(user.user_id, nextFilter, nextKeyword, []);
|
||||
|
||||
if (!forceRefresh) {
|
||||
const cachedPage = meetingCacheService.getPage(filterKey, nextPage);
|
||||
if (cachedPage) {
|
||||
setMeetings(cachedPage.meetings || []);
|
||||
setTotal(cachedPage.pagination?.total || 0);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await apiClient.get(buildApiUrl(API_ENDPOINTS.MEETINGS.LIST), {
|
||||
|
|
@ -111,18 +131,26 @@ const MeetingCenterPage = ({ user }) => {
|
|||
search: nextKeyword || undefined,
|
||||
},
|
||||
});
|
||||
setMeetings(res.data.meetings || []);
|
||||
setTotal(res.data.total || 0);
|
||||
const nextMeetings = res.data.meetings || [];
|
||||
const nextPagination = {
|
||||
page: res.data.page || nextPage,
|
||||
total: res.data.total || 0,
|
||||
has_more: Boolean(res.data.has_more),
|
||||
};
|
||||
|
||||
meetingCacheService.setPage(filterKey, nextPage, nextMeetings, nextPagination);
|
||||
setMeetings(nextMeetings);
|
||||
setTotal(nextPagination.total);
|
||||
} catch (error) {
|
||||
message.error(error?.response?.data?.message || '加载会议失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
}, [filterType, keyword, message, page, pageSize, user.user_id]);
|
||||
|
||||
useEffect(() => {
|
||||
loadMeetings(page, keyword, filterType);
|
||||
}, [page, keyword, filterType]);
|
||||
}, [filterType, keyword, loadMeetings, page]);
|
||||
|
||||
useEffect(() => {
|
||||
if (location.state?.openCreate) {
|
||||
|
|
@ -142,9 +170,10 @@ const MeetingCenterPage = ({ user }) => {
|
|||
try {
|
||||
await apiClient.delete(buildApiUrl(API_ENDPOINTS.MEETINGS.DELETE(meeting.meeting_id)));
|
||||
message.success('会议已删除');
|
||||
meetingCacheService.clearAll();
|
||||
const nextPage = page > 1 && meetings.length === 1 ? page - 1 : page;
|
||||
setPage(nextPage);
|
||||
loadMeetings(nextPage, keyword, filterType);
|
||||
loadMeetings(nextPage, keyword, filterType, { forceRefresh: true });
|
||||
} catch (error) {
|
||||
message.error(error?.response?.data?.message || '删除失败');
|
||||
}
|
||||
|
|
@ -152,151 +181,95 @@ const MeetingCenterPage = ({ user }) => {
|
|||
});
|
||||
};
|
||||
|
||||
const footerText = useMemo(() => {
|
||||
if (!total) return '当前没有会议';
|
||||
return `为您找到 ${total} 场会议`;
|
||||
}, [total]);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
minHeight: 'calc(100vh - 128px)',
|
||||
padding: '10px 0 28px',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
background: '#f3f6fa',
|
||||
borderRadius: 28,
|
||||
padding: 24,
|
||||
boxShadow: 'inset 0 1px 0 rgba(255,255,255,0.9)',
|
||||
}}
|
||||
<div className="meeting-center-page">
|
||||
<Card
|
||||
className="console-surface"
|
||||
variant="borderless"
|
||||
style={{ marginBottom: 16 }}
|
||||
styles={{ body: { padding: '18px 20px' } }}
|
||||
>
|
||||
<Card
|
||||
variant="borderless"
|
||||
style={{
|
||||
borderRadius: 20,
|
||||
marginBottom: 22,
|
||||
boxShadow: '0 8px 30px rgba(40, 72, 120, 0.08)',
|
||||
}}
|
||||
styles={{ body: { padding: '16px 20px' } }}
|
||||
>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', gap: 20, alignItems: 'center', flexWrap: 'wrap' }}>
|
||||
<Space size={14} align="center">
|
||||
<span
|
||||
style={{
|
||||
width: 6,
|
||||
height: 28,
|
||||
borderRadius: 999,
|
||||
background: 'linear-gradient(180deg, #4ea1ff 0%, #1677ff 100%)',
|
||||
display: 'inline-block',
|
||||
}}
|
||||
/>
|
||||
<Title level={3} style={{ margin: 0, fontSize: 32 }}>会议中心</Title>
|
||||
</Space>
|
||||
|
||||
<Space size={16} wrap>
|
||||
<Segmented
|
||||
className="console-segmented"
|
||||
value={filterType}
|
||||
onChange={(value) => {
|
||||
setFilterType(value);
|
||||
setPage(1);
|
||||
}}
|
||||
options={FILTER_OPTIONS}
|
||||
/>
|
||||
<Input
|
||||
value={searchValue}
|
||||
onChange={(event) => setSearchValue(event.target.value)}
|
||||
onPressEnter={() => {
|
||||
setKeyword(searchValue.trim());
|
||||
setPage(1);
|
||||
}}
|
||||
placeholder="搜索会议标题"
|
||||
prefix={<SearchOutlined style={{ color: '#9aa8b6' }} />}
|
||||
allowClear
|
||||
style={{ width: 220, borderRadius: 12 }}
|
||||
/>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => { setEditingMeetingId(null); setFormDrawerOpen(true); }}
|
||||
style={{ borderRadius: 12, height: 40, boxShadow: '0 10px 18px rgba(22, 119, 255, 0.2)' }}
|
||||
>
|
||||
新建会议
|
||||
</Button>
|
||||
</Space>
|
||||
<Space className="meeting-center-header" align="start">
|
||||
<div>
|
||||
<Title level={3} style={{ margin: 0 }}>会议中心</Title>
|
||||
<Text type="secondary">集中查看、筛选和管理全部会议记录</Text>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<div
|
||||
style={{
|
||||
minHeight: 520,
|
||||
background: '#f7fafc',
|
||||
borderRadius: 24,
|
||||
padding: 4,
|
||||
}}
|
||||
>
|
||||
{meetings.length ? (
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fill, minmax(320px, 1fr))',
|
||||
gap: 22,
|
||||
alignItems: 'start',
|
||||
<Space className="meeting-center-header-actions" wrap>
|
||||
<Segmented
|
||||
className="console-segmented"
|
||||
value={filterType}
|
||||
onChange={(value) => {
|
||||
setFilterType(value);
|
||||
setPage(1);
|
||||
}}
|
||||
options={FILTER_OPTIONS}
|
||||
/>
|
||||
<Input.Search
|
||||
className="meeting-center-search-input"
|
||||
value={searchValue}
|
||||
onChange={(event) => setSearchValue(event.target.value)}
|
||||
onSearch={(value) => {
|
||||
setSearchValue(value);
|
||||
setKeyword(value.trim());
|
||||
setPage(1);
|
||||
}}
|
||||
placeholder="搜索会议标题"
|
||||
allowClear
|
||||
/>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
className="btn-pill-primary"
|
||||
onClick={() => {
|
||||
setEditingMeetingId(null);
|
||||
setFormDrawerOpen(true);
|
||||
}}
|
||||
>
|
||||
新建会议
|
||||
</Button>
|
||||
</Space>
|
||||
</Space>
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
className="console-surface meeting-center-content-panel"
|
||||
variant="borderless"
|
||||
styles={{ body: { padding: '20px' } }}
|
||||
>
|
||||
{meetings.length ? (
|
||||
<>
|
||||
<Row gutter={[16, 16]}>
|
||||
{meetings.map((meeting) => {
|
||||
const statusMeta = getStatusMeta(meeting);
|
||||
const isCreator = String(meeting.creator_id) === String(user.user_id);
|
||||
const roleMeta = isCreator ? ROLE_META.creator : ROLE_META.attendee;
|
||||
const summaryPreview = getSummaryPreview(meeting.summary);
|
||||
const hasSummary = Boolean(summaryPreview);
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={meeting.meeting_id}
|
||||
variant="borderless"
|
||||
hoverable
|
||||
onClick={() => navigate(`/meetings/${meeting.meeting_id}`)}
|
||||
style={{
|
||||
borderRadius: 20,
|
||||
background: '#fff',
|
||||
height: 240,
|
||||
boxShadow: '0 18px 34px rgba(70, 92, 120, 0.08)',
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
styles={{ body: { padding: 0 } }}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
width: 5,
|
||||
background: statusMeta.accent,
|
||||
}}
|
||||
/>
|
||||
<div style={{ padding: '20px 20px 18px 28px', display: 'flex', flexDirection: 'column', height: 240 }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: 12, marginBottom: 14 }}>
|
||||
<Tag
|
||||
style={{
|
||||
marginInlineEnd: 0,
|
||||
color: statusMeta.tagColor,
|
||||
background: statusMeta.tagBg,
|
||||
border: `1px solid ${statusMeta.tagBorder}`,
|
||||
borderRadius: 8,
|
||||
paddingInline: 14,
|
||||
lineHeight: '24px',
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
{statusMeta.label}
|
||||
</Tag>
|
||||
{isCreator ? (
|
||||
<Space size={10}>
|
||||
<Tooltip title="编辑">
|
||||
<Button
|
||||
shape="circle"
|
||||
className="btn-icon-soft-blue"
|
||||
<Col key={meeting.meeting_id} xs={24} md={12} xl={8}>
|
||||
<Card
|
||||
variant="borderless"
|
||||
className="meeting-center-card console-surface shared-meeting-card"
|
||||
hoverable
|
||||
onClick={() => navigate(`/meetings/${meeting.meeting_id}`)}
|
||||
>
|
||||
<div className="shared-meeting-card-shell">
|
||||
<div className="shared-meeting-card-top">
|
||||
<Space size={[8, 8]} wrap>
|
||||
<Tag bordered={false} className={`${roleMeta.tagClass} console-tag-large`}>
|
||||
{roleMeta.label}
|
||||
</Tag>
|
||||
<Tag bordered={false} className={`${statusMeta.tagClass} console-tag-large`}>
|
||||
{statusMeta.label}
|
||||
</Tag>
|
||||
</Space>
|
||||
{isCreator ? (
|
||||
<Space size={8}>
|
||||
<ActionButton
|
||||
tone="edit"
|
||||
variant="iconSm"
|
||||
tooltip="编辑"
|
||||
icon={<EditOutlined />}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
|
|
@ -304,77 +277,90 @@ const MeetingCenterPage = ({ user }) => {
|
|||
setFormDrawerOpen(true);
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title="删除">
|
||||
<Button
|
||||
shape="circle"
|
||||
className="btn-icon-soft-red"
|
||||
<ActionButton
|
||||
tone="delete"
|
||||
variant="iconSm"
|
||||
tooltip="删除"
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
handleDeleteMeeting(meeting);
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Space>
|
||||
) : null}
|
||||
</div>
|
||||
</Space>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div style={{ minHeight: 78, marginBottom: 22 }}>
|
||||
<Paragraph
|
||||
style={{ margin: 0, fontSize: 30, lineHeight: 1.3, fontWeight: 700, color: '#0f172a' }}
|
||||
ellipsis={{ rows: 2, tooltip: meeting.title }}
|
||||
>
|
||||
{meeting.title}
|
||||
</Paragraph>
|
||||
</div>
|
||||
|
||||
<Space direction="vertical" size={14} style={{ color: '#6b7a90' }}>
|
||||
<Space size={10}>
|
||||
<CalendarOutlined />
|
||||
<Text style={{ color: '#6b7a90' }}>{formatMeetingTime(meeting.meeting_time || meeting.created_at)}</Text>
|
||||
</Space>
|
||||
<Space size={10} style={{ width: '100%', alignItems: 'flex-start' }}>
|
||||
<TeamOutlined />
|
||||
<div className="shared-meeting-card-title-block">
|
||||
<Paragraph
|
||||
style={{ margin: 0, color: '#6b7a90', flex: 1 }}
|
||||
ellipsis={{ rows: 1, tooltip: meeting.attendees?.length ? meeting.attendees.map((item) => item.caption).join('、') : '无参与人员' }}
|
||||
className="shared-meeting-card-title"
|
||||
ellipsis={{ rows: 2, tooltip: meeting.title }}
|
||||
>
|
||||
{meeting.attendees?.length ? `${meeting.attendees.map((item) => item.caption).join('、')}` : '无参与人员'}
|
||||
{meeting.title}
|
||||
</Paragraph>
|
||||
</Space>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: 'auto', display: 'flex', justifyContent: 'flex-end' }}>
|
||||
<Button
|
||||
type="text"
|
||||
icon={<RightOutlined />}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
navigate(`/meetings/${meeting.meeting_id}`);
|
||||
}}
|
||||
style={{ color: '#b7c0cd' }}
|
||||
/>
|
||||
<div className={`shared-meeting-card-summary ${hasSummary ? '' : 'is-empty'}`}>
|
||||
<Paragraph
|
||||
className="shared-meeting-card-summary-content"
|
||||
ellipsis={hasSummary ? { rows: 2, tooltip: summaryPreview } : false}
|
||||
>
|
||||
{hasSummary ? summaryPreview : '暂无摘要'}
|
||||
</Paragraph>
|
||||
</div>
|
||||
|
||||
<div className="shared-meeting-card-meta">
|
||||
<Space size={10}>
|
||||
<ClockCircleOutlined className="shared-meeting-card-meta-icon" />
|
||||
<Text className="shared-meeting-card-meta-text">
|
||||
{tools.formatTime(meeting.meeting_time || meeting.created_at)}
|
||||
</Text>
|
||||
</Space>
|
||||
<Divider type="vertical" />
|
||||
<Space size={10}>
|
||||
<TeamOutlined className="shared-meeting-card-meta-icon" />
|
||||
<Text className="shared-meeting-card-meta-text">
|
||||
{meeting.attendees?.length || 0} 人
|
||||
</Text>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<div className="shared-meeting-card-footer">
|
||||
<Space>
|
||||
<Avatar size="small" src={meeting.creator_avatar_url} icon={<UserOutlined />} />
|
||||
<Text type="secondary" className="shared-meeting-card-footer-user">
|
||||
{meeting.creator_username || '未知'}
|
||||
</Text>
|
||||
</Space>
|
||||
<ActionButton
|
||||
tone="view"
|
||||
variant="textSm"
|
||||
icon={<ArrowRightOutlined />}
|
||||
className="shared-meeting-card-footer-link"
|
||||
>
|
||||
查看详情
|
||||
</ActionButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</Card>
|
||||
</Col>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ minHeight: 460, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<Empty description={loading ? '加载中...' : '暂无会议'} />
|
||||
</div>
|
||||
)}
|
||||
</Row>
|
||||
|
||||
<CenterPager
|
||||
current={page}
|
||||
total={total}
|
||||
pageSize={pageSize}
|
||||
onChange={setPage}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<CenterPager
|
||||
current={page}
|
||||
total={total}
|
||||
pageSize={pageSize}
|
||||
onChange={setPage}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<div className="meeting-center-empty">
|
||||
<Empty description={loading ? '加载中...' : '暂无会议'} />
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
<MeetingFormDrawer
|
||||
open={formDrawerOpen}
|
||||
|
|
@ -382,10 +368,11 @@ const MeetingCenterPage = ({ user }) => {
|
|||
meetingId={editingMeetingId}
|
||||
user={user}
|
||||
onSuccess={(newMeetingId) => {
|
||||
meetingCacheService.clearAll();
|
||||
if (newMeetingId) {
|
||||
navigate(`/meetings/${newMeetingId}`);
|
||||
} else {
|
||||
loadMeetings(page, keyword, filterType);
|
||||
loadMeetings(page, keyword, filterType, { forceRefresh: true });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -87,8 +87,8 @@
|
|||
|
||||
.error-retry-btn:hover {
|
||||
background: linear-gradient(135deg, #1d4ed8 0%, #1e40af 100%);
|
||||
box-shadow: 0 4px 12px rgba(37, 99, 235, 0.4);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 3px 10px rgba(37, 99, 235, 0.32);
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.error-retry-btn:active {
|
||||
|
|
@ -100,7 +100,7 @@
|
|||
max-width: 800px;
|
||||
width: 100%;
|
||||
background: white;
|
||||
padding: 40px;
|
||||
padding: 32px 32px 36px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
||||
margin: 20px auto;
|
||||
|
|
@ -113,26 +113,33 @@
|
|||
|
||||
.preview-title {
|
||||
color: #2563eb;
|
||||
font-size: 28px;
|
||||
margin-bottom: 30px;
|
||||
font-size: 24px;
|
||||
margin-bottom: 22px;
|
||||
border-bottom: 2px solid #e5e7eb;
|
||||
padding-bottom: 15px;
|
||||
padding-bottom: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.meeting-info-section {
|
||||
background: #f9fafb;
|
||||
padding: 25px;
|
||||
margin-bottom: 35px;
|
||||
border-radius: 8px;
|
||||
padding: 18px 20px;
|
||||
margin-bottom: 24px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
color: #374151;
|
||||
font-size: 20px;
|
||||
margin: 0 0 16px;
|
||||
padding-bottom: 10px;
|
||||
font-size: 16px;
|
||||
line-height: 1.2;
|
||||
margin: 0 0 10px;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* 操作按钮行 */
|
||||
|
|
@ -169,8 +176,8 @@
|
|||
|
||||
.copy-btn:hover {
|
||||
background: linear-gradient(135deg, #5a67d8, #6b46c1);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.35);
|
||||
transform: none;
|
||||
box-shadow: 0 3px 10px rgba(102, 126, 234, 0.28);
|
||||
}
|
||||
|
||||
.copy-btn:active {
|
||||
|
|
@ -187,8 +194,8 @@
|
|||
|
||||
.share-btn:hover {
|
||||
background: linear-gradient(135deg, #059669, #047857);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(16, 185, 129, 0.35);
|
||||
transform: none;
|
||||
box-shadow: 0 3px 10px rgba(16, 185, 129, 0.28);
|
||||
}
|
||||
|
||||
.share-btn:active {
|
||||
|
|
@ -197,8 +204,9 @@
|
|||
}
|
||||
|
||||
.info-item {
|
||||
margin: 12px 0;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
font-size: 15px;
|
||||
line-height: 1.6;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
|
|
@ -297,21 +305,27 @@
|
|||
}
|
||||
|
||||
.preview-content {
|
||||
padding: 20px 15px;
|
||||
padding: 18px 14px 24px;
|
||||
margin: 10px auto;
|
||||
}
|
||||
|
||||
.preview-title {
|
||||
font-size: 20px;
|
||||
margin-bottom: 20px;
|
||||
font-size: 16px;
|
||||
margin-bottom: 14px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 18px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
font-size: 14px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.meeting-info-section {
|
||||
padding: 14px 14px 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
/* Tab 移动端优化 */
|
||||
|
|
@ -359,7 +373,7 @@
|
|||
}
|
||||
|
||||
.preview-title {
|
||||
font-size: 24px;
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
/* 思维导图平板优化 */
|
||||
|
|
@ -557,8 +571,8 @@
|
|||
|
||||
.password-verify-btn:hover:not(:disabled) {
|
||||
background: linear-gradient(135deg, #d97706 0%, #b45309 100%);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(245, 158, 11, 0.4);
|
||||
transform: none;
|
||||
box-shadow: 0 3px 10px rgba(245, 158, 11, 0.32);
|
||||
}
|
||||
|
||||
.password-verify-btn:active:not(:disabled) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { useParams, Link } from 'react-router-dom';
|
||||
import { Layout, Space, Button, App, Tag, Empty, Input, Tabs } from 'antd';
|
||||
import { Layout, Space, Button, App, 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';
|
||||
|
|
@ -106,7 +106,9 @@ const MeetingPreview = () => {
|
|||
try {
|
||||
await navigator.share({ title: meeting?.title || branding.preview_title, url: window.location.href });
|
||||
return;
|
||||
} catch {}
|
||||
} catch {
|
||||
// Fallback to copying the link when native share is cancelled or unavailable.
|
||||
}
|
||||
}
|
||||
await handleCopyLink();
|
||||
};
|
||||
|
|
@ -210,19 +212,12 @@ const MeetingPreview = () => {
|
|||
|
||||
<div className="meeting-info-section">
|
||||
<h2 className="section-title">会议信息</h2>
|
||||
<div className="info-item"><strong>创建人:</strong>{meeting.creator_username || '未知'}</div>
|
||||
<div className="info-item"><strong>会议时间:</strong>{tools.formatDateTime(meeting.meeting_time)}</div>
|
||||
<div className="info-item"><strong>参会人员:</strong>{meeting.attendees?.length ? meeting.attendees.map((item) => item.caption).join('、') : '未设置'}</div>
|
||||
<div className="info-item"><strong>计算人数:</strong>{meeting.attendees_count || meeting.attendees?.length || 0}人</div>
|
||||
<div className="info-item"><strong>总结模板:</strong>{meeting.prompt_name || '默认模板'}</div>
|
||||
{meeting.tags?.length ? (
|
||||
<div className="info-item">
|
||||
<strong>标签:</strong>
|
||||
<Space wrap style={{ marginLeft: 8 }}>
|
||||
{meeting.tags.map((tag) => <Tag key={tag.id || tag.name} color="blue">{tag.name}</Tag>)}
|
||||
</Space>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="info-grid">
|
||||
<div className="info-item"><strong>创建人:</strong>{meeting.creator_username || '未知'}</div>
|
||||
<div className="info-item"><strong>会议时间:</strong>{tools.formatDateTime(meeting.meeting_time)}</div>
|
||||
<div className="info-item"><strong>参会人员:</strong>{meeting.attendees?.length ? meeting.attendees.map((item) => item.caption).join('、') : '未设置'}</div>
|
||||
<div className="info-item"><strong>计算人数:</strong>{meeting.attendees_count || meeting.attendees?.length || 0}人</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="action-buttons">
|
||||
|
|
@ -245,12 +240,26 @@ const MeetingPreview = () => {
|
|||
label: (
|
||||
<Space size={8}>
|
||||
<FileTextOutlined />
|
||||
<span>会议总结</span>
|
||||
<span>总结</span>
|
||||
</Space>
|
||||
),
|
||||
children: (
|
||||
<div className="summary-content">
|
||||
{meeting.summary ? <MarkdownRenderer content={meeting.summary} /> : <Empty description="暂无会议总结内容" />}
|
||||
{meeting.summary ? <MarkdownRenderer content={meeting.summary} /> : <Empty description="暂无总结内容" />}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'mindmap',
|
||||
label: (
|
||||
<Space size={8}>
|
||||
<PartitionOutlined />
|
||||
<span>思维导图</span>
|
||||
</Space>
|
||||
),
|
||||
children: (
|
||||
<div className="mindmap-wrapper">
|
||||
<MindMap content={meeting.summary} title={meeting.title} />
|
||||
</div>
|
||||
),
|
||||
},
|
||||
|
|
@ -300,21 +309,7 @@ const MeetingPreview = () => {
|
|||
)}
|
||||
</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>
|
||||
|
|
|
|||
|
|
@ -26,12 +26,13 @@ import apiClient from '../utils/apiClient';
|
|||
import { API_ENDPOINTS, buildApiUrl } from '../config/api';
|
||||
import PromptManagementPage from './PromptManagementPage';
|
||||
import MarkdownRenderer from '../components/MarkdownRenderer';
|
||||
import ActionButton from '../components/ActionButton';
|
||||
|
||||
const { Title, Text, Paragraph } = Typography;
|
||||
|
||||
const TASK_TYPES = [
|
||||
{ label: '会议总结', value: 'MEETING_TASK' },
|
||||
{ label: '知识库整合', value: 'KNOWLEDGE_TASK' },
|
||||
{ label: '总结模版', value: 'MEETING_TASK' },
|
||||
{ label: '知识库模版', value: 'KNOWLEDGE_TASK' },
|
||||
];
|
||||
|
||||
const TASK_TYPE_OPTIONS = TASK_TYPES.map((item) => ({ label: item.label, value: item.value }));
|
||||
|
|
@ -167,12 +168,7 @@ const PromptConfigPage = ({ user }) => {
|
|||
<Space>
|
||||
{isSystem ? <Tag color="blue">系统</Tag> : <Tag>个人</Tag>}
|
||||
{isSystem && item.is_default ? <Tag color="gold" icon={<StarFilled />}>默认</Tag> : null}
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<EyeOutlined />}
|
||||
onClick={() => setViewingPrompt(item)}
|
||||
/>
|
||||
<ActionButton tone="view" variant="iconSm" tooltip="查看模板" icon={<EyeOutlined />} onClick={() => setViewingPrompt(item)} />
|
||||
</Space>
|
||||
</Space>
|
||||
{item.desc && (
|
||||
|
|
|
|||
|
|
@ -31,12 +31,13 @@ import { buildApiUrl, API_ENDPOINTS } from '../config/api';
|
|||
import MarkdownEditor from '../components/MarkdownEditor';
|
||||
import CenterPager from '../components/CenterPager';
|
||||
import StatusTag from '../components/StatusTag';
|
||||
import ActionButton from '../components/ActionButton';
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
|
||||
const TASK_TYPES = [
|
||||
{ label: '会议总结', value: 'MEETING_TASK' },
|
||||
{ label: '知识库整合', value: 'KNOWLEDGE_TASK' },
|
||||
{ label: '总结模版', value: 'MEETING_TASK' },
|
||||
{ label: '知识库模版', value: 'KNOWLEDGE_TASK' },
|
||||
];
|
||||
|
||||
const TASK_TYPE_OPTIONS = TASK_TYPES.map((item) => ({ label: item.label, value: item.value }));
|
||||
|
|
@ -194,22 +195,8 @@ const PromptManagementPage = ({ user, mode = 'default', embedded = false }) => {
|
|||
</div>
|
||||
<Space size={8} align="start">
|
||||
<StatusTag active={!isDisabled} />
|
||||
<Tooltip title="编辑">
|
||||
<Button
|
||||
size="small"
|
||||
className="btn-icon-soft-blue"
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => openEditDrawer(prompt)}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title="删除">
|
||||
<Button
|
||||
size="small"
|
||||
className="btn-icon-soft-red"
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={() => removePrompt(prompt)}
|
||||
/>
|
||||
</Tooltip>
|
||||
<ActionButton tone="edit" variant="iconSm" tooltip="编辑" icon={<EditOutlined />} onClick={() => openEditDrawer(prompt)} />
|
||||
<ActionButton tone="delete" variant="iconSm" tooltip="删除" icon={<DeleteOutlined />} onClick={() => removePrompt(prompt)} />
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ import {
|
|||
} from '@ant-design/icons';
|
||||
import apiClient from '../../utils/apiClient';
|
||||
import { buildApiUrl, API_ENDPOINTS } from '../../config/api';
|
||||
|
||||
const { Option } = Select;
|
||||
const { TextArea } = Input;
|
||||
const { Title, Text } = Typography;
|
||||
|
|
@ -285,7 +284,7 @@ const DictManagement = () => {
|
|||
okText="确定"
|
||||
cancelText="取消"
|
||||
>
|
||||
<Button danger size="small" icon={<DeleteOutlined />}>删除</Button>
|
||||
<Button size="small" icon={<DeleteOutlined />} className="btn-soft-red">删除</Button>
|
||||
</Popconfirm>
|
||||
)}
|
||||
<Button size="small" icon={<CloseOutlined />} onClick={handleCancel}>取消</Button>
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ import {
|
|||
import apiClient from '../../utils/apiClient';
|
||||
import { buildApiUrl, API_ENDPOINTS } from '../../config/api';
|
||||
import AdminModuleShell from '../../components/AdminModuleShell';
|
||||
import ActionButton from '../../components/ActionButton';
|
||||
|
||||
const { Text } = Typography;
|
||||
const { TextArea } = Input;
|
||||
|
|
@ -330,23 +331,10 @@ const ExternalAppManagement = () => {
|
|||
fixed: 'right',
|
||||
width: 140,
|
||||
render: (_, record) => (
|
||||
<Space size="middle">
|
||||
<Tooltip title="打开入口">
|
||||
<Button
|
||||
type="text"
|
||||
className="btn-text-view"
|
||||
icon={<ExportOutlined />}
|
||||
href={getAppEntryUrl(record)}
|
||||
target="_blank"
|
||||
disabled={!getAppEntryUrl(record)}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title="编辑">
|
||||
<Button type="text" className="btn-text-edit" icon={<EditOutlined />} onClick={() => handleOpenModal(record)} />
|
||||
</Tooltip>
|
||||
<Tooltip title="删除">
|
||||
<Button type="text" danger className="btn-text-delete" icon={<DeleteOutlined />} onClick={() => handleDelete(record)} />
|
||||
</Tooltip>
|
||||
<Space size={6}>
|
||||
<ActionButton tone="view" variant="iconSm" tooltip="打开入口" icon={<ExportOutlined />} href={getAppEntryUrl(record)} target="_blank" disabled={!getAppEntryUrl(record)} />
|
||||
<ActionButton tone="edit" variant="iconSm" tooltip="编辑" icon={<EditOutlined />} onClick={() => handleOpenModal(record)} />
|
||||
<ActionButton tone="delete" variant="iconSm" tooltip="删除" icon={<DeleteOutlined />} onClick={() => handleDelete(record)} />
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
|
|
|
|||
|
|
@ -6,12 +6,13 @@ import {
|
|||
import {
|
||||
PlusOutlined, SyncOutlined, DeleteOutlined, EditOutlined,
|
||||
FontSizeOutlined, SearchOutlined, ReloadOutlined,
|
||||
CheckCircleOutlined, ExclamationCircleOutlined, SaveOutlined,
|
||||
SaveOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import apiClient from '../../utils/apiClient';
|
||||
import { buildApiUrl, API_ENDPOINTS } from '../../config/api';
|
||||
import configService from '../../utils/configService';
|
||||
import AdminModuleShell from '../../components/AdminModuleShell';
|
||||
import ActionButton from '../../components/ActionButton';
|
||||
import StatusTag from '../../components/StatusTag';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
|
@ -286,13 +287,9 @@ const HotWordManagement = () => {
|
|||
{
|
||||
title: '操作', key: 'action', fixed: 'right', width: 100,
|
||||
render: (_, record) => (
|
||||
<Space size="middle">
|
||||
<Tooltip title="编辑">
|
||||
<Button type="text" className="btn-text-edit" icon={<EditOutlined />} onClick={() => openEditItem(record)} />
|
||||
</Tooltip>
|
||||
<Tooltip title="删除">
|
||||
<Button type="text" danger className="btn-text-delete" icon={<DeleteOutlined />} onClick={() => deleteItem(record)} />
|
||||
</Tooltip>
|
||||
<Space size={6}>
|
||||
<ActionButton tone="edit" variant="iconSm" tooltip="编辑" icon={<EditOutlined />} onClick={() => openEditItem(record)} />
|
||||
<ActionButton tone="delete" variant="iconSm" tooltip="删除" icon={<DeleteOutlined />} onClick={() => deleteItem(record)} />
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
|
|
@ -300,17 +297,13 @@ const HotWordManagement = () => {
|
|||
|
||||
// ── Sync status rendering ──
|
||||
const renderSyncStatus = (group) => {
|
||||
if (group.vocabulary_id) {
|
||||
return (
|
||||
<Tag icon={<CheckCircleOutlined />} color="success" style={{ marginTop: 4 }}>
|
||||
已同步
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Tag icon={<ExclamationCircleOutlined />} color="warning" style={{ marginTop: 4 }}>
|
||||
未同步
|
||||
</Tag>
|
||||
<StatusTag
|
||||
active={Boolean(group.vocabulary_id)}
|
||||
activeText="已同步"
|
||||
inactiveText="未同步"
|
||||
compact
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -322,7 +315,7 @@ const HotWordManagement = () => {
|
|||
<div
|
||||
key={group.id}
|
||||
onClick={() => setSelectedGroupId(group.id)}
|
||||
className={`hot-word-group-card ${isSelected ? 'hot-word-group-card-active' : ''}`}
|
||||
className={`hot-word-group-card console-surface ${isSelected ? 'hot-word-group-card-active' : ''}`}
|
||||
>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
|
|
@ -337,22 +330,10 @@ const HotWordManagement = () => {
|
|||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
<Space size={2} onClick={(e) => e.stopPropagation()}>
|
||||
<Tooltip title="同步到阿里云">
|
||||
<Button
|
||||
type="text" size="small"
|
||||
className="btn-text-view"
|
||||
icon={<SyncOutlined spin={isSyncing} />}
|
||||
loading={isSyncing}
|
||||
onClick={(e) => handleSync(group, e)}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title="编辑">
|
||||
<Button type="text" size="small" className="btn-text-edit" icon={<EditOutlined />} onClick={(e) => openEditGroup(group, e)} />
|
||||
</Tooltip>
|
||||
<Tooltip title="删除">
|
||||
<Button type="text" size="small" danger className="btn-text-delete" icon={<DeleteOutlined />} onClick={(e) => deleteGroup(group, e)} />
|
||||
</Tooltip>
|
||||
<Space size={6} onClick={(e) => e.stopPropagation()}>
|
||||
<ActionButton tone="view" variant="iconSm" tooltip="同步到阿里云" icon={<SyncOutlined spin={isSyncing} />} loading={isSyncing} onClick={(e) => handleSync(group, e)} />
|
||||
<ActionButton tone="edit" variant="iconSm" tooltip="编辑" icon={<EditOutlined />} onClick={(e) => openEditGroup(group, e)} />
|
||||
<ActionButton tone="delete" variant="iconSm" tooltip="删除" icon={<DeleteOutlined />} onClick={(e) => deleteGroup(group, e)} />
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -379,7 +360,7 @@ const HotWordManagement = () => {
|
|||
<div style={{ display: 'flex', gap: 16, minHeight: 480 }}>
|
||||
{/* ── Left: Group list ── */}
|
||||
<div style={{ width: 280, flexShrink: 0, display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||
<Button type="dashed" icon={<PlusOutlined />} block onClick={openAddGroup}>
|
||||
<Button className="btn-pill-secondary" icon={<PlusOutlined />} block onClick={openAddGroup}>
|
||||
新建热词组
|
||||
</Button>
|
||||
<Spin spinning={groupsLoading}>
|
||||
|
|
@ -398,7 +379,13 @@ const HotWordManagement = () => {
|
|||
{selectedGroup ? (
|
||||
<>
|
||||
{/* 选中组的阿里云同步详情 */}
|
||||
<Card size="small" style={{ marginBottom: 12 }} styles={{ body: { padding: '8px 16px' } }}>
|
||||
<Card
|
||||
size="small"
|
||||
variant="borderless"
|
||||
className="console-surface"
|
||||
style={{ marginBottom: 12 }}
|
||||
styles={{ body: { padding: '10px 16px' } }}
|
||||
>
|
||||
<Descriptions size="small" column={3}>
|
||||
<Descriptions.Item label="组名">{selectedGroup.name}</Descriptions.Item>
|
||||
<Descriptions.Item label="阿里云词表ID">
|
||||
|
|
@ -407,7 +394,7 @@ const HotWordManagement = () => {
|
|||
{selectedGroup.vocabulary_id}
|
||||
</Text>
|
||||
) : (
|
||||
<Text type="warning">未同步</Text>
|
||||
<StatusTag active={false} inactiveText="未同步" compact />
|
||||
)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="最后同步">
|
||||
|
|
@ -417,26 +404,33 @@ const HotWordManagement = () => {
|
|||
</Card>
|
||||
|
||||
{/* Toolbar */}
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12, flexWrap: 'wrap', gap: 8 }}>
|
||||
<Space wrap>
|
||||
<Input
|
||||
allowClear value={keyword} prefix={<SearchOutlined />}
|
||||
placeholder="搜索热词" onChange={(e) => { setKeyword(e.target.value); setPage(1); }}
|
||||
style={{ width: 220 }}
|
||||
/>
|
||||
<Select
|
||||
value={langFilter}
|
||||
onChange={(v) => { setLangFilter(v); setPage(1); }}
|
||||
options={languageOptions}
|
||||
style={{ width: 120 }}
|
||||
/>
|
||||
</Space>
|
||||
<Space>
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={openAddItem}>
|
||||
添加热词
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
<Card
|
||||
variant="borderless"
|
||||
className="console-surface"
|
||||
style={{ marginBottom: 12 }}
|
||||
styles={{ body: { padding: '12px 14px' } }}
|
||||
>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: 8 }}>
|
||||
<Space wrap>
|
||||
<Input
|
||||
allowClear value={keyword} prefix={<SearchOutlined />}
|
||||
placeholder="搜索热词" onChange={(e) => { setKeyword(e.target.value); setPage(1); }}
|
||||
style={{ width: 220 }}
|
||||
/>
|
||||
<Select
|
||||
value={langFilter}
|
||||
onChange={(v) => { setLangFilter(v); setPage(1); }}
|
||||
options={languageOptions}
|
||||
style={{ width: 120 }}
|
||||
/>
|
||||
</Space>
|
||||
<Space>
|
||||
<Button type="primary" className="btn-pill-primary" icon={<PlusOutlined />} onClick={openAddItem}>
|
||||
添加热词
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<div className="console-table">
|
||||
<Table
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import {
|
|||
Table,
|
||||
Tag,
|
||||
Tabs,
|
||||
Tooltip,
|
||||
} from 'antd';
|
||||
import { DeleteOutlined, EditOutlined, ExperimentOutlined, PlusOutlined, ReloadOutlined, RobotOutlined, SaveOutlined } from '@ant-design/icons';
|
||||
import apiClient from '../../utils/apiClient';
|
||||
|
|
@ -29,6 +30,7 @@ import {
|
|||
getProviderSelectOptions,
|
||||
} from '../../config/modelProviderCatalog';
|
||||
import AdminModuleShell from '../../components/AdminModuleShell';
|
||||
import ActionButton from '../../components/ActionButton';
|
||||
import StatusTag from '../../components/StatusTag';
|
||||
|
||||
const AUDIO_SCENE_OPTIONS = [
|
||||
|
|
@ -382,12 +384,12 @@ const ModelManagement = () => {
|
|||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 150,
|
||||
width: 90,
|
||||
render: (_, row) => (
|
||||
<Space size={4}>
|
||||
<Button type="link" className="btn-text-edit" icon={<EditOutlined />} onClick={() => openEdit(row)}>编辑</Button>
|
||||
<Space size={6}>
|
||||
<ActionButton tone="edit" variant="iconSm" tooltip="编辑" icon={<EditOutlined />} onClick={() => openEdit(row)} />
|
||||
<Popconfirm title="确认删除该LLM模型吗?" onConfirm={() => removeModel('llm', row.model_code)}>
|
||||
<Button type="link" danger className="btn-text-delete" icon={<DeleteOutlined />}>删除</Button>
|
||||
<ActionButton tone="delete" variant="iconSm" tooltip="删除" icon={<DeleteOutlined />} />
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
),
|
||||
|
|
@ -425,12 +427,12 @@ const ModelManagement = () => {
|
|||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 150,
|
||||
width: 90,
|
||||
render: (_, row) => (
|
||||
<Space size={4}>
|
||||
<Button type="link" className="btn-text-edit" icon={<EditOutlined />} onClick={() => openEdit(row)}>编辑</Button>
|
||||
<Space size={6}>
|
||||
<ActionButton tone="edit" variant="iconSm" tooltip="编辑" icon={<EditOutlined />} onClick={() => openEdit(row)} />
|
||||
<Popconfirm title="确认删除该音频模型吗?" onConfirm={() => removeModel('audio', row.model_code)}>
|
||||
<Button type="link" danger className="btn-text-delete" icon={<DeleteOutlined />}>删除</Button>
|
||||
<ActionButton tone="delete" variant="iconSm" tooltip="删除" icon={<DeleteOutlined />} />
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { App, Button, Drawer, Form, Input, Popconfirm, Select, Space, Switch, Table, Tag } from 'antd';
|
||||
import { App, Button, Drawer, Form, Input, Popconfirm, Select, Space, Switch, Table, Tag, Tooltip } from 'antd';
|
||||
import { DeleteOutlined, EditOutlined, PlusOutlined, ReloadOutlined, SaveOutlined, SettingOutlined } from '@ant-design/icons';
|
||||
import apiClient from '../../utils/apiClient';
|
||||
import { API_ENDPOINTS, buildApiUrl } from '../../config/api';
|
||||
import AdminModuleShell from '../../components/AdminModuleShell';
|
||||
import ActionButton from '../../components/ActionButton';
|
||||
import StatusTag from '../../components/StatusTag';
|
||||
|
||||
const ParameterManagement = () => {
|
||||
|
|
@ -90,10 +91,10 @@ const ParameterManagement = () => {
|
|||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 180,
|
||||
width: 90,
|
||||
render: (_, row) => (
|
||||
<Space>
|
||||
<Button type="link" icon={<EditOutlined />} onClick={() => openEdit(row)}>编辑</Button>
|
||||
<Space size={6}>
|
||||
<ActionButton tone="edit" variant="iconSm" tooltip="编辑" icon={<EditOutlined />} onClick={() => openEdit(row)} />
|
||||
<Popconfirm
|
||||
title="确认删除参数?"
|
||||
description={`参数键:${row.param_key}`}
|
||||
|
|
@ -110,7 +111,7 @@ const ParameterManagement = () => {
|
|||
}
|
||||
}}
|
||||
>
|
||||
<Button type="link" danger icon={<DeleteOutlined />}>删除</Button>
|
||||
<ActionButton tone="delete" variant="iconSm" tooltip="删除" icon={<DeleteOutlined />} />
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
),
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ import {
|
|||
ColumnHeightOutlined,
|
||||
CompressOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import ActionButton from '../../components/ActionButton';
|
||||
import apiClient from '../../utils/apiClient';
|
||||
import { buildApiUrl } from '../../config/api';
|
||||
import AdminModuleShell from '../../components/AdminModuleShell';
|
||||
|
|
@ -255,44 +256,6 @@ const PermissionManagement = () => {
|
|||
fetchRoleUsers();
|
||||
}, [selectedRoleId, roleUsersPage, roleUsersPageSize]);
|
||||
|
||||
const toggleSelectedRolePermission = (menuId) => {
|
||||
if (!selectedRoleId) return;
|
||||
|
||||
setPermissionsByRole((prev) => {
|
||||
const rolePerms = new Set(prev[selectedRoleId] || []);
|
||||
const hasPermission = rolePerms.has(menuId);
|
||||
const descendants = getDescendants(menuId);
|
||||
|
||||
if (!hasPermission) {
|
||||
rolePerms.add(menuId);
|
||||
descendants.forEach((descId) => rolePerms.add(descId));
|
||||
|
||||
let cursor = menuRelation.parentById[menuId];
|
||||
while (cursor) {
|
||||
rolePerms.add(cursor);
|
||||
cursor = menuRelation.parentById[cursor];
|
||||
}
|
||||
} else {
|
||||
rolePerms.delete(menuId);
|
||||
descendants.forEach((descId) => rolePerms.delete(descId));
|
||||
|
||||
let cursor = menuRelation.parentById[menuId];
|
||||
while (cursor) {
|
||||
const childIds = menuRelation.childrenById[cursor] || [];
|
||||
const stillChecked = childIds.some((childId) => rolePerms.has(childId));
|
||||
if (!stillChecked) {
|
||||
rolePerms.delete(cursor);
|
||||
cursor = menuRelation.parentById[cursor];
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { ...prev, [selectedRoleId]: Array.from(rolePerms) };
|
||||
});
|
||||
};
|
||||
|
||||
const saveSelectedRolePermissions = async () => {
|
||||
if (!selectedRoleId) return;
|
||||
|
||||
|
|
@ -708,16 +671,10 @@ const PermissionManagement = () => {
|
|||
<TeamOutlined style={{ color: '#1d4ed8' }} />
|
||||
<Text strong>{role.role_name}</Text>
|
||||
</Space>
|
||||
<Button
|
||||
size="small"
|
||||
type="text"
|
||||
className="btn-text-edit"
|
||||
icon={<EditOutlined />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
openEditRoleDrawer(role);
|
||||
}}
|
||||
/>
|
||||
<ActionButton tone="edit" variant="iconSm" tooltip="编辑角色" icon={<EditOutlined />} onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
openEditRoleDrawer(role);
|
||||
}} />
|
||||
</div>
|
||||
<div style={{ marginTop: 6 }}>
|
||||
<Text type="secondary">权限数 {role.menu_count || 0}</Text>
|
||||
|
|
@ -928,9 +885,11 @@ const PermissionManagement = () => {
|
|||
}
|
||||
extra={selectedManageMenu ? (
|
||||
<Space>
|
||||
<Button icon={<PlusOutlined />} onClick={() => openCreateMenuPanel(selectedManageMenu.menu_id)}>新增子菜单</Button>
|
||||
<Button icon={<EditOutlined />} className="btn-soft-blue" onClick={() => openEditMenuPanel(selectedManageMenu)}>编辑</Button>
|
||||
<Button danger icon={<DeleteOutlined />} onClick={() => confirmDeleteMenu(selectedManageMenu)}>删除</Button>
|
||||
<ActionButton tone="neutral" variant="textLg" icon={<PlusOutlined />} onClick={() => openCreateMenuPanel(selectedManageMenu.menu_id)}>
|
||||
新增子菜单
|
||||
</ActionButton>
|
||||
<ActionButton tone="edit" variant="textLg" icon={<EditOutlined />} onClick={() => openEditMenuPanel(selectedManageMenu)}>编辑</ActionButton>
|
||||
<ActionButton tone="delete" variant="textLg" icon={<DeleteOutlined />} onClick={() => confirmDeleteMenu(selectedManageMenu)}>删除</ActionButton>
|
||||
</Space>
|
||||
) : (
|
||||
<Button icon={<PlusOutlined />} type="primary" onClick={() => openCreateMenuPanel(null)}>新增一级菜单</Button>
|
||||
|
|
@ -1055,9 +1014,7 @@ const PermissionManagement = () => {
|
|||
|
||||
<Space>
|
||||
{menuPanelMode === 'view' ? (
|
||||
<Button type="primary" icon={<EditOutlined />} onClick={() => selectedManageMenu && openEditMenuPanel(selectedManageMenu)}>
|
||||
编辑当前菜单
|
||||
</Button>
|
||||
<ActionButton tone="edit" variant="textLg" icon={<EditOutlined />} onClick={() => selectedManageMenu && openEditMenuPanel(selectedManageMenu)}>编辑当前菜单</ActionButton>
|
||||
) : (
|
||||
<>
|
||||
<Button type="primary" loading={menuSubmitting} icon={<SaveOutlined />} onClick={submitMenu}>
|
||||
|
|
|
|||
|
|
@ -68,8 +68,8 @@
|
|||
|
||||
.btn-new-prompt:hover {
|
||||
background: #5568d3;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
|
||||
transform: none;
|
||||
box-shadow: 0 3px 10px rgba(102, 126, 234, 0.24);
|
||||
}
|
||||
|
||||
.btn-toggle-sidebar {
|
||||
|
|
@ -130,7 +130,8 @@
|
|||
.prompt-list-item:hover {
|
||||
background: #f8fafc;
|
||||
border-color: #cbd5e1;
|
||||
transform: translateX(4px);
|
||||
transform: none;
|
||||
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.08);
|
||||
}
|
||||
|
||||
.prompt-list-item.active {
|
||||
|
|
@ -277,8 +278,8 @@
|
|||
|
||||
.icon-btn-small.confirm-btn:hover:not(:disabled) {
|
||||
background: #059669;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(16, 185, 129, 0.3);
|
||||
transform: none;
|
||||
box-shadow: 0 3px 10px rgba(16, 185, 129, 0.24);
|
||||
}
|
||||
|
||||
.icon-btn-small.cancel-btn {
|
||||
|
|
@ -335,8 +336,8 @@
|
|||
|
||||
.icon-btn.save-btn:hover:not(:disabled) {
|
||||
background: #5568d3;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
|
||||
transform: none;
|
||||
box-shadow: 0 3px 10px rgba(102, 126, 234, 0.24);
|
||||
}
|
||||
|
||||
.icon-btn.delete-btn {
|
||||
|
|
@ -611,8 +612,8 @@
|
|||
|
||||
.btn-primary:hover:not(:disabled) {
|
||||
background-color: #5568d3;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
|
||||
transform: none;
|
||||
box-shadow: 0 3px 10px rgba(102, 126, 234, 0.24);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import {
|
|||
} from '@ant-design/icons';
|
||||
import apiClient from '../../utils/apiClient';
|
||||
import { buildApiUrl, API_ENDPOINTS } from '../../config/api';
|
||||
import ActionButton from '../../components/ActionButton';
|
||||
import MarkdownEditor from '../../components/MarkdownEditor';
|
||||
import StatusTag from '../../components/StatusTag';
|
||||
|
||||
|
|
@ -29,8 +30,8 @@ const { Title, Text, Paragraph } = Typography;
|
|||
const { TextArea } = Input;
|
||||
|
||||
const TASK_TYPES = {
|
||||
MEETING_TASK: { label: '会议总结', icon: <MessageOutlined /> },
|
||||
KNOWLEDGE_TASK: { label: '知识库整合', icon: <DatabaseOutlined /> }
|
||||
MEETING_TASK: { label: '总结模版', icon: <MessageOutlined /> },
|
||||
KNOWLEDGE_TASK: { label: '知识库模版', icon: <DatabaseOutlined /> }
|
||||
};
|
||||
|
||||
const PromptManagement = () => {
|
||||
|
|
@ -174,13 +175,13 @@ const PromptManagement = () => {
|
|||
<div style={{ flex: 1 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 8 }}>
|
||||
<Title level={3} style={{ margin: 0 }}>{selectedPrompt.name}</Title>
|
||||
<Button type="text" icon={<EditOutlined />} onClick={() => {}} />
|
||||
<ActionButton tone="edit" variant="iconSm" tooltip="编辑" icon={<EditOutlined />} onClick={() => {}} />
|
||||
</div>
|
||||
<Paragraph type="secondary">{selectedPrompt.desc || '暂无描述'}</Paragraph>
|
||||
</div>
|
||||
<Space>
|
||||
<Button icon={<DeleteOutlined />} className="btn-soft-red" onClick={() => handleDelete(selectedPrompt)}>删除</Button>
|
||||
<Button icon={<SaveOutlined />} type="primary" loading={isSaving} onClick={handleSave}>保存更改</Button>
|
||||
<Button icon={<DeleteOutlined />} danger onClick={() => handleDelete(selectedPrompt)}>删除</Button>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
|
|
@ -229,8 +230,8 @@ const PromptManagement = () => {
|
|||
</Form.Item>
|
||||
<Form.Item name="task_type" label="任务类型" rules={[{ required: true }]} initialValue="MEETING_TASK">
|
||||
<Select>
|
||||
<Select.Option value="MEETING_TASK">会议总结</Select.Option>
|
||||
<Select.Option value="KNOWLEDGE_TASK">知识库整合</Select.Option>
|
||||
<Select.Option value="MEETING_TASK">总结模版</Select.Option>
|
||||
<Select.Option value="KNOWLEDGE_TASK">知识库模版</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item name="desc" label="模版描述">
|
||||
|
|
|
|||
|
|
@ -162,8 +162,8 @@
|
|||
}
|
||||
|
||||
.config-btn-primary:hover:not(:disabled) {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 8px rgba(59, 130, 246, 0.3);
|
||||
transform: none;
|
||||
box-shadow: 0 3px 8px rgba(59, 130, 246, 0.24);
|
||||
}
|
||||
|
||||
.config-btn-primary:disabled {
|
||||
|
|
@ -247,4 +247,4 @@
|
|||
.config-btn {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ import {
|
|||
import apiClient from '../../utils/apiClient';
|
||||
import { buildApiUrl, API_ENDPOINTS } from '../../config/api';
|
||||
import AdminModuleShell from '../../components/AdminModuleShell';
|
||||
import ActionButton from '../../components/ActionButton';
|
||||
|
||||
const { Text } = Typography;
|
||||
const { TextArea } = Input;
|
||||
|
|
@ -299,13 +300,9 @@ const TerminalManagement = () => {
|
|||
fixed: 'right',
|
||||
width: 100,
|
||||
render: (_, record) => (
|
||||
<Space size="middle">
|
||||
<Tooltip title="编辑">
|
||||
<Button type="text" className="btn-text-edit" icon={<EditOutlined />} onClick={() => handleOpenModal(record)} />
|
||||
</Tooltip>
|
||||
<Tooltip title="删除">
|
||||
<Button type="text" danger className="btn-text-delete" icon={<DeleteOutlined />} onClick={() => handleDelete(record)} />
|
||||
</Tooltip>
|
||||
<Space size={6}>
|
||||
<ActionButton tone="edit" variant="iconSm" tooltip="编辑" icon={<EditOutlined />} onClick={() => handleOpenModal(record)} />
|
||||
<ActionButton tone="delete" variant="iconSm" tooltip="删除" icon={<DeleteOutlined />} onClick={() => handleDelete(record)} />
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import apiClient from '../../utils/apiClient';
|
|||
import { buildApiUrl, API_ENDPOINTS } from '../../config/api';
|
||||
import configService from '../../utils/configService';
|
||||
import AdminModuleShell from '../../components/AdminModuleShell';
|
||||
import ActionButton from '../../components/ActionButton';
|
||||
|
||||
const UserManagement = () => {
|
||||
const { message, modal } = App.useApp();
|
||||
|
|
@ -190,16 +191,10 @@ const UserManagement = () => {
|
|||
fixed: 'right',
|
||||
width: 150,
|
||||
render: (_, record) => (
|
||||
<Space size="middle">
|
||||
<Tooltip title="修改">
|
||||
<Button type="text" className="btn-text-edit" icon={<EditOutlined />} onClick={() => handleOpenModal(record)} />
|
||||
</Tooltip>
|
||||
<Tooltip title="重置密码">
|
||||
<Button type="text" className="btn-text-accent" icon={<KeyOutlined />} onClick={() => handleResetPassword(record)} />
|
||||
</Tooltip>
|
||||
<Tooltip title="删除">
|
||||
<Button type="text" danger className="btn-text-delete" icon={<DeleteOutlined />} onClick={() => handleDelete(record)} />
|
||||
</Tooltip>
|
||||
<Space size={6}>
|
||||
<ActionButton tone="edit" variant="iconSm" tooltip="修改" icon={<EditOutlined />} onClick={() => handleOpenModal(record)} />
|
||||
<ActionButton tone="accent" variant="iconSm" tooltip="重置密码" icon={<KeyOutlined />} onClick={() => handleResetPassword(record)} />
|
||||
<ActionButton tone="delete" variant="iconSm" tooltip="删除" icon={<DeleteOutlined />} onClick={() => handleDelete(record)} />
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
|
|
|
|||
|
|
@ -13,6 +13,9 @@
|
|||
--ime-warning: #d97706;
|
||||
--ime-danger: #c2410c;
|
||||
--ime-tab-indicator-size: 3px;
|
||||
--ime-tag-radius: 999px;
|
||||
--ime-tag-height: 26px;
|
||||
--ime-tag-padding-x: 10px;
|
||||
}
|
||||
|
||||
body {
|
||||
|
|
@ -196,14 +199,15 @@ body {
|
|||
background: rgba(29, 78, 216, 0.1) !important;
|
||||
}
|
||||
|
||||
.main-layout-header {
|
||||
.main-layout-header.ant-layout-header {
|
||||
margin: 16px 16px 0;
|
||||
padding: 0 22px;
|
||||
height: 74px;
|
||||
padding: 0 20px !important;
|
||||
height: 74px !important;
|
||||
border: 1px solid #e7eef8;
|
||||
border-radius: 18px;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 4px 10px rgba(36, 76, 128, 0.05);
|
||||
line-height: 74px;
|
||||
}
|
||||
|
||||
.main-layout-page-title {
|
||||
|
|
@ -219,6 +223,11 @@ body {
|
|||
color: #5f7392;
|
||||
}
|
||||
|
||||
.main-layout-page-path ol {
|
||||
margin: 0;
|
||||
padding-inline-start: 0;
|
||||
}
|
||||
|
||||
.main-layout-content {
|
||||
margin: 16px;
|
||||
min-height: 280px;
|
||||
|
|
@ -434,6 +443,7 @@ body {
|
|||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.meeting-audio-toolbar-player {
|
||||
|
|
@ -441,44 +451,57 @@ body {
|
|||
min-width: 0;
|
||||
}
|
||||
|
||||
.meeting-audio-toolbar-native {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
border-radius: 999px;
|
||||
background: #f5f7fb;
|
||||
}
|
||||
|
||||
.meeting-audio-toolbar-native::-webkit-media-controls-panel {
|
||||
background: #f5f7fb;
|
||||
}
|
||||
|
||||
.meeting-audio-toolbar-empty {
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 14px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #dbe7f5;
|
||||
background: linear-gradient(180deg, #fbfdff 0%, #f4f8fe 100%);
|
||||
border-radius: 999px;
|
||||
background: #f5f7fb;
|
||||
}
|
||||
|
||||
.meeting-audio-toolbar-actions {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
gap: 8px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.meeting-audio-toolbar-button.ant-btn {
|
||||
height: 32px;
|
||||
padding: 0 10px;
|
||||
border: 1px solid #dfe7f3;
|
||||
border-radius: 10px;
|
||||
background: linear-gradient(180deg, #ffffff 0%, #f6f9fd 100%);
|
||||
color: #5a7394;
|
||||
box-shadow: none;
|
||||
border: 1px solid #dbe3ef;
|
||||
border-radius: 999px;
|
||||
background: #fff;
|
||||
color: #526581;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.meeting-audio-toolbar-button.ant-btn:hover,
|
||||
.meeting-audio-toolbar-button.ant-btn:focus {
|
||||
color: #274f87;
|
||||
border-color: #d0dcf0;
|
||||
background: linear-gradient(180deg, #ffffff 0%, #f0f6ff 100%) !important;
|
||||
color: #355171;
|
||||
border-color: #c7d4e4;
|
||||
background: #f8fafc !important;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.meeting-audio-toolbar-button.ant-btn:disabled {
|
||||
color: #a1b1c4;
|
||||
border-color: #e5ebf4;
|
||||
background: linear-gradient(180deg, #fbfcfe 0%, #f6f8fb 100%) !important;
|
||||
border-color: #e3e8f0;
|
||||
background: #f8fafc !important;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
|
|
@ -486,7 +509,7 @@ body {
|
|||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
min-width: 72px;
|
||||
min-width: 68px;
|
||||
justify-content: center;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
|
@ -501,7 +524,19 @@ body {
|
|||
|
||||
.meeting-audio-toolbar-more.ant-btn {
|
||||
width: 32px;
|
||||
min-width: 32px;
|
||||
padding-inline: 0;
|
||||
padding: 0;
|
||||
border-color: #c9ddfb;
|
||||
background: linear-gradient(180deg, #f8fbff 0%, #eef5ff 100%);
|
||||
color: #1d4ed8;
|
||||
}
|
||||
|
||||
.meeting-audio-toolbar-more.ant-btn:hover,
|
||||
.meeting-audio-toolbar-more.ant-btn:focus {
|
||||
border-color: #a9c9fa;
|
||||
background: linear-gradient(180deg, #f2f8ff 0%, #e7f0ff 100%) !important;
|
||||
color: #1d4ed8;
|
||||
}
|
||||
|
||||
.meeting-audio-toolbar-more.ant-btn .anticon {
|
||||
|
|
@ -522,15 +557,15 @@ body {
|
|||
}
|
||||
|
||||
.console-segmented.ant-segmented {
|
||||
--console-segmented-gap: 3px;
|
||||
--console-segmented-radius: 15px;
|
||||
--console-segmented-thumb-radius: 13px;
|
||||
--console-segmented-gap: 2px;
|
||||
--console-segmented-radius: 10px;
|
||||
--console-segmented-thumb-radius: 9px;
|
||||
padding: var(--console-segmented-gap);
|
||||
border: 1px solid #cfdef3;
|
||||
border: 1px solid #d4e0ef;
|
||||
border-radius: var(--console-segmented-radius);
|
||||
overflow: hidden;
|
||||
background: linear-gradient(180deg, #f2f7ff 0%, #e8f1fe 100%);
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.82), 0 4px 12px rgba(61, 99, 161, 0.06);
|
||||
background: linear-gradient(180deg, #f7faff 0%, #edf3fb 100%);
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.88), 0 2px 8px rgba(61, 99, 161, 0.05);
|
||||
}
|
||||
|
||||
.console-segmented .ant-segmented-group {
|
||||
|
|
@ -538,12 +573,12 @@ body {
|
|||
}
|
||||
|
||||
.console-segmented .ant-segmented-item {
|
||||
min-height: 26px;
|
||||
line-height: 26px;
|
||||
padding: 0 12px;
|
||||
min-height: 24px;
|
||||
line-height: 24px;
|
||||
padding: 0 10px;
|
||||
border-radius: var(--console-segmented-thumb-radius);
|
||||
color: #5d7291;
|
||||
font-size: 12px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
transition: color 0.2s ease, opacity 0.2s ease;
|
||||
}
|
||||
|
|
@ -566,7 +601,167 @@ body {
|
|||
border-radius: var(--console-segmented-thumb-radius);
|
||||
background: linear-gradient(180deg, #ffffff 0%, #f8fbff 100%);
|
||||
border: 1px solid #d7e4f6;
|
||||
box-shadow: 0 1px 2px rgba(25, 63, 121, 0.08), 0 4px 10px rgba(25, 63, 121, 0.08);
|
||||
box-shadow: 0 1px 2px rgba(25, 63, 121, 0.06), 0 2px 6px rgba(25, 63, 121, 0.06);
|
||||
}
|
||||
|
||||
.ant-tag:not(.console-status-tag) {
|
||||
--ime-tag-text: #4a627f;
|
||||
--ime-tag-bg-start: #ffffff;
|
||||
--ime-tag-bg-end: #f7fbff;
|
||||
--ime-tag-border: #d8e3f2;
|
||||
--ime-tag-glow: rgba(29, 78, 216, 0.06);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
margin-inline-end: 0;
|
||||
padding: 0 var(--ime-tag-padding-x);
|
||||
min-height: var(--ime-tag-height);
|
||||
line-height: calc(var(--ime-tag-height) - 2px);
|
||||
border-radius: var(--ime-tag-radius);
|
||||
border: 1px solid var(--ime-tag-border);
|
||||
background: linear-gradient(180deg, var(--ime-tag-bg-start) 0%, var(--ime-tag-bg-end) 100%);
|
||||
color: var(--ime-tag-text);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.9), 0 4px 10px var(--ime-tag-glow);
|
||||
transition: color 0.2s ease, border-color 0.2s ease, background 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.ant-tag:not(.console-status-tag) .anticon {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.ant-tag:not(.console-status-tag) .ant-tag-close-icon {
|
||||
margin-inline-start: 2px;
|
||||
color: inherit;
|
||||
opacity: 0.55;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.ant-tag:not(.console-status-tag) .ant-tag-close-icon:hover {
|
||||
color: inherit;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.ant-tag:not(.console-status-tag).console-tag-large {
|
||||
min-height: 28px;
|
||||
padding: 0 12px;
|
||||
line-height: 26px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.ant-tag:not(.console-status-tag).console-tag-compact {
|
||||
min-height: 22px;
|
||||
padding: 0 8px;
|
||||
line-height: 20px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.ant-tag:not(.console-status-tag).console-tag-clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ant-tag:not(.console-status-tag).console-tag-soft-blue,
|
||||
.ant-tag:not(.console-status-tag).ant-tag-blue,
|
||||
.ant-tag:not(.console-status-tag).ant-tag-processing {
|
||||
--ime-tag-text: #1d4ed8;
|
||||
--ime-tag-bg-start: #f7fbff;
|
||||
--ime-tag-bg-end: #e8f1ff;
|
||||
--ime-tag-border: #bfdbfe;
|
||||
--ime-tag-glow: rgba(29, 78, 216, 0.1);
|
||||
}
|
||||
|
||||
.ant-tag:not(.console-status-tag).console-tag-soft-geekblue,
|
||||
.ant-tag:not(.console-status-tag).ant-tag-geekblue {
|
||||
--ime-tag-text: #3730a3;
|
||||
--ime-tag-bg-start: #f7f7ff;
|
||||
--ime-tag-bg-end: #ececff;
|
||||
--ime-tag-border: #c7d2fe;
|
||||
--ime-tag-glow: rgba(67, 56, 202, 0.1);
|
||||
}
|
||||
|
||||
.ant-tag:not(.console-status-tag).console-tag-soft-green,
|
||||
.ant-tag:not(.console-status-tag).ant-tag-green,
|
||||
.ant-tag:not(.console-status-tag).ant-tag-success {
|
||||
--ime-tag-text: #166534;
|
||||
--ime-tag-bg-start: #f3fff8;
|
||||
--ime-tag-bg-end: #dcfce7;
|
||||
--ime-tag-border: #bbf7d0;
|
||||
--ime-tag-glow: rgba(34, 197, 94, 0.1);
|
||||
}
|
||||
|
||||
.ant-tag:not(.console-status-tag).console-tag-soft-orange,
|
||||
.ant-tag:not(.console-status-tag).ant-tag-orange,
|
||||
.ant-tag:not(.console-status-tag).ant-tag-warning {
|
||||
--ime-tag-text: #b45309;
|
||||
--ime-tag-bg-start: #fff9f0;
|
||||
--ime-tag-bg-end: #ffedd5;
|
||||
--ime-tag-border: #fed7aa;
|
||||
--ime-tag-glow: rgba(245, 158, 11, 0.1);
|
||||
}
|
||||
|
||||
.ant-tag:not(.console-status-tag).console-tag-soft-gold,
|
||||
.ant-tag:not(.console-status-tag).ant-tag-gold {
|
||||
--ime-tag-text: #a16207;
|
||||
--ime-tag-bg-start: #fffdf3;
|
||||
--ime-tag-bg-end: #fef3c7;
|
||||
--ime-tag-border: #fde68a;
|
||||
--ime-tag-glow: rgba(234, 179, 8, 0.1);
|
||||
}
|
||||
|
||||
.ant-tag:not(.console-status-tag).console-tag-soft-red,
|
||||
.ant-tag:not(.console-status-tag).ant-tag-red,
|
||||
.ant-tag:not(.console-status-tag).ant-tag-error {
|
||||
--ime-tag-text: #b91c1c;
|
||||
--ime-tag-bg-start: #fff7f7;
|
||||
--ime-tag-bg-end: #fee2e2;
|
||||
--ime-tag-border: #fecaca;
|
||||
--ime-tag-glow: rgba(239, 68, 68, 0.1);
|
||||
}
|
||||
|
||||
.ant-tag:not(.console-status-tag).console-tag-soft-cyan,
|
||||
.ant-tag:not(.console-status-tag).ant-tag-cyan {
|
||||
--ime-tag-text: #0f766e;
|
||||
--ime-tag-bg-start: #f3fffd;
|
||||
--ime-tag-bg-end: #ccfbf1;
|
||||
--ime-tag-border: #99f6e4;
|
||||
--ime-tag-glow: rgba(20, 184, 166, 0.1);
|
||||
}
|
||||
|
||||
.ant-tag:not(.console-status-tag).console-tag-soft-purple,
|
||||
.ant-tag:not(.console-status-tag).ant-tag-purple {
|
||||
--ime-tag-text: #7c3aed;
|
||||
--ime-tag-bg-start: #faf7ff;
|
||||
--ime-tag-bg-end: #ede9fe;
|
||||
--ime-tag-border: #ddd6fe;
|
||||
--ime-tag-glow: rgba(124, 58, 237, 0.1);
|
||||
}
|
||||
|
||||
.ant-tag:not(.console-status-tag).console-tag-soft-default,
|
||||
.ant-tag:not(.console-status-tag).ant-tag-default {
|
||||
--ime-tag-text: #5d7291;
|
||||
--ime-tag-bg-start: #ffffff;
|
||||
--ime-tag-bg-end: #f6f9fd;
|
||||
--ime-tag-border: #dbe5f1;
|
||||
--ime-tag-glow: rgba(71, 85, 105, 0.06);
|
||||
}
|
||||
|
||||
.ant-tag-checkable:not(.console-status-tag):hover {
|
||||
color: #1d4ed8;
|
||||
border-color: #bfd5f6;
|
||||
background: linear-gradient(180deg, #ffffff 0%, #eff6ff 100%);
|
||||
}
|
||||
|
||||
.ant-tag-checkable-checked:not(.console-status-tag) {
|
||||
--ime-tag-text: #1d4ed8;
|
||||
--ime-tag-bg-start: #f7fbff;
|
||||
--ime-tag-bg-end: #e0efff;
|
||||
--ime-tag-border: #bfdbfe;
|
||||
--ime-tag-glow: rgba(29, 78, 216, 0.1);
|
||||
}
|
||||
|
||||
.ant-tag-checkable-checked:not(.console-status-tag):hover {
|
||||
color: #1d4ed8;
|
||||
}
|
||||
|
||||
.console-status-tag.ant-tag {
|
||||
|
|
@ -794,34 +989,61 @@ body {
|
|||
|
||||
/* ── Hot-Word Group Cards ── */
|
||||
.hot-word-group-card {
|
||||
padding: 10px 12px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--ime-line);
|
||||
padding: 14px 16px;
|
||||
border-radius: 18px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
background: var(--ime-bg-2);
|
||||
transition: border-color 0.18s ease, background 0.18s ease, box-shadow 0.18s ease;
|
||||
}
|
||||
.hot-word-group-card:hover {
|
||||
border-color: var(--ime-primary);
|
||||
background: var(--ime-primary-soft);
|
||||
border-color: #bfd4f6;
|
||||
background: rgba(248, 251, 255, 0.96);
|
||||
box-shadow: 0 10px 22px rgba(20, 58, 112, 0.1);
|
||||
}
|
||||
.hot-word-group-card-active {
|
||||
border-color: var(--ime-primary);
|
||||
background: var(--ime-primary-soft);
|
||||
box-shadow: 0 0 0 2px rgba(29, 78, 216, 0.12);
|
||||
background: linear-gradient(180deg, rgba(239, 246, 255, 0.96) 0%, rgba(255, 255, 255, 0.98) 100%);
|
||||
box-shadow: 0 0 0 2px rgba(29, 78, 216, 0.12), 0 10px 24px rgba(29, 78, 216, 0.1);
|
||||
}
|
||||
|
||||
/* ── Switch pill-shape fix (global borderRadius override) ── */
|
||||
/* ── Global Switch geometry ── */
|
||||
.ant-switch {
|
||||
border-radius: 100px !important;
|
||||
min-width: 40px;
|
||||
min-width: 42px;
|
||||
height: 24px;
|
||||
padding: 2px;
|
||||
border: 1px solid #cbdcf1;
|
||||
border-radius: 999px !important;
|
||||
background: linear-gradient(180deg, #eef4fc 0%, #dde8f7 100%) !important;
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.9), 0 2px 6px rgba(49, 86, 139, 0.08);
|
||||
}
|
||||
|
||||
.ant-switch.ant-switch-checked {
|
||||
border-color: #8db5f5;
|
||||
background: linear-gradient(180deg, #4f8cff 0%, #2f6ae6 100%) !important;
|
||||
}
|
||||
|
||||
.ant-switch-handle {
|
||||
top: 2px !important;
|
||||
width: 18px !important;
|
||||
height: 18px !important;
|
||||
border-radius: 50% !important;
|
||||
}
|
||||
|
||||
.ant-switch .ant-switch-handle::before {
|
||||
border-radius: 50% !important;
|
||||
background: linear-gradient(180deg, #ffffff 0%, #f7fbff 100%);
|
||||
box-shadow: 0 1px 3px rgba(17, 43, 78, 0.18);
|
||||
}
|
||||
|
||||
.ant-switch-inner {
|
||||
border-radius: 100px !important;
|
||||
border-radius: 999px !important;
|
||||
}
|
||||
|
||||
.ant-switch.ant-switch-small {
|
||||
min-width: 34px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.ant-switch.ant-switch-small .ant-switch-handle {
|
||||
width: 14px !important;
|
||||
height: 14px !important;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ import { buildApiUrl, API_ENDPOINTS } from '../config/api';
|
|||
export const DEFAULT_BRANDING_CONFIG = {
|
||||
app_name: '智听云平台',
|
||||
home_headline: '智听云平台',
|
||||
home_tagline: '让每一次谈话都产生价值。',
|
||||
console_subtitle: '智能会议控制台',
|
||||
home_tagline: '让每一次谈话都产生价值',
|
||||
console_subtitle: 'iMeeting控制台',
|
||||
preview_title: '会议预览',
|
||||
login_welcome: '欢迎回来,请输入您的登录凭证。',
|
||||
footer_text: '©2026 智听云平台',
|
||||
|
|
|
|||
|
|
@ -380,11 +380,136 @@
|
|||
resolved "https://registry.npmmirror.com/@emotion/unitless/-/unitless-0.7.5.tgz"
|
||||
integrity sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==
|
||||
|
||||
"@esbuild/aix-ppc64@0.27.4":
|
||||
version "0.27.4"
|
||||
resolved "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz"
|
||||
integrity sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==
|
||||
|
||||
"@esbuild/android-arm@0.27.4":
|
||||
version "0.27.4"
|
||||
resolved "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.4.tgz"
|
||||
integrity sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==
|
||||
|
||||
"@esbuild/android-arm64@0.27.4":
|
||||
version "0.27.4"
|
||||
resolved "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz"
|
||||
integrity sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==
|
||||
|
||||
"@esbuild/android-x64@0.27.4":
|
||||
version "0.27.4"
|
||||
resolved "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.4.tgz"
|
||||
integrity sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==
|
||||
|
||||
"@esbuild/darwin-arm64@0.27.4":
|
||||
version "0.27.4"
|
||||
resolved "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz"
|
||||
integrity sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==
|
||||
|
||||
"@esbuild/darwin-x64@0.27.4":
|
||||
version "0.27.4"
|
||||
resolved "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz"
|
||||
integrity sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==
|
||||
|
||||
"@esbuild/freebsd-arm64@0.27.4":
|
||||
version "0.27.4"
|
||||
resolved "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz"
|
||||
integrity sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==
|
||||
|
||||
"@esbuild/freebsd-x64@0.27.4":
|
||||
version "0.27.4"
|
||||
resolved "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz"
|
||||
integrity sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==
|
||||
|
||||
"@esbuild/linux-arm@0.27.4":
|
||||
version "0.27.4"
|
||||
resolved "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz"
|
||||
integrity sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==
|
||||
|
||||
"@esbuild/linux-arm64@0.27.4":
|
||||
version "0.27.4"
|
||||
resolved "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz"
|
||||
integrity sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==
|
||||
|
||||
"@esbuild/linux-ia32@0.27.4":
|
||||
version "0.27.4"
|
||||
resolved "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz"
|
||||
integrity sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==
|
||||
|
||||
"@esbuild/linux-loong64@0.27.4":
|
||||
version "0.27.4"
|
||||
resolved "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz"
|
||||
integrity sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==
|
||||
|
||||
"@esbuild/linux-mips64el@0.27.4":
|
||||
version "0.27.4"
|
||||
resolved "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz"
|
||||
integrity sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==
|
||||
|
||||
"@esbuild/linux-ppc64@0.27.4":
|
||||
version "0.27.4"
|
||||
resolved "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz"
|
||||
integrity sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==
|
||||
|
||||
"@esbuild/linux-riscv64@0.27.4":
|
||||
version "0.27.4"
|
||||
resolved "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz"
|
||||
integrity sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==
|
||||
|
||||
"@esbuild/linux-s390x@0.27.4":
|
||||
version "0.27.4"
|
||||
resolved "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz"
|
||||
integrity sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==
|
||||
|
||||
"@esbuild/linux-x64@0.27.4":
|
||||
version "0.27.4"
|
||||
resolved "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz"
|
||||
integrity sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==
|
||||
|
||||
"@esbuild/netbsd-arm64@0.27.4":
|
||||
version "0.27.4"
|
||||
resolved "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz"
|
||||
integrity sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==
|
||||
|
||||
"@esbuild/netbsd-x64@0.27.4":
|
||||
version "0.27.4"
|
||||
resolved "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz"
|
||||
integrity sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==
|
||||
|
||||
"@esbuild/openbsd-arm64@0.27.4":
|
||||
version "0.27.4"
|
||||
resolved "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz"
|
||||
integrity sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==
|
||||
|
||||
"@esbuild/openbsd-x64@0.27.4":
|
||||
version "0.27.4"
|
||||
resolved "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz"
|
||||
integrity sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==
|
||||
|
||||
"@esbuild/openharmony-arm64@0.27.4":
|
||||
version "0.27.4"
|
||||
resolved "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz"
|
||||
integrity sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==
|
||||
|
||||
"@esbuild/sunos-x64@0.27.4":
|
||||
version "0.27.4"
|
||||
resolved "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz"
|
||||
integrity sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==
|
||||
|
||||
"@esbuild/win32-arm64@0.27.4":
|
||||
version "0.27.4"
|
||||
resolved "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz"
|
||||
integrity sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==
|
||||
|
||||
"@esbuild/win32-ia32@0.27.4":
|
||||
version "0.27.4"
|
||||
resolved "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz"
|
||||
integrity sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==
|
||||
|
||||
"@esbuild/win32-x64@0.27.4":
|
||||
version "0.27.4"
|
||||
resolved "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz"
|
||||
integrity sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==
|
||||
|
||||
"@eslint-community/eslint-utils@^4.2.0":
|
||||
version "4.7.0"
|
||||
resolved "https://registry.npmmirror.com/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz"
|
||||
|
|
@ -658,6 +783,91 @@
|
|||
resolved "https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz"
|
||||
integrity sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==
|
||||
|
||||
"@rollup/rollup-android-arm-eabi@4.60.0":
|
||||
version "4.60.0"
|
||||
resolved "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.0.tgz"
|
||||
integrity sha512-WOhNW9K8bR3kf4zLxbfg6Pxu2ybOUbB2AjMDHSQx86LIF4rH4Ft7vmMwNt0loO0eonglSNy4cpD3MKXXKQu0/A==
|
||||
|
||||
"@rollup/rollup-android-arm64@4.60.0":
|
||||
version "4.60.0"
|
||||
resolved "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.0.tgz"
|
||||
integrity sha512-u6JHLll5QKRvjciE78bQXDmqRqNs5M/3GVqZeMwvmjaNODJih/WIrJlFVEihvV0MiYFmd+ZyPr9wxOVbPAG2Iw==
|
||||
|
||||
"@rollup/rollup-darwin-arm64@4.60.0":
|
||||
version "4.60.0"
|
||||
resolved "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.0.tgz"
|
||||
integrity sha512-qEF7CsKKzSRc20Ciu2Zw1wRrBz4g56F7r/vRwY430UPp/nt1x21Q/fpJ9N5l47WWvJlkNCPJz3QRVw008fi7yA==
|
||||
|
||||
"@rollup/rollup-darwin-x64@4.60.0":
|
||||
version "4.60.0"
|
||||
resolved "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.0.tgz"
|
||||
integrity sha512-WADYozJ4QCnXCH4wPB+3FuGmDPoFseVCUrANmA5LWwGmC6FL14BWC7pcq+FstOZv3baGX65tZ378uT6WG8ynTw==
|
||||
|
||||
"@rollup/rollup-freebsd-arm64@4.60.0":
|
||||
version "4.60.0"
|
||||
resolved "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.0.tgz"
|
||||
integrity sha512-6b8wGHJlDrGeSE3aH5mGNHBjA0TTkxdoNHik5EkvPHCt351XnigA4pS7Wsj/Eo9Y8RBU6f35cjN9SYmCFBtzxw==
|
||||
|
||||
"@rollup/rollup-freebsd-x64@4.60.0":
|
||||
version "4.60.0"
|
||||
resolved "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.0.tgz"
|
||||
integrity sha512-h25Ga0t4jaylMB8M/JKAyrvvfxGRjnPQIR8lnCayyzEjEOx2EJIlIiMbhpWxDRKGKF8jbNH01NnN663dH638mA==
|
||||
|
||||
"@rollup/rollup-linux-arm-gnueabihf@4.60.0":
|
||||
version "4.60.0"
|
||||
resolved "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.0.tgz"
|
||||
integrity sha512-RzeBwv0B3qtVBWtcuABtSuCzToo2IEAIQrcyB/b2zMvBWVbjo8bZDjACUpnaafaxhTw2W+imQbP2BD1usasK4g==
|
||||
|
||||
"@rollup/rollup-linux-arm-musleabihf@4.60.0":
|
||||
version "4.60.0"
|
||||
resolved "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.0.tgz"
|
||||
integrity sha512-Sf7zusNI2CIU1HLzuu9Tc5YGAHEZs5Lu7N1ssJG4Tkw6e0MEsN7NdjUDDfGNHy2IU+ENyWT+L2obgWiguWibWQ==
|
||||
|
||||
"@rollup/rollup-linux-arm64-gnu@4.60.0":
|
||||
version "4.60.0"
|
||||
resolved "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.0.tgz"
|
||||
integrity sha512-DX2x7CMcrJzsE91q7/O02IJQ5/aLkVtYFryqCjduJhUfGKG6yJV8hxaw8pZa93lLEpPTP/ohdN4wFz7yp/ry9A==
|
||||
|
||||
"@rollup/rollup-linux-arm64-musl@4.60.0":
|
||||
version "4.60.0"
|
||||
resolved "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.0.tgz"
|
||||
integrity sha512-09EL+yFVbJZlhcQfShpswwRZ0Rg+z/CsSELFCnPt3iK+iqwGsI4zht3secj5vLEs957QvFFXnzAT0FFPIxSrkQ==
|
||||
|
||||
"@rollup/rollup-linux-loong64-gnu@4.60.0":
|
||||
version "4.60.0"
|
||||
resolved "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.0.tgz"
|
||||
integrity sha512-i9IcCMPr3EXm8EQg5jnja0Zyc1iFxJjZWlb4wr7U2Wx/GrddOuEafxRdMPRYVaXjgbhvqalp6np07hN1w9kAKw==
|
||||
|
||||
"@rollup/rollup-linux-loong64-musl@4.60.0":
|
||||
version "4.60.0"
|
||||
resolved "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.0.tgz"
|
||||
integrity sha512-DGzdJK9kyJ+B78MCkWeGnpXJ91tK/iKA6HwHxF4TAlPIY7GXEvMe8hBFRgdrR9Ly4qebR/7gfUs9y2IoaVEyog==
|
||||
|
||||
"@rollup/rollup-linux-ppc64-gnu@4.60.0":
|
||||
version "4.60.0"
|
||||
resolved "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.0.tgz"
|
||||
integrity sha512-RwpnLsqC8qbS8z1H1AxBA1H6qknR4YpPR9w2XX0vo2Sz10miu57PkNcnHVaZkbqyw/kUWfKMI73jhmfi9BRMUQ==
|
||||
|
||||
"@rollup/rollup-linux-ppc64-musl@4.60.0":
|
||||
version "4.60.0"
|
||||
resolved "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.0.tgz"
|
||||
integrity sha512-Z8pPf54Ly3aqtdWC3G4rFigZgNvd+qJlOE52fmko3KST9SoGfAdSRCwyoyG05q1HrrAblLbk1/PSIV+80/pxLg==
|
||||
|
||||
"@rollup/rollup-linux-riscv64-gnu@4.60.0":
|
||||
version "4.60.0"
|
||||
resolved "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.0.tgz"
|
||||
integrity sha512-3a3qQustp3COCGvnP4SvrMHnPQ9d1vzCakQVRTliaz8cIp/wULGjiGpbcqrkv0WrHTEp8bQD/B3HBjzujVWLOA==
|
||||
|
||||
"@rollup/rollup-linux-riscv64-musl@4.60.0":
|
||||
version "4.60.0"
|
||||
resolved "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.0.tgz"
|
||||
integrity sha512-pjZDsVH/1VsghMJ2/kAaxt6dL0psT6ZexQVrijczOf+PeP2BUqTHYejk3l6TlPRydggINOeNRhvpLa0AYpCWSQ==
|
||||
|
||||
"@rollup/rollup-linux-s390x-gnu@4.60.0":
|
||||
version "4.60.0"
|
||||
resolved "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.0.tgz"
|
||||
integrity sha512-3ObQs0BhvPgiUVZrN7gqCSvmFuMWvWvsjG5ayJ3Lraqv+2KhOsp+pUbigqbeWqueGIsnn+09HBw27rJ+gYK4VQ==
|
||||
|
||||
"@rollup/rollup-linux-x64-gnu@4.60.0":
|
||||
version "4.60.0"
|
||||
resolved "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.0.tgz"
|
||||
|
|
@ -668,6 +878,36 @@
|
|||
resolved "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.0.tgz"
|
||||
integrity sha512-k09oiRCi/bHU9UVFqD17r3eJR9bn03TyKraCrlz5ULFJGdJGi7VOmm9jl44vOJvRJ6P7WuBi/s2A97LxxHGIdw==
|
||||
|
||||
"@rollup/rollup-openbsd-x64@4.60.0":
|
||||
version "4.60.0"
|
||||
resolved "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.0.tgz"
|
||||
integrity sha512-1o/0/pIhozoSaDJoDcec+IVLbnRtQmHwPV730+AOD29lHEEo4F5BEUB24H0OBdhbBBDwIOSuf7vgg0Ywxdfiiw==
|
||||
|
||||
"@rollup/rollup-openharmony-arm64@4.60.0":
|
||||
version "4.60.0"
|
||||
resolved "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.0.tgz"
|
||||
integrity sha512-pESDkos/PDzYwtyzB5p/UoNU/8fJo68vcXM9ZW2V0kjYayj1KaaUfi1NmTUTUpMn4UhU4gTuK8gIaFO4UGuMbA==
|
||||
|
||||
"@rollup/rollup-win32-arm64-msvc@4.60.0":
|
||||
version "4.60.0"
|
||||
resolved "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.0.tgz"
|
||||
integrity sha512-hj1wFStD7B1YBeYmvY+lWXZ7ey73YGPcViMShYikqKT1GtstIKQAtfUI6yrzPjAy/O7pO0VLXGmUVWXQMaYgTQ==
|
||||
|
||||
"@rollup/rollup-win32-ia32-msvc@4.60.0":
|
||||
version "4.60.0"
|
||||
resolved "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.0.tgz"
|
||||
integrity sha512-SyaIPFoxmUPlNDq5EHkTbiKzmSEmq/gOYFI/3HHJ8iS/v1mbugVa7dXUzcJGQfoytp9DJFLhHH4U3/eTy2Bq4w==
|
||||
|
||||
"@rollup/rollup-win32-x64-gnu@4.60.0":
|
||||
version "4.60.0"
|
||||
resolved "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.0.tgz"
|
||||
integrity sha512-RdcryEfzZr+lAr5kRm2ucN9aVlCCa2QNq4hXelZxb8GG0NJSazq44Z3PCCc8wISRuCVnGs0lQJVX5Vp6fKA+IA==
|
||||
|
||||
"@rollup/rollup-win32-x64-msvc@4.60.0":
|
||||
version "4.60.0"
|
||||
resolved "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.0.tgz"
|
||||
integrity sha512-PrsWNQ8BuE00O3Xsx3ALh2Df8fAj9+cvvX9AIA6o4KpATR98c9mud4XtDWVvsEuyia5U4tVSTKygawyJkjm60w==
|
||||
|
||||
"@types/babel__core@^7.20.5":
|
||||
version "7.20.5"
|
||||
resolved "https://registry.npmmirror.com/@types/babel__core/-/babel__core-7.20.5.tgz"
|
||||
|
|
@ -1891,6 +2131,11 @@ form-data@^4.0.5:
|
|||
hasown "^2.0.2"
|
||||
mime-types "^2.1.12"
|
||||
|
||||
fsevents@~2.3.2, fsevents@~2.3.3:
|
||||
version "2.3.3"
|
||||
resolved "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz"
|
||||
integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
|
||||
|
||||
function-bind@^1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz"
|
||||
|
|
|
|||
Loading…
Reference in New Issue