fix: 优化登录相关权限问题 添加按键权限判断
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
import type { AuthData } from './menu';
|
||||
|
||||
/** Promise 级缓存:并发调用共享同一个请求,失败时清除以便重试 */
|
||||
let authDataPromise: Promise<AuthData> | null = null;
|
||||
|
||||
export function getAuthDataPromise() {
|
||||
return authDataPromise;
|
||||
}
|
||||
|
||||
export function setAuthDataPromise(promise: Promise<AuthData> | null) {
|
||||
authDataPromise = promise;
|
||||
}
|
||||
|
||||
/** 清除权限数据缓存(退出登录时调用,确保下次登录不读到旧数据) */
|
||||
export function clearAuthDataCache() {
|
||||
authDataPromise = null;
|
||||
}
|
||||
+34
-23
@@ -1,15 +1,10 @@
|
||||
import { get } from '@/utils/request';
|
||||
import type { MenuNode, PermissionCode } from '@/types';
|
||||
import { FALLBACK_MENU_NODES } from '@/config/fallbackRoutes';
|
||||
import { getAuthDataPromise, setAuthDataPromise, clearAuthDataCache } from '@/api/authCache';
|
||||
import { forceReLogin } from '@/hooks/useAuth';
|
||||
|
||||
// ============================================================
|
||||
// 权限数据缓存(/op/permission 只请求一次)
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Promise 级缓存:并发调用共享同一个请求,失败时清除以便重试
|
||||
*/
|
||||
let authDataPromise: Promise<AuthData> | null = null;
|
||||
export { clearAuthDataCache } from '@/api/authCache';
|
||||
|
||||
/**
|
||||
* 权限数据(角色 + 权限码)
|
||||
@@ -19,30 +14,46 @@ export interface AuthData {
|
||||
permsList: string[];
|
||||
}
|
||||
|
||||
const AUTH_FETCH_FAIL_MSG = '账户权限获取失败,请重新登录';
|
||||
|
||||
function normalizeAuthData(res: any): AuthData {
|
||||
if (res?.code != null && res.code != 200) {
|
||||
throw new Error(res.msg || AUTH_FETCH_FAIL_MSG);
|
||||
}
|
||||
|
||||
const roleList = res?.data?.roleList || [];
|
||||
const permsList = res?.data?.permsList || [];
|
||||
|
||||
if (permsList.length === 0) {
|
||||
throw new Error(AUTH_FETCH_FAIL_MSG);
|
||||
}
|
||||
|
||||
return { roleList, permsList };
|
||||
}
|
||||
|
||||
function handleAuthFetchFailure() {
|
||||
clearAuthDataCache();
|
||||
forceReLogin(AUTH_FETCH_FAIL_MSG);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户的完整权限数据(角色列表 + 权限编码列表)
|
||||
* 接口: GET /op/permission
|
||||
* Promise 级缓存:并发调用共享同一个请求,失败时清除缓存以允许重试
|
||||
*/
|
||||
export function fetchAuthData(): Promise<AuthData> {
|
||||
if (authDataPromise) return authDataPromise;
|
||||
authDataPromise = get('/op/permission')
|
||||
.then((res: any) => ({
|
||||
roleList: res.data?.roleList || [],
|
||||
permsList: res.data?.permsList || [],
|
||||
}))
|
||||
const cached = getAuthDataPromise();
|
||||
if (cached) return cached;
|
||||
|
||||
const promise = get('/op/permission')
|
||||
.then((res) => normalizeAuthData(res))
|
||||
.catch((err) => {
|
||||
authDataPromise = null;
|
||||
handleAuthFetchFailure();
|
||||
throw err;
|
||||
});
|
||||
return authDataPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除权限数据缓存(退出登录时调用,确保下次登录不读到旧数据)
|
||||
*/
|
||||
export function clearAuthDataCache() {
|
||||
authDataPromise = null;
|
||||
setAuthDataPromise(promise);
|
||||
return promise;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -95,7 +106,7 @@ function filterMenuNodesByPermission(nodes: MenuNode[], permsSet: Set<string>):
|
||||
* 2. 用权限码过滤 FALLBACK_MENU_NODES
|
||||
* 3. 返回过滤后的菜单树
|
||||
*
|
||||
* 异常时抛出,由调用方(menuStore.loadMenu)降级使用未过滤的兜底路由
|
||||
* 权限获取失败时由 fetchAuthData 统一强制重新登录
|
||||
*/
|
||||
export function fetchMenuTree(): Promise<MenuNode[]> {
|
||||
return fetchAuthData().then(({ permsList }) => {
|
||||
|
||||
@@ -57,12 +57,12 @@ export function updateUser(params: UserUpdateParams): Promise<ApiResult<Record<s
|
||||
export function toggleUserActive(
|
||||
params: UserActiveParams,
|
||||
): Promise<ApiResult<Record<string, never>>> {
|
||||
return post('/sys/user/active', params);
|
||||
return post('/admin/sys/user/active', params);
|
||||
}
|
||||
|
||||
/** POST /sys/user/updatepwd — 修改密码(直接调接口) */
|
||||
export function updateUserPwd(
|
||||
params: UserUpdatePwdParams,
|
||||
): Promise<ApiResult<Record<string, never>>> {
|
||||
return post('/sys/user/updatepwd', params);
|
||||
return post('/admin/sys/user/updatepwd', params);
|
||||
}
|
||||
|
||||
+43
-12
@@ -1,14 +1,18 @@
|
||||
// src/hooks/useAuth.ts
|
||||
import { notification } from 'ant-design-vue';
|
||||
import { useState } from './useState';
|
||||
import { useEffect } from './useEffect';
|
||||
import { useMenuStore } from '@/stores/menuStore';
|
||||
import { usePermissionStore } from '@/stores/permissionStore';
|
||||
import { useTabsStore } from '@/stores/tabsStore';
|
||||
import { clearAuthDataCache } from '@/api/menu';
|
||||
import { useUserStore } from '@/stores/userStore';
|
||||
import { clearAuthDataCache } from '@/api/authCache';
|
||||
import router from '@/router';
|
||||
|
||||
const TOKEN_KEY = 'MY_APP_AUTH_TOKEN';
|
||||
|
||||
let isForceLoggingOut = false;
|
||||
|
||||
function createAuth() {
|
||||
const [token, setToken] = useState<string | null>(window.localStorage.getItem(TOKEN_KEY));
|
||||
|
||||
@@ -23,10 +27,26 @@ function createAuth() {
|
||||
|
||||
const isLoggedIn = () => !!token.value;
|
||||
|
||||
const clearSession = () => {
|
||||
setToken(null);
|
||||
|
||||
const { clearUser } = useUserStore();
|
||||
const { clearMenu } = useMenuStore();
|
||||
const { clearPermissions } = usePermissionStore();
|
||||
const { clearTabs } = useTabsStore();
|
||||
|
||||
clearUser();
|
||||
clearTabs();
|
||||
clearMenu();
|
||||
clearPermissions();
|
||||
clearAuthDataCache();
|
||||
};
|
||||
|
||||
/**
|
||||
* 登录:保存 token → 拉取菜单+权限 → 动态注册路由 → 跳转首页
|
||||
*/
|
||||
const login = async (newToken: string, targetPath?: string) => {
|
||||
isForceLoggingOut = false;
|
||||
setToken(newToken);
|
||||
|
||||
const { loadMenu, homePath, isRoutePathAvailable } = useMenuStore();
|
||||
@@ -35,6 +55,9 @@ function createAuth() {
|
||||
// 并行拉取菜单和权限
|
||||
await Promise.all([loadMenu(), loadPermissions()]);
|
||||
|
||||
// 权限获取失败时会清空会话,此时不再跳转首页
|
||||
if (!isLoggedIn()) return;
|
||||
|
||||
const nextPath =
|
||||
targetPath && isRoutePathAvailable(targetPath) ? targetPath : homePath.value || '/404';
|
||||
|
||||
@@ -46,17 +69,7 @@ function createAuth() {
|
||||
* 退出:清除 token → 清除菜单+权限 → 清除动态路由 → 跳转登录页
|
||||
*/
|
||||
const logout = () => {
|
||||
setToken(null);
|
||||
|
||||
const { clearMenu } = useMenuStore();
|
||||
const { clearPermissions } = usePermissionStore();
|
||||
const { clearTabs } = useTabsStore();
|
||||
|
||||
clearTabs();
|
||||
clearMenu();
|
||||
clearPermissions();
|
||||
clearAuthDataCache();
|
||||
|
||||
clearSession();
|
||||
router.push('/login');
|
||||
};
|
||||
|
||||
@@ -65,7 +78,25 @@ function createAuth() {
|
||||
isLoggedIn,
|
||||
login,
|
||||
logout,
|
||||
clearSession,
|
||||
};
|
||||
}
|
||||
|
||||
export const auth = createAuth();
|
||||
|
||||
/**
|
||||
* 强制重新登录:提示错误 → 清空会话 → 跳转登录页
|
||||
* 用于权限获取失败、登录态失效等需要彻底重置会话的场景
|
||||
*/
|
||||
export function forceReLogin(msg = '账户权限获取失败,请重新登录') {
|
||||
if (isForceLoggingOut) return;
|
||||
isForceLoggingOut = true;
|
||||
|
||||
notification.error({
|
||||
message: '提示',
|
||||
description: msg,
|
||||
duration: 2600,
|
||||
});
|
||||
auth.clearSession();
|
||||
router.push('/login');
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
} from 'ant-design-vue';
|
||||
import { useBannerModel, BANNER_STATUS_OPTIONS } from './model/useBannerModel';
|
||||
import { useContainerSize } from '@/hooks';
|
||||
import { usePermissionStore } from '@/stores/permissionStore';
|
||||
import BannerFormModal from './components/BannerFormModal';
|
||||
import pageStyles from '@/assets/styles/pageLayout.module.less';
|
||||
|
||||
@@ -28,6 +29,7 @@ function renderBodyCell({
|
||||
onEdit,
|
||||
onToggleStatus,
|
||||
onDelete,
|
||||
permissions,
|
||||
}: {
|
||||
column: any;
|
||||
text: any;
|
||||
@@ -37,6 +39,11 @@ function renderBodyCell({
|
||||
onEdit: (record: any) => void;
|
||||
onToggleStatus: (record: any) => void;
|
||||
onDelete: (record: any) => void;
|
||||
permissions: {
|
||||
canEdit: boolean;
|
||||
canToggleStatus: boolean;
|
||||
canDelete: boolean;
|
||||
};
|
||||
}) {
|
||||
// 图片列
|
||||
if (column.key === 'image') {
|
||||
@@ -92,17 +99,26 @@ function renderBodyCell({
|
||||
// 操作列
|
||||
if (column.key === 'action') {
|
||||
const isEnabled = record.status === '1';
|
||||
const { canEdit, canToggleStatus, canDelete } = permissions;
|
||||
if (!canEdit && !canToggleStatus && !canDelete) return <span>-</span>;
|
||||
|
||||
return (
|
||||
<Space>
|
||||
<Button type="link" size="small" onClick={() => onEdit(record)}>
|
||||
编辑
|
||||
</Button>
|
||||
<Button type="link" size="small" onClick={() => onToggleStatus(record)}>
|
||||
{isEnabled ? '禁用' : '启用'}
|
||||
</Button>
|
||||
<Button type="link" size="small" danger onClick={() => onDelete(record)}>
|
||||
删除
|
||||
</Button>
|
||||
{canEdit && (
|
||||
<Button type="link" size="small" onClick={() => onEdit(record)}>
|
||||
编辑
|
||||
</Button>
|
||||
)}
|
||||
{canToggleStatus && (
|
||||
<Button type="link" size="small" onClick={() => onToggleStatus(record)}>
|
||||
{isEnabled ? '禁用' : '启用'}
|
||||
</Button>
|
||||
)}
|
||||
{canDelete && (
|
||||
<Button type="link" size="small" danger onClick={() => onDelete(record)}>
|
||||
删除
|
||||
</Button>
|
||||
)}
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
@@ -136,6 +152,13 @@ export default defineComponent({
|
||||
} = useBannerModel();
|
||||
|
||||
const { containerRef, height } = useContainerSize();
|
||||
const { hasPermission } = usePermissionStore();
|
||||
const canAdd = hasPermission('events.banner.add');
|
||||
const operationPermissions = {
|
||||
canEdit: hasPermission('events.banner.edit'),
|
||||
canToggleStatus: hasPermission('events.banner.toggle_status'),
|
||||
canDelete: hasPermission('events.banner.delete'),
|
||||
};
|
||||
|
||||
const confirmDelete = (record: any) => {
|
||||
Modal.confirm({
|
||||
@@ -200,9 +223,11 @@ export default defineComponent({
|
||||
查询
|
||||
</Button>
|
||||
<Button onClick={handleReset}>重置</Button>
|
||||
<Button type="primary" onClick={handleAdd}>
|
||||
新增
|
||||
</Button>
|
||||
{canAdd && (
|
||||
<Button type="primary" onClick={handleAdd}>
|
||||
新增
|
||||
</Button>
|
||||
)}
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
@@ -226,6 +251,7 @@ export default defineComponent({
|
||||
onEdit: handleEdit,
|
||||
onToggleStatus: confirmToggleStatus,
|
||||
onDelete: confirmDelete,
|
||||
permissions: operationPermissions,
|
||||
}),
|
||||
}}
|
||||
</Table>
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
import { useEventListColumns } from './model/useEventListColumns';
|
||||
import { getProvinceCityCascaderOptions } from '@/utils/areaData';
|
||||
import { useContainerSize } from '@/hooks';
|
||||
import { usePermissionStore } from '@/stores/permissionStore';
|
||||
import EventRegulationModal from './components/EventRegulationModal';
|
||||
import EventDetailModal from './components/EventDetailModal';
|
||||
import EventAuditModal from './components/EventAuditModal';
|
||||
@@ -63,6 +64,9 @@ export default defineComponent({
|
||||
|
||||
const { columns } = useEventListColumns();
|
||||
const { containerRef, height } = useContainerSize();
|
||||
const { hasPermission } = usePermissionStore();
|
||||
const canToggleStatus = hasPermission('events.list.toggle_status');
|
||||
const canViewRegulation = hasPermission('events.regulation.view');
|
||||
|
||||
/** 省市两级 Cascader 数据(无区) */
|
||||
const provinceCityOptions = getProvinceCityCascaderOptions();
|
||||
@@ -91,14 +95,16 @@ export default defineComponent({
|
||||
<Button type="link" size="small" onClick={() => openDetailModal(record)}>
|
||||
查看
|
||||
</Button>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
loading={togglingId.value === record.id}
|
||||
onClick={() => handleToggleShelf(record)}
|
||||
>
|
||||
下架
|
||||
</Button>
|
||||
{canToggleStatus && (
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
loading={togglingId.value === record.id}
|
||||
onClick={() => handleToggleShelf(record)}
|
||||
>
|
||||
下架
|
||||
</Button>
|
||||
)}
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
@@ -109,14 +115,16 @@ export default defineComponent({
|
||||
<Button type="link" size="small" onClick={() => openDetailModal(record)}>
|
||||
查看
|
||||
</Button>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
loading={togglingId.value === record.id}
|
||||
onClick={() => handleToggleShelf(record)}
|
||||
>
|
||||
上架
|
||||
</Button>
|
||||
{canToggleStatus && (
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
loading={togglingId.value === record.id}
|
||||
onClick={() => handleToggleShelf(record)}
|
||||
>
|
||||
上架
|
||||
</Button>
|
||||
)}
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
@@ -145,9 +153,15 @@ export default defineComponent({
|
||||
width: 100,
|
||||
align: 'center' as const,
|
||||
customRender: ({ record }: { record: any }) => (
|
||||
<a style={{ cursor: 'pointer' }} onClick={() => openRegulationModal(record)}>
|
||||
查看
|
||||
</a>
|
||||
<>
|
||||
{canViewRegulation ? (
|
||||
<a style={{ cursor: 'pointer' }} onClick={() => openRegulationModal(record)}>
|
||||
查看
|
||||
</a>
|
||||
) : (
|
||||
<span>-</span>
|
||||
)}
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
|
||||
@@ -17,6 +17,7 @@ interface OrderDetailModalProps {
|
||||
uniqueId: string;
|
||||
onClose: () => void;
|
||||
onReRefund: () => void;
|
||||
canReRefund?: boolean;
|
||||
}
|
||||
|
||||
const GENDER_MAP: Record<string, string> = { '1': '男', '2': '女' };
|
||||
@@ -118,6 +119,7 @@ export default defineComponent({
|
||||
uniqueId: { type: String, default: '' },
|
||||
onClose: { type: Function, default: null },
|
||||
onReRefund: { type: Function, default: null },
|
||||
canReRefund: { type: Boolean, default: false },
|
||||
},
|
||||
setup(props: OrderDetailModalProps) {
|
||||
const [detail, setDetail] = useState<TournamentAdminOrderInfoVO | null>(null);
|
||||
@@ -360,7 +362,7 @@ export default defineComponent({
|
||||
<div class={styles.modalFooter}>
|
||||
<Button onClick={props.onClose}>取消</Button>
|
||||
{/* 只有存在退款失败记录时才显示重新退款按钮 */}
|
||||
{hasRefundFailed.value ? (
|
||||
{props.canReRefund && hasRefundFailed.value ? (
|
||||
<Button danger onClick={props.onReRefund}>
|
||||
重新退款
|
||||
</Button>
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
import { useOrderModel, ORDER_STATUS_OPTIONS, REFUND_STATUS_OPTIONS } from './model/useOrderModel';
|
||||
import { retryRefundOrder } from './model/services';
|
||||
import { useContainerSize, useState } from '@/hooks';
|
||||
import { usePermissionStore } from '@/stores/permissionStore';
|
||||
import OrderDetailModal from './components/OrderDetailModal';
|
||||
import pageStyles from '@/assets/styles/pageLayout.module.less';
|
||||
|
||||
@@ -58,12 +59,14 @@ function renderBodyCell({
|
||||
record,
|
||||
onView,
|
||||
onReRefund,
|
||||
canReRefund,
|
||||
}: {
|
||||
column: any;
|
||||
text: any;
|
||||
record: any;
|
||||
onView: (record: any) => void;
|
||||
onReRefund: (record: any) => void;
|
||||
canReRefund: boolean;
|
||||
}) {
|
||||
// 赛事名称:最多15字符,超出显示 Tooltip
|
||||
if (column.dataIndex === 'name') {
|
||||
@@ -82,7 +85,7 @@ function renderBodyCell({
|
||||
if (column.key === 'action') {
|
||||
// 退款状态:1=无退款,2=退款成功,3=退款失败,-1=其他
|
||||
// 只有退款失败(3)时显示重新退款按钮
|
||||
const showReRefund = record.refundStatus === '3';
|
||||
const showReRefund = canReRefund && record.refundStatus === '3';
|
||||
return (
|
||||
<Space>
|
||||
<Button type="link" size="small" onClick={() => onView(record)}>
|
||||
@@ -119,6 +122,8 @@ export default defineComponent({
|
||||
} = useOrderModel();
|
||||
|
||||
const { containerRef, height } = useContainerSize();
|
||||
const { hasPermission } = usePermissionStore();
|
||||
const canReRefund = hasPermission('events.orders.refund');
|
||||
|
||||
// 详情弹窗状态
|
||||
const [detailVisible, setDetailVisible] = useState<boolean>(false);
|
||||
@@ -257,6 +262,7 @@ export default defineComponent({
|
||||
...args,
|
||||
onView: handleView,
|
||||
onReRefund: handleReRefund,
|
||||
canReRefund,
|
||||
}),
|
||||
}}
|
||||
</Table>
|
||||
@@ -286,6 +292,7 @@ export default defineComponent({
|
||||
setDetailVisible(false);
|
||||
promptReRefund(currentRecord.value);
|
||||
}}
|
||||
canReRefund={canReRefund}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Button, Input, Table, Form, Space, Select, Pagination } from 'ant-desig
|
||||
import { useUserModel, USER_STATUS_OPTIONS } from './model/useUserModel';
|
||||
import { useUserColumns } from './model/useUserColumns';
|
||||
import { useContainerSize } from '@/hooks';
|
||||
import { usePermissionStore } from '@/stores/permissionStore';
|
||||
import pageStyles from '@/assets/styles/pageLayout.module.less';
|
||||
|
||||
/**
|
||||
@@ -30,9 +31,13 @@ export default defineComponent({
|
||||
|
||||
const { columns } = useUserColumns();
|
||||
const { containerRef, height } = useContainerSize();
|
||||
const { hasPermission } = usePermissionStore();
|
||||
const canToggleStatus = hasPermission('events.users.toggle_status');
|
||||
|
||||
/** 渲染操作列 */
|
||||
const renderAction = (record: any) => {
|
||||
if (!canToggleStatus) return <span>-</span>;
|
||||
|
||||
const isActive = record.status === '1';
|
||||
return (
|
||||
<Button
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Button, Input, Table, Form, Space, Pagination } from 'ant-design-vue';
|
||||
import { WalletOutlined, LockOutlined, RiseOutlined } from '@ant-design/icons-vue';
|
||||
import { useWalletModel } from './model/useWalletModel';
|
||||
import { useContainerSize } from '@/hooks';
|
||||
import { usePermissionStore } from '@/stores/permissionStore';
|
||||
import WalletDetailModal from './components/WalletDetailModal';
|
||||
import styles from './index.module.less';
|
||||
import pageStyles from '@/assets/styles/pageLayout.module.less';
|
||||
@@ -20,13 +21,17 @@ function renderBodyCell({
|
||||
column,
|
||||
record,
|
||||
onViewDetail,
|
||||
canViewDetail,
|
||||
}: {
|
||||
column: any;
|
||||
text: any;
|
||||
record: any;
|
||||
onViewDetail: (record: any) => void;
|
||||
canViewDetail: boolean;
|
||||
}) {
|
||||
if (column.key === 'action') {
|
||||
if (!canViewDetail) return <span>-</span>;
|
||||
|
||||
return (
|
||||
<Button type="link" size="small" onClick={() => onViewDetail(record)}>
|
||||
明细
|
||||
@@ -56,6 +61,8 @@ export default defineComponent({
|
||||
} = useWalletModel();
|
||||
|
||||
const { containerRef, height } = useContainerSize({ headerOffset: 55 });
|
||||
const { hasPermission } = usePermissionStore();
|
||||
const canViewDetail = hasPermission('finance.wallet.detail');
|
||||
|
||||
const tableColumns = [
|
||||
...columns,
|
||||
@@ -196,7 +203,11 @@ export default defineComponent({
|
||||
>
|
||||
{{
|
||||
bodyCell: (args: any) =>
|
||||
renderBodyCell({ ...args, onViewDetail: handleViewDetail }),
|
||||
renderBodyCell({
|
||||
...args,
|
||||
onViewDetail: handleViewDetail,
|
||||
canViewDetail,
|
||||
}),
|
||||
}}
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { defineComponent } from 'vue';
|
||||
import { Button, Input, Table, Form, Space, Pagination, Select } from 'ant-design-vue';
|
||||
import { useWithdrawModel, renderAuditStatus, renderPayStatus } from './model/useWithdrawModel';
|
||||
import { useContainerSize } from '@/hooks';
|
||||
import { usePermissionStore } from '@/stores/permissionStore';
|
||||
import WithdrawDetailModal from './components/WithdrawDetailModal';
|
||||
import pageStyles from '@/assets/styles/pageLayout.module.less';
|
||||
|
||||
@@ -13,12 +14,14 @@ function renderBodyCell({
|
||||
record,
|
||||
onAudit,
|
||||
onView,
|
||||
canAudit,
|
||||
}: {
|
||||
column: any;
|
||||
text: any;
|
||||
record: any;
|
||||
onAudit: (record: any) => void;
|
||||
onView: (record: any) => void;
|
||||
canAudit: boolean;
|
||||
}) {
|
||||
if (column.key === 'auditStatus') {
|
||||
return renderAuditStatus(record.auditStatus);
|
||||
@@ -28,7 +31,7 @@ function renderBodyCell({
|
||||
}
|
||||
if (column.key === 'action') {
|
||||
// auditStatus=0(待审核)→ 显示「审核」,其他 → 显示「查看」
|
||||
if (record.auditStatus === 0) {
|
||||
if (canAudit && record.auditStatus === 0) {
|
||||
return (
|
||||
<Button type="link" size="small" onClick={() => onAudit(record)}>
|
||||
审核
|
||||
@@ -69,6 +72,8 @@ export default defineComponent({
|
||||
} = useWithdrawModel();
|
||||
|
||||
const { containerRef, height } = useContainerSize({ headerOffset: 55 });
|
||||
const { hasPermission } = usePermissionStore();
|
||||
const canAudit = hasPermission('finance.withdraw.audit');
|
||||
|
||||
/** 最终表格列:模型列 + 操作 */
|
||||
const tableColumns = [
|
||||
@@ -162,6 +167,7 @@ export default defineComponent({
|
||||
...args,
|
||||
onAudit: handleAudit,
|
||||
onView: handleView,
|
||||
canAudit,
|
||||
}),
|
||||
}}
|
||||
</Table>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Button, Input, Table, Form, Space, Select, Modal, Pagination } from 'an
|
||||
import type { ModalProps } from 'ant-design-vue';
|
||||
import { useRoleModel } from './model/useRoleModel';
|
||||
import { useContainerSize } from '@/hooks';
|
||||
import { usePermissionStore } from '@/stores/permissionStore';
|
||||
import RoleFormModal from './components/RoleFormModal';
|
||||
import UserListModal from './components/UserListModal';
|
||||
import pageStyles from '@/assets/styles/pageLayout.module.less';
|
||||
@@ -16,6 +17,7 @@ function renderBodyCell({
|
||||
onViewUsers,
|
||||
onEdit,
|
||||
onDelete,
|
||||
permissions,
|
||||
}: {
|
||||
column: any;
|
||||
text: any;
|
||||
@@ -23,6 +25,10 @@ function renderBodyCell({
|
||||
onViewUsers: (record: any) => void;
|
||||
onEdit: (record: any) => void;
|
||||
onDelete: (record: any) => void;
|
||||
permissions: {
|
||||
canEdit: boolean;
|
||||
canDelete: boolean;
|
||||
};
|
||||
}) {
|
||||
if (column.dataIndex === 'userCount') {
|
||||
return (
|
||||
@@ -33,14 +39,21 @@ function renderBodyCell({
|
||||
}
|
||||
|
||||
if (column.key === 'action') {
|
||||
const { canEdit, canDelete } = permissions;
|
||||
if (!canEdit && !canDelete) return <span>-</span>;
|
||||
|
||||
return (
|
||||
<Space>
|
||||
<Button type="link" size="small" onClick={() => onEdit(record)}>
|
||||
编辑
|
||||
</Button>
|
||||
<Button type="link" size="small" danger onClick={() => onDelete(record)}>
|
||||
删除
|
||||
</Button>
|
||||
{canEdit && (
|
||||
<Button type="link" size="small" onClick={() => onEdit(record)}>
|
||||
编辑
|
||||
</Button>
|
||||
)}
|
||||
{canDelete && (
|
||||
<Button type="link" size="small" danger onClick={() => onDelete(record)}>
|
||||
删除
|
||||
</Button>
|
||||
)}
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
@@ -100,6 +113,12 @@ export default defineComponent({
|
||||
} = useRoleModel();
|
||||
|
||||
const { containerRef, height } = useContainerSize();
|
||||
const { hasPermission } = usePermissionStore();
|
||||
const canAdd = hasPermission('system.roles.add');
|
||||
const operationPermissions = {
|
||||
canEdit: hasPermission('system.roles.edit'),
|
||||
canDelete: hasPermission('system.roles.delete'),
|
||||
};
|
||||
|
||||
const triggerDelete = (record: any) => confirmDelete({ record, onOk: handleDelete });
|
||||
|
||||
@@ -135,9 +154,11 @@ export default defineComponent({
|
||||
<Button type="primary" onClick={handleSearch} loading={loading.value}>
|
||||
查询
|
||||
</Button>
|
||||
<Button type="primary" onClick={handleAdd}>
|
||||
新增角色
|
||||
</Button>
|
||||
{canAdd && (
|
||||
<Button type="primary" onClick={handleAdd}>
|
||||
新增角色
|
||||
</Button>
|
||||
)}
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
@@ -160,6 +181,7 @@ export default defineComponent({
|
||||
onViewUsers: handleViewUsers,
|
||||
onEdit: handleEdit,
|
||||
onDelete: triggerDelete,
|
||||
permissions: operationPermissions,
|
||||
}),
|
||||
}}
|
||||
</Table>
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
import { useUserModel, USER_STATUS_OPTIONS } from './model/useUserModel';
|
||||
import { toggleUserActive, saveUser, updateUser } from './model/services';
|
||||
import { useState, useThrottleFn, useContainerSize } from '@/hooks';
|
||||
import { usePermissionStore } from '@/stores/permissionStore';
|
||||
import UserFormModal from './components/UserFormModal';
|
||||
import pageStyles from '@/assets/styles/pageLayout.module.less';
|
||||
|
||||
@@ -24,24 +25,36 @@ function renderBodyCell({
|
||||
record,
|
||||
onEdit,
|
||||
onToggleStatus,
|
||||
permissions,
|
||||
}: {
|
||||
column: any;
|
||||
text: any;
|
||||
record: any;
|
||||
onEdit: (record: any) => void;
|
||||
onToggleStatus: (record: any) => void;
|
||||
permissions: {
|
||||
canEdit: boolean;
|
||||
canToggleStatus: boolean;
|
||||
};
|
||||
}) {
|
||||
// 操作列
|
||||
if (column.key === 'action') {
|
||||
const isActive = record.status === '1';
|
||||
const { canEdit, canToggleStatus } = permissions;
|
||||
if (!canEdit && !canToggleStatus) return <span>-</span>;
|
||||
|
||||
return (
|
||||
<Space>
|
||||
<Button type="link" size="small" onClick={() => onEdit(record)}>
|
||||
编辑
|
||||
</Button>
|
||||
<Button type="link" size="small" onClick={() => onToggleStatus(record)}>
|
||||
{isActive ? '停用' : '启用'}
|
||||
</Button>
|
||||
{canEdit && (
|
||||
<Button type="link" size="small" onClick={() => onEdit(record)}>
|
||||
编辑
|
||||
</Button>
|
||||
)}
|
||||
{canToggleStatus && (
|
||||
<Button type="link" size="small" onClick={() => onToggleStatus(record)}>
|
||||
{isActive ? '停用' : '启用'}
|
||||
</Button>
|
||||
)}
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
@@ -67,6 +80,12 @@ export default defineComponent({
|
||||
} = useUserModel();
|
||||
|
||||
const { containerRef, height } = useContainerSize();
|
||||
const { hasPermission } = usePermissionStore();
|
||||
const canAdd = hasPermission('system.users.add');
|
||||
const operationPermissions = {
|
||||
canEdit: hasPermission('system.users.edit'),
|
||||
canToggleStatus: hasPermission('system.users.toggle_status'),
|
||||
};
|
||||
|
||||
// ===== 弹窗状态(组件级) =====
|
||||
const [modalVisible, setModalVisible] = useState<boolean>(false);
|
||||
@@ -216,9 +235,11 @@ export default defineComponent({
|
||||
<Button type="primary" onClick={handleSearch} loading={loading.value}>
|
||||
查询
|
||||
</Button>
|
||||
<Button type="primary" onClick={handleAdd}>
|
||||
新增
|
||||
</Button>
|
||||
{canAdd && (
|
||||
<Button type="primary" onClick={handleAdd}>
|
||||
新增
|
||||
</Button>
|
||||
)}
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
@@ -240,6 +261,7 @@ export default defineComponent({
|
||||
...args,
|
||||
onEdit: handleEdit,
|
||||
onToggleStatus: handleToggleStatus,
|
||||
permissions: operationPermissions,
|
||||
}),
|
||||
}}
|
||||
</Table>
|
||||
|
||||
@@ -32,7 +32,7 @@ const userPage = '/admin/sys/user/page';
|
||||
const userSave = '/admin/sys/user/save';
|
||||
const userUpdate = '/admin/sys/user/update';
|
||||
const userActive = '/admin/sys/user/active';
|
||||
const userUpdatePwd = '/sys/user/updatepwd';
|
||||
const userUpdatePwd = '/admin/sys/user/updatepwd';
|
||||
|
||||
// ============================================================
|
||||
// API 函数
|
||||
|
||||
@@ -39,6 +39,11 @@ router.beforeEach(async (to, _from, next) => {
|
||||
await Promise.all([loadMenu(), loadPermissions()]);
|
||||
}
|
||||
|
||||
if (!auth.isLoggedIn()) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
const redirectPath = getRedirectPath(to.query.redirect);
|
||||
const targetPath =
|
||||
redirectPath && isRoutePathAvailable(redirectPath)
|
||||
@@ -63,6 +68,12 @@ router.beforeEach(async (to, _from, next) => {
|
||||
const { loadPermissions } = usePermissionStore();
|
||||
await Promise.all([loadMenu(), loadPermissions()]);
|
||||
|
||||
// 权限获取失败时会清空会话,此时回登录页而不是继续进入业务页
|
||||
if (!auth.isLoggedIn()) {
|
||||
next({ path: '/login', replace: true });
|
||||
return;
|
||||
}
|
||||
|
||||
// 动态路由刚注册,需要用新路由表重新匹配当前路径
|
||||
next({ path: to.fullPath, replace: true });
|
||||
return;
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { RouteRecordRaw } from 'vue-router';
|
||||
import type { MenuNode, MenuItemRaw } from '@/types';
|
||||
import router, { resetRouter } from '@/router';
|
||||
import { fetchMenuTree } from '@/api/menu';
|
||||
import { auth } from '@/hooks/useAuth';
|
||||
import { pageModules } from '@/router/glob';
|
||||
import { FALLBACK_MENU_NODES } from '@/config/fallbackRoutes';
|
||||
|
||||
@@ -276,6 +277,8 @@ export function useMenuStore() {
|
||||
state.loaded = true;
|
||||
} catch (err) {
|
||||
console.error('加载菜单失败:', err);
|
||||
// 权限获取失败时会强制重新登录,此时不再降级兜底菜单,避免错误路由状态
|
||||
if (!auth.isLoggedIn()) return;
|
||||
// 降级:使用本地兜底菜单
|
||||
applyMenuTree(FALLBACK_MENU_NODES);
|
||||
state.loaded = true;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { reactive, computed, toRefs } from 'vue';
|
||||
import type { PermissionCode } from '@/types';
|
||||
import { fetchAuthData } from '@/api/menu';
|
||||
import { auth } from '@/hooks/useAuth';
|
||||
|
||||
interface PermissionState {
|
||||
/** 权限编码集合 */
|
||||
@@ -31,6 +32,7 @@ export function usePermissionStore() {
|
||||
state.loaded = true;
|
||||
} catch (err) {
|
||||
console.error('加载权限失败:', err);
|
||||
if (!auth.isLoggedIn()) return;
|
||||
state.codes = new Set();
|
||||
state.roles = [];
|
||||
state.loaded = true;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { auth } from '@/hooks/';
|
||||
import { auth, forceReLogin } from '@/hooks/';
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
const baseUrl = import.meta.env?.VITE_API_BASE_URL || '';
|
||||
@@ -8,9 +8,7 @@ const activeControllers = new Set<AbortController>();
|
||||
|
||||
/** 统一处理 401 未授权逻辑 */
|
||||
const handleUnauthorized = (msg?: string) => {
|
||||
auth.logout();
|
||||
message.error(msg || '登录状态已过期,请重新登录');
|
||||
window.location.href = '/login';
|
||||
forceReLogin(msg || '登录状态已过期,请重新登录');
|
||||
};
|
||||
|
||||
const request = (
|
||||
|
||||
Reference in New Issue
Block a user