169 lines
8.7 KiB
JavaScript
169 lines
8.7 KiB
JavaScript
import {
|
|
clearAuthSession,
|
|
getAuthContext,
|
|
startAuthSync,
|
|
switchAccount,
|
|
} from '/static/portal/common/auth.js';
|
|
import { escapeHtml } from '/static/portal/common/formatters.js';
|
|
import { mountCustomerServiceWidget } from '/static/portal/common/customer-service-widget/widget.js';
|
|
|
|
const PUBLIC_LINKS = [
|
|
['home', '首页', '/portal/guest/home/'],
|
|
['products', '基金产品', '/portal/guest/products/'],
|
|
['ranking', '基金排行', '/portal/guest/products/?view=ranking'],
|
|
];
|
|
|
|
const CUSTOMER_LINKS = [
|
|
['dashboard', '资产总览', '/portal/customer/dashboard/'],
|
|
['holdings', '我的持仓', '/portal/customer/holdings/'],
|
|
['profit-loss', '收益明细', '/portal/customer/profit-loss/'],
|
|
['orders', '交易记录', '/portal/customer/orders/'],
|
|
['transactions', '成交明细', '/portal/customer/transactions/'],
|
|
['risk-questionnaire', '风险测评', '/portal/customer/risk-questionnaire/'],
|
|
];
|
|
|
|
const RISK_LINKS = [
|
|
['risk-dashboard', '风险工作台', '/portal/employee-risk/dashboard/'],
|
|
];
|
|
|
|
const ADVISOR_LINKS = [
|
|
['advisor-dashboard', '投顾工作台', '/portal/employee-advisor/dashboard/'],
|
|
['public-products', '公开产品', '/portal/guest/products/'],
|
|
];
|
|
|
|
const OPERATOR_LINKS = [
|
|
['operator-dashboard', '运营工作台', '/portal/employee-operations/dashboard/'],
|
|
['operator-offsite', '场外申赎', '/portal/employee-operations/offsite/'],
|
|
['operator-promotion', '推介材料', '/portal/employee-operations/promotion/'],
|
|
['operator-nl2sql', 'NL2SQL', '/portal/employee-operations/nl2sql/'],
|
|
['public-products', '公开产品', '/portal/guest/products/'],
|
|
];
|
|
|
|
const ADMIN_LINKS = [
|
|
['admin-workspace', '平台治理', '/portal/employee-console/workspace/'],
|
|
['risk-dashboard', '风控中心', '/portal/employee-risk/dashboard/'],
|
|
];
|
|
|
|
const ROLE_LABELS = Object.freeze({
|
|
customer: '客户账户',
|
|
risk_operator: '风控人员',
|
|
operator: '运营人员',
|
|
advisor: '投顾人员',
|
|
admin: '管理员',
|
|
super_admin: '超级管理员',
|
|
});
|
|
|
|
function accountActions(context) {
|
|
const username = escapeHtml(context.username || '已登录用户');
|
|
const role = context.roles.map((item) => ROLE_LABELS[item]).find(Boolean) || '已认证账户';
|
|
return `<div class="account-menu" data-account-menu><button class="account-menu__trigger" type="button" aria-haspopup="menu" aria-expanded="false" data-account-toggle><span class="account-menu__identity"><strong>${username}</strong><span>${escapeHtml(role)}</span></span><span class="account-menu__indicator" aria-hidden="true">⌄</span></button><div class="account-menu__popover" role="menu" data-account-popover><button class="account-menu__action" type="button" role="menuitem" data-switch-account>切换账号</button><button class="account-menu__action account-menu__action--danger" type="button" role="menuitem" data-logout>退出登录</button></div></div>`;
|
|
}
|
|
|
|
function headerMarkup(links, active, context, mode) {
|
|
const nav = links.map(([key, label, href]) => `<a class="site-header__link${key === active ? ' site-header__link--active' : ''}" href="${href}"${key === active ? ' aria-current="page"' : ''}>${label}</a>`).join('');
|
|
const actions = context
|
|
? accountActions(context)
|
|
: '<a class="button button--quiet" href="/portal/employee-console/login/">员工入口</a><a class="button button--primary" href="/portal/customer/login/">登录</a>';
|
|
const menuButton = mode === 'customer' ? '' : '<button class="site-header__menu-button" type="button" aria-label="展开导航" aria-expanded="false" data-menu>菜单</button>';
|
|
return `<a class="skip-link" href="#main-content">跳到主要内容</a><header class="site-header"><div class="site-header__inner"><a class="site-header__brand" href="/portal/guest/home/"><img class="site-header__mark" src="/static/portal/common/layout/brandmark.svg" alt=""><span class="site-header__brand-copy"><strong class="site-header__brand-name">南方财富</strong><span class="site-header__brand-subtitle">模拟基金服务</span></span></a>${menuButton}<nav class="site-header__nav" aria-label="主导航" data-nav>${nav}</nav><div class="site-header__actions">${actions}</div></div></header>`;
|
|
}
|
|
|
|
function footerMarkup() {
|
|
return '<footer class="site-footer"><div class="site-footer__inner"><span>基金投资需谨慎。本系统仅提供场内基金模拟交易,不构成投资建议。</span><span>数据截至页面标注时间</span></div></footer><div class="network-status" role="status" data-network-status></div><div class="trace-watermark" data-trace-watermark></div>';
|
|
}
|
|
|
|
export function mountShell({ active, mode = 'public' }) {
|
|
startAuthSync();
|
|
document.body.classList.add(`portal-${mode}`);
|
|
const context = getAuthContext();
|
|
const links = mode === 'customer'
|
|
? CUSTOMER_LINKS
|
|
: mode === 'risk'
|
|
? RISK_LINKS
|
|
: mode === 'advisor'
|
|
? ADVISOR_LINKS
|
|
: mode === 'operator'
|
|
? OPERATOR_LINKS
|
|
: mode === 'admin'
|
|
? ADMIN_LINKS
|
|
: PUBLIC_LINKS;
|
|
document.body.insertAdjacentHTML('afterbegin', headerMarkup(links, active, context, mode));
|
|
document.body.insertAdjacentHTML('beforeend', footerMarkup());
|
|
const menu = document.querySelector('[data-menu]');
|
|
const nav = document.querySelector('[data-nav]');
|
|
menu?.addEventListener('click', () => {
|
|
const open = nav.classList.toggle('site-header__nav--open');
|
|
menu.setAttribute('aria-expanded', String(open));
|
|
});
|
|
const accountMenu = document.querySelector('[data-account-menu]');
|
|
const accountToggle = document.querySelector('[data-account-toggle]');
|
|
const accountPopover = document.querySelector('[data-account-popover]');
|
|
const closeAccountMenu = () => {
|
|
accountMenu?.classList.remove('account-menu--open');
|
|
accountToggle?.setAttribute('aria-expanded', 'false');
|
|
};
|
|
accountToggle?.addEventListener('click', () => {
|
|
const open = accountMenu.classList.toggle('account-menu--open');
|
|
accountToggle.setAttribute('aria-expanded', String(open));
|
|
if (open) accountPopover?.querySelector('[role="menuitem"]')?.focus();
|
|
});
|
|
document.addEventListener('click', (event) => {
|
|
if (accountMenu && !accountMenu.contains(event.target)) closeAccountMenu();
|
|
});
|
|
document.addEventListener('keydown', (event) => {
|
|
if (event.key === 'Escape') {
|
|
closeAccountMenu();
|
|
accountToggle?.focus();
|
|
}
|
|
});
|
|
document.querySelector('[data-switch-account]')?.addEventListener('click', () => {
|
|
switchAccount(mode === 'customer' ? 'customer' : 'staff');
|
|
});
|
|
document.querySelector('[data-logout]')?.addEventListener('click', () => {
|
|
clearAuthSession();
|
|
window.location.assign('/portal/guest/home/?reason=signed-out');
|
|
});
|
|
const networkStatus = document.querySelector('[data-network-status]');
|
|
let networkStatusTimer = 0;
|
|
const hideNetworkStatus = () => {
|
|
window.clearTimeout(networkStatusTimer);
|
|
networkStatus.classList.remove('network-status--visible');
|
|
};
|
|
const showNetworkStatus = (message, duration = 3600) => {
|
|
window.clearTimeout(networkStatusTimer);
|
|
networkStatus.textContent = message;
|
|
networkStatus.classList.add('network-status--visible');
|
|
if (duration > 0) networkStatusTimer = window.setTimeout(hideNetworkStatus, duration);
|
|
};
|
|
window.addEventListener('offline', () => {
|
|
showNetworkStatus('网络已断开', 0);
|
|
});
|
|
window.addEventListener('online', () => {
|
|
showNetworkStatus('网络已恢复,请点击刷新', 3000);
|
|
});
|
|
window.addEventListener('portal:error', (event) => {
|
|
const detail = event.detail || {};
|
|
showNetworkStatus(detail.message || '请求未完成');
|
|
});
|
|
if (new URLSearchParams(window.location.search).get('reason') === 'signed-out') {
|
|
networkStatus.textContent = '您已安全退出登录';
|
|
networkStatus.classList.add('network-status--visible');
|
|
window.setTimeout(() => networkStatus.classList.remove('network-status--visible'), 3200);
|
|
window.history.replaceState({}, '', window.location.pathname);
|
|
}
|
|
const observer = new MutationObserver(() => {
|
|
const traceId = document.documentElement.dataset.traceId || '';
|
|
const watermark = document.querySelector('[data-trace-watermark]');
|
|
if (watermark) watermark.textContent = traceId ? `追踪标识 ${traceId}` : '';
|
|
});
|
|
observer.observe(document.documentElement, { attributes: true, attributeFilter: ['data-trace-id'] });
|
|
const isCustomerContext = context?.roles?.includes('customer');
|
|
const canUseCustomerService = mode === 'customer' || mode === 'public' && (!context || isCustomerContext);
|
|
if (canUseCustomerService) {
|
|
const style = document.createElement('link');
|
|
style.rel = 'stylesheet'; style.href = '/static/portal/common/customer-service-widget/widget.css?v=20260913';
|
|
document.head.appendChild(style);
|
|
mountCustomerServiceWidget(isCustomerContext ? 'customer' : mode);
|
|
}
|
|
}
|