/** * 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, '>') .replace(/"/g, '"') .replace(/'/g, '''); }, 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 = $('