v1.1.1
parent
aa99ee1f6a
commit
ad16567e82
|
|
@ -15,7 +15,7 @@ router = APIRouter()
|
||||||
redis_client = redis.Redis(**REDIS_CONFIG)
|
redis_client = redis.Redis(**REDIS_CONFIG)
|
||||||
|
|
||||||
# 常量定义
|
# 常量定义
|
||||||
AUDIO_FILE_EXTENSIONS = ('.wav', '.mp3', '.m4a', '.aac', '.flac', '.ogg')
|
AUDIO_FILE_EXTENSIONS = ('.wav', '.mp3', '.m4a', '.aac', '.flac', '.ogg', '.mpeg', '.mp4', '.webm')
|
||||||
BYTES_TO_GB = 1024 ** 3
|
BYTES_TO_GB = 1024 ** 3
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -79,7 +79,8 @@ def _calculate_audio_storage() -> Dict[str, float]:
|
||||||
if os.path.exists(AUDIO_DIR):
|
if os.path.exists(AUDIO_DIR):
|
||||||
for root, _, files in os.walk(AUDIO_DIR):
|
for root, _, files in os.walk(AUDIO_DIR):
|
||||||
for file in files:
|
for file in files:
|
||||||
if file.endswith(AUDIO_FILE_EXTENSIONS):
|
file_extension = os.path.splitext(file)[1].lower()
|
||||||
|
if file_extension in AUDIO_FILE_EXTENSIONS:
|
||||||
audio_files_count += 1
|
audio_files_count += 1
|
||||||
file_path = os.path.join(root, file)
|
file_path = os.path.join(root, file)
|
||||||
try:
|
try:
|
||||||
|
|
@ -90,6 +91,7 @@ def _calculate_audio_storage() -> Dict[str, float]:
|
||||||
print(f"统计音频文件失败: {e}")
|
print(f"统计音频文件失败: {e}")
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
"audio_file_count": audio_files_count,
|
||||||
"audio_files_count": audio_files_count,
|
"audio_files_count": audio_files_count,
|
||||||
"audio_total_size_gb": round(audio_total_size / BYTES_TO_GB, 2)
|
"audio_total_size_gb": round(audio_total_size / BYTES_TO_GB, 2)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -357,6 +357,7 @@ async def create_parameter(request: ParameterUpsertRequest, current_user=Depends
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
SystemConfigService.invalidate_cache()
|
||||||
return create_api_response(code="200", message="创建参数成功")
|
return create_api_response(code="200", message="创建参数成功")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return create_api_response(code="500", message=f"创建参数失败: {str(e)}")
|
return create_api_response(code="500", message=f"创建参数失败: {str(e)}")
|
||||||
|
|
@ -401,6 +402,7 @@ async def update_parameter(
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
SystemConfigService.invalidate_cache()
|
||||||
return create_api_response(code="200", message="更新参数成功")
|
return create_api_response(code="200", message="更新参数成功")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return create_api_response(code="500", message=f"更新参数失败: {str(e)}")
|
return create_api_response(code="500", message=f"更新参数失败: {str(e)}")
|
||||||
|
|
@ -418,6 +420,7 @@ async def delete_parameter(param_key: str, current_user=Depends(get_current_admi
|
||||||
|
|
||||||
cursor.execute("DELETE FROM sys_system_parameters WHERE param_key = %s", (param_key,))
|
cursor.execute("DELETE FROM sys_system_parameters WHERE param_key = %s", (param_key,))
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
SystemConfigService.invalidate_cache()
|
||||||
return create_api_response(code="200", message="删除参数成功")
|
return create_api_response(code="200", message="删除参数成功")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return create_api_response(code="500", message=f"删除参数失败: {str(e)}")
|
return create_api_response(code="500", message=f"删除参数失败: {str(e)}")
|
||||||
|
|
@ -807,7 +810,7 @@ async def get_public_system_config():
|
||||||
return create_api_response(
|
return create_api_response(
|
||||||
code="200",
|
code="200",
|
||||||
message="获取公开配置成功",
|
message="获取公开配置成功",
|
||||||
data=SystemConfigService.get_branding_config()
|
data=SystemConfigService.get_public_configs()
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return create_api_response(code="500", message=f"获取公开配置失败: {str(e)}")
|
return create_api_response(code="500", message=f"获取公开配置失败: {str(e)}")
|
||||||
|
|
|
||||||
|
|
@ -304,7 +304,7 @@ def get_meetings(
|
||||||
):
|
):
|
||||||
# 使用配置的默认页面大小
|
# 使用配置的默认页面大小
|
||||||
if page_size is None:
|
if page_size is None:
|
||||||
page_size = SystemConfigService.get_timeline_pagesize(default=10)
|
page_size = SystemConfigService.get_page_size(default=10)
|
||||||
|
|
||||||
with get_db_connection() as connection:
|
with get_db_connection() as connection:
|
||||||
cursor = connection.cursor(dictionary=True)
|
cursor = connection.cursor(dictionary=True)
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
import json
|
import json
|
||||||
|
import time
|
||||||
|
from threading import RLock
|
||||||
from typing import Optional, Dict, Any
|
from typing import Optional, Dict, Any
|
||||||
from app.core.database import get_db_connection
|
from app.core.database import get_db_connection
|
||||||
|
|
||||||
|
|
@ -7,18 +9,18 @@ class SystemConfigService:
|
||||||
"""系统配置服务 - 优先从新配置表读取,兼容 dict_data(system_config) 回退"""
|
"""系统配置服务 - 优先从新配置表读取,兼容 dict_data(system_config) 回退"""
|
||||||
|
|
||||||
DICT_TYPE = 'system_config'
|
DICT_TYPE = 'system_config'
|
||||||
|
PUBLIC_CATEGORY = 'public'
|
||||||
DEFAULT_LLM_ENDPOINT_URL = 'https://dashscope.aliyuncs.com/compatible-mode/v1'
|
DEFAULT_LLM_ENDPOINT_URL = 'https://dashscope.aliyuncs.com/compatible-mode/v1'
|
||||||
|
CACHE_TTL_SECONDS = 60
|
||||||
|
|
||||||
# 配置键常量
|
# 配置键常量
|
||||||
ASR_VOCABULARY_ID = 'asr_vocabulary_id'
|
ASR_VOCABULARY_ID = 'asr_vocabulary_id'
|
||||||
TIMELINE_PAGESIZE = 'timeline_pagesize'
|
PAGE_SIZE = 'page_size'
|
||||||
DEFAULT_RESET_PASSWORD = 'default_reset_password'
|
DEFAULT_RESET_PASSWORD = 'default_reset_password'
|
||||||
MAX_AUDIO_SIZE = 'max_audio_size'
|
MAX_AUDIO_SIZE = 'max_audio_size'
|
||||||
|
|
||||||
# 品牌配置
|
# 品牌配置
|
||||||
BRANDING_APP_NAME = 'branding_app_name'
|
APP_NAME = 'app_name'
|
||||||
BRANDING_HOME_HEADLINE = 'branding_home_headline'
|
|
||||||
BRANDING_HOME_TAGLINE = 'branding_home_tagline'
|
|
||||||
BRANDING_CONSOLE_SUBTITLE = 'branding_console_subtitle'
|
BRANDING_CONSOLE_SUBTITLE = 'branding_console_subtitle'
|
||||||
BRANDING_PREVIEW_TITLE = 'branding_preview_title'
|
BRANDING_PREVIEW_TITLE = 'branding_preview_title'
|
||||||
BRANDING_LOGIN_WELCOME = 'branding_login_welcome'
|
BRANDING_LOGIN_WELCOME = 'branding_login_welcome'
|
||||||
|
|
@ -36,6 +38,38 @@ class SystemConfigService:
|
||||||
LLM_TIMEOUT = 'llm_timeout'
|
LLM_TIMEOUT = 'llm_timeout'
|
||||||
LLM_TEMPERATURE = 'llm_temperature'
|
LLM_TEMPERATURE = 'llm_temperature'
|
||||||
LLM_TOP_P = 'llm_top_p'
|
LLM_TOP_P = 'llm_top_p'
|
||||||
|
_cache_lock = RLock()
|
||||||
|
_config_cache: Dict[str, tuple[float, Any]] = {}
|
||||||
|
_category_cache: Dict[str, tuple[float, Dict[str, Any]]] = {}
|
||||||
|
_all_configs_cache: tuple[float, Dict[str, Any]] | None = None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _is_cache_valid(cls, cached_at: float) -> bool:
|
||||||
|
return (time.time() - cached_at) < cls.CACHE_TTL_SECONDS
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _get_cached_config(cls, cache_key: str) -> Any:
|
||||||
|
with cls._cache_lock:
|
||||||
|
cached = cls._config_cache.get(cache_key)
|
||||||
|
if not cached:
|
||||||
|
return None
|
||||||
|
cached_at, value = cached
|
||||||
|
if not cls._is_cache_valid(cached_at):
|
||||||
|
cls._config_cache.pop(cache_key, None)
|
||||||
|
return None
|
||||||
|
return value
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _set_cached_config(cls, cache_key: str, value: Any) -> None:
|
||||||
|
with cls._cache_lock:
|
||||||
|
cls._config_cache[cache_key] = (time.time(), value)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def invalidate_cache(cls) -> None:
|
||||||
|
with cls._cache_lock:
|
||||||
|
cls._config_cache.clear()
|
||||||
|
cls._category_cache.clear()
|
||||||
|
cls._all_configs_cache = None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _parse_json_object(value: Any) -> Dict[str, Any]:
|
def _parse_json_object(value: Any) -> Dict[str, Any]:
|
||||||
|
|
@ -262,9 +296,14 @@ class SystemConfigService:
|
||||||
Returns:
|
Returns:
|
||||||
配置项的值
|
配置项的值
|
||||||
"""
|
"""
|
||||||
|
cached_value = cls._get_cached_config(dict_code)
|
||||||
|
if cached_value is not None:
|
||||||
|
return cached_value
|
||||||
|
|
||||||
# 1) 新参数表
|
# 1) 新参数表
|
||||||
value = cls._get_parameter_value(dict_code)
|
value = cls._get_parameter_value(dict_code)
|
||||||
if value is not None:
|
if value is not None:
|
||||||
|
cls._set_cached_config(dict_code, value)
|
||||||
return value
|
return value
|
||||||
|
|
||||||
# 2) 兼容旧 sys_dict_data
|
# 2) 兼容旧 sys_dict_data
|
||||||
|
|
@ -284,10 +323,13 @@ class SystemConfigService:
|
||||||
if result and result['extension_attr']:
|
if result and result['extension_attr']:
|
||||||
try:
|
try:
|
||||||
ext_attr = json.loads(result['extension_attr']) if isinstance(result['extension_attr'], str) else result['extension_attr']
|
ext_attr = json.loads(result['extension_attr']) if isinstance(result['extension_attr'], str) else result['extension_attr']
|
||||||
return ext_attr.get('value', default_value)
|
resolved_value = ext_attr.get('value', default_value)
|
||||||
|
cls._set_cached_config(dict_code, resolved_value)
|
||||||
|
return resolved_value
|
||||||
except (json.JSONDecodeError, AttributeError):
|
except (json.JSONDecodeError, AttributeError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
cls._set_cached_config(dict_code, default_value)
|
||||||
return default_value
|
return default_value
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
@ -410,6 +452,7 @@ class SystemConfigService:
|
||||||
)
|
)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
cursor.close()
|
cursor.close()
|
||||||
|
cls.invalidate_cache()
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error setting config in sys_system_parameters {dict_code}: {e}")
|
print(f"Error setting config in sys_system_parameters {dict_code}: {e}")
|
||||||
|
|
@ -451,6 +494,7 @@ class SystemConfigService:
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
cursor.close()
|
cursor.close()
|
||||||
|
cls.invalidate_cache()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
@ -465,6 +509,12 @@ class SystemConfigService:
|
||||||
Returns:
|
Returns:
|
||||||
配置字典 {dict_code: value}
|
配置字典 {dict_code: value}
|
||||||
"""
|
"""
|
||||||
|
with cls._cache_lock:
|
||||||
|
if cls._all_configs_cache and cls._is_cache_valid(cls._all_configs_cache[0]):
|
||||||
|
return dict(cls._all_configs_cache[1])
|
||||||
|
if cls._all_configs_cache and not cls._is_cache_valid(cls._all_configs_cache[0]):
|
||||||
|
cls._all_configs_cache = None
|
||||||
|
|
||||||
# 1) 新参数表
|
# 1) 新参数表
|
||||||
try:
|
try:
|
||||||
with get_db_connection() as conn:
|
with get_db_connection() as conn:
|
||||||
|
|
@ -480,7 +530,10 @@ class SystemConfigService:
|
||||||
rows = cursor.fetchall()
|
rows = cursor.fetchall()
|
||||||
cursor.close()
|
cursor.close()
|
||||||
if rows:
|
if rows:
|
||||||
return {row["param_key"]: row["param_value"] for row in rows}
|
configs = {row["param_key"]: row["param_value"] for row in rows}
|
||||||
|
with cls._cache_lock:
|
||||||
|
cls._all_configs_cache = (time.time(), configs)
|
||||||
|
return dict(configs)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error getting all configs from sys_system_parameters: {e}")
|
print(f"Error getting all configs from sys_system_parameters: {e}")
|
||||||
|
|
||||||
|
|
@ -509,12 +562,46 @@ class SystemConfigService:
|
||||||
else:
|
else:
|
||||||
configs[row['dict_code']] = None
|
configs[row['dict_code']] = None
|
||||||
|
|
||||||
return configs
|
with cls._cache_lock:
|
||||||
|
cls._all_configs_cache = (time.time(), configs)
|
||||||
|
return dict(configs)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error getting all configs: {e}")
|
print(f"Error getting all configs: {e}")
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_configs_by_category(cls, category: str) -> Dict[str, Any]:
|
||||||
|
"""按分类获取启用中的参数配置。"""
|
||||||
|
with cls._cache_lock:
|
||||||
|
cached = cls._category_cache.get(category)
|
||||||
|
if cached and cls._is_cache_valid(cached[0]):
|
||||||
|
return dict(cached[1])
|
||||||
|
if cached and not cls._is_cache_valid(cached[0]):
|
||||||
|
cls._category_cache.pop(category, None)
|
||||||
|
|
||||||
|
try:
|
||||||
|
with get_db_connection() as conn:
|
||||||
|
cursor = conn.cursor(dictionary=True)
|
||||||
|
cursor.execute(
|
||||||
|
"""
|
||||||
|
SELECT param_key, param_value
|
||||||
|
FROM sys_system_parameters
|
||||||
|
WHERE is_active = 1 AND category = %s
|
||||||
|
ORDER BY param_key
|
||||||
|
""",
|
||||||
|
(category,),
|
||||||
|
)
|
||||||
|
rows = cursor.fetchall()
|
||||||
|
cursor.close()
|
||||||
|
configs = {row["param_key"]: row["param_value"] for row in rows} if rows else {}
|
||||||
|
with cls._cache_lock:
|
||||||
|
cls._category_cache[category] = (time.time(), configs)
|
||||||
|
return dict(configs)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error getting configs by category {category}: {e}")
|
||||||
|
return {}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def batch_set_configs(cls, configs: Dict[str, Any]) -> bool:
|
def batch_set_configs(cls, configs: Dict[str, Any]) -> bool:
|
||||||
"""
|
"""
|
||||||
|
|
@ -590,9 +677,9 @@ class SystemConfigService:
|
||||||
return default
|
return default
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_timeline_pagesize(cls, default: int = 10) -> int:
|
def get_page_size(cls, default: int = 10) -> int:
|
||||||
"""获取会议时间轴每页数量"""
|
"""获取系统通用分页数量。"""
|
||||||
value = cls.get_config(cls.TIMELINE_PAGESIZE, str(default))
|
value = cls.get_config(cls.PAGE_SIZE, str(default))
|
||||||
try:
|
try:
|
||||||
return int(value)
|
return int(value)
|
||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
|
|
@ -613,22 +700,59 @@ class SystemConfigService:
|
||||||
return default
|
return default
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_branding_config(cls) -> Dict[str, str]:
|
def get_public_configs(cls) -> Dict[str, Any]:
|
||||||
max_audio_size_mb = cls.get_max_audio_size(100)
|
"""获取提供给前端初始化使用的公开参数。"""
|
||||||
max_image_size_mb = cls.get_config("max_image_size", "10")
|
public_configs = cls.get_configs_by_category(cls.PUBLIC_CATEGORY)
|
||||||
|
|
||||||
|
app_name = str(public_configs.get(cls.APP_NAME) or "智听云平台")
|
||||||
|
console_subtitle = str(
|
||||||
|
public_configs.get("console_subtitle")
|
||||||
|
or public_configs.get(cls.BRANDING_CONSOLE_SUBTITLE)
|
||||||
|
or "iMeeting控制台"
|
||||||
|
)
|
||||||
|
preview_title = str(
|
||||||
|
public_configs.get("preview_title")
|
||||||
|
or public_configs.get(cls.BRANDING_PREVIEW_TITLE)
|
||||||
|
or "会议预览"
|
||||||
|
)
|
||||||
|
login_welcome = str(
|
||||||
|
public_configs.get("login_welcome")
|
||||||
|
or public_configs.get(cls.BRANDING_LOGIN_WELCOME)
|
||||||
|
or "欢迎回来,请输入您的登录凭证。"
|
||||||
|
)
|
||||||
|
footer_text = str(
|
||||||
|
public_configs.get("footer_text")
|
||||||
|
or public_configs.get(cls.BRANDING_FOOTER_TEXT)
|
||||||
|
or "©2026 智听云平台"
|
||||||
|
)
|
||||||
|
|
||||||
|
raw_page_size = public_configs.get(cls.PAGE_SIZE, "10")
|
||||||
try:
|
try:
|
||||||
max_image_size_mb = int(max_image_size_mb)
|
page_size = int(raw_page_size)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
page_size = 10
|
||||||
|
|
||||||
|
raw_max_audio_size = public_configs.get(cls.MAX_AUDIO_SIZE, "100")
|
||||||
|
try:
|
||||||
|
max_audio_size_mb = int(raw_max_audio_size)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
max_audio_size_mb = 100
|
||||||
|
|
||||||
|
raw_max_image_size = public_configs.get("max_image_size", "10")
|
||||||
|
try:
|
||||||
|
max_image_size_mb = int(raw_max_image_size)
|
||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
max_image_size_mb = 10
|
max_image_size_mb = 10
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"app_name": str(cls.get_config(cls.BRANDING_APP_NAME, "智听云平台") or "智听云平台"),
|
**public_configs,
|
||||||
"home_headline": str(cls.get_config(cls.BRANDING_HOME_HEADLINE, "智听云平台") or "智听云平台"),
|
"app_name": app_name,
|
||||||
"home_tagline": str(cls.get_config(cls.BRANDING_HOME_TAGLINE, "让每一次谈话都产生价值") or "让每一次谈话都产生价值"),
|
"console_subtitle": console_subtitle,
|
||||||
"console_subtitle": str(cls.get_config(cls.BRANDING_CONSOLE_SUBTITLE, "iMeeting控制台") or "iMeeting控制台"),
|
"preview_title": preview_title,
|
||||||
"preview_title": str(cls.get_config(cls.BRANDING_PREVIEW_TITLE, "会议预览") or "会议预览"),
|
"login_welcome": login_welcome,
|
||||||
"login_welcome": str(cls.get_config(cls.BRANDING_LOGIN_WELCOME, "欢迎回来,请输入您的登录凭证。") or "欢迎回来,请输入您的登录凭证。"),
|
"footer_text": footer_text,
|
||||||
"footer_text": str(cls.get_config(cls.BRANDING_FOOTER_TEXT, "©2026 智听云平台") or "©2026 智听云平台"),
|
"page_size": str(page_size),
|
||||||
|
"PAGE_SIZE": page_size,
|
||||||
"max_audio_size": str(max_audio_size_mb),
|
"max_audio_size": str(max_audio_size_mb),
|
||||||
"MAX_FILE_SIZE": max_audio_size_mb * 1024 * 1024,
|
"MAX_FILE_SIZE": max_audio_size_mb * 1024 * 1024,
|
||||||
"max_image_size": str(max_image_size_mb),
|
"max_image_size": str(max_image_size_mb),
|
||||||
|
|
|
||||||
|
|
@ -346,7 +346,7 @@ INSERT INTO `dict_data` (`id`, `dict_type`, `dict_code`, `parent_code`, `tree_pa
|
||||||
INSERT INTO `dict_data` (`id`, `dict_type`, `dict_code`, `parent_code`, `tree_path`, `label_cn`, `label_en`, `sort_order`, `extension_attr`, `is_default`, `status`, `update_time`, `create_time`) VALUES (22, 'system_config', 'asr_vocabulary_id', 'ROOT', NULL, '热词字典ID', NULL, 0, '{\"value\": \"vocab-imeeting-734e93f5bd8a4f3bb665dd526d584516\"}', 0, 1, '2026-01-19 20:25:13', '2026-01-05 14:13:36');
|
INSERT INTO `dict_data` (`id`, `dict_type`, `dict_code`, `parent_code`, `tree_path`, `label_cn`, `label_en`, `sort_order`, `extension_attr`, `is_default`, `status`, `update_time`, `create_time`) VALUES (22, 'system_config', 'asr_vocabulary_id', 'ROOT', NULL, '热词字典ID', NULL, 0, '{\"value\": \"vocab-imeeting-734e93f5bd8a4f3bb665dd526d584516\"}', 0, 1, '2026-01-19 20:25:13', '2026-01-05 14:13:36');
|
||||||
INSERT INTO `dict_data` (`id`, `dict_type`, `dict_code`, `parent_code`, `tree_path`, `label_cn`, `label_en`, `sort_order`, `extension_attr`, `is_default`, `status`, `update_time`, `create_time`) VALUES (23, 'system_config', 'default_reset_password', 'ROOT', NULL, '默认重置密码', NULL, 2, '{\"value\": \"123456\"}', 0, 1, '2026-01-06 07:04:51', '2026-01-06 02:54:02');
|
INSERT INTO `dict_data` (`id`, `dict_type`, `dict_code`, `parent_code`, `tree_path`, `label_cn`, `label_en`, `sort_order`, `extension_attr`, `is_default`, `status`, `update_time`, `create_time`) VALUES (23, 'system_config', 'default_reset_password', 'ROOT', NULL, '默认重置密码', NULL, 2, '{\"value\": \"123456\"}', 0, 1, '2026-01-06 07:04:51', '2026-01-06 02:54:02');
|
||||||
INSERT INTO `dict_data` (`id`, `dict_type`, `dict_code`, `parent_code`, `tree_path`, `label_cn`, `label_en`, `sort_order`, `extension_attr`, `is_default`, `status`, `update_time`, `create_time`) VALUES (24, 'system_config', 'max_audio_size', 'ROOT', NULL, '上传音频文件大小', NULL, 3, '{\"value\": \"100\"}', 0, 1, '2026-01-06 07:55:57', '2026-01-06 02:56:06');
|
INSERT INTO `dict_data` (`id`, `dict_type`, `dict_code`, `parent_code`, `tree_path`, `label_cn`, `label_en`, `sort_order`, `extension_attr`, `is_default`, `status`, `update_time`, `create_time`) VALUES (24, 'system_config', 'max_audio_size', 'ROOT', NULL, '上传音频文件大小', NULL, 3, '{\"value\": \"100\"}', 0, 1, '2026-01-06 07:55:57', '2026-01-06 02:56:06');
|
||||||
INSERT INTO `dict_data` (`id`, `dict_type`, `dict_code`, `parent_code`, `tree_path`, `label_cn`, `label_en`, `sort_order`, `extension_attr`, `is_default`, `status`, `update_time`, `create_time`) VALUES (25, 'system_config', 'timeline_pagesize', 'ROOT', NULL, '会议时间轴每页数量', NULL, 1, '{\"value\": \"10\"}', 0, 1, '2026-01-06 07:04:51', '2026-01-06 02:57:35');
|
INSERT INTO `dict_data` (`id`, `dict_type`, `dict_code`, `parent_code`, `tree_path`, `label_cn`, `label_en`, `sort_order`, `extension_attr`, `is_default`, `status`, `update_time`, `create_time`) VALUES (25, 'system_config', 'page_size', 'ROOT', NULL, '通用分页数量', NULL, 1, '{\"value\": \"10\"}', 0, 1, '2026-01-06 07:04:51', '2026-01-06 02:57:35');
|
||||||
INSERT INTO `dict_data` (`id`, `dict_type`, `dict_code`, `parent_code`, `tree_path`, `label_cn`, `label_en`, `sort_order`, `extension_attr`, `is_default`, `status`, `update_time`, `create_time`) VALUES (26, 'system_config', 'voiceprint', 'ROOT', NULL, '声纹采集配置', NULL, 0, '{\"channels\": 1, \"sample_rate\": 16000, \"template_text\": \"我正在进行声纹采集,这段语音将用于身份识别和验证。\\n\\n声纹技术能够准确识别每个人独特的声音特征。\", \"duration_seconds\": 12, \"voiceprint_max_size\": 5242880}', 0, 1, NULL, '2026-01-06 03:46:22');
|
INSERT INTO `dict_data` (`id`, `dict_type`, `dict_code`, `parent_code`, `tree_path`, `label_cn`, `label_en`, `sort_order`, `extension_attr`, `is_default`, `status`, `update_time`, `create_time`) VALUES (26, 'system_config', 'voiceprint', 'ROOT', NULL, '声纹采集配置', NULL, 0, '{\"channels\": 1, \"sample_rate\": 16000, \"template_text\": \"我正在进行声纹采集,这段语音将用于身份识别和验证。\\n\\n声纹技术能够准确识别每个人独特的声音特征。\", \"duration_seconds\": 12, \"voiceprint_max_size\": 5242880}', 0, 1, NULL, '2026-01-06 03:46:22');
|
||||||
INSERT INTO `dict_data` (`id`, `dict_type`, `dict_code`, `parent_code`, `tree_path`, `label_cn`, `label_en`, `sort_order`, `extension_attr`, `is_default`, `status`, `update_time`, `create_time`) VALUES (29, 'system_config', 'llm_model', 'ROOT', NULL, '文本大模型', 'llm_model', 6, '{\"top_p\": \"0.9\", \"time_out\": \"120\", \"model_name\": \"qwen-plus\", \"temperature\": \"0.7\", \"system_prompt\": \"You are a professional meeting transcript analysis assistant. Please generate a clear meeting summary based on the provided meeting transcript.\"}', 0, 1, NULL, '2026-01-06 10:23:34');
|
INSERT INTO `dict_data` (`id`, `dict_type`, `dict_code`, `parent_code`, `tree_path`, `label_cn`, `label_en`, `sort_order`, `extension_attr`, `is_default`, `status`, `update_time`, `create_time`) VALUES (29, 'system_config', 'llm_model', 'ROOT', NULL, '文本大模型', 'llm_model', 6, '{\"top_p\": \"0.9\", \"time_out\": \"120\", \"model_name\": \"qwen-plus\", \"temperature\": \"0.7\", \"system_prompt\": \"You are a professional meeting transcript analysis assistant. Please generate a clear meeting summary based on the provided meeting transcript.\"}', 0, 1, NULL, '2026-01-06 10:23:34');
|
||||||
INSERT INTO `dict_data` (`id`, `dict_type`, `dict_code`, `parent_code`, `tree_path`, `label_cn`, `label_en`, `sort_order`, `extension_attr`, `is_default`, `status`, `update_time`, `create_time`) VALUES (30, 'external_apps', 'NATIVE', 'ROOT', NULL, '原生应用', 'Native App', 1, '{\"suffix\": \"apk\"}', 0, 1, NULL, '2026-01-14 16:54:25');
|
INSERT INTO `dict_data` (`id`, `dict_type`, `dict_code`, `parent_code`, `tree_path`, `label_cn`, `label_en`, `sort_order`, `extension_attr`, `is_default`, `status`, `update_time`, `create_time`) VALUES (30, 'external_apps', 'NATIVE', 'ROOT', NULL, '原生应用', 'Native App', 1, '{\"suffix\": \"apk\"}', 0, 1, NULL, '2026-01-14 16:54:25');
|
||||||
|
|
|
||||||
|
|
@ -41,11 +41,37 @@ CREATE TABLE IF NOT EXISTS `ai_model_configs` (
|
||||||
-- migrate system_config parameters except model-like records
|
-- migrate system_config parameters except model-like records
|
||||||
INSERT INTO `sys_system_parameters` (`param_key`, `param_name`, `param_value`, `value_type`, `category`, `description`, `is_active`)
|
INSERT INTO `sys_system_parameters` (`param_key`, `param_name`, `param_value`, `value_type`, `category`, `description`, `is_active`)
|
||||||
SELECT
|
SELECT
|
||||||
d.`dict_code`,
|
CASE
|
||||||
|
WHEN d.`dict_code` = 'timeline_pagesize' THEN 'page_size'
|
||||||
|
WHEN d.`dict_code` = 'branding_app_name' THEN 'app_name'
|
||||||
|
WHEN d.`dict_code` = 'branding_console_subtitle' THEN 'console_subtitle'
|
||||||
|
WHEN d.`dict_code` = 'branding_preview_title' THEN 'preview_title'
|
||||||
|
WHEN d.`dict_code` = 'branding_login_welcome' THEN 'login_welcome'
|
||||||
|
WHEN d.`dict_code` = 'branding_footer_text' THEN 'footer_text'
|
||||||
|
ELSE d.`dict_code`
|
||||||
|
END,
|
||||||
d.`label_cn`,
|
d.`label_cn`,
|
||||||
JSON_UNQUOTE(JSON_EXTRACT(d.`extension_attr`, '$.value')),
|
JSON_UNQUOTE(JSON_EXTRACT(d.`extension_attr`, '$.value')),
|
||||||
'string',
|
'string',
|
||||||
'system',
|
CASE
|
||||||
|
WHEN d.`dict_code` IN (
|
||||||
|
'timeline_pagesize',
|
||||||
|
'page_size',
|
||||||
|
'branding_app_name',
|
||||||
|
'app_name',
|
||||||
|
'branding_console_subtitle',
|
||||||
|
'console_subtitle',
|
||||||
|
'branding_preview_title',
|
||||||
|
'preview_title',
|
||||||
|
'branding_login_welcome',
|
||||||
|
'login_welcome',
|
||||||
|
'branding_footer_text',
|
||||||
|
'footer_text',
|
||||||
|
'max_audio_size',
|
||||||
|
'max_image_size'
|
||||||
|
) THEN 'public'
|
||||||
|
ELSE 'system'
|
||||||
|
END,
|
||||||
CONCAT('migrated from dict_data.system_config(', d.`dict_code`, ')'),
|
CONCAT('migrated from dict_data.system_config(', d.`dict_code`, ')'),
|
||||||
CASE WHEN d.`status` = 1 THEN 1 ELSE 0 END
|
CASE WHEN d.`status` = 1 THEN 1 ELSE 0 END
|
||||||
FROM `sys_dict_data` d
|
FROM `sys_dict_data` d
|
||||||
|
|
@ -55,6 +81,7 @@ WHERE d.`dict_type` = 'system_config'
|
||||||
ON DUPLICATE KEY UPDATE
|
ON DUPLICATE KEY UPDATE
|
||||||
`param_name` = VALUES(`param_name`),
|
`param_name` = VALUES(`param_name`),
|
||||||
`param_value` = VALUES(`param_value`),
|
`param_value` = VALUES(`param_value`),
|
||||||
|
`category` = VALUES(`category`),
|
||||||
`is_active` = VALUES(`is_active`);
|
`is_active` = VALUES(`is_active`);
|
||||||
|
|
||||||
-- migrate llm model
|
-- migrate llm model
|
||||||
|
|
|
||||||
|
|
@ -534,11 +534,37 @@ CREATE TABLE IF NOT EXISTS `sys_user_mcp` (
|
||||||
INSERT INTO `sys_system_parameters`
|
INSERT INTO `sys_system_parameters`
|
||||||
(`param_key`, `param_name`, `param_value`, `value_type`, `category`, `description`, `is_active`)
|
(`param_key`, `param_name`, `param_value`, `value_type`, `category`, `description`, `is_active`)
|
||||||
SELECT
|
SELECT
|
||||||
d.`dict_code`,
|
CASE
|
||||||
|
WHEN d.`dict_code` = 'timeline_pagesize' THEN 'page_size'
|
||||||
|
WHEN d.`dict_code` = 'branding_app_name' THEN 'app_name'
|
||||||
|
WHEN d.`dict_code` = 'branding_console_subtitle' THEN 'console_subtitle'
|
||||||
|
WHEN d.`dict_code` = 'branding_preview_title' THEN 'preview_title'
|
||||||
|
WHEN d.`dict_code` = 'branding_login_welcome' THEN 'login_welcome'
|
||||||
|
WHEN d.`dict_code` = 'branding_footer_text' THEN 'footer_text'
|
||||||
|
ELSE d.`dict_code`
|
||||||
|
END,
|
||||||
d.`label_cn`,
|
d.`label_cn`,
|
||||||
JSON_UNQUOTE(JSON_EXTRACT(d.`extension_attr`, '$.value')),
|
JSON_UNQUOTE(JSON_EXTRACT(d.`extension_attr`, '$.value')),
|
||||||
'string',
|
'string',
|
||||||
'system',
|
CASE
|
||||||
|
WHEN d.`dict_code` IN (
|
||||||
|
'timeline_pagesize',
|
||||||
|
'page_size',
|
||||||
|
'branding_app_name',
|
||||||
|
'app_name',
|
||||||
|
'branding_console_subtitle',
|
||||||
|
'console_subtitle',
|
||||||
|
'branding_preview_title',
|
||||||
|
'preview_title',
|
||||||
|
'branding_login_welcome',
|
||||||
|
'login_welcome',
|
||||||
|
'branding_footer_text',
|
||||||
|
'footer_text',
|
||||||
|
'max_audio_size',
|
||||||
|
'max_image_size'
|
||||||
|
) THEN 'public'
|
||||||
|
ELSE 'system'
|
||||||
|
END,
|
||||||
CONCAT('migrated from dict_data.system_config(', d.`dict_code`, ')'),
|
CONCAT('migrated from dict_data.system_config(', d.`dict_code`, ')'),
|
||||||
CASE WHEN d.`status` = 1 THEN 1 ELSE 0 END
|
CASE WHEN d.`status` = 1 THEN 1 ELSE 0 END
|
||||||
FROM `sys_dict_data` d
|
FROM `sys_dict_data` d
|
||||||
|
|
@ -548,6 +574,7 @@ WHERE d.`dict_type` = 'system_config'
|
||||||
ON DUPLICATE KEY UPDATE
|
ON DUPLICATE KEY UPDATE
|
||||||
`param_name` = VALUES(`param_name`),
|
`param_name` = VALUES(`param_name`),
|
||||||
`param_value` = VALUES(`param_value`),
|
`param_value` = VALUES(`param_value`),
|
||||||
|
`category` = VALUES(`category`),
|
||||||
`description` = VALUES(`description`),
|
`description` = VALUES(`description`),
|
||||||
`is_active` = VALUES(`is_active`);
|
`is_active` = VALUES(`is_active`);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,499 @@
|
||||||
|
# 通用代码结构设计规范(强制执行)
|
||||||
|
|
||||||
|
本文档定义项目在长期演进中应遵守的结构边界、拆分原则与审计标准。
|
||||||
|
|
||||||
|
目标不是机械追求“小文件”“多目录”或“某种固定架构”,而是让任意语言、任意框架、任意部署形态下的代码都满足以下要求:
|
||||||
|
|
||||||
|
- 入口清晰
|
||||||
|
- 依赖方向稳定
|
||||||
|
- 职责边界明确
|
||||||
|
- 修改影响面可控
|
||||||
|
- 新人可顺序读懂
|
||||||
|
|
||||||
|
本文档适用于:
|
||||||
|
|
||||||
|
- 前端应用
|
||||||
|
- 后端服务
|
||||||
|
- CLI / 脚本 / Worker
|
||||||
|
- SDK / Library
|
||||||
|
- 单仓或多仓项目
|
||||||
|
|
||||||
|
本文档自落地起作为后续开发与重构的默认结构基线。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 核心原则
|
||||||
|
|
||||||
|
### 1.1 先划清职责,再决定目录
|
||||||
|
|
||||||
|
- 先区分“入口层 / 业务编排层 / 领域规则层 / 基础设施层 / 共享基础层”,再决定是否拆目录、拆文件、拆包。
|
||||||
|
- 目录结构是职责设计的结果,不是先验答案。
|
||||||
|
- 同一个团队可以采用不同目录形态,但不能模糊职责边界。
|
||||||
|
|
||||||
|
### 1.2 领域内聚优先于机械拆分
|
||||||
|
|
||||||
|
- 第一判断标准是“是否仍然属于同一业务主题”,不是“文件还能不能再拆小”。
|
||||||
|
- 同一主题内的读取、写入、校验、少量派生逻辑,可以保留在同一模块中。
|
||||||
|
- 如果拆分只会制造更多跳转、隐藏真实依赖、降低顺序可读性,就不应继续拆。
|
||||||
|
|
||||||
|
### 1.3 装配层必须薄
|
||||||
|
|
||||||
|
- 启动入口、路由入口、页面入口、命令入口都只负责装配。
|
||||||
|
- 装配层可以做依赖注入、参数收集、状态接线、组件拼装、调用编排入口。
|
||||||
|
- 装配层不应承载复杂业务规则、数据库细节、文件系统细节、网络细节或长流程状态机。
|
||||||
|
|
||||||
|
### 1.4 副作用必须收口
|
||||||
|
|
||||||
|
- 数据库访问、文件读写、网络调用、缓存、定时器、浏览器存储、进程环境依赖等,都属于副作用。
|
||||||
|
- 副作用应集中在可识别的边界模块中,不应在页面、视图、路由、DTO、纯工具里四处散落。
|
||||||
|
- 任何需要 mock、替换、复用或测试隔离的外部依赖,都应有明确归属。
|
||||||
|
|
||||||
|
### 1.5 依赖方向必须单向
|
||||||
|
|
||||||
|
- 默认依赖方向应从外向内:入口层 -> 业务编排层 -> 领域规则层 -> 基础设施实现。
|
||||||
|
- 共享基础层可以被多个层使用,但不能反向依赖业务实现。
|
||||||
|
- 低层不能反向引用高层具体实现来“图省事”。
|
||||||
|
|
||||||
|
### 1.6 文件大小不是目标,跨职责才是风险
|
||||||
|
|
||||||
|
- 行数只作为预警信号,不作为强制拆分指标。
|
||||||
|
- 真正需要拆分的信号包括:
|
||||||
|
- 一个模块服务多个业务主题
|
||||||
|
- 一个模块同时承担输入解析、业务决策、数据访问和展示
|
||||||
|
- 一个改动常常需要在同一文件中切换多种关注点
|
||||||
|
- 一个模块需要为不同调用方维持多套语义
|
||||||
|
|
||||||
|
### 1.7 重构优先低风险搬运
|
||||||
|
|
||||||
|
- 结构重构优先做“职责收口、边界清理、命名校正、依赖下沉/上提”。
|
||||||
|
- 默认不要在同一轮改动里同时进行:
|
||||||
|
- 大规模结构调整
|
||||||
|
- 新功能开发
|
||||||
|
- 行为修复
|
||||||
|
- 如果确需并行,必须以最小范围控制风险,并显式验证关键路径。
|
||||||
|
|
||||||
|
### 1.8 命名必须体现主题
|
||||||
|
|
||||||
|
- 文件、目录、模块、类型、服务名都应直接表达责任。
|
||||||
|
- 禁止使用模糊命名掩盖职责,例如:
|
||||||
|
- `misc`
|
||||||
|
- `helpers2`
|
||||||
|
- `commonThing`
|
||||||
|
- `temp_service`
|
||||||
|
- `manager_new`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 通用分层模型
|
||||||
|
|
||||||
|
以下是跨语言可复用的职责模型。项目不要求逐字使用这些目录名,但必须能映射到这些边界。
|
||||||
|
|
||||||
|
### 2.1 入口层 / 接口层
|
||||||
|
|
||||||
|
典型形态:
|
||||||
|
|
||||||
|
- 前端的 `App`、路由入口、页面入口
|
||||||
|
- 后端的 router / controller / handler
|
||||||
|
- CLI 的 command / main
|
||||||
|
- Worker 的 job handler / consumer entry
|
||||||
|
|
||||||
|
职责:
|
||||||
|
|
||||||
|
- 接收输入
|
||||||
|
- 做基础参数解析与协议适配
|
||||||
|
- 调用业务编排层
|
||||||
|
- 返回结果或渲染输出
|
||||||
|
|
||||||
|
禁止:
|
||||||
|
|
||||||
|
- 写复杂业务规则
|
||||||
|
- 直接拼装 SQL / ORM 流程
|
||||||
|
- 直接进行大段文件系统读写
|
||||||
|
- 直接进行复杂网络编排
|
||||||
|
- 在入口层内维护长生命周期状态机
|
||||||
|
|
||||||
|
### 2.2 业务编排层 / 用例层
|
||||||
|
|
||||||
|
典型形态:
|
||||||
|
|
||||||
|
- service
|
||||||
|
- use case
|
||||||
|
- action
|
||||||
|
- controller hook
|
||||||
|
- page model
|
||||||
|
- workflow
|
||||||
|
|
||||||
|
职责:
|
||||||
|
|
||||||
|
- 表达一个明确业务流程
|
||||||
|
- 协调多个依赖
|
||||||
|
- 承载事务边界、步骤顺序、状态推进
|
||||||
|
- 组织权限判断、前置校验、错误分支
|
||||||
|
|
||||||
|
要求:
|
||||||
|
|
||||||
|
- 一个模块只负责一个业务域或一个稳定子流程
|
||||||
|
- 可以依赖基础设施接口,但不应把具体协议细节暴露给上层
|
||||||
|
- 可以包含少量私有 helper,但 helper 仅服务当前主题
|
||||||
|
|
||||||
|
### 2.3 领域规则层
|
||||||
|
|
||||||
|
典型形态:
|
||||||
|
|
||||||
|
- domain service
|
||||||
|
- policy
|
||||||
|
- rule
|
||||||
|
- validator
|
||||||
|
- entity behavior
|
||||||
|
- pure business helpers
|
||||||
|
|
||||||
|
职责:
|
||||||
|
|
||||||
|
- 承载稳定的业务规则和领域语义
|
||||||
|
- 保持尽量纯净、可测试、与外部协议解耦
|
||||||
|
- 统一业务概念、状态转换、派生计算
|
||||||
|
|
||||||
|
要求:
|
||||||
|
|
||||||
|
- 不直接访问数据库、网络、文件、浏览器环境
|
||||||
|
- 不依赖 UI、HTTP、CLI、消息队列等入口协议
|
||||||
|
|
||||||
|
### 2.4 基础设施层 / 数据访问层
|
||||||
|
|
||||||
|
典型形态:
|
||||||
|
|
||||||
|
- repository
|
||||||
|
- gateway
|
||||||
|
- API client
|
||||||
|
- storage adapter
|
||||||
|
- cache adapter
|
||||||
|
- filesystem adapter
|
||||||
|
- persistence implementation
|
||||||
|
|
||||||
|
职责:
|
||||||
|
|
||||||
|
- 封装外部系统细节
|
||||||
|
- 处理数据库、缓存、HTTP、对象存储、消息队列、浏览器存储、操作系统能力
|
||||||
|
- 提供可复用的边界接口
|
||||||
|
|
||||||
|
要求:
|
||||||
|
|
||||||
|
- 只解决“怎么接外部系统”,不承担业务决策
|
||||||
|
- 协议转换、序列化、连接管理、重试策略等应在此层收口
|
||||||
|
|
||||||
|
### 2.5 共享基础层
|
||||||
|
|
||||||
|
典型形态:
|
||||||
|
|
||||||
|
- constants
|
||||||
|
- shared types
|
||||||
|
- date / string / number helpers
|
||||||
|
- 通用 UI 基础组件
|
||||||
|
- 通用错误定义
|
||||||
|
|
||||||
|
要求:
|
||||||
|
|
||||||
|
- 必须是真正跨域、稳定、低语义耦合的内容
|
||||||
|
- 不允许把业务逻辑伪装成“common / shared / utils”
|
||||||
|
- 一旦某模块开始依赖特定业务名词,它就不再是共享基础层
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 目录与模块组织规范
|
||||||
|
|
||||||
|
### 3.1 允许的组织方式
|
||||||
|
|
||||||
|
项目可以采用以下任一方式:
|
||||||
|
|
||||||
|
- 按领域优先组织:`<domain>/<entry|application|infra|shared>`
|
||||||
|
- 按层优先组织:`entry/ application/ domain/ infra/`
|
||||||
|
- 混合组织:顶层按领域,领域内再分层
|
||||||
|
- Monorepo 组织:`apps/ packages/ services/ workers/`
|
||||||
|
|
||||||
|
允许多种组织方式并存,但必须满足:
|
||||||
|
|
||||||
|
- 同一仓库内的同类代码遵循一致的判断逻辑
|
||||||
|
- 每个模块都能被映射到明确职责层
|
||||||
|
- 依赖方向清晰、稳定、可审计
|
||||||
|
|
||||||
|
### 3.2 推荐的判断方式
|
||||||
|
|
||||||
|
当你不确定某段代码应该放哪里时,按顺序判断:
|
||||||
|
|
||||||
|
1. 它是在接收输入、渲染输出、还是拼装启动吗
|
||||||
|
2. 它是在表达一个完整业务流程吗
|
||||||
|
3. 它是在表达不依赖外部协议的业务规则吗
|
||||||
|
4. 它是在接数据库、文件、网络、缓存、浏览器或系统能力吗
|
||||||
|
5. 它真的是跨域共享能力吗
|
||||||
|
|
||||||
|
### 3.3 一个模块只能有一个主语义
|
||||||
|
|
||||||
|
- 一个模块可以有多个函数,但只能服务一个主职责。
|
||||||
|
- 如果一个文件既是“页面”又是“API 聚合器”又是“缓存控制器”又是“视图组件”,就已经越界。
|
||||||
|
- 如果一个 router 文件开始长时间停留在 SQL、文件读写、事务与状态轮询细节上,也已经越界。
|
||||||
|
|
||||||
|
### 3.4 兼容层与过渡层
|
||||||
|
|
||||||
|
- 允许存在短期兼容层、导出层、适配层。
|
||||||
|
- 兼容层必须被明确标记为过渡用途。
|
||||||
|
- 禁止长期把新逻辑继续堆回兼容层。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 前端通用规范
|
||||||
|
|
||||||
|
本节适用于 Web、桌面端、移动端和前端壳应用,不绑定 React / Vue / Svelte 等具体框架。
|
||||||
|
|
||||||
|
### 4.1 页面/路由入口必须薄
|
||||||
|
|
||||||
|
- 页面文件默认负责页面装配、布局组织、边界兜底。
|
||||||
|
- 页面可持有少量与页面展示强绑定的状态。
|
||||||
|
- 当页面开始同时承担以下两项及以上时,应拆出页面级编排模块:
|
||||||
|
- 多个接口请求
|
||||||
|
- 轮询或定时器
|
||||||
|
- 上传/下载流程
|
||||||
|
- 多个 Drawer / Modal / Sheet 子流程
|
||||||
|
- 复杂权限判断
|
||||||
|
- 大量数据清洗与派生
|
||||||
|
|
||||||
|
### 4.2 视图组件默认无副作用
|
||||||
|
|
||||||
|
- 纯视图组件只接收整理好的 props。
|
||||||
|
- 纯视图组件默认不直接请求接口、不直接碰浏览器存储、不直接起轮询。
|
||||||
|
- 如果一个组件必须自带数据流程,它应被明确视为“功能组件”或“场景组件”,而不是伪装成通用组件。
|
||||||
|
|
||||||
|
### 4.3 页面级业务流程应集中编排
|
||||||
|
|
||||||
|
- 页面相关的请求、轮询、草稿保存、上传进度、权限行为、状态联动,应尽量集中在页面编排层。
|
||||||
|
- 页面编排层可以是 hook、store、controller、presenter 或 view-model,不限定技术名词。
|
||||||
|
- 不要求为了形式而一律抽 hook;只有在页面入口已经承担过多流程时才拆。
|
||||||
|
|
||||||
|
### 4.4 前端基础设施应收口
|
||||||
|
|
||||||
|
- API client
|
||||||
|
- 本地存储
|
||||||
|
- Session / token 持久化
|
||||||
|
- 浏览器标题、副作用事件、定时器策略
|
||||||
|
- 配置读取
|
||||||
|
|
||||||
|
以上能力应收口在可识别模块中,不应被页面随机复制。
|
||||||
|
|
||||||
|
### 4.5 复用原则
|
||||||
|
|
||||||
|
- 提炼稳定复用模式,不提炼偶然重复。
|
||||||
|
- 三处以上重复,优先评估抽取。
|
||||||
|
- 如果抽取后的接口比原地代码更难理解,不应抽取。
|
||||||
|
- 不允许制造“只有一个页面使用、但包装层很多”的伪复用。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 后端通用规范
|
||||||
|
|
||||||
|
本节适用于 HTTP 服务、RPC 服务、任务处理器和后台作业,不绑定 FastAPI / Spring / NestJS / Gin 等框架。
|
||||||
|
|
||||||
|
### 5.1 启动入口必须只做装配
|
||||||
|
|
||||||
|
- `main`
|
||||||
|
- app factory
|
||||||
|
- bootstrap
|
||||||
|
- container
|
||||||
|
|
||||||
|
这些入口只负责:
|
||||||
|
|
||||||
|
- 创建应用实例
|
||||||
|
- 注册路由/处理器
|
||||||
|
- 初始化中间件
|
||||||
|
- 装配依赖
|
||||||
|
- 生命周期绑定
|
||||||
|
|
||||||
|
不应承担:
|
||||||
|
|
||||||
|
- 业务规则
|
||||||
|
- SQL 或 ORM 编排
|
||||||
|
- 文件系统细节
|
||||||
|
- 长流程任务控制
|
||||||
|
|
||||||
|
### 5.2 Router / Controller / Handler 只做协议转换
|
||||||
|
|
||||||
|
允许:
|
||||||
|
|
||||||
|
- 接收请求参数
|
||||||
|
- 基础校验
|
||||||
|
- 调用用例层
|
||||||
|
- 将领域错误映射为接口错误
|
||||||
|
|
||||||
|
不允许:
|
||||||
|
|
||||||
|
- 在 handler 内直接堆大量 SQL
|
||||||
|
- 一边处理权限,一边处理事务,一边操作文件,一边组装响应模型
|
||||||
|
- 在 handler 内实现长流程状态机
|
||||||
|
|
||||||
|
### 5.3 Service / Use Case 以业务域组织
|
||||||
|
|
||||||
|
- 一个 service 文件只负责一个业务域或一个稳定子主题。
|
||||||
|
- 同域内的查询、写入、校验、少量派生逻辑可以在一起。
|
||||||
|
- 如果一个 service 同时承担多个主题,应优先拆主题而不是拆技术动作。
|
||||||
|
|
||||||
|
### 5.4 数据访问与外部适配要收口
|
||||||
|
|
||||||
|
- 数据库查询
|
||||||
|
- ORM 组装
|
||||||
|
- 缓存细节
|
||||||
|
- 第三方 HTTP 调用
|
||||||
|
- 文件上传下载
|
||||||
|
- 对象存储
|
||||||
|
- 队列/任务系统
|
||||||
|
|
||||||
|
这些细节应尽量沉到 repository / gateway / adapter / infra 中。
|
||||||
|
|
||||||
|
### 5.5 Schema / DTO / Contract 必须纯净
|
||||||
|
|
||||||
|
- DTO 只用于定义契约,不应携带数据库、文件系统、网络调用或业务副作用。
|
||||||
|
- 契约字段演进必须可追踪。
|
||||||
|
- 避免让数据库模型、接口模型、领域模型长期混成一种结构。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. CLI / 脚本 / Worker 规范
|
||||||
|
|
||||||
|
- 命令入口只负责解析参数、准备依赖、调用用例。
|
||||||
|
- 脚本如果会长期保留,必须从“一次性脚本”升级为可读的结构化模块。
|
||||||
|
- Worker handler 只负责接收消息、提取 payload、调用业务流程、回写状态。
|
||||||
|
- 重试、幂等、死信、超时策略等运行时策略应有单独归属,不应散落在业务逻辑内部。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 拆分与合并准则
|
||||||
|
|
||||||
|
### 7.1 何时应拆分
|
||||||
|
|
||||||
|
满足任一项即可考虑拆分:
|
||||||
|
|
||||||
|
- 同一模块出现多个业务主题
|
||||||
|
- 同一模块同时依赖多种外部系统
|
||||||
|
- 同一模块同时承担输入解析、业务编排、持久化和展示
|
||||||
|
- 多人修改时经常产生冲突
|
||||||
|
- 阅读一个改动需要频繁跨越无关上下文
|
||||||
|
- 相同规则被复制到多个入口
|
||||||
|
|
||||||
|
### 7.2 何时不应拆分
|
||||||
|
|
||||||
|
- 仍是单一主题
|
||||||
|
- 代码虽长但顺序可读
|
||||||
|
- 继续拆只会制造纯转发层
|
||||||
|
- 继续拆会让调用链更深、定位更慢
|
||||||
|
- 抽出后接口语义比原代码更含糊
|
||||||
|
|
||||||
|
### 7.3 合并也是一种优化
|
||||||
|
|
||||||
|
- 如果多个模块只是在相互转发、没有独立语义,应考虑合并。
|
||||||
|
- 如果拆分之后需要同时打开 4 到 6 个文件才能理解一个简单流程,通常已经过度拆分。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 命名与依赖规则
|
||||||
|
|
||||||
|
### 8.1 命名规则
|
||||||
|
|
||||||
|
- 名称应表达业务主题或技术边界,不表达情绪和历史包袱。
|
||||||
|
- 优先使用“对象 + 语义”命名,而不是“抽象 + 序号”命名。
|
||||||
|
- 避免模糊后缀:
|
||||||
|
- `handler2`
|
||||||
|
- `newService`
|
||||||
|
- `commonUtils`
|
||||||
|
- `tempPage`
|
||||||
|
|
||||||
|
### 8.2 依赖规则
|
||||||
|
|
||||||
|
- 上层可以依赖下层抽象,不应依赖下层杂乱细节。
|
||||||
|
- 共享层不能反向依赖业务层。
|
||||||
|
- 领域模块之间若需协作,应通过明确用例、接口或边界对象完成,而不是互相穿透内部实现。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 测试与验证要求
|
||||||
|
|
||||||
|
### 9.1 结构改动后的默认验证
|
||||||
|
|
||||||
|
- 前端结构改动后,至少执行构建或类型校验。
|
||||||
|
- 后端结构改动后,至少执行语法校验、启动校验或最小测试集。
|
||||||
|
- 如果改动涉及契约、权限、状态流转、持久化边界,应追加针对性验证。
|
||||||
|
|
||||||
|
### 9.2 测试优先级
|
||||||
|
|
||||||
|
- 先保关键业务路径
|
||||||
|
- 再保跨层边界
|
||||||
|
- 再保复杂状态流转
|
||||||
|
- 最后补充纯工具覆盖
|
||||||
|
|
||||||
|
### 9.3 文档同步要求
|
||||||
|
|
||||||
|
以下情况必须同步设计文档或架构说明:
|
||||||
|
|
||||||
|
- 新增一层明确职责边界
|
||||||
|
- 新增一个稳定领域模块模板
|
||||||
|
- 改变入口层、用例层、基础设施层的责任划分
|
||||||
|
- 引入新的运行时或新的跨项目复用规范
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. 评审与审计清单
|
||||||
|
|
||||||
|
做结构评审时,默认检查以下问题:
|
||||||
|
|
||||||
|
### 10.1 入口层
|
||||||
|
|
||||||
|
- 入口是否足够薄
|
||||||
|
- 是否混入业务规则
|
||||||
|
- 是否混入外部系统细节
|
||||||
|
|
||||||
|
### 10.2 业务编排层
|
||||||
|
|
||||||
|
- 是否以业务主题组织
|
||||||
|
- 是否承担了过多无关流程
|
||||||
|
- 是否存在纯转发服务
|
||||||
|
|
||||||
|
### 10.3 领域规则层
|
||||||
|
|
||||||
|
- 是否仍保持纯净
|
||||||
|
- 是否被框架、HTTP、数据库协议污染
|
||||||
|
|
||||||
|
### 10.4 基础设施层
|
||||||
|
|
||||||
|
- 副作用是否收口
|
||||||
|
- 是否把业务规则偷偷塞回 adapter / utils / core
|
||||||
|
|
||||||
|
### 10.5 共享层
|
||||||
|
|
||||||
|
- 是否真的跨域复用
|
||||||
|
- 是否把业务逻辑伪装成 common / shared / utils
|
||||||
|
|
||||||
|
### 10.6 演进风险
|
||||||
|
|
||||||
|
- 新增功能是否沿着既有边界落位
|
||||||
|
- 兼容层是否在持续变厚
|
||||||
|
- 是否出现单文件多职责继续膨胀
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. 禁止事项
|
||||||
|
|
||||||
|
- 禁止为了图省事把新逻辑堆回入口层
|
||||||
|
- 禁止为了“文件更短”制造无语义的纯包装层
|
||||||
|
- 禁止在 `utils` / `common` / `shared` 中隐藏领域逻辑
|
||||||
|
- 禁止让页面、路由、handler 直接承载大量持久化或文件系统细节
|
||||||
|
- 禁止把 schema / DTO / config 当成业务逻辑容器
|
||||||
|
- 禁止一次改动里同时重写结构、协议、UI 和业务行为,且没有明确验证策略
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. 执行基线
|
||||||
|
|
||||||
|
后续所有新增功能、重构与代码审计,均以本文档为默认判断依据:
|
||||||
|
|
||||||
|
- 先判断职责边界是否正确
|
||||||
|
- 再判断依赖方向是否健康
|
||||||
|
- 再判断是否需要拆分或合并
|
||||||
|
- 最后才考虑目录美观、文件长短和风格一致性
|
||||||
|
|
||||||
|
当“看起来更模块化”和“真实可读、可改、可验证”发生冲突时,优先后者。
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>iMeeting - 智能会议助手</title>
|
<title>智听云平台</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import AccountSettings from './pages/AccountSettings';
|
||||||
import MeetingCenterPage from './pages/MeetingCenterPage';
|
import MeetingCenterPage from './pages/MeetingCenterPage';
|
||||||
import MainLayout from './components/MainLayout';
|
import MainLayout from './components/MainLayout';
|
||||||
import menuService from './services/menuService';
|
import menuService from './services/menuService';
|
||||||
|
import configService from './utils/configService';
|
||||||
import './App.css';
|
import './App.css';
|
||||||
import './styles/console-theme.css';
|
import './styles/console-theme.css';
|
||||||
|
|
||||||
|
|
@ -106,6 +107,20 @@ function App() {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let active = true;
|
||||||
|
|
||||||
|
configService.getBrandingConfig().then((branding) => {
|
||||||
|
if (active && branding?.app_name) {
|
||||||
|
document.title = branding.app_name;
|
||||||
|
}
|
||||||
|
}).catch(() => {});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
active = false;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleLogin = (authData) => {
|
const handleLogin = (authData) => {
|
||||||
if (authData) {
|
if (authData) {
|
||||||
menuService.clearCache();
|
menuService.clearCache();
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import React, { useState } from 'react';
|
import React from 'react';
|
||||||
import { Input } from 'antd';
|
import { Input } from 'antd';
|
||||||
import { SearchOutlined } from '@ant-design/icons';
|
|
||||||
|
|
||||||
const ExpandSearchBox = ({ onSearch, placeholder = "搜索会议..." }) => {
|
const ExpandSearchBox = ({ onSearch, placeholder = "搜索会议..." }) => {
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -105,7 +105,7 @@ const MainLayout = ({ children, user, onLogout }) => {
|
||||||
return roots
|
return roots
|
||||||
.sort((a, b) => (a.sort_order || 0) - (b.sort_order || 0))
|
.sort((a, b) => (a.sort_order || 0) - (b.sort_order || 0))
|
||||||
.map(toMenuItem);
|
.map(toMenuItem);
|
||||||
}, [navigate, onLogout, userMenus]);
|
}, [navigate, userMenus]);
|
||||||
|
|
||||||
const flatMenuKeys = useMemo(() => {
|
const flatMenuKeys = useMemo(() => {
|
||||||
const keys = [];
|
const keys = [];
|
||||||
|
|
|
||||||
|
|
@ -17,27 +17,27 @@ const MarkdownRenderer = ({ content, className = "", emptyMessage = "暂无内
|
||||||
remarkPlugins={[remarkGfm]}
|
remarkPlugins={[remarkGfm]}
|
||||||
rehypePlugins={[rehypeRaw]}
|
rehypePlugins={[rehypeRaw]}
|
||||||
components={{
|
components={{
|
||||||
h1: ({node, ...props}) => <Typography.Title level={1} style={{ marginTop: 24 }} {...props} />,
|
h1: (props) => <Typography.Title level={1} style={{ marginTop: 24 }} {...props} />,
|
||||||
h2: ({node, ...props}) => <Typography.Title level={2} style={{ marginTop: 20 }} {...props} />,
|
h2: (props) => <Typography.Title level={2} style={{ marginTop: 20 }} {...props} />,
|
||||||
h3: ({node, ...props}) => <Typography.Title level={3} style={{ marginTop: 16 }} {...props} />,
|
h3: (props) => <Typography.Title level={3} style={{ marginTop: 16 }} {...props} />,
|
||||||
h4: ({node, ...props}) => <Typography.Title level={4} style={{ marginTop: 12 }} {...props} />,
|
h4: (props) => <Typography.Title level={4} style={{ marginTop: 12 }} {...props} />,
|
||||||
p: ({node, ...props}) => <Paragraph style={{ marginBottom: 16 }} {...props} />,
|
p: (props) => <Paragraph style={{ marginBottom: 16 }} {...props} />,
|
||||||
blockquote: ({node, ...props}) => (
|
blockquote: (props) => (
|
||||||
<blockquote style={{
|
<blockquote style={{
|
||||||
margin: '12px 0', padding: '8px 16px',
|
margin: '12px 0', padding: '8px 16px',
|
||||||
borderLeft: '4px solid #1677ff', background: '#f0f5ff',
|
borderLeft: '4px solid #1677ff', background: '#f0f5ff',
|
||||||
borderRadius: '0 8px 8px 0', color: '#444',
|
borderRadius: '0 8px 8px 0', color: '#444',
|
||||||
}} {...props} />
|
}} {...props} />
|
||||||
),
|
),
|
||||||
li: ({node, ...props}) => <li style={{ marginBottom: 8 }} {...props} />,
|
li: (props) => <li style={{ marginBottom: 8 }} {...props} />,
|
||||||
ul: ({node, ...props}) => <ul style={{ paddingLeft: 24, marginBottom: 16 }} {...props} />,
|
ul: (props) => <ul style={{ paddingLeft: 24, marginBottom: 16 }} {...props} />,
|
||||||
ol: ({node, ...props}) => <ol style={{ paddingLeft: 24, marginBottom: 16 }} {...props} />,
|
ol: (props) => <ol style={{ paddingLeft: 24, marginBottom: 16 }} {...props} />,
|
||||||
hr: ({node, ...props}) => <hr style={{ border: 'none', borderTop: '1px solid #e5e7eb', margin: '20px 0' }} {...props} />,
|
hr: (props) => <hr style={{ border: 'none', borderTop: '1px solid #e5e7eb', margin: '20px 0' }} {...props} />,
|
||||||
strong: ({node, ...props}) => <strong style={{ fontWeight: 600 }} {...props} />,
|
strong: (props) => <strong style={{ fontWeight: 600 }} {...props} />,
|
||||||
table: ({node, ...props}) => <table style={{ borderCollapse: 'collapse', width: '100%', marginBottom: 16 }} {...props} />,
|
table: (props) => <table style={{ borderCollapse: 'collapse', width: '100%', marginBottom: 16 }} {...props} />,
|
||||||
th: ({node, ...props}) => <th style={{ border: '1px solid #d9d9d9', padding: '8px 12px', background: '#f5f5f5', fontWeight: 600 }} {...props} />,
|
th: (props) => <th style={{ border: '1px solid #d9d9d9', padding: '8px 12px', background: '#f5f5f5', fontWeight: 600 }} {...props} />,
|
||||||
td: ({node, ...props}) => <td style={{ border: '1px solid #d9d9d9', padding: '8px 12px' }} {...props} />,
|
td: (props) => <td style={{ border: '1px solid #d9d9d9', padding: '8px 12px' }} {...props} />,
|
||||||
code: ({node, inline, className, ...props}) => {
|
code: ({ inline, className, ...props }) => {
|
||||||
if (inline) {
|
if (inline) {
|
||||||
return <code style={{ background: '#f5f5f5', padding: '2px 6px', borderRadius: 4, fontSize: '0.9em', color: '#d63384' }} {...props} />;
|
return <code style={{ background: '#f5f5f5', padding: '2px 6px', borderRadius: 4, fontSize: '0.9em', color: '#d63384' }} {...props} />;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
import { Drawer, Form, Input, Button, DatePicker, Select, Space, App, Upload, Card, Progress, Typography } from 'antd';
|
import { Drawer, Form, Input, Button, DatePicker, Select, Space, App, Upload, Card, Progress, Typography } from 'antd';
|
||||||
import { SaveOutlined, UploadOutlined, DeleteOutlined, AudioOutlined } from '@ant-design/icons';
|
import { SaveOutlined, UploadOutlined, DeleteOutlined, AudioOutlined } from '@ant-design/icons';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
|
@ -13,7 +13,6 @@ const MeetingFormDrawer = ({ open, onClose, onSuccess, meetingId = null }) => {
|
||||||
const { message } = App.useApp();
|
const { message } = App.useApp();
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [fetching, setFetching] = useState(false);
|
|
||||||
const [users, setUsers] = useState([]);
|
const [users, setUsers] = useState([]);
|
||||||
const [prompts, setPrompts] = useState([]);
|
const [prompts, setPrompts] = useState([]);
|
||||||
const [selectedAudioFile, setSelectedAudioFile] = useState(null);
|
const [selectedAudioFile, setSelectedAudioFile] = useState(null);
|
||||||
|
|
@ -24,6 +23,44 @@ const MeetingFormDrawer = ({ open, onClose, onSuccess, meetingId = null }) => {
|
||||||
|
|
||||||
const isEdit = Boolean(meetingId);
|
const isEdit = Boolean(meetingId);
|
||||||
|
|
||||||
|
const fetchOptions = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const [uRes, pRes] = await Promise.all([
|
||||||
|
apiClient.get(buildApiUrl(API_ENDPOINTS.USERS.LIST)),
|
||||||
|
apiClient.get(buildApiUrl(API_ENDPOINTS.PROMPTS.ACTIVE('MEETING_TASK'))),
|
||||||
|
]);
|
||||||
|
setUsers(uRes.data.users || []);
|
||||||
|
setPrompts(pRes.data.prompts || []);
|
||||||
|
} catch {
|
||||||
|
message.error('加载会议表单选项失败');
|
||||||
|
}
|
||||||
|
}, [message]);
|
||||||
|
|
||||||
|
const loadAudioUploadConfig = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const nextMaxAudioSize = await configService.getMaxAudioSize();
|
||||||
|
setMaxAudioSize(nextMaxAudioSize || 100 * 1024 * 1024);
|
||||||
|
} catch {
|
||||||
|
setMaxAudioSize(100 * 1024 * 1024);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const fetchMeeting = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const res = await apiClient.get(buildApiUrl(API_ENDPOINTS.MEETINGS.DETAIL(meetingId)));
|
||||||
|
const meeting = res.data;
|
||||||
|
form.setFieldsValue({
|
||||||
|
title: meeting.title,
|
||||||
|
meeting_time: dayjs(meeting.meeting_time),
|
||||||
|
attendee_ids: meeting.attendee_ids || meeting.attendees?.map((a) => a.user_id).filter(Boolean) || [],
|
||||||
|
prompt_id: meeting.prompt_id,
|
||||||
|
tags: meeting.tags?.map((t) => t.name) || [],
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
message.error('加载会议数据失败');
|
||||||
|
}
|
||||||
|
}, [form, meetingId, message]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open) return;
|
if (!open) return;
|
||||||
fetchOptions();
|
fetchOptions();
|
||||||
|
|
@ -38,46 +75,7 @@ const MeetingFormDrawer = ({ open, onClose, onSuccess, meetingId = null }) => {
|
||||||
setAudioUploadProgress(0);
|
setAudioUploadProgress(0);
|
||||||
setAudioUploadMessage('');
|
setAudioUploadMessage('');
|
||||||
}
|
}
|
||||||
}, [open, meetingId]);
|
}, [fetchMeeting, fetchOptions, form, isEdit, loadAudioUploadConfig, open]);
|
||||||
|
|
||||||
const fetchOptions = async () => {
|
|
||||||
try {
|
|
||||||
const [uRes, pRes] = await Promise.all([
|
|
||||||
apiClient.get(buildApiUrl(API_ENDPOINTS.USERS.LIST)),
|
|
||||||
apiClient.get(buildApiUrl(API_ENDPOINTS.PROMPTS.ACTIVE('MEETING_TASK'))),
|
|
||||||
]);
|
|
||||||
setUsers(uRes.data.users || []);
|
|
||||||
setPrompts(pRes.data.prompts || []);
|
|
||||||
} catch {}
|
|
||||||
};
|
|
||||||
|
|
||||||
const loadAudioUploadConfig = async () => {
|
|
||||||
try {
|
|
||||||
const nextMaxAudioSize = await configService.getMaxAudioSize();
|
|
||||||
setMaxAudioSize(nextMaxAudioSize || 100 * 1024 * 1024);
|
|
||||||
} catch (error) {
|
|
||||||
setMaxAudioSize(100 * 1024 * 1024);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchMeeting = async () => {
|
|
||||||
setFetching(true);
|
|
||||||
try {
|
|
||||||
const res = await apiClient.get(buildApiUrl(API_ENDPOINTS.MEETINGS.DETAIL(meetingId)));
|
|
||||||
const meeting = res.data;
|
|
||||||
form.setFieldsValue({
|
|
||||||
title: meeting.title,
|
|
||||||
meeting_time: dayjs(meeting.meeting_time),
|
|
||||||
attendee_ids: meeting.attendee_ids || meeting.attendees?.map((a) => a.user_id).filter(Boolean) || [],
|
|
||||||
prompt_id: meeting.prompt_id,
|
|
||||||
tags: meeting.tags?.map((t) => t.name) || [],
|
|
||||||
});
|
|
||||||
} catch {
|
|
||||||
message.error('加载会议数据失败');
|
|
||||||
} finally {
|
|
||||||
setFetching(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleAudioBeforeUpload = (file) => {
|
const handleAudioBeforeUpload = (file) => {
|
||||||
const validationMessage = validateMeetingAudioFile(file, maxAudioSize, configService.formatFileSize.bind(configService));
|
const validationMessage = validateMeetingAudioFile(file, maxAudioSize, configService.formatFileSize.bind(configService));
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ const hasRenderableSize = (element) => {
|
||||||
return Number.isFinite(rect.width) && Number.isFinite(rect.height) && rect.width > 0 && rect.height > 0;
|
return Number.isFinite(rect.width) && Number.isFinite(rect.height) && rect.width > 0 && rect.height > 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
const MindMap = ({ content, title }) => {
|
const MindMap = ({ content }) => {
|
||||||
const svgRef = useRef(null);
|
const svgRef = useRef(null);
|
||||||
const markmapRef = useRef(null);
|
const markmapRef = useRef(null);
|
||||||
const latestRootRef = useRef(null);
|
const latestRootRef = useRef(null);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useState, useEffect } from 'react';
|
import React from 'react';
|
||||||
import { FloatButton } from 'antd';
|
import { FloatButton } from 'antd';
|
||||||
import { VerticalAlignTopOutlined } from '@ant-design/icons';
|
import { VerticalAlignTopOutlined } from '@ant-design/icons';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
import { Tag, Space, Typography, Skeleton, Empty } from 'antd';
|
import { Tag, Space, Typography, Skeleton, Empty } from 'antd';
|
||||||
import { TagsOutlined } from '@ant-design/icons';
|
import { TagsOutlined } from '@ant-design/icons';
|
||||||
import apiClient from '../utils/apiClient';
|
import apiClient from '../utils/apiClient';
|
||||||
|
|
@ -16,11 +16,7 @@ const TagCloud = ({
|
||||||
const [allTags, setAllTags] = useState([]);
|
const [allTags, setAllTags] = useState([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
const fetchAllTags = useCallback(async () => {
|
||||||
fetchAllTags();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const fetchAllTags = async () => {
|
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.TAGS.LIST));
|
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.TAGS.LIST));
|
||||||
|
|
@ -31,7 +27,11 @@ const TagCloud = ({
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
}, [limitTags]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchAllTags();
|
||||||
|
}, [fetchAllTags]);
|
||||||
|
|
||||||
const handleTagClick = (tag) => {
|
const handleTagClick = (tag) => {
|
||||||
if (onTagClick) {
|
if (onTagClick) {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useState, useEffect, useRef } from 'react';
|
import React, { useState, useRef } from 'react';
|
||||||
import { Modal, Button, Typography, Space, Progress, App, Card, Alert } from 'antd';
|
import { Modal, Button, Typography, Space, Progress, App, Card, Alert } from 'antd';
|
||||||
import {
|
import {
|
||||||
AudioOutlined,
|
AudioOutlined,
|
||||||
|
|
@ -50,7 +50,7 @@ const VoiceprintCollectionModal = ({ isOpen, onClose, onSuccess, templateConfig
|
||||||
stopRecording();
|
stopRecording();
|
||||||
}
|
}
|
||||||
}, 100);
|
}, 100);
|
||||||
} catch (err) {
|
} catch {
|
||||||
message.error('无法访问麦克风,请检查权限设置');
|
message.error('无法访问麦克风,请检查权限设置');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -71,7 +71,7 @@ const VoiceprintCollectionModal = ({ isOpen, onClose, onSuccess, templateConfig
|
||||||
await onSuccess(formData);
|
await onSuccess(formData);
|
||||||
message.success('声纹采集成功');
|
message.success('声纹采集成功');
|
||||||
onClose();
|
onClose();
|
||||||
} catch (err) {
|
} catch {
|
||||||
message.error('上传失败,请重试');
|
message.error('上传失败,请重试');
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import configService from '../utils/configService';
|
||||||
|
|
||||||
|
const normalizePageSize = (value, fallback) => {
|
||||||
|
const numericValue = Number(value);
|
||||||
|
if (!Number.isFinite(numericValue) || numericValue <= 0) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
return Math.min(100, Math.max(5, Math.floor(numericValue)));
|
||||||
|
};
|
||||||
|
|
||||||
|
const useSystemPageSize = (fallback = 10, options = {}) => {
|
||||||
|
const { suspendUntilReady = false } = options;
|
||||||
|
const [pageSize, setPageSize] = useState(() => {
|
||||||
|
const cachedPageSize = configService.getCachedPageSize();
|
||||||
|
if (cachedPageSize) {
|
||||||
|
return normalizePageSize(cachedPageSize, fallback);
|
||||||
|
}
|
||||||
|
return suspendUntilReady ? null : normalizePageSize(fallback, 10);
|
||||||
|
});
|
||||||
|
const [isReady, setIsReady] = useState(() => pageSize !== null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let active = true;
|
||||||
|
|
||||||
|
configService.getPageSize().then((size) => {
|
||||||
|
if (!active) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setPageSize(normalizePageSize(size, fallback));
|
||||||
|
setIsReady(true);
|
||||||
|
}).catch(() => {
|
||||||
|
if (!active) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setPageSize((prev) => prev ?? normalizePageSize(fallback, 10));
|
||||||
|
setIsReady(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
active = false;
|
||||||
|
};
|
||||||
|
}, [fallback]);
|
||||||
|
|
||||||
|
if (suspendUntilReady) {
|
||||||
|
return { pageSize, isReady };
|
||||||
|
}
|
||||||
|
|
||||||
|
return pageSize ?? normalizePageSize(fallback, 10);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useSystemPageSize;
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
Alert,
|
Alert,
|
||||||
App,
|
App,
|
||||||
|
|
@ -82,13 +82,7 @@ const AccountSettings = ({ user, onUpdateUser }) => {
|
||||||
const [mcpConfig, setMcpConfig] = useState(null);
|
const [mcpConfig, setMcpConfig] = useState(null);
|
||||||
const [mcpError, setMcpError] = useState('');
|
const [mcpError, setMcpError] = useState('');
|
||||||
|
|
||||||
useEffect(() => {
|
const fetchUserData = useCallback(async () => {
|
||||||
if (!user?.user_id) return;
|
|
||||||
fetchUserData();
|
|
||||||
fetchMcpConfig();
|
|
||||||
}, [user?.user_id]);
|
|
||||||
|
|
||||||
const fetchUserData = async () => {
|
|
||||||
try {
|
try {
|
||||||
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.USERS.DETAIL(user.user_id)));
|
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.USERS.DETAIL(user.user_id)));
|
||||||
profileForm.setFieldsValue({
|
profileForm.setFieldsValue({
|
||||||
|
|
@ -101,9 +95,9 @@ const AccountSettings = ({ user, onUpdateUser }) => {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error(error?.response?.data?.message || '获取用户资料失败');
|
message.error(error?.response?.data?.message || '获取用户资料失败');
|
||||||
}
|
}
|
||||||
};
|
}, [message, profileForm, user.user_id]);
|
||||||
|
|
||||||
const fetchMcpConfig = async () => {
|
const fetchMcpConfig = useCallback(async () => {
|
||||||
if (!user?.user_id) return;
|
if (!user?.user_id) return;
|
||||||
|
|
||||||
setMcpLoading(true);
|
setMcpLoading(true);
|
||||||
|
|
@ -117,7 +111,13 @@ const AccountSettings = ({ user, onUpdateUser }) => {
|
||||||
} finally {
|
} finally {
|
||||||
setMcpLoading(false);
|
setMcpLoading(false);
|
||||||
}
|
}
|
||||||
};
|
}, [user?.user_id]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!user?.user_id) return;
|
||||||
|
fetchUserData();
|
||||||
|
fetchMcpConfig();
|
||||||
|
}, [fetchMcpConfig, fetchUserData, user?.user_id]);
|
||||||
|
|
||||||
const handleAvatarBeforeUpload = (file) => {
|
const handleAvatarBeforeUpload = (file) => {
|
||||||
const isAllowedType = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'].includes(file.type);
|
const isAllowedType = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'].includes(file.type);
|
||||||
|
|
@ -193,7 +193,7 @@ const AccountSettings = ({ user, onUpdateUser }) => {
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(value);
|
await navigator.clipboard.writeText(value);
|
||||||
message.success(`${label}已复制`);
|
message.success(`${label}已复制`);
|
||||||
} catch (error) {
|
} catch {
|
||||||
message.error(`${label}复制失败`);
|
message.error(`${label}复制失败`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@
|
||||||
.admin-overview-icon {
|
.admin-overview-icon {
|
||||||
width: 88px;
|
width: 88px;
|
||||||
height: 88px;
|
height: 88px;
|
||||||
|
flex: 0 0 88px;
|
||||||
border-radius: 24px;
|
border-radius: 24px;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
@ -85,15 +86,26 @@
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 14px;
|
gap: 14px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-resource-row {
|
.admin-resource-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 18px 1fr auto;
|
grid-template-columns: 112px 1fr auto;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.admin-resource-label {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
color: #37475f;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
.admin-resource-row .ant-progress {
|
.admin-resource-row .ant-progress {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useState, useEffect, useMemo, useRef } from 'react';
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
Table,
|
Table,
|
||||||
|
|
@ -27,7 +27,7 @@ import {
|
||||||
SearchOutlined,
|
SearchOutlined,
|
||||||
DeploymentUnitOutlined,
|
DeploymentUnitOutlined,
|
||||||
HddOutlined,
|
HddOutlined,
|
||||||
ApartmentOutlined,
|
DashboardOutlined,
|
||||||
FileTextOutlined,
|
FileTextOutlined,
|
||||||
ReloadOutlined,
|
ReloadOutlined,
|
||||||
PauseCircleOutlined,
|
PauseCircleOutlined,
|
||||||
|
|
@ -38,6 +38,7 @@ import {
|
||||||
import apiClient from '../utils/apiClient';
|
import apiClient from '../utils/apiClient';
|
||||||
import { buildApiUrl, API_ENDPOINTS } from '../config/api';
|
import { buildApiUrl, API_ENDPOINTS } from '../config/api';
|
||||||
import ActionButton from '../components/ActionButton';
|
import ActionButton from '../components/ActionButton';
|
||||||
|
import useSystemPageSize from '../hooks/useSystemPageSize';
|
||||||
import './AdminDashboard.css';
|
import './AdminDashboard.css';
|
||||||
|
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
|
|
@ -75,6 +76,7 @@ const AdminDashboard = () => {
|
||||||
|
|
||||||
const [taskType, setTaskType] = useState('all');
|
const [taskType, setTaskType] = useState('all');
|
||||||
const [taskStatus, setTaskStatus] = useState('all');
|
const [taskStatus, setTaskStatus] = useState('all');
|
||||||
|
const pageSize = useSystemPageSize(10);
|
||||||
|
|
||||||
const [autoRefresh, setAutoRefresh] = useState(true);
|
const [autoRefresh, setAutoRefresh] = useState(true);
|
||||||
const [countdown, setCountdown] = useState(AUTO_REFRESH_INTERVAL);
|
const [countdown, setCountdown] = useState(AUTO_REFRESH_INTERVAL);
|
||||||
|
|
@ -90,31 +92,41 @@ const AdminDashboard = () => {
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
const fetchStats = useCallback(async () => {
|
||||||
fetchAllData();
|
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.ADMIN.DASHBOARD_STATS));
|
||||||
|
if (response.code === '200') setStats(response.data);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
const fetchOnlineUsers = useCallback(async () => {
|
||||||
if (!autoRefresh || showMeetingModal) return;
|
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.ADMIN.ONLINE_USERS));
|
||||||
|
if (response.code === '200') setOnlineUsers(response.data.users || []);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const timer = setInterval(() => {
|
const fetchUsersList = useCallback(async () => {
|
||||||
setCountdown((prev) => {
|
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.ADMIN.USER_STATS));
|
||||||
if (prev <= 1) {
|
if (response.code === '200') setUsersList(response.data.users || []);
|
||||||
fetchAllData({ silent: true });
|
}, []);
|
||||||
return AUTO_REFRESH_INTERVAL;
|
|
||||||
}
|
|
||||||
return prev - 1;
|
|
||||||
});
|
|
||||||
}, 1000);
|
|
||||||
|
|
||||||
return () => clearInterval(timer);
|
const fetchTasks = useCallback(async () => {
|
||||||
}, [autoRefresh, showMeetingModal]);
|
try {
|
||||||
|
setTaskLoading(true);
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (taskType !== 'all') params.append('task_type', taskType);
|
||||||
|
if (taskStatus !== 'all') params.append('status', taskStatus);
|
||||||
|
params.append('limit', '20');
|
||||||
|
const response = await apiClient.get(buildApiUrl(`${API_ENDPOINTS.ADMIN.TASKS_MONITOR}?${params.toString()}`));
|
||||||
|
if (response.code === '200') setTasks(response.data.tasks || []);
|
||||||
|
} finally {
|
||||||
|
setTaskLoading(false);
|
||||||
|
}
|
||||||
|
}, [taskStatus, taskType]);
|
||||||
|
|
||||||
useEffect(() => {
|
const fetchResources = useCallback(async () => {
|
||||||
fetchTasks();
|
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.ADMIN.SYSTEM_RESOURCES));
|
||||||
}, [taskType, taskStatus]);
|
if (response.code === '200') setResources(response.data);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const fetchAllData = async ({ silent = false } = {}) => {
|
const fetchAllData = useCallback(async ({ silent = false } = {}) => {
|
||||||
if (inFlightRef.current) return;
|
if (inFlightRef.current) return;
|
||||||
inFlightRef.current = true;
|
inFlightRef.current = true;
|
||||||
|
|
||||||
|
|
@ -138,41 +150,31 @@ const AdminDashboard = () => {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
}, [fetchOnlineUsers, fetchResources, fetchStats, fetchTasks, fetchUsersList, message]);
|
||||||
|
|
||||||
const fetchStats = async () => {
|
useEffect(() => {
|
||||||
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.ADMIN.DASHBOARD_STATS));
|
fetchAllData();
|
||||||
if (response.code === '200') setStats(response.data);
|
}, [fetchAllData]);
|
||||||
};
|
|
||||||
|
|
||||||
const fetchOnlineUsers = async () => {
|
useEffect(() => {
|
||||||
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.ADMIN.ONLINE_USERS));
|
if (!autoRefresh || showMeetingModal) return;
|
||||||
if (response.code === '200') setOnlineUsers(response.data.users || []);
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchUsersList = async () => {
|
const timer = setInterval(() => {
|
||||||
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.ADMIN.USER_STATS));
|
setCountdown((prev) => {
|
||||||
if (response.code === '200') setUsersList(response.data.users || []);
|
if (prev <= 1) {
|
||||||
};
|
fetchAllData({ silent: true });
|
||||||
|
return AUTO_REFRESH_INTERVAL;
|
||||||
|
}
|
||||||
|
return prev - 1;
|
||||||
|
});
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
const fetchTasks = async () => {
|
return () => clearInterval(timer);
|
||||||
try {
|
}, [autoRefresh, fetchAllData, showMeetingModal]);
|
||||||
setTaskLoading(true);
|
|
||||||
const params = new URLSearchParams();
|
|
||||||
if (taskType !== 'all') params.append('task_type', taskType);
|
|
||||||
if (taskStatus !== 'all') params.append('status', taskStatus);
|
|
||||||
params.append('limit', '20');
|
|
||||||
const response = await apiClient.get(buildApiUrl(`${API_ENDPOINTS.ADMIN.TASKS_MONITOR}?${params.toString()}`));
|
|
||||||
if (response.code === '200') setTasks(response.data.tasks || []);
|
|
||||||
} finally {
|
|
||||||
setTaskLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchResources = async () => {
|
useEffect(() => {
|
||||||
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.ADMIN.SYSTEM_RESOURCES));
|
fetchTasks();
|
||||||
if (response.code === '200') setResources(response.data);
|
}, [fetchTasks]);
|
||||||
};
|
|
||||||
|
|
||||||
const handleKickUser = (u) => {
|
const handleKickUser = (u) => {
|
||||||
modal.confirm({
|
modal.confirm({
|
||||||
|
|
@ -234,6 +236,33 @@ const AdminDashboard = () => {
|
||||||
return Math.round((completed / all) * 100);
|
return Math.round((completed / all) * 100);
|
||||||
}, [tasks]);
|
}, [tasks]);
|
||||||
|
|
||||||
|
const resourceRows = useMemo(() => ([
|
||||||
|
{
|
||||||
|
key: 'cpu',
|
||||||
|
label: 'CPU',
|
||||||
|
tooltip: resources?.cpu?.count ? `${resources.cpu.count} 核逻辑处理器` : 'CPU 使用率',
|
||||||
|
icon: <DashboardOutlined style={{ color: '#334155' }} />,
|
||||||
|
percent: resources?.cpu?.percent || 0,
|
||||||
|
strokeColor: { from: '#60a5fa', to: '#2563eb' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'memory',
|
||||||
|
label: 'Memory',
|
||||||
|
tooltip: resources?.memory?.total_gb ? `已用 ${resources.memory.used_gb} / ${resources.memory.total_gb} GB` : '内存使用率',
|
||||||
|
icon: <DatabaseOutlined style={{ color: '#334155' }} />,
|
||||||
|
percent: resources?.memory?.percent || 0,
|
||||||
|
strokeColor: { from: '#a78bfa', to: '#7c3aed' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'disk',
|
||||||
|
label: 'Disk',
|
||||||
|
tooltip: resources?.disk?.total_gb ? `已用 ${resources.disk.used_gb} / ${resources.disk.total_gb} GB` : '磁盘使用率',
|
||||||
|
icon: <HddOutlined style={{ color: '#334155' }} />,
|
||||||
|
percent: resources?.disk?.percent || 0,
|
||||||
|
strokeColor: { from: '#818cf8', to: '#6d28d9' },
|
||||||
|
},
|
||||||
|
]), [resources]);
|
||||||
|
|
||||||
const userColumns = [
|
const userColumns = [
|
||||||
{ title: 'ID', dataIndex: 'user_id', key: 'user_id', width: 80 },
|
{ title: 'ID', dataIndex: 'user_id', key: 'user_id', width: 80 },
|
||||||
{ title: '用户名', dataIndex: 'username', key: 'username' },
|
{ title: '用户名', dataIndex: 'username', key: 'username' },
|
||||||
|
|
@ -377,7 +406,7 @@ const AdminDashboard = () => {
|
||||||
<div>
|
<div>
|
||||||
<div className="admin-overview-value compact">{Number(stats?.storage?.audio_total_size_gb || 0).toFixed(2)} GB</div>
|
<div className="admin-overview-value compact">{Number(stats?.storage?.audio_total_size_gb || 0).toFixed(2)} GB</div>
|
||||||
<div className="admin-overview-meta">
|
<div className="admin-overview-meta">
|
||||||
<span>音频文件:{stats?.storage?.audio_file_count || 0} 个</span>
|
<span>音频文件:{stats?.storage?.audio_files_count ?? stats?.storage?.audio_file_count ?? 0} 个</span>
|
||||||
<span>音频目录占用汇总</span>
|
<span>音频目录占用汇总</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -390,21 +419,18 @@ const AdminDashboard = () => {
|
||||||
<div className="admin-overview-main">
|
<div className="admin-overview-main">
|
||||||
<span className="admin-overview-icon resources"><DeploymentUnitOutlined /></span>
|
<span className="admin-overview-icon resources"><DeploymentUnitOutlined /></span>
|
||||||
<div className="admin-resource-list">
|
<div className="admin-resource-list">
|
||||||
<div className="admin-resource-row">
|
{resourceRows.map((resource) => (
|
||||||
<ApartmentOutlined style={{ color: '#334155' }} />
|
<div key={resource.key} className="admin-resource-row">
|
||||||
<Progress percent={resources?.cpu?.percent || 0} size="small" showInfo={false} strokeColor={{ from: '#60a5fa', to: '#2563eb' }} />
|
<Tooltip title={resource.tooltip}>
|
||||||
<span className="admin-resource-value">{formatResourcePercent(resources?.cpu?.percent)}</span>
|
<div className="admin-resource-label">
|
||||||
</div>
|
{resource.icon}
|
||||||
<div className="admin-resource-row">
|
<span>{resource.label}</span>
|
||||||
<DatabaseOutlined style={{ color: '#334155' }} />
|
</div>
|
||||||
<Progress percent={resources?.memory?.percent || 0} size="small" showInfo={false} strokeColor={{ from: '#a78bfa', to: '#7c3aed' }} />
|
</Tooltip>
|
||||||
<span className="admin-resource-value">{formatResourcePercent(resources?.memory?.percent)}</span>
|
<Progress percent={resource.percent} size="small" showInfo={false} strokeColor={resource.strokeColor} />
|
||||||
</div>
|
<span className="admin-resource-value">{formatResourcePercent(resource.percent)}</span>
|
||||||
<div className="admin-resource-row">
|
</div>
|
||||||
<HddOutlined style={{ color: '#334155' }} />
|
))}
|
||||||
<Progress percent={resources?.disk?.percent || 0} size="small" showInfo={false} strokeColor={{ from: '#818cf8', to: '#6d28d9' }} />
|
|
||||||
<span className="admin-resource-value">{formatResourcePercent(resources?.disk?.percent)}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
@ -457,7 +483,7 @@ const AdminDashboard = () => {
|
||||||
columns={taskColumns}
|
columns={taskColumns}
|
||||||
dataSource={tasks}
|
dataSource={tasks}
|
||||||
rowKey={(record) => `${record.task_type}-${record.task_id}`}
|
rowKey={(record) => `${record.task_type}-${record.task_id}`}
|
||||||
pagination={{ pageSize: 6 }}
|
pagination={{ pageSize }}
|
||||||
loading={taskLoading}
|
loading={taskLoading}
|
||||||
scroll={{ x: 760 }}
|
scroll={{ x: 760 }}
|
||||||
/>
|
/>
|
||||||
|
|
@ -472,7 +498,7 @@ const AdminDashboard = () => {
|
||||||
columns={userColumns}
|
columns={userColumns}
|
||||||
dataSource={usersList}
|
dataSource={usersList}
|
||||||
rowKey="user_id"
|
rowKey="user_id"
|
||||||
pagination={{ pageSize: 10 }}
|
pagination={{ pageSize }}
|
||||||
scroll={{ x: 760 }}
|
scroll={{ x: 760 }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -481,7 +507,13 @@ const AdminDashboard = () => {
|
||||||
<Col xs={24} lg={8}>
|
<Col xs={24} lg={8}>
|
||||||
<Card className="console-surface" title={`在线用户 (${onlineUsers.length})`}>
|
<Card className="console-surface" title={`在线用户 (${onlineUsers.length})`}>
|
||||||
<div className="console-table">
|
<div className="console-table">
|
||||||
<Table columns={onlineUserColumns} dataSource={onlineUsers} rowKey="user_id" pagination={false} />
|
<Table
|
||||||
|
columns={onlineUserColumns}
|
||||||
|
dataSource={onlineUsers}
|
||||||
|
rowKey="user_id"
|
||||||
|
pagination={{ pageSize }}
|
||||||
|
scroll={{ x: 420 }}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Divider style={{ margin: '16px 0 12px' }} />
|
<Divider style={{ margin: '16px 0 12px' }} />
|
||||||
<Text type="secondary">在线用户会话可在此直接踢出,立即失效其 Token。</Text>
|
<Text type="secondary">在线用户会话可在此直接踢出,立即失效其 Token。</Text>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useEffect, useMemo, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
Button,
|
Button,
|
||||||
|
|
@ -40,6 +40,7 @@ import apiClient from '../utils/apiClient';
|
||||||
import { buildApiUrl, API_ENDPOINTS } from '../config/api';
|
import { buildApiUrl, API_ENDPOINTS } from '../config/api';
|
||||||
import AdminModuleShell from '../components/AdminModuleShell';
|
import AdminModuleShell from '../components/AdminModuleShell';
|
||||||
import ActionButton from '../components/ActionButton';
|
import ActionButton from '../components/ActionButton';
|
||||||
|
import useSystemPageSize from '../hooks/useSystemPageSize';
|
||||||
|
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
const { TextArea } = Input;
|
const { TextArea } = Input;
|
||||||
|
|
@ -74,16 +75,12 @@ const ClientManagement = () => {
|
||||||
const [activeTab, setActiveTab] = useState('all');
|
const [activeTab, setActiveTab] = useState('all');
|
||||||
const [uploadingFile, setUploadingFile] = useState(false);
|
const [uploadingFile, setUploadingFile] = useState(false);
|
||||||
const [updatingStatusId, setUpdatingStatusId] = useState(null);
|
const [updatingStatusId, setUpdatingStatusId] = useState(null);
|
||||||
|
const pageSize = useSystemPageSize(10);
|
||||||
|
|
||||||
const [platforms, setPlatforms] = useState({ tree: [], items: [] });
|
const [platforms, setPlatforms] = useState({ tree: [], items: [] });
|
||||||
const [platformsMap, setPlatformsMap] = useState({});
|
const [platformsMap, setPlatformsMap] = useState({});
|
||||||
|
|
||||||
useEffect(() => {
|
const fetchPlatforms = useCallback(async () => {
|
||||||
fetchPlatforms();
|
|
||||||
fetchClients();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const fetchPlatforms = async () => {
|
|
||||||
try {
|
try {
|
||||||
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.DICT_DATA.BY_TYPE('client_platform')));
|
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.DICT_DATA.BY_TYPE('client_platform')));
|
||||||
if (response.code === '200') {
|
if (response.code === '200') {
|
||||||
|
|
@ -99,9 +96,9 @@ const ClientManagement = () => {
|
||||||
} catch {
|
} catch {
|
||||||
message.error('获取平台列表失败');
|
message.error('获取平台列表失败');
|
||||||
}
|
}
|
||||||
};
|
}, [message]);
|
||||||
|
|
||||||
const fetchClients = async () => {
|
const fetchClients = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.CLIENT_DOWNLOADS.LIST));
|
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.CLIENT_DOWNLOADS.LIST));
|
||||||
|
|
@ -113,9 +110,17 @@ const ClientManagement = () => {
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
}, [message]);
|
||||||
|
|
||||||
const getPlatformLabel = (platformCode) => platformsMap[platformCode]?.label_cn || platformCode || '-';
|
const getPlatformLabel = useCallback(
|
||||||
|
(platformCode) => platformsMap[platformCode]?.label_cn || platformCode || '-',
|
||||||
|
[platformsMap],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchPlatforms();
|
||||||
|
fetchClients();
|
||||||
|
}, [fetchClients, fetchPlatforms]);
|
||||||
|
|
||||||
const openModal = (client = null) => {
|
const openModal = (client = null) => {
|
||||||
if (client) {
|
if (client) {
|
||||||
|
|
@ -295,7 +300,7 @@ const ClientManagement = () => {
|
||||||
client.download_url,
|
client.download_url,
|
||||||
client.release_notes,
|
client.release_notes,
|
||||||
].some((field) => String(field || '').toLowerCase().includes(query));
|
].some((field) => String(field || '').toLowerCase().includes(query));
|
||||||
}), [activeTab, clients, searchQuery, statusFilter, platformsMap]);
|
}), [activeTab, clients, getPlatformLabel, searchQuery, statusFilter]);
|
||||||
|
|
||||||
const publishedCount = clients.length;
|
const publishedCount = clients.length;
|
||||||
const activeCount = clients.filter((item) => isTruthy(item.is_active)).length;
|
const activeCount = clients.filter((item) => isTruthy(item.is_active)).length;
|
||||||
|
|
@ -448,7 +453,7 @@ const ClientManagement = () => {
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
size="small"
|
size="small"
|
||||||
scroll={{ x: 1040 }}
|
scroll={{ x: 1040 }}
|
||||||
pagination={{ pageSize: 10, showTotal: (total) => `共 ${total} 条记录` }}
|
pagination={{ pageSize, showTotal: (total) => `共 ${total} 条记录` }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</AdminModuleShell>
|
</AdminModuleShell>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
Card, Form, Input, Button, DatePicker, Select,
|
Card, Form, Input, Button, DatePicker, Select,
|
||||||
Typography, App, Divider, Row, Col, Upload, Space, Progress
|
Typography, App, Divider, Row, Col, Upload, Space, Progress
|
||||||
|
|
@ -29,34 +29,38 @@ const CreateMeeting = () => {
|
||||||
const [audioUploadMessage, setAudioUploadMessage] = useState('');
|
const [audioUploadMessage, setAudioUploadMessage] = useState('');
|
||||||
const [maxAudioSize, setMaxAudioSize] = useState(100 * 1024 * 1024);
|
const [maxAudioSize, setMaxAudioSize] = useState(100 * 1024 * 1024);
|
||||||
|
|
||||||
|
const fetchUsers = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const res = await apiClient.get(buildApiUrl(API_ENDPOINTS.USERS.LIST));
|
||||||
|
setUsers(res.data.users || []);
|
||||||
|
} catch {
|
||||||
|
setUsers([]);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const fetchPrompts = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const res = await apiClient.get(buildApiUrl(API_ENDPOINTS.PROMPTS.ACTIVE('MEETING_TASK')));
|
||||||
|
setPrompts(res.data.prompts || []);
|
||||||
|
} catch {
|
||||||
|
setPrompts([]);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const loadAudioUploadConfig = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const nextMaxAudioSize = await configService.getMaxAudioSize();
|
||||||
|
setMaxAudioSize(nextMaxAudioSize || 100 * 1024 * 1024);
|
||||||
|
} catch {
|
||||||
|
setMaxAudioSize(100 * 1024 * 1024);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchUsers();
|
fetchUsers();
|
||||||
fetchPrompts();
|
fetchPrompts();
|
||||||
loadAudioUploadConfig();
|
loadAudioUploadConfig();
|
||||||
}, []);
|
}, [fetchPrompts, fetchUsers, loadAudioUploadConfig]);
|
||||||
|
|
||||||
const fetchUsers = async () => {
|
|
||||||
try {
|
|
||||||
const res = await apiClient.get(buildApiUrl(API_ENDPOINTS.USERS.LIST));
|
|
||||||
setUsers(res.data.users || []);
|
|
||||||
} catch (e) {}
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchPrompts = async () => {
|
|
||||||
try {
|
|
||||||
const res = await apiClient.get(buildApiUrl(API_ENDPOINTS.PROMPTS.ACTIVE('MEETING_TASK')));
|
|
||||||
setPrompts(res.data.prompts || []);
|
|
||||||
} catch (e) {}
|
|
||||||
};
|
|
||||||
|
|
||||||
const loadAudioUploadConfig = async () => {
|
|
||||||
try {
|
|
||||||
const nextMaxAudioSize = await configService.getMaxAudioSize();
|
|
||||||
setMaxAudioSize(nextMaxAudioSize || 100 * 1024 * 1024);
|
|
||||||
} catch (error) {
|
|
||||||
setMaxAudioSize(100 * 1024 * 1024);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleAudioBeforeUpload = (file) => {
|
const handleAudioBeforeUpload = (file) => {
|
||||||
const validationMessage = validateMeetingAudioFile(file, maxAudioSize, configService.formatFileSize.bind(configService));
|
const validationMessage = validateMeetingAudioFile(file, maxAudioSize, configService.formatFileSize.bind(configService));
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useEffect, useMemo, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
App,
|
App,
|
||||||
Avatar,
|
Avatar,
|
||||||
|
|
@ -62,16 +62,6 @@ const Dashboard = ({ user }) => {
|
||||||
const [voiceprintTemplate, setVoiceprintTemplate] = useState(null);
|
const [voiceprintTemplate, setVoiceprintTemplate] = useState(null);
|
||||||
const [voiceprintLoading, setVoiceprintLoading] = useState(true);
|
const [voiceprintLoading, setVoiceprintLoading] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchUserData();
|
|
||||||
fetchMeetingsStats();
|
|
||||||
fetchVoiceprintData();
|
|
||||||
}, [user.user_id]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchMeetings(1, false);
|
|
||||||
}, [selectedTags, filterType, searchQuery]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedTags.length > 0) {
|
if (selectedTags.length > 0) {
|
||||||
setShowTagFilters(true);
|
setShowTagFilters(true);
|
||||||
|
|
@ -120,7 +110,7 @@ const Dashboard = ({ user }) => {
|
||||||
}, {})
|
}, {})
|
||||||
), [meetings]);
|
), [meetings]);
|
||||||
|
|
||||||
const fetchVoiceprintData = async () => {
|
const fetchVoiceprintData = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
setVoiceprintLoading(true);
|
setVoiceprintLoading(true);
|
||||||
const statusResponse = await apiClient.get(buildApiUrl(API_ENDPOINTS.VOICEPRINT.STATUS(user.user_id)));
|
const statusResponse = await apiClient.get(buildApiUrl(API_ENDPOINTS.VOICEPRINT.STATUS(user.user_id)));
|
||||||
|
|
@ -132,9 +122,9 @@ const Dashboard = ({ user }) => {
|
||||||
} finally {
|
} finally {
|
||||||
setVoiceprintLoading(false);
|
setVoiceprintLoading(false);
|
||||||
}
|
}
|
||||||
};
|
}, [user.user_id]);
|
||||||
|
|
||||||
const fetchMeetings = async (page = 1, isLoadMore = false) => {
|
const fetchMeetings = useCallback(async (page = 1, isLoadMore = false) => {
|
||||||
try {
|
try {
|
||||||
const filterKey = meetingCacheService.generateFilterKey(user.user_id, filterType, searchQuery, selectedTags);
|
const filterKey = meetingCacheService.generateFilterKey(user.user_id, filterType, searchQuery, selectedTags);
|
||||||
|
|
||||||
|
|
@ -188,9 +178,9 @@ const Dashboard = ({ user }) => {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
setLoadingMore(false);
|
setLoadingMore(false);
|
||||||
}
|
}
|
||||||
};
|
}, [filterType, message, searchQuery, selectedTags, user.user_id]);
|
||||||
|
|
||||||
const fetchMeetingsStats = async () => {
|
const fetchMeetingsStats = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.MEETINGS.STATS), {
|
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.MEETINGS.STATS), {
|
||||||
params: { user_id: user.user_id },
|
params: { user_id: user.user_id },
|
||||||
|
|
@ -199,16 +189,26 @@ const Dashboard = ({ user }) => {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching meetings stats:', error);
|
console.error('Error fetching meetings stats:', error);
|
||||||
}
|
}
|
||||||
};
|
}, [user.user_id]);
|
||||||
|
|
||||||
const fetchUserData = async () => {
|
const fetchUserData = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const userResponse = await apiClient.get(buildApiUrl(API_ENDPOINTS.USERS.DETAIL(user.user_id)));
|
const userResponse = await apiClient.get(buildApiUrl(API_ENDPOINTS.USERS.DETAIL(user.user_id)));
|
||||||
setUserInfo(userResponse.data);
|
setUserInfo(userResponse.data);
|
||||||
} catch {
|
} catch {
|
||||||
message.error('获取用户信息失败');
|
message.error('获取用户信息失败');
|
||||||
}
|
}
|
||||||
};
|
}, [message, user.user_id]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchUserData();
|
||||||
|
fetchMeetingsStats();
|
||||||
|
fetchVoiceprintData();
|
||||||
|
}, [fetchMeetingsStats, fetchUserData, fetchVoiceprintData]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchMeetings(1, false);
|
||||||
|
}, [fetchMeetings, selectedTags, filterType, searchQuery]);
|
||||||
|
|
||||||
const handleLoadMore = () => {
|
const handleLoadMore = () => {
|
||||||
if (!loadingMore && pagination.has_more) {
|
if (!loadingMore && pagination.has_more) {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
Card, Form, Input, Button, Space, Typography,
|
Card, Form, Input, Button, Space, Typography,
|
||||||
App, Divider, Skeleton
|
App, Divider, Skeleton
|
||||||
|
|
@ -13,7 +13,7 @@ import MarkdownEditor from '../components/MarkdownEditor';
|
||||||
|
|
||||||
const { Title } = Typography;
|
const { Title } = Typography;
|
||||||
|
|
||||||
const EditKnowledgeBase = ({ user }) => {
|
const EditKnowledgeBase = () => {
|
||||||
const { kb_id } = useParams();
|
const { kb_id } = useParams();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { message } = App.useApp();
|
const { message } = App.useApp();
|
||||||
|
|
@ -21,20 +21,20 @@ const EditKnowledgeBase = ({ user }) => {
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
const fetchKbDetail = useCallback(async () => {
|
||||||
fetchKbDetail();
|
|
||||||
}, [kb_id]);
|
|
||||||
|
|
||||||
const fetchKbDetail = async () => {
|
|
||||||
try {
|
try {
|
||||||
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.KNOWLEDGE_BASE.DETAIL(kb_id)));
|
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.KNOWLEDGE_BASE.DETAIL(kb_id)));
|
||||||
form.setFieldsValue(response.data);
|
form.setFieldsValue(response.data);
|
||||||
} catch (error) {
|
} catch {
|
||||||
message.error('加载知识库详情失败');
|
message.error('加载知识库详情失败');
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
}, [form, kb_id, message]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchKbDetail();
|
||||||
|
}, [fetchKbDetail]);
|
||||||
|
|
||||||
const onFinish = async (values) => {
|
const onFinish = async (values) => {
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
|
|
@ -42,7 +42,7 @@ const EditKnowledgeBase = ({ user }) => {
|
||||||
await apiClient.put(buildApiUrl(API_ENDPOINTS.KNOWLEDGE_BASE.UPDATE(kb_id)), values);
|
await apiClient.put(buildApiUrl(API_ENDPOINTS.KNOWLEDGE_BASE.UPDATE(kb_id)), values);
|
||||||
message.success('更新成功');
|
message.success('更新成功');
|
||||||
navigate('/knowledge-base');
|
navigate('/knowledge-base');
|
||||||
} catch (error) {
|
} catch {
|
||||||
message.error('更新失败');
|
message.error('更新失败');
|
||||||
} finally {
|
} finally {
|
||||||
setSaving(false);
|
setSaving(false);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
Card, Form, Input, Button, DatePicker, Select,
|
Card, Form, Input, Button, DatePicker, Select,
|
||||||
Typography, App, Divider, Row, Col
|
Typography, App, Divider, Row, Col
|
||||||
|
|
@ -23,11 +23,7 @@ const EditMeeting = () => {
|
||||||
const [users, setUsers] = useState([]);
|
const [users, setUsers] = useState([]);
|
||||||
const [prompts, setPrompts] = useState([]);
|
const [prompts, setPrompts] = useState([]);
|
||||||
|
|
||||||
useEffect(() => {
|
const fetchData = useCallback(async () => {
|
||||||
fetchData();
|
|
||||||
}, [meeting_id]);
|
|
||||||
|
|
||||||
const fetchData = async () => {
|
|
||||||
try {
|
try {
|
||||||
const [uRes, pRes, mRes] = await Promise.all([
|
const [uRes, pRes, mRes] = await Promise.all([
|
||||||
apiClient.get(buildApiUrl(API_ENDPOINTS.USERS.LIST)),
|
apiClient.get(buildApiUrl(API_ENDPOINTS.USERS.LIST)),
|
||||||
|
|
@ -45,12 +41,16 @@ const EditMeeting = () => {
|
||||||
attendee_ids: meeting.attendee_ids || meeting.attendees.map(a => a.user_id).filter(Boolean),
|
attendee_ids: meeting.attendee_ids || meeting.attendees.map(a => a.user_id).filter(Boolean),
|
||||||
tags: meeting.tags?.map(t => t.name) || []
|
tags: meeting.tags?.map(t => t.name) || []
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch {
|
||||||
message.error('加载会议数据失败');
|
message.error('加载会议数据失败');
|
||||||
} finally {
|
} finally {
|
||||||
setFetching(false);
|
setFetching(false);
|
||||||
}
|
}
|
||||||
};
|
}, [form, meeting_id, message]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchData();
|
||||||
|
}, [fetchData]);
|
||||||
|
|
||||||
const onFinish = async (values) => {
|
const onFinish = async (values) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ const BRAND_HIGHLIGHTS = [
|
||||||
'时间轴回看与全文检索',
|
'时间轴回看与全文检索',
|
||||||
'沉淀可追踪的会议资产',
|
'沉淀可追踪的会议资产',
|
||||||
];
|
];
|
||||||
|
const HOME_TAGLINE = '让每一次谈话都产生价值';
|
||||||
|
|
||||||
const HomePage = ({ onLogin }) => {
|
const HomePage = ({ onLogin }) => {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
@ -131,7 +132,7 @@ const HomePage = ({ onLogin }) => {
|
||||||
color: '#1677ff',
|
color: '#1677ff',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{branding.home_headline}
|
{branding.app_name}
|
||||||
</Title>
|
</Title>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -144,7 +145,7 @@ const HomePage = ({ onLogin }) => {
|
||||||
maxWidth: 560,
|
maxWidth: 560,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{branding.home_tagline}
|
{HOME_TAGLINE}
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
|
|
||||||
<Paragraph
|
<Paragraph
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
Layout, Card, Row, Col, Button, Space, Typography, Tag,
|
Layout, Card, Row, Col, Button, Space, Typography, Tag,
|
||||||
Tooltip, Modal, Progress, Spin, App, Descriptions,
|
Tooltip, Modal, Progress, Spin, App, Descriptions,
|
||||||
|
|
@ -12,14 +12,12 @@ import {
|
||||||
ClockCircleOutlined, UserOutlined, CalendarOutlined,
|
ClockCircleOutlined, UserOutlined, CalendarOutlined,
|
||||||
CheckCircleOutlined, InfoCircleOutlined, ThunderboltOutlined
|
CheckCircleOutlined, InfoCircleOutlined, ThunderboltOutlined
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { Link, useNavigate } from 'react-router-dom';
|
||||||
import apiClient from '../utils/apiClient';
|
import apiClient from '../utils/apiClient';
|
||||||
import { buildApiUrl, API_ENDPOINTS } from '../config/api';
|
import { buildApiUrl, API_ENDPOINTS } from '../config/api';
|
||||||
import ContentViewer from '../components/ContentViewer';
|
import ContentViewer from '../components/ContentViewer';
|
||||||
import ActionButton from '../components/ActionButton';
|
import ActionButton from '../components/ActionButton';
|
||||||
import exportService from '../services/exportService';
|
|
||||||
import tools from '../utils/tools';
|
import tools from '../utils/tools';
|
||||||
import meetingCacheService from '../services/meetingCacheService';
|
|
||||||
|
|
||||||
const { Title, Text, Paragraph } = Typography;
|
const { Title, Text, Paragraph } = Typography;
|
||||||
const { Sider, Content } = Layout;
|
const { Sider, Content } = Layout;
|
||||||
|
|
@ -30,7 +28,6 @@ const KnowledgeBasePage = ({ user }) => {
|
||||||
const { message, modal } = App.useApp();
|
const { message, modal } = App.useApp();
|
||||||
|
|
||||||
const [kbs, setKbs] = useState([]);
|
const [kbs, setKbs] = useState([]);
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [selectedKb, setSelectedKb] = useState(null);
|
const [selectedKb, setSelectedKb] = useState(null);
|
||||||
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
|
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
|
||||||
const [showCreateForm, setShowCreateForm] = useState(false);
|
const [showCreateForm, setShowCreateForm] = useState(false);
|
||||||
|
|
@ -50,39 +47,29 @@ const KnowledgeBasePage = ({ user }) => {
|
||||||
const [selectedPromptId, setSelectedPromptId] = useState(null);
|
const [selectedPromptId, setSelectedPromptId] = useState(null);
|
||||||
const [taskProgress, setTaskProgress] = useState(0);
|
const [taskProgress, setTaskProgress] = useState(0);
|
||||||
|
|
||||||
useEffect(() => {
|
const loadKbDetail = useCallback(async (id) => {
|
||||||
fetchAllKbs();
|
try {
|
||||||
fetchAvailableTags();
|
const res = await apiClient.get(buildApiUrl(API_ENDPOINTS.KNOWLEDGE_BASE.DETAIL(id)));
|
||||||
}, []);
|
setSelectedKb(res.data);
|
||||||
|
} catch {
|
||||||
useEffect(() => {
|
message.error('加载知识库详情失败');
|
||||||
if (showCreateForm && createStep === 0) {
|
|
||||||
fetchMeetings(meetingsPagination.page);
|
|
||||||
}
|
}
|
||||||
}, [searchQuery, selectedTags, showCreateForm, createStep, meetingsPagination.page]);
|
}, [message]);
|
||||||
|
|
||||||
const fetchAllKbs = async () => {
|
const fetchAllKbs = useCallback(async () => {
|
||||||
setLoading(true);
|
|
||||||
try {
|
try {
|
||||||
const res = await apiClient.get(buildApiUrl(API_ENDPOINTS.KNOWLEDGE_BASE.LIST));
|
const res = await apiClient.get(buildApiUrl(API_ENDPOINTS.KNOWLEDGE_BASE.LIST));
|
||||||
const sorted = res.data.kbs.sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
|
const sorted = (res.data.kbs || []).sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
|
||||||
setKbs(sorted);
|
setKbs(sorted);
|
||||||
if (sorted.length > 0 && !selectedKb) {
|
if (sorted.length > 0 && !selectedKb) {
|
||||||
loadKbDetail(sorted[0].kb_id);
|
loadKbDetail(sorted[0].kb_id);
|
||||||
}
|
}
|
||||||
} finally {
|
} catch {
|
||||||
setLoading(false);
|
message.error('加载知识库列表失败');
|
||||||
}
|
}
|
||||||
};
|
}, [loadKbDetail, message, selectedKb]);
|
||||||
|
|
||||||
const loadKbDetail = async (id) => {
|
const fetchMeetings = useCallback(async (page = 1) => {
|
||||||
try {
|
|
||||||
const res = await apiClient.get(buildApiUrl(API_ENDPOINTS.KNOWLEDGE_BASE.DETAIL(id)));
|
|
||||||
setSelectedKb(res.data);
|
|
||||||
} catch (e) {}
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchMeetings = async (page = 1) => {
|
|
||||||
setLoadingMeetings(true);
|
setLoadingMeetings(true);
|
||||||
try {
|
try {
|
||||||
const params = {
|
const params = {
|
||||||
|
|
@ -94,26 +81,43 @@ const KnowledgeBasePage = ({ user }) => {
|
||||||
const res = await apiClient.get(buildApiUrl(API_ENDPOINTS.MEETINGS.LIST), { params });
|
const res = await apiClient.get(buildApiUrl(API_ENDPOINTS.MEETINGS.LIST), { params });
|
||||||
setMeetings(res.data.meetings || []);
|
setMeetings(res.data.meetings || []);
|
||||||
setMeetingsPagination({ page: res.data.page, total: res.data.total });
|
setMeetingsPagination({ page: res.data.page, total: res.data.total });
|
||||||
|
} catch {
|
||||||
|
message.error('获取会议列表失败');
|
||||||
} finally {
|
} finally {
|
||||||
setLoadingMeetings(false);
|
setLoadingMeetings(false);
|
||||||
}
|
}
|
||||||
};
|
}, [message, searchQuery, selectedTags, user.user_id]);
|
||||||
|
|
||||||
const fetchAvailableTags = async () => {
|
const fetchAvailableTags = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const res = await apiClient.get(buildApiUrl(API_ENDPOINTS.TAGS.LIST));
|
const res = await apiClient.get(buildApiUrl(API_ENDPOINTS.TAGS.LIST));
|
||||||
setAvailableTags(res.data?.slice(0, 10) || []);
|
setAvailableTags(res.data?.slice(0, 10) || []);
|
||||||
} catch (e) {}
|
} catch {
|
||||||
};
|
setAvailableTags([]);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
const fetchPrompts = async () => {
|
const fetchPrompts = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const res = await apiClient.get(buildApiUrl(API_ENDPOINTS.PROMPTS.ACTIVE('KNOWLEDGE_TASK')));
|
const res = await apiClient.get(buildApiUrl(API_ENDPOINTS.PROMPTS.ACTIVE('KNOWLEDGE_TASK')));
|
||||||
setAvailablePrompts(res.data.prompts || []);
|
setAvailablePrompts(res.data.prompts || []);
|
||||||
const def = res.data.prompts?.find(p => p.is_default) || res.data.prompts?.[0];
|
const def = res.data.prompts?.find(p => p.is_default) || res.data.prompts?.[0];
|
||||||
if (def) setSelectedPromptId(def.id);
|
if (def) setSelectedPromptId(def.id);
|
||||||
} catch (e) {}
|
} catch {
|
||||||
};
|
setAvailablePrompts([]);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchAllKbs();
|
||||||
|
fetchAvailableTags();
|
||||||
|
}, [fetchAllKbs, fetchAvailableTags]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (showCreateForm && createStep === 0) {
|
||||||
|
fetchMeetings(meetingsPagination.page);
|
||||||
|
}
|
||||||
|
}, [createStep, fetchMeetings, meetingsPagination.page, selectedTags, searchQuery, showCreateForm]);
|
||||||
|
|
||||||
const handleStartCreate = () => {
|
const handleStartCreate = () => {
|
||||||
setShowCreateForm(true);
|
setShowCreateForm(true);
|
||||||
|
|
@ -122,7 +126,7 @@ const KnowledgeBasePage = ({ user }) => {
|
||||||
fetchPrompts();
|
fetchPrompts();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleGenerate = async () => {
|
const handleGenerate = useCallback(async () => {
|
||||||
setGenerating(true);
|
setGenerating(true);
|
||||||
setTaskProgress(10);
|
setTaskProgress(10);
|
||||||
try {
|
try {
|
||||||
|
|
@ -149,10 +153,11 @@ const KnowledgeBasePage = ({ user }) => {
|
||||||
message.error('生成失败');
|
message.error('生成失败');
|
||||||
}
|
}
|
||||||
}, 3000);
|
}, 3000);
|
||||||
} catch (e) {
|
} catch {
|
||||||
setGenerating(false);
|
setGenerating(false);
|
||||||
|
message.error('创建知识库任务失败');
|
||||||
}
|
}
|
||||||
};
|
}, [fetchAllKbs, message, selectedMeetings, selectedPromptId, userPrompt]);
|
||||||
|
|
||||||
const handleDelete = (kb) => {
|
const handleDelete = (kb) => {
|
||||||
modal.confirm({
|
modal.confirm({
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ import CenterPager from '../components/CenterPager';
|
||||||
import MeetingFormDrawer from '../components/MeetingFormDrawer';
|
import MeetingFormDrawer from '../components/MeetingFormDrawer';
|
||||||
import meetingCacheService from '../services/meetingCacheService';
|
import meetingCacheService from '../services/meetingCacheService';
|
||||||
import tools from '../utils/tools';
|
import tools from '../utils/tools';
|
||||||
|
import useSystemPageSize from '../hooks/useSystemPageSize';
|
||||||
import './MeetingCenterPage.css';
|
import './MeetingCenterPage.css';
|
||||||
import '../components/MeetingInfoCard.css';
|
import '../components/MeetingInfoCard.css';
|
||||||
|
|
||||||
|
|
@ -101,7 +102,7 @@ const MeetingCenterPage = ({ user }) => {
|
||||||
const [searchValue, setSearchValue] = useState('');
|
const [searchValue, setSearchValue] = useState('');
|
||||||
const [filterType, setFilterType] = useState('all');
|
const [filterType, setFilterType] = useState('all');
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
const [pageSize] = useState(8);
|
const { pageSize, isReady: pageSizeReady } = useSystemPageSize(10, { suspendUntilReady: true });
|
||||||
const [total, setTotal] = useState(0);
|
const [total, setTotal] = useState(0);
|
||||||
const [formDrawerOpen, setFormDrawerOpen] = useState(false);
|
const [formDrawerOpen, setFormDrawerOpen] = useState(false);
|
||||||
const [editingMeetingId, setEditingMeetingId] = useState(null);
|
const [editingMeetingId, setEditingMeetingId] = useState(null);
|
||||||
|
|
@ -149,8 +150,15 @@ const MeetingCenterPage = ({ user }) => {
|
||||||
}, [filterType, keyword, message, page, pageSize, user.user_id]);
|
}, [filterType, keyword, message, page, pageSize, user.user_id]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
meetingCacheService.clearAll();
|
||||||
|
}, [pageSize]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!pageSizeReady || !pageSize) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
loadMeetings(page, keyword, filterType);
|
loadMeetings(page, keyword, filterType);
|
||||||
}, [filterType, keyword, loadMeetings, page]);
|
}, [filterType, keyword, loadMeetings, page, pageSize, pageSizeReady]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (location.state?.openCreate) {
|
if (location.state?.openCreate) {
|
||||||
|
|
@ -236,7 +244,11 @@ const MeetingCenterPage = ({ user }) => {
|
||||||
variant="borderless"
|
variant="borderless"
|
||||||
styles={{ body: { padding: '20px' } }}
|
styles={{ body: { padding: '20px' } }}
|
||||||
>
|
>
|
||||||
{meetings.length ? (
|
{!pageSizeReady ? (
|
||||||
|
<div className="meeting-center-empty">
|
||||||
|
<Empty description="加载分页配置中..." />
|
||||||
|
</div>
|
||||||
|
) : meetings.length ? (
|
||||||
<>
|
<>
|
||||||
<Row gutter={[16, 16]}>
|
<Row gutter={[16, 16]}>
|
||||||
{meetings.map((meeting) => {
|
{meetings.map((meeting) => {
|
||||||
|
|
@ -351,7 +363,7 @@ const MeetingCenterPage = ({ user }) => {
|
||||||
<CenterPager
|
<CenterPager
|
||||||
current={page}
|
current={page}
|
||||||
total={total}
|
total={total}
|
||||||
pageSize={pageSize}
|
pageSize={pageSize || 10}
|
||||||
onChange={setPage}
|
onChange={setPage}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -159,6 +159,8 @@ const MeetingDetails = ({ user }) => {
|
||||||
|
|
||||||
/* ══════════════════ 数据获取 ══════════════════ */
|
/* ══════════════════ 数据获取 ══════════════════ */
|
||||||
|
|
||||||
|
// The initial bootstrap should run only when the meeting id changes.
|
||||||
|
// Polling helpers are intentionally excluded here to avoid restarting intervals on every render.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchMeetingDetails();
|
fetchMeetingDetails();
|
||||||
fetchTranscript();
|
fetchTranscript();
|
||||||
|
|
@ -168,17 +170,18 @@ const MeetingDetails = ({ user }) => {
|
||||||
if (summaryPollIntervalRef.current) clearInterval(summaryPollIntervalRef.current);
|
if (summaryPollIntervalRef.current) clearInterval(summaryPollIntervalRef.current);
|
||||||
if (summaryBootstrapTimeoutRef.current) clearTimeout(summaryBootstrapTimeoutRef.current);
|
if (summaryBootstrapTimeoutRef.current) clearTimeout(summaryBootstrapTimeoutRef.current);
|
||||||
};
|
};
|
||||||
}, [meeting_id]);
|
}, [meeting_id]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
const loadAudioUploadConfig = async () => {
|
const loadAudioUploadConfig = async () => {
|
||||||
try {
|
try {
|
||||||
const nextMaxAudioSize = await configService.getMaxAudioSize();
|
const nextMaxAudioSize = await configService.getMaxAudioSize();
|
||||||
setMaxAudioSize(nextMaxAudioSize || 100 * 1024 * 1024);
|
setMaxAudioSize(nextMaxAudioSize || 100 * 1024 * 1024);
|
||||||
} catch (error) {
|
} catch {
|
||||||
setMaxAudioSize(100 * 1024 * 1024);
|
setMaxAudioSize(100 * 1024 * 1024);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Summary resources are loaded lazily when the drawer opens; the existing prompt/model caches gate repeat fetches.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!showSummaryDrawer) {
|
if (!showSummaryDrawer) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -189,7 +192,7 @@ const MeetingDetails = ({ user }) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchSummaryResources();
|
fetchSummaryResources();
|
||||||
}, [showSummaryDrawer]);
|
}, [showSummaryDrawer]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
transcriptRefs.current = [];
|
transcriptRefs.current = [];
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useEffect, useRef, useState } from 'react';
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { useParams, Link } from 'react-router-dom';
|
import { useParams, Link } from 'react-router-dom';
|
||||||
import { Layout, Space, Button, App, Empty, Input, Tabs } from 'antd';
|
import { Layout, Space, Button, App, Empty, Input, Tabs } from 'antd';
|
||||||
import { LockOutlined, EyeOutlined, CopyOutlined, ShareAltOutlined, HomeOutlined, FileTextOutlined, PartitionOutlined, AudioOutlined } from '@ant-design/icons';
|
import { LockOutlined, EyeOutlined, CopyOutlined, ShareAltOutlined, HomeOutlined, FileTextOutlined, PartitionOutlined, AudioOutlined } from '@ant-design/icons';
|
||||||
|
|
@ -36,22 +36,7 @@ const MeetingPreview = () => {
|
||||||
configService.getBrandingConfig().then(setBranding).catch(() => {});
|
configService.getBrandingConfig().then(setBranding).catch(() => {});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
const fetchTranscriptAndAudio = useCallback(async () => {
|
||||||
setMeeting(null);
|
|
||||||
setTranscript([]);
|
|
||||||
transcriptRefs.current = [];
|
|
||||||
setAudioUrl('');
|
|
||||||
setActiveSegmentIndex(-1);
|
|
||||||
setPlaybackRate(1);
|
|
||||||
setError(null);
|
|
||||||
setPassword('');
|
|
||||||
setPasswordError('');
|
|
||||||
setPasswordRequired(false);
|
|
||||||
setIsAuthorized(false);
|
|
||||||
fetchPreview();
|
|
||||||
}, [meeting_id]);
|
|
||||||
|
|
||||||
const fetchTranscriptAndAudio = async () => {
|
|
||||||
const [transcriptRes, audioRes] = await Promise.allSettled([
|
const [transcriptRes, audioRes] = await Promise.allSettled([
|
||||||
apiClient.get(buildApiUrl(API_ENDPOINTS.MEETINGS.TRANSCRIPT(meeting_id))),
|
apiClient.get(buildApiUrl(API_ENDPOINTS.MEETINGS.TRANSCRIPT(meeting_id))),
|
||||||
apiClient.get(buildApiUrl(API_ENDPOINTS.MEETINGS.AUDIO(meeting_id))),
|
apiClient.get(buildApiUrl(API_ENDPOINTS.MEETINGS.AUDIO(meeting_id))),
|
||||||
|
|
@ -68,9 +53,9 @@ const MeetingPreview = () => {
|
||||||
} else {
|
} else {
|
||||||
setAudioUrl('');
|
setAudioUrl('');
|
||||||
}
|
}
|
||||||
};
|
}, [meeting_id]);
|
||||||
|
|
||||||
const fetchPreview = async (pwd = '') => {
|
const fetchPreview = useCallback(async (pwd = '') => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const endpoint = API_ENDPOINTS.MEETINGS.PREVIEW_DATA(meeting_id);
|
const endpoint = API_ENDPOINTS.MEETINGS.PREVIEW_DATA(meeting_id);
|
||||||
|
|
@ -100,7 +85,22 @@ const MeetingPreview = () => {
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
}, [fetchTranscriptAndAudio, meeting_id]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setMeeting(null);
|
||||||
|
setTranscript([]);
|
||||||
|
transcriptRefs.current = [];
|
||||||
|
setAudioUrl('');
|
||||||
|
setActiveSegmentIndex(-1);
|
||||||
|
setPlaybackRate(1);
|
||||||
|
setError(null);
|
||||||
|
setPassword('');
|
||||||
|
setPasswordError('');
|
||||||
|
setPasswordRequired(false);
|
||||||
|
setIsAuthorized(false);
|
||||||
|
fetchPreview();
|
||||||
|
}, [fetchPreview, meeting_id]);
|
||||||
|
|
||||||
const handleCopyLink = async () => {
|
const handleCopyLink = async () => {
|
||||||
await navigator.clipboard.writeText(window.location.href);
|
await navigator.clipboard.writeText(window.location.href);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useEffect, useMemo, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
App,
|
App,
|
||||||
Button,
|
Button,
|
||||||
|
|
@ -46,7 +46,7 @@ const PromptConfigPage = ({ user }) => {
|
||||||
const [selectedPromptIds, setSelectedPromptIds] = useState([]);
|
const [selectedPromptIds, setSelectedPromptIds] = useState([]);
|
||||||
const [viewingPrompt, setViewingPrompt] = useState(null);
|
const [viewingPrompt, setViewingPrompt] = useState(null);
|
||||||
|
|
||||||
const loadConfig = async () => {
|
const loadConfig = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const res = await apiClient.get(buildApiUrl(API_ENDPOINTS.PROMPTS.CONFIG(taskType)));
|
const res = await apiClient.get(buildApiUrl(API_ENDPOINTS.PROMPTS.CONFIG(taskType)));
|
||||||
|
|
@ -57,11 +57,11 @@ const PromptConfigPage = ({ user }) => {
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
}, [message, taskType]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadConfig();
|
loadConfig();
|
||||||
}, [taskType]);
|
}, [loadConfig]);
|
||||||
|
|
||||||
const selectedPromptCards = useMemo(() => {
|
const selectedPromptCards = useMemo(() => {
|
||||||
const map = new Map(availablePrompts.map((item) => [item.id, item]));
|
const map = new Map(availablePrompts.map((item) => [item.id, item]));
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
App,
|
App,
|
||||||
Button,
|
Button,
|
||||||
|
|
@ -32,6 +32,7 @@ import MarkdownEditor from '../components/MarkdownEditor';
|
||||||
import CenterPager from '../components/CenterPager';
|
import CenterPager from '../components/CenterPager';
|
||||||
import StatusTag from '../components/StatusTag';
|
import StatusTag from '../components/StatusTag';
|
||||||
import ActionButton from '../components/ActionButton';
|
import ActionButton from '../components/ActionButton';
|
||||||
|
import useSystemPageSize from '../hooks/useSystemPageSize';
|
||||||
|
|
||||||
const { Title, Text } = Typography;
|
const { Title, Text } = Typography;
|
||||||
|
|
||||||
|
|
@ -49,7 +50,7 @@ const PromptManagementPage = ({ user, mode = 'default', embedded = false }) => {
|
||||||
const [statusFilter, setStatusFilter] = useState('all');
|
const [statusFilter, setStatusFilter] = useState('all');
|
||||||
const [keyword, setKeyword] = useState('');
|
const [keyword, setKeyword] = useState('');
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
const [size] = useState(12);
|
const size = useSystemPageSize(10);
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [prompts, setPrompts] = useState([]);
|
const [prompts, setPrompts] = useState([]);
|
||||||
|
|
@ -67,7 +68,7 @@ const PromptManagementPage = ({ user, mode = 'default', embedded = false }) => {
|
||||||
? '仅维护你自己创建的提示词,供个人配置启用。'
|
? '仅维护你自己创建的提示词,供个人配置启用。'
|
||||||
: '系统管理员可管理系统提示词,普通用户管理自己的提示词。';
|
: '系统管理员可管理系统提示词,普通用户管理自己的提示词。';
|
||||||
|
|
||||||
const loadPrompts = async () => {
|
const loadPrompts = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const is_active = statusFilter === 'all' ? undefined : Number(statusFilter);
|
const is_active = statusFilter === 'all' ? undefined : Number(statusFilter);
|
||||||
|
|
@ -88,11 +89,11 @@ const PromptManagementPage = ({ user, mode = 'default', embedded = false }) => {
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
}, [keyword, message, page, scope, size, statusFilter, taskType]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadPrompts();
|
loadPrompts();
|
||||||
}, [taskType, statusFilter, page]);
|
}, [loadPrompts]);
|
||||||
|
|
||||||
const openCreateDrawer = () => {
|
const openCreateDrawer = () => {
|
||||||
setEditingPrompt(null);
|
setEditingPrompt(null);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
import { Tree, Button, Form, Input, InputNumber, Select, Switch, Space, Card, Empty, Popconfirm, App, Row, Col, Typography, Checkbox } from 'antd';
|
import { Tree, Button, Form, Input, InputNumber, Select, Switch, Space, Card, Empty, Popconfirm, App, Row, Col, Typography, Checkbox } from 'antd';
|
||||||
import {
|
import {
|
||||||
BookOutlined,
|
BookOutlined,
|
||||||
|
|
@ -17,7 +17,6 @@ const { Title, Text } = Typography;
|
||||||
|
|
||||||
const DictManagement = () => {
|
const DictManagement = () => {
|
||||||
const { message } = App.useApp();
|
const { message } = App.useApp();
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [dictTypes, setDictTypes] = useState([]); // 字典类型列表
|
const [dictTypes, setDictTypes] = useState([]); // 字典类型列表
|
||||||
const [selectedDictType, setSelectedDictType] = useState('client_platform'); // 当前选中的字典类型
|
const [selectedDictType, setSelectedDictType] = useState('client_platform'); // 当前选中的字典类型
|
||||||
const [dictData, setDictData] = useState([]); // 当前字典类型的数据
|
const [dictData, setDictData] = useState([]); // 当前字典类型的数据
|
||||||
|
|
@ -27,7 +26,7 @@ const DictManagement = () => {
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
|
|
||||||
// 获取所有字典类型
|
// 获取所有字典类型
|
||||||
const fetchDictTypes = async () => {
|
const fetchDictTypes = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.DICT_DATA.TYPES));
|
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.DICT_DATA.TYPES));
|
||||||
if (response.code === '200') {
|
if (response.code === '200') {
|
||||||
|
|
@ -37,14 +36,29 @@ const DictManagement = () => {
|
||||||
setSelectedDictType(types[0] || '');
|
setSelectedDictType(types[0] || '');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch {
|
||||||
message.error('获取字典类型失败');
|
message.error('获取字典类型失败');
|
||||||
}
|
}
|
||||||
};
|
}, [message, selectedDictType]);
|
||||||
|
|
||||||
// 获取指定类型的字典数据
|
// 获取指定类型的字典数据
|
||||||
const fetchDictData = async (dictType) => {
|
// 将树形数据转换为 antd Tree 组件格式
|
||||||
setLoading(true);
|
const buildAntdTreeData = useCallback((tree) => {
|
||||||
|
return tree.map(node => ({
|
||||||
|
title: (
|
||||||
|
<Space>
|
||||||
|
{node.parent_code === 'ROOT' ? <FolderOpenOutlined /> : <FileOutlined />}
|
||||||
|
<span>{node.label_cn}</span>
|
||||||
|
<Text type="secondary" size="small">({node.dict_code})</Text>
|
||||||
|
</Space>
|
||||||
|
),
|
||||||
|
key: node.dict_code,
|
||||||
|
data: node,
|
||||||
|
children: node.children && node.children.length > 0 ? buildAntdTreeData(node.children) : []
|
||||||
|
}));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const fetchDictData = useCallback(async (dictType) => {
|
||||||
try {
|
try {
|
||||||
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.DICT_DATA.BY_TYPE(dictType)));
|
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.DICT_DATA.BY_TYPE(dictType)));
|
||||||
if (response.code === '200') {
|
if (response.code === '200') {
|
||||||
|
|
@ -59,38 +73,20 @@ const DictManagement = () => {
|
||||||
setIsEditing(false);
|
setIsEditing(false);
|
||||||
form.resetFields();
|
form.resetFields();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch {
|
||||||
message.error('获取字典数据失败');
|
message.error('获取字典数据失败');
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
}
|
||||||
};
|
}, [buildAntdTreeData, form, message]);
|
||||||
|
|
||||||
// 将树形数据转换为 antd Tree 组件格式
|
|
||||||
const buildAntdTreeData = (tree) => {
|
|
||||||
return tree.map(node => ({
|
|
||||||
title: (
|
|
||||||
<Space>
|
|
||||||
{node.parent_code === 'ROOT' ? <FolderOpenOutlined /> : <FileOutlined />}
|
|
||||||
<span>{node.label_cn}</span>
|
|
||||||
<Text type="secondary" size="small">({node.dict_code})</Text>
|
|
||||||
</Space>
|
|
||||||
),
|
|
||||||
key: node.dict_code,
|
|
||||||
data: node,
|
|
||||||
children: node.children && node.children.length > 0 ? buildAntdTreeData(node.children) : []
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchDictTypes();
|
fetchDictTypes();
|
||||||
}, []);
|
}, [fetchDictTypes]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedDictType) {
|
if (selectedDictType) {
|
||||||
fetchDictData(selectedDictType);
|
fetchDictData(selectedDictType);
|
||||||
}
|
}
|
||||||
}, [selectedDictType]);
|
}, [fetchDictData, selectedDictType]);
|
||||||
|
|
||||||
// 选中树节点
|
// 选中树节点
|
||||||
const handleSelectNode = (selectedKeys, info) => {
|
const handleSelectNode = (selectedKeys, info) => {
|
||||||
|
|
@ -137,7 +133,7 @@ const DictManagement = () => {
|
||||||
if (values.extension_attr) {
|
if (values.extension_attr) {
|
||||||
try {
|
try {
|
||||||
values.extension_attr = JSON.parse(values.extension_attr);
|
values.extension_attr = JSON.parse(values.extension_attr);
|
||||||
} catch (e) {
|
} catch {
|
||||||
message.error('扩展属性 JSON 格式错误');
|
message.error('扩展属性 JSON 格式错误');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -167,9 +163,9 @@ const DictManagement = () => {
|
||||||
fetchDictData(selectedDictType);
|
fetchDictData(selectedDictType);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (err) {
|
||||||
if (!error.errorFields) {
|
if (!err.errorFields) {
|
||||||
message.error(error.response?.data?.message || '操作失败');
|
message.error(err.response?.data?.message || '操作失败');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -184,7 +180,7 @@ const DictManagement = () => {
|
||||||
setIsEditing(false);
|
setIsEditing(false);
|
||||||
form.resetFields();
|
form.resetFields();
|
||||||
fetchDictData(selectedDictType);
|
fetchDictData(selectedDictType);
|
||||||
} catch (error) {
|
} catch {
|
||||||
message.error('删除失败');
|
message.error('删除失败');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useEffect, useMemo, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
Button,
|
Button,
|
||||||
|
|
@ -39,6 +39,7 @@ import apiClient from '../../utils/apiClient';
|
||||||
import { buildApiUrl, API_ENDPOINTS } from '../../config/api';
|
import { buildApiUrl, API_ENDPOINTS } from '../../config/api';
|
||||||
import AdminModuleShell from '../../components/AdminModuleShell';
|
import AdminModuleShell from '../../components/AdminModuleShell';
|
||||||
import ActionButton from '../../components/ActionButton';
|
import ActionButton from '../../components/ActionButton';
|
||||||
|
import useSystemPageSize from '../../hooks/useSystemPageSize';
|
||||||
|
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
const { TextArea } = Input;
|
const { TextArea } = Input;
|
||||||
|
|
@ -74,6 +75,7 @@ const getAppEntryUrl = (app) => {
|
||||||
const ExternalAppManagement = () => {
|
const ExternalAppManagement = () => {
|
||||||
const { message, modal } = App.useApp();
|
const { message, modal } = App.useApp();
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
|
const pageSize = useSystemPageSize(10);
|
||||||
|
|
||||||
const [apps, setApps] = useState([]);
|
const [apps, setApps] = useState([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
@ -85,11 +87,7 @@ const ExternalAppManagement = () => {
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
const [uploading, setUploading] = useState(false);
|
const [uploading, setUploading] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
const fetchApps = useCallback(async () => {
|
||||||
fetchApps();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const fetchApps = async () => {
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.EXTERNAL_APPS.LIST));
|
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.EXTERNAL_APPS.LIST));
|
||||||
|
|
@ -101,7 +99,11 @@ const ExternalAppManagement = () => {
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
}, [message]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchApps();
|
||||||
|
}, [fetchApps]);
|
||||||
|
|
||||||
const handleOpenModal = (app = null) => {
|
const handleOpenModal = (app = null) => {
|
||||||
if (app) {
|
if (app) {
|
||||||
|
|
@ -411,7 +413,7 @@ const ExternalAppManagement = () => {
|
||||||
loading={loading}
|
loading={loading}
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
scroll={{ x: 980 }}
|
scroll={{ x: 980 }}
|
||||||
pagination={{ pageSize: 10, showTotal: (count) => `共 ${count} 条记录` }}
|
pagination={{ pageSize, showTotal: (count) => `共 ${count} 条记录` }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</AdminModuleShell>
|
</AdminModuleShell>
|
||||||
|
|
|
||||||
|
|
@ -10,10 +10,10 @@ import {
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import apiClient from '../../utils/apiClient';
|
import apiClient from '../../utils/apiClient';
|
||||||
import { buildApiUrl, API_ENDPOINTS } from '../../config/api';
|
import { buildApiUrl, API_ENDPOINTS } from '../../config/api';
|
||||||
import configService from '../../utils/configService';
|
|
||||||
import AdminModuleShell from '../../components/AdminModuleShell';
|
import AdminModuleShell from '../../components/AdminModuleShell';
|
||||||
import ActionButton from '../../components/ActionButton';
|
import ActionButton from '../../components/ActionButton';
|
||||||
import StatusTag from '../../components/StatusTag';
|
import StatusTag from '../../components/StatusTag';
|
||||||
|
import useSystemPageSize from '../../hooks/useSystemPageSize';
|
||||||
|
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
|
|
||||||
|
|
@ -48,7 +48,7 @@ const HotWordManagement = () => {
|
||||||
const [keyword, setKeyword] = useState('');
|
const [keyword, setKeyword] = useState('');
|
||||||
const [langFilter, setLangFilter] = useState('all');
|
const [langFilter, setLangFilter] = useState('all');
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
const [pageSize, setPageSize] = useState(10);
|
const pageSize = useSystemPageSize(10);
|
||||||
|
|
||||||
// ── Fetch groups ──
|
// ── Fetch groups ──
|
||||||
const fetchGroups = useCallback(async () => {
|
const fetchGroups = useCallback(async () => {
|
||||||
|
|
@ -82,7 +82,6 @@ const HotWordManagement = () => {
|
||||||
}, [message]);
|
}, [message]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
configService.getPageSize().then((size) => setPageSize(size));
|
|
||||||
fetchGroups();
|
fetchGroups();
|
||||||
}, [fetchGroups]);
|
}, [fetchGroups]);
|
||||||
|
|
||||||
|
|
@ -442,8 +441,7 @@ const HotWordManagement = () => {
|
||||||
pagination={{
|
pagination={{
|
||||||
current: page, pageSize,
|
current: page, pageSize,
|
||||||
total: filteredItems.length,
|
total: filteredItems.length,
|
||||||
onChange: (p, s) => { setPage(p); setPageSize(s); },
|
onChange: (p) => { setPage(p); },
|
||||||
showSizeChanger: true,
|
|
||||||
showTotal: (count) => `共 ${count} 条`,
|
showTotal: (count) => `共 ${count} 条`,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ import {
|
||||||
import AdminModuleShell from '../../components/AdminModuleShell';
|
import AdminModuleShell from '../../components/AdminModuleShell';
|
||||||
import ActionButton from '../../components/ActionButton';
|
import ActionButton from '../../components/ActionButton';
|
||||||
import StatusTag from '../../components/StatusTag';
|
import StatusTag from '../../components/StatusTag';
|
||||||
|
import useSystemPageSize from '../../hooks/useSystemPageSize';
|
||||||
|
|
||||||
const AUDIO_SCENE_OPTIONS = [
|
const AUDIO_SCENE_OPTIONS = [
|
||||||
{ label: '全部', value: 'all' },
|
{ label: '全部', value: 'all' },
|
||||||
|
|
@ -67,6 +68,7 @@ const ModelManagement = () => {
|
||||||
const [testing, setTesting] = useState(false);
|
const [testing, setTesting] = useState(false);
|
||||||
const [audioSceneFilter, setAudioSceneFilter] = useState('all');
|
const [audioSceneFilter, setAudioSceneFilter] = useState('all');
|
||||||
const [hotWordGroups, setHotWordGroups] = useState([]);
|
const [hotWordGroups, setHotWordGroups] = useState([]);
|
||||||
|
const pageSize = useSystemPageSize(10);
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const watchedScene = Form.useWatch('audio_scene', form);
|
const watchedScene = Form.useWatch('audio_scene', form);
|
||||||
const watchedProvider = Form.useWatch('provider', form);
|
const watchedProvider = Form.useWatch('provider', form);
|
||||||
|
|
@ -478,7 +480,7 @@ const ModelManagement = () => {
|
||||||
columns={llmColumns}
|
columns={llmColumns}
|
||||||
dataSource={llmItems}
|
dataSource={llmItems}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
pagination={{ pageSize: 10 }}
|
pagination={{ pageSize }}
|
||||||
scroll={{ x: 1100 }}
|
scroll={{ x: 1100 }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -512,7 +514,7 @@ const ModelManagement = () => {
|
||||||
columns={audioColumns}
|
columns={audioColumns}
|
||||||
dataSource={filteredAudioItems}
|
dataSource={filteredAudioItems}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
pagination={{ pageSize: 10 }}
|
pagination={{ pageSize }}
|
||||||
scroll={{ x: 1100 }}
|
scroll={{ x: 1100 }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,36 @@
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { App, Button, Drawer, Form, Input, Popconfirm, Select, Space, Switch, Table, Tag, Tooltip } from 'antd';
|
import { Alert, App, Button, Drawer, Form, Input, Popconfirm, Select, Space, Switch, Table, Tag, Tooltip, Typography } from 'antd';
|
||||||
import { DeleteOutlined, EditOutlined, PlusOutlined, ReloadOutlined, SaveOutlined, SettingOutlined } from '@ant-design/icons';
|
import { DeleteOutlined, EditOutlined, PlusOutlined, ReloadOutlined, SaveOutlined, SettingOutlined } from '@ant-design/icons';
|
||||||
import apiClient from '../../utils/apiClient';
|
import apiClient from '../../utils/apiClient';
|
||||||
import { API_ENDPOINTS, buildApiUrl } from '../../config/api';
|
import { API_ENDPOINTS, buildApiUrl } from '../../config/api';
|
||||||
import AdminModuleShell from '../../components/AdminModuleShell';
|
import AdminModuleShell from '../../components/AdminModuleShell';
|
||||||
import ActionButton from '../../components/ActionButton';
|
import ActionButton from '../../components/ActionButton';
|
||||||
import StatusTag from '../../components/StatusTag';
|
import StatusTag from '../../components/StatusTag';
|
||||||
|
import configService from '../../utils/configService';
|
||||||
|
import useSystemPageSize from '../../hooks/useSystemPageSize';
|
||||||
|
|
||||||
|
const { Text } = Typography;
|
||||||
|
|
||||||
|
const CATEGORY_OPTIONS = [
|
||||||
|
{ label: 'public', value: 'public' },
|
||||||
|
{ label: 'system', value: 'system' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const VALUE_TYPE_OPTIONS = [
|
||||||
|
{ label: 'string', value: 'string' },
|
||||||
|
{ label: 'number', value: 'number' },
|
||||||
|
{ label: 'json', value: 'json' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const PUBLIC_PARAM_KEYS = new Set([
|
||||||
|
'app_name',
|
||||||
|
'page_size',
|
||||||
|
'max_audio_size',
|
||||||
|
'preview_title',
|
||||||
|
'login_welcome',
|
||||||
|
'footer_text',
|
||||||
|
'console_subtitle',
|
||||||
|
]);
|
||||||
|
|
||||||
const ParameterManagement = () => {
|
const ParameterManagement = () => {
|
||||||
const { message } = App.useApp();
|
const { message } = App.useApp();
|
||||||
|
|
@ -15,8 +40,21 @@ const ParameterManagement = () => {
|
||||||
const [editing, setEditing] = useState(null);
|
const [editing, setEditing] = useState(null);
|
||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
|
const pageSize = useSystemPageSize(10);
|
||||||
|
const currentParamKey = Form.useWatch('param_key', form);
|
||||||
|
|
||||||
const fetchItems = async () => {
|
const categorySuggestion = useMemo(() => {
|
||||||
|
const normalizedKey = String(currentParamKey || '').trim();
|
||||||
|
if (!normalizedKey) {
|
||||||
|
return '前端需要读取并缓存的参数请选择 public,仅后端内部使用的参数请选择 system。';
|
||||||
|
}
|
||||||
|
if (PUBLIC_PARAM_KEYS.has(normalizedKey)) {
|
||||||
|
return `检测到参数键 ${normalizedKey} 更适合作为 public 参数,前端会统一初始化并缓存本地。`;
|
||||||
|
}
|
||||||
|
return `参数键 ${normalizedKey} 暂未命中公开参数白名单,如仅后端使用,建议归类为 system。`;
|
||||||
|
}, [currentParamKey]);
|
||||||
|
|
||||||
|
const fetchItems = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const res = await apiClient.get(buildApiUrl(API_ENDPOINTS.ADMIN.PARAMETERS));
|
const res = await apiClient.get(buildApiUrl(API_ENDPOINTS.ADMIN.PARAMETERS));
|
||||||
|
|
@ -26,11 +64,11 @@ const ParameterManagement = () => {
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
}, [message]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchItems();
|
fetchItems();
|
||||||
}, []);
|
}, [fetchItems]);
|
||||||
|
|
||||||
const openCreate = () => {
|
const openCreate = () => {
|
||||||
setEditing(null);
|
setEditing(null);
|
||||||
|
|
@ -66,6 +104,7 @@ const ParameterManagement = () => {
|
||||||
await apiClient.post(buildApiUrl(API_ENDPOINTS.ADMIN.PARAMETERS), values);
|
await apiClient.post(buildApiUrl(API_ENDPOINTS.ADMIN.PARAMETERS), values);
|
||||||
message.success('参数创建成功');
|
message.success('参数创建成功');
|
||||||
}
|
}
|
||||||
|
configService.clearCache();
|
||||||
setDrawerOpen(false);
|
setDrawerOpen(false);
|
||||||
fetchItems();
|
fetchItems();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -80,7 +119,17 @@ const ParameterManagement = () => {
|
||||||
{ title: '参数名', dataIndex: 'param_name', key: 'param_name', width: 180 },
|
{ title: '参数名', dataIndex: 'param_name', key: 'param_name', width: 180 },
|
||||||
{ title: '值', dataIndex: 'param_value', key: 'param_value' },
|
{ title: '值', dataIndex: 'param_value', key: 'param_value' },
|
||||||
{ title: '类型', dataIndex: 'value_type', key: 'value_type', width: 100, render: (v) => <Tag>{v}</Tag> },
|
{ title: '类型', dataIndex: 'value_type', key: 'value_type', width: 100, render: (v) => <Tag>{v}</Tag> },
|
||||||
{ title: '分类', dataIndex: 'category', key: 'category', width: 120 },
|
{
|
||||||
|
title: '分类',
|
||||||
|
dataIndex: 'category',
|
||||||
|
key: 'category',
|
||||||
|
width: 120,
|
||||||
|
render: (value) => (
|
||||||
|
<Tag color={value === 'public' ? 'blue' : 'default'}>
|
||||||
|
{value || 'system'}
|
||||||
|
</Tag>
|
||||||
|
),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: '状态',
|
title: '状态',
|
||||||
dataIndex: 'is_active',
|
dataIndex: 'is_active',
|
||||||
|
|
@ -104,6 +153,7 @@ const ParameterManagement = () => {
|
||||||
onConfirm={async () => {
|
onConfirm={async () => {
|
||||||
try {
|
try {
|
||||||
await apiClient.delete(buildApiUrl(API_ENDPOINTS.ADMIN.PARAMETER_DELETE(row.param_key)));
|
await apiClient.delete(buildApiUrl(API_ENDPOINTS.ADMIN.PARAMETER_DELETE(row.param_key)));
|
||||||
|
configService.clearCache();
|
||||||
message.success('参数删除成功');
|
message.success('参数删除成功');
|
||||||
fetchItems();
|
fetchItems();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -141,7 +191,7 @@ const ParameterManagement = () => {
|
||||||
columns={columns}
|
columns={columns}
|
||||||
dataSource={items}
|
dataSource={items}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
pagination={{ pageSize: 10, showTotal: (count) => `共 ${count} 条` }}
|
pagination={{ pageSize, showTotal: (count) => `共 ${count} 条` }}
|
||||||
scroll={{ x: 1100 }}
|
scroll={{ x: 1100 }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -160,20 +210,37 @@ const ParameterManagement = () => {
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Form form={form} layout="vertical">
|
<Form form={form} layout="vertical">
|
||||||
|
<Alert
|
||||||
|
type="info"
|
||||||
|
showIcon
|
||||||
|
style={{ marginBottom: 16 }}
|
||||||
|
message="参数分类说明"
|
||||||
|
description={(
|
||||||
|
<Space direction="vertical" size={4}>
|
||||||
|
<Text>`public`:前端可读取,适合页面交互参数。</Text>
|
||||||
|
<Text>`system`:仅后端内部使用,不对前端公开。</Text>
|
||||||
|
</Space>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<Form.Item name="param_key" label="参数键" rules={[{ required: true, message: '请输入参数键' }]}>
|
<Form.Item name="param_key" label="参数键" rules={[{ required: true, message: '请输入参数键' }]}>
|
||||||
<Input placeholder="timeline_pagesize" disabled={Boolean(editing)} />
|
<Input placeholder="page_size" disabled={Boolean(editing)} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item name="param_name" label="参数名" rules={[{ required: true, message: '请输入参数名' }]}>
|
<Form.Item name="param_name" label="参数名" rules={[{ required: true, message: '请输入参数名' }]}>
|
||||||
<Input placeholder="会议时间轴分页大小" />
|
<Input placeholder="通用分页数量" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item name="param_value" label="参数值" rules={[{ required: true, message: '请输入参数值' }]}>
|
<Form.Item name="param_value" label="参数值" rules={[{ required: true, message: '请输入参数值' }]}>
|
||||||
<Input.TextArea rows={3} />
|
<Input.TextArea rows={3} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item name="value_type" label="值类型" rules={[{ required: true, message: '请选择值类型' }]}>
|
<Form.Item name="value_type" label="值类型" rules={[{ required: true, message: '请选择值类型' }]}>
|
||||||
<Select options={[{ label: 'string', value: 'string' }, { label: 'number', value: 'number' }, { label: 'json', value: 'json' }]} />
|
<Select options={VALUE_TYPE_OPTIONS} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item name="category" label="分类">
|
<Form.Item
|
||||||
<Input placeholder="system" />
|
name="category"
|
||||||
|
label="分类"
|
||||||
|
rules={[{ required: true, message: '请选择分类' }]}
|
||||||
|
extra={categorySuggestion}
|
||||||
|
>
|
||||||
|
<Select options={CATEGORY_OPTIONS} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item name="description" label="描述">
|
<Form.Item name="description" label="描述">
|
||||||
<Input.TextArea rows={2} />
|
<Input.TextArea rows={2} />
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useEffect, useMemo, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
Button,
|
Button,
|
||||||
|
|
@ -42,6 +42,7 @@ import ActionButton from '../../components/ActionButton';
|
||||||
import apiClient from '../../utils/apiClient';
|
import apiClient from '../../utils/apiClient';
|
||||||
import { buildApiUrl } from '../../config/api';
|
import { buildApiUrl } from '../../config/api';
|
||||||
import AdminModuleShell from '../../components/AdminModuleShell';
|
import AdminModuleShell from '../../components/AdminModuleShell';
|
||||||
|
import useSystemPageSize from '../../hooks/useSystemPageSize';
|
||||||
import { MENU_ICON_OPTIONS, renderMenuIcon } from '../../utils/menuIcons';
|
import { MENU_ICON_OPTIONS, renderMenuIcon } from '../../utils/menuIcons';
|
||||||
|
|
||||||
const { Text, Title } = Typography;
|
const { Text, Title } = Typography;
|
||||||
|
|
@ -67,6 +68,7 @@ const PermissionManagement = () => {
|
||||||
const [roleUsersTotal, setRoleUsersTotal] = useState(0);
|
const [roleUsersTotal, setRoleUsersTotal] = useState(0);
|
||||||
const [roleUsersPage, setRoleUsersPage] = useState(1);
|
const [roleUsersPage, setRoleUsersPage] = useState(1);
|
||||||
const [roleUsersPageSize, setRoleUsersPageSize] = useState(10);
|
const [roleUsersPageSize, setRoleUsersPageSize] = useState(10);
|
||||||
|
const systemPageSize = useSystemPageSize(10);
|
||||||
const [roleUsersLoading, setRoleUsersLoading] = useState(false);
|
const [roleUsersLoading, setRoleUsersLoading] = useState(false);
|
||||||
const [roleDetailTab, setRoleDetailTab] = useState('role-permissions');
|
const [roleDetailTab, setRoleDetailTab] = useState('role-permissions');
|
||||||
const [menuKeyword, setMenuKeyword] = useState('');
|
const [menuKeyword, setMenuKeyword] = useState('');
|
||||||
|
|
@ -93,6 +95,11 @@ const PermissionManagement = () => {
|
||||||
const [menuForm] = Form.useForm();
|
const [menuForm] = Form.useForm();
|
||||||
const selectedMenuIconValue = Form.useWatch('menu_icon', menuForm);
|
const selectedMenuIconValue = Form.useWatch('menu_icon', menuForm);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setRoleUsersPageSize(systemPageSize);
|
||||||
|
setRoleUsersPage(1);
|
||||||
|
}, [systemPageSize]);
|
||||||
|
|
||||||
const orderedMenus = useMemo(() => {
|
const orderedMenus = useMemo(() => {
|
||||||
if (!menus.length) return [];
|
if (!menus.length) return [];
|
||||||
|
|
||||||
|
|
@ -176,8 +183,14 @@ const PermissionManagement = () => {
|
||||||
[roles, selectedRoleId],
|
[roles, selectedRoleId],
|
||||||
);
|
);
|
||||||
|
|
||||||
const selectedRoleMenuIds = permissionsByRole[selectedRoleId] || [];
|
const selectedRoleMenuIds = useMemo(
|
||||||
const baselineSelectedRoleMenuIds = baselinePermissionsByRole[selectedRoleId] || [];
|
() => permissionsByRole[selectedRoleId] || [],
|
||||||
|
[permissionsByRole, selectedRoleId],
|
||||||
|
);
|
||||||
|
const baselineSelectedRoleMenuIds = useMemo(
|
||||||
|
() => baselinePermissionsByRole[selectedRoleId] || [],
|
||||||
|
[baselinePermissionsByRole, selectedRoleId],
|
||||||
|
);
|
||||||
const activeMenus = useMemo(() => menus.filter((menu) => Boolean(menu.is_active)), [menus]);
|
const activeMenus = useMemo(() => menus.filter((menu) => Boolean(menu.is_active)), [menus]);
|
||||||
const activeMenuIdSet = useMemo(() => new Set(activeMenus.map((menu) => menu.menu_id)), [activeMenus]);
|
const activeMenuIdSet = useMemo(() => new Set(activeMenus.map((menu) => menu.menu_id)), [activeMenus]);
|
||||||
const permissionMenuIds = useMemo(() => activeMenus.map((menu) => String(menu.menu_id)), [activeMenus]);
|
const permissionMenuIds = useMemo(() => activeMenus.map((menu) => String(menu.menu_id)), [activeMenus]);
|
||||||
|
|
@ -197,7 +210,7 @@ const PermissionManagement = () => {
|
||||||
setExpandedManageKeys(allMenuIds);
|
setExpandedManageKeys(allMenuIds);
|
||||||
}, [allMenuIds]);
|
}, [allMenuIds]);
|
||||||
|
|
||||||
const fetchBaseData = async () => {
|
const fetchBaseData = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const [rolesRes, menusRes, rolePermsRes] = await Promise.all([
|
const [rolesRes, menusRes, rolePermsRes] = await Promise.all([
|
||||||
|
|
@ -214,47 +227,51 @@ const PermissionManagement = () => {
|
||||||
setMenus(menusList);
|
setMenus(menusList);
|
||||||
setPermissionsByRole(permsMap);
|
setPermissionsByRole(permsMap);
|
||||||
setBaselinePermissionsByRole(permsMap);
|
setBaselinePermissionsByRole(permsMap);
|
||||||
|
setSelectedRoleId((prevSelectedRoleId) => {
|
||||||
if (!selectedRoleId && rolesList.length > 0) {
|
if (!prevSelectedRoleId && rolesList.length > 0) {
|
||||||
setSelectedRoleId(rolesList[0].role_id);
|
return rolesList[0].role_id;
|
||||||
} else if (selectedRoleId && !rolesList.find((r) => r.role_id === selectedRoleId)) {
|
}
|
||||||
setSelectedRoleId(rolesList[0]?.role_id || null);
|
if (prevSelectedRoleId && !rolesList.find((r) => r.role_id === prevSelectedRoleId)) {
|
||||||
}
|
return rolesList[0]?.role_id || null;
|
||||||
|
}
|
||||||
|
return prevSelectedRoleId;
|
||||||
|
});
|
||||||
} catch {
|
} catch {
|
||||||
message.error('获取权限数据失败');
|
message.error('获取权限数据失败');
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
}, [message]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchBaseData();
|
fetchBaseData();
|
||||||
}, []);
|
}, [fetchBaseData]);
|
||||||
|
|
||||||
|
const fetchRoleUsers = useCallback(async () => {
|
||||||
|
if (!selectedRoleId) {
|
||||||
|
setRoleUsers([]);
|
||||||
|
setRoleUsersTotal(0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setRoleUsersLoading(true);
|
||||||
|
try {
|
||||||
|
const res = await apiClient.get(
|
||||||
|
buildApiUrl(`/api/admin/roles/${selectedRoleId}/users?page=${roleUsersPage}&size=${roleUsersPageSize}`),
|
||||||
|
);
|
||||||
|
setRoleUsers(res.data.users || []);
|
||||||
|
setRoleUsersTotal(res.data.total || 0);
|
||||||
|
} catch {
|
||||||
|
setRoleUsers([]);
|
||||||
|
setRoleUsersTotal(0);
|
||||||
|
message.error('获取角色用户失败');
|
||||||
|
} finally {
|
||||||
|
setRoleUsersLoading(false);
|
||||||
|
}
|
||||||
|
}, [message, roleUsersPage, roleUsersPageSize, selectedRoleId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchRoleUsers = async () => {
|
|
||||||
if (!selectedRoleId) {
|
|
||||||
setRoleUsers([]);
|
|
||||||
setRoleUsersTotal(0);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setRoleUsersLoading(true);
|
|
||||||
try {
|
|
||||||
const res = await apiClient.get(
|
|
||||||
buildApiUrl(`/api/admin/roles/${selectedRoleId}/users?page=${roleUsersPage}&size=${roleUsersPageSize}`),
|
|
||||||
);
|
|
||||||
setRoleUsers(res.data.users || []);
|
|
||||||
setRoleUsersTotal(res.data.total || 0);
|
|
||||||
} catch {
|
|
||||||
setRoleUsers([]);
|
|
||||||
setRoleUsersTotal(0);
|
|
||||||
message.error('获取角色用户失败');
|
|
||||||
} finally {
|
|
||||||
setRoleUsersLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
fetchRoleUsers();
|
fetchRoleUsers();
|
||||||
}, [selectedRoleId, roleUsersPage, roleUsersPageSize]);
|
}, [fetchRoleUsers]);
|
||||||
|
|
||||||
const saveSelectedRolePermissions = async () => {
|
const saveSelectedRolePermissions = async () => {
|
||||||
if (!selectedRoleId) return;
|
if (!selectedRoleId) return;
|
||||||
|
|
@ -357,7 +374,7 @@ const PermissionManagement = () => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const openViewMenuPanel = (menu) => {
|
const openViewMenuPanel = useCallback((menu) => {
|
||||||
setEditingMenu(menu);
|
setEditingMenu(menu);
|
||||||
setMenuPanelMode('view');
|
setMenuPanelMode('view');
|
||||||
setMenuIconPickerOpen(false);
|
setMenuIconPickerOpen(false);
|
||||||
|
|
@ -373,7 +390,7 @@ const PermissionManagement = () => {
|
||||||
is_active: Boolean(menu.is_active),
|
is_active: Boolean(menu.is_active),
|
||||||
description: menu.description,
|
description: menu.description,
|
||||||
});
|
});
|
||||||
};
|
}, [menuForm]);
|
||||||
|
|
||||||
const submitMenu = async () => {
|
const submitMenu = async () => {
|
||||||
const values = await menuForm.validateFields();
|
const values = await menuForm.validateFields();
|
||||||
|
|
@ -530,7 +547,7 @@ const PermissionManagement = () => {
|
||||||
if (menuPanelMode === 'view') {
|
if (menuPanelMode === 'view') {
|
||||||
openViewMenuPanel(selectedManageMenu);
|
openViewMenuPanel(selectedManageMenu);
|
||||||
}
|
}
|
||||||
}, [selectedManageMenu, menuPanelMode]);
|
}, [menuPanelMode, openViewMenuPanel, selectedManageMenu]);
|
||||||
|
|
||||||
const handleMenuTreeDrop = async (info) => {
|
const handleMenuTreeDrop = async (info) => {
|
||||||
const dragId = Number(info.dragNode?.key);
|
const dragId = Number(info.dragNode?.key);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
Layout, List, Button, Space, Typography, Card,
|
Layout, List, Button, Space, Typography, Card,
|
||||||
Input, Switch, App, Modal, Form, Badge, Tooltip, Divider, Checkbox, Empty, Select, Tag
|
Input, Switch, App, Modal, Form, Badge, Tooltip, Divider, Checkbox, Empty, Select, Tag
|
||||||
|
|
@ -41,18 +41,11 @@ const PromptManagement = () => {
|
||||||
const [prompts, setPrompts] = useState([]);
|
const [prompts, setPrompts] = useState([]);
|
||||||
const [selectedPrompt, setSelectedPrompt] = useState(null);
|
const [selectedPrompt, setSelectedPrompt] = useState(null);
|
||||||
const [editingPrompt, setEditingPrompt] = useState(null);
|
const [editingPrompt, setEditingPrompt] = useState(null);
|
||||||
const [editingTitle, setEditingTitle] = useState(false);
|
|
||||||
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
|
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [showCreateModal, setShowCreateModal] = useState(false);
|
const [showCreateModal, setShowCreateModal] = useState(false);
|
||||||
const [isSaving, setIsSaving] = useState(false);
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
const fetchPrompts = useCallback(async () => {
|
||||||
fetchPrompts();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const fetchPrompts = async () => {
|
|
||||||
setLoading(true);
|
|
||||||
try {
|
try {
|
||||||
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.PROMPTS.LIST));
|
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.PROMPTS.LIST));
|
||||||
const list = response.data.prompts || [];
|
const list = response.data.prompts || [];
|
||||||
|
|
@ -61,15 +54,18 @@ const PromptManagement = () => {
|
||||||
setSelectedPrompt(list[0]);
|
setSelectedPrompt(list[0]);
|
||||||
setEditingPrompt({ ...list[0] });
|
setEditingPrompt({ ...list[0] });
|
||||||
}
|
}
|
||||||
} finally {
|
} catch {
|
||||||
setLoading(false);
|
message.error('加载模板失败');
|
||||||
}
|
}
|
||||||
};
|
}, [message, selectedPrompt]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchPrompts();
|
||||||
|
}, [fetchPrompts]);
|
||||||
|
|
||||||
const handlePromptSelect = (prompt) => {
|
const handlePromptSelect = (prompt) => {
|
||||||
setSelectedPrompt(prompt);
|
setSelectedPrompt(prompt);
|
||||||
setEditingPrompt({ ...prompt });
|
setEditingPrompt({ ...prompt });
|
||||||
setEditingTitle(false);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEditChange = (field, value) => {
|
const handleEditChange = (field, value) => {
|
||||||
|
|
@ -88,7 +84,7 @@ const PromptManagement = () => {
|
||||||
await apiClient.put(buildApiUrl(API_ENDPOINTS.PROMPTS.UPDATE(editingPrompt.id)), payload);
|
await apiClient.put(buildApiUrl(API_ENDPOINTS.PROMPTS.UPDATE(editingPrompt.id)), payload);
|
||||||
message.success('保存成功');
|
message.success('保存成功');
|
||||||
fetchPrompts();
|
fetchPrompts();
|
||||||
} catch (e) {
|
} catch {
|
||||||
message.error('保存失败');
|
message.error('保存失败');
|
||||||
} finally {
|
} finally {
|
||||||
setIsSaving(false);
|
setIsSaving(false);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useEffect, useMemo, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
App,
|
App,
|
||||||
Alert,
|
Alert,
|
||||||
|
|
@ -75,7 +75,7 @@ const SystemManagementOverview = () => {
|
||||||
const [apps, setApps] = useState([]);
|
const [apps, setApps] = useState([]);
|
||||||
const [terminals, setTerminals] = useState([]);
|
const [terminals, setTerminals] = useState([]);
|
||||||
|
|
||||||
const fetchOverviewData = async () => {
|
const fetchOverviewData = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const [clientRes, appRes, terminalRes] = await Promise.all([
|
const [clientRes, appRes, terminalRes] = await Promise.all([
|
||||||
|
|
@ -92,11 +92,11 @@ const SystemManagementOverview = () => {
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
}, [message]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchOverviewData();
|
fetchOverviewData();
|
||||||
}, []);
|
}, [fetchOverviewData]);
|
||||||
|
|
||||||
const governanceItems = useMemo(() => {
|
const governanceItems = useMemo(() => {
|
||||||
const inactiveLatestCount = clients.filter((item) => {
|
const inactiveLatestCount = clients.filter((item) => {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useEffect, useMemo, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
Button,
|
Button,
|
||||||
|
|
@ -34,6 +34,7 @@ import apiClient from '../../utils/apiClient';
|
||||||
import { buildApiUrl, API_ENDPOINTS } from '../../config/api';
|
import { buildApiUrl, API_ENDPOINTS } from '../../config/api';
|
||||||
import AdminModuleShell from '../../components/AdminModuleShell';
|
import AdminModuleShell from '../../components/AdminModuleShell';
|
||||||
import ActionButton from '../../components/ActionButton';
|
import ActionButton from '../../components/ActionButton';
|
||||||
|
import useSystemPageSize from '../../hooks/useSystemPageSize';
|
||||||
|
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
const { TextArea } = Input;
|
const { TextArea } = Input;
|
||||||
|
|
@ -43,6 +44,7 @@ const ONLINE_MINUTES = 10;
|
||||||
const TerminalManagement = () => {
|
const TerminalManagement = () => {
|
||||||
const { message, modal } = App.useApp();
|
const { message, modal } = App.useApp();
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
|
const pageSize = useSystemPageSize(10);
|
||||||
|
|
||||||
const [terminals, setTerminals] = useState([]);
|
const [terminals, setTerminals] = useState([]);
|
||||||
const [terminalTypes, setTerminalTypes] = useState([]);
|
const [terminalTypes, setTerminalTypes] = useState([]);
|
||||||
|
|
@ -57,12 +59,7 @@ const TerminalManagement = () => {
|
||||||
const [isEditing, setIsEditing] = useState(false);
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
const [selectedTerminal, setSelectedTerminal] = useState(null);
|
const [selectedTerminal, setSelectedTerminal] = useState(null);
|
||||||
|
|
||||||
useEffect(() => {
|
const fetchTerminalTypes = useCallback(async () => {
|
||||||
fetchTerminalTypes();
|
|
||||||
fetchTerminals();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const fetchTerminalTypes = async () => {
|
|
||||||
try {
|
try {
|
||||||
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.DICT_DATA.BY_TYPE('client_platform')), {
|
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.DICT_DATA.BY_TYPE('client_platform')), {
|
||||||
params: { parent_code: 'TERMINAL' },
|
params: { parent_code: 'TERMINAL' },
|
||||||
|
|
@ -73,9 +70,9 @@ const TerminalManagement = () => {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch terminal types:', error);
|
console.error('Failed to fetch terminal types:', error);
|
||||||
}
|
}
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
const fetchTerminals = async () => {
|
const fetchTerminals = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.TERMINALS.LIST), {
|
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.TERMINALS.LIST), {
|
||||||
|
|
@ -89,7 +86,12 @@ const TerminalManagement = () => {
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
}, [message]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchTerminalTypes();
|
||||||
|
fetchTerminals();
|
||||||
|
}, [fetchTerminalTypes, fetchTerminals]);
|
||||||
|
|
||||||
const handleOpenModal = (terminal = null) => {
|
const handleOpenModal = (terminal = null) => {
|
||||||
if (terminal) {
|
if (terminal) {
|
||||||
|
|
@ -391,7 +393,7 @@ const TerminalManagement = () => {
|
||||||
loading={loading}
|
loading={loading}
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
scroll={{ x: 1220 }}
|
scroll={{ x: 1220 }}
|
||||||
pagination={{ pageSize: 10, showTotal: (count) => `共 ${count} 条记录` }}
|
pagination={{ pageSize, showTotal: (count) => `共 ${count} 条记录` }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</AdminModuleShell>
|
</AdminModuleShell>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useEffect, useMemo, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { Table, Button, Input, Space, Drawer, Form, Select, App, Tooltip, Tag } from 'antd';
|
import { Table, Button, Input, Space, Drawer, Form, Select, App, Tooltip, Tag } from 'antd';
|
||||||
import {
|
import {
|
||||||
PlusOutlined,
|
PlusOutlined,
|
||||||
|
|
@ -14,9 +14,9 @@ import {
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import apiClient from '../../utils/apiClient';
|
import apiClient from '../../utils/apiClient';
|
||||||
import { buildApiUrl, API_ENDPOINTS } from '../../config/api';
|
import { buildApiUrl, API_ENDPOINTS } from '../../config/api';
|
||||||
import configService from '../../utils/configService';
|
|
||||||
import AdminModuleShell from '../../components/AdminModuleShell';
|
import AdminModuleShell from '../../components/AdminModuleShell';
|
||||||
import ActionButton from '../../components/ActionButton';
|
import ActionButton from '../../components/ActionButton';
|
||||||
|
import useSystemPageSize from '../../hooks/useSystemPageSize';
|
||||||
|
|
||||||
const UserManagement = () => {
|
const UserManagement = () => {
|
||||||
const { message, modal } = App.useApp();
|
const { message, modal } = App.useApp();
|
||||||
|
|
@ -25,7 +25,7 @@ const UserManagement = () => {
|
||||||
const [users, setUsers] = useState([]);
|
const [users, setUsers] = useState([]);
|
||||||
const [total, setTotal] = useState(0);
|
const [total, setTotal] = useState(0);
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
const [pageSize, setPageSize] = useState(10);
|
const pageSize = useSystemPageSize(10);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [showUserDrawer, setShowUserDrawer] = useState(false);
|
const [showUserDrawer, setShowUserDrawer] = useState(false);
|
||||||
const [isEditing, setIsEditing] = useState(false);
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
|
|
@ -35,10 +35,6 @@ const UserManagement = () => {
|
||||||
const [debouncedSearchText, setDebouncedSearchText] = useState('');
|
const [debouncedSearchText, setDebouncedSearchText] = useState('');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
configService.getPageSize().then((size) => {
|
|
||||||
const safeSize = Number.isFinite(size) ? Math.min(100, Math.max(5, size)) : 10;
|
|
||||||
setPageSize(safeSize);
|
|
||||||
});
|
|
||||||
fetchRoles();
|
fetchRoles();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
@ -51,11 +47,7 @@ const UserManagement = () => {
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}, [searchText]);
|
}, [searchText]);
|
||||||
|
|
||||||
useEffect(() => {
|
const fetchUsers = useCallback(async () => {
|
||||||
fetchUsers();
|
|
||||||
}, [page, pageSize, debouncedSearchText]);
|
|
||||||
|
|
||||||
const fetchUsers = async () => {
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
let url = `${API_ENDPOINTS.USERS.LIST}?page=${page}&size=${pageSize}`;
|
let url = `${API_ENDPOINTS.USERS.LIST}?page=${page}&size=${pageSize}`;
|
||||||
|
|
@ -70,7 +62,11 @@ const UserManagement = () => {
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
}, [debouncedSearchText, message, page, pageSize]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchUsers();
|
||||||
|
}, [fetchUsers]);
|
||||||
|
|
||||||
const fetchRoles = async () => {
|
const fetchRoles = async () => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -239,11 +235,9 @@ const UserManagement = () => {
|
||||||
current: page,
|
current: page,
|
||||||
pageSize,
|
pageSize,
|
||||||
total,
|
total,
|
||||||
onChange: (nextPage, size) => {
|
onChange: (nextPage) => {
|
||||||
setPage(nextPage);
|
setPage(nextPage);
|
||||||
setPageSize(size);
|
|
||||||
},
|
},
|
||||||
showSizeChanger: true,
|
|
||||||
showTotal: (count) => `共 ${count} 条记录`,
|
showTotal: (count) => `共 ${count} 条记录`,
|
||||||
}}
|
}}
|
||||||
rowKey="user_id"
|
rowKey="user_id"
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
import apiClient from './apiClient';
|
import apiClient from './apiClient';
|
||||||
import { buildApiUrl, API_ENDPOINTS } from '../config/api';
|
import { buildApiUrl, API_ENDPOINTS } from '../config/api';
|
||||||
|
|
||||||
|
const PUBLIC_CONFIG_CACHE_KEY = 'imeeting_public_config_cache';
|
||||||
|
const PUBLIC_CONFIG_CACHE_TTL = 5 * 60 * 1000;
|
||||||
|
|
||||||
export const DEFAULT_BRANDING_CONFIG = {
|
export const DEFAULT_BRANDING_CONFIG = {
|
||||||
app_name: '智听云平台',
|
app_name: '智听云平台',
|
||||||
home_headline: '智听云平台',
|
|
||||||
home_tagline: '让每一次谈话都产生价值',
|
|
||||||
console_subtitle: 'iMeeting控制台',
|
console_subtitle: 'iMeeting控制台',
|
||||||
preview_title: '会议预览',
|
preview_title: '会议预览',
|
||||||
login_welcome: '欢迎回来,请输入您的登录凭证。',
|
login_welcome: '欢迎回来,请输入您的登录凭证。',
|
||||||
|
|
@ -13,13 +14,73 @@ export const DEFAULT_BRANDING_CONFIG = {
|
||||||
|
|
||||||
class ConfigService {
|
class ConfigService {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.configs = null;
|
this.configs = this.readCachedPublicConfig();
|
||||||
this.loadPromise = null;
|
this.loadPromise = null;
|
||||||
this._pageSize = null;
|
this._pageSize = this.extractPageSize(this.configs);
|
||||||
this.brandingConfig = null;
|
this.brandingConfig = this.configs ? this.buildBrandingConfig(this.configs) : null;
|
||||||
this.brandingPromise = null;
|
this.brandingPromise = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
readCachedPublicConfig() {
|
||||||
|
try {
|
||||||
|
const raw = window.localStorage.getItem(PUBLIC_CONFIG_CACHE_KEY);
|
||||||
|
if (!raw) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const parsed = JSON.parse(raw);
|
||||||
|
if (!parsed || typeof parsed !== 'object') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (parsed.data && parsed.cached_at) {
|
||||||
|
if (Date.now() - Number(parsed.cached_at) > PUBLIC_CONFIG_CACHE_TTL) {
|
||||||
|
window.localStorage.removeItem(PUBLIC_CONFIG_CACHE_KEY);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return parsed.data;
|
||||||
|
}
|
||||||
|
return parsed;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
writeCachedPublicConfig(configs) {
|
||||||
|
try {
|
||||||
|
window.localStorage.setItem(PUBLIC_CONFIG_CACHE_KEY, JSON.stringify({
|
||||||
|
data: configs || {},
|
||||||
|
cached_at: Date.now(),
|
||||||
|
}));
|
||||||
|
} catch {
|
||||||
|
// Ignore storage failures and continue with in-memory cache.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extractPageSize(configs) {
|
||||||
|
const raw = configs?.PAGE_SIZE ?? configs?.page_size;
|
||||||
|
const val = parseInt(raw, 10);
|
||||||
|
if (isNaN(val) || val <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return Math.min(100, Math.max(5, val));
|
||||||
|
}
|
||||||
|
|
||||||
|
buildBrandingConfig(configs) {
|
||||||
|
return {
|
||||||
|
...DEFAULT_BRANDING_CONFIG,
|
||||||
|
...(configs || {}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
applyPublicConfig(configs) {
|
||||||
|
this.configs = configs || null;
|
||||||
|
this._pageSize = this.extractPageSize(this.configs);
|
||||||
|
this.brandingConfig = this.buildBrandingConfig(this.configs);
|
||||||
|
if (this.configs) {
|
||||||
|
this.writeCachedPublicConfig(this.configs);
|
||||||
|
}
|
||||||
|
return this.configs;
|
||||||
|
}
|
||||||
|
|
||||||
async getConfigs() {
|
async getConfigs() {
|
||||||
if (this.configs) {
|
if (this.configs) {
|
||||||
return this.configs;
|
return this.configs;
|
||||||
|
|
@ -30,21 +91,24 @@ class ConfigService {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.loadPromise = this.loadConfigsFromServer();
|
this.loadPromise = this.loadConfigsFromServer();
|
||||||
this.configs = await this.loadPromise;
|
const configs = await this.loadPromise;
|
||||||
return this.configs;
|
this.loadPromise = null;
|
||||||
|
return configs;
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadConfigsFromServer() {
|
async loadConfigsFromServer() {
|
||||||
try {
|
try {
|
||||||
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.PUBLIC.SYSTEM_CONFIG));
|
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.PUBLIC.SYSTEM_CONFIG));
|
||||||
return response.data;
|
return this.applyPublicConfig(response.data || {});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Failed to load system configs, using defaults:', error);
|
console.warn('Failed to load system configs, using defaults:', error);
|
||||||
// 返回默认配置
|
// 返回默认配置
|
||||||
return {
|
return this.applyPublicConfig({
|
||||||
MAX_FILE_SIZE: 100 * 1024 * 1024, // 100MB
|
MAX_FILE_SIZE: 100 * 1024 * 1024, // 100MB
|
||||||
MAX_IMAGE_SIZE: 10 * 1024 * 1024 // 10MB
|
MAX_IMAGE_SIZE: 10 * 1024 * 1024, // 10MB
|
||||||
};
|
PAGE_SIZE: 10,
|
||||||
|
page_size: '10',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -65,21 +129,22 @@ class ConfigService {
|
||||||
async getPageSize() {
|
async getPageSize() {
|
||||||
if (this._pageSize) return this._pageSize;
|
if (this._pageSize) return this._pageSize;
|
||||||
try {
|
try {
|
||||||
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.ADMIN.PARAMETER_DETAIL('timeline_pagesize')));
|
const configs = await this.getConfigs();
|
||||||
const raw = response.data?.param_value;
|
const cachedPageSize = this.extractPageSize(configs);
|
||||||
const val = parseInt(raw);
|
if (cachedPageSize) {
|
||||||
if (!isNaN(val) && val > 0) {
|
this._pageSize = cachedPageSize;
|
||||||
// 防御异常配置,避免一次性拉取过多数据导致页面卡顿
|
return cachedPageSize;
|
||||||
const safeVal = Math.min(100, Math.max(5, val));
|
|
||||||
this._pageSize = safeVal;
|
|
||||||
return safeVal;
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (error) {
|
||||||
console.warn('Failed to load page_size config:', e.message);
|
console.warn('Failed to load page_size config:', error.message);
|
||||||
}
|
}
|
||||||
return 10;
|
return 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getCachedPageSize() {
|
||||||
|
return this._pageSize;
|
||||||
|
}
|
||||||
|
|
||||||
async getBrandingConfig() {
|
async getBrandingConfig() {
|
||||||
if (this.brandingConfig) {
|
if (this.brandingConfig) {
|
||||||
return this.brandingConfig;
|
return this.brandingConfig;
|
||||||
|
|
@ -90,17 +155,15 @@ class ConfigService {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.brandingPromise = this.loadBrandingConfigFromServer();
|
this.brandingPromise = this.loadBrandingConfigFromServer();
|
||||||
this.brandingConfig = await this.brandingPromise;
|
const branding = await this.brandingPromise;
|
||||||
return this.brandingConfig;
|
this.brandingPromise = null;
|
||||||
|
return branding;
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadBrandingConfigFromServer() {
|
async loadBrandingConfigFromServer() {
|
||||||
try {
|
try {
|
||||||
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.PUBLIC.SYSTEM_CONFIG));
|
const configs = await this.getConfigs();
|
||||||
return {
|
return this.buildBrandingConfig(configs);
|
||||||
...DEFAULT_BRANDING_CONFIG,
|
|
||||||
...(response.data || {}),
|
|
||||||
};
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Failed to load branding configs, using defaults:', error);
|
console.warn('Failed to load branding configs, using defaults:', error);
|
||||||
return { ...DEFAULT_BRANDING_CONFIG };
|
return { ...DEFAULT_BRANDING_CONFIG };
|
||||||
|
|
@ -123,6 +186,11 @@ class ConfigService {
|
||||||
this._pageSize = null;
|
this._pageSize = null;
|
||||||
this.brandingConfig = null;
|
this.brandingConfig = null;
|
||||||
this.brandingPromise = null;
|
this.brandingPromise = null;
|
||||||
|
try {
|
||||||
|
window.localStorage.removeItem(PUBLIC_CONFIG_CACHE_KEY);
|
||||||
|
} catch {
|
||||||
|
// Ignore storage cleanup failures.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -95,7 +95,6 @@ const ICON_COMPONENTS = {
|
||||||
FileTextOutlined,
|
FileTextOutlined,
|
||||||
ClusterOutlined,
|
ClusterOutlined,
|
||||||
BranchesOutlined,
|
BranchesOutlined,
|
||||||
ApartmentOutlined,
|
|
||||||
NotificationOutlined,
|
NotificationOutlined,
|
||||||
CommentOutlined,
|
CommentOutlined,
|
||||||
CustomerServiceOutlined,
|
CustomerServiceOutlined,
|
||||||
|
|
|
||||||
|
|
@ -316,7 +316,7 @@ export function deepClone(obj) {
|
||||||
if (obj instanceof Object) {
|
if (obj instanceof Object) {
|
||||||
const clonedObj = {};
|
const clonedObj = {};
|
||||||
for (const key in obj) {
|
for (const key in obj) {
|
||||||
if (obj.hasOwnProperty(key)) {
|
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
||||||
clonedObj[key] = deepClone(obj[key]);
|
clonedObj[key] = deepClone(obj[key]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue