feat(layout): 添加音频、热词和提示词菜单项并优化布局结构
- 在AppLayout中添加AudioOutlined、TagsOutlined和BulbOutlined图标 - 为audio、hotword和prompt路由配置对应的菜单图标映射 - 重构菜单项生成逻辑,使用useMemo优化性能并修复TDZ错误 - 在菜单树查找函数中添加数组验证防止运行时错误 - 添加新业务模块数据库表:声纹发言人表、热词管理表和提示词模板表 - 更新租户ID字段的自动填充逻辑和权限过滤配置 - 在认证流程中添加displayName和pwdResetRequired字段支持 - 添加React Markdown依赖用于内容渲染功能dev_na
parent
f93d797382
commit
1a392d96b9
|
|
@ -221,3 +221,66 @@
|
||||||
| created_at | TIMESTAMP | NOT NULL | 创建时间 |
|
| created_at | TIMESTAMP | NOT NULL | 创建时间 |
|
||||||
| updated_at | TIMESTAMP | NOT NULL | 更新时间 |
|
| updated_at | TIMESTAMP | NOT NULL | 更新时间 |
|
||||||
|
|
||||||
|
## 5. 业务模块
|
||||||
|
|
||||||
|
### 5.1 `biz_speakers`(声纹发言人表)
|
||||||
|
| 字段 | 类型 | 约束 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| id | BIGSERIAL | PK | 主键ID |
|
||||||
|
| tenant_id | BIGINT | NOT NULL | 租户ID |
|
||||||
|
| user_id | BIGINT | | 关联系统用户ID |
|
||||||
|
| name | VARCHAR(100) | NOT NULL | 发言人姓名 |
|
||||||
|
| voice_path | VARCHAR(512) | | 原始文件路径 |
|
||||||
|
| voice_ext | VARCHAR(10) | | 文件后缀 |
|
||||||
|
| voice_size | BIGINT | | 文件大小 |
|
||||||
|
| status | SMALLINT | DEFAULT 1 | 状态 (1:已保存, 2:注册中, 3:已注册) |
|
||||||
|
| embedding | VECTOR | | 声纹特征向量 |
|
||||||
|
| remark | TEXT | | 备注 |
|
||||||
|
| created_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 创建时间 |
|
||||||
|
| updated_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 更新时间 |
|
||||||
|
| is_deleted | SMALLINT | DEFAULT 0 | 逻辑删除 |
|
||||||
|
|
||||||
|
索引:
|
||||||
|
- `idx_speaker_tenant`: `(tenant_id)`
|
||||||
|
- `idx_speaker_user`: `(user_id) WHERE is_deleted = 0`
|
||||||
|
|
||||||
|
### 5.2 `biz_hot_words`(热词管理表)
|
||||||
|
| 字段 | 类型 | 约束 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| id | BIGSERIAL | PK | 主键ID |
|
||||||
|
| tenant_id | BIGINT | NOT NULL | 租户ID |
|
||||||
|
| word | VARCHAR(100) | NOT NULL | 热词原文 |
|
||||||
|
| pinyin_list | JSONB | | 拼音数组 |
|
||||||
|
| match_strategy | SMALLINT | DEFAULT 1 | 匹配策略 (1:精确, 2:模糊) |
|
||||||
|
| category | VARCHAR(50) | | 类别 (人名、术语等) |
|
||||||
|
| weight | INTEGER | DEFAULT 10 | 权重 (1-100) |
|
||||||
|
| status | SMALLINT | DEFAULT 1 | 状态 (1:启用, 0:禁用) |
|
||||||
|
| is_synced | SMALLINT | DEFAULT 0 | 已同步第三方标记 |
|
||||||
|
| remark | TEXT | | 备注 |
|
||||||
|
| created_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 创建时间 |
|
||||||
|
| updated_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 更新时间 |
|
||||||
|
| is_deleted | SMALLINT | DEFAULT 0 | 逻辑删除 |
|
||||||
|
|
||||||
|
索引:
|
||||||
|
- `idx_hotword_tenant`: `(tenant_id)`
|
||||||
|
- `idx_hotword_word`: `(word) WHERE is_deleted = 0`
|
||||||
|
|
||||||
|
### 5.3 `biz_prompt_templates`(提示词模板表)
|
||||||
|
| 字段 | 类型 | 约束 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| id | BIGSERIAL | PK | 主键ID |
|
||||||
|
| tenant_id | BIGINT | NOT NULL | 租户ID |
|
||||||
|
| template_name | VARCHAR(100) | NOT NULL | 模板名称 |
|
||||||
|
| category | VARCHAR(20) | | 分类 (字典: biz_prompt_category) |
|
||||||
|
| is_system | SMALLINT | DEFAULT 0 | 是否预置 (1:是, 0:否) |
|
||||||
|
| prompt_content | TEXT | NOT NULL | 提示词内容 |
|
||||||
|
| status | SMALLINT | DEFAULT 1 | 状态 (1:启用, 0:禁用) |
|
||||||
|
| remark | VARCHAR(255) | | 备注 |
|
||||||
|
| created_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 创建时间 |
|
||||||
|
| updated_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 更新时间 |
|
||||||
|
| is_deleted | SMALLINT | DEFAULT 0 | 逻辑删除 |
|
||||||
|
|
||||||
|
索引:
|
||||||
|
- `idx_prompt_tenant`: `(tenant_id)`
|
||||||
|
- `idx_prompt_system`: `(is_system) WHERE is_deleted = 0`
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -220,6 +220,77 @@ CREATE TABLE sys_platform_config (
|
||||||
INSERT INTO sys_platform_config (id, project_name, copyright_info)
|
INSERT INTO sys_platform_config (id, project_name, copyright_info)
|
||||||
VALUES (1, 'iMeeting 智能会议系统', '© 2026 iMeeting Team. All rights reserved.');
|
VALUES (1, 'iMeeting 智能会议系统', '© 2026 iMeeting Team. All rights reserved.');
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- 6. 业务模块 - 声纹管理
|
||||||
|
-- ----------------------------
|
||||||
|
DROP TABLE IF EXISTS biz_speakers CASCADE;
|
||||||
|
CREATE TABLE biz_speakers (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
user_id BIGINT NOT NULL, -- 关联系统用户ID
|
||||||
|
name VARCHAR(100) NOT NULL, -- 发言人姓名
|
||||||
|
voice_path VARCHAR(512), -- 原始声纹文件存储路径
|
||||||
|
voice_ext VARCHAR(10), -- 文件后缀
|
||||||
|
voice_size BIGINT, -- 文件大小
|
||||||
|
status SMALLINT DEFAULT 1, -- 状态: 1=已保存, 2=注册中, 3=已注册, 4=失败
|
||||||
|
embedding VECTOR(512), -- 声纹特征向量 (预留 pgvector 字段)
|
||||||
|
remark TEXT, -- 备注
|
||||||
|
created_at TIMESTAMP(6) NOT NULL DEFAULT now(),
|
||||||
|
updated_at TIMESTAMP(6) NOT NULL DEFAULT now(),
|
||||||
|
is_deleted SMALLINT NOT NULL DEFAULT 0
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_speaker_user ON biz_speakers (user_id) WHERE is_deleted = 0;
|
||||||
|
|
||||||
|
COMMENT ON TABLE biz_speakers IS '声纹发言人基础信息表 (用户全局资源)';
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- 7. 业务模块 - 热词管理
|
||||||
|
-- ----------------------------
|
||||||
|
DROP TABLE IF EXISTS biz_hot_words CASCADE;
|
||||||
|
CREATE TABLE biz_hot_words (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
tenant_id BIGINT NOT NULL, -- 租户ID (强制隔离)
|
||||||
|
word VARCHAR(100) NOT NULL, -- 热词原文
|
||||||
|
pinyin_list JSONB, -- 拼音数组 (支持多音字, 如 ["i mi ting", "i mei ting"])
|
||||||
|
match_strategy SMALLINT DEFAULT 1, -- 匹配策略: 1:精确匹配, 2:拼音模糊匹配
|
||||||
|
category VARCHAR(50), -- 类别 (人名、术语、地名)
|
||||||
|
weight INTEGER DEFAULT 10, -- 权重 (1-100)
|
||||||
|
status SMALLINT DEFAULT 1, -- 状态: 1:启用, 0:禁用
|
||||||
|
is_synced SMALLINT DEFAULT 0, -- 是否已同步至第三方引擎: 0:未同步, 1:已同步
|
||||||
|
remark TEXT, -- 备注
|
||||||
|
created_at TIMESTAMP(6) NOT NULL DEFAULT now(),
|
||||||
|
updated_at TIMESTAMP(6) NOT NULL DEFAULT now(),
|
||||||
|
is_deleted SMALLINT NOT NULL DEFAULT 0
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_hotword_tenant ON biz_hot_words (tenant_id);
|
||||||
|
CREATE INDEX idx_hotword_word ON biz_hot_words (word) WHERE is_deleted = 0;
|
||||||
|
|
||||||
|
COMMENT ON TABLE biz_hot_words IS '语音识别热词表';
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- 8. 业务模块 - 提示词模板
|
||||||
|
-- ----------------------------
|
||||||
|
DROP TABLE IF EXISTS biz_prompt_templates CASCADE;
|
||||||
|
CREATE TABLE biz_prompt_templates (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
tenant_id BIGINT NOT NULL DEFAULT 0, -- 租户ID (0为系统级)
|
||||||
|
template_name VARCHAR(100) NOT NULL, -- 模板名称
|
||||||
|
category VARCHAR(20), -- 分类 (字典: biz_prompt_category)
|
||||||
|
is_system SMALLINT DEFAULT 0, -- 是否系统预置 (1:是, 0:否)
|
||||||
|
prompt_content TEXT NOT NULL, -- 提示词内容
|
||||||
|
status SMALLINT DEFAULT 1, -- 状态: 1:启用, 0:禁用
|
||||||
|
remark VARCHAR(255), -- 备注
|
||||||
|
created_at TIMESTAMP(6) NOT NULL DEFAULT now(),
|
||||||
|
updated_at TIMESTAMP(6) NOT NULL DEFAULT now(),
|
||||||
|
is_deleted SMALLINT NOT NULL DEFAULT 0
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_prompt_tenant ON biz_prompt_templates (tenant_id);
|
||||||
|
CREATE INDEX idx_prompt_system ON biz_prompt_templates (is_system) WHERE is_deleted = 0;
|
||||||
|
|
||||||
|
COMMENT ON TABLE biz_prompt_templates IS '会议总结提示词模板表';
|
||||||
|
|
||||||
-- ----------------------------
|
-- ----------------------------
|
||||||
-- 5. 基础初始化数据
|
-- 5. 基础初始化数据
|
||||||
-- ----------------------------
|
-- ----------------------------
|
||||||
|
|
|
||||||
|
|
@ -79,6 +79,11 @@
|
||||||
<artifactId>easy-captcha</artifactId>
|
<artifactId>easy-captcha</artifactId>
|
||||||
<version>${easycaptcha.version}</version>
|
<version>${easycaptcha.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.belerweb</groupId>
|
||||||
|
<artifactId>pinyin4j</artifactId>
|
||||||
|
<version>2.5.1</version>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.projectlombok</groupId>
|
<groupId>org.projectlombok</groupId>
|
||||||
<artifactId>lombok</artifactId>
|
<artifactId>lombok</artifactId>
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,7 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||||
try {
|
try {
|
||||||
Claims claims = jwtTokenProvider.parseToken(token);
|
Claims claims = jwtTokenProvider.parseToken(token);
|
||||||
String username = claims.get("username", String.class);
|
String username = claims.get("username", String.class);
|
||||||
|
String displayName = claims.get("displayName", String.class);
|
||||||
Long userId = claims.get("userId", Long.class);
|
Long userId = claims.get("userId", Long.class);
|
||||||
Long tenantId = claims.get("tenantId", Long.class);
|
Long tenantId = claims.get("tenantId", Long.class);
|
||||||
|
|
||||||
|
|
@ -116,7 +117,7 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LoginUser loginUser = new LoginUser(userId, activeTenantId, username, user.getIsPlatformAdmin(), permissions);
|
LoginUser loginUser = new LoginUser(userId, activeTenantId, username, displayName, user.getIsPlatformAdmin(), permissions);
|
||||||
|
|
||||||
UsernamePasswordAuthenticationToken authentication =
|
UsernamePasswordAuthenticationToken authentication =
|
||||||
new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
|
new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ public class TokenResponse {
|
||||||
private String refreshToken;
|
private String refreshToken;
|
||||||
private long accessExpiresInMinutes;
|
private long accessExpiresInMinutes;
|
||||||
private long refreshExpiresInDays;
|
private long refreshExpiresInDays;
|
||||||
|
private Integer pwdResetRequired;
|
||||||
private List<TenantInfo> availableTenants;
|
private List<TenantInfo> availableTenants;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,7 @@ public class MybatisPlusConfig {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 公共表始终忽略过滤
|
// 公共表始终忽略过滤
|
||||||
return List.of("sys_tenant","sys_platform_config", "sys_user", "sys_tenant_user", "sys_permission", "sys_role_permission", "sys_user_role", "sys_dict_type", "sys_dict_item", "sys_param").contains(tableName.toLowerCase());
|
return List.of("sys_tenant","sys_platform_config", "sys_user", "sys_tenant_user", "sys_permission", "sys_role_permission", "sys_user_role", "sys_dict_type", "sys_dict_item", "sys_param", "biz_speakers", "biz_prompt_templates").contains(tableName.toLowerCase());
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
|
interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
|
||||||
|
|
@ -67,6 +67,14 @@ public class MybatisPlusConfig {
|
||||||
return new MetaObjectHandler() {
|
return new MetaObjectHandler() {
|
||||||
@Override
|
@Override
|
||||||
public void insertFill(MetaObject metaObject) {
|
public void insertFill(MetaObject metaObject) {
|
||||||
|
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
|
||||||
|
if (auth != null && auth.getPrincipal() instanceof LoginUser) {
|
||||||
|
LoginUser user = (LoginUser) auth.getPrincipal();
|
||||||
|
strictInsertFill(metaObject, "tenantId", Long.class, user.getTenantId());
|
||||||
|
} else {
|
||||||
|
strictInsertFill(metaObject, "tenantId", Long.class, 0L);
|
||||||
|
}
|
||||||
|
|
||||||
strictInsertFill(metaObject, "createdAt", LocalDateTime::now, LocalDateTime.class);
|
strictInsertFill(metaObject, "createdAt", LocalDateTime::now, LocalDateTime.class);
|
||||||
strictInsertFill(metaObject, "updatedAt", LocalDateTime::now, LocalDateTime.class);
|
strictInsertFill(metaObject, "updatedAt", LocalDateTime::now, LocalDateTime.class);
|
||||||
strictInsertFill(metaObject, "status", () -> 1, Integer.class);
|
strictInsertFill(metaObject, "status", () -> 1, Integer.class);
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,6 @@ public class DictItemController {
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/type/{typeCode}")
|
@GetMapping("/type/{typeCode}")
|
||||||
@PreAuthorize("@ss.hasPermi('sys_dict:query')")
|
|
||||||
public ApiResponse<List<SysDictItem>> getByType(@PathVariable String typeCode) {
|
public ApiResponse<List<SysDictItem>> getByType(@PathVariable String typeCode) {
|
||||||
return ApiResponse.ok(sysDictItemService.getItemsByTypeCode(typeCode));
|
return ApiResponse.ok(sysDictItemService.getItemsByTypeCode(typeCode));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,9 @@ import java.time.LocalDateTime;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
public class BaseEntity {
|
public class BaseEntity {
|
||||||
|
@TableField(fill = FieldFill.INSERT)
|
||||||
private Long tenantId;
|
private Long tenantId;
|
||||||
|
|
||||||
private Integer status;
|
private Integer status;
|
||||||
|
|
||||||
@TableLogic(value = "0", delval = "1")
|
@TableLogic(value = "0", delval = "1")
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ public class LoginUser implements UserDetails {
|
||||||
private Long userId;
|
private Long userId;
|
||||||
private Long tenantId;
|
private Long tenantId;
|
||||||
private String username;
|
private String username;
|
||||||
|
private String displayName;
|
||||||
private Boolean isPlatformAdmin;
|
private Boolean isPlatformAdmin;
|
||||||
private Set<String> permissions;
|
private Set<String> permissions;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -303,7 +303,9 @@ public class AuthServiceImpl implements AuthService {
|
||||||
accessClaims.put("userId", user.getUserId());
|
accessClaims.put("userId", user.getUserId());
|
||||||
accessClaims.put("tenantId", tenantId);
|
accessClaims.put("tenantId", tenantId);
|
||||||
accessClaims.put("username", user.getUsername());
|
accessClaims.put("username", user.getUsername());
|
||||||
|
accessClaims.put("displayName", user.getDisplayName());
|
||||||
accessClaims.put("deviceCode", deviceCode);
|
accessClaims.put("deviceCode", deviceCode);
|
||||||
|
accessClaims.put("pwdResetRequired", user.getPwdResetRequired());
|
||||||
|
|
||||||
Map<String, Object> refreshClaims = new HashMap<>();
|
Map<String, Object> refreshClaims = new HashMap<>();
|
||||||
refreshClaims.put("tokenType", "refresh");
|
refreshClaims.put("tokenType", "refresh");
|
||||||
|
|
@ -318,6 +320,7 @@ public class AuthServiceImpl implements AuthService {
|
||||||
.refreshToken(refresh)
|
.refreshToken(refresh)
|
||||||
.accessExpiresInMinutes(accessMinutes)
|
.accessExpiresInMinutes(accessMinutes)
|
||||||
.refreshExpiresInDays(refreshDays)
|
.refreshExpiresInDays(refreshDays)
|
||||||
|
.pwdResetRequired(user.getPwdResetRequired())
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,14 +3,14 @@
|
||||||
|
|
||||||
spring:
|
spring:
|
||||||
datasource:
|
datasource:
|
||||||
url: jdbc:postgresql://10.100.51.51:5432/imeeting
|
url: jdbc:postgresql://10.100.51.199:5432/imeeting_db
|
||||||
username: postgres
|
username: postgres
|
||||||
password: Unis@123
|
password: postgres
|
||||||
data:
|
data:
|
||||||
redis:
|
redis:
|
||||||
host: 10.100.51.51
|
host: 10.100.51.199
|
||||||
port: 6379
|
port: 6379
|
||||||
password: Unis@123
|
password: unis@123
|
||||||
database: 15
|
database: 15
|
||||||
cache:
|
cache:
|
||||||
type: redis
|
type: redis
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -17,6 +17,7 @@
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-i18next": "^16.5.4",
|
"react-i18next": "^16.5.4",
|
||||||
|
"react-markdown": "^10.1.0",
|
||||||
"react-router-dom": "^6.22.3",
|
"react-router-dom": "^6.22.3",
|
||||||
"zustand": "^4.5.2"
|
"zustand": "^4.5.2"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ export interface TokenResponse {
|
||||||
refreshToken: string;
|
refreshToken: string;
|
||||||
accessExpiresInMinutes: number;
|
accessExpiresInMinutes: number;
|
||||||
refreshExpiresInDays: number;
|
refreshExpiresInDays: number;
|
||||||
|
pwdResetRequired?: number;
|
||||||
availableTenants?: TenantInfo[];
|
availableTenants?: TenantInfo[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,18 @@ import { UserProfile } from "../types";
|
||||||
export function useAuth() {
|
export function useAuth() {
|
||||||
const [accessToken, setAccessToken] = useState<string | null>(() => localStorage.getItem("accessToken"));
|
const [accessToken, setAccessToken] = useState<string | null>(() => localStorage.getItem("accessToken"));
|
||||||
|
|
||||||
|
const parseJwtPayload = (token: string) => {
|
||||||
|
try {
|
||||||
|
const payloadPart = token.split(".")[1];
|
||||||
|
if (!payloadPart) return null;
|
||||||
|
const normalized = payloadPart.replace(/-/g, "+").replace(/_/g, "/");
|
||||||
|
const padded = normalized + "=".repeat((4 - (normalized.length % 4)) % 4);
|
||||||
|
return JSON.parse(atob(padded));
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handler = () => setAccessToken(localStorage.getItem("accessToken"));
|
const handler = () => setAccessToken(localStorage.getItem("accessToken"));
|
||||||
window.addEventListener("storage", handler);
|
window.addEventListener("storage", handler);
|
||||||
|
|
@ -12,7 +24,17 @@ export function useAuth() {
|
||||||
|
|
||||||
const profile = useMemo<UserProfile | null>(() => {
|
const profile = useMemo<UserProfile | null>(() => {
|
||||||
const data = sessionStorage.getItem("userProfile");
|
const data = sessionStorage.getItem("userProfile");
|
||||||
return data ? JSON.parse(data) : null;
|
if (data) {
|
||||||
|
return JSON.parse(data);
|
||||||
|
}
|
||||||
|
if (!accessToken) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const payload = parseJwtPayload(accessToken);
|
||||||
|
if (payload && (payload.pwdResetRequired === 0 || payload.pwdResetRequired === 1)) {
|
||||||
|
return { pwdResetRequired: Number(payload.pwdResetRequired) } as UserProfile;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}, [accessToken]);
|
}, [accessToken]);
|
||||||
|
|
||||||
const isAuthed = !!accessToken;
|
const isAuthed = !!accessToken;
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,10 @@ import {
|
||||||
BellOutlined,
|
BellOutlined,
|
||||||
SettingOutlined,
|
SettingOutlined,
|
||||||
GlobalOutlined,
|
GlobalOutlined,
|
||||||
ShopOutlined
|
ShopOutlined,
|
||||||
|
AudioOutlined,
|
||||||
|
TagsOutlined,
|
||||||
|
BulbOutlined
|
||||||
} from "@ant-design/icons";
|
} from "@ant-design/icons";
|
||||||
import { useAuth } from "../hooks/useAuth";
|
import { useAuth } from "../hooks/useAuth";
|
||||||
import { usePermission } from "../hooks/usePermission";
|
import { usePermission } from "../hooks/usePermission";
|
||||||
|
|
@ -32,6 +35,9 @@ const iconMap: Record<string, any> = {
|
||||||
"role": <TeamOutlined />,
|
"role": <TeamOutlined />,
|
||||||
"permission": <SafetyCertificateOutlined />,
|
"permission": <SafetyCertificateOutlined />,
|
||||||
"device": <DesktopOutlined />,
|
"device": <DesktopOutlined />,
|
||||||
|
"audio": <AudioOutlined />,
|
||||||
|
"hotword": <TagsOutlined />,
|
||||||
|
"prompt": <BulbOutlined />,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function AppLayout() {
|
export default function AppLayout() {
|
||||||
|
|
@ -140,29 +146,32 @@ export default function AppLayout() {
|
||||||
return roots;
|
return roots;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const toMenuItems = useCallback((nodes: (SysPermission & { children?: SysPermission[] })[]): any[] =>
|
const menuItems = useMemo(() => {
|
||||||
nodes.map((m) => {
|
const toMenuItems = (nodes: (SysPermission & { children?: SysPermission[] })[]): any[] => {
|
||||||
const key = m.path || m.code || String(m.permId);
|
if (!Array.isArray(nodes)) return [];
|
||||||
const icon = m.icon ? (iconMap[m.icon] || <SettingOutlined />) : <SettingOutlined />;
|
return nodes.map((m) => {
|
||||||
|
const key = m.path || m.code || String(m.permId);
|
||||||
|
const icon = m.icon ? (iconMap[m.icon] || <SettingOutlined />) : <SettingOutlined />;
|
||||||
|
|
||||||
|
if (m.permType === 'directory' || (m.children && m.children.length > 0)) {
|
||||||
|
return {
|
||||||
|
key,
|
||||||
|
icon,
|
||||||
|
label: m.name,
|
||||||
|
children: m.children && m.children.length > 0 ? toMenuItems(m.children as any) : undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Directory type or item with children should not have a link if it's a directory
|
|
||||||
if (m.permType === 'directory' || (m.children && m.children.length > 0)) {
|
|
||||||
return {
|
return {
|
||||||
key,
|
key,
|
||||||
icon,
|
icon,
|
||||||
label: m.name,
|
label: <Link to={m.path || "#"}>{m.name}</Link>,
|
||||||
children: m.children && m.children.length > 0 ? toMenuItems(m.children) : undefined,
|
|
||||||
};
|
};
|
||||||
}
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return toMenuItems(buildMenuTree(menus));
|
||||||
key,
|
}, [menus, buildMenuTree]);
|
||||||
icon,
|
|
||||||
label: <Link to={m.path || "#"}>{m.name}</Link>,
|
|
||||||
};
|
|
||||||
}), []); // 移除 [toMenuItems] 依赖项以解决 TDZ 错误
|
|
||||||
|
|
||||||
const menuItems = useMemo(() => toMenuItems(buildMenuTree(menus)), [menus, buildMenuTree, toMenuItems]);
|
|
||||||
|
|
||||||
// Calculate open keys based on current path
|
// Calculate open keys based on current path
|
||||||
const [openKeys, setOpenKeys] = useState<string[]>([]);
|
const [openKeys, setOpenKeys] = useState<string[]>([]);
|
||||||
|
|
@ -170,9 +179,10 @@ export default function AppLayout() {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (menus.length > 0) {
|
if (menus.length > 0) {
|
||||||
const findParentKeys = (nodes: any[], path: string, parents: string[] = []): string[] | null => {
|
const findParentKeys = (nodes: any[], path: string, parents: string[] = []): string[] | null => {
|
||||||
|
if (!Array.isArray(nodes)) return null;
|
||||||
for (const node of nodes) {
|
for (const node of nodes) {
|
||||||
if (node.key === path) return parents;
|
if (node.key === path) return parents;
|
||||||
if (node.children) {
|
if (node.children && Array.isArray(node.children)) {
|
||||||
const found = findParentKeys(node.children, path, [...parents, node.key]);
|
const found = findParentKeys(node.children, path, [...parents, node.key]);
|
||||||
if (found) return found;
|
if (found) return found;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,18 @@ export default function Login() {
|
||||||
const [platformConfig, setPlatformConfig] = useState<SysPlatformConfig | null>(null);
|
const [platformConfig, setPlatformConfig] = useState<SysPlatformConfig | null>(null);
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
|
|
||||||
|
const parseJwtPayload = (token: string) => {
|
||||||
|
try {
|
||||||
|
const payloadPart = token.split(".")[1];
|
||||||
|
if (!payloadPart) return null;
|
||||||
|
const normalized = payloadPart.replace(/-/g, "+").replace(/_/g, "/");
|
||||||
|
const padded = normalized + "=".repeat((4 - (normalized.length % 4)) % 4);
|
||||||
|
return JSON.parse(atob(padded));
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const loadCaptcha = useCallback(async () => {
|
const loadCaptcha = useCallback(async () => {
|
||||||
if (!captchaEnabled) {
|
if (!captchaEnabled) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -75,18 +87,20 @@ export default function Login() {
|
||||||
localStorage.setItem("username", values.username);
|
localStorage.setItem("username", values.username);
|
||||||
if (data.availableTenants) {
|
if (data.availableTenants) {
|
||||||
localStorage.setItem("availableTenants", JSON.stringify(data.availableTenants));
|
localStorage.setItem("availableTenants", JSON.stringify(data.availableTenants));
|
||||||
// We should infer activeTenantId from token or just use the first/default logic
|
}
|
||||||
// For simplicity, we can parse the JWT to get tenantId, or backend can return it.
|
const payload = parseJwtPayload(data.accessToken);
|
||||||
// Let's assume for now we use the first one if not platform admin, or the backend logic.
|
if (payload?.tenantId !== undefined && payload?.tenantId !== null) {
|
||||||
// Actually, if we use a helper to parse JWT:
|
|
||||||
const payload = JSON.parse(atob(data.accessToken.split('.')[1]));
|
|
||||||
localStorage.setItem("activeTenantId", String(payload.tenantId));
|
localStorage.setItem("activeTenantId", String(payload.tenantId));
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const profile = await getCurrentUser();
|
const profile = await getCurrentUser();
|
||||||
sessionStorage.setItem("userProfile", JSON.stringify(profile));
|
sessionStorage.setItem("userProfile", JSON.stringify(profile));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
sessionStorage.removeItem("userProfile");
|
if (data.pwdResetRequired === 0 || data.pwdResetRequired === 1) {
|
||||||
|
sessionStorage.setItem("userProfile", JSON.stringify({ pwdResetRequired: data.pwdResetRequired }));
|
||||||
|
} else {
|
||||||
|
sessionStorage.removeItem("userProfile");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
message.success(t('common.success'));
|
message.success(t('common.success'));
|
||||||
window.location.href = "/";
|
window.location.href = "/";
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,9 @@ import RolePermissionBinding from "../pages/RolePermissionBinding";
|
||||||
import SysParams from "../pages/SysParams";
|
import SysParams from "../pages/SysParams";
|
||||||
import PlatformSettings from "../pages/PlatformSettings";
|
import PlatformSettings from "../pages/PlatformSettings";
|
||||||
import Profile from "../pages/Profile";
|
import Profile from "../pages/Profile";
|
||||||
|
import SpeakerReg from "../pages/business/SpeakerReg";
|
||||||
|
import HotWords from "../pages/business/HotWords";
|
||||||
|
import PromptTemplates from "../pages/business/PromptTemplates";
|
||||||
|
|
||||||
import type { MenuRoute } from "../types";
|
import type { MenuRoute } from "../types";
|
||||||
|
|
||||||
|
|
@ -29,5 +32,8 @@ export const menuRoutes: MenuRoute[] = [
|
||||||
{ path: "/logs", label: "日志管理", element: <Logs />, perm: "menu:logs" },
|
{ path: "/logs", label: "日志管理", element: <Logs />, perm: "menu:logs" },
|
||||||
{ path: "/devices", label: "设备管理", element: <Devices />, perm: "menu:devices" },
|
{ path: "/devices", label: "设备管理", element: <Devices />, perm: "menu:devices" },
|
||||||
{ path: "/user-roles", label: "用户角色绑定", element: <UserRoleBinding />, perm: "menu:user-roles" },
|
{ path: "/user-roles", label: "用户角色绑定", element: <UserRoleBinding />, perm: "menu:user-roles" },
|
||||||
{ path: "/role-permissions", label: "角色权限绑定", element: <RolePermissionBinding />, perm: "menu:role-permissions" }
|
{ path: "/role-permissions", label: "角色权限绑定", element: <RolePermissionBinding />, perm: "menu:role-permissions" },
|
||||||
|
{ path: "/speaker-reg", label: "声纹注册", element: <SpeakerReg />, perm: "menu:speaker" },
|
||||||
|
{ path: "/hotwords", label: "热词管理", element: <HotWords />, perm: "menu:hotword" },
|
||||||
|
{ path: "/prompts", label: "总结模板", element: <PromptTemplates />, perm: "menu:prompt" }
|
||||||
];
|
];
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue