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 | 创建时间 |
|
||||
| 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)
|
||||
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. 基础初始化数据
|
||||
-- ----------------------------
|
||||
|
|
|
|||
|
|
@ -79,6 +79,11 @@
|
|||
<artifactId>easy-captcha</artifactId>
|
||||
<version>${easycaptcha.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.belerweb</groupId>
|
||||
<artifactId>pinyin4j</artifactId>
|
||||
<version>2.5.1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
|||
try {
|
||||
Claims claims = jwtTokenProvider.parseToken(token);
|
||||
String username = claims.get("username", String.class);
|
||||
String displayName = claims.get("displayName", String.class);
|
||||
Long userId = claims.get("userId", 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 =
|
||||
new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ public class TokenResponse {
|
|||
private String refreshToken;
|
||||
private long accessExpiresInMinutes;
|
||||
private long refreshExpiresInDays;
|
||||
private Integer pwdResetRequired;
|
||||
private List<TenantInfo> availableTenants;
|
||||
|
||||
@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());
|
||||
|
|
@ -67,6 +67,14 @@ public class MybatisPlusConfig {
|
|||
return new MetaObjectHandler() {
|
||||
@Override
|
||||
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, "updatedAt", LocalDateTime::now, LocalDateTime.class);
|
||||
strictInsertFill(metaObject, "status", () -> 1, Integer.class);
|
||||
|
|
|
|||
|
|
@ -55,7 +55,6 @@ public class DictItemController {
|
|||
}
|
||||
|
||||
@GetMapping("/type/{typeCode}")
|
||||
@PreAuthorize("@ss.hasPermi('sys_dict:query')")
|
||||
public ApiResponse<List<SysDictItem>> getByType(@PathVariable String typeCode) {
|
||||
return ApiResponse.ok(sysDictItemService.getItemsByTypeCode(typeCode));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,9 @@ import java.time.LocalDateTime;
|
|||
|
||||
@Data
|
||||
public class BaseEntity {
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private Long tenantId;
|
||||
|
||||
private Integer status;
|
||||
|
||||
@TableLogic(value = "0", delval = "1")
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ public class LoginUser implements UserDetails {
|
|||
private Long userId;
|
||||
private Long tenantId;
|
||||
private String username;
|
||||
private String displayName;
|
||||
private Boolean isPlatformAdmin;
|
||||
private Set<String> permissions;
|
||||
|
||||
|
|
|
|||
|
|
@ -303,7 +303,9 @@ public class AuthServiceImpl implements AuthService {
|
|||
accessClaims.put("userId", user.getUserId());
|
||||
accessClaims.put("tenantId", tenantId);
|
||||
accessClaims.put("username", user.getUsername());
|
||||
accessClaims.put("displayName", user.getDisplayName());
|
||||
accessClaims.put("deviceCode", deviceCode);
|
||||
accessClaims.put("pwdResetRequired", user.getPwdResetRequired());
|
||||
|
||||
Map<String, Object> refreshClaims = new HashMap<>();
|
||||
refreshClaims.put("tokenType", "refresh");
|
||||
|
|
@ -318,6 +320,7 @@ public class AuthServiceImpl implements AuthService {
|
|||
.refreshToken(refresh)
|
||||
.accessExpiresInMinutes(accessMinutes)
|
||||
.refreshExpiresInDays(refreshDays)
|
||||
.pwdResetRequired(user.getPwdResetRequired())
|
||||
.build();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,14 +3,14 @@
|
|||
|
||||
spring:
|
||||
datasource:
|
||||
url: jdbc:postgresql://10.100.51.51:5432/imeeting
|
||||
url: jdbc:postgresql://10.100.51.199:5432/imeeting_db
|
||||
username: postgres
|
||||
password: Unis@123
|
||||
password: postgres
|
||||
data:
|
||||
redis:
|
||||
host: 10.100.51.51
|
||||
host: 10.100.51.199
|
||||
port: 6379
|
||||
password: Unis@123
|
||||
password: unis@123
|
||||
database: 15
|
||||
cache:
|
||||
type: redis
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -17,6 +17,7 @@
|
|||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-i18next": "^16.5.4",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-router-dom": "^6.22.3",
|
||||
"zustand": "^4.5.2"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ export interface TokenResponse {
|
|||
refreshToken: string;
|
||||
accessExpiresInMinutes: number;
|
||||
refreshExpiresInDays: number;
|
||||
pwdResetRequired?: number;
|
||||
availableTenants?: TenantInfo[];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,18 @@ import { UserProfile } from "../types";
|
|||
export function useAuth() {
|
||||
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(() => {
|
||||
const handler = () => setAccessToken(localStorage.getItem("accessToken"));
|
||||
window.addEventListener("storage", handler);
|
||||
|
|
@ -12,7 +24,17 @@ export function useAuth() {
|
|||
|
||||
const profile = useMemo<UserProfile | null>(() => {
|
||||
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]);
|
||||
|
||||
const isAuthed = !!accessToken;
|
||||
|
|
|
|||
|
|
@ -15,7 +15,10 @@ import {
|
|||
BellOutlined,
|
||||
SettingOutlined,
|
||||
GlobalOutlined,
|
||||
ShopOutlined
|
||||
ShopOutlined,
|
||||
AudioOutlined,
|
||||
TagsOutlined,
|
||||
BulbOutlined
|
||||
} from "@ant-design/icons";
|
||||
import { useAuth } from "../hooks/useAuth";
|
||||
import { usePermission } from "../hooks/usePermission";
|
||||
|
|
@ -32,6 +35,9 @@ const iconMap: Record<string, any> = {
|
|||
"role": <TeamOutlined />,
|
||||
"permission": <SafetyCertificateOutlined />,
|
||||
"device": <DesktopOutlined />,
|
||||
"audio": <AudioOutlined />,
|
||||
"hotword": <TagsOutlined />,
|
||||
"prompt": <BulbOutlined />,
|
||||
};
|
||||
|
||||
export default function AppLayout() {
|
||||
|
|
@ -140,18 +146,19 @@ export default function AppLayout() {
|
|||
return roots;
|
||||
}, []);
|
||||
|
||||
const toMenuItems = useCallback((nodes: (SysPermission & { children?: SysPermission[] })[]): any[] =>
|
||||
nodes.map((m) => {
|
||||
const menuItems = useMemo(() => {
|
||||
const toMenuItems = (nodes: (SysPermission & { children?: SysPermission[] })[]): any[] => {
|
||||
if (!Array.isArray(nodes)) return [];
|
||||
return nodes.map((m) => {
|
||||
const key = m.path || m.code || String(m.permId);
|
||||
const icon = m.icon ? (iconMap[m.icon] || <SettingOutlined />) : <SettingOutlined />;
|
||||
|
||||
// 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 {
|
||||
key,
|
||||
icon,
|
||||
label: m.name,
|
||||
children: m.children && m.children.length > 0 ? toMenuItems(m.children) : undefined,
|
||||
children: m.children && m.children.length > 0 ? toMenuItems(m.children as any) : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -160,9 +167,11 @@ export default function AppLayout() {
|
|||
icon,
|
||||
label: <Link to={m.path || "#"}>{m.name}</Link>,
|
||||
};
|
||||
}), []); // 移除 [toMenuItems] 依赖项以解决 TDZ 错误
|
||||
});
|
||||
};
|
||||
|
||||
const menuItems = useMemo(() => toMenuItems(buildMenuTree(menus)), [menus, buildMenuTree, toMenuItems]);
|
||||
return toMenuItems(buildMenuTree(menus));
|
||||
}, [menus, buildMenuTree]);
|
||||
|
||||
// Calculate open keys based on current path
|
||||
const [openKeys, setOpenKeys] = useState<string[]>([]);
|
||||
|
|
@ -170,9 +179,10 @@ export default function AppLayout() {
|
|||
useEffect(() => {
|
||||
if (menus.length > 0) {
|
||||
const findParentKeys = (nodes: any[], path: string, parents: string[] = []): string[] | null => {
|
||||
if (!Array.isArray(nodes)) return null;
|
||||
for (const node of nodes) {
|
||||
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]);
|
||||
if (found) return found;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,18 @@ export default function Login() {
|
|||
const [platformConfig, setPlatformConfig] = useState<SysPlatformConfig | null>(null);
|
||||
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 () => {
|
||||
if (!captchaEnabled) {
|
||||
return;
|
||||
|
|
@ -75,19 +87,21 @@ export default function Login() {
|
|||
localStorage.setItem("username", values.username);
|
||||
if (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.
|
||||
// Let's assume for now we use the first one if not platform admin, or the backend logic.
|
||||
// Actually, if we use a helper to parse JWT:
|
||||
const payload = JSON.parse(atob(data.accessToken.split('.')[1]));
|
||||
}
|
||||
const payload = parseJwtPayload(data.accessToken);
|
||||
if (payload?.tenantId !== undefined && payload?.tenantId !== null) {
|
||||
localStorage.setItem("activeTenantId", String(payload.tenantId));
|
||||
}
|
||||
try {
|
||||
const profile = await getCurrentUser();
|
||||
sessionStorage.setItem("userProfile", JSON.stringify(profile));
|
||||
} catch (e) {
|
||||
if (data.pwdResetRequired === 0 || data.pwdResetRequired === 1) {
|
||||
sessionStorage.setItem("userProfile", JSON.stringify({ pwdResetRequired: data.pwdResetRequired }));
|
||||
} else {
|
||||
sessionStorage.removeItem("userProfile");
|
||||
}
|
||||
}
|
||||
message.success(t('common.success'));
|
||||
window.location.href = "/";
|
||||
} catch (e: any) {
|
||||
|
|
|
|||
|
|
@ -12,6 +12,9 @@ import RolePermissionBinding from "../pages/RolePermissionBinding";
|
|||
import SysParams from "../pages/SysParams";
|
||||
import PlatformSettings from "../pages/PlatformSettings";
|
||||
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";
|
||||
|
||||
|
|
@ -29,5 +32,8 @@ export const menuRoutes: MenuRoute[] = [
|
|||
{ path: "/logs", label: "日志管理", element: <Logs />, perm: "menu:logs" },
|
||||
{ path: "/devices", label: "设备管理", element: <Devices />, perm: "menu:devices" },
|
||||
{ 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