feat(system): 添加国际化支持和系统配置管理功能

- 集成 i18next 多语言框架,实现中英双语支持
- 创建完整的多语言资源配置文件 (en-US.json)
- 实现平台配置管理页面,支持项目名称、Logo 等可视化设置
- 新增系统参数管理页面,支持参数键值对的增删改查操作
- 添加后端平台配置控制器和相关实体服务层
- 实现系统参数分页查询、创建、更新和删除接口
- 配置多语言检测器和默认语言设置
master
chenhao 2026-02-26 16:31:02 +08:00
parent 9b721929c6
commit 8959680d31
20 changed files with 1555 additions and 0 deletions

View File

@ -0,0 +1,35 @@
package com.imeeting.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.io.File;
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Value("${app.upload-path}")
private String uploadPath;
@Value("${app.resource-prefix}")
private String resourcePrefix;
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
// 确保目录存在
File directory = new File(uploadPath);
if (!directory.exists()) {
directory.mkdirs();
}
// 将资源前缀映射到物理路径
// 注意addResourceLocations 需要以 file: 开头
String pathPattern = resourcePrefix.endsWith("/") ? resourcePrefix + "**" : resourcePrefix + "/**";
String location = uploadPath.endsWith("/") ? "file:" + uploadPath : "file:" + uploadPath + "/";
registry.addResourceHandler(pathPattern)
.addResourceLocations(location);
}
}

View File

@ -0,0 +1,55 @@
package com.imeeting.controller;
import com.imeeting.common.ApiResponse;
import com.imeeting.dto.PlatformConfigVO;
import com.imeeting.entity.SysPlatformConfig;
import com.imeeting.service.SysPlatformConfigService;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
@RestController
@RequestMapping("/api")
public class PlatformConfigController {
private final SysPlatformConfigService platformConfigService;
public PlatformConfigController(SysPlatformConfigService platformConfigService) {
this.platformConfigService = platformConfigService;
}
/**
* (favicon)
*/
@GetMapping("/open/platform/config")
public ApiResponse<PlatformConfigVO> getOpenConfig() {
return ApiResponse.ok(platformConfigService.getConfig());
}
/**
* ()
*/
@GetMapping("/admin/platform/config")
@PreAuthorize("isAuthenticated()")
public ApiResponse<PlatformConfigVO> getAdminConfig() {
return ApiResponse.ok(platformConfigService.getConfig());
}
/**
* ()
*/
@PutMapping("/admin/platform/config")
@PreAuthorize("hasRole('ADMIN') or @ss.hasPermi('sys_platform:config:update')")
public ApiResponse<Boolean> updateConfig(@RequestBody SysPlatformConfig config) {
return ApiResponse.ok(platformConfigService.updateConfig(config));
}
/**
* ()
*/
@PostMapping("/admin/platform/config/upload")
@PreAuthorize("hasRole('ADMIN') or @ss.hasPermi('sys_platform:config:update')")
public ApiResponse<String> upload(@RequestParam("file") MultipartFile file) {
return ApiResponse.ok(platformConfigService.uploadAsset(file));
}
}

View File

@ -0,0 +1,15 @@
package com.imeeting.dto;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class PlatformConfigVO {
private String projectName;
private String logoUrl;
private String iconUrl;
private String loginBgUrl;
private String icpInfo;
private String copyrightInfo;
private String systemDescription;
}

View File

@ -0,0 +1,12 @@
package com.imeeting.dto;
import lombok.Data;
@Data
public class SysParamQueryDTO {
private String paramKey;
private String paramType;
private String description;
private Integer pageNum = 1;
private Integer pageSize = 10;
}

View File

@ -0,0 +1,17 @@
package com.imeeting.dto;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class SysParamVO {
private Long paramId;
private String paramKey;
private String paramValue;
private String paramType;
private Integer isSystem;
private String description;
private Integer status;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}

View File

@ -0,0 +1,25 @@
package com.imeeting.entity;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.TableField;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("sys_platform_config")
public class SysPlatformConfig extends BaseEntity {
@TableId
private Long id;
private String projectName;
private String logoUrl;
private String iconUrl;
private String loginBgUrl;
private String icpInfo;
private String copyrightInfo;
private String systemDescription;
@TableField(exist = false)
private Long tenantId;
}

View File

@ -0,0 +1,25 @@
package com.imeeting.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("sys_tenant_user")
public class SysTenantUser extends BaseEntity {
@TableId(type = IdType.AUTO)
private Long id;
private Long userId;
private Long tenantId;
private Long orgId;
@com.baomidou.mybatisplus.annotation.TableField(exist = false)
private String orgName;
@com.baomidou.mybatisplus.annotation.TableLogic(value = "0", delval = "0")
@com.baomidou.mybatisplus.annotation.TableField(exist = false)
private Integer isDeleted;
}

View File

@ -0,0 +1,9 @@
package com.imeeting.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.imeeting.entity.SysPlatformConfig;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface SysPlatformConfigMapper extends BaseMapper<SysPlatformConfig> {
}

View File

@ -0,0 +1,9 @@
package com.imeeting.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.imeeting.entity.SysTenantUser;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface SysTenantUserMapper extends BaseMapper<SysTenantUser> {
}

View File

@ -0,0 +1,12 @@
package com.imeeting.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.imeeting.entity.SysPlatformConfig;
import com.imeeting.dto.PlatformConfigVO;
import org.springframework.web.multipart.MultipartFile;
public interface SysPlatformConfigService extends IService<SysPlatformConfig> {
PlatformConfigVO getConfig();
boolean updateConfig(SysPlatformConfig config);
String uploadAsset(MultipartFile file);
}

