任务三

This commit is contained in:
Yvan 2026-01-07 19:37:45 +08:00
parent be3b6641a6
commit 66e28bad92
12 changed files with 372 additions and 286 deletions

View File

@ -2,11 +2,13 @@ package middleware
import (
"context"
"encoding/json"
"errors"
"strconv"
"strings"
"time"
"github.com/go-kratos/kratos/v2/metadata"
"github.com/go-kratos/kratos/v2/middleware"
"github.com/go-kratos/kratos/v2/transport"
"github.com/go-kratos/kratos/v2/transport/http"
@ -14,7 +16,14 @@ import (
pkgjwt "kra/pkg/jwt"
)
type authKey struct{}
const (
// metadata keys for claims
mdKeyUserID = "x-md-user-id"
mdKeyUsername = "x-md-username"
mdKeyAuthorityID = "x-md-authority-id"
mdKeyUUID = "x-md-uuid"
mdKeyClaims = "x-md-claims"
)
var (
ErrMissingToken = errors.New("未登录或非法访问")
@ -65,8 +74,15 @@ func JWTAuth(cfg JWTAuthConfig) middleware.Middleware {
return nil, ErrInvalidToken
}
// 将claims存入context
ctx = context.WithValue(ctx, authKey{}, claims)
// 将claims存入metadata
claimsJSON, _ := json.Marshal(claims)
ctx = metadata.AppendToClientContext(ctx,
mdKeyUserID, strconv.FormatUint(uint64(claims.BaseClaims.ID), 10),
mdKeyUsername, claims.Username,
mdKeyAuthorityID, strconv.FormatUint(uint64(claims.AuthorityID), 10),
mdKeyUUID, claims.UUID,
mdKeyClaims, string(claimsJSON),
)
// 检查是否需要刷新token
if cfg.JWT.NeedRefresh(claims) {
@ -127,30 +143,46 @@ func setToken(ht http.Transporter, token string, maxAge int) {
// GetClaims 从context获取claims
func GetClaims(ctx context.Context) (*pkgjwt.CustomClaims, bool) {
claims, ok := ctx.Value(authKey{}).(*pkgjwt.CustomClaims)
return claims, ok
if md, ok := metadata.FromServerContext(ctx); ok {
claimsStr := md.Get(mdKeyClaims)
if claimsStr != "" {
var claims pkgjwt.CustomClaims
if err := json.Unmarshal([]byte(claimsStr), &claims); err == nil {
return &claims, true
}
}
}
return nil, false
}
// GetUserID 从context获取用户ID
func GetUserID(ctx context.Context) uint {
if claims, ok := GetClaims(ctx); ok {
return claims.BaseClaims.ID
if md, ok := metadata.FromServerContext(ctx); ok {
if idStr := md.Get(mdKeyUserID); idStr != "" {
if id, err := strconv.ParseUint(idStr, 10, 64); err == nil {
return uint(id)
}
}
}
return 0
}
// GetUsername 从context获取用户名
func GetUsername(ctx context.Context) string {
if claims, ok := GetClaims(ctx); ok {
return claims.Username
if md, ok := metadata.FromServerContext(ctx); ok {
return md.Get(mdKeyUsername)
}
return ""
}
// GetAuthorityID 从context获取角色ID
func GetAuthorityID(ctx context.Context) uint {
if claims, ok := GetClaims(ctx); ok {
return claims.AuthorityID
if md, ok := metadata.FromServerContext(ctx); ok {
if idStr := md.Get(mdKeyAuthorityID); idStr != "" {
if id, err := strconv.ParseUint(idStr, 10, 64); err == nil {
return uint(id)
}
}
}
return 0
}

View File

@ -1,46 +0,0 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment include:
- Using welcoming and inclusive language
- Being respectful of differing viewpoints and experiences
- Gracefully accepting constructive criticism
- Focusing on what is best for the community
- Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
- The use of sexualized language or imagery and unwelcome sexual attention or advances
- Trolling, insulting/derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or electronic address, without explicit permission
- Other conduct which could reasonably be considered inappropriate in a professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at afc163@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/

View File

@ -24,6 +24,12 @@ export default defineConfig({
*/
hash: true,
/**
* @name esbuild minify IIFE
* @description esbuild helpers
*/
esbuildMinifyIIFE: true,
publicPath: PUBLIC_PATH,
/**
@ -83,7 +89,7 @@ export default defineConfig({
* @name layout
* @doc https://umijs.org/docs/max/layout-menu
*/
title: "Ant Design Pro",
title: "KRA Admin",
layout: {
locale: true,
...defaultSettings,

View File

@ -1,27 +1,47 @@
import type { ProLayoutProps } from '@ant-design/pro-components';
/**
* @name
* @name KRA
* @description GVA Ant Design Pro
*/
const Settings: ProLayoutProps & {
pwa?: boolean;
logo?: string;
showWatermark?: boolean;
} = {
navTheme: 'light',
// 拂晓蓝
colorPrimary: '#1890ff',
// 主题色 - 与 GVA 保持一致的蓝色
colorPrimary: '#3b82f6',
// 布局模式: side | top | mix
layout: 'mix',
contentWidth: 'Fluid',
fixedHeader: false,
// 固定头部
fixedHeader: true,
// 固定侧边栏
fixSiderbar: true,
// 色弱模式
colorWeak: false,
title: 'Ant Design Pro',
// 应用标题
title: 'KRA Admin',
pwa: true,
logo: 'https://gw.alipayobjects.com/zos/rmsportal/KDpgvguMpGfqaHPjicRK.svg',
// Logo
logo: '/logo.svg',
iconfontUrl: '',
// 显示水印
showWatermark: true,
// 侧边栏宽度配置
siderWidth: 256,
token: {
// 参见ts声明demo 见文档通过token 修改样式
//https://procomponents.ant.design/components/layout#%E9%80%9A%E8%BF%87-token-%E4%BF%AE%E6%94%B9%E6%A0%B7%E5%BC%8F
// 头部高度
header: {
heightLayoutHeader: 64,
},
// 侧边栏配置
sider: {
colorMenuBackground: '#fff',
colorTextMenuSelected: '#3b82f6',
colorBgMenuItemSelected: '#e6f4ff',
},
},
};

View File

@ -1 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200" version="1.1" viewBox="0 0 200 200"><title>Group 28 Copy 5</title><desc>Created with Sketch.</desc><defs><linearGradient id="linearGradient-1" x1="62.102%" x2="108.197%" y1="0%" y2="37.864%"><stop offset="0%" stop-color="#4285EB"/><stop offset="100%" stop-color="#2EC7FF"/></linearGradient><linearGradient id="linearGradient-2" x1="69.644%" x2="54.043%" y1="0%" y2="108.457%"><stop offset="0%" stop-color="#29CDFF"/><stop offset="37.86%" stop-color="#148EFF"/><stop offset="100%" stop-color="#0A60FF"/></linearGradient><linearGradient id="linearGradient-3" x1="69.691%" x2="16.723%" y1="-12.974%" y2="117.391%"><stop offset="0%" stop-color="#FA816E"/><stop offset="41.473%" stop-color="#F74A5C"/><stop offset="100%" stop-color="#F51D2C"/></linearGradient><linearGradient id="linearGradient-4" x1="68.128%" x2="30.44%" y1="-35.691%" y2="114.943%"><stop offset="0%" stop-color="#FA8E7D"/><stop offset="51.264%" stop-color="#F74A5C"/><stop offset="100%" stop-color="#F51D2C"/></linearGradient></defs><g id="Page-1" fill="none" fill-rule="evenodd" stroke="none" stroke-width="1"><g id="logo" transform="translate(-20.000000, -20.000000)"><g id="Group-28-Copy-5" transform="translate(20.000000, 20.000000)"><g id="Group-27-Copy-3"><g id="Group-25" fill-rule="nonzero"><g id="2"><path id="Shape" fill="url(#linearGradient-1)" d="M91.5880863,4.17652823 L4.17996544,91.5127728 C-0.519240605,96.2081146 -0.519240605,103.791885 4.17996544,108.487227 L91.5880863,195.823472 C96.2872923,200.518814 103.877304,200.518814 108.57651,195.823472 L145.225487,159.204632 C149.433969,154.999611 149.433969,148.181924 145.225487,143.976903 C141.017005,139.771881 134.193707,139.771881 129.985225,143.976903 L102.20193,171.737352 C101.032305,172.906015 99.2571609,172.906015 98.0875359,171.737352 L28.285908,101.993122 C27.1162831,100.824459 27.1162831,99.050775 28.285908,97.8821118 L98.0875359,28.1378823 C99.2571609,26.9692191 101.032305,26.9692191 102.20193,28.1378823 L129.985225,55.8983314 C134.193707,60.1033528 141.017005,60.1033528 145.225487,55.8983314 C149.433969,51.69331 149.433969,44.8756232 145.225487,40.6706018 L108.58055,4.05574592 C103.862049,-0.537986846 96.2692618,-0.500797906 91.5880863,4.17652823 Z"/><path id="Shape" fill="url(#linearGradient-2)" d="M91.5880863,4.17652823 L4.17996544,91.5127728 C-0.519240605,96.2081146 -0.519240605,103.791885 4.17996544,108.487227 L91.5880863,195.823472 C96.2872923,200.518814 103.877304,200.518814 108.57651,195.823472 L145.225487,159.204632 C149.433969,154.999611 149.433969,148.181924 145.225487,143.976903 C141.017005,139.771881 134.193707,139.771881 129.985225,143.976903 L102.20193,171.737352 C101.032305,172.906015 99.2571609,172.906015 98.0875359,171.737352 L28.285908,101.993122 C27.1162831,100.824459 27.1162831,99.050775 28.285908,97.8821118 L98.0875359,28.1378823 C100.999864,25.6271836 105.751642,20.541824 112.729652,19.3524487 C117.915585,18.4685261 123.585219,20.4140239 129.738554,25.1889424 C125.624663,21.0784292 118.571995,14.0340304 108.58055,4.05574592 C103.862049,-0.537986846 96.2692618,-0.500797906 91.5880863,4.17652823 Z"/></g><path id="Shape" fill="url(#linearGradient-3)" d="M153.685633,135.854579 C157.894115,140.0596 164.717412,140.0596 168.925894,135.854579 L195.959977,108.842726 C200.659183,104.147384 200.659183,96.5636133 195.960527,91.8688194 L168.690777,64.7181159 C164.472332,60.5180858 157.646868,60.5241425 153.435895,64.7316526 C149.227413,68.936674 149.227413,75.7543607 153.435895,79.9593821 L171.854035,98.3623765 C173.02366,99.5310396 173.02366,101.304724 171.854035,102.473387 L153.685633,120.626849 C149.47715,124.83187 149.47715,131.649557 153.685633,135.854579 Z"/></g><ellipse id="Combined-Shape" cx="100.519" cy="100.437" fill="url(#linearGradient-4)" rx="23.6" ry="23.581"/></g></g></g></g></svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
<rect width="64" height="64" rx="12" fill="#3b82f6"/>
<text x="50%" y="55%" dominant-baseline="middle" text-anchor="middle" fill="white" font-family="Arial, sans-serif" font-size="28" font-weight="bold">K</text>
</svg>

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 296 B

View File

@ -1,11 +1,42 @@
/**
* - GVA的权限验证逻辑
* @see https://umijs.org/docs/max/access#access
* */
*/
export default function access(
initialState: { currentUser?: API.CurrentUser } | undefined,
initialState: { currentUser?: API.UserInfo } | undefined,
) {
const { currentUser } = initialState ?? {};
// 获取当前用户的角色ID
const authorityId = currentUser?.authorityId || currentUser?.authority?.authorityId;
// 超级管理员角色ID与GVA保持一致888为超级管理员
const isSuperAdmin = authorityId === 888;
// 普通管理员
const isAdmin = !!currentUser && !!authorityId;
return {
canAdmin: currentUser && currentUser.access === 'admin',
// 是否为超级管理员
canSuperAdmin: isSuperAdmin,
// 是否为管理员(已登录且有角色)
canAdmin: isAdmin,
// 是否已登录
isLogin: !!currentUser,
// 按钮权限检查函数 - 参考GVA的btnAuth
canAccess: (btnKey: string) => {
// 超级管理员拥有所有权限
if (isSuperAdmin) return true;
// 检查当前路由的按钮权限
const btns = (currentUser as any)?.btns || {};
return !!btns[btnKey];
},
};
}
/**
* Hook - GVA的useBtnAuth
* 在组件中使用: const { canAccess } = useAccess();
* 然后: canAccess('add')
*/

View File

@ -2,7 +2,6 @@ import {
AvatarDropdown,
AvatarName,
Footer,
Question,
SelectLang,
} from "@/components";
import { getUserInfo } from "@/services/system/user";
@ -18,38 +17,42 @@ const isDev = process.env.NODE_ENV === "development";
const isDevOrTest = isDev || process.env.CI;
const loginPath = "/user/login";
/**
* - GVA的GetUserInfo
*/
const fetchUserInfo = async (): Promise<API.UserInfo | undefined> => {
try {
const token = localStorage.getItem('token');
if (!token) {
return undefined;
}
const res = await getUserInfo();
// 与GVA一致code === 0 表示成功
if (res.code === 0 && res.data?.userInfo) {
// 同步更新localStorage中的userInfo - 与GVA一致
localStorage.setItem('userInfo', JSON.stringify(res.data.userInfo));
return res.data.userInfo;
}
return undefined;
} catch (error) {
// 错误已在requestErrorConfig中处理这里不需要额外处理
// 401错误会弹出Modal用户点击确定后跳转登录页
return undefined;
}
};
/**
* @see https://umijs.org/docs/api/runtime-config#getinitialstate
* */
*/
export async function getInitialState(): Promise<{
settings?: Partial<LayoutSettings>;
settings?: Partial<LayoutSettings> & { showWatermark?: boolean };
currentUser?: API.UserInfo;
loading?: boolean;
fetchUserInfo?: () => Promise<API.UserInfo | undefined>;
}> {
const fetchUserInfo = async () => {
try {
const token = localStorage.getItem('token');
if (!token) {
return undefined;
}
const res = await getUserInfo();
if (res.code === 0 && res.data?.userInfo) {
return res.data.userInfo;
}
return undefined;
} catch (error) {
history.push(loginPath);
}
return undefined;
};
// 如果不是登录页面,执行
// 如果不是登录页面,获取用户信息 - 与GVA的permission.js逻辑一致
const { location } = history;
if (
![loginPath, "/user/register", "/user/register-result"].includes(
location.pathname
)
) {
if (![loginPath, "/user/register", "/user/register-result"].includes(location.pathname)) {
const currentUser = await fetchUserInfo();
return {
fetchUserInfo,
@ -62,98 +65,3 @@ export async function getInitialState(): Promise<{
settings: defaultSettings as Partial<LayoutSettings>,
};
}
// ProLayout 支持的api https://procomponents.ant.design/components/layout
export const layout: RunTimeLayoutConfig = ({
initialState,
setInitialState,
}) => {
return {
actionsRender: () => [
<Question key="doc" />,
<SelectLang key="SelectLang" />,
],
avatarProps: {
src: initialState?.currentUser?.avatar,
title: <AvatarName />,
render: (_, avatarChildren) => (
<AvatarDropdown>{avatarChildren}</AvatarDropdown>
),
},
waterMarkProps: {
content: initialState?.currentUser?.name,
},
footerRender: () => <Footer />,
onPageChange: () => {
const { location } = history;
// 如果没有登录,重定向到 login
if (!initialState?.currentUser && location.pathname !== loginPath) {
history.push(loginPath);
}
},
bgLayoutImgList: [
{
src: "https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/D2LWSqNny4sAAAAAAAAAAAAAFl94AQBr",
left: 85,
bottom: 100,
height: "303px",
},
{
src: "https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/C2TWRpJpiC0AAAAAAAAAAAAAFl94AQBr",
bottom: -68,
right: -45,
height: "303px",
},
{
src: "https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/F6vSTbj8KpYAAAAAAAAAAAAAFl94AQBr",
bottom: 0,
left: 0,
width: "331px",
},
],
links: isDevOrTest
? [
<Link key="openapi" to="/umi/plugin/openapi" target="_blank">
<LinkOutlined />
<span>OpenAPI </span>
</Link>,
]
: [],
menuHeaderRender: undefined,
// 自定义 403 页面
// unAccessible: <div>unAccessible</div>,
// 增加一个 loading 的状态
childrenRender: (children) => {
// if (initialState?.loading) return <PageLoading />;
return (
<>
{children}
{isDevOrTest && (
<SettingDrawer
disableUrlParams
enableDarkTheme
settings={initialState?.settings}
onSettingChange={(settings) => {
setInitialState((preInitialState) => ({
...preInitialState,
settings,
}));
}}
/>
)}
</>
);
},
...initialState?.settings,
};
};
/**
* @name request
* axios ahooks useRequest
* @doc https://umijs.org/docs/max/request#配置
*/
export const request: RequestConfig = {
baseURL: isDev ? "" : "https://proapi.azurewebsites.net",
...errorConfig,
};

View File

@ -3,29 +3,31 @@ import { DefaultFooter } from '@ant-design/pro-components';
import React from 'react';
const Footer: React.FC = () => {
const currentYear = new Date().getFullYear();
return (
<DefaultFooter
style={{
background: 'none',
}}
copyright="Powered by Ant Desgin"
copyright={`${currentYear} KRA Admin - Kratos React Admin`}
links={[
{
key: 'Ant Design Pro',
title: 'Ant Design Pro',
href: 'https://pro.ant.design',
key: 'KRA Admin',
title: 'KRA Admin',
href: 'https://github.com/your-org/kra',
blankTarget: true,
},
{
key: 'github',
title: <GithubOutlined />,
href: 'https://github.com/ant-design/ant-design-pro',
href: 'https://github.com/your-org/kra',
blankTarget: true,
},
{
key: 'Ant Design',
title: 'Ant Design',
href: 'https://ant.design',
key: 'Kratos',
title: 'Powered by Kratos',
href: 'https://go-kratos.dev',
blankTarget: true,
},
]}

View File

@ -1,15 +1,16 @@
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 React, { useMemo } from "react";
import { flushSync } from "react-dom";
import HeaderDropdown from "../HeaderDropdown";
import { setUserAuthority } from "@/services/system/user";
export type GlobalHeaderRightProps = {
menu?: boolean;
@ -41,14 +42,13 @@ const useStyles = createStyles(({ token }) => {
});
export const AvatarDropdown: React.FC<GlobalHeaderRightProps> = ({
menu,
children,
}) => {
/**
* 退 url
*/
const { styles } = useStyles();
const { initialState, setInitialState } = useModel("@@initialState");
// 退出登录
const loginOut = async () => {
// 清除本地存储的token和用户信息
localStorage.removeItem('token');
localStorage.removeItem('userInfo');
const { search, pathname } = window.location;
@ -56,9 +56,7 @@ export const AvatarDropdown: React.FC<GlobalHeaderRightProps> = ({
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",
@ -66,9 +64,21 @@ export const AvatarDropdown: React.FC<GlobalHeaderRightProps> = ({
});
}
};
const { styles } = useStyles();
const { initialState, setInitialState } = useModel("@@initialState");
// 切换角色
const changeUserAuth = async (authorityId: number) => {
try {
const res = await setUserAuthority({ 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;
@ -83,18 +93,17 @@ export const AvatarDropdown: React.FC<GlobalHeaderRightProps> = ({
history.push('/person');
return;
}
history.push(`/account/${key}`);
// 处理角色切换
if (key.startsWith('authority_')) {
const authorityId = parseInt(key.replace('authority_', ''), 10);
changeUserAuth(authorityId);
return;
}
};
const loading = (
<span className={styles.action}>
<Spin
size="small"
style={{
marginLeft: 8,
marginRight: 8,
}}
/>
<Spin size="small" style={{ marginLeft: 8, marginRight: 8 }} />
</span>
);
@ -108,30 +117,57 @@ export const AvatarDropdown: React.FC<GlobalHeaderRightProps> = ({
return loading;
}
const menuItems = [
...(menu
? [
{
key: "center",
icon: <UserOutlined />,
label: "个人中心",
},
{
key: "settings",
icon: <SettingOutlined />,
label: "个人设置",
},
{
type: "divider" as const,
},
]
: []),
{
key: "logout",
// 构建菜单项 - 参考GVA的角色切换功能
const menuItems = useMemo(() => {
const items: MenuProps['items'] = [];
// 当前角色显示
if (currentUser.authority?.authorityName) {
items.push({
key: 'currentRole',
label: <span style={{ fontWeight: 'bold' }}>{currentUser.authority.authorityName}</span>,
disabled: true,
});
}
// 可切换的其他角色
if (currentUser.authorities && currentUser.authorities.length > 1) {
const otherAuthorities = currentUser.authorities.filter(
(auth: any) => auth.authorityId !== currentUser.authorityId
);
if (otherAuthorities.length > 0) {
items.push({ type: 'divider' });
otherAuthorities.forEach((auth: any) => {
items.push({
key: `authority_${auth.authorityId}`,
icon: <SwapOutlined />,
label: `切换为:${auth.authorityName}`,
});
});
}
}
items.push({ type: 'divider' });
// 个人信息
items.push({
key: 'center',
icon: <UserOutlined />,
label: '个人信息',
});
items.push({ type: 'divider' });
// 退出登录
items.push({
key: 'logout',
icon: <LogoutOutlined />,
label: "退出登录",
},
];
label: '退出登录',
});
return items;
}, [currentUser]);
return (
<HeaderDropdown

View File

@ -6,7 +6,8 @@
*
*/
import Footer from './Footer';
import Logo from './Logo';
import { Question, SelectLang } from './RightContent';
import { AvatarDropdown, AvatarName } from './RightContent/AvatarDropdown';
export { AvatarDropdown, AvatarName, Footer, Question, SelectLang };
export { AvatarDropdown, AvatarName, Footer, Logo, Question, SelectLang };

View File

@ -1,72 +1,165 @@
import type { RequestOptions } from "@@/plugin-request/request";
/**
* - GVA的request.js实现
*
*/
import type { RequestOptions } from "@@/plugin-request/request";
import type { RequestConfig } from "@umijs/max";
import { message as toast } from "antd";
import { message, Modal } from "antd";
import { history } from "@umijs/max";
// Define the structure of the expected response.
interface ResponseStructure {
code: number;
reason?: string;
message?: string;
msg?: string;
metadata?: Map<string, number>;
}
const loginPath = "/user/login";
// Request configuration with error handling and interceptors.
// 获取错误消息 - 与GVA的getErrorMessage一致
function getErrorMessage(error: any): string {
return error.response?.data?.msg || error.response?.statusText || '请求失败';
}
// 清除存储并跳转登录 - 与GVA的ClearStorage一致
function clearStorageAndRedirect() {
localStorage.removeItem('token');
localStorage.removeItem('userInfo');
localStorage.removeItem('originSetting');
sessionStorage.clear();
history.push({ pathname: loginPath });
}
// 预设错误信息 - 与GVA的presetErrors一致
const presetErrors: Record<string | number, { title: string; tips: string }> = {
500: {
title: '服务器发生内部错误',
tips: '此类错误内容常见于后台panic请先查看后台日志',
},
404: {
title: '资源未找到',
tips: '此类错误多为接口未注册或请求路径与api路径不符',
},
401: {
title: '身份认证失败',
tips: '您的身份认证已过期或无效,请重新登录',
},
network: {
title: '网络错误',
tips: '无法连接到服务器,请检查您的网络连接',
},
};
// 显示错误弹窗 - 与GVA的ErrorPreview组件逻辑一致
function showErrorModal(code: string | number, errorMessage: string, onConfirm?: () => void) {
const preset = presetErrors[code] || { title: '请求错误', tips: '请检查控制台获取更多信息' };
Modal.error({
title: preset.title,
content: (
<div>
<p style={{ marginBottom: 8 }}>{errorMessage}</p>
<p style={{ color: '#666', fontSize: 12 }}>{preset.tips}</p>
</div>
),
okText: '确定',
onOk: () => {
onConfirm?.();
},
});
}
// 请求配置
export const errorConfig: RequestConfig = {
timeout: 99999, // 超时时间 - 与GVA保持一致
errorConfig: {
// 错误抛出器 - 与GVA一致code !== 0 时抛出错误
errorThrower: (res) => {
const { code, reason, message, msg } = res as unknown as ResponseStructure;
if (code !== 0 && code !== 200) {
const error: any = new Error(message || msg);
error.info = { reason, message: message || msg };
const { code, msg, message: errorMsg } = res as any;
if (code !== 0) {
const error: any = new Error(msg || errorMsg || '请求失败');
error.info = { code, message: msg || errorMsg };
throw error;
}
},
// 错误处理器 - 与GVA的response error拦截器一致
errorHandler: (error: any, opts: any) => {
if (opts?.skipErrorHandler) throw error;
if (error.response?.status === 401) {
localStorage.removeItem('token');
localStorage.removeItem('userInfo');
history.push(loginPath);
// 网络错误 - 与GVA一致
if (!error.response) {
showErrorModal('network', getErrorMessage(error));
return;
}
if (error.response?.data) {
const errorInfo: ResponseStructure | undefined = error.response?.data;
if (errorInfo) {
const { message, msg, reason } = errorInfo;
toast.error((reason ? reason + ": " : "") + (message || msg || "请求失败"));
}
} else if (error.response) {
toast.error(`Response status: ${error.response.status}`);
} else if (error.request) {
toast.error("None response! Please retry.");
} else {
toast.error("Request error, please retry.");
const status = error.response?.status;
const errorMsg = getErrorMessage(error);
// 401 身份认证失败 - 与GVA一致点击确定后清除存储并跳转
if (status === 401) {
showErrorModal(401, errorMsg, () => {
clearStorageAndRedirect();
});
return;
}
// 其他HTTP错误 - 与GVA一致
showErrorModal(status, errorMsg);
},
},
// 请求拦截器 - 与GVA的request拦截器一致
requestInterceptors: [
(config: RequestOptions) => {
// 添加token到请求头
const token = localStorage.getItem('token');
if (token) {
config.headers = {
...config.headers,
'x-token': token,
};
const token = localStorage.getItem('token') || '';
let userId = '';
try {
const userInfo = localStorage.getItem('userInfo');
if (userInfo) {
const user = JSON.parse(userInfo);
userId = String(user.ID || user.id || '');
}
} catch (e) {
// ignore parse error
}
// 设置请求头 - 与GVA完全一致
config.headers = {
'Content-Type': 'application/json',
'x-token': token,
'x-user-id': userId,
...config.headers,
};
return config;
},
],
// 响应拦截器 - 与GVA的response拦截器一致
responseInterceptors: [
(response) => {
if (response.status !== 200) {
toast.error(`Request error: ${response.status}`);
(response: any) => {
// 处理新token - 与GVA一致
const newToken = response.headers?.['new-token'];
if (newToken) {
localStorage.setItem('token', newToken);
}
return response;
const { data } = response;
// 如果没有code字段直接返回 - 与GVA一致
if (typeof data?.code === 'undefined') {
return response;
}
// code === 0 或 headers.success === 'true' 表示成功 - 与GVA完全一致
if (data.code === 0 || response.headers?.success === 'true') {
// 处理header中的msg - 与GVA一致
if (response.headers?.msg) {
data.msg = decodeURI(response.headers.msg);
}
// 返回 response.data 而不是整个 response - 与GVA一致
return { ...response, data };
}
// 业务错误,显示错误消息 - 与GVA一致
message.error(data.msg || decodeURI(response.headers?.msg || '') || '请求失败');
return { ...response, data };
},
],
};

View File

@ -44,7 +44,7 @@ export async function getUserList(data: API.PageParams) {
}
/** 设置用户权限 POST /user/setUserAuthority */
export async function setUserAuthority(data: { uuid: string; authorityId: number }) {
export async function setUserAuthority(data: { authorityId: number }) {
return request<API.BaseResult>('/api/user/setUserAuthority', {
method: 'POST',
data,