fix: 优化登录相关权限问题 添加按键权限判断

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