Files
Vue3/src/utils/club-context.js
2026-07-26 03:08:10 +08:00

464 lines
20 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const CLUB_ID_KEY = 'admin_club_id';
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 = '27';
const DEFAULT_CLUB_ID = 'xq';
let menuAccessInflight = null;
export function getAdminClubId() {
return localStorage.getItem(CLUB_ID_KEY) || DEFAULT_CLUB_ID;
}
export function getAdminClubScope() {
return localStorage.getItem(CLUB_SCOPE_KEY) || 'single';
}
export function setAdminClubContext(ctx) {
if (!ctx) return;
localStorage.setItem(CLUB_CONTEXT_KEY, JSON.stringify(ctx));
localStorage.setItem(CLUB_ID_KEY, ctx.club_id || DEFAULT_CLUB_ID);
localStorage.setItem(CLUB_SCOPE_KEY, ctx.scope === 'ALL_CLUBS' ? 'all' : 'single');
}
export function mergeServerClubContext(serverCtx) {
if (!serverCtx) return;
const allowedIds = new Set((serverCtx.clubs || []).map((c) => c.club_id));
const prevScope = localStorage.getItem(CLUB_SCOPE_KEY);
const prevClubId = localStorage.getItem(CLUB_ID_KEY);
let clubId = serverCtx.club_id || DEFAULT_CLUB_ID;
let scopeKey = serverCtx.scope === 'ALL_CLUBS' ? 'all' : 'single';
if (prevClubId && allowedIds.has(prevClubId)) {
clubId = prevClubId;
if (prevScope === 'all' && serverCtx.is_group_admin) {
scopeKey = 'all';
} else if (prevScope === 'single') {
scopeKey = 'single';
}
} else if (prevScope === 'all' && serverCtx.is_group_admin) {
scopeKey = 'all';
} else if (!allowedIds.has(clubId) && allowedIds.size > 0) {
clubId = serverCtx.club_id || [...allowedIds][0] || DEFAULT_CLUB_ID;
}
const merged = {
...serverCtx,
club_id: clubId,
scope: scopeKey === 'all' ? 'ALL_CLUBS' : 'SINGLE_CLUB',
};
if (scopeKey === 'single' && merged.assignments?.length) {
const hit = merged.assignments.find((a) => a.club_id === clubId);
if (hit) {
merged.role_code = hit.role_code;
merged.role_name = hit.role_name;
}
}
if (prevClubId && prevClubId !== clubId) {
clearMenuAccess();
}
setAdminClubContext(merged);
return merged;
}
export function resetAdminClubContextOnLogin() {
localStorage.removeItem(CLUB_ID_KEY);
localStorage.removeItem(CLUB_SCOPE_KEY);
localStorage.removeItem(CLUB_CONTEXT_KEY);
clearMenuAccess();
}
export function getAdminClubContext() {
try {
const raw = localStorage.getItem(CLUB_CONTEXT_KEY);
return raw ? JSON.parse(raw) : null;
} catch {
return null;
}
}
export function setMenuAccess(access) {
if (!access) return;
localStorage.setItem(MENU_ACCESS_KEY, JSON.stringify(access));
sessionStorage.setItem(MENU_SESSION_KEY, MENU_SESSION_VERSION);
}
export function clearMenuAccess() {
localStorage.removeItem(MENU_ACCESS_KEY);
sessionStorage.removeItem(MENU_SESSION_KEY);
}
export function getMenuAccess() {
try {
const raw = localStorage.getItem(MENU_ACCESS_KEY);
return raw ? JSON.parse(raw) : null;
} catch {
return null;
}
}
export function isMenuSessionFresh() {
return sessionStorage.getItem(MENU_SESSION_KEY) === MENU_SESSION_VERSION;
}
export function shouldShowAllMenus(access) {
if (!access) 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 false;
}
/** ??? 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', '002ab', '003aa'] },
{ 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'] },
// 管理员:超级管理 000001 可见(不靠 perm_codes
{ page_id: 'admin.role', parent_id: 'admin', path: '/admin/role', sort_order: 71, require_super: true },
{ page_id: 'admin.user', parent_id: 'admin', path: '/admin/user', sort_order: 74, require_super: true },
{ page_id: 'admin.operation-log', parent_id: 'admin', path: '/admin/operation-log', sort_order: 75, perm_codes: ['czrz666'] },
{ page_id: 'punishment', parent_id: '', path: '/punishment', sort_order: 80, perm_codes: ['005aa', '66693a', '66693b', '66693c', '66694c', '99933abs'] },
{ page_id: 'gongdan', parent_id: '', path: '/gongdan', sort_order: 81, perm_codes: ['gdck666', 'gdcl666'] },
{ 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.turntable', parent_id: 'product', path: '/product/turntable', sort_order: 93, perm_codes: ['2200a'] },
{ page_id: 'product.data', parent_id: 'product', path: '/product/data', sort_order: 94, perm_codes: ['2200a'] },
{ page_id: 'product.fake-orders', parent_id: 'product', path: '/product/fake-orders', sort_order: 95, 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: ['3300a'] },
{ 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: 'miniapp.rank-reward', parent_id: 'miniapp', path: '/miniapp/rank-reward', sort_order: 118, perm_codes: ['phbj666', '8080a'] },
{ page_id: 'miniapp.recharge-copy', parent_id: 'miniapp', path: '/miniapp/recharge-copy', sort_order: 119, perm_codes: ['8080a'] },
{ page_id: 'miniapp.withdraw-settings', parent_id: 'miniapp', path: '/withdraw/settings', sort_order: 114, perm_codes: ['5500a', '5500b', '5500c'] },
{ 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 = {
admin: ['admin.role', 'admin.data-scope', 'admin.club-config', 'admin.order-grab-share', 'admin.user', 'admin.operation-log'],
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.turntable', 'product.data', 'product.fake-orders'],
member: ['member.list', 'member.data'],
miniapp: ['miniapp.carousel', 'miniapp.assets', 'miniapp.popup', 'miniapp.leixing', 'miniapp.kaohe-tags', 'miniapp.dashou-exam', 'miniapp.rank-reward', 'miniapp.recharge-copy', '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;
return Array.isArray(paths) ? paths : [];
}
/**
* 兼容尚未 seed 到服务端的新菜单:本地已定义且 canShowMenuPage 通过即可进路由。
* 避免「侧栏看得见、点进去却被踢到默认页(如板块配置)」。
*/
export function isLocallyDefinedMenuPathAllowed(path) {
if (!path) return false;
const row = KEFU_MENU_ROWS.find(
(r) => r.path && (path === r.path || path.startsWith(`${r.path}/`)),
);
if (!row) return false;
return canShowMenuPage(row.page_id);
}
export function hasPermCode(access, ...codes) {
if (!access || !codes.length) return false;
const list = access.permission_codes || [];
return codes.some((c) => list.includes(c));
}
export function hasMerchantOrderPerm(access) {
return hasPermCode(access, '002ac', '002ab', '003aa');
}
export function canShowMenuPage(pageId) {
const access = getMenuAccess();
if (!access) return false;
const codes = access.permission_codes || [];
const hasSuper = !!access.is_super || codes.includes('000001');
// 本店超级管理:管理员分组 / 角色管理 / 后台用户(不依赖服务端是否漏 seed
if (
hasSuper &&
(pageId === 'admin' || pageId === 'admin.role' || pageId === 'admin.user')
) {
return true;
}
// 集团权限:订单互通配置(菜单 seed 未跑时本地兜底)
if (pageId === 'admin.order-grab-share' && access.is_group_admin) {
return true;
}
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];
if (children && children.some((id) => ids.includes(id))) return true;
// 兼容尚未 seed 的新菜单:本地已定义 + 用户有对应权限(或超管)
const row = KEFU_MENU_ROWS.find((r) => r.page_id === pageId);
if (!row) return false;
if (access.is_super || hasSuper) {
if (row.require_super) return true;
}
return pageAllowedByCodes(pageId, codes);
}
if (access.is_super || access.is_group_admin || hasSuper) {
const row = KEFU_MENU_ROWS.find((r) => r.page_id === pageId);
if (row?.require_super && hasSuper) return true;
return !!KEFU_MENU_ROWS.find((r) => r.page_id === pageId);
}
return pageAllowedByCodes(pageId, codes);
}
/** 写入菜单缓存,并同步服务端校正后的 effective_club_id */
export function applyMenuAccessPayload(data) {
if (!data) return null;
const scope = getAdminClubScope();
const expectedClub = getAdminClubId();
if (
data.effective_club_id &&
scope !== 'all' &&
data.effective_club_id !== expectedClub
) {
const ctx = getAdminClubContext() || {};
setAdminClubContext({
...ctx,
club_id: data.effective_club_id,
scope: 'SINGLE_CLUB',
});
}
setMenuAccess(data);
return data;
}
export function getFirstVisiblePath() {
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`;
}
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() {
const ctx = getAdminClubContext();
if (ctx?.can_switch_club) return true;
const access = getMenuAccess();
return !!access?.can_switch_club;
}
export async function syncClubContextFromServer(fetcher) {
const prevClubId = getAdminClubId();
try {
const data = await fetcher();
if (data) {
mergeServerClubContext(data);
if (getAdminClubId() !== prevClubId) {
clearMenuAccess();
}
}
} catch (e) {
console.error('syncClubContextFromServer failed:', e);
}
}
export async function ensureMenuAccess(fetcher, { force = false } = {}) {
if (!localStorage.getItem('token')) return getMenuAccess();
if (!force && isMenuSessionFresh() && getMenuAccess()) {
return getMenuAccess();
}
if (!menuAccessInflight) {
menuAccessInflight = (async () => {
try {
const data = await fetcher();
if (data) return applyMenuAccessPayload(data);
return getMenuAccess();
} catch (e) {
console.error('ensureMenuAccess failed:', e);
return getMenuAccess();
} finally {
menuAccessInflight = null;
}
})();
}
return menuAccessInflight;
}
export function applyClubHeaders(config = {}) {
const headers = { ...(config.headers || {}) };
const clubId = getAdminClubId();
const scope = getAdminClubScope();
headers['X-Club-Id'] = clubId;
if (scope === 'all') {
headers['X-Club-Scope'] = 'all';
}
return { ...config, headers };
}
export const JITUAN_API = {
kefuLogin: '/jituan/auth/kefu-login',
meContext: '/jituan/auth/me-context',
menuAccess: '/jituan/auth/menu-access',
permDebug: '/jituan/auth/perm-debug',
clubList: '/jituan/club/list',
caiwu: '/jituan/houtai/caiwu',
szxx: '/jituan/houtai/szxx',
auditLog: '/jituan/houtai/audit-log',
orderTypes: '/jituan/houtai/cwddhqlx',
orderFinance: '/jituan/houtai/cwhqjtddsj',
chongzhiFinance: '/jituan/houtai/cwqtczhq',
huiyuanBankuai: '/jituan/houtai/cwhybkhq',
huiyuanStats: '/jituan/houtai/hybkjtsj',
userSummary: '/jituan/houtai/user-summary',
dashouList: '/jituan/houtai/kefuhqdslb',
guanshiList: '/jituan/houtai/hthqgslb',
shangjiaList: '/jituan/houtai/hqsjgllb',
shangjiaAdd: '/jituan/houtai/tjsj',
zuzhangList: '/jituan/houtai/hthqzzlb',
operationLog: '/jituan/houtai/hqczrz',
withdrawGet: '/jituan/houtai/hthqtxpz',
withdrawUpdate: '/jituan/houtai/htxgtxsz',
lunboList: '/jituan/houtai/lunbo-list',
lunboManage: '/jituan/houtai/lunbo-manage',
gonggao: '/jituan/houtai/gonggao',
rechargePageCopyGet: '/jituan/houtai/recharge-page-copy/get',
rechargePageCopySave: '/jituan/houtai/recharge-page-copy/save',
tupianList: '/jituan/houtai/tupian-list',
tupianManage: '/jituan/houtai/tupian-manage',
popupList: '/jituan/houtai/hthqtcxx',
popupModify: '/jituan/houtai/htxgtcxx',
penaltyStats: '/jituan/houtai/htglyhqcfsltj',
penaltyList: '/jituan/houtai/hthqfklb',
penaltyAction: '/jituan/houtai/glyclfk',
penaltyPlatformAudit: '/jituan/houtai/glyptshfk',
penaltyCreate: '/jituan/houtai/htfksc',
kefuCfgl: '/jituan/houtai/kefu-cfgl',
adminAssignments: '/jituan/houtai/admin-assignments',
clubManage: '/jituan/houtai/club-manage',
orderGrabLink: '/jituan/houtai/order-grab-link',
paymentChannelManage: '/jituan/houtai/payment-channel-manage',
clubCertUpload: '/jituan/houtai/club-cert-upload',
memberList: '/jituan/houtai/hthqhylb',
memberUpdate: '/jituan/houtai/htxghyxx',
memberAdd: '/jituan/houtai/httjhy',
memberCatalog: '/jituan/houtai/hthycatalog',
memberEnable: '/jituan/houtai/hthysj',
memberDisable: '/jituan/houtai/hthyxj',
memberCardUpload: '/jituan/houtai/hy-card-upload',
memberRechargeRecords: '/jituan/houtai/hy-recharge-records',
memberBundleCreate: '/jituan/houtai/hy-bundle-create',
identityTagList: '/jituan/houtai/identity-tag/list',
identityTagManage: '/jituan/houtai/identity-tag/manage',
identityTagBindList: '/jituan/houtai/identity-tag/bind-list',
identityTagBind: '/jituan/houtai/identity-tag/bind',
miniappIconList: '/jituan/houtai/miniapp-icon-list',
miniappIconManage: '/jituan/houtai/miniapp-icon-manage',
pindaoManage: '/jituan/houtai/pindao-manage',
fakeGrabOrderList: '/jituan/houtai/fake-grab-order-list',
fakeGrabOrderManage: '/jituan/houtai/fake-grab-order-manage',
fakeGrabOrderAvatar: '/jituan/houtai/fake-grab-order-avatar',
clubLeixingList: '/jituan/houtai/club-leixing-list',
clubLeixingCatalog: '/jituan/houtai/club-leixing-catalog',
clubLeixingEnable: '/jituan/houtai/club-leixing-enable',
clubLeixingDisable: '/jituan/houtai/club-leixing-disable',
clubLeixingIcon: '/jituan/houtai/club-leixing-icon',
clubKaoheList: '/jituan/houtai/club-kaohe-list',
clubKaoheCatalog: '/jituan/houtai/club-kaohe-catalog',
clubKaoheEnable: '/jituan/houtai/club-kaohe-enable',
clubKaoheDisable: '/jituan/houtai/club-kaohe-disable',
khggl: '/jituan/houtai/khggl',
khgglAdd: '/jituan/houtai/khggl-add',
shgxgsj: '/jituan/houtai/shgxgsj',
dashouExamConfig: '/jituan/houtai/dashou-exam-config',
dashouExamQuestionList: '/jituan/houtai/dashou-exam-question-list',
dashouExamQuestionManage: '/jituan/houtai/dashou-exam-question-manage',
dashouExamImageManage: '/jituan/houtai/dashou-exam-image-manage',
};