View File

@ -0,0 +1,11 @@
package com.imeeting.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.imeeting.entity.SysTenantUser;
import java.util.List;
public interface SysTenantUserService extends IService<SysTenantUser> {
List<SysTenantUser> listByUserId(Long userId);
void saveTenantUser(Long userId, Long tenantId, Long orgId);
void syncMemberships(Long userId, List<SysTenantUser> memberships);
}

View File

@ -0,0 +1,119 @@
package com.imeeting.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.imeeting.common.RedisKeys;
import com.imeeting.dto.PlatformConfigVO;
import com.imeeting.entity.SysPlatformConfig;
import com.imeeting.mapper.SysPlatformConfigMapper;
import com.imeeting.service.SysPlatformConfigService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.Duration;
import java.util.UUID;
@Slf4j
@Service
public class SysPlatformConfigServiceImpl extends ServiceImpl<SysPlatformConfigMapper, SysPlatformConfig> implements SysPlatformConfigService {
private final StringRedisTemplate redisTemplate;
private final ObjectMapper objectMapper;
@Value("${app.upload-path}")
private String uploadPath;
@Value("${app.resource-prefix}")
private String resourcePrefix;
public SysPlatformConfigServiceImpl(StringRedisTemplate redisTemplate, ObjectMapper objectMapper) {
this.redisTemplate = redisTemplate;
this.objectMapper = objectMapper;
}
@Override
public PlatformConfigVO getConfig() {
String key = RedisKeys.platformConfigKey();
try {
String cached = redisTemplate.opsForValue().get(key);
if (cached != null) {
return objectMapper.readValue(cached, PlatformConfigVO.class);
}
} catch (Exception e) {
log.error("Read platform config from redis error", e);
}
SysPlatformConfig config = getById(1L);
if (config == null) {
return new PlatformConfigVO();
}
PlatformConfigVO vo = toVO(config);
try {
redisTemplate.opsForValue().set(key, objectMapper.writeValueAsString(vo), Duration.ofDays(1));
} catch (Exception e) {
log.error("Write platform config to redis error", e);
}
return vo;
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean updateConfig(SysPlatformConfig config) {
config.setId(1L);
SysPlatformConfig old = getById(1L);
boolean success = updateById(config);
if (success) {
redisTemplate.delete(RedisKeys.platformConfigKey());
// 物理文件清理逻辑可以在这里根据需要扩展(例如对比 old 和 config 的 URL
}
return success;
}
@Override
public String uploadAsset(MultipartFile file) {
if (file.isEmpty()) {
throw new RuntimeException("File is empty");
}
String originalFilename = file.getOriginalFilename();
String extension = "";
if (originalFilename != null && originalFilename.contains(".")) {
extension = originalFilename.substring(originalFilename.lastIndexOf("."));
}
String fileName = UUID.randomUUID().toString() + extension;
Path path = Paths.get(uploadPath, fileName);
try {
Files.copy(file.getInputStream(), path);
String prefix = resourcePrefix.endsWith("/") ? resourcePrefix : resourcePrefix + "/";
return prefix + fileName;
} catch (IOException e) {
log.error("Upload asset error", e);
throw new RuntimeException("Failed to store file");
}
}
private PlatformConfigVO toVO(SysPlatformConfig entity) {
PlatformConfigVO vo = new PlatformConfigVO();
vo.setProjectName(entity.getProjectName());
vo.setLogoUrl(entity.getLogoUrl());
vo.setIconUrl(entity.getIconUrl());
vo.setLoginBgUrl(entity.getLoginBgUrl());
vo.setIcpInfo(entity.getIcpInfo());
vo.setCopyrightInfo(entity.getCopyrightInfo());
vo.setSystemDescription(entity.getSystemDescription());
return vo;
}
}

View File

@ -0,0 +1,75 @@
package com.imeeting.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.imeeting.entity.SysTenantUser;
import com.imeeting.mapper.SysTenantUserMapper;
import com.imeeting.service.SysTenantUserService;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class SysTenantUserServiceImpl extends ServiceImpl<SysTenantUserMapper, SysTenantUser> implements SysTenantUserService {
private final com.imeeting.service.SysOrgService sysOrgService;
public SysTenantUserServiceImpl(com.imeeting.service.SysOrgService sysOrgService) {
this.sysOrgService = sysOrgService;
}
@Override
public List<SysTenantUser> listByUserId(Long userId) {
List<SysTenantUser> list = list(new LambdaQueryWrapper<SysTenantUser>().eq(SysTenantUser::getUserId, userId));
if (list != null && !list.isEmpty()) {
for (SysTenantUser tu : list) {
if (tu.getOrgId() != null) {
com.imeeting.entity.SysOrg org = sysOrgService.getById(tu.getOrgId());
if (org != null) {
tu.setOrgName(org.getOrgName());
}
}
}
}
return list;
}
@Override
public void saveTenantUser(Long userId, Long tenantId, Long orgId) {
LambdaQueryWrapper<SysTenantUser> query = new LambdaQueryWrapper<SysTenantUser>()
.eq(SysTenantUser::getUserId, userId)
.eq(SysTenantUser::getTenantId, tenantId);
SysTenantUser existing = getOne(query);
if (existing != null) {
existing.setOrgId(orgId);
updateById(existing);
} else {
SysTenantUser tu = new SysTenantUser();
tu.setUserId(userId);
tu.setTenantId(tenantId);
tu.setOrgId(orgId);
save(tu);
}
}
@Override
public void syncMemberships(Long userId, List<SysTenantUser> memberships) {
if (userId == null) return;
// 1. Physical removal of all existing memberships for this user
getBaseMapper().delete(new LambdaQueryWrapper<SysTenantUser>().eq(SysTenantUser::getUserId, userId));
// 2. Add new ones
if (memberships != null && !memberships.isEmpty()) {
java.util.Set<Long> processedTenants = new java.util.HashSet<>();
for (SysTenantUser m : memberships) {
if (m.getTenantId() != null && processedTenants.add(m.getTenantId())) {
SysTenantUser tu = new SysTenantUser();
tu.setUserId(userId);
tu.setTenantId(m.getTenantId());
tu.setOrgId(m.getOrgId());
save(tu);
}
}
}
}
}

View File

@ -0,0 +1,41 @@
import http from "./http";
import { SysPlatformConfig } from "../types";
/**
*
*/
export async function getOpenPlatformConfig() {
const resp = await http.get("/api/open/platform/config");
return resp.data.data as SysPlatformConfig;
}
/**
*
*/
export async function getAdminPlatformConfig() {
const resp = await http.get("/api/admin/platform/config");
return resp.data.data as SysPlatformConfig;
}
/**
*
*/
export async function updatePlatformConfig(payload: SysPlatformConfig) {
const resp = await http.put("/api/admin/platform/config", payload);
return resp.data.data as boolean;
}
/**
*
* @param file
*/
export async function uploadPlatformAsset(file: File) {
const formData = new FormData();
formData.append("file", file);
const resp = await http.post("/api/admin/platform/config/upload", formData, {
headers: {
"Content-Type": "multipart/form-data",
},
});
return resp.data.data as string;
}

View File

@ -0,0 +1,25 @@
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
import zhCN from './locales/zh-CN.json';
import enUS from './locales/en-US.json';
i18n
.use(LanguageDetector)
.use(initReactI18next)
.init({
resources: {
'zh-CN': {
translation: zhCN,
},
'en-US': {
translation: enUS,
},
},
fallbackLng: 'zh-CN',
interpolation: {
escapeValue: false,
},
});
export default i18n;

View File

@ -0,0 +1,231 @@
{
"common": {
"search": "Search",
"reset": "Reset",
"create": "Create",
"edit": "Edit",
"delete": "Delete",
"confirm": "Confirm",
"cancel": "Cancel",
"save": "Save",
"action": "Action",
"status": "Status",
"remark": "Remark",
"refresh": "Refresh",
"success": "Success",
"error": "Error",
"total": "Total {{total}} items",
"loading": "Loading...",
"view": "View"
},
"layout": {
"profile": "Profile",
"settings": "Settings",
"logout": "Logout",
"language": "Language",
"notification": "Notification"
},
"dashboard": {
"title": "Dashboard",
"subtitle": "Real-time monitoring of meeting transcription and system metrics",
"todayMeetings": "Today's Meetings",
"activeDevices": "Active Devices",
"transcriptionDuration": "Transcription Duration",
"totalUsers": "Total Users",
"recentMeetings": "Recent Meetings",
"deviceLoad": "Device Load",
"viewAll": "View All",
"meetingName": "Meeting Name",
"startTime": "Start Time",
"duration": "Duration"
},
"users": {
"title": "User Management",
"subtitle": "Manage user information, organizations, and roles across tenants",
"userInfo": "User Info",
"org": "Tenant/Org",
"platformAdmin": "Platform Admin",
"searchPlaceholder": "Search username, name, or email...",
"tenantFilter": "Filter by tenant...",
"drawerTitleCreate": "Create User",
"drawerTitleEdit": "Edit User",
"username": "Username",
"displayName": "Display Name",
"email": "Email",
"phone": "Phone",
"password": "Password",
"roles": "Roles",
"tenant": "Tenant",
"orgNode": "Organization"
},
"permissions": {
"title": "Permission Management",
"subtitle": "Configure system menu structure and function buttons",
"permName": "Name",
"permCode": "Code",
"permType": "Type",
"sort": "Sort",
"route": "Route/Component",
"visible": "Visible",
"isVisible": "Visible in Menu",
"drawerTitleCreate": "Create Permission",
"drawerTitleEdit": "Edit Permission",
"parentId": "Parent",
"level": "Level",
"path": "Route Path",
"component": "Component",
"icon": "Icon",
"description": "Description"
},
"roles": {
"title": "System Roles",
"subtitle": "Manage system roles and their associated permissions and users",
"roleName": "Role Name",
"roleCode": "Role Code",
"searchPlaceholder": "Search roles...",
"drawerTitleCreate": "Create Role",
"drawerTitleEdit": "Edit Role Info",
"funcPerms": "Functional Permissions",
"assignedUsers": "Assigned Users",
"savePerms": "Save Permission Changes",
"selectRole": "Please select a role from the left list to manage"
},
"login": {
"welcome": "Welcome back",
"subtitle": "Please log in to your account",
"username": "Username",
"password": "Password",
"tenantCode": "Tenant Code",
"tenantCodePlaceholder": "Tenant Code (Empty for Platform Admin)",
"captcha": "Captcha",
"rememberMe": "Remember Me",
"forgotPassword": "Forgot Password?",
"submit": "Login Now",
"loggingIn": "Logging in...",
"demoAccount": "Demo Account",
"heroTitle1": "Intelligent Meeting",
"heroTitle2": "Real-time Voice",
"heroTitle3": "System",
"heroDesc": "Full-process automated meeting records, voiceprint recognition, and smart summaries. Next-gen solution for team collaboration efficiency.",
"enterpriseSecurity": "Enterprise Security",
"multiLang": "Multi-language Support",
"loginTimeout": "Login timeout, please log in again"
},
"devices": {
"title": "Device Management",
"subtitle": "Manage hardware terminals and associated users",
"deviceInfo": "Device Info",
"owner": "Owner",
"updateTime": "Update Time",
"searchPlaceholder": "Search device name, code, or owner...",
"drawerTitleCreate": "Register Device",
"drawerTitleEdit": "Edit Device Info",
"deviceCode": "Device Code",
"deviceName": "Device Name"
},
"dicts": {
"title": "Data Dictionaries",
"subtitle": "Manage system enumerations and constants",
"dictType": "Dict Type",
"dictItem": "Dict Items",
"typeName": "Type Name",
"typeCode": "Type Code",
"itemLabel": "Item Label",
"itemValue": "Item Value",
"sort": "Sort",
"selectType": "Please select a dictionary type from the left",
"drawerTitleTypeCreate": "Create Dict Type",
"drawerTitleTypeEdit": "Edit Dict Type",
"drawerTitleItemCreate": "Create Dict Item",
"drawerTitleItemEdit": "Edit Dict Item"
},
"logs": {
"title": "Log Management",
"subtitle": "Track important system operations for security and traceability",
"opLog": "Operation Log",
"loginLog": "Login Log",
"opAccount": "Account",
"opDetail": "Operation",
"ip": "IP Address",
"duration": "Duration",
"time": "Time",
"method": "Method",
"params": "Parameters",
"searchPlaceholder": "Search operations...",
"detailTitle": "Log Details"
},
"tenants": {
"title": "Tenant Management",
"subtitle": "Manage system tenants, their status and expiration",
"tenantInfo": "Tenant Info",
"contact": "Contact",
"expireTime": "Expiration",
"forever": "Permanent",
"drawerTitleCreate": "Create Tenant",
"drawerTitleEdit": "Edit Tenant",
"tenantName": "Tenant Name",
"tenantCode": "Tenant Code",
"contactName": "Contact Name",
"contactPhone": "Phone"
},
"orgs": {
"title": "Organization Management",
"subtitle": "Manage internal department hierarchy and multi-tenant isolation",
"orgName": "Organization Name",
"orgCode": "Organization Code",
"parentOrg": "Parent Department",
"rootOrg": "Root Organization",
"addSub": "Add Sub-department",
"drawerTitleCreate": "Create Department",
"drawerTitleEdit": "Edit Organization Node",
"selectTenant": "Please select a tenant to view its organization structure",
"createRoot": "Create Root Organization",
"sort": "sort"
},
"userRole": {
"title": "User-Role Authorization",
"subtitle": "Assign system access roles to specific users and control their operational boundaries",
"userList": "User List",
"grantRoles": "Grant Role Permissions",
"searchUser": "Search username or display name...",
"selectUser": "Please select a user from the left list first",
"editing": "Editing"
},
"rolePerm": {
"title": "Role-Permission Authorization",
"subtitle": "Configure menu access and function points for various roles in the system",
"roleList": "System Role List",
"permConfig": "Permission Configuration",
"searchRole": "Search role name or code...",
"selectRole": "Please select a role from the left list first",
"currentRole": "Current Role",
"savePolicy": "Save Permission Policy"
},
"sysParams": {
"title": "System Parameters",
"subtitle": "Configure various dynamic parameters, configuration items and switches required for system operation",
"paramKey": "Parameter Key",
"paramValue": "Parameter Value",
"paramType": "Parameter Type",
"isSystem": "System Built-in",
"description": "Description",
"searchPlaceholder": "Search parameter key or description...",
"drawerTitleCreate": "Create System Parameter",
"drawerTitleEdit": "Edit System Parameter",
"yes": "Yes",
"no": "No"
},
"platformSettings": {
"title": "Platform Settings",
"subtitle": "Manage system branding, visual style, and legal compliance info",
"projectName": "Project Name",
"logo": "System Logo",
"icon": "Browser Icon",
"loginBg": "Login Background",
"icp": "ICP Number",
"copyright": "Copyright",
"desc": "System Description",
"uploadHint": "Click or drag to upload",
"uploadLimit": "Recommend 1:1 ratio, max 2MB"
}
}

View File

@ -0,0 +1,231 @@
{
"common": {
"search": "查询",
"reset": "重置",
"create": "新增",
"edit": "编辑",
"delete": "删除",
"confirm": "确定",
"cancel": "取消",
"save": "保存",
"action": "操作",
"status": "状态",
"remark": "备注",
"refresh": "刷新",
"success": "操作成功",
"error": "操作失败",
"total": "共 {{total}} 条数据",
"loading": "加载中...",
"view": "查看"
},
"layout": {
"profile": "个人信息",
"settings": "系统设置",
"logout": "退出登录",
"language": "语言",
"notification": "通知"
},
"dashboard": {
"title": "系统总览",
"subtitle": "实时监控会议转录状态与系统关键指标",
"todayMeetings": "今日会议",
"activeDevices": "活跃设备",
"transcriptionDuration": "转录时长",
"totalUsers": "总用户数",
"recentMeetings": "最近会议",
"deviceLoad": "设备负载",
"viewAll": "查看全部",
"meetingName": "会议名称",
"startTime": "开始时间",
"duration": "时长"
},
"users": {
"title": "系统用户管理",
"subtitle": "维护系统多租户下的用户信息、组织归属及权限角色",
"userInfo": "用户信息",
"org": "所属租户/组织",
"platformAdmin": "平台管理",
"searchPlaceholder": "搜索用户名、姓名或邮箱...",
"tenantFilter": "按租户筛选...",
"drawerTitleCreate": "创建系统用户",
"drawerTitleEdit": "修改用户信息",
"username": "用户名",
"displayName": "显示姓名",
"email": "邮箱地址",
"phone": "手机号码",
"password": "登录密码",
"roles": "授予角色",
"tenant": "所属租户",
"orgNode": "所属组织"
},
"permissions": {
"title": "功能权限管理",
"subtitle": "配置系统的菜单结构与功能按钮的操作权限点",
"permName": "权限名称",
"permCode": "权限编码",
"permType": "类型",
"sort": "排序",
"route": "路由/组件",
"visible": "可见",
"isVisible": "是否在菜单可见",
"drawerTitleCreate": "新增功能权限",
"drawerTitleEdit": "修改权限点信息",
"parentId": "上级权限",
"level": "权限层级",
"path": "路由路径",
"component": "组件路径",
"icon": "图标名称",
"description": "描述说明"
},
"roles": {
"title": "系统角色",
"subtitle": "管理系统角色及其关联的权限和用户",
"roleName": "角色名称",
"roleCode": "角色编码",
"searchPlaceholder": "搜索角色...",
"drawerTitleCreate": "新增系统角色",
"drawerTitleEdit": "修改角色基础信息",
"funcPerms": "功能权限",
"assignedUsers": "关联用户",
"savePerms": "保存权限更改",
"selectRole": "请从左侧列表选择一个角色进行管理"
},
"login": {
"welcome": "欢迎回来",
"subtitle": "请登录您的账号",
"username": "用户名",
"password": "密码",
"tenantCode": "租户编码",
"tenantCodePlaceholder": "租户编码 (平台管理可留空)",
"captcha": "验证码",
"rememberMe": "记住我",
"forgotPassword": "忘记密码?",
"submit": "立即登录",
"loggingIn": "登录中...",
"demoAccount": "演示账号",
"heroTitle1": "智能会议",
"heroTitle2": "实时语音处理",
"heroTitle3": "系统",
"heroDesc": "全流程自动化会议记录,声纹识别与智能摘要。提升团队协作效率的新一代解决方案。",
"enterpriseSecurity": "企业级安全",
"multiLang": "多语言支持",
"loginTimeout": "登录超时,请重新登录"
},
"devices": {
"title": "设备管理",
"subtitle": "管理接入系统的硬件终端及关联用户",
"deviceInfo": "设备信息",
"owner": "归属用户",
"updateTime": "更新时间",
"searchPlaceholder": "搜索设备名称、编码或归属用户...",
"drawerTitleCreate": "接入新设备",
"drawerTitleEdit": "修改设备信息",
"deviceCode": "设备识别码",
"deviceName": "设备名称"
},
"dicts": {
"title": "数据字典管理",
"subtitle": "维护系统各类枚举值和常量的映射关系",
"dictType": "字典类型",
"dictItem": "字典项内容",
"typeName": "类型名称",
"typeCode": "类型Code",
"itemLabel": "显示标签",
"itemValue": "存储数值",
"sort": "排序",
"selectType": "请从左侧选择一个字典类型",
"drawerTitleTypeCreate": "新增字典类型",
"drawerTitleTypeEdit": "编辑字典类型",
"drawerTitleItemCreate": "新增字典项",
"drawerTitleItemEdit": "编辑字典项"
},
"logs": {
"title": "系统日志管理",
"subtitle": "追踪系统内的每一次重要操作,保障系统安全与可追溯性",
"opLog": "操作日志",
"loginLog": "登录日志",
"opAccount": "操作账号",
"opDetail": "操作详情",
"ip": "IP 地址",
"duration": "耗时",
"time": "发生时间",
"method": "请求方法",
"params": "请求参数",
"searchPlaceholder": "搜索操作内容...",
"detailTitle": "日志详细信息"
},
"tenants": {
"title": "租户管理",
"subtitle": "管理系统多租户基础信息、授权状态及过期时间",
"tenantInfo": "租户信息",
"contact": "联系人",
"expireTime": "过期时间",
"forever": "永久有效",
"drawerTitleCreate": "创建新租户",
"drawerTitleEdit": "编辑租户信息",
"tenantName": "租户名称",
"tenantCode": "租户编码",
"contactName": "联系人姓名",
"contactPhone": "联系电话"
},
"orgs": {
"title": "组织架构管理",
"subtitle": "维护企业内部部门层级关系,支持多租户架构隔离",
"orgName": "组织名称",
"orgCode": "组织编码",
"parentOrg": "上级部门",
"rootOrg": "顶级部门",
"addSub": "添加下级",
"drawerTitleCreate": "新增组织部门",
"drawerTitleEdit": "编辑组织节点",
"selectTenant": "请先选择一个租户以查看其组织架构",
"createRoot": "新增根组织",
"sort": "排序"
},
"userRole": {
"title": "用户角色授权",
"subtitle": "为指定用户分配系统访问角色,控制其操作权限边界",
"userList": "用户选择列表",
"grantRoles": "授予角色权限",
"searchUser": "搜索用户名或显示名...",
"selectUser": "请先从左侧选择一个用户",
"editing": "正在编辑"
},
"rolePerm": {
"title": "角色权限授权",
"subtitle": "配置系统中各类角色所拥有的菜单访问权限与功能操作权限点",
"roleList": "系统角色列表",
"permConfig": "功能权限配置",
"searchRole": "搜索角色名称或编码...",
"selectRole": "请先从左侧列表中选择一个角色",
"currentRole": "当前角色",
"savePolicy": "保存权限策略"
},
"sysParams": {
"title": "系统参数管理",
"subtitle": "配置系统运行所需的各种动态参数、配置项及开关",
"paramKey": "参数键名",
"paramValue": "参数键值",
"paramType": "参数类型",
"isSystem": "系统内置",
"description": "描述",
"searchPlaceholder": "搜索参数键名或描述...",
"drawerTitleCreate": "新增系统参数",
"drawerTitleEdit": "修改系统参数",
"yes": "是",
"no": "否"
},
"platformSettings": {
"title": "平台信息配置",
"subtitle": "管理系统的品牌标识、视觉风格及法律合规信息",
"projectName": "项目名称",
"logo": "系统 Logo",
"icon": "浏览器图标 (Icon)",
"loginBg": "登录页背景图",
"icp": "ICP 备案号",
"copyright": "版权信息",
"desc": "系统描述",
"uploadHint": "点击或拖拽上传图片",
"uploadLimit": "建议比例 1:1大小不超过 2MB"
}
}

View File

@ -0,0 +1,200 @@
import {
Button,
Card,
Form,
Input,
message,
Space,
Typography,
Upload,
Row,
Col,
Divider
} from "antd";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { getAdminPlatformConfig, updatePlatformConfig, uploadPlatformAsset } from "../api";
import {
UploadOutlined,
SaveOutlined,
GlobalOutlined,
PictureOutlined,
FileTextOutlined
} from "@ant-design/icons";
import type { SysPlatformConfig } from "../types";
const { Title, Text } = Typography;
export default function PlatformSettings() {
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const [saving, setSaving] = useState(false);
const [form] = Form.useForm();
const loadConfig = async () => {
setLoading(true);
try {
const data = await getAdminPlatformConfig();
form.setFieldsValue(data);
} catch (e) {
message.error(t('common.error'));
} finally {
setLoading(false);
}
};
useEffect(() => {
loadConfig();
}, []);
const handleUpload = async (file: File, fieldName: keyof SysPlatformConfig) => {
try {
const url = await uploadPlatformAsset(file);
form.setFieldValue(fieldName, url);
message.success(t('common.success'));
} catch (e) {
message.error(t('common.error'));
}
return false; // 阻止自动上传
};
const onFinish = async (values: SysPlatformConfig) => {
setSaving(true);
try {
await updatePlatformConfig(values);
message.success(t('common.success'));
} catch (e) {
message.error(t('common.error'));
} finally {
setSaving(false);
}
};
const ImagePreview = ({ url, label }: { url?: string; label: string }) => (
<div className="flex flex-col items-center justify-center p-4 border border-dashed border-gray-300 rounded-lg bg-gray-50 h-32">
{url ? (
<img src={url} alt={label} className="max-h-full max-w-full object-contain" />
) : (
<div className="text-gray-400">
<PictureOutlined style={{ fontSize: 24 }} />
<div className="text-xs mt-1">{t('platformSettings.uploadHint')}</div>
</div>
)}
</div>
);
return (
<div className="p-6 max-w-4xl mx-auto">
<div className="flex justify-between items-end mb-6">
<div>
<Title level={4} className="mb-1">{t('platformSettings.title')}</Title>
<Text type="secondary">{t('platformSettings.subtitle')}</Text>
</div>
<Button
type="primary"
icon={<SaveOutlined />}
loading={saving}
onClick={() => form.submit()}
>
{t('common.save')}
</Button>
</div>
<Form
form={form}
layout="vertical"
onFinish={onFinish}
initialValues={{ projectName: 'iMeeting' }}
>
<Row gutter={24}>
<Col span={24}>
<Card
title={<><GlobalOutlined className="mr-2" /> </>}
className="shadow-sm mb-6"
>
<Form.Item
label={t('platformSettings.projectName')}
name="projectName"
rules={[{ required: true, message: t('platformSettings.projectName') }]}
>
<Input placeholder="例如iMeeting 智能会议系统" />
</Form.Item>
<Form.Item label={t('platformSettings.desc')} name="systemDescription">
<Input.TextArea rows={3} placeholder="系统的简要介绍..." />
</Form.Item>
</Card>
</Col>
<Col span={24}>
<Card
title={<><PictureOutlined className="mr-2" /> </>}
className="shadow-sm mb-6"
>
<Row gutter={24}>
<Col span={8}>
<Form.Item label={t('platformSettings.logo')} name="logoUrl">
<Input placeholder="Logo URL" className="mb-2" />
</Form.Item>
<ImagePreview url={Form.useWatch('logoUrl', form)} label="Logo" />
<Upload
accept="image/*"
showUploadList={false}
beforeUpload={(file) => handleUpload(file, 'logoUrl')}
>
<Button icon={<UploadOutlined />} block className="mt-2"> Logo</Button>
</Upload>
</Col>
<Col span={8}>
<Form.Item label={t('platformSettings.icon')} name="iconUrl">
<Input placeholder="Icon URL" className="mb-2" />
</Form.Item>
<ImagePreview url={Form.useWatch('iconUrl', form)} label="Icon" />
<Upload
accept="image/*"
showUploadList={false}
beforeUpload={(file) => handleUpload(file, 'iconUrl')}
>
<Button icon={<UploadOutlined />} block className="mt-2"> Icon</Button>
</Upload>
</Col>
<Col span={8}>
<Form.Item label={t('platformSettings.loginBg')} name="loginBgUrl">
<Input placeholder="Background URL" className="mb-2" />
</Form.Item>
<ImagePreview url={Form.useWatch('loginBgUrl', form)} label="Background" />
<Upload
accept="image/*"
showUploadList={false}
beforeUpload={(file) => handleUpload(file, 'loginBgUrl')}
>
<Button icon={<UploadOutlined />} block className="mt-2"></Button>
</Upload>
</Col>
</Row>
</Card>
</Col>
<Col span={24}>
<Card
title={<><FileTextOutlined className="mr-2" /> </>}
className="shadow-sm"
>
<Row gutter={16}>
<Col span={12}>
<Form.Item label={t('platformSettings.icp')} name="icpInfo">
<Input placeholder="例如京ICP备12345678号" />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item label={t('platformSettings.copyright')} name="copyrightInfo">
<Input placeholder="例如:© 2026 iMeeting Team." />
</Form.Item>
</Col>
</Row>
</Card>
</Col>
</Row>
</Form>
</div>
);
}

View File

@ -0,0 +1,33 @@
.sys-params-page {
min-height: 100%;
}
.sys-params-header {
display: flex;
justify-content: space-between;
align-items: flex-end;
}
.sys-params-table-card {
border-radius: 8px;
}
.sys-params-table-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
}
.sys-params-search-input {
border-radius: 6px;
}
.param-key-text {
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
font-weight: 500;
color: #1890ff;
}
.param-type-tag {
border-radius: 4px;
}

