Files
shencaisuan/utils/CommonUtil.js
T
2026-06-04 11:43:41 +08:00

1266 lines
49 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);
},
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
: '000000';
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 '会员';
},
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);
},
applyWebConfigs(configs) {
const $ = this.$;
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 = [];
configs.forEach((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') {
copyrightItems.push(item);
}
});
this.applyFooterCopyright(copyrightItems);
},
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 footerInfo = list.find((item) => Number(item.sort) === 3) || list[list.length - 1];
if (footerInfo?.content) $('#copyright-info').text(footerInfo.content.trim());
const $meta = $('.site-footer__meta');
if (!$meta.length) return;
$meta.empty();
list
.filter((item) => item !== footerInfo)
.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',
text: content,
}).appendTo($meta);
} else {
$('<span>', {
id: isPolice ? 'police-record' : undefined,
text: content,
}).appendTo($meta);
}
});
if (!$meta.children().length && footerInfo?.content) {
$('<span>', {
id: 'additional-info',
text: footerInfo.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')
);
},
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();
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();
if (options.links !== false) this.loadLinks();
if (typeof options.onSuccess === 'function') options.onSuccess(result);
})
.catch((error) => {
if (typeof options.onError === 'function') options.onError(error);
});
},
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) => /\*/.test(String(value || ''));
const normalizeRecommend = (value) =>
String(value || '')
.replace(/[,、]/g, ',')
.replace(/[|]/g, '+')
.replace(/\s+/g, ',')
.replace(/,+/g, ',')
.replace(/\+,/g, '+')
.replace(/,\+/g, '+')
.replace(/^,|,$/g, '');
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 getWrongNumbers = (item) => {
const codeResultWrong = (typeof ApiClient !== 'undefined' ? ApiClient.asArray(item && item.codeResults) : [])
.filter((result) => result && typeof result === 'object' && result.correct === false)
.map((result) => result.code || result.value || result.number || result.result || '')
.filter(Boolean);
const raw =
item.wrongNumbers ||
item.wrongCode ||
item.errorNumbers ||
item.errorCode ||
item.missNumbers ||
item.missCode ||
item.reverseNumbers ||
item.reverseCode ||
item.falseNumbers ||
item.falseCode ||
item.fanZhi ||
item.fanzhi ||
codeResultWrong.join(',');
const groups = splitNumbers(Array.isArray(raw) ? raw.join(',') : raw);
const values = groups.flat();
return new Set(
values.flatMap((num) => {
const text = String(num);
return [text, text.padStart(2, '0'), normalizeNumberKey(text)];
})
);
};
const getHitNumbers = (item) => {
const codeResultHit = (typeof ApiClient !== 'undefined' ? ApiClient.asArray(item && item.codeResults) : [])
.filter((result) => result && typeof result === 'object' && result.correct === true)
.map((result) => result.code || result.value || result.number || result.result || '')
.filter(Boolean);
const raw =
item.hitNumbers ||
item.hitCode ||
item.hitCodes ||
item.correctNumbers ||
item.correctCode ||
item.correctCodes ||
item.winNumbers ||
item.winCode ||
item.winCodes ||
codeResultHit.join(',');
const groups = splitNumbers(Array.isArray(raw) ? raw.join(',') : raw);
const values = groups.flat();
return new Set(
values.flatMap((num) => {
const text = String(num);
return [text, text.padStart(2, '0'), normalizeNumberKey(text)];
})
);
};
const getCodeResultMarks = (item, recommend) => {
const recommendGroups = splitNumbers(recommend);
const codeResults =
typeof ApiClient !== 'undefined' ? ApiClient.asArray(item && item.codeResults) : [];
if (!recommendGroups.length || !codeResults.length) return null;
const flatCount = recommendGroups.reduce((total, group) => total + group.length, 0);
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 null;
if (marks.length < flatCount) return null;
let offset = 0;
return recommendGroups.map((group) => {
const groupMarks = marks.slice(offset, offset + group.length);
offset += group.length;
return groupMarks;
});
};
const getPositionMarkerSets = (item, recommend) => {
const recommendGroups = splitNumbers(recommend);
if (!recommendGroups.length) return null;
const codeResults = typeof ApiClient !== 'undefined' ? ApiClient.asArray(item && item.codeResults) : [];
if (codeResults.length === recommendGroups.length) {
return codeResults.map((result) => {
if (result && typeof result === 'object' && result.correct !== false) {
return new Set();
}
const value =
result && typeof result === 'object'
? result.code || result.value || result.number || result.result || ''
: result;
const values = splitNumbers(value).flat();
return new Set(
values.flatMap((num) => {
const text = String(num);
return [text, text.padStart(2, '0'), normalizeNumberKey(text)];
})
);
});
}
const rawStatus = String(
item.status ||
item.resultText ||
item.statusText ||
item.drawStatusText ||
item.openStatusText ||
(typeof item.result === 'string' ? item.result : '')
).trim();
const resultValue = item.result ?? item.winStatus;
const isMissResult =
/全错|错误|未中|错/.test(rawStatus) ||
resultValue === 0 ||
resultValue === '0' ||
resultValue === false;
if (!isMissResult) return null;
const openGroups = splitNumbers(item.openCode || item.lotteryResult || item.resultCode || item.openResult || '');
const openValues = openGroups.flat();
if (openValues.length === recommendGroups.length) {
return openValues.map((num) => {
const text = String(num);
return new Set([text, text.padStart(2, '0'), normalizeNumberKey(text)]);
});
}
return null;
};
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.markerSets || 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 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 codeResultMark =
Array.isArray(codeResultMarks) &&
codeResultMarks[groupIndex] &&
codeResultMarks[groupIndex][numberIndex] !== undefined
? codeResultMarks[groupIndex][numberIndex]
: null;
const isExplicitHit =
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 = [
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('');
return `<span class="cxz-number-group">${balls}</span>`;
})
.join('');
};
const fullBody = buildBody();
const shortBody = buildBody(5);
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--open cxz-number-wrap--popup">${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 || '';
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.post('/api/web/menu/getFormatList', {})
.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) => {
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 locked = isMasked(recommend);
const canCopy = recommend && !locked;
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 unlockedByVisibleRecommend = !openCode && price > 0 && !locked && !!recommend;
const purchased = isPurchased(item) || unlockedByVisibleRecommend;
const collectText = isCollected(item.isCollect) ? '已收藏' : '收藏';
const collectClass = isCollected(item.isCollect) ? ' collected' : '';
const buyButton =
locked && !openCode && !purchased && articleId
? `<button class="cxz-btn-buy" onclick="openBuyDialog('${escapeJs(articleId)}')">购买</button> `
: '';
const purchasedBadge = purchased ? '<span class="cxz-purchased-badge">已购买</span> ' : '';
const wrongSet = getWrongNumbers(item);
const hitSet = getHitNumbers(item);
const codeResultMarks = getCodeResultMarks(item, recommend);
const markerSets = getPositionMarkerSets(item, recommend);
const recommendHtml = locked
? '<span class="cxz-recommend-locked">购买后可见</span>'
: renderBalls(recommend, {
type: 'recommend',
wrongSet,
hitSet,
markerSets: codeResultMarks ? null : markerSets,
codeResultMarks,
blueStartGroupIndex,
});
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(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>${purchasedBadge}${buyButton}<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.post('/api/web/link/getList', {}).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);
});
});
},
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();
},
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();
}