diff --git a/src/router/index.js b/src/router/index.js index 1892ab2..843ffcf 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -4,7 +4,8 @@ import request from '@/utils/request' import { ensureMenuAccess, getMenuAccess, - getFirstVisiblePath, + getDefaultLandingPath, + getEffectiveVisiblePaths, isMenuSessionFresh, JITUAN_API, } from '@/utils/club-context' @@ -17,10 +18,13 @@ const routes = [ { path: '/', component: Layout, - redirect: () => getFirstVisiblePath() || '/order/platform', + redirect: () => getDefaultLandingPath() || '/welcome', children: [ - - + { + path: 'welcome', + component: () => import('@/views/Welcome.vue'), + meta: { title: '首页' } + }, // ========== 板块与财务 ========== { path: 'finance/bankuai', @@ -352,18 +356,31 @@ router.beforeEach(async (to, from, next) => { } const access = getMenuAccess() - if (!isMenuSessionFresh() || !access?.visible_paths?.length) { + + if (!isMenuSessionFresh()) { + next() + return + } + + if (to.path === '/welcome') { + next() + return + } + + if (!getEffectiveVisiblePaths(access).length) { + if (to.path !== '/welcome') { + next('/welcome') + return + } next() return } const path = to.path - if (!isRouteAllowed(path, access.visible_paths)) { - const fallback = getFirstVisiblePath() - if (fallback && path !== fallback) { - next(fallback) - return - } + if (!isRouteAllowed(path, getEffectiveVisiblePaths(access))) { + const fallback = getDefaultLandingPath(access) || '/welcome' + next(fallback) + return } next() }) diff --git a/src/utils/club-context.js b/src/utils/club-context.js index 8c1a871..4721e78 100644 --- a/src/utils/club-context.js +++ b/src/utils/club-context.js @@ -3,7 +3,7 @@ const CLUB_SCOPE_KEY = 'admin_club_scope'; const CLUB_CONTEXT_KEY = 'admin_club_context'; const MENU_ACCESS_KEY = 'kefu_menu_access'; const MENU_SESSION_KEY = 'kefu_menu_session_v'; -const MENU_SESSION_VERSION = '8'; +const MENU_SESSION_VERSION = '11'; const DEFAULT_CLUB_ID = 'xq'; let menuAccessInflight = null; @@ -82,24 +82,145 @@ export function isMenuSessionFresh() { export function shouldShowAllMenus(access) { if (!isMenuSessionFresh()) return true; if (!access) return true; - if (access.menu_ready === false) return true; - if (!Array.isArray(access.visible_page_ids) || !Array.isArray(access.visible_paths)) return true; - return false; + if (Array.isArray(access.visible_page_ids) && access.visible_page_ids.length > 0) { + return false; + } + if (Array.isArray(access.visible_paths) && access.visible_paths.length > 0) { + return false; + } + if (Array.isArray(access.permission_codes) && access.permission_codes.length > 0) { + return false; + } + return true; +} + +/** ??? kefu_menu_definitions ???visible_paths ???? permission_codes ?? */ +const KEFU_MENU_ROWS = [ + { page_id: 'finance.bankuai', parent_id: 'finance', path: '/finance/bankuai', sort_order: 11, perm_codes: ['bankuai'] }, + { page_id: 'finance.data', parent_id: 'finance', path: '/finance/data', sort_order: 12, perm_codes: ['caiwu'] }, + { page_id: 'assessment.examiner', parent_id: 'assessment', path: '/assessment/examiner', sort_order: 21, perm_codes: ['kaohepeizhi'] }, + { page_id: 'assessment.config', parent_id: 'assessment', path: '/assessment/config', sort_order: 22, perm_codes: ['kaohepeizhi'] }, + { page_id: 'assessment.record', parent_id: 'assessment', path: '/assessment/record', sort_order: 23, perm_codes: ['kaohepeizhi'] }, + { page_id: 'order.platform', parent_id: 'order', path: '/order/platform', sort_order: 31, perm_codes: ['002ab'] }, + { page_id: 'order.merchant', parent_id: 'order', path: '/order/merchant', sort_order: 32, perm_codes: ['002ac'] }, + { page_id: 'cross-platform.order', parent_id: 'cross-platform', path: '/cross-order', sort_order: 41, perm_codes: ['003aa'] }, + { page_id: 'cross-platform.settings', parent_id: 'cross-platform', path: '/cross-settings', sort_order: 42, perm_codes: ['003aa'] }, + { page_id: 'cross-platform.data', parent_id: 'cross-platform', path: '/cross-data', sort_order: 43, perm_codes: ['003aa'] }, + { page_id: 'cross-platform.pre', parent_id: 'cross-platform', path: '/cross-pre', sort_order: 44, perm_codes: ['003aa'] }, + { page_id: 'user.player', parent_id: 'user', path: '/user/player', sort_order: 51, perm_codes: ['001aa', '001bb', '001cc', '001dd', '001ee', '001ff', '001gg'] }, + { page_id: 'user.guanli', parent_id: 'user', path: '/user/guanli', sort_order: 52, perm_codes: ['4400a', '4400b', '4400c', '4400d', '4400e', '4400f'] }, + { page_id: 'user.shangjia', parent_id: 'user', path: '/user/shangjia', sort_order: 53, perm_codes: ['1100a', '1100b'] }, + { page_id: 'user.zuzhang', parent_id: 'user', path: '/user/zuzhang', sort_order: 54, perm_codes: ['6600a', '6600b', '6600c', '6600d', '6600e'] }, + { page_id: 'user.identity-tag', parent_id: 'user', path: '/user/identity-tag', sort_order: 55, perm_codes: ['8080a', 'sfbq666'] }, + { page_id: 'withdraw.audit', parent_id: 'withdraw', path: '/withdraw/audit', sort_order: 61, perm_codes: ['005bb'] }, + { page_id: 'withdraw.data', parent_id: 'withdraw', path: '/withdraw/data', sort_order: 63, perm_codes: ['005bb'] }, + { page_id: 'withdraw.settings', parent_id: 'withdraw', path: '/withdraw/settings', sort_order: 64, perm_codes: ['5500a', '5500b', '5500c'] }, + { page_id: 'punishment', parent_id: '', path: '/punishment', sort_order: 80, perm_codes: ['005aa', '66693a', '66693b', '66693c', '66694c', '99933abs'] }, + { page_id: 'product.list', parent_id: 'product', path: '/product/list', sort_order: 91, perm_codes: ['2200a'] }, + { page_id: 'product.type-zone', parent_id: 'product', path: '/product/type-zone', sort_order: 92, perm_codes: ['7007a'] }, + { page_id: 'product.data', parent_id: 'product', path: '/product/data', sort_order: 93, perm_codes: ['2200a'] }, + { page_id: 'member.list', parent_id: 'member', path: '/member/list', sort_order: 101, perm_codes: ['3300a'] }, + { page_id: 'member.data', parent_id: 'member', path: '/member/data', sort_order: 102, perm_codes: ['huiyuancaiwu'] }, + { page_id: 'miniapp.carousel', parent_id: 'miniapp', path: '/miniapp/carousel', sort_order: 111, perm_codes: ['8080a'] }, + { page_id: 'miniapp.assets', parent_id: 'miniapp', path: '/miniapp/assets', sort_order: 112, perm_codes: ['8080a'] }, + { page_id: 'miniapp.popup', parent_id: 'miniapp', path: '/miniapp/popup-notice', sort_order: 113, perm_codes: ['8080a'] }, + { page_id: 'miniapp.leixing', parent_id: 'miniapp', path: '/miniapp/leixing', sort_order: 115, perm_codes: ['8080a'] }, + { page_id: 'miniapp.kaohe-tags', parent_id: 'miniapp', path: '/miniapp/kaohe-tags', sort_order: 116, perm_codes: ['8080a'] }, + { page_id: 'miniapp.dashou-exam', parent_id: 'miniapp', path: '/miniapp/dashou-exam', sort_order: 117, perm_codes: ['dsks666', '8080a'] }, + { page_id: 'shop.list', parent_id: 'shop', path: '/shop/list', sort_order: 121, perm_codes: ['999a', '999b', '999c', '999d', '999e'] }, + { page_id: 'shop.withdraw', parent_id: 'shop', path: '/shop/withdraw-apply', sort_order: 122, perm_codes: ['999a', '999b', '999c', '999d', '999e'] }, + { page_id: 'shop.products', parent_id: 'shop', path: '/shop/products', sort_order: 123, perm_codes: ['999a', '999b', '999c', '999d', '999e'] }, + { page_id: 'chat.agent', parent_id: '', path: '/chat/agent', sort_order: 130, perm_codes: ['abca1', 'baac2', 'cabc3', 'cb3a2', 'bcaa4'] }, +]; + +const KEFU_MENU_PARENT_IDS = { + finance: ['finance.bankuai', 'finance.data'], + assessment: ['assessment.examiner', 'assessment.config', 'assessment.record'], + order: ['order.platform', 'order.merchant'], + 'cross-platform': ['cross-platform.order', 'cross-platform.settings', 'cross-platform.data', 'cross-platform.pre'], + user: ['user.player', 'user.guanli', 'user.shangjia', 'user.zuzhang', 'user.identity-tag'], + withdraw: ['withdraw.audit', 'withdraw.data', 'withdraw.settings'], + product: ['product.list', 'product.type-zone', 'product.data'], + member: ['member.list', 'member.data'], + miniapp: ['miniapp.carousel', 'miniapp.assets', 'miniapp.popup', 'miniapp.leixing', 'miniapp.kaohe-tags', 'miniapp.dashou-exam', 'miniapp.withdraw-settings'], + shop: ['shop.list', 'shop.withdraw', 'shop.products'], +}; + +function pathsFromPermissionCodes(codes) { + if (!codes?.length) return []; + const seen = new Set(); + const paths = []; + for (const row of KEFU_MENU_ROWS) { + if (!row.path) continue; + const perms = row.perm_codes || []; + if (!perms.some((p) => codes.includes(p))) continue; + if (seen.has(row.path)) continue; + seen.add(row.path); + paths.push(row.path); + } + return paths; +} + +function pageAllowedByCodes(pageId, codes) { + const row = KEFU_MENU_ROWS.find((r) => r.page_id === pageId); + if (row?.perm_codes?.some((p) => codes.includes(p))) return true; + const children = KEFU_MENU_PARENT_IDS[pageId]; + if (!children) return false; + return children.some((childId) => pageAllowedByCodes(childId, codes)); +} + +export function getEffectiveVisiblePaths(access = null) { + const a = access || getMenuAccess(); + const paths = a?.visible_paths; + if (Array.isArray(paths) && paths.length > 0) { + return paths; + } + return pathsFromPermissionCodes(a?.permission_codes || []); } export function canShowMenuPage(pageId) { const access = getMenuAccess(); if (shouldShowAllMenus(access)) return true; - if (access.visible_page_ids.length === 0) return false; - return access.visible_page_ids.includes(pageId); + const ids = access?.visible_page_ids; + if (Array.isArray(ids) && ids.length > 0) { + if (ids.includes(pageId)) return true; + const children = KEFU_MENU_PARENT_IDS[pageId]; + return children ? children.some((id) => ids.includes(id)) : false; + } + const codes = access?.permission_codes || []; + if (!codes.length) return false; + return pageAllowedByCodes(pageId, codes); } export function getFirstVisiblePath() { - const access = getMenuAccess(); - if (!access || !Array.isArray(access.visible_paths) || access.visible_paths.length === 0) { - return null; + const paths = getEffectiveVisiblePaths(); + return paths.length > 0 ? paths[0] : null; +} + +export function getDefaultLandingPath(access = null) { + const paths = getEffectiveVisiblePaths(access); + return paths.length > 0 ? paths[0] : null; +} + +/** menu_ready=false: server seed missing; empty codes: role not bound */ +export function formatNoMenuAccessMessage(access) { + const codes = access?.permission_codes || []; + const roles = access?.role_names || []; + const uid = access?.yonghuid || ''; + const idHint = uid ? ` UserID=${uid}.` : ''; + if (access?.menu_ready === false && access?.menu_source === 'db') { + return `Menu table empty.${idHint} Server: python manage.py migrate jituan && python manage.py seed_kefu_menu`; } - return access.visible_paths[0]; + if (!codes.length) { + if (roles.length) { + return `No permissions.${idHint} Roles: [${roles.join(', ')}]. Add perm kaohepeizhi to these roles in Role Mgmt.`; + } + return `No role on account.${idHint} Admin Users -> open THIS user (check UserID) -> Add Role -> Save -> re-login.`; + } + if (!getEffectiveVisiblePaths(access).length) { + return `Perms: [${codes.join(', ')}].${idHint} No menu path from server.`; + } + return `Perms: [${codes.join(', ')}].${idHint} Contact admin.`; } export function canSwitchClubByMenu() { diff --git a/src/utils/request.js b/src/utils/request.js index 0c5c2ad..099299d 100644 --- a/src/utils/request.js +++ b/src/utils/request.js @@ -39,7 +39,9 @@ service.interceptors.response.use( router.push('/login') ElMessage.error('登录已过期,请重新登录') } else if (status === 403) { - ElMessage.error('没有权限') + if (!error.config?.skipErrorToast) { + ElMessage.error('没有权限') + } } else { ElMessage.error(error.response.data?.message || '请求失败') } diff --git a/src/views/Layout.vue b/src/views/Layout.vue index a010f17..eaf91cc 100644 --- a/src/views/Layout.vue +++ b/src/views/Layout.vue @@ -256,18 +256,13 @@ import { setMenuAccess, getMenuAccess, canSwitchClubByMenu, - shouldShowAllMenus, ensureMenuAccess, + canShowMenuPage, } from '@/utils/club-context' const menuAccessState = ref(getMenuAccess()) -const canShow = (pageId) => { - const access = menuAccessState.value ?? getMenuAccess() - if (shouldShowAllMenus(access)) return true - if (access.visible_page_ids.length === 0) return false - return access.visible_page_ids.includes(pageId) -} +const canShow = (pageId) => canShowMenuPage(pageId) const { proxy } = getCurrentInstance() const route = useRoute() @@ -327,7 +322,7 @@ const loadMenuAccess = async () => { const loadClubContext = async () => { try { - const res = await request.get(JITUAN_API.meContext) + const res = await request.get(JITUAN_API.meContext, { skipErrorToast: true }) if (res.code === 0 && res.data) { mergeServerClubContext(res.data) clubOptions.value = res.data.clubs || [] @@ -386,7 +381,7 @@ const fetchStats = async () => { try { const res = await request.post('/yonghu/kfjrhqzl', { phone: userPhone.value - }, { signal: abortController.signal }) + }, { signal: abortController.signal, skipErrorToast: true }) if (res.code === 0) { stats.value = res.data } @@ -427,7 +422,7 @@ const fetchKefuPermission = async () => { try { const res = await request.post('/houtai/kfhqltqx', { phone: userPhone.value - }) + }, { skipErrorToast: true }) if (res.code === 0) { const { permissions, goeasy_appkey } = res.data kefuPermissions.value = permissions || [] diff --git a/src/views/Login.vue b/src/views/Login.vue index c4ed746..055c253 100644 --- a/src/views/Login.vue +++ b/src/views/Login.vue @@ -65,7 +65,7 @@ import { useRouter } from 'vue-router' import { ElMessage } from 'element-plus' import { User, Lock, Key } from '@element-plus/icons-vue' import axios from 'axios' // 直接使用 axios,不经过封装的 request -import { JITUAN_API, setAdminClubContext, setMenuAccess } from '@/utils/club-context' +import { JITUAN_API, getDefaultLandingPath, setAdminClubContext, setMenuAccess } from '@/utils/club-context' const router = useRouter() const loading = ref(false) @@ -123,9 +123,10 @@ const handleLogin = async () => { } else if (res.data.menu_access) { setMenuAccess(res.data.menu_access) } + const ma = res.data.menu_access || {} + const landing = getDefaultLandingPath(ma) ElMessage.success('登录成功') - const firstPath = res.data.menu_access?.visible_paths?.[0] - router.push(firstPath || '/order/platform') + router.push(landing || '/welcome') } else if (res.code === 4) { // 账号被封禁 ElMessage.error('账号已被封禁') diff --git a/src/views/Welcome.vue b/src/views/Welcome.vue new file mode 100644 index 0000000..300fa3a --- /dev/null +++ b/src/views/Welcome.vue @@ -0,0 +1,75 @@ + + + + + diff --git a/src/views/admin/AdminUser.vue b/src/views/admin/AdminUser.vue index ddfa5bf..76059ad 100644 --- a/src/views/admin/AdminUser.vue +++ b/src/views/admin/AdminUser.vue @@ -51,6 +51,7 @@ +