Files
shencaisuan/utils/CommonUtil.js
T

2528 lines
94 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.
/**
* Shared helpers for page configuration, login state, ads, links and safe HTML.
* Depends on config.js, ApiClient.js, DateUtil.js and layui.
*/
const CommonUtil = {
get $() {
return layui.$;
},
getAuthToken() {
const raw = localStorage.getItem('token');
if (!raw) return null;
try {
return JSON.parse(raw);
} catch (e) {
return raw;
}
},
getToken() {
return typeof ApiClient !== 'undefined' && ApiClient.getToken
? ApiClient.getToken()
: this.getAuthToken();
},
escapeHtml(value) {
return String(value === undefined || value === null ? '' : value)
.replace(/&/g, '&')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
},
isLocalDevHost() {
return /^(localhost|127\.0\.0\.1|0\.0\.0\.0)$/i.test(window.location.hostname);
},
safeHref(url) {
if (!url) return '#';
const value = String(url).trim();
const lowered = value.toLowerCase();
if (lowered.startsWith('javascript:') || lowered.startsWith('data:') || lowered.startsWith('vbscript:')) {
return '#';
}
return value;
},
siteHref(url) {
const value = this.safeHref(url);
if (!value || value === '#') return value;
if (/^(?:https?:)?\/\//i.test(value) || value.startsWith('javascript:')) return value;
if (!value.startsWith('/')) return value;
if (!this.isLocalHtmlPathMode()) return value;
if (value.startsWith('/html/')) return value;
const freeArticleMatch = value.match(/^\/mianfei\/([^/?#]+)\.html([?#].*)?$/i);
if (freeArticleMatch) {
return `/html/mianfei.html?id=${encodeURIComponent(decodeURIComponent(freeArticleMatch[1]))}${freeArticleMatch[2] || ''}`;
}
if (/^\/[^/?#]+\.html(?:[?#].*)?$/i.test(value)) return `/html${value}`;
return value;
},
isLocalHtmlPathMode() {
const hostname = window.location && window.location.hostname;
return hostname === 'localhost' || hostname === '127.0.0.1' || Boolean(window.location && window.location.port);
},
rewritePageRootLinks() {
const $ = this.$;
$('a[href^="/"]').each((index, link) => {
const $link = $(link);
const href = $link.attr('href');
const nextHref = this.siteHref(href);
if (href !== nextHref) $link.attr('href', nextHref);
});
},
freeArticleHref(articleId) {
const id = encodeURIComponent(articleId || '');
if (!id) return 'javascript:void(0)';
if (this.isLocalDevHost()) return `mianfei.html?id=${id}`;
return `/mianfei/${id}.html`;
},
authHeaders() {
const token =
typeof ApiClient !== 'undefined' && ApiClient.getToken
? ApiClient.getToken()
: this.getAuthToken();
const tenantId =
typeof ApiClient !== 'undefined' && ApiClient.tenantId
? ApiClient.tenantId
: '936208';
const headers = { tenantId };
if (token) headers.Authorization = `Bearer ${token}`;
return headers;
},
requireAuth(callback) {
if (this.getAuthToken()) {
callback();
return;
}
layui.use('layer', function () {
layui.layer.confirm(
'请先登录',
{ btn: ['去登录', '取消'] },
function (index) {
layui.layer.close(index);
window.location.href = 'login.html';
}
);
});
},
getUserLevelText(user = {}) {
const truthy = (value) =>
value === true ||
value === 1 ||
value === '1' ||
String(value).toLowerCase() === 'true';
const expertStatus = String(
user.expertStatus || user.expert_status || user.expertLevel || user.expert_level || ''
).toLowerCase();
const isRegularExpert =
truthy(user.regularExpert) ||
truthy(user.isRegularExpert) ||
truthy(user.is_regular_expert) ||
truthy(user.formalExpert) ||
truthy(user.isFormalExpert) ||
/正式|regular|formal/.test(expertStatus);
const isInternExpert =
truthy(user.internExpert) ||
truthy(user.isInternExpert) ||
truthy(user.is_intern_expert) ||
truthy(user.trialExpert) ||
truthy(user.isTrialExpert) ||
truthy(user.is_expert) ||
truthy(user.isExpert) ||
truthy(user.expert) ||
/实习|intern|trial/.test(expertStatus);
if (isRegularExpert) return '正式专家';
if (isInternExpert) return '实习专家';
return '会员';
},
getExpertState(user = {}) {
const truthy = (value) =>
value === true ||
value === 1 ||
value === '1' ||
String(value).toLowerCase() === 'true';
const firstPresent = (...values) => {
for (const value of values) {
if (value !== undefined && value !== null && value !== '') return value;
}
return '';
};
const statusText = String(
firstPresent(
user.expertStatus,
user.expert_status,
user.expertApplyStatus,
user.expert_apply_status,
user.applyStatus,
user.apply_status,
user.auditStatus,
user.audit_status,
user.expertAuditStatus,
user.expert_audit_status
)
).toLowerCase();
const isExpert =
truthy(user.regularExpert) ||
truthy(user.isRegularExpert) ||
truthy(user.is_regular_expert) ||
truthy(user.formalExpert) ||
truthy(user.isFormalExpert) ||
truthy(user.internExpert) ||
truthy(user.isInternExpert) ||
truthy(user.is_intern_expert) ||
truthy(user.trialExpert) ||
truthy(user.isTrialExpert) ||
truthy(user.is_expert) ||
truthy(user.isExpert) ||
truthy(user.expert) ||
/实习|正式|intern|trial|regular|formal|expert/.test(statusText);
const isApplying =
!isExpert &&
([
'0',
'pending',
'reviewing',
'review',
'audit',
'auditing',
'apply',
'applying',
'submitted',
'wait',
'waiting',
].includes(statusText) ||
/审核|待审|申请中|pending|review|audit|applying|waiting/.test(statusText));
return {
isExpert,
isApplying,
status: statusText,
};
},
getUserLevelTextLegacy(user = {}) {
if (user.regularExpert || user.isExpert === '1' || user.expertStatus === 'regular') {
return '正式专家';
}
if (user.internExpert || user.isInternExpert || user.expertStatus === 'intern') {
return '实习专家';
}
return '会员';
},
applyTopbarUserLevel(user = {}) {
const $ = this.$;
const text = this.getUserLevelText(user);
$('#userLevel, #headerUserLevel').text(text);
$('.topbar__ubadge, .uc-role').text(text);
},
ensureTopbarSignInStyle() {
if (typeof document === 'undefined' || document.getElementById('topbar-signin-style')) return;
const style = document.createElement('style');
style.id = 'topbar-signin-style';
style.textContent = `
.topbar__btn--signin {
min-width: 58px;
height: 34px;
padding: 0 14px;
color: #ffd66e;
font-size: 13px;
font-weight: 700;
line-height: 34px;
background: rgba(255, 255, 255, .08);
border: 1px solid rgba(253, 185, 51, .42);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, .12);
}
.topbar__btn--signin:hover {
color: #fff3c4;
background: rgba(253, 185, 51, .16);
border-color: rgba(253, 185, 51, .62);
filter: none;
}
.topbar__btn--signin:disabled {
cursor: default;
opacity: .72;
}
.topbar__btn--signin.is-signed {
color: rgba(255, 255, 255, .72);
background: rgba(255, 255, 255, .06);
border-color: rgba(255, 255, 255, .16);
box-shadow: none;
}
`;
document.head.appendChild(style);
},
setTopbarSignInSigned(signed) {
if (typeof document === 'undefined') return;
const $ = this.$;
$('.topbar__btn--signin').each((index, button) => {
const $button = $(button);
$button
.prop('disabled', !!signed)
.toggleClass('is-signed', !!signed)
.text(signed ? '已签到' : '签到');
});
},
hasTopbarSignInStatusValue(data) {
if (typeof data === 'boolean') return true;
if (!data || typeof data !== 'object') return false;
const source = data.data && typeof data.data === 'object' ? data.data : data;
return [
'signedToday',
'todaySigned',
'isSignedToday',
'signed',
'isSigned',
'hasSigned',
'signIn',
'signInStatus',
].some((key) => source[key] !== undefined);
},
readTopbarSignInSigned(data) {
if (typeof data === 'boolean') return data;
if (!data || typeof data !== 'object') return false;
const source = data.data && typeof data.data === 'object' ? data.data : data;
const keys = [
'signedToday',
'todaySigned',
'isSignedToday',
'signed',
'isSigned',
'hasSigned',
'signIn',
'signInStatus',
];
const value = keys.map((key) => source[key]).find((item) => item !== undefined && item !== null && item !== '');
return value === true || value === 1 || value === '1' || String(value).toLowerCase() === 'true';
},
isAlreadySignedResponse(res) {
if (this.hasTopbarSignInStatusValue(res) && this.readTopbarSignInSigned(res)) return true;
const data = res && typeof res.data === 'object' ? res.data : {};
const text = [
res && res.msg,
res && res.message,
data.msg,
data.message,
].filter(Boolean).join(' ');
return /已签到|已签|已经签到|签到过/.test(text);
},
isSignInSuccessResponse(res) {
if (typeof ApiClient !== 'undefined' && ApiClient.isSuccess) return ApiClient.isSuccess(res);
return !!res && res.code === 0;
},
loadTopbarSignInStatus() {
if (typeof ApiClient === 'undefined' || !this.getToken()) return;
const url = ApiClient.API && ApiClient.API.mineSignInStatus
? ApiClient.API.mineSignInStatus
: '/api/web/mine/sign-in/status';
ApiClient.get(url, null, { headers: this.authHeaders() })
.then((res) => {
if (this.isAlreadySignedResponse(res)) {
this.setTopbarSignInSigned(true);
} else if (this.hasTopbarSignInStatusValue(res)) {
this.setTopbarSignInSigned(this.readTopbarSignInSigned(res));
}
})
.catch(() => {});
},
installTopbarUserCenterEntry() {
if (typeof document === 'undefined') return;
this.ensureTopbarSignInStyle();
const $ = this.$;
$('#loggedInBox').each((index, box) => {
const $box = $(box);
const $logout = $box.find('.topbar__btn--logout').first();
let $actions = $box.find('.topbar__actions').first();
if (!$actions.length) {
$actions = $('<div>', { class: 'topbar__actions' });
if ($logout.length) $actions.insertBefore($logout);
else $box.append($actions);
}
if ($logout.length && !$logout.parent().is($actions)) {
$logout.appendTo($actions);
}
if (!$box.find('.topbar__btn--signin').length) {
const $signIn = $('<button>', {
type: 'button',
class: 'topbar__btn topbar__btn--signin',
text: '签到',
});
$signIn.on('click', function () {
const $button = $(this);
if ($button.prop('disabled')) return;
$button.prop('disabled', true).text('签到中');
CommonUtil.signIn((res) => {
if (CommonUtil.isSignInSuccessResponse(res) || CommonUtil.isAlreadySignedResponse(res)) {
CommonUtil.setTopbarSignInSigned(true);
} else {
CommonUtil.setTopbarSignInSigned(false);
}
});
});
const $profile = $box.find('.topbar__profile').first();
if ($profile.length) $signIn.insertBefore($profile);
else $box.prepend($signIn);
}
if (!$actions.find('.topbar__btn--center').length) {
const $entry = $('<a>', {
href: 'usercenter.html',
class: 'topbar__btn topbar__btn--center',
text: '个人中心',
});
$actions.prepend($entry);
}
});
this.loadTopbarSignInStatus();
},
getAssetRoot() {
if (typeof window === 'undefined' || !window.location) return '../images/';
return /\/html\/[^/]*$/i.test(window.location.pathname) ? '../images/' : 'images/';
},
getCurrentPageName() {
if (typeof window === 'undefined' || !window.location) return '';
return (window.location.pathname.split('/').pop() || 'index.html').toLowerCase() || 'index.html';
},
isTopbarCaiNavPage() {
return ['cai.html', 'caiinfo.html'].includes(this.getCurrentPageName());
},
isTopbarEmptyNavPage() {
return [
'usercenter.html',
'mianfeishoucang.html',
'tuiguang.html',
'jine.html',
'chongzhi.html',
'tixian.html',
'tixianlist.html',
'fabumianfeiwenzhang.html',
'fufei.html',
'appdown.html',
'apply.html',
'open-browser.html',
'reg.html',
'repwd.html',
'social-callback.html',
'upgrade.html',
'wodefenxiao.html',
'yinsixieyi.html',
'yonghuxieyi.html',
].includes(this.getCurrentPageName());
},
getTopbarFeatureNavItems() {
const root = this.getAssetRoot();
return [
{ label: '双色球走势图', icon: root + 'trend-icons/ssq-trend.png' },
{ label: '基本走势', icon: root + 'trend-icons/basic-trend.png' },
{ label: '出号分布', icon: root + 'trend-icons/number-distribution.png' },
{ label: '红蓝走势', icon: root + 'trend-icons/red-blue-trend.png' },
{ label: '综合走势', icon: root + 'trend-icons/combined-trend.png' },
{ label: '蓝球走势', icon: root + 'trend-icons/blue-trend.png' },
{ label: '蓝球振幅', icon: root + 'trend-icons/blue-amplitude.png' },
{ label: '蓝球尾数', icon: root + 'trend-icons/blue-tail.png' },
{ label: '蓝球两数和', icon: root + 'trend-icons/blue-two-sum.png' },
];
},
ensureTopbarNavigationModeStyle() {
if (typeof document === 'undefined' || document.getElementById('topbar-nav-mode-style')) return;
const style = document.createElement('style');
style.id = 'topbar-nav-mode-style';
style.textContent = `
.topbar__nav.is-empty {
min-width: 0;
}
.topbar__nav--feature {
gap: 6px;
align-items: stretch;
}
.topbar__nav-link--feature {
flex-direction: column;
gap: 4px;
min-width: 82px;
padding: 5px 9px;
line-height: 1.15;
}
.topbar__nav-feature-icon {
width: 34px;
height: 34px;
flex: 0 0 34px;
object-fit: contain;
display: block;
}
.topbar__nav-feature-text {
display: inline-block;
font-size: 12px;
text-align: center;
white-space: nowrap;
}
.topbar-dev-modal {
position: relative;
box-sizing: border-box;
width: 100%;
padding: 24px 24px 22px;
text-align: center;
overflow: hidden;
background:
radial-gradient(circle at 22% 16%, rgba(253, 185, 51, .22), transparent 28%),
linear-gradient(180deg, #f7fbff 0%, #ffffff 76%);
border: 1px solid rgba(16, 43, 106, .08);
border-radius: 12px;
}
.topbar-dev-modal::before {
content: "";
position: absolute;
left: 0;
right: 0;
top: 0;
height: 5px;
background: linear-gradient(90deg, #102b6a, #2478d7 58%, #fdb933);
}
.topbar-dev-badge {
display: inline-flex;
align-items: center;
justify-content: center;
height: 24px;
padding: 0 10px;
margin-bottom: 12px;
border-radius: 999px;
color: #102b6a;
font-size: 12px;
font-weight: 700;
background: rgba(16, 43, 106, .08);
}
.topbar-dev-modal img {
display: block;
width: 118px;
height: 118px;
object-fit: contain;
margin: 0 auto 14px;
padding: 12px;
border-radius: 24px;
background: rgba(255, 255, 255, .82);
box-shadow: 0 12px 28px rgba(16, 43, 106, .12);
}
.topbar-dev-modal strong {
display: block;
color: #102b6a;
font-size: 20px;
line-height: 1.4;
}
.topbar-dev-modal span {
display: block;
margin-top: 7px;
color: #7b8495;
font-size: 14px;
}
.layui-layer.topbar-dev-layer {
border-radius: 12px;
overflow: hidden;
background: transparent;
box-shadow: 0 20px 54px rgba(11, 31, 73, .24);
}
.layui-layer.topbar-dev-layer .layui-layer-content {
overflow: visible;
background: transparent;
}
.topbar-dev-confirm {
display: inline-flex;
align-items: center;
justify-content: center;
height: 34px;
min-width: 108px;
margin-top: 18px;
padding: 0 18px;
color: #fff;
font-size: 14px;
font-weight: 700;
border: 0;
outline: 0;
cursor: pointer;
border-radius: 999px;
background: linear-gradient(135deg, #102b6a, #2478d7);
box-shadow: 0 8px 18px rgba(16, 43, 106, .2);
}
.topbar-dev-confirm:hover {
filter: brightness(1.05);
}
.topbar-dev-confirm:active {
transform: translateY(1px);
}
@media (max-width: 768px) {
.topbar__nav-link--feature {
min-width: 68px;
padding: 4px 7px;
}
.topbar__nav-feature-icon {
width: 28px;
height: 28px;
flex-basis: 28px;
}
.topbar__nav-feature-text {
font-size: 11px;
}
}
`;
document.head.appendChild(style);
},
showTopbarFeatureComingSoon(item) {
const root = this.getAssetRoot();
const image = root + 'trend-icons/dev-coming.png';
const content =
'<div class="topbar-dev-modal">' +
'<div class="topbar-dev-badge">功能预告</div>' +
'<img src="' + this.escapeHtml(image) + '" alt="开发中" />' +
'<strong>开发中敬请期待</strong>' +
'<span>' + this.escapeHtml(item.label || '') + '</span>' +
'<button type="button" class="topbar-dev-confirm">我知道了</button>' +
'</div>';
if (typeof layui !== 'undefined' && layui.layer) {
const index = layui.layer.open({
type: 1,
title: false,
closeBtn: 0,
shadeClose: true,
area: ['348px', 'auto'],
skin: 'topbar-dev-layer',
content,
end: () => {
this.$('#navLotteryItems .topbar__nav-link--feature').removeClass('is-active');
},
});
setTimeout(() => {
this.$('.topbar-dev-confirm').off('click.topbarDev').on('click.topbarDev', () => layui.layer.close(index));
}, 0);
} else {
alert('开发中敬请期待');
}
},
renderTopbarEmptyNav() {
const $ = this.$;
const $nav = $('#navLotteryItems');
if (!$nav.length) return;
this.ensureTopbarNavigationModeStyle();
if ($nav.attr('data-nav-mode') === 'empty' && !$nav.children().length) return;
$nav
.attr('data-nav-mode', 'empty')
.removeClass('topbar__nav--feature')
.addClass('is-empty')
.empty();
},
renderTopbarFeatureNav() {
const $ = this.$;
const $nav = $('#navLotteryItems');
if (!$nav.length) return;
const items = this.getTopbarFeatureNavItems();
if (
$nav.attr('data-nav-mode') === 'feature' &&
$nav.find('.topbar__nav-link--feature').length === items.length
) {
return;
}
this.ensureTopbarNavigationModeStyle();
$nav
.attr('data-nav-mode', 'feature')
.removeClass('is-empty')
.addClass('topbar__nav--feature')
.empty();
items.forEach((item) => {
$('<a>', {
href: 'javascript:void(0)',
class: 'topbar__nav-link topbar__nav-link--feature',
'data-nav-label': item.label,
title: item.label,
})
.append($('<img>', {
src: item.icon,
alt: '',
class: 'topbar__nav-feature-icon',
'aria-hidden': 'true',
}))
.append($('<span>', {
class: 'topbar__nav-feature-text',
text: item.label,
}))
.on('click', (event) => {
event.preventDefault();
event.stopPropagation();
if (event.stopImmediatePropagation) event.stopImmediatePropagation();
$('#navLotteryItems .topbar__nav-link--feature').removeClass('is-active');
this.showTopbarFeatureComingSoon(item);
})
.appendTo($nav);
});
},
applyTopbarNavigationMode() {
if (typeof document === 'undefined' || this.isTopbarCaiNavPage()) return;
if (this.isTopbarEmptyNavPage()) {
this.renderTopbarEmptyNav();
return;
}
this.renderTopbarFeatureNav();
},
installTopbarNavigationMode() {
if (typeof document === 'undefined' || this.isTopbarCaiNavPage() || window.__TOPBAR_NAV_MODE_INSTALLED__) return;
window.__TOPBAR_NAV_MODE_INSTALLED__ = true;
const run = () => this.applyTopbarNavigationMode();
run();
const nav = document.getElementById('navLotteryItems');
if (!nav || typeof MutationObserver === 'undefined') return;
const observer = new MutationObserver(() => {
window.clearTimeout(window.__TOPBAR_NAV_MODE_TIMER__);
window.__TOPBAR_NAV_MODE_TIMER__ = window.setTimeout(run, 0);
});
observer.observe(nav, { childList: true, subtree: true });
},
applyWebConfigs(configs) {
const $ = this.$;
this.ensureGlobalFooterLayout();
if (!configs) return;
if (!Array.isArray(configs)) {
configs = Object.keys(configs).map((key, index) => ({
type: key,
content: configs[key],
sort: index,
}));
}
configs = configs
.filter((item) => item && item.isEnabled !== '0' && item.isEnabled !== 0 && item.isEnabled !== false)
.map((item, index) => ({
...item,
sort: item.sort === undefined || item.sort === null ? index : Number(item.sort),
}));
const copyrightItems = [];
const footerContactItems = [];
configs.forEach((item) => {
const isFooterContact = this.isFooterContactConfig(item);
if (isFooterContact) footerContactItems.push(item);
if (item.type === 'web_title') {
$('#page-title').text(item.content || '');
if (item.content) document.title = item.content;
} else if (item.type === 'web_keywords') {
this.setMeta('keywords', item.content || '');
} else if (item.type === 'web_introduce' || item.type === 'web_description') {
this.setMeta('description', item.content || '');
} else if (item.type === 'web_icon') {
this.setIcon(item.content || '');
} else if (item.type === 'web_copyright' && !isFooterContact) {
copyrightItems.push(item);
}
});
this.applyFooterCopyright(copyrightItems);
this.renderFooterContact(footerContactItems);
},
ensureGlobalFooterLayout() {
if (typeof document === 'undefined') return;
const $ = this.$;
$('.site-footer__inner').each((index, footerInner) => {
const $inner = $(footerInner);
let $layout = $inner.find('.site-footer__layout').first();
if ($layout.length) {
let $brand = $layout.find('.site-footer__brand').first();
if (!$brand.length) {
$brand = $('<div>', { class: 'site-footer__brand' }).prependTo($layout);
}
let $contact = $layout.find('.site-footer__contact').first();
if (!$contact.length) {
$contact = this.buildFooterContactShell($);
$layout.append($contact);
} else if (!$contact.find('.site-footer__qr-grid').length) {
$contact.append($('<div>', { class: 'site-footer__qr-grid' }));
}
return;
}
const $desc = $inner.find('#additional-info').first().detach();
const $logo = $inner.find('.site-footer__logo').first().detach();
const $meta = $inner.find('.site-footer__meta').first().detach();
const $copyright = $inner.find('#copyright-info').first().detach();
const $brand = $('<div>', { class: 'site-footer__brand' });
if ($logo.length) {
$brand.append($logo);
} else {
$brand.append($('<img>', {
src: '../images/home-logo.png',
alt: '神彩算',
class: 'site-footer__logo',
}));
}
const $legal = $meta.length
? $meta.addClass('site-footer__legal')
: $('<div>', { class: 'site-footer__legal site-footer__meta' });
$brand.append($legal);
if ($copyright.length) {
$brand.append($copyright.addClass('site-footer__copyright'));
} else {
$brand.append($('<p>', {
id: 'copyright-info',
class: 'site-footer__copyright',
text: '© 2014-2026 神彩算 版权所有',
}));
}
if ($desc.length) {
$brand.append($desc.addClass('site-footer__desc'));
} else {
$brand.append($('<p>', {
id: 'additional-info',
class: 'site-footer__desc',
text: '本站内容仅供数据分析参考,不构成任何投注建议',
}));
}
$layout = $('<div>', { class: 'site-footer__layout' });
$layout.append($brand);
$layout.append(this.buildFooterContactShell($));
$inner.empty().append($layout);
});
},
buildFooterContactShell($) {
return $('<div>', { class: 'site-footer__contact' })
.append(
$('<div>', { class: 'site-footer__contact-head' })
.append($('<span>', { class: 'site-footer__contact-kicker', text: 'Contact' }))
.append($('<strong>', { text: '联系客服' }))
.append($('<span>', { text: '微信 / QQ 扫码咨询' }))
)
.append(
$('<div>', { class: 'site-footer__qr-grid' })
.append(
$('<div>', { class: 'site-footer__qr-card' })
.append($('<img>', { src: '../public/img/kfwx.jpg', alt: '客服微信二维码' }))
.append($('<span>', { text: '客服微信' }))
)
.append(
$('<div>', { class: 'site-footer__qr-card' })
.append($('<img>', { src: '../public/img/kfqq.jpg', alt: '客服QQ二维码' }))
.append($('<span>', { text: '客服QQ' }))
)
);
},
isFooterContactConfig(item) {
if (!item) return false;
const text = [
item.type,
item.title,
item.name,
item.label,
item.remark,
item.content,
]
.filter(Boolean)
.join(' ');
return /(客服|联系|微信|QQ|wechat|weixin|wx|qq|qrcode|qr|二维码)/i.test(text) &&
Boolean(this.extractFooterImageUrl(item));
},
extractFooterImageUrl(item) {
if (!item) return '';
const fields = [
'content',
'url',
'image',
'img',
'src',
'qrCode',
'qrcode',
'qrCodeUrl',
'qrcodeUrl',
'wechatQr',
'wechatQrcode',
'wechatQrCode',
'wechatQrCodeUrl',
'qqQr',
'qqQrcode',
'qqQrCode',
'qqQrCodeUrl',
'ossUrl',
'fileUrl',
];
const values = [];
fields.forEach((field) => {
if (item[field]) values.push(item[field]);
});
if (typeof item.content === 'string' && item.content.trim().charAt(0) === '{') {
try {
const parsed = JSON.parse(item.content);
fields.forEach((field) => {
if (parsed && parsed[field]) values.push(parsed[field]);
});
} catch (error) {
// Non-JSON content is valid for plain image URLs.
}
}
return values
.map((value) => String(value || '').trim())
.find((value) => /^(https?:)?\/\//i.test(value) || /^data:image\//i.test(value) || /\.(png|jpe?g|webp|gif|svg)(\?.*)?$/i.test(value)) || '';
},
renderFooterContact(items) {
const $ = this.$;
const $contact = $('.site-footer__contact');
if (!$contact.length) return;
const qrItems = [];
(Array.isArray(items) ? items : []).forEach((item) => {
const src = this.extractFooterImageUrl(item);
if (!src) return;
const text = [item.type, item.title, item.name, item.label, item.remark, item.content].filter(Boolean).join(' ');
const isQQ = /QQ|qq/.test(text);
const isWechat = /微信|wechat|weixin|wx/i.test(text);
qrItems.push({
src,
title: isQQ ? '客服QQ' : isWechat ? '客服微信' : '客服二维码',
});
});
const uniqueItems = qrItems
.filter((item, index, array) => array.findIndex((target) => target.src === item.src) === index)
.sort((a, b) => (a.title === '客服微信' ? -1 : 0) - (b.title === '客服微信' ? -1 : 0));
const displayItems = uniqueItems.length
? uniqueItems.slice(0, 2)
: [
{
src: '../public/img/kfwx.jpg',
title: '客服微信',
},
{
src: '../public/img/kfqq.jpg',
title: '客服QQ',
},
];
const $grid = $contact.find('.site-footer__qr-grid');
if (!$grid.length) return;
$grid.empty();
displayItems.forEach((item) => {
$('<div>', { class: 'site-footer__qr-card' })
.append($('<img>', { src: item.src, alt: item.title + '二维码' }))
.append($('<span>', { text: item.title }))
.appendTo($grid);
});
$contact.removeClass('is-empty');
},
applyFooterCopyright(items) {
const $ = this.$;
if (!Array.isArray(items) || !items.length) return;
const list = items
.filter((item) => item && item.content)
.sort((a, b) => (Number(a.sort) || 0) - (Number(b.sort) || 0));
if (!list.length) return;
const copyrightItem = list.find((item) => /©|版权|copyright/i.test(String(item.content || ''))) || null;
const descItem =
list.find((item) => /本站|历史数据|数据分析|投注建议|不涉及|仅供/i.test(String(item.content || ''))) ||
list.find((item) => item !== copyrightItem && !/ICP备|工信部|公网安备|公安/.test(String(item.content || ''))) ||
null;
if (copyrightItem?.content) $('#copyright-info').text(copyrightItem.content.trim());
if (descItem?.content) $('#additional-info').text(descItem.content.trim());
const $meta = $('.site-footer__legal, .site-footer__meta').first();
if (!$meta.length) return;
$meta.empty();
list
.forEach((item) => {
const content = String(item.content || '').trim();
if (!content) return;
const isIcp = /ICP备|工信部/.test(content);
const isPolice = /公网安备|公安/.test(content);
if (isIcp) {
$('<a>', {
id: 'beian',
href: 'https://beian.miit.gov.cn/',
target: '_blank',
class: 'index-style-02',
text: content,
}).appendTo($meta);
} else if (isPolice) {
$('<span>', {
id: 'police-record',
text: content,
}).appendTo($meta);
}
});
if (!$meta.children().length && descItem?.content) {
$('<span>', {
id: 'additional-info',
text: descItem.content.trim(),
}).appendTo($meta);
}
},
setMeta(name, content) {
if (!content) return;
let meta = document.querySelector(`meta[name="${name}"]`);
if (!meta) {
meta = document.createElement('meta');
meta.setAttribute('name', name);
document.head.appendChild(meta);
}
meta.setAttribute('content', content);
},
setIcon(url) {
if (!url) return;
let icon = document.querySelector('link[rel="icon"]');
if (!icon) {
icon = document.createElement('link');
icon.setAttribute('rel', 'icon');
document.head.appendChild(icon);
}
icon.setAttribute('href', url);
},
isHomePageDom() {
if (typeof document === 'undefined') return false;
return Boolean(
document.querySelector('#bannerBox, #resultsGrid, #articleGroups, #lotteryNavGrid')
);
},
installCaiSingleLotteryMode() {
if (typeof document === 'undefined' || typeof ApiClient === 'undefined') return;
const $ = this.$;
const $lotteryFilter = $('#lotteryFilter');
if (!$lotteryFilter.length) return;
$lotteryFilter.closest('.cxz-filter-row').hide();
const $nav = $('#navLotteryItems');
if (!$nav.length) return;
const currentCode = new URLSearchParams(window.location.search).get('id') || 'ssq';
ApiClient.loadMenuCompat()
.then((res) => {
if (!res || res.code !== 0 || !Array.isArray(res.data)) return;
$nav.find('.topbar__nav-link').remove();
res.data.forEach((item) => {
const code = item.suoxie || item.code || '';
if (!code) return;
const activeClass = String(code) === currentCode ? ' is-active' : '';
$nav.append(
'<a href="cai.html?id=' +
encodeURIComponent(code) +
'" class="topbar__nav-link' +
activeClass +
'">' +
this.escapeHtml(item.name || code) +
'</a>'
);
});
})
.catch(() => {});
},
asArray(value) {
if (typeof ApiClient !== 'undefined' && ApiClient.asArray) return ApiClient.asArray(value);
if (Array.isArray(value)) return value;
if (!value || typeof value !== 'object') return [];
if (Array.isArray(value.records)) return value.records;
if (Array.isArray(value.rows)) return value.rows;
if (Array.isArray(value.list)) return value.list;
return [];
},
pickData(result, fallback) {
if (!result || typeof result !== 'object') return fallback;
return result.data === undefined || result.data === null ? fallback : result.data;
},
loadPageConfig(options = {}) {
if (typeof ApiClient === 'undefined') return;
this.installUnifiedPagination();
this.ensureGlobalFooterLayout();
this.installWechatLoginEntry();
this.installWechatAccountBindEntry();
this.installCaiSingleLotteryMode();
this.installTopbarNavigationMode();
const loadHomeData = options.home !== false && this.isHomePageDom();
const request = loadHomeData ? this.loadHomePageData() : this.loadBasePageData(options);
this.applyCaiExpertListLabels();
request
.then((result) => {
const data = result.data || {};
if (data.webConfigs) this.applyWebConfigs(data.webConfigs);
else if (data.siteConfig) this.applyWebConfigs(data.siteConfig);
else if (data.config) this.applyWebConfigs(data.config);
else if (result.data) this.applyWebConfigs(result.data);
if (!loadHomeData && options.ads !== false && data.adList) {
this.processAndRenderAds(data.adList, options.adPositions);
}
this.applyCaiExpertListLabels();
this.installWechatLoginEntry();
this.installWechatAccountBindEntry();
if (options.links !== false) this.loadLinks();
if (typeof options.onSuccess === 'function') options.onSuccess(result);
})
.catch((error) => {
if (typeof options.onError === 'function') options.onError(error);
});
},
installWechatLoginEntry() {
if (typeof window === 'undefined' || window.__WECHAT_LOGIN_ENTRY_INSTALLED__) return;
const $entry = this.$('.wxdl');
if (!$entry.length || typeof ApiClient === 'undefined') return;
window.__WECHAT_LOGIN_ENTRY_INSTALLED__ = true;
$entry.find('a').attr({
href: 'javascript:void(0)',
title: '微信登录',
});
$entry.on('click.wechatLogin', 'a, .wx_Box', (event) => {
event.preventDefault();
event.stopPropagation();
this.startWechatLogin();
});
},
getWechatLoginDomain() {
if (typeof window === 'undefined' || !window.location || window.location.protocol === 'file:') {
return window.CURRENT_CONFIG && window.CURRENT_CONFIG.SITE_DOMAIN
? window.CURRENT_CONFIG.SITE_DOMAIN
: 'http://47.108.24.205:9100';
}
return window.location.origin;
},
extractWechatAuthUrl(res) {
const data = res && res.data;
if (typeof data === 'string') return data;
if (data && typeof data === 'object') {
return data.url || data.authUrl || data.authorizeUrl || data.redirectUrl || data.loginUrl || '';
}
return '';
},
startWechatLogin() {
const $ = this.$;
const layer = typeof layui !== 'undefined' && layui.layer;
const $agreement = $('#checkbox, #agreementCheckbox').first();
if ($agreement.length && !$agreement.prop('checked')) {
if (layer) layer.msg('请先阅读并同意用户协议和隐私政策', { icon: 2 });
return;
}
const $loading = $('#loadingOverlay');
const $button = $('.wxdl');
$loading.addClass('is-active');
$button.addClass('is-loading');
ApiClient.get(
ApiClient.API.authWechatOpen,
{ domain: this.getWechatLoginDomain() },
{
publicRequest: true,
headers: { clientid: ApiClient.CLIENT_ID },
}
)
.then((res) => {
if (!ApiClient.isSuccess(res)) {
throw new Error((res && res.msg) || '微信授权地址获取失败');
}
const authUrl = this.extractWechatAuthUrl(res);
if (!authUrl) throw new Error('后端未返回微信授权地址');
window.location.href = authUrl;
})
.catch((error) => {
const message = error.message || '微信登录暂不可用,请稍后再试';
if (layer) layer.msg(message, { icon: 2 });
else alert(message);
})
.finally(() => {
$loading.removeClass('is-active');
$button.removeClass('is-loading');
});
},
installWechatAccountBindEntry() {
if (typeof window === 'undefined') return;
const button = document.getElementById('wechat-bind-button');
if (!button || typeof ApiClient === 'undefined') return;
this.normalizeWechatAccountBindPanel();
if (window.__WECHAT_ACCOUNT_BIND_ENTRY_INSTALLED__) return;
window.__WECHAT_ACCOUNT_BIND_ENTRY_INSTALLED__ = true;
button.setAttribute('title', '绑定微信');
button.addEventListener(
'click',
(event) => {
event.preventDefault();
event.stopPropagation();
if (event.stopImmediatePropagation) event.stopImmediatePropagation();
this.startWechatAccountBind();
},
true
);
this.observeWechatAccountBindPanel();
},
isWechatAccountBindPanelBound() {
const $ = this.$;
const descText = ($('#wechat-bind-desc').text() || '').trim();
const buttonText = ($('#wechat-bind-button').text() || '').trim();
return (
this.hasWechatAccountBindSuccessMark() ||
$('#wechat-bind-card').hasClass('is-bound') ||
descText.indexOf('已绑定') > -1 ||
buttonText.indexOf('已绑定') > -1
);
},
hasWechatAccountBindSuccessMark() {
if (typeof window === 'undefined') return false;
try {
return sessionStorage.getItem('wechatAccountBindSuccess') === '1';
} catch (e) {
return false;
}
},
applyWechatAccountBindBoundPanel() {
const $button = this.$('#wechat-bind-button');
if (!$button.length) return;
this.$('#wechat-bind-desc').text('微信已绑定');
this.$('#wechat-bind-card').removeClass('is-pending is-ready').addClass('is-bound');
$button
.text('已绑定')
.attr('title', '微信已绑定')
.removeClass('is-pending')
.addClass('is-bound')
.prop('disabled', true);
},
normalizeWechatAccountBindPanel() {
const $button = this.$('#wechat-bind-button');
if (!$button.length) return;
if (this.hasWechatAccountBindSuccessMark()) {
this.applyWechatAccountBindBoundPanel();
return;
}
if (this.isWechatAccountBindPanelBound()) return;
this.$('#wechat-bind-desc').text('未绑定微信');
this.$('#wechat-bind-card').removeClass('is-pending').addClass('is-ready');
$button
.text('绑定微信')
.attr('title', '绑定微信')
.removeClass('is-bound is-pending')
.prop('disabled', false);
},
observeWechatAccountBindPanel() {
if (typeof window === 'undefined' || window.__WECHAT_ACCOUNT_BIND_OBSERVER__) return;
const target = document.getElementById('wechat-bind-card') || document.getElementById('wechat-bind-button');
if (!target || typeof MutationObserver === 'undefined') return;
window.__WECHAT_ACCOUNT_BIND_OBSERVER__ = new MutationObserver(() => {
window.clearTimeout(window.__WECHAT_ACCOUNT_BIND_NORMALIZE_TIMER__);
window.__WECHAT_ACCOUNT_BIND_NORMALIZE_TIMER__ = window.setTimeout(() => {
this.normalizeWechatAccountBindPanel();
}, 0);
});
window.__WECHAT_ACCOUNT_BIND_OBSERVER__.observe(target, {
attributes: true,
childList: true,
subtree: true,
});
},
rememberWechatAccountBindMode() {
if (typeof window === 'undefined') return;
const returnUrl =
`${window.location.pathname}${window.location.search}${window.location.hash}` || '/html/usercenter.html';
const pairs = {
wechatSocialMode: 'account_bind',
wechatSocialReturnUrl: returnUrl,
wechatSocialStartedAt: String(Date.now()),
};
Object.keys(pairs).forEach((key) => {
try {
sessionStorage.setItem(key, pairs[key]);
} catch (e) {}
try {
localStorage.setItem(key, pairs[key]);
} catch (e) {}
});
},
startWechatAccountBind() {
const layer = typeof layui !== 'undefined' && layui.layer;
const token = typeof ApiClient !== 'undefined' && ApiClient.token ? ApiClient.token() : '';
if (!token) {
if (layer) layer.msg('请先登录后再绑定微信', { icon: 2 });
return;
}
const $button = this.$('#wechat-bind-button');
this.rememberWechatAccountBindMode();
$button.addClass('is-loading').prop('disabled', true);
ApiClient.get(ApiClient.API.authWechatOpen, { domain: this.getWechatLoginDomain() })
.then((res) => {
if (!ApiClient.isSuccess(res)) {
throw new Error((res && res.msg) || '微信授权地址获取失败');
}
const authUrl = this.extractWechatAuthUrl(res);
if (!authUrl) throw new Error('后端未返回微信授权地址');
window.location.href = authUrl;
})
.catch((error) => {
const message = error.message || '微信绑定暂不可用,请稍后再试';
if (layer) layer.msg(message, { icon: 2 });
else alert(message);
})
.finally(() => {
$button.removeClass('is-loading').prop('disabled', false);
});
},
installUnifiedPagination() {
if (typeof window === 'undefined' || typeof document === 'undefined') return;
this.injectPaginationStyles();
this.patchLaypageDefaults();
this.applyCaiinfoCompat();
},
applyCaiinfoCompat() {
if (window.__cxzCaiinfoDisableCompat) return;
if (window.__cxzCaiinfoCompatScheduled) return;
if (!/\/caiinfo\.html(?:$|\?)/.test(window.location.pathname + window.location.search)) return;
window.__cxzCaiinfoCompatScheduled = true;
const run = () => {
const $ = window.layui && layui.$ ? layui.$ : window.jQuery;
if (!$ || !document.getElementById('articleBody')) return;
const escapeHtml = (value) =>
String(value || '')
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
const escapeJs = (value) => escapeHtml(value).replace(/\\/g, '\\\\');
const isMasked = (value) => {
const text = String(value || '').trim();
return (
!text ||
/\*/.test(text) ||
text.indexOf('购买后可见') >= 0 ||
text.indexOf('登录后购买可见') >= 0 ||
text.indexOf('待发布') >= 0
);
};
const normalizeRecommend = (value) =>
String(value || '')
.replace(/[,、]/g, ',')
.replace(/[|]/g, '+')
.replace(/\s+/g, ',')
.replace(/,+/g, ',')
.replace(/\+,/g, '+')
.replace(/,\+/g, '+')
.replace(/^,|,$/g, '');
const formatIssueText = (issue) => {
const value = String(issue || '').trim();
return /^\d{7,}$/.test(value) ? value.slice(-3) : value;
};
const splitNumbers = (value) => {
const text = normalizeRecommend(value);
if (!text || isMasked(text)) return [];
return text
.split('+')
.map((group) => group.split(',').map((num) => num.trim()).filter(Boolean))
.filter((group) => group.length);
};
const normalizeNumberKey = (value) => {
const text = String(value || '').trim();
return /^\d+$/.test(text) ? String(Number(text)) : text;
};
const toNumberSet = (values) =>
new Set(
values.flatMap((num) => {
const text = String(num);
return [text, text.padStart(2, '0'), normalizeNumberKey(text)];
})
);
const getRecommendGroupSeparator = (value) => (String(value || '').includes('|') ? '|' : '+');
const splitRecommendValues = (value) => splitNumbers(value).flat();
const getArticleRecommendValue = (item) =>
(item && (item.predictedCode || item.recommendCode || item.latestRecommend || item.recommend || item.content)) || '';
const getArticleIssueValue = (item) =>
(item && (item.issue || item.latestIssue || item.expect || item.period)) || '';
const isMaskedRecommendText = (value) => isMasked(value);
const isTruthyFlag = (value) =>
value === true || value === 1 || value === '1' || String(value).toLowerCase() === 'true';
const isArticlePrePurchased = (item) =>
Boolean(
item &&
(isTruthyFlag(item.prePurchased) ||
isTruthyFlag(item.pre_purchased) ||
isTruthyFlag(item.prepaid) ||
isTruthyFlag(item.prepaidArticle))
);
const isArticlePrePurchase = (item) =>
Boolean(
item &&
(isTruthyFlag(item.prePurchase) ||
isTruthyFlag(item.pre_purchase) ||
isTruthyFlag(item.canPrePurchase) ||
isTruthyFlag(item.preOrder))
);
const getArticleCodeNumbers = (item, fields) => {
let value = '';
fields.some((field) => {
if (item && item[field] !== undefined && item[field] !== null && item[field] !== '') {
value = item[field];
return true;
}
return false;
});
if (Array.isArray(value)) {
return value.flatMap((entry) =>
splitRecommendValues(
entry && typeof entry === 'object'
? entry.code || entry.value || entry.number || entry.result || ''
: entry
)
);
}
return splitRecommendValues(value);
};
const getCodeResultNumbers = (item, correctValue) =>
(typeof ApiClient !== 'undefined' ? ApiClient.asArray(item && item.codeResults) : [])
.filter((result) => result && typeof result === 'object' && result.correct === correctValue)
.flatMap((result) => splitRecommendValues(result.code || result.value || result.number || result.result || ''));
const getArticleHitNumbers = (item) => {
const fields = [
'hitCodes',
'hitCode',
'hitNumbers',
'hits',
'rightCodes',
'rightCode',
'rightNumbers',
'correctCodes',
'correctCode',
'correctNumbers',
];
const hitNumbers = getArticleCodeNumbers(item, fields);
return hitNumbers.length ? hitNumbers : getCodeResultNumbers(item, true);
};
const getArticleMissedNumbers = (item) => {
const fields = [
'missCodes',
'missCode',
'missNumbers',
'missedCodes',
'missedCode',
'missedNumbers',
'wrongCodes',
'wrongCode',
'wrongNumbers',
'errorCodes',
'errorCode',
'errorNumbers',
];
const missedNumbers = getArticleCodeNumbers(item, fields);
return missedNumbers.length ? missedNumbers : getCodeResultNumbers(item, false);
};
const getCodeResultCorrectGroups = (item, recommendGroups) => {
const codeResults = typeof ApiClient !== 'undefined' ? ApiClient.asArray(item && item.codeResults) : [];
if (!codeResults.length || !recommendGroups || !recommendGroups.length) return [];
const marks = codeResults.map((result) => {
if (!result || typeof result !== 'object') return null;
return result.correct === true ? true : result.correct === false ? false : null;
});
if (!marks.some((mark) => mark === true || mark === false)) return [];
const recommendCount = recommendGroups.reduce((total, group) => total + group.length, 0);
if (marks.length < recommendCount) return [];
let offset = 0;
return recommendGroups.map((group) => {
const groupMarks = marks.slice(offset, offset + group.length);
offset += group.length;
return groupMarks;
});
};
const getCodeResultMarkerGroups = (item, correctValue) => {
const codeResults = typeof ApiClient !== 'undefined' ? ApiClient.asArray(item && item.codeResults) : [];
if (!codeResults.length) return [];
return codeResults
.map((result) => {
if (Array.isArray(result)) return [];
if (result && typeof result === 'object' && result.correct !== correctValue) return [];
const value =
result && typeof result === 'object'
? result.code || result.value || result.number || result.result || ''
: result;
return String(value || '').trim() ? [String(value).trim()] : [];
})
.filter((group) => group.length > 0);
};
const getArticleHitMarkerGroups = (item, recommendGroups) => {
if (!recommendGroups || !recommendGroups.length) return [];
const hitNumbers = getArticleHitNumbers(item);
if (hitNumbers.length === recommendGroups.length) return hitNumbers.map((num) => splitRecommendValues(num));
const codeResultGroups = getCodeResultMarkerGroups(item, true);
return codeResultGroups.length === recommendGroups.length ? codeResultGroups : [];
};
const getArticleMissedMarkerGroups = (item, recommendGroups) => {
if (!recommendGroups || recommendGroups.length <= 1) return [];
const missedNumbers = getArticleMissedNumbers(item);
if (missedNumbers.length === recommendGroups.length) return missedNumbers.map((num) => splitRecommendValues(num));
const codeResultGroups = getCodeResultMarkerGroups(item, false);
return codeResultGroups.length === recommendGroups.length ? codeResultGroups : [];
};
const isWinStatusText = (value) => /全对|正确|命中|中奖|/.test(String(value || ''));
const isLoseStatusText = (value) => /全错|错误|/.test(String(value || ''));
const isArticleDrawn = (item, openCode) => {
if (openCode && String(openCode).trim()) return true;
const text = getExplicitStatusText(item);
if (/正确|错误|已开奖|开奖号|开奖/.test(text) && !/未开奖|待开奖/.test(text)) return true;
if (isWinStatusText(text) || isLoseStatusText(text)) return true;
return Boolean(
item &&
(item.isDrawn === true ||
item.drawn === true ||
item.opened === true ||
item.openStatus === 1 ||
item.openStatus === '1' ||
item.drawStatus === 1 ||
item.drawStatus === '1')
);
};
const isLatestArticle = (item, list) => {
if (item && isTruthyFlag(item.isLatest)) return true;
const currentId = getArticleId(item);
const firstId = getArticleId(Array.isArray(list) ? list[0] : null);
if (currentId && firstId) return String(currentId) === String(firstId);
const currentIssue = Number(getArticleIssueValue(item));
const issues = (Array.isArray(list) ? list : [])
.map((row) => Number(getArticleIssueValue(row)))
.filter((issue) => Number.isFinite(issue));
return issues.length > 0 && Number.isFinite(currentIssue) && currentIssue === Math.max(...issues);
};
const isArticleMasked = (item, recommend) =>
Boolean(
item &&
(item.masked === true ||
item.masked === 1 ||
item.masked === '1' ||
item.masked === 'true' ||
isMaskedRecommendText(recommend))
);
const canShowArticleRecommend = (item, isLatestIssue, hasDrawResult, unlockedByVisibleRecommend, recommend) => {
if (!isArticleMasked(item, recommend)) return true;
if (hasDrawResult) return true;
if (!isLatestIssue) return true;
return Boolean((ApiClient.getToken ? ApiClient.getToken() : this.getToken()) && (isPurchased(item) || unlockedByVisibleRecommend));
};
const shouldShowBuyButton = (item, isLatestIssue, hasDrawResult, purchased, recommend) =>
Boolean(isLatestIssue && !hasDrawResult && isArticleMasked(item, recommend) && !purchased);
const shouldShowPrePurchaseButton = (item, purchased) =>
Boolean(
!purchased &&
!isArticlePrePurchased(item) &&
(isArticlePrePurchase(item) || (!getArticleId(item) && getExplicitStatusText(item) === '待发布'))
);
const getRecommendLockedText = () =>
ApiClient.getToken && ApiClient.getToken() ? '购买后可见' : '登录后购买可见';
const applyRecommendHitOptions = (options, item, statusText) => {
const recommendGroups = splitNumbers(getArticleRecommendValue(item));
const codeResultGroupMarks = getCodeResultCorrectGroups(item, recommendGroups);
const hitGroups = getArticleHitMarkerGroups(item, recommendGroups);
const missedGroups = getArticleMissedMarkerGroups(item, recommendGroups);
const hitNumbers = hitGroups.length ? hitGroups.flat() : getArticleHitNumbers(item);
const missedNumbers = missedGroups.length ? missedGroups.flat() : getArticleMissedNumbers(item);
const isFinalStatus = isWinStatusText(statusText) || isLoseStatusText(statusText);
options.codeResultMarks = codeResultGroupMarks.length ? codeResultGroupMarks : null;
options.hitSet = codeResultGroupMarks.length || (hitNumbers.length > 0 && isFinalStatus) ? toNumberSet(hitNumbers) : new Set();
options.hitGroupSets =
hitGroups.length === recommendGroups.length ? hitGroups.map((group) => toNumberSet(group)) : null;
options.wrongSet = !codeResultGroupMarks.length && missedNumbers.length > 0 && isFinalStatus ? toNumberSet(missedNumbers) : new Set();
options.wrongGroupSets =
!codeResultGroupMarks.length && missedGroups.length === recommendGroups.length
? missedGroups.map((group) => toNumberSet(group))
: null;
return options;
};
const renderBalls = (value, options = {}) => {
const groups = splitNumbers(value);
if (!groups.length) return '<span class="cxz-number-empty">--</span>';
const wrongSet = options.wrongSet || new Set();
const hitSet = options.hitSet || new Set();
const markerSets = options.wrongGroupSets || null;
const hitGroupSets = options.hitGroupSets || null;
const codeResultMarks = options.codeResultMarks || null;
const isRecommend = options.type === 'recommend';
const isOpen = options.type === 'open';
const count = groups.reduce((total, group) => total + group.length, 0);
const groupSeparator = getRecommendGroupSeparator(value);
const buildBody = (limit) => {
let rendered = 0;
return groups
.map((group, groupIndex) => {
const nums = typeof limit === 'number' ? group.slice(0, Math.max(0, limit - rendered)) : group;
rendered += nums.length;
if (!nums.length) return '';
const balls = group
.slice(0, nums.length)
.map((num, numberIndex) => {
const isTextToken = /[^\d]/.test(String(num));
const normalized = String(num).padStart(2, '0');
const markerSet = Array.isArray(markerSets) ? markerSets[groupIndex] : null;
const hitGroupSet = Array.isArray(hitGroupSets) ? hitGroupSets[groupIndex] : null;
const codeResultMark =
Array.isArray(codeResultMarks) &&
codeResultMarks[groupIndex] &&
codeResultMarks[groupIndex][numberIndex] !== undefined
? codeResultMarks[groupIndex][numberIndex]
: null;
const isExplicitHit =
(hitGroupSet
? hitGroupSet.has(normalized) || hitGroupSet.has(String(num)) || hitGroupSet.has(normalizeNumberKey(num))
: hitSet.has(normalized) || hitSet.has(String(num)) || hitSet.has(normalizeNumberKey(num)));
const isHit = codeResultMark === true || isExplicitHit;
const isWrong = isHit
? false
: codeResultMark === false
? true
: markerSet
? markerSet.has(normalized) || markerSet.has(String(num)) || markerSet.has(normalizeNumberKey(num))
: wrongSet.has(normalized) || wrongSet.has(String(num)) || wrongSet.has(normalizeNumberKey(num));
const isBlueGroup =
(isOpen && groupIndex > 0) ||
(Number.isInteger(options.blueStartGroupIndex) && groupIndex >= options.blueStartGroupIndex);
const classes = isRecommend
? [
'recommend-number-text',
isBlueGroup ? 'recommend-number-blue' : '',
isWrong ? 'recommend-number-missed' : '',
isHit ? 'recommend-number-hit' : '',
]
.filter(Boolean)
.join(' ')
: [
isTextToken ? 'cxz-number-text' : isBlueGroup ? 'bule-q' : 'red-q',
isTextToken ? '' : 'cxz-number-ball',
isWrong ? 'cxz-number-ball--wrong' : '',
isHit ? 'cxz-number-ball--hit' : '',
]
.filter(Boolean)
.join(' ');
const title = isWrong ? '标记号码' : options.type === 'open' ? '开奖号码' : '推荐号码';
return `<span class="${classes}" title="${title}">${escapeHtml(num)}</span>`;
})
.join(isRecommend ? '<span class="recommend-separator">,</span>' : '');
return `<span class="cxz-number-group">${balls}</span>`;
})
.filter(Boolean)
.join(isRecommend ? `<span class="recommend-separator">${escapeHtml(groupSeparator)}</span>` : '');
};
const fullBody = buildBody();
const shortBody = buildBody(4);
const body = isRecommend ? fullBody : buildBody();
const classes = [
'cxz-number-wrap',
isOpen ? 'cxz-number-wrap--open' : '',
isOpen && options.openRows === 2 ? 'cxz-number-wrap--open-2' : '',
isOpen && options.openRows === 1 ? 'cxz-number-wrap--open-1' : '',
isRecommend && count > 4 ? 'cxz-number-wrap--recommend' : '',
isRecommend && wrongSet.size ? 'cxz-number-wrap--checked' : '',
]
.filter(Boolean)
.join(' ');
let meta = '';
if (isRecommend && count > 4) {
window.__cxzNumberPopups = window.__cxzNumberPopups || {};
const popupId = `cxz-number-${Date.now()}-${Math.random().toString(36).slice(2)}`;
window.__cxzNumberPopups[popupId] = `<div class="cxz-number-popup"><div class="cxz-number-popup-title">完整推荐号码 · 共${count}码</div><div class="cxz-number-wrap cxz-number-wrap--popup cxz-number-wrap--recommend-full">${fullBody}</div></div>`;
meta += '<span class="cxz-number-more">点击查看全部</span>';
return `<button type="button" class="${classes}" data-cxz-popup="${escapeHtml(popupId)}" onclick="if (this.classList.contains('cxz-number-wrap--overflow')) showCaiinfoNumbers('${escapeJs(popupId)}')"><span class="cxz-number-preview cxz-number-preview--full">${body}</span><span class="cxz-number-preview cxz-number-preview--short">${shortBody}</span>${meta}</button>`;
}
return `<div class="${classes}">${body}${meta}</div>`;
};
const adjustRecommendNumbers = () => {
$('.cxz-number-wrap--recommend').each(function () {
const preview = this.querySelector('.cxz-number-preview--full');
if (!preview) return;
this.classList.remove('cxz-number-wrap--overflow', 'cxz-number-wrap--fit');
this.classList.add('cxz-number-wrap--fit');
const available = Math.min(this.parentElement ? this.parentElement.clientWidth : preview.clientWidth, 280);
const fits = preview.scrollWidth <= available + 1;
this.classList.remove('cxz-number-wrap--fit');
this.classList.add(fits ? 'cxz-number-wrap--fit' : 'cxz-number-wrap--overflow');
});
};
window.showCaiinfoNumbers = function (popupId) {
const content = window.__cxzNumberPopups && window.__cxzNumberPopups[popupId];
if (!content) return;
if (window.layui && layui.layer) {
const width = Math.min(520, Math.max(320, window.innerWidth - 32));
layui.layer.open({
type: 1,
title: false,
area: [`${width}px`, 'auto'],
maxWidth: 560,
shadeClose: true,
content,
});
return;
}
alert($(content).text());
};
const getArticleId = (item) =>
item.id || item.articleId || item.payArticleId || item.latestArticleId || item.contentId || item.recordId || '';
const isCollected = (value) => value === 1 || value === 2 || value === true || value === '1' || value === '2';
const isPurchased = (item) =>
item.isBuy === 1 ||
item.isBuy === 2 ||
item.isBuy === '1' ||
item.isBuy === '2' ||
item.buyStatus === 1 ||
item.buyStatus === '1' ||
item.purchased === true ||
item.purchased === 1 ||
item.purchased === '1' ||
item.isPurchased === true ||
item.isPurchased === 1 ||
item.isPurchased === '1' ||
item.bought === true ||
item.bought === 1 ||
item.bought === '1' ||
item.unlocked === true ||
item.unlocked === 1 ||
item.unlocked === '1' ||
item.canView === true ||
item.canView === 1 ||
item.canView === '1' ||
item.payStatus === 1 ||
item.payStatus === '1' ||
item.subscribed === true ||
item.subscribed === 1 ||
item.subscribed === '1' ||
item.subscribed === 'true' ||
item.subscribeStatus === 1 ||
item.subscribeStatus === '1';
const isNumericStatusValue = (value) =>
value !== undefined &&
value !== null &&
value !== '' &&
/^-?\d+(?:\.\d+)?$/.test(String(value).trim());
const getExplicitStatusText = (item) =>
String(
(item &&
(item.resultText ||
item.statusText ||
item.drawStatusText ||
item.openStatusText ||
(typeof item.result === 'string' && !isNumericStatusValue(item.result)
? item.result
: '') ||
(typeof item.status === 'string' && !isNumericStatusValue(item.status)
? item.status
: ''))) ||
''
).trim();
const getNumericStatus = (item) => {
if (!item || !isNumericStatusValue(item.status)) return null;
const value = Number(item.status);
return Number.isFinite(value) ? value : null;
};
const getStatusMatchText = (item, extraText = '') =>
[
extraText,
window.__cxzCaiinfoMenuMeta &&
window.__cxzCaiinfoMenuMeta.lottery &&
window.__cxzCaiinfoMenuMeta.lottery.name,
window.__cxzCaiinfoMenuMeta &&
window.__cxzCaiinfoMenuMeta.current &&
window.__cxzCaiinfoMenuMeta.current.name,
window.__cxzCaiinfoMenuMeta &&
window.__cxzCaiinfoMenuMeta.current &&
window.__cxzCaiinfoMenuMeta.current.parent &&
window.__cxzCaiinfoMenuMeta.current.parent.name,
item && item.lotteryName,
item && item.name,
item && item.codeName,
item && item.code,
item && item.lotteryCode,
item && item.suoxie,
item && item.playName,
item && item.playType,
item && item.typeName,
item && item.parentTypeName,
item && item.menuName,
item && item.title,
item && item.articleTitle,
item && item.latestTitle,
]
.filter(Boolean)
.join(' ')
.toLowerCase();
const getDisplayPlayTypeName = (lotteryName, typeName, item) => {
const matchText = getStatusMatchText(item, typeName);
const lotteryText = `${lotteryName || ''} ${matchText}`.toLowerCase();
if (/七乐彩|qlc/.test(lotteryText) && (matchText.includes('组选') || /^\d+码$/.test(String(typeName || '').trim()))) {
return '组选';
}
if (/大乐透|cjdlt|\bdlt\b/.test(lotteryText) && matchText.includes('复式')) {
return '复式';
}
if (/双色球|ssq/.test(lotteryText) && matchText.includes('复式')) {
return '复式';
}
if (/快乐8|kl8/.test(lotteryText) && matchText.includes('胆码')) {
return '胆码';
}
return typeName || '玩法';
};
const isStatusLoseByPlay = (item, text, typeName = '') => {
const matchText = getStatusMatchText(item, typeName);
const isQlc = /七乐彩|qlc/.test(matchText);
const isThreeDigit = /福彩3d|fc3d|排列三|排列3|pl3/.test(matchText);
const isKl8 = /快乐8|kl8/.test(matchText);
const isSsqOrDlt = /双色球|ssq|大乐透|cjdlt|\bdlt\b/.test(matchText);
const isGroup = matchText.indexOf('组选') >= 0;
const isDuplex = matchText.indexOf('复式') >= 0;
const isDanma = matchText.indexOf('胆码') >= 0;
if (isQlc && isGroup && /^中[01]$/.test(text)) return true;
if (isThreeDigit && isGroup && /^中[012]$/.test(text)) return true;
if (isKl8 && isDanma && text === '中0') return true;
if (isSsqOrDlt && isDuplex && /^中(?:1\+0|0\+0)$/.test(text)) return true;
return false;
};
const buildFrontBackStatusHtml = (statusText) => {
const match = String(statusText || '').trim().match(/^中(\d+)\+(\d+)$/);
if (!match) return escapeHtml(statusText);
return [
'<span class="caiinfo-status-label">中</span>',
`<span class="caiinfo-status-front">${escapeHtml(match[1])}</span>`,
'<span class="caiinfo-status-separator">+</span>',
`<span class="caiinfo-status-back">${escapeHtml(match[2])}</span>`,
].join('');
};
const getDisplayStatusInfo = (item, statusText, className, typeName = '') => {
const text = String(statusText || '').trim();
const matchText = getStatusMatchText(item, typeName);
const isSsqOrDlt = /双色球|ssq|大乐透|cjdlt|\bdlt\b/.test(matchText);
const isDuplex = matchText.indexOf('复式') >= 0;
const nextClassName = isStatusLoseByPlay(item, text, typeName)
? 'caiinfo-status-lose'
: className || 'caiinfo-status-pending';
return {
text,
className: nextClassName,
html: isSsqOrDlt && isDuplex ? buildFrontBackStatusHtml(text) : escapeHtml(text),
};
};
const getStatus = (item, openCode, typeName = '') => {
const raw = getExplicitStatusText(item);
const numericStatus = getNumericStatus(item);
const resultValue = item.result ?? item.winStatus;
const codeResults = typeof ApiClient !== 'undefined' ? ApiClient.asArray(item.codeResults) : [];
const allCorrect =
codeResults.length > 0 && codeResults.every((result) => result && result.correct === true);
if (!raw && numericStatus !== null) {
const text = `中${numericStatus}`;
return getDisplayStatusInfo(item, text, 'caiinfo-status-win', typeName);
}
if (/全对|正确|命中|中奖|中/.test(raw) || allCorrect || resultValue === 1 || resultValue === '1' || resultValue === true) {
const text = raw || '全对';
return getDisplayStatusInfo(item, text, 'caiinfo-status-win', typeName);
}
if (/全错|错误|错/.test(raw) || resultValue === 0 || resultValue === '0' || resultValue === false) {
return getDisplayStatusInfo(item, raw === '错误' ? '错' : raw || '错', 'caiinfo-status-lose', typeName);
}
if (/待|未开奖/.test(raw)) return getDisplayStatusInfo(item, raw, 'caiinfo-status-pending', typeName);
if (raw) return getDisplayStatusInfo(item, raw, 'caiinfo-status-pending', typeName);
if (openCode) return getDisplayStatusInfo(item, '待判定', 'caiinfo-status-pending', typeName);
return getDisplayStatusInfo(item, raw || '待开奖', 'caiinfo-status-pending', typeName);
};
const pageParams = new URLSearchParams(window.location.search);
const currentCode = pageParams.get('code') || '';
const currentMenuId = pageParams.get('menuId') || pageParams.get('parentId') || '';
const findMenuMeta = (menus) => {
let lottery = null;
let current = null;
const walk = (items, parent, root) => {
(items || []).forEach((item) => {
const itemRoot = root || item;
if (item.code === currentCode || item.suoxie === currentCode) lottery = itemRoot;
if (String(item.id) === String(currentMenuId)) {
current = { ...item, parent };
lottery = itemRoot;
}
if (item.children) walk(item.children, item, itemRoot);
});
};
walk(menus || [], null, null);
return { lottery, current };
};
if (!window.__cxzCaiinfoMenuLoading && !window.__cxzCaiinfoMenuMeta && typeof ApiClient !== 'undefined') {
window.__cxzCaiinfoMenuLoading = true;
ApiClient.loadMenuCompat()
.then((res) => {
window.__cxzCaiinfoMenuMeta = findMenuMeta(res.data || []);
if (typeof window.loadArticles === 'function') window.loadArticles();
})
.catch(() => {
window.__cxzCaiinfoMenuMeta = { lottery: null, current: null };
});
}
$('.cxz-table-title').text('预测方案列表');
$('.cxz-data-table thead tr').html(
'<th>名称</th><th>类型</th><th>期号</th><th>开奖号码</th><th>推荐号码</th><th>复制</th><th>状态</th><th>彩币</th><th>操作</th>'
);
window.renderArticles = function (list) {
const $body = $('#articleBody');
$body.empty();
if (!Array.isArray(list) || !list.length) {
$body.html('<tr><td colspan="9" style="text-align:center;color:var(--text-muted);padding:40px;">暂无数据</td></tr>');
return;
}
list.forEach((item, index) => {
const menuMeta = window.__cxzCaiinfoMenuMeta || {};
const metaLottery = menuMeta.lottery || {};
const metaPlay = menuMeta.current || {};
const lotteryName =
item.lotteryName ||
metaLottery.name ||
item.lottery ||
item.categoryName ||
item.codeName ||
'彩票';
const rawPlayName =
metaPlay.name ||
item.playName ||
item.playType ||
(item.menuName && item.menuName !== lotteryName ? item.menuName : '') ||
item.typeName ||
item.parentTypeName ||
item.planName ||
'--';
const playName = getDisplayPlayTypeName(lotteryName, rawPlayName, item);
const nameText = lotteryName;
const issue = item.issue || item.latestIssue || item.expect || item.period || '';
const openCode = item.openCode || item.lotteryResult || item.resultCode || item.openResult || '';
const recommend = item.predictedCode || item.recommendCode || item.latestRecommend || item.recommend || '';
const openRows = /快乐8|kl8/i.test(`${lotteryName}${currentCode}`) ? 2 : 1;
const status = getStatus(item, openCode, playName);
const price = Number(item.price || item.amount || item.points || 0);
const articleId = getArticleId(item);
const blueStartGroupIndex = /大乐透|双色球|dlt|ssq/i.test(`${lotteryName}${currentCode}`) ? 1 : undefined;
const hasDrawResult = isArticleDrawn(item, openCode);
const isLatestIssue = isLatestArticle(item, list);
const hasVisibleRecommend = Boolean(recommend && !isMaskedRecommendText(recommend));
const unlockedByVisibleRecommend =
isLatestIssue && !hasDrawResult && price > 0 && !isArticleMasked(item, recommend) && hasVisibleRecommend;
const purchased = isPurchased(item) || unlockedByVisibleRecommend;
const prePurchased = isArticlePrePurchased(item);
const canShowRecommend = canShowArticleRecommend(
item,
isLatestIssue,
hasDrawResult,
unlockedByVisibleRecommend,
recommend
);
const canCopy = canShowRecommend && hasVisibleRecommend;
const collectText = isCollected(item.isCollect) ? '已收藏' : '收藏';
const collectClass = isCollected(item.isCollect) ? ' collected' : '';
let actionPrefix = '';
if (prePurchased) {
actionPrefix = '<span class="cxz-prebuy-badge">已预购</span> ';
} else if (shouldShowPrePurchaseButton(item, purchased) && typeof window.openPrePurchaseDialog === 'function') {
actionPrefix = `<button class="cxz-btn-buy" onclick="openPrePurchaseDialog(${index})">购买</button> `;
} else if (shouldShowBuyButton(item, isLatestIssue, hasDrawResult, purchased, recommend) && articleId) {
actionPrefix = `<button class="cxz-btn-buy" onclick="openBuyDialog('${escapeJs(articleId)}')">购买</button> `;
} else if (purchased) {
actionPrefix = '<span class="cxz-purchased-badge">已购买</span> ';
}
const recommendOptions = applyRecommendHitOptions(
{
type: 'recommend',
blueStartGroupIndex,
},
item,
status.text
);
let recommendHtml = '';
if (String(recommend || '').trim() === '待发布') {
recommendHtml = '<span class="cxz-recommend-locked">待发布</span>';
} else if (canCopy) {
recommendHtml = renderBalls(recommend, recommendOptions);
} else if (recommend) {
recommendHtml = `<span class="cxz-recommend-locked">${escapeHtml(getRecommendLockedText())}</span>`;
} else {
recommendHtml = '<span class="cxz-recommend-locked">暂无</span>';
}
const copyHtml = canCopy
? `<button class="cxz-btn-copy" onclick="copyCode('${escapeJs(normalizeRecommend(recommend))}')">复制</button>`
: '-';
$body.append(
`<tr>
<td>${escapeHtml(nameText)}</td>
<td>${escapeHtml(playName)}</td>
<td>${escapeHtml(formatIssueText(issue))}</td>
<td>${renderBalls(openCode, { type: 'open', openRows })}</td>
<td>${recommendHtml}</td>
<td>${copyHtml}</td>
<td><span class="${status.className}">${status.html || escapeHtml(status.text)}</span></td>
<td>${Number.isFinite(price) ? price.toFixed(2) : '0.00'}</td>
<td>${actionPrefix}<button class="cxz-btn-article-collect${collectClass}" data-id="${escapeHtml(articleId)}" onclick="toggleArticleCollect(this, '${escapeJs(articleId)}')">${collectText}</button></td>
</tr>`
);
});
window.setTimeout(adjustRecommendNumbers, 0);
};
window.updateStats = function (list, summary) {
const rows = Array.isArray(list) ? list : [];
const total = Number(summary && (summary.totalCount || summary.total)) || rows.length;
const hit =
Number(summary && (summary.correctCount || summary.hitCount)) ||
rows.filter((item) => {
const playName = getDisplayPlayTypeName(
item.lotteryName || item.categoryName || item.codeName || '',
item.playName ||
item.playType ||
item.menuName ||
item.typeName ||
item.parentTypeName ||
item.planName ||
'',
item
);
return getStatus(item, item.openCode, playName).className === 'caiinfo-status-win';
}).length;
const rate =
summary && summary.successRate !== undefined
? Number(summary.successRate)
: total > 0
? (hit / total) * 100
: 0;
$('#statTotal').text(total);
$('#statHit').text(hit);
$('#statRate').text(`${(Number.isFinite(rate) ? rate : 0).toFixed(1)}%`);
};
if (!window.__cxzCaiinfoCompatReloaded && typeof window.loadArticles === 'function') {
window.__cxzCaiinfoCompatReloaded = true;
window.loadArticles();
}
};
document.addEventListener('DOMContentLoaded', () => {
[0, 400, 1000].forEach((delay) => window.setTimeout(run, delay));
});
[600, 1400].forEach((delay) => window.setTimeout(run, delay));
},
injectPaginationStyles() {
if (document.getElementById('cxz-unified-pagination-style')) return;
const script =
document.querySelector('script[src$="utils/CommonUtil.js"], script[src*="utils/CommonUtil.js?"]') ||
document.currentScript;
const href = script && script.src
? script.src.replace(/utils\/CommonUtil\.js(?:\?.*)?$/, 'public/css/pagination.css')
: '../public/css/pagination.css';
const link = document.createElement('link');
link.id = 'cxz-unified-pagination-style';
link.rel = 'stylesheet';
link.href = href;
document.head.appendChild(link);
},
patchLaypageDefaults() {
if (window.__cxzLaypageDefaultsPatched) return;
if (typeof layui === 'undefined' || !layui.use) return;
const applyPatch = () => {
if (!layui.laypage || !layui.laypage.render || layui.laypage.__cxzPatched) return;
const rawRender = layui.laypage.render.bind(layui.laypage);
layui.laypage.render = function (options) {
const isCaiExpertPager =
/\/cai\.html(?:$|\?)/.test(window.location.pathname + window.location.search) &&
options &&
options.elem === 'pagination';
const nextOptions = {
prev: '上一页',
next: '下一页',
groups: window.innerWidth <= 768 ? 3 : 5,
theme: '#102b6a',
hide: true,
...options,
};
if (isCaiExpertPager) {
nextOptions.limit = 10;
}
const instance = rawRender(nextOptions);
setTimeout(() => {
document.querySelectorAll('.layui-laypage-prev').forEach((el) => {
el.textContent = el.textContent.replace(/[<>]/g, '').trim() || '上一页';
});
document.querySelectorAll('.layui-laypage-next').forEach((el) => {
el.textContent = el.textContent.replace(/[<>]/g, '').trim() || '下一页';
});
}, 0);
return instance;
};
layui.laypage.__cxzPatched = true;
window.__cxzLaypageDefaultsPatched = true;
};
applyPatch();
layui.use('laypage', applyPatch);
},
applyCaiExpertListLabels() {
if (typeof window === 'undefined' || typeof document === 'undefined') return;
if (!/\/cai\.html(?:$|\?)/.test(window.location.pathname + window.location.search)) return;
const replaceText = () => {
const title = document.getElementById('tableTitle');
if (title && title.textContent.indexOf('预测列表') >= 0) {
title.textContent = title.textContent.replace('预测列表', '专家列表');
}
const breadcrumb = document.getElementById('breadcrumbLottery');
if (breadcrumb && breadcrumb.textContent.indexOf('预测推荐') >= 0) {
breadcrumb.textContent = breadcrumb.textContent.replace('预测推荐', '专家推荐');
}
};
replaceText();
[0, 350, 800].forEach((delay) => window.setTimeout(replaceText, delay));
},
async loadBasePageData(options = {}) {
const [configList, config, siteConfig, ads] = await Promise.all([
ApiClient.get('/api/web/config/list').catch(() => ({ data: [] })),
ApiClient.get('/api/web/config').catch(() => ({ data: {} })),
ApiClient.get('/api/web/config/site').catch(() => ({ data: {} })),
options.ads === false
? Promise.resolve({ data: [] })
: ApiClient.get('/api/web/ad/list', { inTime: true }).catch(() => ({ data: [] })),
]);
return {
code: 0,
success: true,
data: {
config: config.data || {},
siteConfig: siteConfig.data || {},
webConfigs: this.asArray(this.pickData(configList, [])).length
? this.asArray(this.pickData(configList, []))
: ApiClient.configObjectToList
? ApiClient.configObjectToList(config.data || {})
: config.data || {},
adList: this.asArray(this.pickData(ads, [])),
},
};
},
async loadHomePageData() {
if (ApiClient.loadHomeIndexCompat) {
return ApiClient.loadHomeIndexCompat();
}
const [configList, config, siteConfig, menus, banners, notices, ads, freeArticles, latest] =
await Promise.all([
ApiClient.get('/api/web/config/list').catch(() => ({ data: [] })),
ApiClient.get('/api/web/config').catch(() => ({ data: {} })),
ApiClient.get('/api/web/config/site').catch(() => ({ data: {} })),
ApiClient.get('/api/web/menu/home-nav').catch(() => ({ data: [] })),
ApiClient.get('/api/web/banner/list').catch(() => ({ data: [] })),
ApiClient.get('/api/web/notice/list', { count: 10 }).catch(() => ({ data: [] })),
ApiClient.get('/api/web/ad/list', { inTime: true }).catch(() => ({ data: [] })),
ApiClient.get('/api/web/free-article/recommend', { articleCount: 5 }).catch(() => ({
data: [],
})),
ApiClient.get('/api/web/lottery/result/latest').catch(() => ({ data: [] })),
]);
const lotteryList = this.asArray(this.pickData(latest, []));
const latestMap = {};
lotteryList.forEach((item) => {
latestMap[item.code] = {
...item,
expect: item.expect,
name: item.expect,
time: item.time,
openCode: item.openCode,
code: item.openCode,
red: item.redNumbers,
blue: item.blueNumbers,
};
});
const articleGroups = this.asArray(this.pickData(freeArticles, []));
const articleMap = {};
articleGroups.forEach((group) => {
if (Array.isArray(group.articles)) {
articleMap[group.code] = group.articles.map((article) => ({
...article,
code: article.code || group.code,
lotteryName: article.lotteryName || group.lotteryName,
accountAvatar: article.accountAvatar || article.expertAvatar || article.avatar,
createUserName:
article.createUserName || article.author || article.nickName || article.issuer,
}));
return;
}
const code = group.code || group.suoxie;
if (!code) return;
if (!articleMap[code]) articleMap[code] = [];
articleMap[code].push({
...group,
accountAvatar: group.accountAvatar || group.expertAvatar || group.avatar,
createUserName: group.createUserName || group.author || group.nickName || group.issuer,
});
});
const navList = this
.asArray(this.pickData(menus, []))
.filter((item) => item.type === 'lottery')
.map((menu) => ({
...menu,
suoxie: menu.suoxie || menu.code,
url: menu.url || menu.path,
lotteryResult: latestMap[menu.suoxie || menu.code],
freeArticleList: articleMap[menu.suoxie || menu.code] || [],
}));
return {
code: 0,
success: true,
data: {
config: config.data || {},
webConfigs: this.asArray(this.pickData(configList, [])).length
? this.asArray(this.pickData(configList, []))
: ApiClient.configObjectToList
? ApiClient.configObjectToList(config.data || {})
: config.data || {},
siteConfig: siteConfig.data || {},
bannerList: this.asArray(this.pickData(banners, [])),
noticeList: this.asArray(this.pickData(notices, [])),
adList: this.asArray(this.pickData(ads, [])),
freeArticleData: articleGroups,
lotteryList,
navList,
menuList: navList,
},
};
},
filterValidAds(adList, position) {
if (!Array.isArray(adList)) return [];
const now = new Date();
return adList.filter((item) => {
const enabled = item.isEnabled !== false && item.delFlag !== '1';
const typeOk = item.adType === undefined || item.adType === 1;
const positionOk = position === undefined || String(item.position) === String(position);
const startOk = !item.startTime || new Date(item.startTime) <= now;
const endOk = !item.endTime || new Date(item.endTime) >= now;
return enabled && typeOk && positionOk && startOk && endOk;
});
},
processAndRenderAds(adList, positions) {
const validAds = this.filterValidAds(adList);
const $wrap = this.$('#sidebarAdWrap');
if (!$wrap.length || !validAds.length) return;
$wrap.empty();
const groupSize = 8;
for (let i = 0; i < validAds.length; i += groupSize) {
const group = validAds.slice(i, i + groupSize);
const $section = this.$('<div>').addClass('ad-section');
const $title = this.$('<h3>').addClass('sidebar-title').text('广告推荐');
const $grid = this.$('<div>').addClass('ad-grid');
group.forEach((ad) => {
const $a = this.$('<a>', {
href: ad.link || '#',
target: '_blank',
rel: 'noopener noreferrer',
});
const $img = this.$('<img>', {
src: ad.bannerUrl || ad.touxiang || '',
alt: ad.title || '广告',
loading: 'lazy',
class: 'PublicAdvertisingImg',
});
$a.append($img);
$grid.append($a);
});
$section.append($title).append($grid);
$wrap.append($section);
}
},
renderAds(ads, containerSelector, itemClassName) {
const $container = this.$(containerSelector);
if (!$container.length) return;
$container.empty();
(ads || []).forEach((ad) => {
const $li = this.$('<li>').addClass(itemClassName);
const $a = this.$('<a>', {
href: ad.link || '#',
target: '_blank',
rel: 'noopener noreferrer',
});
const $img = this.$('<img>', {
src: ad.bannerUrl || ad.touxiang || '',
class: 'PublicAdvertisingImg',
alt: ad.title || '广告',
loading: 'lazy',
});
$a.append($img);
$li.append($a);
$container.append($li);
});
},
renderLinks(links, selector) {
const $container = this.$(selector);
if (!$container.length) return;
$container.empty();
(links || []).forEach((item) => {
const $li = this.$('<li>').addClass('link-listLi');
const $a = this.$('<a>', {
href: item.url || item.link || '#',
target: '_blank',
});
$a.append(this.$('<div>').addClass('link-listLiText').text(item.name || item.title || '链接'));
$li.append($a);
$container.append($li);
});
},
loadLinks() {
ApiClient.loadLinksCompat().then((res) => {
const list = res.data || [];
this.renderLinks(
list.filter((item) => item.type === 1),
'.FooterPublicFriendship .link-box:not(.links-box) .link-listUl'
);
this.renderLinks(
list.filter((item) => item.type === 2),
'.FooterPublicFriendship .link-box.links-box .link-listUl'
);
});
},
filterDuanzu(list, lotteryName) {
if (!Array.isArray(list)) return [];
if (!/福彩3D|fc3d/i.test(String(lotteryName || ''))) return list;
return list.filter((item) => {
const name = String(item && item.name ? item.name : '').trim();
return !/^(断组|断组合|杀组合)$/.test(name);
});
},
getCurrentUserDetail(onSuccess, onError) {
ApiClient.get('/api/web/mine/summary', null, { headers: this.authHeaders() })
.then((res) => {
if (res.code !== 0) {
localStorage.removeItem('token');
if (typeof onError === 'function') onError(res);
return;
}
const data = res.data || {};
this.applyTopbarUserLevel(data);
if (typeof onSuccess === 'function') onSuccess(data);
this.applyTopbarUserLevel(data);
})
.catch((error) => {
if (typeof onError === 'function') onError(error);
});
},
signIn(onComplete) {
layui.use('layer', () => {
const layer = layui.layer;
ApiClient.post('/api/web/mine/sign-in', null, { headers: this.authHeaders() })
.then((res) => {
layer.msg(res.code === 0 ? '签到成功' : res.msg || '签到失败', {
icon: res.code === 0 ? 1 : 2,
time: 1500,
});
if (typeof onComplete === 'function') onComplete(res);
})
.catch((error) => {
layer.msg(error?.message || '签到失败', {
icon: 2,
time: 1500,
});
if (typeof onComplete === 'function') onComplete({ code: -1, msg: '签到失败', error });
});
});
},
generateQRCode(url, targetImgId, options = {}) {
const $qrImg = this.$('#' + targetImgId);
if (!$qrImg.length || typeof QRCode === 'undefined') return;
$qrImg.hide();
const $tempContainer = this.$('<div>').hide();
this.$('body').append($tempContainer);
new QRCode($tempContainer[0], {
text: url,
width: options.width || 150,
height: options.height || 150,
colorDark: options.colorDark || 'rgba(19, 34, 122, 0.4)',
colorLight: options.colorLight || 'transparent',
correctLevel: QRCode.CorrectLevel.H,
});
const $canvas = $tempContainer.find('canvas');
if ($canvas.length) {
$qrImg.attr('src', $canvas[0].toDataURL('image/png')).show();
}
$tempContainer.remove();
},
renderTrendOpenCode(openCode, options = {}) {
const value = String(openCode === undefined || openCode === null ? '' : openCode).trim();
if (!value) return '<span class="trend-open-empty">--</span>';
const mode = options.mode || 'red';
const splitTokens = (text) =>
String(text || '')
.split(/[,\s、,]+/)
.map((item) => item.trim())
.filter(Boolean);
const splitDigits = (text) => {
const tokens = splitTokens(text);
if (tokens.length > 1) return tokens;
return String(text || '')
.replace(/[^\d]/g, '')
.split('')
.filter(Boolean);
};
const pad = (num, shouldPad = true) => (shouldPad && /^\d$/.test(num) ? `0${num}` : num);
const renderNum = (num, className, shouldPad = true) =>
`<span class="trend-open-number ${className}">${this.escapeHtml(pad(String(num), shouldPad))}</span>`;
const renderGroup = (list, className, shouldPad = true) =>
list.map((num) => renderNum(num, className, shouldPad)).join('');
const renderPlus = () => '<span class="trend-open-separator">+</span>';
const parts = value.split('+');
let body = '';
if (mode === 'ssq' || mode === 'dlt' || mode === 'qlc') {
const front = splitTokens(parts[0]);
let back = parts.slice(1).flatMap(splitTokens);
const frontLimit = mode === 'ssq' ? 6 : mode === 'dlt' ? 5 : 7;
if (!back.length && front.length > frontLimit) back = front.splice(frontLimit);
body = renderGroup(front, 'trend-open-red');
if (back.length) body += renderPlus() + renderGroup(back, 'trend-open-blue');
} else if (mode === 'qxc') {
const nums = splitDigits(value);
const front = nums.slice(0, 6);
const back = nums.slice(6);
body = renderGroup(front, 'trend-open-red', false);
if (back.length) body += renderPlus() + renderGroup(back, 'trend-open-blue', false);
} else if (mode === 'position') {
body = renderGroup(splitDigits(value), 'trend-open-red', false);
} else {
body = renderGroup(splitTokens(value), 'trend-open-red');
}
return `<span class="trend-open-code">${body || this.escapeHtml(value)}</span>`;
},
sanitizeHTML(html) {
if (!html) return '';
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
doc.querySelectorAll('script, iframe').forEach((el) => el.remove());
doc.querySelectorAll('*').forEach((el) => {
Array.from(el.attributes).forEach((attr) => {
if (attr.name.indexOf('on') === 0) el.removeAttribute(attr.name);
});
});
return doc.body.innerHTML;
},
};
if (typeof module !== 'undefined' && module.exports) {
module.exports = CommonUtil;
} else if (typeof window !== 'undefined') {
window.CommonUtil = CommonUtil;
CommonUtil.installUnifiedPagination();
const installTopbarUserCenterEntry = () => CommonUtil.installTopbarUserCenterEntry();
const installTopbarNavigationMode = () => CommonUtil.installTopbarNavigationMode();
const ensureGlobalFooterLayout = () => CommonUtil.ensureGlobalFooterLayout();
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', installTopbarUserCenterEntry);
document.addEventListener('DOMContentLoaded', installTopbarNavigationMode);
document.addEventListener('DOMContentLoaded', ensureGlobalFooterLayout);
} else {
installTopbarUserCenterEntry();
installTopbarNavigationMode();
ensureGlobalFooterLayout();
}
}