From 8959680d31ac46594e5b19b18140008dd14aa19c Mon Sep 17 00:00:00 2001 From: chenhao Date: Thu, 26 Feb 2026 16:31:02 +0800 Subject: [PATCH] =?UTF-8?q?feat(system):=20=E6=B7=BB=E5=8A=A0=E5=9B=BD?= =?UTF-8?q?=E9=99=85=E5=8C=96=E6=94=AF=E6=8C=81=E5=92=8C=E7=B3=BB=E7=BB=9F?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E7=AE=A1=E7=90=86=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 集成 i18next 多语言框架,实现中英双语支持 - 创建完整的多语言资源配置文件 (en-US.json) - 实现平台配置管理页面,支持项目名称、Logo 等可视化设置 - 新增系统参数管理页面,支持参数键值对的增删改查操作 - 添加后端平台配置控制器和相关实体服务层 - 实现系统参数分页查询、创建、更新和删除接口 - 配置多语言检测器和默认语言设置 --- .../com/imeeting/config/WebMvcConfig.java | 35 ++ .../controller/PlatformConfigController.java | 55 +++ .../com/imeeting/dto/PlatformConfigVO.java | 15 + .../com/imeeting/dto/SysParamQueryDTO.java | 12 + .../java/com/imeeting/dto/SysParamVO.java | 17 + .../imeeting/entity/SysPlatformConfig.java | 25 ++ .../com/imeeting/entity/SysTenantUser.java | 25 ++ .../mapper/SysPlatformConfigMapper.java | 9 + .../imeeting/mapper/SysTenantUserMapper.java | 9 + .../service/SysPlatformConfigService.java | 12 + .../service/SysTenantUserService.java | 11 + .../impl/SysPlatformConfigServiceImpl.java | 119 ++++++ .../impl/SysTenantUserServiceImpl.java | 75 ++++ frontend/src/api/platform.ts | 41 ++ frontend/src/i18n.ts | 25 ++ frontend/src/locales/en-US.json | 231 +++++++++++ frontend/src/locales/zh-CN.json | 231 +++++++++++ frontend/src/pages/PlatformSettings.tsx | 200 ++++++++++ frontend/src/pages/SysParams.css | 33 ++ frontend/src/pages/SysParams.tsx | 375 ++++++++++++++++++ 20 files changed, 1555 insertions(+) create mode 100644 backend/src/main/java/com/imeeting/config/WebMvcConfig.java create mode 100644 backend/src/main/java/com/imeeting/controller/PlatformConfigController.java create mode 100644 backend/src/main/java/com/imeeting/dto/PlatformConfigVO.java create mode 100644 backend/src/main/java/com/imeeting/dto/SysParamQueryDTO.java create mode 100644 backend/src/main/java/com/imeeting/dto/SysParamVO.java create mode 100644 backend/src/main/java/com/imeeting/entity/SysPlatformConfig.java create mode 100644 backend/src/main/java/com/imeeting/entity/SysTenantUser.java create mode 100644 backend/src/main/java/com/imeeting/mapper/SysPlatformConfigMapper.java create mode 100644 backend/src/main/java/com/imeeting/mapper/SysTenantUserMapper.java create mode 100644 backend/src/main/java/com/imeeting/service/SysPlatformConfigService.java create mode 100644 backend/src/main/java/com/imeeting/service/SysTenantUserService.java create mode 100644 backend/src/main/java/com/imeeting/service/impl/SysPlatformConfigServiceImpl.java create mode 100644 backend/src/main/java/com/imeeting/service/impl/SysTenantUserServiceImpl.java create mode 100644 frontend/src/api/platform.ts create mode 100644 frontend/src/i18n.ts create mode 100644 frontend/src/locales/en-US.json create mode 100644 frontend/src/locales/zh-CN.json create mode 100644 frontend/src/pages/PlatformSettings.tsx create mode 100644 frontend/src/pages/SysParams.css create mode 100644 frontend/src/pages/SysParams.tsx diff --git a/backend/src/main/java/com/imeeting/config/WebMvcConfig.java b/backend/src/main/java/com/imeeting/config/WebMvcConfig.java new file mode 100644 index 0000000..30f616d --- /dev/null +++ b/backend/src/main/java/com/imeeting/config/WebMvcConfig.java @@ -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); + } +} diff --git a/backend/src/main/java/com/imeeting/controller/PlatformConfigController.java b/backend/src/main/java/com/imeeting/controller/PlatformConfigController.java new file mode 100644 index 0000000..986feea --- /dev/null +++ b/backend/src/main/java/com/imeeting/controller/PlatformConfigController.java @@ -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 getOpenConfig() { + return ApiResponse.ok(platformConfigService.getConfig()); + } + + /** + * 获取管理配置 (需要登录) + */ + @GetMapping("/admin/platform/config") + @PreAuthorize("isAuthenticated()") + public ApiResponse getAdminConfig() { + return ApiResponse.ok(platformConfigService.getConfig()); + } + + /** + * 更新配置 (仅限平台管理员) + */ + @PutMapping("/admin/platform/config") + @PreAuthorize("hasRole('ADMIN') or @ss.hasPermi('sys_platform:config:update')") + public ApiResponse 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 upload(@RequestParam("file") MultipartFile file) { + return ApiResponse.ok(platformConfigService.uploadAsset(file)); + } +} diff --git a/backend/src/main/java/com/imeeting/dto/PlatformConfigVO.java b/backend/src/main/java/com/imeeting/dto/PlatformConfigVO.java new file mode 100644 index 0000000..68d08e5 --- /dev/null +++ b/backend/src/main/java/com/imeeting/dto/PlatformConfigVO.java @@ -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; +} diff --git a/backend/src/main/java/com/imeeting/dto/SysParamQueryDTO.java b/backend/src/main/java/com/imeeting/dto/SysParamQueryDTO.java new file mode 100644 index 0000000..1a098c5 --- /dev/null +++ b/backend/src/main/java/com/imeeting/dto/SysParamQueryDTO.java @@ -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; +} diff --git a/backend/src/main/java/com/imeeting/dto/SysParamVO.java b/backend/src/main/java/com/imeeting/dto/SysParamVO.java new file mode 100644 index 0000000..b00587a --- /dev/null +++ b/backend/src/main/java/com/imeeting/dto/SysParamVO.java @@ -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; +} diff --git a/backend/src/main/java/com/imeeting/entity/SysPlatformConfig.java b/backend/src/main/java/com/imeeting/entity/SysPlatformConfig.java new file mode 100644 index 0000000..0759820 --- /dev/null +++ b/backend/src/main/java/com/imeeting/entity/SysPlatformConfig.java @@ -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; +} diff --git a/backend/src/main/java/com/imeeting/entity/SysTenantUser.java b/backend/src/main/java/com/imeeting/entity/SysTenantUser.java new file mode 100644 index 0000000..2804114 --- /dev/null +++ b/backend/src/main/java/com/imeeting/entity/SysTenantUser.java @@ -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; +} diff --git a/backend/src/main/java/com/imeeting/mapper/SysPlatformConfigMapper.java b/backend/src/main/java/com/imeeting/mapper/SysPlatformConfigMapper.java new file mode 100644 index 0000000..1c6874f --- /dev/null +++ b/backend/src/main/java/com/imeeting/mapper/SysPlatformConfigMapper.java @@ -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 { +} diff --git a/backend/src/main/java/com/imeeting/mapper/SysTenantUserMapper.java b/backend/src/main/java/com/imeeting/mapper/SysTenantUserMapper.java new file mode 100644 index 0000000..bfd653b --- /dev/null +++ b/backend/src/main/java/com/imeeting/mapper/SysTenantUserMapper.java @@ -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 { +} diff --git a/backend/src/main/java/com/imeeting/service/SysPlatformConfigService.java b/backend/src/main/java/com/imeeting/service/SysPlatformConfigService.java new file mode 100644 index 0000000..20d9fe5 --- /dev/null +++ b/backend/src/main/java/com/imeeting/service/SysPlatformConfigService.java @@ -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 { + PlatformConfigVO getConfig(); + boolean updateConfig(SysPlatformConfig config); + String uploadAsset(MultipartFile file); +} diff --git a/backend/src/main/java/com/imeeting/service/SysTenantUserService.java b/backend/src/main/java/com/imeeting/service/SysTenantUserService.java new file mode 100644 index 0000000..7e869fc --- /dev/null +++ b/backend/src/main/java/com/imeeting/service/SysTenantUserService.java @@ -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 { + List listByUserId(Long userId); + void saveTenantUser(Long userId, Long tenantId, Long orgId); + void syncMemberships(Long userId, List memberships); +} diff --git a/backend/src/main/java/com/imeeting/service/impl/SysPlatformConfigServiceImpl.java b/backend/src/main/java/com/imeeting/service/impl/SysPlatformConfigServiceImpl.java new file mode 100644 index 0000000..8a6fca7 --- /dev/null +++ b/backend/src/main/java/com/imeeting/service/impl/SysPlatformConfigServiceImpl.java @@ -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 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; + } +} diff --git a/backend/src/main/java/com/imeeting/service/impl/SysTenantUserServiceImpl.java b/backend/src/main/java/com/imeeting/service/impl/SysTenantUserServiceImpl.java new file mode 100644 index 0000000..ba54cf1 --- /dev/null +++ b/backend/src/main/java/com/imeeting/service/impl/SysTenantUserServiceImpl.java @@ -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 implements SysTenantUserService { + + private final com.imeeting.service.SysOrgService sysOrgService; + + public SysTenantUserServiceImpl(com.imeeting.service.SysOrgService sysOrgService) { + this.sysOrgService = sysOrgService; + } + + @Override + public List listByUserId(Long userId) { + List list = list(new LambdaQueryWrapper().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 query = new LambdaQueryWrapper() + .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 memberships) { + if (userId == null) return; + + // 1. Physical removal of all existing memberships for this user + getBaseMapper().delete(new LambdaQueryWrapper().eq(SysTenantUser::getUserId, userId)); + + // 2. Add new ones + if (memberships != null && !memberships.isEmpty()) { + java.util.Set 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); + } + } + } + } +} diff --git a/frontend/src/api/platform.ts b/frontend/src/api/platform.ts new file mode 100644 index 0000000..560d634 --- /dev/null +++ b/frontend/src/api/platform.ts @@ -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; +} diff --git a/frontend/src/i18n.ts b/frontend/src/i18n.ts new file mode 100644 index 0000000..fb6a8d2 --- /dev/null +++ b/frontend/src/i18n.ts @@ -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; diff --git a/frontend/src/locales/en-US.json b/frontend/src/locales/en-US.json new file mode 100644 index 0000000..f8f27d6 --- /dev/null +++ b/frontend/src/locales/en-US.json @@ -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" + } +} diff --git a/frontend/src/locales/zh-CN.json b/frontend/src/locales/zh-CN.json new file mode 100644 index 0000000..b51fe3b --- /dev/null +++ b/frontend/src/locales/zh-CN.json @@ -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" + } +} diff --git a/frontend/src/pages/PlatformSettings.tsx b/frontend/src/pages/PlatformSettings.tsx new file mode 100644 index 0000000..e4b3363 --- /dev/null +++ b/frontend/src/pages/PlatformSettings.tsx @@ -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 }) => ( +
+ {url ? ( + {label} + ) : ( +
+ +
{t('platformSettings.uploadHint')}
+
+ )} +
+ ); + + return ( +
+
+
+ {t('platformSettings.title')} + {t('platformSettings.subtitle')} +
+ +
+ +
+ + + 基础信息} + className="shadow-sm mb-6" + > + + + + + + + + + + + 视觉资源} + className="shadow-sm mb-6" + > + + + + + + + handleUpload(file, 'logoUrl')} + > + + + + + + + + + handleUpload(file, 'iconUrl')} + > + + + + + + + + + handleUpload(file, 'loginBgUrl')} + > + + + + + + + + + 合规与版权} + className="shadow-sm" + > + + + + + + + + + + + + + + + +
+
+ ); +} diff --git a/frontend/src/pages/SysParams.css b/frontend/src/pages/SysParams.css new file mode 100644 index 0000000..5171246 --- /dev/null +++ b/frontend/src/pages/SysParams.css @@ -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; +} diff --git a/frontend/src/pages/SysParams.tsx b/frontend/src/pages/SysParams.tsx new file mode 100644 index 0000000..9fa2d59 --- /dev/null +++ b/frontend/src/pages/SysParams.tsx @@ -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([]); + const [total, setTotal] = useState(0); + + const [queryParams, setQueryParams] = useState({ + pageNum: 1, + pageSize: 10 + }); + + // Drawer state + const [drawerOpen, setDrawerOpen] = useState(false); + const [editing, setEditing] = useState(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) => ( + + {text} + {record.isSystem === 1 && ( + + {t('sysParams.isSystem')} + + )} + + ), + }, + { + title: t('sysParams.paramValue'), + dataIndex: "paramValue", + key: "paramValue", + ellipsis: true, + render: (text: string) => ( + + {text} + + ), + }, + { + title: t('sysParams.paramType'), + dataIndex: "paramType", + key: "paramType", + width: 120, + render: (type: string) => {type || 'DEFAULT'} + }, + { + title: t('sysParams.description'), + dataIndex: "description", + key: "description", + ellipsis: true, + }, + { + title: t('common.status'), + dataIndex: "status", + width: 80, + render: (status: number) => ( + + {status === 1 ? "正常" : "禁用"} + + ), + }, + { + title: t('common.action'), + key: "action", + width: 110, + fixed: "right" as const, + render: (_: any, record: SysParamVO) => ( + + {can("sys_param:update") && ( + + )} + + + +
+ + } + allowClear + style={{ width: 200 }} + /> + + + + + + + + + + + + + + + + + + + {t('sysParams.isSystem')} + + + + + } + name="isSystem" + valuePropName="checked" + getValueProps={(value) => ({ checked: value === 1 })} + getValueFromEvent={(checked) => (checked ? 1 : 0)} + > + + + + + + +
+ + + ); +}