diff --git a/internal/server/middleware/jwt.go b/internal/server/middleware/jwt.go
index c00b40a..8625298 100644
--- a/internal/server/middleware/jwt.go
+++ b/internal/server/middleware/jwt.go
@@ -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
}
diff --git a/web/CODE_OF_CONDUCT.md b/web/CODE_OF_CONDUCT.md
deleted file mode 100644
index 2b4571c..0000000
--- a/web/CODE_OF_CONDUCT.md
+++ /dev/null
@@ -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/
diff --git a/web/config/config.ts b/web/config/config.ts
index 40e29df..417cfa6 100644
--- a/web/config/config.ts
+++ b/web/config/config.ts
@@ -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,
diff --git a/web/config/defaultSettings.ts b/web/config/defaultSettings.ts
index 6710cad..b079a83 100644
--- a/web/config/defaultSettings.ts
+++ b/web/config/defaultSettings.ts
@@ -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',
+ },
},
};
diff --git a/web/public/logo.svg b/web/public/logo.svg
index 239bf69..52aafa5 100644
--- a/web/public/logo.svg
+++ b/web/public/logo.svg
@@ -1 +1,4 @@
-
\ No newline at end of file
+
diff --git a/web/src/access.ts b/web/src/access.ts
index 373d9fa..3fb22f0 100644
--- a/web/src/access.ts
+++ b/web/src/access.ts
@@ -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') 检查是否有添加权限
+ */
diff --git a/web/src/app.tsx b/web/src/app.tsx
index 759c004..3031e42 100644
--- a/web/src/app.tsx
+++ b/web/src/app.tsx
@@ -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 => {
+ 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;
+ settings?: Partial & { showWatermark?: boolean };
currentUser?: API.UserInfo;
loading?: boolean;
fetchUserInfo?: () => Promise;
}> {
- 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,
};
}
-
-// ProLayout 支持的api https://procomponents.ant.design/components/layout
-export const layout: RunTimeLayoutConfig = ({
- initialState,
- setInitialState,
-}) => {
- return {
- actionsRender: () => [
- ,
- ,
- ],
- avatarProps: {
- src: initialState?.currentUser?.avatar,
- title: ,
- render: (_, avatarChildren) => (
- {avatarChildren}
- ),
- },
- waterMarkProps: {
- content: initialState?.currentUser?.name,
- },
- footerRender: () => ,
- 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
- ? [
-
-
- OpenAPI 文档
- ,
- ]
- : [],
- menuHeaderRender: undefined,
- // 自定义 403 页面
- // unAccessible: unAccessible
,
- // 增加一个 loading 的状态
- childrenRender: (children) => {
- // if (initialState?.loading) return ;
- return (
- <>
- {children}
- {isDevOrTest && (
- {
- 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,
-};
diff --git a/web/src/components/Footer/index.tsx b/web/src/components/Footer/index.tsx
index 64359c6..389a8fc 100644
--- a/web/src/components/Footer/index.tsx
+++ b/web/src/components/Footer/index.tsx
@@ -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 (
,
- 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,
},
]}
diff --git a/web/src/components/RightContent/AvatarDropdown.tsx b/web/src/components/RightContent/AvatarDropdown.tsx
index 6ef7fc1..2205c52 100644
--- a/web/src/components/RightContent/AvatarDropdown.tsx
+++ b/web/src/components/RightContent/AvatarDropdown.tsx
@@ -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 = ({
- 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 = ({
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 = ({
});
}
};
- 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 = ({
history.push('/person');
return;
}
- history.push(`/account/${key}`);
+ // 处理角色切换
+ if (key.startsWith('authority_')) {
+ const authorityId = parseInt(key.replace('authority_', ''), 10);
+ changeUserAuth(authorityId);
+ return;
+ }
};
const loading = (
-
+
);
@@ -108,30 +117,57 @@ export const AvatarDropdown: React.FC = ({
return loading;
}
- const menuItems = [
- ...(menu
- ? [
- {
- key: "center",
- icon: ,
- label: "个人中心",
- },
- {
- key: "settings",
- icon: ,
- label: "个人设置",
- },
- {
- type: "divider" as const,
- },
- ]
- : []),
- {
- key: "logout",
+ // 构建菜单项 - 参考GVA的角色切换功能
+ const menuItems = useMemo(() => {
+ const items: MenuProps['items'] = [];
+
+ // 当前角色显示
+ if (currentUser.authority?.authorityName) {
+ items.push({
+ key: 'currentRole',
+ label: 当前角色:{currentUser.authority.authorityName},
+ 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: ,
+ label: `切换为:${auth.authorityName}`,
+ });
+ });
+ }
+ }
+
+ items.push({ type: 'divider' });
+
+ // 个人信息
+ items.push({
+ key: 'center',
+ icon: ,
+ label: '个人信息',
+ });
+
+ items.push({ type: 'divider' });
+
+ // 退出登录
+ items.push({
+ key: 'logout',
icon: ,
- label: "退出登录",
- },
- ];
+ label: '退出登录',
+ });
+
+ return items;
+ }, [currentUser]);
return (
;
-}
-
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 = {
+ 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: (
+
+
{errorMessage}
+
{preset.tips}
+
+ ),
+ 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 };
},
],
};
diff --git a/web/src/services/system/user.ts b/web/src/services/system/user.ts
index d639436..bb41b52 100644
--- a/web/src/services/system/user.ts
+++ b/web/src/services/system/user.ts
@@ -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/user/setUserAuthority', {
method: 'POST',
data,