feat(system): 添加国际化支持和系统配置管理功能
- 集成 i18next 多语言框架,实现中英双语支持 - 创建完整的多语言资源配置文件 (en-US.json) - 实现平台配置管理页面,支持项目名称、Logo 等可视化设置 - 新增系统参数管理页面,支持参数键值对的增删改查操作 - 添加后端平台配置控制器和相关实体服务层 - 实现系统参数分页查询、创建、更新和删除接口 - 配置多语言检测器和默认语言设置master
parent
9b721929c6
commit
8959680d31
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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> {
|
||||
}
|
||||
|
|
@ -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> {
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in New Issue