diff --git a/web/config/config.ts b/web/config/config.ts index fdb74b4..861eafc 100644 --- a/web/config/config.ts +++ b/web/config/config.ts @@ -83,9 +83,9 @@ export default defineConfig({ * @name layout 插件 * @doc https://umijs.org/docs/max/layout-menu */ - title: "Ant Design Pro", + title: "KRA Admin", layout: { - locale: true, + locale: false, // 禁用菜单国际化,与 GVA 保持一致 ...defaultSettings, }, /** @@ -100,14 +100,9 @@ export default defineConfig({ /** * @name 国际化插件 * @doc https://umijs.org/docs/max/i18n + * @description 禁用国际化,与 GVA 保持一致 */ - locale: { - // default zh-CN - default: "zh-CN", - antd: true, - // default true, when it is true, will use `navigator.language` overwrite default - baseNavigator: true, - }, + locale: false, /** * @name antd 插件 * @description 内置了 babel import 插件 diff --git a/web/public/default-avatar.svg b/web/public/default-avatar.svg new file mode 100644 index 0000000..7586870 --- /dev/null +++ b/web/public/default-avatar.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/web/src/app.tsx b/web/src/app.tsx index e759d2c..1f95c91 100644 --- a/web/src/app.tsx +++ b/web/src/app.tsx @@ -2,15 +2,22 @@ * KRA - App Configuration * 应用运行时配置 */ -import { AvatarDropdown, AvatarName, Footer, Question } from '@/components'; +import { AvatarDropdown, AvatarName, Footer } from '@/components'; import { getUserInfo } from '@/services/kratos/user'; import { getToken, removeToken } from '@/utils/auth'; -import { UserOutlined } from '@ant-design/icons'; +import { + BulbOutlined, + BulbFilled, + ReloadOutlined, + SettingOutlined, + UserOutlined, + QuestionCircleOutlined, +} from '@ant-design/icons'; import type { Settings as LayoutSettings } from '@ant-design/pro-components'; import { SettingDrawer } from '@ant-design/pro-components'; import type { RequestConfig, RunTimeLayoutConfig } from '@umijs/max'; import { history } from '@umijs/max'; -import { message } from 'antd'; +import { message, Tooltip } from 'antd'; import defaultSettings from '../config/defaultSettings'; import { errorConfig } from './requestErrorConfig'; @@ -90,13 +97,93 @@ export async function getInitialState(): Promise<{ * @see https://procomponents.ant.design/components/layout */ export const layout: RunTimeLayoutConfig = ({ initialState, setInitialState }) => { + // 刷新页面 + const handleRefresh = () => { + window.location.reload(); + }; + + // 切换主题 + const handleToggleTheme = () => { + const newNavTheme = initialState?.settings?.navTheme === 'realDark' ? 'light' : 'realDark'; + setInitialState((preInitialState) => ({ + ...preInitialState, + settings: { + ...preInitialState?.settings, + navTheme: newNavTheme, + }, + })); + }; + return { // 先展开 settings,让后面的配置可以覆盖 ...initialState?.settings, // 右上角操作区 actionsRender: () => { - return []; + const isDark = initialState?.settings?.navTheme === 'realDark'; + return [ + // 帮助文档 + + + + + , + // 刷新 + + + + + , + // 切换主题 + + + {isDark ? ( + + ) : ( + + )} + + , + ]; }, // 头像配置 @@ -104,7 +191,7 @@ export const layout: RunTimeLayoutConfig = ({ initialState, setInitialState }) = src: initialState?.currentUser?.avatar || initialState?.currentUser?.headerImg, title: , icon: , - render: (_, avatarChildren) => {avatarChildren}, + render: (_, avatarChildren) => {avatarChildren}, }, // 水印 diff --git a/web/src/assets/index.ts b/web/src/assets/index.ts index 8c47e9e..4dae361 100644 --- a/web/src/assets/index.ts +++ b/web/src/assets/index.ts @@ -21,7 +21,7 @@ */ // 默认头像 -export const defaultAvatar = '/logo.svg'; +export const defaultAvatar = '/default-avatar.svg'; // Logo export const logo = '/logo.svg'; diff --git a/web/src/components/HeaderDropdown/index.tsx b/web/src/components/HeaderDropdown/index.tsx index b77a8de..c5c0c2f 100644 --- a/web/src/components/HeaderDropdown/index.tsx +++ b/web/src/components/HeaderDropdown/index.tsx @@ -32,7 +32,7 @@ const HeaderDropdown: React.FC = ({ const { styles } = useStyles(); return ( ); diff --git a/web/src/components/RightContent/AvatarDropdown.tsx b/web/src/components/RightContent/AvatarDropdown.tsx index c14dc35..1ecbd14 100644 --- a/web/src/components/RightContent/AvatarDropdown.tsx +++ b/web/src/components/RightContent/AvatarDropdown.tsx @@ -1,12 +1,14 @@ import { createAdminService } from "@/services/index"; +import { setUserAuthority } from "@/services/kratos/user"; import { LogoutOutlined, SettingOutlined, + SwapOutlined, UserOutlined, } from "@ant-design/icons"; import { history, useModel } from "@umijs/max"; import type { MenuProps } from "antd"; -import { Spin } from "antd"; +import { message, Spin } from "antd"; import { createStyles } from "antd-style"; import React from "react"; import { flushSync } from "react-dom"; @@ -47,8 +49,11 @@ export const AvatarDropdown: React.FC = ({ menu, children, }) => { + const { styles } = useStyles(); + const { initialState, setInitialState } = useModel("@@initialState"); + /** - * 退出登录,并且将当前的 url 保存 + * 退出登录 */ const loginOut = async () => { await adminService.Logout({}); @@ -57,9 +62,7 @@ export const AvatarDropdown: React.FC = ({ const searchParams = new URLSearchParams({ redirect: pathname + search, }); - /** 此方法会跳转到 redirect 参数所在的位置 */ const redirect = urlParams.get("redirect"); - // Note: There may be security issues, please note if (window.location.pathname !== "/user/login" && !redirect) { history.replace({ pathname: "/user/login", @@ -67,9 +70,27 @@ export const AvatarDropdown: React.FC = ({ }); } }; - const { styles } = useStyles(); - const { initialState, setInitialState } = useModel("@@initialState"); + /** + * 切换角色 + */ + const changeUserAuth = async (authorityId: string) => { + try { + const res = await setUserAuthority({ + uuid: initialState?.currentUser?.uuid || '', + authorityId + }); + if (res.code === 0) { + message.success('角色切换成功'); + // 标记需要关闭所有标签页并跳转首页 + window.sessionStorage.setItem('needCloseAll', 'true'); + window.sessionStorage.setItem('needToHome', 'true'); + window.location.reload(); + } + } catch (error) { + message.error('角色切换失败'); + } + }; const onMenuClick: MenuProps["onClick"] = (event) => { const { key } = event; @@ -80,6 +101,15 @@ export const AvatarDropdown: React.FC = ({ loginOut(); return; } + if (key === "person") { + history.push("/person"); + return; + } + if (key.startsWith("switch_")) { + const authorityId = key.replace("switch_", ""); + changeUserAuth(authorityId); + return; + } history.push(`/account/${key}`); }; @@ -105,30 +135,67 @@ export const AvatarDropdown: React.FC = ({ return loading; } - const menuItems = [ - ...(menu - ? [ - { - key: "center", - icon: , - label: "个人中心", - }, - { - key: "settings", - icon: , - label: "个人设置", - }, - { - type: "divider" as const, - }, - ] - : []), - { - key: "logout", - icon: , - label: "退出登录", - }, - ]; + // 构建菜单项 + const menuItems: MenuProps['items'] = []; + + // 当前角色显示 + if (currentUser.authority) { + menuItems.push({ + key: "current_role", + label: ( + + 当前角色:{currentUser.authority.authorityName} + + ), + disabled: true, + }); + } + + // 切换角色选项 + if (currentUser.authorities && currentUser.authorities.length > 1) { + const otherAuthorities = currentUser.authorities.filter( + (auth: any) => auth.authorityId !== currentUser.authority?.authorityId + ); + if (otherAuthorities.length > 0) { + menuItems.push({ type: "divider" as const }); + otherAuthorities.forEach((auth: any) => { + menuItems.push({ + key: `switch_${auth.authorityId}`, + icon: , + label: `切换为:${auth.authorityName}`, + }); + }); + } + } + + // 分隔线 + menuItems.push({ type: "divider" as const }); + + // 个人信息 + menuItems.push({ + key: "person", + icon: , + label: "个人信息", + }); + + // 个人设置(可选) + if (menu) { + menuItems.push({ + key: "settings", + icon: , + label: "个人设置", + }); + } + + // 分隔线 + menuItems.push({ type: "divider" as const }); + + // 退出登录 + menuItems.push({ + key: "logout", + icon: , + label: "退出登录", + }); return ( { return ( { { {stats.map((stat, index) => ( - + { title={stat.title} value={stat.value} suffix={stat.suffix} - valueStyle={{ color: stat.color }} + styles={{ content: { color: stat.color } }} />