feat(layout): 添加音频、热词和提示词菜单项并优化布局结构

- 在AppLayout中添加AudioOutlined、TagsOutlined和BulbOutlined图标
- 为audio、hotword和prompt路由配置对应的菜单图标映射
- 重构菜单项生成逻辑,使用useMemo优化性能并修复TDZ错误
- 在菜单树查找函数中添加数组验证防止运行时错误
- 添加新业务模块数据库表:声纹发言人表、热词管理表和提示词模板表
- 更新租户ID字段的自动填充逻辑和权限过滤配置
- 在认证流程中添加displayName和pwdResetRequired字段支持
- 添加React Markdown依赖用于内容渲染功能
dev_na
chenhao 2026-03-02 09:09:53 +08:00
parent f93d797382
commit 1a392d96b9
18 changed files with 1418 additions and 40 deletions

View File

@ -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`

View File

@ -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. 基础初始化数据
-- ---------------------------- -- ----------------------------

View File

@ -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>

View File

@ -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());

View File

@ -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

View File

@ -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);

View File

@ -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));
} }

View File

@ -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")

View File

@ -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;

View File

@ -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();
} }

View File

@ -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

View File

@ -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"
}, },

View File

@ -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[];
} }

View File

@ -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;

View File

@ -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);
// Directory type or item with children should not have a link if it's a directory const icon = m.icon ? (iconMap[m.icon] || <SettingOutlined />) : <SettingOutlined />;
if (m.permType === 'directory' || (m.children && m.children.length > 0)) {
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,
};
}
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 {
key,
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 // 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;
} }

View File

@ -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 = "/";

View File

@ -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" }
]; ];