View File

@ -0,0 +1,375 @@
import {
Button,
Drawer,
Form,
Input,
message,
Popconfirm,
Select,
Space,
Table,
Tag,
Typography,
Card,
Switch,
Tooltip,
Row,
Col
} from "antd";
import { useEffect, useState, useCallback } from "react";
import { useTranslation } from "react-i18next";
import {
pageParams,
createParam,
updateParam,
deleteParam
} from "../api";
import { usePermission } from "../hooks/usePermission";
import {
PlusOutlined,
EditOutlined,
DeleteOutlined,
SearchOutlined,
ReloadOutlined,
SettingOutlined,
InfoCircleOutlined
} from "@ant-design/icons";
import type { SysParamVO, SysParamQuery } from "../types";
import "./SysParams.css";
const { Title, Text } = Typography;
export default function SysParams() {
const { t } = useTranslation();
const { can } = usePermission();
const [loading, setLoading] = useState(false);
const [saving, setSaving] = useState(false);
const [data, setData] = useState<SysParamVO[]>([]);
const [total, setTotal] = useState(0);
const [queryParams, setQueryParams] = useState<SysParamQuery>({
pageNum: 1,
pageSize: 10
});
// Drawer state
const [drawerOpen, setDrawerOpen] = useState(false);
const [editing, setEditing] = useState<SysParamVO | null>(null);
const [form] = Form.useForm();
const loadData = useCallback(async (query = queryParams) => {
setLoading(true);
try {
const res = await pageParams(query);
setData(res.records || []);
setTotal(res.total || 0);
} catch (e) {
message.error(t('common.error'));
} finally {
setLoading(false);
}
}, [queryParams, t]);
useEffect(() => {
loadData();
}, [loadData]);
const handleSearch = (values: any) => {
const newQuery = { ...queryParams, ...values, pageNum: 1 };
setQueryParams(newQuery);
};
const handleReset = () => {
form.resetFields();
const newQuery = { pageNum: 1, pageSize: 10 };
setQueryParams(newQuery);
};
const handlePageChange = (page: number, pageSize: number) => {
setQueryParams(prev => ({ ...prev, pageNum: page, pageSize }));
};
const openCreate = () => {
setEditing(null);
form.resetFields();
form.setFieldsValue({ isSystem: false, status: 1 });
setDrawerOpen(true);
};
const openEdit = (record: SysParamVO) => {
setEditing(record);
form.setFieldsValue(record);
setDrawerOpen(true);
};
const handleDelete = async (id: number) => {
try {
await deleteParam(id);
message.success(t('common.success'));
loadData();
} catch (e) {
message.error(t('common.error'));
}
};
const submit = async () => {
try {
const values = await form.validateFields();
setSaving(true);
if (editing) {
await updateParam(editing.paramId, values);
} else {
await createParam(values);
}
message.success(t('common.success'));
setDrawerOpen(false);
loadData();
} catch (e) {
// Form validation errors or API errors
} finally {
setSaving(false);
}
};
const columns = [
{
title: t('sysParams.paramKey'),
dataIndex: "paramKey",
key: "paramKey",
render: (text: string, record: SysParamVO) => (
<Space direction="vertical" size={0}>
<Text className="param-key-text">{text}</Text>
{record.isSystem === 1 && (
<Tag color="orange" size={0} style={{ fontSize: 10, marginTop: 2, padding: '0 4px' }}>
{t('sysParams.isSystem')}
</Tag>
)}
</Space>
),
},
{
title: t('sysParams.paramValue'),
dataIndex: "paramValue",
key: "paramValue",
ellipsis: true,
render: (text: string) => (
<Tooltip title={text}>
<Text code>{text}</Text>
</Tooltip>
),
},
{
title: t('sysParams.paramType'),
dataIndex: "paramType",
key: "paramType",
width: 120,
render: (type: string) => <Tag className="param-type-tag">{type || 'DEFAULT'}</Tag>
},
{
title: t('sysParams.description'),
dataIndex: "description",
key: "description",
ellipsis: true,
},
{
title: t('common.status'),
dataIndex: "status",
width: 80,
render: (status: number) => (
<Tag color={status === 1 ? "green" : "red"}>
{status === 1 ? "正常" : "禁用"}
</Tag>
),
},
{
title: t('common.action'),
key: "action",
width: 110,
fixed: "right" as const,
render: (_: any, record: SysParamVO) => (
<Space>
{can("sys_param:update") && (
<Button
type="text"
icon={<EditOutlined />}
onClick={() => openEdit(record)}
/>
)}
{can("sys_param:delete") && record.isSystem !== 1 && (
<Popconfirm title="确定删除该参数吗?" onConfirm={() => handleDelete(record.paramId)}>
<Button type="text" danger icon={<DeleteOutlined />} />
</Popconfirm>
)}
</Space>
),
},
];
return (
<div className="sys-params-page p-6">
<div className="sys-params-header mb-6">
<div>
<Title level={4} className="mb-1">{t('sysParams.title')}</Title>
<Text type="secondary">{t('sysParams.subtitle')}</Text>
</div>
{can("sys_param:create") && (
<Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>
{t('common.create')}
</Button>
)}
</div>
<Card className="sys-params-table-card shadow-sm mb-4">
<Form
layout="inline"
onFinish={handleSearch}
className="mb-4"
>
<Form.Item name="paramKey">
<Input
placeholder={t('sysParams.paramKey')}
prefix={<SearchOutlined />}
allowClear
style={{ width: 200 }}
/>
</Form.Item>
<Form.Item name="paramType">
<Select
placeholder={t('sysParams.paramType')}
allowClear
style={{ width: 150 }}
options={[
{ label: 'String', value: 'String' },
{ label: 'Number', value: 'Number' },
{ label: 'Boolean', value: 'Boolean' },
{ label: 'JSON', value: 'JSON' }
]}
/>
</Form.Item>
<Form.Item>
<Space>
<Button type="primary" htmlType="submit">
{t('common.search')}
</Button>
<Button onClick={handleReset}>
{t('common.reset')}
</Button>
</Space>
</Form.Item>
</Form>
<Table
rowKey="paramId"
columns={columns}
dataSource={data}
loading={loading}
size="middle"
pagination={{
current: queryParams.pageNum,
pageSize: queryParams.pageSize,
total: total,
showTotal: (tTotal) => t('common.total', { total: tTotal }),
onChange: handlePageChange,
showSizeChanger: true,
}}
/>
</Card>
<Drawer
title={
<span>
<SettingOutlined className="mr-2" />
{editing ? t('sysParams.drawerTitleEdit') : t('sysParams.drawerTitleCreate')}
</span>
}
open={drawerOpen}
onClose={() => setDrawerOpen(false)}
width={500}
destroyOnClose
footer={
<div className="flex justify-end gap-2 p-2">
<Button onClick={() => setDrawerOpen(false)}>{t('common.cancel')}</Button>
<Button type="primary" loading={saving} onClick={submit}>
{t('common.save')}
</Button>
</div>
}
>
<Form form={form} layout="vertical">
<Form.Item
label={t('sysParams.paramKey')}
name="paramKey"
rules={[{ required: true, message: t('sysParams.paramKey') }]}
>
<Input placeholder="sys.config.example" disabled={!!editing} />
</Form.Item>
<Form.Item
label={t('sysParams.paramValue')}
name="paramValue"
rules={[{ required: true, message: t('sysParams.paramValue') }]}
>
<Input.TextArea rows={4} placeholder="Enter parameter value" />
</Form.Item>
<Row gutter={16}>
<Col span={12}>
<Form.Item
label={t('sysParams.paramType')}
name="paramType"
rules={[{ required: true, message: t('sysParams.paramType') }]}
>
<Select
options={[
{ label: 'String', value: 'String' },
{ label: 'Number', value: 'Number' },
{ label: 'Boolean', value: 'Boolean' },
{ label: 'JSON', value: 'JSON' }
]}
/>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
label={t('common.status')}
name="status"
initialValue={1}
>
<Select
options={[
{ label: '启用', value: 1 },
{ label: '禁用', value: 0 }
]}
/>
</Form.Item>
</Col>
</Row>
<Form.Item
label={
<Space>
{t('sysParams.isSystem')}
<Tooltip title="系统参数通常禁止删除,由开发者在代码中使用">
<InfoCircleOutlined style={{ color: '#8c8c8c' }} />
</Tooltip>
</Space>
}
name="isSystem"
valuePropName="checked"
getValueProps={(value) => ({ checked: value === 1 })}
getValueFromEvent={(checked) => (checked ? 1 : 0)}
>
<Switch />
</Form.Item>
<Form.Item label={t('sysParams.description')} name="description">
<Input.TextArea rows={3} placeholder="Describe the purpose of this parameter" />
</Form.Item>
</Form>
</Drawer>
</div>
);
}