样式修改完成,待测试

This commit is contained in:
rain
2026-07-07 17:07:34 +08:00
parent 0f6ed0cad3
commit 8a6a8acab7
32 changed files with 2097 additions and 505 deletions
@@ -0,0 +1,334 @@
# 当前项目与参考项目功能差异审计
审计日期:2026-07-07
当前项目:`C:\Users\Administrator\Desktop\job\shencaisuan`
参考项目:`C:\Users\Administrator\Desktop\job\3Dyjs`
## 审计边界
本次按 AGENTS.md 的约束执行:只做源码静态对比和差异记录,不改业务代码,不删除现有迁移结果,不把有意外链化的页面误判成缺失。
已对比范围:
- `html/*.html` 页面文件清单、页面脚本引用、页面内主要函数和接口调用。
- `utils/*.js` 公共请求、公共工具、认证工具、权限工具。
- 首页、导航、底部、走势图、专家/付费文章、免费文章、个人中心、钱包、推广、登录注册找回、协议公告下载等核心功能。
未完成范围:
- 未逐页浏览器点测。
- 未用真实接口账号完整走通购买、提现、微信授权、专家申请审核流。
- 未对 CSS 视觉像素级复刻做审计,本文件只记录功能与交互差异。
## 总体结论
当前项目不是简单落后于参考项目,而是做过一轮“品牌化 + 新接口兼容 + 公共导航底部 + 移动端适配 + 部分页面外链化”的迁移。主要差异集中在三类:
1. 明确替换:`duanzuhe.html``shazuhe.html` 本地方案页被改为跳转 `https://www.3dyjs.cn/duanzuhe.html``https://www.3dyjs.cn/shazuhe.html`;专家收藏从参考项目的 `zhuanjiashoucang.html` 合并到当前 `mianfeishoucang.html`
2. 项目裁剪:当前项目明确不需要本地 `zhuanjia.html` 专家推荐页,这属于允许差异;只需要确保公共导航、接口返回入口和页面跳转不会误指向该页面。
3. 实现风险:参考项目把趋势图渲染收敛到 `public/js/trend-pages.js`,当前项目是每个走势图页面各自实现,后续维护一致性风险更高。
## 文件级差异
### 当前项目独有页面
| 页面 | 判断 | 说明 |
| --- | --- | --- |
| `html/c.html` | 新增/替代 | 当前项目的免费文章收藏页入口,调用 `mineFreeCollectList``freeArticleCollect`。参考项目没有同名页面。 |
| `html/jine.html` | 新增 | 当前项目的钱包资金明细/余额分类页,覆盖账户余额、销售余额、专家余额等明细查看。参考项目没有同名页面。 |
### 参考项目独有页面
| 页面 | 当前状态 | 风险 |
| --- | --- | --- |
| `html/duanzuhe.html` | 当前项目已改为外链到 `https://www.3dyjs.cn/duanzuhe.html` | 本地不再承载断组方案购买、预测列表、侧栏文章等功能;依赖外站可用性和登录态跨站体验。 |
| `html/shazuhe.html` | 当前项目已改为外链到 `https://www.3dyjs.cn/shazuhe.html` | 同上,杀组合方案详情、购买、钱包余额等能力不在本项目内。 |
| `html/zhuanjia.html` | 当前项目明确不需要本地页面 | 允许差异;只需确认公共导航、接口入口和页面跳转不会指向该页面。 |
| `html/zhuanjiashoucang.html` | 当前项目未提供本地页面 | 当前 `mianfeishoucang.html` 已包含专家/免费/付费收藏 tab,可视为合并替代;需要确认入口文案是否仍叫“专家收藏”。 |
### 公共工具差异
| 模块 | 参考项目 | 当前项目 | 判断 |
| --- | --- | --- | --- |
| `utils/ApiClient.js` | 新版 API 集中表,`TENANT_ID = 000000`,包含大量注释说明。 | API 集中表更兼容旧路径,`TENANT_ID = 531923`,有请求去重、旧接口映射、走势图兼容、专家列表兼容。 | 当前更偏兼容层,但要防止旧路径映射掩盖错误接口。 |
| `utils/CommonUtil.js` | 公共配置、导航、底部、首页渲染、cai/caiinfo 兼容。 | 公共配置、导航、底部、专家状态判断、移动适配、cai/caiinfo 兼容更重。 | 当前承担职责更多,后续改动需小心回归。 |
| `utils/AuthPageUtil.js` | 包含邀请码缓存、绑定、滑块移动端居中、滑块头注入等完整能力。 | 具备验证码/滑块/短信计时,但邀请码持久化和滑块移动端增强比参考项目少。 | 登录注册可用,但邀请关系跨登录/注册页保留能力需要重点验证。 |
| `utils/PublishPermission.js` | 独立发布权限判断工具。 | 没有独立文件,权限判断散落在 `fabumianfeiwenzhang.html``CommonUtil.js` 等处。 | 功能可覆盖,但契约不集中,后续容易出现免费/付费发布条件不一致。 |
## 功能模块差异
### 1. 登录、注册、忘记密码
已覆盖:
- 登录:手机号、密码、验证码/滑块验证、协议勾选、微信登录入口。
- 注册:手机号、短信验证码、图片验证码/滑块、协议勾选、密码校验。
- 找回密码:手机号、短信验证码、新密码、确认密码、验证码/滑块。
差异与风险:
- 参考项目 `AuthPageUtil` 有更完整的邀请码捕获、session 缓存、登录后自动绑定推广关系逻辑。当前项目 API 里有 `referralBindPreview/referralBind`,但登录页/注册页需要重点确认是否完整覆盖“带邀请码访问 -> 注册/登录后绑定”的链路。
- 参考项目滑块组件有移动端居中 patch;当前项目滑块功能存在,但移动端弹层居中和 viewport resize 回收能力不如参考完整。
- 当前项目登录注册页面已经做了视觉重构,字段顺序和 DOM 类名与参考不同;后续复制参考逻辑时不能直接照搬选择器。
建议:
- 用一个新手机号和邀请链接跑一次:`?inviteCode=xxx` 进入注册、登录后 `referralBind` 是否触发。
- 移动端验证滑块弹窗是否居中、是否能关闭、是否能重复打开。
### 2. 首页
已覆盖:
- 公告、APP 下载二维码、走势工具、组合工具、彩票分类导航、最新开奖、免费文章、广告、其它推荐、友情链接、公共底部。
- 组合工具当前跳转外站断组/杀组合。
差异与风险:
- 参考项目首页保留 3Dyjs 原站专家推荐入口,公共工具里也会把“专家推荐/专家”指向 `zhuanjia.html`
- 当前项目明确不需要本地 `zhuanjia.html`,如果接口 `home-nav` 返回专家推荐项,当前公共导航必须过滤或改写,不能出现 404。
- 2026-07-07 任务 1 复查结果:当前代码中未发现 `zhuanjia.html` 作为运行时跳转目标;保留的“专家推荐”主要是 `cai.html` 面包屑文案或发布页菜单过滤规则。
- 当前首页底部和友情链接已重新设计,客服二维码使用本地 `public/img/kfwx.jpg``public/img/kfqq.jpg`,这属于当前新增要求。
- 当前项目 app 下载使用 `appDownloadInfo` 或页面内 `/api/web/app/getDownloadInfo`,参考项目没有在 API 表里集中同名 key。
建议:
- 明确“专家推荐”入口策略:当前项目不展示本地 `zhuanjia.html`,入口应删除、隐藏或稳定改到已有等价页面。
- 断组/杀组合外链需要确认是否新开页、是否要求携带登录态。
### 3. 走势图
已覆盖:
- 当前项目包含 `fc3d/pl3/pl5/kl8/ssq/cjdlt/qlc/qxc` 8 个走势图页面。
- 期数按钮已对齐为 `10期/20期/30期/60期`
- 每 10 期有分隔线。
- 当前项目已处理 `lotteryTrend` 返回对象包装,避免 `allData.slice is not a function`
- 表格最大宽度已向 1420px 调整过。
差异与风险:
- 参考项目所有走势图页面都引用 `public/js/trend-pages.js`,真正渲染逻辑集中在一个文件里。
- 当前项目每个走势图页面内都有自己的 `renderData``buildHeader``calcStats` 等逻辑,维护成本高,且快乐8、双色球、七乐彩、七星彩等特殊玩法更容易出现局部不一致。
- 参考项目的 `trend-pages.js` 具备 `lotteryTrend` 优先、`latestLotteryResult` fallback 的统一请求策略;当前项目要逐页确认是否一致。
- 参考项目表格区分区背景、开奖号码颜色、蓝球特殊标色、统计行、横向滚动统一。当前项目此前已多次调整颜色和分线,但仍建议按 8 个页面逐页比对。
建议:
- 后续重构方向:引入当前项目版 `public/js/trend-pages.js`,把 8 个页面收敛成配置驱动。
- 回归清单:8 个彩种分别测 10/20/30/60 期、移动端横向滚动、快乐8 20 个号两行展示、双色球/大乐透/七乐彩蓝球标色。
### 4. 专家排行、专家详情
已覆盖:
- `cai.html` 有按彩种/玩法查看专家排行,调用 `payArticleExpertRank`
- `caiinfo.html` 有专家信息、文章列表、购买/预购、收藏专家、收藏文章、余额支付、积分抵扣等复杂功能。
允许差异:
- 参考项目有独立 `zhuanjia.html`,支持专家推荐聚合、彩种/方案/类型筛选、期数切换、分页。
- 当前项目明确不需要 `zhuanjia.html`,这个页面不作为必须补齐项。
风险:
- 如果 `home-nav` 或其它入口仍包含 `zhuanjia.html`,当前项目会跳空。
- `CommonUtil` 中需要确认专家推荐路径映射是否已改掉,否则公共导航会受影响。
建议:
- 按当前项目口径,不恢复 `zhuanjia.html`;只做入口清理或入口改写,确保不会跳空。
- 文案层面统一“专家申请/申请审核中/发布文章”的状态。
### 5. 免费文章
已覆盖:
- 列表页 `mianfeilist.html`
- 详情页 `mianfei.html`
- 发布页 `fabumianfeiwenzhang.html`
- 收藏页当前有 `mianfeishoucang.html``c.html` 两种入口形态。
- 首页免费文章推荐、免费文章列表中的头像、昵称 `issuer`、标题省略等已按近期要求调整过。
差异与风险:
- 参考项目免费文章发布权限与付费发布权限通过 `PublishPermission.js` 做集中判断;当前免费发布页里存在页面内判断。
- 参考项目很多内容页引入 `purify.min.js`,当前项目更多依赖 `CommonUtil.escapeHtml` 和局部渲染,富文本内容页需要确认是否所有 HTML 内容都经过净化。
- 当前 `mianfeishoucang.html` 已承担专家/免费/付费收藏,但又新增 `c.html`,入口功能可能重复。
建议:
- 明确收藏入口只保留一个,另一个作为兼容跳转。
- 发布权限建议收敛为 `CommonUtil` 或新增当前项目版 `PublishPermission.js`
### 6. 付费文章、购买与预购
已覆盖:
- `cai.html` 专家预测列表。
- `caiinfo.html` 专家文章详情、购买、预购、收藏、余额支付。
- `fufei.html` 发布付费文章。
- `usercenter.html` 我的付费文章、购买记录、删除购买记录。
差异与风险:
- 参考项目的 `caiinfo.html` 对菜单元数据、购买记录、已购状态、预购状态、专家收藏、文章收藏有大量兼容字段;当前项目也有大量兼容,但两边函数名和数据归一化入口不同。
- 当前项目购买记录中断组/杀组合购买详情会跳外站;参考项目可在本地方案页展示。
- 当前项目没有本地方案详情页后,`walletPurchaseFc3dScheme` 的本地消费入口减少,但 API 仍存在。
建议:
-`caiinfo.html` 做一次真实账号回归:未登录、余额不足、购买成功、预购成功、已购复看、专家收藏取消、文章收藏取消。
- 购买记录跳转需要明确:付费文章本地打开,方案类外链打开。
### 7. 断组、杀组合方案
参考项目能力:
- 本地 `duanzuhe.html``shazuhe.html` 提供方案列表、当前方案、历史预测、侧栏文章、钱包余额、购买弹窗、复制推荐码等。
- 调用 `fc3dFreePredictMenus``fc3dFreePredictList``fc3dSchemeList``fc3dSchemeDetail``walletPurchaseFc3dScheme`
当前项目状态:
- 首页组合工具和购买记录相关逻辑已改为跳转外站。
- 当前 API 仍保留 `fc3dFreePredict*``fc3dScheme*``walletPurchaseFc3dScheme`,但本地页面入口已删除。
判断:
- 这是有意架构差异,不是单纯缺文件。
- 风险在于用户登录态、支付状态、购买记录复看是否能在外站无缝完成。
### 8. 个人中心
已覆盖:
- 用户资料、头像、昵称、微信绑定、签到、专家申请状态、发布入口、免费文章、付费文章、购买记录。
- 资料编辑弹窗已改为保存 `nickName/password/avatar`
- 专家申请按钮已按 `expertStatus` 等状态显示“专家申请/申请审核中/发布文章”。
差异与风险:
- 参考项目个人中心布局更大,付费文章、免费文章、购买记录渲染逻辑更复杂,包含更多空状态与错误状态。
- 当前项目有重复的微信绑定卡片 DOM:头部区域和侧栏区域都存在同样 `id="wechat-bind-card"``id="wechat-bind-button"`,这会让 jQuery 选择器只操作首个或产生重复 ID 风险。
- 当前项目资料编辑是 layer 新弹窗,参考项目是页面内旧弹窗;当前实现更干净,但要真实接口验证头像返回字段。
- 当前项目 `pageSize` 全局值复用较多,文章列表、签到、购买记录之间后续改动需避免互相影响。
建议:
- 立即检查重复 ID:微信绑定卡片应只保留一处,或改成唯一 ID + class 统一更新。
- 用真实账号验证:只改昵称、只改头像、只改密码、三者都改、取消弹窗。
### 9. 专家申请
已覆盖:
- 身份证正反面上传、个人简介字数、读取 `expertConfig`、提交 `expertApply`
- 已是专家或审核中时禁用提交。
差异与风险:
- 参考项目文案中明确“审核前三天每天发布 3 篇以上免费文章”等说明;当前项目简化为资料和简介说明。
- 参考项目 `apply.html` 使用 `mineSummary` 渲染用户信息;当前项目用 `CommonUtil.getCurrentUserDetail``CommonUtil.getExpertState`
- 当前项目如果后端 `expertStatus: 0` 表示审核中,之前已修过 truthy 丢失问题,但仍需真实数据确认。
建议:
- 明确后端审核状态字典:`0/pending/audit/applying` 哪些代表审核中,`1/regular/intern` 哪些代表专家。
- 文案是否需要恢复参考项目的申请规则说明。
### 10. 钱包、充值、提现、明细
已覆盖:
- `chongzhi.html` 充值套餐、下单、二维码、轮询订单状态。
- `tixian.html` 提现、收款账户、二维码上传、默认账户、转入余额。
- `tixianlist.html` 提现记录/资金流水筛选、删除流水。
- `jine.html` 资金明细为当前新增。
差异与风险:
- 参考项目充值页会加载 `walletOverview` 刷新余额;当前充值页主要加载充值配置和订单状态,需确认充值前余额展示是否仍需要。
- 当前 `tixianlist.html` 比参考更偏筛选化,功能上覆盖更广。
- 收款账户字段兼容较多,真实上传二维码后返回字段需要验证。
建议:
- 真实回归:充值二维码生成、支付成功轮询、提现账户新增/编辑/设默认/删除、提现提交、提现列表筛选。
### 11. 推广与分销
已覆盖:
- `tuiguang.html` 推广链接、二维码、邀请列表、返佣列表、统计。
- `fenxiao.html` 申请推广/专家入口。
- `wodefenxiao.html` 我的分销列表。
差异与风险:
- 参考项目 `AuthPageUtil` 有邀请码跨页面缓存和登录后自动绑定能力,当前需要确认从推广链接进入注册/登录后的绑定链路是否完整。
- 当前 `wodefenxiao.html` 调用 `referralInviteList`,参考项目同名页主要调用 `referralRebateList`。这里可能是业务口径差异:我的下级 vs 我的返佣。
建议:
- 明确 `wodefenxiao.html` 应展示“邀请用户”还是“返佣流水”。
- 用推广链接从匿名态进入注册/登录,验证 `referralBind` 是否发生。
### 12. 收藏
已覆盖:
- 免费文章收藏。
- 付费文章收藏。
- 专家收藏。
差异与风险:
- 参考项目有 `zhuanjiashoucang.html` 独立收藏聚合页。
- 当前项目主要合并到 `mianfeishoucang.html`,另有 `c.html`。入口命名和展示口径需要统一。
建议:
- 保留一个主入口:建议 `mianfeishoucang.html` 作为三类收藏聚合,`c.html` 改兼容重定向或入口隐藏。
### 13. 公告、协议、APP 下载
已覆盖:
- 公告列表 `gonggaolist.html`、详情 `gonggao.html`
- 用户协议 `yonghuxieyi.html`、隐私协议 `yinsixieyi.html`
- APP 下载 `appdown.html`
差异与风险:
- 当前 `appdown.html` 有独立下载信息接口,参考项目主要是默认信息 + 页面渲染。
- 当前协议页使用 `agreement(type)`,覆盖参考项目功能。
- 当前项目移除了参考项目的 umami 统计脚本引用,若线上需要访问统计,需要补回或确认由其它系统接入。
## 高优先级待确认项
1. 公共导航、首页接口入口和公共路径映射是否已彻底过滤 `zhuanjia.html`,避免跳空。
2. `duanzuhe.html``shazuhe.html` 外链化是否接受登录态不互通、购买记录跨站打开的体验。
3. `mianfeishoucang.html``c.html` 是否重复,需要保留哪个入口。
4. `usercenter.html` 微信绑定卡片重复 ID 是否要立即清理。
5. 发布权限是否要恢复成独立 `PublishPermission.js`,避免免费/付费文章权限逻辑分叉。
6. 走势图是否要收敛成共享 `trend-pages.js`,避免 8 个页面维护不一致。
7. 邀请码/推广绑定链路是否完整覆盖参考项目的跨页缓存和登录后绑定。
## 建议执行顺序
1. 先确认“有意差异”:断组/杀组合外链、收藏页是否合并,`zhuanjia.html` 不恢复。
2. 修真实风险:重复 ID、公共入口误跳 `zhuanjia.html`、邀请码绑定链路、资料编辑真实接口回归。
3. 做结构收敛:发布权限工具化、走势图共享渲染模块化。
4. 做完整手工回归:登录注册找回、首页入口、8 个走势图、专家详情购买、个人中心、钱包、推广、收藏。
## 证据摘录
- 当前页面数 38,参考页面数 40,共同页面 36。
- 当前独有:`c.html``jine.html`
- 参考独有:`duanzuhe.html``shazuhe.html``zhuanjia.html``zhuanjiashoucang.html`;其中 `zhuanjia.html` 为本项目明确不需要的允许差异。
- 当前工具文件 4 个:`ApiClient.js``AuthPageUtil.js``CommonUtil.js``DateUtil.js`
- 参考工具文件 5 个,多出 `PublishPermission.js`
- 当前首页组合工具已经指向外站 `https://www.3dyjs.cn/duanzuhe.html``https://www.3dyjs.cn/shazuhe.html`
- 参考走势图页面统一引用 `../public/js/trend-pages.js?v=20260615-limit`;当前走势图页面各自定义 `renderData`
@@ -0,0 +1,567 @@
# 功能等价修复实施计划
> **给后续执行者:** 实施本计划时必须使用 `superpowers:subagent-driven-development`(推荐)或 `superpowers:executing-plans`,按任务逐项执行。任务步骤使用复选框格式,方便跟踪进度。
**目标:**`shencaisuan` 在功能上与参考项目 `3Dyjs` 保持一致,同时保留当前项目的换皮样式、页面组织方式和已经明确允许的差异。
**架构:**`3Dyjs` 作为功能事实标准。先补齐公共业务规则,再让各页面复用同一套规则,避免每个页面各自判断。视觉样式可以不同,但接口、参数、状态判断、标色规则、购买规则、注册登录、分销、专家申请、文章权限等功能必须和参考项目一致,除非本计划明确列为允许差异。
**技术栈:** 静态 HTML 页面、Layui、jQuery、WangEditor、`utils/*.js` 浏览器全局工具、`utils/ApiClient.js` 统一接口、`utils/CommonUtil.js` 公共渲染和工具方法。
## 全局约束
- 参考项目路径:`C:\Users\Administrator\Desktop\job\3Dyjs`
- 当前项目路径:`C:\Users\Administrator\Desktop\job\shencaisuan`
- 功能行为默认对齐参考项目,除非写在“允许差异”里。
- 样式、品牌、公共导航外观、底部外观、移动端视觉表现可以不同。
- 当前项目不需要 `zhuanjia.html`,不要恢复这个页面。
- 如果公共导航或接口返回入口指向 `zhuanjia.html`,必须过滤或改写到当前项目有效页面。
-`docs/reference-project-function-diff-2026-07-07.md` 作为差异对比基线。
- 不删除、不回退与本任务无关的用户已有改动。
- 每个任务完成后必须做最小可行验证。
---
## 允许差异
这些差异是当前项目明确允许的,不作为缺陷处理:
- `html/zhuanjia.html` 不属于当前项目。
- 视觉皮肤、间距、渐变、导航底部样式、移动端呈现可以不同于参考项目。
- 部分页面可以合并,但合并后必须保留等价功能和有效入口。
- 客服二维码使用当前项目本地图片:`public/img/kfwx.jpg``public/img/kfqq.jpg`
- 工信部备案链接必须使用 `https://beian.miit.gov.cn/`
## 任务 1:锁定导航和允许差异契约
**涉及文件:**
- 修改:`docs/reference-project-function-diff-2026-07-07.md`
- 检查:`utils/CommonUtil.js`
- 检查:`html/index.html`
- 检查:所有渲染公共导航的 `html/*.html`
**输入:**
- 当前项目明确不需要 `zhuanjia.html`
**输出:**
- 找出当前项目里所有仍可能跳转到 `zhuanjia.html` 的运行时代码路径。
- [ ] 步骤 1:搜索错误专家页入口。
执行:
```powershell
rg -n "zhuanjia\.html|专家推荐" html utils public
```
期望:
- 所有 `zhuanjia.html` 结果都必须被检查。
- 只出现在文档里的结果允许存在。
- 当前项目运行时导航不能打开缺失的 `zhuanjia.html`
- [ ] 步骤 2:检查 `CommonUtil.js` 的导航映射。
检查菜单名称为“专家推荐”或路径为 `zhuanjia.html` 时,当前公共工具如何处理。
期望:
- 公共导航不会打开缺失的 `zhuanjia.html`
- 如果接口返回专家推荐入口,当前代码必须过滤它,或改写到一个有效页面。
- [ ] 步骤 3:如果发现新风险,同步更新差异文档。
期望:
- 文档继续把 `zhuanjia.html` 标为允许差异。
- 文档只记录真实运行时风险。
- [ ] 步骤 4:验证文档格式。
执行:
```powershell
git diff --check -- docs/reference-project-function-diff-2026-07-07.md
```
期望:无输出。
## 任务 2:补齐发布权限公共契约
**涉及文件:**
- 新增:`utils/PublishPermission.js`
- 修改:`html/fabumianfeiwenzhang.html`
- 修改:`html/fufei.html`
- 参考:`C:\Users\Administrator\Desktop\job\3Dyjs\utils\PublishPermission.js`
- 参考:`C:\Users\Administrator\Desktop\job\3Dyjs\html\fabumianfeiwenzhang.html`
- 参考:`C:\Users\Administrator\Desktop\job\3Dyjs\html\fufei.html`
**输入:**
- `ApiClient.API.mineSummary`
**输出:**
- 全局对象:`PublishPermission.parseSummary(userData, options)`
- 权限对象字段:`hasExpertIdentity``isRegularExpert``canFree``canPaid``quotaExempt``todayFreeArticleCount``requiredDailyFreeArticleCount``paidReason``reason`
- [ ] 步骤 1:按参考项目逻辑新增 `utils/PublishPermission.js`
期望行为:
- 专家身份兼容字段:`is_expert``isExpert``expert``is_regular_expert``isRegularExpert``regularExpert`、文本型 `expertStatus`
- 付费发布兼容字段:`canPublishPayArticle``payArticleFreeQuotaExempt``todayApprovedFreeArticleCount``requiredDailyApprovedFreeArticleCount``payArticlePublishReason`
- 默认每天需要审核通过的免费文章数量为 `2`
- [ ] 步骤 2:两个发布页都引入新脚本。
期望增加:
```html
<script src="../utils/PublishPermission.js"></script>
```
- [ ] 步骤 3:替换 `fabumianfeiwenzhang.html` 里的本地权限判断。
期望:
- 免费发布页请求 `mineSummary`
- 使用 `PublishPermission.parseSummary(res.data)`
- `canFree` 为假时禁止发布,并引导到 `apply.html`
- “发布付费文章”tab 使用 `canPaid``paidReason` 判断。
- [ ] 步骤 4:给 `fufei.html` 增加付费发布拦截。
期望:
- 页面加载付费发布数据前先请求 `mineSummary`
- `canPaid` 为假时显示原因,并跳回 `fabumianfeiwenzhang.html`
- 不改变现有视觉表单和选号逻辑。
- [ ] 步骤 5:静态验证。
执行:
```powershell
rg -n "PublishPermission|parsePublishPermission|loadTodayPublishedFreeCount|checkPaidPublishPermission" html\fabumianfeiwenzhang.html html\fufei.html utils\PublishPermission.js
```
期望:
- `PublishPermission.js` 存在,并暴露 `parseSummary`
- 两个发布页调用 `PublishPermission.parseSummary`
- 旧的重复权限解析函数被删除,或不再被使用。
- [ ] 步骤 6:语法验证。
执行:
```powershell
node -c utils\PublishPermission.js
```
期望:没有语法错误。
## 任务 3:修正个人中心契约和重复 DOM 风险
**涉及文件:**
- 修改:`html/usercenter.html`
- 参考:`C:\Users\Administrator\Desktop\job\3Dyjs\html\usercenter.html`
**输入:**
- `CommonUtil.getExpertState(user)`
- `ApiClient.js` 中已有的账号概要和资料接口
**输出:**
- 微信绑定区域只有一个明确 DOM 所有者,或使用 class 统一更新多个展示点。
- 资料编辑只走一条清晰提交路径。
- [ ] 步骤 1:搜索 `usercenter.html` 里的重复 ID。
执行:
```powershell
rg -n "wechat-bind-card|wechat-bind-desc|wechat-bind-button|editProfile|expertStatus|expertEnabled" html\usercenter.html
```
期望:
- 编辑前先确认重复 ID 位置。
- [ ] 步骤 2:把重复 ID 改成单一所有者或 class 更新。
期望:
- 页面中不再有重复 `id`
- 重复视觉区域使用 class。
- 微信绑定状态能同步更新所有可见区域。
- [ ] 步骤 3:验证专家申请按钮逻辑。
期望:
- 已是专家:展示专家相关操作,不显示重复申请。
- 审核中:显示“申请审核中”,不允许重复提交。
- 非专家:显示“专家申请”。
- `expertStatus: 0` 这种 falsey 值不能被丢失。
- [ ] 步骤 4:验证资料编辑契约。
期望:
- 昵称字段提交为 `nickName`
- 密码只有在两次输入都存在并一致时提交。
- 头像上传结果提交为 `avatar`
- 没有任何修改时提示“暂无修改”,不发送空请求。
- [ ] 步骤 5:静态验证。
执行:
```powershell
git diff --check -- html\usercenter.html
```
期望:无输出。
## 任务 4:对齐登录、注册、找回密码、验证码和推广绑定
**涉及文件:**
- 修改:`utils/AuthPageUtil.js`
- 修改:`html/login.html`
- 修改:`html/reg.html`
- 修改:`html/repwd.html`
- 参考:`C:\Users\Administrator\Desktop\job\3Dyjs\utils\AuthPageUtil.js`
- 参考:`C:\Users\Administrator\Desktop\job\3Dyjs\html\login.html`
**输入:**
- `referralBindPreview`
- `referralBind`
- 登录、注册、短信验证码、滑块验证码接口
**输出:**
- 邀请码捕获、预览、缓存、登录/注册后绑定流程与参考项目一致。
- [ ] 步骤 1:对比 `AuthPageUtil.js` 和参考项目。
执行:
```powershell
rg -n "invite|referral|captcha|slide|viewport|resize|bind" utils\AuthPageUtil.js C:\Users\Administrator\Desktop\job\3Dyjs\utils\AuthPageUtil.js
```
期望:
- 编辑前列出缺失的邀请码和滑块移动端行为。
- [ ] 步骤 2:迁移邀请码缓存和登录/注册后绑定逻辑。
期望:
- 支持参考项目里的邀请参数。
- 邀请码能在登录/注册页面跳转之间保留。
- 登录或注册成功后,在合适时机调用 `referralBind`
- [ ] 步骤 3:迁移滑块验证码移动端定位逻辑。
期望:
- 移动端验证码弹层居中。
- 多次打开不会残留旧遮罩或旧状态。
- [ ] 步骤 4:确认登录注册页面仍保持当前皮肤。
期望:
- 不回退成参考项目旧样式。
- 表单字段保持当前布局,但行为与参考项目一致。
## 任务 5:对齐免费文章列表、详情、发布和收藏
**涉及文件:**
- 修改:`html/mianfeilist.html`
- 修改:`html/mianfei.html`
- 修改:`html/fabumianfeiwenzhang.html`
- 修改:`html/mianfeishoucang.html`
- 检查:`html/c.html`
- 参考:`C:\Users\Administrator\Desktop\job\3Dyjs\html\mianfeilist.html`
- 参考:`C:\Users\Administrator\Desktop\job\3Dyjs\html\mianfei.html`
**输入:**
- `ApiClient.API` 中免费文章列表、详情、收藏相关接口
**输出:**
- 免费文章列表数量、分页、详情渲染、收藏行为与参考项目等价。
- [ ] 步骤 1:对比免费文章列表接口参数。
期望:
- `pageNum``pageSize``menuId`、状态/类型筛选与参考项目一致。
- 如果页面合并导致参数适配不同,需要在文档中记录。
- [ ] 步骤 2:验证作者和标题展示。
期望:
- 头像、`issuer` 昵称、标题、日期按当前需求展示。
- 标题有最大宽度和省略号。
- [ ] 步骤 3:确定收藏页主入口。
期望:
- 要么 `mianfeishoucang.html` 负责所有收藏 tab`c.html` 变成兼容入口。
- 要么两个页面职责明确,不重复。
- [ ] 步骤 4:验证富文本安全。
期望:
- 文章内容渲染达到参考项目同等级别的净化或转义要求。
- 不出现未处理的危险 HTML 注入。
## 任务 6:对齐付费文章、专家详情、购买和预购
**涉及文件:**
- 修改:`html/cai.html`
- 修改:`html/caiinfo.html`
- 修改:`html/fufei.html`
- 修改:`html/usercenter.html`
- 参考:`C:\Users\Administrator\Desktop\job\3Dyjs\html\cai.html`
- 参考:`C:\Users\Administrator\Desktop\job\3Dyjs\html\caiinfo.html`
- 参考:`C:\Users\Administrator\Desktop\job\3Dyjs\html\fufei.html`
**输入:**
- 专家排行、付费文章列表/详情、购买、预购、收藏、钱包概要接口
**输出:**
- 购买、预购、已购、收藏、余额判断等行为与参考项目等价。
- [ ] 步骤 1:对比 `caiinfo.html` 状态归一化。
期望:
- 已购买、可预购、已预购、文章收藏、专家收藏等状态判断与参考项目一致。
- [ ] 步骤 2:对比支付请求参数。
期望:
- 价格、文章 ID、菜单 ID、专家 ID、预购字段与参考项目接口契约一致。
- [ ] 步骤 3:验证个人中心付费文章和购买记录。
期望:
- 我的付费文章、已购文章、删除购买记录、空状态与参考项目等价。
## 任务 7:对齐走势图渲染规则
**涉及文件:**
- 新增或修改:`public/js/trend-pages.js`
- 修改:`html/fc3d.html`
- 修改:`html/pl3.html`
- 修改:`html/pl5.html`
- 修改:`html/kl8.html`
- 修改:`html/ssq.html`
- 修改:`html/cjdlt.html`
- 修改:`html/qlc.html`
- 修改:`html/qxc.html`
- 参考:`C:\Users\Administrator\Desktop\job\3Dyjs\public\js\trend-pages.js`
**输入:**
- `lotteryTrend`
- `latestLotteryResult`
**输出:**
- 期数选项、走势图行、颜色标记、每 10 期分隔线、统计行、横向滚动规则按彩种与参考项目一致。
- [ ] 步骤 1:对比 8 个走势图页面和参考配置。
期望:
- 确认每个彩种的号码分区、蓝球规则、特别号列、统计行。
- [ ] 步骤 2:实现当前项目共享走势图渲染,或逐页完全对齐。
期望:
- 期数按钮为:`10期``20期``30期``60期`
- 每 10 期使用与参考项目粗细一致的分隔线。
- 红球为红色,蓝球为蓝色,特殊色只在参考项目使用时保留。
- 快乐8 不出现错误的“开奖号”表头,并正确展示 20 个号码。
- [ ] 步骤 3:静态验证 8 个页面。
执行:
```powershell
rg -n "10期|20期|30期|60期|trend-period-separator|lotteryTrend|latestLotteryResult" html\fc3d.html html\pl3.html html\pl5.html html\kl8.html html\ssq.html html\cjdlt.html html\qlc.html html\qxc.html
```
期望:
- 每个页面都有一致的期数选项和数据 fallback 策略。
## 任务 8:对齐钱包、充值、提现和资金明细
**涉及文件:**
- 修改:`html/chongzhi.html`
- 修改:`html/tixian.html`
- 修改:`html/tixianlist.html`
- 修改:`html/jine.html`
- 参考:`C:\Users\Administrator\Desktop\job\3Dyjs\html\tixian.html`
- 参考:`C:\Users\Administrator\Desktop\job\3Dyjs\html\tixianlist.html`
**输入:**
- 钱包概要、充值配置、充值下单、订单状态、提现账户、提现提交、资金流水接口
**输出:**
- 余额展示、二维码上传、默认账户、提现提交、记录筛选行为与参考项目等价。
- [ ] 步骤 1:对比钱包接口 key 和请求参数。
期望:
- 当前页面调用的接口语义与参考项目一致。
- [ ] 步骤 2:验证充值余额和订单轮询。
期望:
- 用户能看到相关余额。
- 二维码和订单状态轮询在成功或失败后正确停止。
- [ ] 步骤 3:验证提现账户管理。
期望:
- 新增、编辑、删除、设默认、二维码上传、提交提现与参考项目等价。
## 任务 9:对齐推广和分销流程
**涉及文件:**
- 修改:`html/tuiguang.html`
- 修改:`html/fenxiao.html`
- 修改:`html/wodefenxiao.html`
- 修改:`utils/AuthPageUtil.js`
- 参考:`C:\Users\Administrator\Desktop\job\3Dyjs\html\fenxiao.html`
- 参考:`C:\Users\Administrator\Desktop\job\3Dyjs\html\wodefenxiao.html`
**输入:**
- 推广概要、邀请列表、返佣列表、绑定预览、绑定提交接口
**输出:**
- 推广链接、二维码、绑定关系、邀请列表、返佣列表行为与参考项目等价。
- [ ] 步骤 1:确认 `wodefenxiao.html` 的业务含义。
期望:
- 如果参考项目展示返佣记录,当前项目不能静默展示邀请用户列表。
- [ ] 步骤 2:对齐接口调用和表格字段。
期望:
- 用户看到的数据含义与参考项目一致。
- [ ] 步骤 3:验证推广绑定链路。
期望:
- 匿名访问邀请链接,注册或登录后能绑定推广关系。
## 任务 10:最终跨模块回归
**涉及文件:**
- 检查所有已修改文件。
- 如决策变化,更新:`docs/reference-project-function-diff-2026-07-07.md`
**输出:**
- 当前批次的功能等价验证记录。
- [ ] 步骤 1:检查所有改动文件格式。
执行:
```powershell
git diff --check
```
期望:无空白错误。
- [ ] 步骤 2:搜索参考项目中当前项目不应误跳的本地路径。
执行:
```powershell
rg -n "zhuanjia\.html|duanzuhe\.html|shazuhe\.html|zhuanjiashoucang\.html" html utils public
```
期望:
- `zhuanjia.html` 不作为运行时目标出现。
- `duanzuhe.html``shazuhe.html` 要么是确认过的外链目标,要么已恢复本地等价功能。
- `zhuanjiashoucang.html` 不作为运行时目标出现,除非明确恢复。
- [ ] 步骤 3:浏览器手工回归。
期望覆盖:
- 登录、注册、找回密码。
- 首页导航和底部。
- 8 个走势图页面。
- 免费文章列表、详情、发布、收藏。
- 付费文章列表、详情、购买、预购、发布。
- 个人中心资料、签到、微信绑定、专家申请状态。
- 充值、提现、资金明细。
- 推广邀请和返佣页面。
## 执行顺序
1. 任务 1:锁定允许差异,防止缺失页面误跳。
2. 任务 2:补齐发布权限公共契约。
3. 任务 3:修正个人中心契约和重复 DOM 风险。
4. 任务 4:对齐登录、注册、验证码和推广绑定基础能力。
5. 任务 5:对齐免费文章流程。
6. 任务 6:对齐付费文章和购买流程。
7. 任务 7:对齐走势图。
8. 任务 8:对齐钱包流程。
9. 任务 9:对齐推广和分销页面。
10. 任务 10:跨模块回归。
## 自检结果
- 需求覆盖:覆盖 `docs/reference-project-function-diff-2026-07-07.md` 中记录的主要高风险差异。
- 占位检查:没有把“待定/以后实现”作为任务要求。
- 契约一致性:发布权限对象字段与参考项目 `PublishPermission.parseSummary` 保持一致。
- 范围检查:这是多批次功能等价修复,每个任务都可以独立验证,并且必须按顺序执行。
+16 -8
View File
@@ -140,6 +140,7 @@
<button class="gotop" id="backTop" type="button" onclick="window.scrollTo({ top: 0, behavior: 'smooth' })"></button>
<script>
var $;
var updateData = {
idCardFrontId: "",
idCardBackId: "",
@@ -148,7 +149,7 @@
var expertIntroMinLength = 80;
layui.use(["layer", "jquery"], function () {
var $ = layui.$;
$ = layui.$;
CommonUtil.loadPageConfig({ ads: false, links: false });
renderLoginState();
@@ -204,13 +205,20 @@
CommonUtil.getCurrentUserDetail(function (data) {
if (!data) return;
$("#currentUsername").text(data.nickName || data.username || data.phoneNumber || "用户");
var isExpert =
data.expert === true ||
data.regularExpert === true ||
data.isExpert === 1 ||
data.isExpert === "1";
if (isExpert) {
$("#applyBtn").text("已申请").prop("disabled", true);
var expertState = CommonUtil.getExpertState
? CommonUtil.getExpertState(data)
: {
isExpert:
data.expert === true ||
data.regularExpert === true ||
data.isExpert === 1 ||
data.isExpert === "1",
isApplying: false,
};
if (expertState.isExpert || expertState.isApplying) {
$("#applyBtn")
.text(expertState.isExpert ? "已是专家" : "申请审核中")
.prop("disabled", true);
}
});
}
+3
View File
@@ -4,6 +4,9 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title id="page-title">神彩算 - 免费文章收藏</title>
<script>
window.location.replace("mianfeishoucang.html?tab=free");
</script>
<script src="../public/css/layui/layui.js"></script>
<script src="../config.js"></script>
<script src="../utils/ApiClient.js"></script>
+36 -2
View File
@@ -851,9 +851,43 @@
if (!item) return false;
const articleId = getArticleId(item);
if (articleId && purchasedArticleIds.has(String(articleId))) return true;
return getArticlePurchaseKeys(item).some(function (key) {
const purchasedByKey = getArticlePurchaseKeys(item).some(function (key) {
return purchasedArticleKeys.has(key);
});
if (purchasedByKey) return true;
return (
item.isBuy === 1 ||
item.isBuy === 2 ||
item.isBuy === true ||
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"
);
}
function canShowArticleRecommend(
@@ -1836,7 +1870,7 @@
const params = {
menuId: getArticleMenuId(item),
expertId: getArticleExpertId(item),
accountType: Number(accountType) || 1,
accountType: Number(item.accountType || accountType) || 1,
issue: getArticleIssue(item),
count: 1,
walletType:
+1 -2
View File
@@ -1,4 +1,4 @@
<!doctype html>
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
@@ -563,7 +563,6 @@
status === "success" ||
status === "paid" ||
status === "2" ||
status === "1" ||
Number(data.status) === 2 ||
Number(data.payStatus) === 2
);
+9 -4
View File
@@ -66,9 +66,10 @@
<div class="page-title-bar">
<div class="page-title">大乐透走势图</div>
<div class="qishu">
<div class="qishu-but" data-count="10">10期</div>
<div class="qishu-but" data-count="20">20期</div>
<div class="qishu-but active" data-count="30">30期</div>
<div class="qishu-but" data-count="60">60期</div>
<div class="qishu-but" data-count="100">100期</div>
</div>
</div>
@@ -216,9 +217,12 @@
backSet[n] = true;
});
html += "<tr>";
html +=
r > 0 && r % 10 === 0
? '<tr class="trend-period-separator">'
: "<tr>";
html += '<td class="col-issue">' + item.issue + "</td>";
html += '<td class="col-code">' + item.openCode + "</td>";
html += '<td class="col-code">' + CommonUtil.renderTrendOpenCode(item.openCode, { mode: "dlt" }) + "</td>";
for (var i = 1; i <= FRONT_MAX; i++) {
if (frontSet[i]) {
@@ -339,7 +343,8 @@
function loadData() {
ApiClient.requestLegacyLotteryResultCompat({ suoxie: "cjdlt" }).then(function (res) {
if (res && res.data) {
allData = res.data;
allData = Array.isArray(res.data) ? res.data : (res.data && (res.data.records || res.data.rows || res.data.list || res.data.data)) || [];
if (!Array.isArray(allData)) allData = [];
renderData(currentCount);
}
});
+8 -152
View File
@@ -7,6 +7,7 @@
<script src="../public/css/layui/layui.js"></script>
<script src="../config.js"></script>
<script src="../utils/ApiClient.js"></script>
<script src="../utils/PublishPermission.js"></script>
<script src="../utils/DateUtil.js"></script>
<script src="../utils/CommonUtil.js"></script>
<link rel="stylesheet" href="../public/css/layui/css/layui.css" />
@@ -185,7 +186,6 @@
canFree: false,
canPaid: false,
};
var REQUIRED_DAILY_FREE_ARTICLES = 2;
var FREE_TITLE_MIN_LENGTH = 15;
var FREE_TITLE_MAX_LENGTH = 30;
@@ -298,146 +298,14 @@
return res && (res.code === 0 || res.code === 200);
}
function isTruthy(value) {
return (
value === true ||
value === 1 ||
value === "1" ||
String(value).toLowerCase() === "true"
);
}
function padDatePart(value) {
return String(value).padStart(2, "0");
}
function formatLocalDate(date) {
return (
date.getFullYear() +
"-" +
padDatePart(date.getMonth() + 1) +
"-" +
padDatePart(date.getDate())
);
}
function getArticleDateText(value) {
if (!value) return "";
if (typeof value === "number") {
var numericDate = new Date(value);
return isNaN(numericDate.getTime())
? ""
: formatLocalDate(numericDate);
}
var text = String(value).trim();
var match = text.match(/^(\d{4})[-/](\d{1,2})[-/](\d{1,2})/);
if (match) {
return (
match[1] + "-" + padDatePart(match[2]) + "-" + padDatePart(match[3])
);
}
var parsedDate = new Date(text.replace(/-/g, "/"));
return isNaN(parsedDate.getTime()) ? "" : formatLocalDate(parsedDate);
}
function isPublishedFreeArticle(item) {
if (!item) return false;
var statusText = String(
item.statusText ||
item.auditStatusText ||
item.publishStatusText ||
item.stateText ||
"",
).toLowerCase();
if (/已发布|审核通过|通过|published|approved|pass/.test(statusText)) {
return true;
}
var status =
item.status != null
? item.status
: item.auditStatus != null
? item.auditStatus
: item.publishStatus;
return status === true || status === 1 || status === "1";
}
function getTodayPublishedFreeCount(rows) {
var today = formatLocalDate(new Date());
return ApiClient.asArray(rows).filter(function (item) {
var articleDate = getArticleDateText(
item.createTime ||
item.create_time ||
item.publishTime ||
item.publish_time ||
item.auditTime ||
item.audit_time ||
item.updateTime ||
item.update_time ||
item.createdAt,
);
return articleDate === today && isPublishedFreeArticle(item);
}).length;
}
function loadTodayPublishedFreeCount() {
return ApiClient.get(ApiClient.API.mineFreeArticleList, {
pageNum: 1,
pageSize: 100,
})
.then(function (res) {
if (!ApiClient.isSuccess(res)) return 0;
var data = res.data || res;
var rows = ApiClient.asArray(
data.rows || data.records || data.list || data.data || data,
);
return getTodayPublishedFreeCount(rows);
})
.catch(function () {
return 0;
});
}
function parsePublishPermission(userData) {
var status = String(
userData.expertStatus || userData.expert_status || "",
).toLowerCase();
var isIntern = /实习|intern|trial/.test(status);
var isRegularByStatus = /正式|regular|formal/.test(status);
var hasExpertIdentity =
isTruthy(userData.is_expert) ||
isTruthy(userData.isExpert) ||
isTruthy(userData.expert) ||
isTruthy(userData.is_regular_expert) ||
isTruthy(userData.isRegularExpert) ||
isTruthy(userData.regularExpert) ||
isIntern ||
isRegularByStatus;
var isRegularExpert =
isTruthy(userData.is_regular_expert) ||
isTruthy(userData.isRegularExpert) ||
isTruthy(userData.regularExpert) ||
isRegularByStatus;
return {
hasExpertIdentity: hasExpertIdentity,
isRegularExpert: hasExpertIdentity && isRegularExpert,
canFree: hasExpertIdentity,
canPaid: false,
todayFreeArticleCount: 0,
paidReason: isRegularExpert
? "今天需先发布2篇审核通过的免费文章后,才能发布付费文章"
: "正式专家才能发布付费文章",
};
}
function loadPublishPermission() {
return ApiClient.get(ApiClient.API.mineSummary)
.then(function (res) {
if (!ApiClient.isSuccess(res) || !res.data) {
if (
!ApiClient.isSuccess(res) ||
!res.data ||
!window.PublishPermission
) {
return {
hasExpertIdentity: false,
isRegularExpert: false,
@@ -448,19 +316,7 @@
};
}
var permission = parsePublishPermission(res.data);
if (!permission.canFree || !permission.isRegularExpert) return permission;
return loadTodayPublishedFreeCount().then(function (count) {
permission.todayFreeArticleCount = count;
permission.canPaid = count >= REQUIRED_DAILY_FREE_ARTICLES;
permission.paidReason = permission.canPaid
? ""
: "今天需先发布2篇审核通过的免费文章后,才能发布付费文章(当前" +
count +
"篇)。";
return permission;
});
return PublishPermission.parseSummary(res.data);
})
.catch(function () {
return {
@@ -479,7 +335,7 @@
publishPermission = permission;
return {
canPaid: permission.canPaid,
reason: permission.paidReason,
reason: permission.reason || permission.paidReason,
};
});
}
+9 -4
View File
@@ -66,9 +66,10 @@
<div class="page-title-bar">
<div class="page-title">福彩3D走势图</div>
<div class="qishu">
<div class="qishu-but" data-count="10">10期</div>
<div class="qishu-but" data-count="20">20期</div>
<div class="qishu-but active" data-count="30">30期</div>
<div class="qishu-but" data-count="60">60期</div>
<div class="qishu-but" data-count="100">100期</div>
</div>
</div>
@@ -207,9 +208,12 @@
var parsed = parseCode(item.openCode);
var type = getType(parsed.hundred, parsed.ten, parsed.unit);
html += "<tr>";
html +=
r > 0 && r % 10 === 0
? '<tr class="trend-period-separator">'
: "<tr>";
html += '<td class="col-issue">' + item.issue + "</td>";
html += '<td class="col-code">' + item.openCode + "</td>";
html += '<td class="col-code">' + CommonUtil.renderTrendOpenCode(item.openCode, { mode: "position" }) + "</td>";
// Type columns
var typeNames = ["baozi", "zu3", "zu6"];
@@ -396,7 +400,8 @@
function loadData() {
ApiClient.requestLegacyLotteryResultCompat({ suoxie: "fc3d" }).then(function (res) {
if (res && res.data) {
allData = res.data;
allData = Array.isArray(res.data) ? res.data : (res.data && (res.data.records || res.data.rows || res.data.list || res.data.data)) || [];
if (!Array.isArray(allData)) allData = [];
renderData(currentCount);
}
});
+1
View File
@@ -196,6 +196,7 @@
<script src="../public/js/lay-config.js"></script>
<script src="../config.js"></script>
<script src="../utils/ApiClient.js"></script>
<script src="../utils/PublishPermission.js"></script>
<script src="../utils/DateUtil.js"></script>
<script src="../utils/CommonUtil.js"></script>
<script src="../public/js/jquery.js"></script>
+10 -7
View File
@@ -66,9 +66,10 @@
<div class="page-title-bar">
<div class="page-title">快乐8走势图</div>
<div class="qishu">
<div class="qishu-but" data-count="10">10期</div>
<div class="qishu-but" data-count="20">20期</div>
<div class="qishu-but active" data-count="30">30期</div>
<div class="qishu-but" data-count="60">60期</div>
<div class="qishu-but" data-count="100">100期</div>
</div>
</div>
@@ -77,7 +78,6 @@
<thead>
<tr>
<td rowspan="2" class="col-issue">期号</td>
<td rowspan="2" class="col-code">开奖号</td>
<td colspan="40" class="zone-g1">1-40</td>
<td colspan="40" class="zone-g2">41-80</td>
</tr>
@@ -198,13 +198,15 @@
hitSet[n] = true;
});
html += "<tr>";
html +=
r > 0 && r % 10 === 0
? '<tr class="trend-period-separator">'
: "<tr>";
html += '<td class="col-issue">' + item.issue + "</td>";
html += '<td class="col-code">' + item.openCode + "</td>";
for (var i = 1; i <= NUM_MAX; i++) {
if (hitSet[i]) {
var cls = i <= 40 ? "hit-num" : "hit-num g2";
var cls = "hit-num";
html +=
'<td><span class="' +
cls +
@@ -292,7 +294,7 @@
html +=
'<tr class="' +
cls +
'"><td colspan="2" class="stat-label">' +
'"><td colspan="1" class="stat-label">' +
labels[li] +
"</td>";
for (var i = 1; i <= NUM_MAX; i++)
@@ -305,7 +307,8 @@
function loadData() {
ApiClient.requestLegacyLotteryResultCompat({ suoxie: "kl8" }).then(function (res) {
if (res && res.data) {
allData = res.data;
allData = Array.isArray(res.data) ? res.data : (res.data && (res.data.records || res.data.rows || res.data.list || res.data.data)) || [];
if (!Array.isArray(allData)) allData = [];
renderData(currentCount);
}
});
+22 -8
View File
@@ -305,6 +305,26 @@
return true;
}
function getLoginRedirectUrl() {
const redirect = new URLSearchParams(window.location.search).get("redirect");
return (
redirect ||
(typeof CommonUtil !== "undefined"
? CommonUtil.siteHref("/index.html")
: "index.html")
);
}
function redirectAfterLogin() {
window.location.href = getLoginRedirectUrl();
}
function bindReferralThenRedirect() {
AuthPageUtil.bindPendingReferralInviteCode()
.then(redirectAfterLogin)
.catch(redirectAfterLogin);
}
function submitLogin() {
const payload = Object.assign(
{
@@ -331,14 +351,7 @@
layerInstance.msg(
"登录成功,即将跳转到首页",
{ icon: 1, time: 1200 },
function () {
const redirect = new URLSearchParams(window.location.search).get("redirect");
window.location.href =
redirect ||
(typeof CommonUtil !== "undefined"
? CommonUtil.siteHref("/index.html")
: "index.html");
},
bindReferralThenRedirect,
);
return;
}
@@ -363,6 +376,7 @@
layui.use(["layer", "jquery"], function () {
layerInstance = layui.layer;
AuthPageUtil.captureReferralInviteFromUrl();
loadVerifyConfig().then(refreshCaptcha);
$("#loginForm").on("submit", function (event) {
event.preventDefault();
+25 -5
View File
@@ -9,6 +9,7 @@
<script src="../utils/ApiClient.js"></script>
<script src="../utils/DateUtil.js"></script>
<script src="../utils/CommonUtil.js"></script>
<script src="../public/js/purify.min.js"></script>
<link rel="stylesheet" href="../public/css/layui/css/layui.css" />
<link rel="stylesheet" href="../public/css/public.css" />
<link rel="stylesheet" href="../public/css/mianfei.css" />
@@ -176,7 +177,11 @@
const $ = layui.$;
const urlParams = new URLSearchParams(window.location.search);
const pathMatch = window.location.pathname.match(/\/mianfei\/([^/]+)\.html$/);
const articleId = urlParams.get("id") || (pathMatch ? decodeURIComponent(pathMatch[1]) : "");
const htmlPathMatch = window.location.pathname.match(/(?:^|\/)mianfei\.html\/([^/?#]+)/i);
const articleId =
urlParams.get("id") ||
(pathMatch ? decodeURIComponent(pathMatch[1]) : "") ||
(htmlPathMatch ? decodeURIComponent(htmlPathMatch[1]) : "");
let isLiked = false;
let isCollected = false;
let articleData = null;
@@ -269,7 +274,11 @@
function renderArticle(data) {
$("#articleTitle").text(data.title || "无标题");
$("#articleAuthor").text(data.issuer || data.nickName || data.author || data.createUserName || "佚名");
$("#articleAuthor").text(
typeof CommonUtil.getFreeArticleIssuer === "function"
? CommonUtil.getFreeArticleIssuer(data)
: data.issuer || data.nickName || data.author || data.createUserName || "佚名",
);
$("#articleTime").text(data.updateTime || data.createTime || data.publishTime || "--");
$("#articleContent").html(scopeArticleContent(data.content || ""));
document.title = "神彩算 - " + (data.title || "免费文章详情");
@@ -289,6 +298,10 @@
return '<p class="cxz-content-empty">暂无内容</p>';
}
if (typeof DOMPurify !== "undefined") {
html = DOMPurify.sanitize(html);
}
var scopedStyles = [];
html = html.replace(/<style[^>]*>([\s\S]*?)<\/style>/gi, function (_, css) {
scopedStyles.push(prefixContentCss(css));
@@ -357,7 +370,10 @@
}
function loadRelatedArticles() {
ApiClient.get(ApiClient.API.freeArticleRelated(articleId))
ApiClient.get(ApiClient.API.freeArticleRelated(articleId), {
limit: 12,
articleCount: 12,
})
.then(function (res) {
if (!ApiClient.isSuccess(res)) return;
var data = ApiClient.pickData(res, {}) || {};
@@ -370,14 +386,17 @@
renderFreeArticleList("#freeRelatedList", rows.filter(function (item) {
var id = item && (item.id || item.articleId || item.freeArticleId);
return id && String(id) !== String(articleId);
}).slice(0, 8));
}).slice(0, 12));
})
.catch(function () {});
loadFreeArticleLatest();
}
function loadFreeArticleLatest() {
ApiClient.get(ApiClient.API.freeArticleLatest, { articleCount: 8 })
ApiClient.get(ApiClient.API.freeArticleLatest, {
limit: 12,
articleCount: 12,
})
.then(function (res) {
if (!ApiClient.isSuccess(res)) return;
renderFreeArticleList("#freeLatestList", ApiClient.asArray(ApiClient.pickData(res, [])));
@@ -394,6 +413,7 @@
return;
}
$.each(list, function (_, item) {
if (_ >= 12) return false;
var id = item.id || item.articleId || item.freeArticleId;
if (!id) return;
var title = item.title || item.articleTitle || item.name || "无标题";
+78 -21
View File
@@ -110,7 +110,7 @@
<script>
const $ = layui.$;
const urlParams = new URLSearchParams(window.location.search);
let menuId = urlParams.get("id") || "";
let menuId = urlParams.get("id") || urlParams.get("code") || "";
let currentPage = 1;
const pageSize = 15;
let lotteryMenus = [];
@@ -153,16 +153,15 @@
function resolveCurrentMenuId() {
if (!lotteryMenus.length) return;
if (
menuId &&
lotteryMenus.some(function (item) {
return sameLotteryKey(item, menuId);
})
) {
return;
}
menuId = String((lotteryMenus[0] && lotteryMenus[0].id) || "");
const matched = menuId
? lotteryMenus.find(function (item) {
return sameLotteryKey(item, menuId);
})
: null;
const current = matched || lotteryMenus[0];
menuId = String(
(current && (current.id || current.menuId || current.code || current.suoxie)) || "",
);
}
function renderLotteryTabs() {
@@ -184,9 +183,10 @@
$tabs.empty();
$.each(lotteryMenus, function (_, item) {
const id = item.id || item.menuId || item.code || item.suoxie || "";
const linkKey = item.code || item.suoxie || item.lotteryCode || id;
$tabs.append(
'<a href="mianfeilist.html?id=' +
encodeURIComponent(id) +
encodeURIComponent(linkKey) +
'" class="cxz-lottery-tab" data-id="' +
escapeHtml(id) +
'">' +
@@ -244,6 +244,44 @@
);
}
function currentRequestMenuId() {
const menu = currentMenuInfo();
return String((menu && (menu.id || menu.menuId)) || "");
}
function getArticlePageData(res) {
const data = ApiClient.pickData(res, {}) || {};
const records = ApiClient.asArray(
data.records || data.rows || data.list || data.data || data,
);
const total =
Number(data.total) ||
Number(data.totalRow) ||
Number(data.totalCount) ||
Number(data.count) ||
Number(res && res.total) ||
records.length;
return {
records: records,
total: total,
};
}
function normalizeArticle(article, group) {
article = article || {};
group = group || {};
return Object.assign({}, article, {
id: article.id || article.articleId || article.freeArticleId || "",
title: article.title || article.articleTitle || article.name || "无标题",
dateText:
article.dateText ||
article.updateTime ||
article.createTime ||
article.publishTime ||
group.dateText,
});
}
// Load articles
function loadArticles() {
if (!menuId) {
@@ -253,16 +291,26 @@
renderPagination(0);
return;
}
const requestMenuId = currentRequestMenuId();
if (!requestMenuId) {
renderFallbackArticles();
return;
}
ApiClient.get(ApiClient.API.freeArticleList, {
menuId: menuId,
menuId: requestMenuId,
pageNum: currentPage,
pageSize: pageSize,
}).then(function (res) {
if (ApiClient.isSuccess(res) && ApiClient.pickData(res, null)) {
const data = ApiClient.pickData(res, {}) || {};
const list = data.records || data.rows || data.list || ApiClient.asArray(data);
renderArticles(list);
renderPagination(data.totalRow || data.total || data.count || list.length || 0);
const pageData = getArticlePageData(res);
if (pageData.records.length || pageData.total) {
renderArticles(pageData.records.map(function (item) {
return normalizeArticle(item, currentFallbackGroup());
}));
renderPagination(pageData.total);
} else {
renderFallbackArticles();
}
} else {
renderFallbackArticles();
}
@@ -273,7 +321,9 @@
function renderFallbackArticles() {
const group = currentFallbackGroup();
const allArticles = ApiClient.asArray(group && group.articles);
const allArticles = ApiClient.asArray(group && group.articles).map(function (item) {
return normalizeArticle(item, group);
});
const startIndex = (currentPage - 1) * pageSize;
const records = allArticles.slice(startIndex, startIndex + pageSize);
renderArticles(records);
@@ -291,13 +341,16 @@
}
$.each(list, function (i, item) {
let displayDate = "";
if (item.createTime) displayDate = item.createTime.substring(0, 10);
const article = normalizeArticle(item);
const dateText = article.dateText || "";
const title = article.title || "无标题";
if (dateText) displayDate = String(dateText).substring(0, 10);
$container.append(
'<div class="cxz-article-item">' +
'<a href="' +
CommonUtil.freeArticleHref(item.id || "") +
CommonUtil.freeArticleHref(article.id || "") +
'" target="_blank">' +
escapeHtml(item.title || "无标题") +
escapeHtml(title) +
"</a>" +
'<span class="cxz-article-date">' +
escapeHtml(displayDate) +
@@ -308,6 +361,10 @@
}
function renderPagination(total) {
$("#pagination").empty();
if (!total || total <= pageSize) {
return;
}
layui.laypage.render({
elem: "pagination",
count: total,
+9 -4
View File
@@ -66,9 +66,10 @@
<div class="page-title-bar">
<div class="page-title">排列三走势图</div>
<div class="qishu">
<div class="qishu-but" data-count="10">10期</div>
<div class="qishu-but" data-count="20">20期</div>
<div class="qishu-but active" data-count="30">30期</div>
<div class="qishu-but" data-count="60">60期</div>
<div class="qishu-but" data-count="100">100期</div>
</div>
</div>
@@ -207,9 +208,12 @@
var parsed = parseCode(item.openCode);
var type = getType(parsed.hundred, parsed.ten, parsed.unit);
html += "<tr>";
html +=
r > 0 && r % 10 === 0
? '<tr class="trend-period-separator">'
: "<tr>";
html += '<td class="col-issue">' + item.issue + "</td>";
html += '<td class="col-code">' + item.openCode + "</td>";
html += '<td class="col-code">' + CommonUtil.renderTrendOpenCode(item.openCode, { mode: "position" }) + "</td>";
var typeNames = ["baozi", "zu3", "zu6"];
var typeLabels = ["豹子", "组三", "组六"];
@@ -384,7 +388,8 @@
function loadData() {
ApiClient.requestLegacyLotteryResultCompat({ suoxie: "pl3" }).then(function (res) {
if (res && res.data) {
allData = res.data;
allData = Array.isArray(res.data) ? res.data : (res.data && (res.data.records || res.data.rows || res.data.list || res.data.data)) || [];
if (!Array.isArray(allData)) allData = [];
renderData(currentCount);
}
});
+9 -4
View File
@@ -66,9 +66,10 @@
<div class="page-title-bar">
<div class="page-title">排列五走势图</div>
<div class="qishu">
<div class="qishu-but" data-count="10">10期</div>
<div class="qishu-but" data-count="20">20期</div>
<div class="qishu-but active" data-count="30">30期</div>
<div class="qishu-but" data-count="60">60期</div>
<div class="qishu-but" data-count="100">100期</div>
</div>
</div>
@@ -204,9 +205,12 @@
var item = data[r];
var nums = parseCode(item.openCode);
html += "<tr>";
html +=
r > 0 && r % 10 === 0
? '<tr class="trend-period-separator">'
: "<tr>";
html += '<td class="col-issue">' + item.issue + "</td>";
html += '<td class="col-code">' + item.openCode + "</td>";
html += '<td class="col-code">' + CommonUtil.renderTrendOpenCode(item.openCode, { mode: "position" }) + "</td>";
for (var p = 0; p < POS_COUNT; p++) {
var val = nums[p] !== undefined ? nums[p] : -1;
@@ -313,7 +317,8 @@
function loadData() {
ApiClient.requestLegacyLotteryResultCompat({ suoxie: "pl5" }).then(function (res) {
if (res && res.data) {
allData = res.data;
allData = Array.isArray(res.data) ? res.data : (res.data && (res.data.records || res.data.rows || res.data.list || res.data.data)) || [];
if (!Array.isArray(allData)) allData = [];
renderData(currentCount);
}
});
+21 -30
View File
@@ -66,9 +66,10 @@
<div class="page-title-bar">
<div class="page-title">七乐彩走势图</div>
<div class="qishu">
<div class="qishu-but" data-count="10">10期</div>
<div class="qishu-but" data-count="20">20期</div>
<div class="qishu-but active" data-count="30">30期</div>
<div class="qishu-but" data-count="60">60期</div>
<div class="qishu-but" data-count="100">100期</div>
</div>
</div>
@@ -78,10 +79,9 @@
<tr>
<td rowspan="2" class="col-issue">期号</td>
<td rowspan="2" class="col-code">开奖号</td>
<td colspan="10" class="zone-z1">一区(01-10)</td>
<td colspan="10" class="zone-z2">二区(11-20)</td>
<td colspan="10" class="zone-z3">三区(21-30)</td>
<td colspan="1" class="zone-sp">特别号</td>
<td colspan="10" class="zone-z1">一区</td>
<td colspan="10" class="zone-z2">二区</td>
<td colspan="10" class="zone-z3">三区</td>
</tr>
<tr id="header-row"></tr>
</thead>
@@ -179,8 +179,6 @@
for (var i = 1; i <= MAIN_MAX; i++) {
html += "<td>" + (i < 10 ? "0" + i : i) + "</td>";
}
// Special number column
html += "<td>特</td>";
$("#header-row").html(html);
}
@@ -213,12 +211,20 @@
specialSet[n] = true;
});
html += "<tr>";
html +=
r > 0 && r % 10 === 0
? '<tr class="trend-period-separator">'
: "<tr>";
html += '<td class="col-issue">' + item.issue + "</td>";
html += '<td class="col-code">' + item.openCode + "</td>";
html += '<td class="col-code">' + CommonUtil.renderTrendOpenCode(item.openCode, { mode: "qlc" }) + "</td>";
for (var i = 1; i <= MAIN_MAX; i++) {
if (mainSet[i]) {
if (specialSet[i]) {
html +=
'<td><span class="hit-blue">' +
(i < 10 ? "0" + i : i) +
"</span></td>";
} else if (mainSet[i]) {
html +=
'<td><span class="hit-main">' +
(i < 10 ? "0" + i : i) +
@@ -227,19 +233,8 @@
html +=
'<td><span class="miss-num">' +
(i < 10 ? "0" + i : i) +
"</span></td>";
}
}
// Show special number
if (parsed.specials.length > 0) {
html +=
'<td><span class="hit-special">' +
(parsed.specials[0] < 10
? "0" + parsed.specials[0]
: parsed.specials[0]) +
"</span></td>";
} else {
html += "<td>-</td>";
}
}
html += "</tr>";
}
@@ -299,11 +294,8 @@
function buildStats(data) {
var ms = calcStats(data, MAIN_MAX, function (d) {
return parseCode(d.openCode).mains;
});
// Special number stats (1-30 range for special)
var ss = calcStats(data, MAIN_MAX, function (d) {
return parseCode(d.openCode).specials;
var parsed = parseCode(d.openCode);
return parsed.mains.concat(parsed.specials);
});
var labels = [
@@ -325,8 +317,6 @@
"</td>";
for (var i = 1; i <= MAIN_MAX; i++)
stats += "<td>" + ms[keys[li]][i] + "</td>";
// Special column stats
stats += "<td>-</td>";
stats += "</tr>";
}
return stats;
@@ -335,7 +325,8 @@
function loadData() {
ApiClient.requestLegacyLotteryResultCompat({ suoxie: "qlc" }).then(function (res) {
if (res && res.data) {
allData = res.data;
allData = Array.isArray(res.data) ? res.data : (res.data && (res.data.records || res.data.rows || res.data.list || res.data.data)) || [];
if (!Array.isArray(allData)) allData = [];
renderData(currentCount);
}
});
+22 -12
View File
@@ -66,9 +66,10 @@
<div class="page-title-bar">
<div class="page-title">七星彩走势图</div>
<div class="qishu">
<div class="qishu-but" data-count="10">10期</div>
<div class="qishu-but" data-count="20">20期</div>
<div class="qishu-but active" data-count="30">30期</div>
<div class="qishu-but" data-count="60">60期</div>
<div class="qishu-but" data-count="100">100期</div>
</div>
</div>
@@ -178,13 +179,13 @@
var currentCount = 30;
var allData = [];
var hitClasses = [
"hit-p1",
"hit-p2",
"hit-p3",
"hit-p4",
"hit-p5",
"hit-p6",
"hit-p7",
"hit-red",
"hit-red",
"hit-red",
"hit-red",
"hit-red",
"hit-red",
"hit-blue",
];
var posMaxes = [9, 9, 9, 9, 9, 9, LAST_MAX]; // max value for each position
@@ -198,7 +199,12 @@
}
function parseCode(code) {
var nums = code.split(",").map(function (n) {
var value = String(code || "");
var parts = value.split(/[,+\s、,]+/).filter(Boolean);
if (parts.length <= 1) {
parts = value.replace(/[^\d]/g, "").split("").filter(Boolean);
}
var nums = parts.map(function (n) {
return parseInt(n);
});
return nums;
@@ -211,9 +217,12 @@
var item = data[r];
var nums = parseCode(item.openCode);
html += "<tr>";
html +=
r > 0 && r % 10 === 0
? '<tr class="trend-period-separator">'
: "<tr>";
html += '<td class="col-issue">' + item.issue + "</td>";
html += '<td class="col-code">' + item.openCode + "</td>";
html += '<td class="col-code">' + CommonUtil.renderTrendOpenCode(item.openCode, { mode: "qxc" }) + "</td>";
for (var p = 0; p < POS_COUNT; p++) {
var max = posMaxes[p];
@@ -323,7 +332,8 @@
function loadData() {
ApiClient.requestLegacyLotteryResultCompat({ suoxie: "qxc" }).then(function (res) {
if (res && res.data) {
allData = res.data;
allData = Array.isArray(res.data) ? res.data : (res.data && (res.data.records || res.data.rows || res.data.list || res.data.data)) || [];
if (!Array.isArray(allData)) allData = [];
renderData(currentCount);
}
});
+49 -9
View File
@@ -653,8 +653,10 @@
}
function getInviteCodeFromQuery() {
const params = new URLSearchParams(window.location.search);
return params.get("inviteCode") || params.get("referralCode") || "";
return (
AuthPageUtil.captureReferralInviteFromUrl() ||
AuthPageUtil.getPendingReferralInviteCode()
);
}
function loadReferralBindPreview() {
@@ -694,6 +696,49 @@
});
}
function saveRegisterTokenIfPresent(res) {
const data = ApiClient.pickData(res) || {};
if (!data || typeof data !== "object") return false;
const token = data.access_token || data.token || "";
if (!token) return false;
localStorage.setItem("token", token);
localStorage.setItem("userInfo", JSON.stringify(data));
sessionStorage.removeItem("authExpiredRedirecting");
sessionStorage.removeItem("authExpiredMessage");
return true;
}
function redirectAfterRegister(hasToken) {
if (hasToken) {
window.location.href =
typeof CommonUtil !== "undefined"
? CommonUtil.siteHref("/index.html")
: "index.html";
return;
}
window.location.href = "login.html";
}
function finishRegisterSuccess(res) {
const hasToken = saveRegisterTokenIfPresent(res);
layerInstance.msg(
hasToken ? "注册成功,即将跳转到首页" : "注册成功,即将前往登录",
{ icon: 1, time: 1200 },
function () {
AuthPageUtil.bindPendingReferralInviteCode()
.then(function () {
redirectAfterRegister(hasToken);
})
.catch(function () {
redirectAfterRegister(hasToken);
});
},
);
}
function register() {
$("#registerBtn").prop("disabled", true).text("注册中...");
ApiClient.post(ApiClient.API.authRegister, buildRegisterData(), {
@@ -701,13 +746,7 @@
})
.then(function (res) {
if (ApiClient.isSuccess(res)) {
layerInstance.msg(
"注册成功,即将前往登录",
{ icon: 1, time: 1200 },
function () {
window.location.href = "login.html";
},
);
finishRegisterSuccess(res);
return;
}
layerInstance.msg((res && res.msg) || "注册失败,请稍后重试", {
@@ -726,6 +765,7 @@
layui.use(["layer", "jquery"], function () {
layerInstance = layui.layer;
AuthPageUtil.captureReferralInviteFromUrl();
loadVerifyConfig().then(refreshCaptcha);
loadReferralBindPreview();
$("#sendSmsBtn").on("click", sendSmsCode);
+9 -4
View File
@@ -66,9 +66,10 @@
<div class="page-title-bar">
<div class="page-title">双色球走势图</div>
<div class="qishu">
<div class="qishu-but" data-count="10">10期</div>
<div class="qishu-but" data-count="20">20期</div>
<div class="qishu-but active" data-count="30">30期</div>
<div class="qishu-but" data-count="60">60期</div>
<div class="qishu-but" data-count="100">100期</div>
</div>
</div>
@@ -215,11 +216,14 @@
blueSet[n] = true;
});
html += "<tr>";
html +=
r > 0 && r % 10 === 0
? '<tr class="trend-period-separator">'
: "<tr>";
html += '<td class="col-issue">' + item.issue + "</td>";
html +=
'<td class="col-code">' +
item.openCode.replace(/\+/g, "+") +
CommonUtil.renderTrendOpenCode(item.openCode, { mode: "ssq" }) +
"</td>";
for (var i = 1; i <= RED_MAX; i++) {
@@ -398,7 +402,8 @@
function loadData() {
ApiClient.requestLegacyLotteryResultCompat({ suoxie: "ssq" }).then(function (res) {
if (res && res.data) {
allData = res.data;
allData = Array.isArray(res.data) ? res.data : (res.data && (res.data.records || res.data.rows || res.data.list || res.data.data)) || [];
if (!Array.isArray(allData)) allData = [];
renderData(currentCount);
}
});
+5 -3
View File
@@ -1,4 +1,4 @@
<!doctype html>
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
@@ -542,13 +542,15 @@
if (!isSuccess(res)) throw new Error(res.msg || "收款码上传失败");
const data = pickData(res) || {};
const qrCodeFileId = fieldValue(data, ["qrCodeFileId", "qrcodeFileId", "fileId", "ossId", "id"]) || fieldValue(res, ["qrCodeFileId", "qrcodeFileId", "fileId", "ossId", "id"]);
if (!qrCodeFileId) throw new Error("收款码上传成功,但未返回二维码文件ID");
return { qrCodeFileId: qrCodeFileId };
const qrcodeUrl = fieldValue(data, ["qrcodeUrl", "qrCodeUrl", "fileUrl", "url", "ossUrl"]) || fieldValue(res, ["qrcodeUrl", "qrCodeUrl", "fileUrl", "url", "ossUrl"]);
if (!qrCodeFileId && !qrcodeUrl) throw new Error("收款码上传成功,但未返回二维码文件信息");
return { qrCodeFileId: qrCodeFileId, qrcodeUrl: qrcodeUrl };
});
}
function appendQrcodePayload(payload, uploadData) {
if (uploadData && uploadData.qrCodeFileId) payload.qrCodeFileId = uploadData.qrCodeFileId;
if (uploadData && uploadData.qrcodeUrl) payload.qrcodeUrl = uploadData.qrcodeUrl;
return payload;
}
+67 -33
View File
@@ -139,28 +139,6 @@
</button>
</div>
</div>
<div class="wechatBindCard" id="wechat-bind-card">
<div class="wechatBindTop">
<div class="wechatBindIconBox">
<img class="wechatBindIcon" src="../public/img/pay-wechat.png" alt="微信" />
</div>
<div class="wechatBindInfo">
<div class="wechatBindTitle">微信账号</div>
<div class="wechatBindDesc" id="wechat-bind-desc">正在检测绑定状态...</div>
</div>
<div class="wechatBindStateDot"></div>
</div>
<div class="wechatBindLine"></div>
<button
type="button"
class="wechatBindButton"
id="wechat-bind-button"
onclick="openWeChatBindPopup()"
>
绑定微信
</button>
</div>
<!-- Balance Cards -->
<div class="uc-balance-grid">
<div class="uc-balance-item">
@@ -535,9 +513,24 @@
}
function updateExpertActions(data) {
var expert = isExpertUser(data);
$("#expertApplyBtn").toggle(!expert);
$("#publishArticleBtn").toggle(expert);
var state = CommonUtil.getExpertState
? CommonUtil.getExpertState(data)
: { isExpert: isExpertUser(data), isApplying: false };
var $applyBtn = $("#expertApplyBtn");
var $publishBtn = $("#publishArticleBtn");
$publishBtn.toggle(state.isExpert);
$applyBtn.toggle(!state.isExpert);
if (state.isExpert) {
return;
}
$applyBtn
.text(state.isApplying ? "申请审核中" : "专家申请")
.prop("disabled", !!state.isApplying);
if (state.isApplying) {
$applyBtn.removeAttr("onclick");
} else {
$applyBtn.attr("onclick", "window.location.href = 'apply.html'");
}
}
function loadAccountInfoForWechat() {
@@ -1379,7 +1372,7 @@
? currentUserData.nickName || currentUserData.username || ""
: "";
uploadedAvatarUrl = "";
uploadedAvatarId = "";
userAvatarFile = null;
var content =
@@ -1422,14 +1415,14 @@
content: content,
end: function () {
editProfileLayerIndex = null;
uploadedAvatarUrl = "";
uploadedAvatarId = "";
userAvatarFile = null;
},
});
});
}
var uploadedAvatarUrl = "";
var uploadedAvatarId = "";
var editProfileLayerIndex = null;
var userAvatarFile = null;
@@ -1443,14 +1436,19 @@
if (!input.files || !input.files[0]) return;
var file = input.files[0];
if (!/^image\//.test(file.type || "")) {
layui.layer.msg("请选择图片文件", { icon: 2 });
if (!/^(image\/jpeg|image\/png)$/.test(file.type || "")) {
layui.layer.msg("只支持 JPG/PNG 格式的图片", { icon: 2 });
input.value = "";
return;
}
if (file.size > 2 * 1024 * 1024) {
layui.layer.msg("图片大小不能超过 2MB", { icon: 2 });
input.value = "";
return;
}
userAvatarFile = file;
uploadedAvatarUrl = "";
uploadedAvatarId = "";
var reader = new FileReader();
reader.onload = function (e) {
@@ -1504,31 +1502,67 @@
if (!avatarId) {
throw new Error("头像上传返回缺少文件编号");
}
uploadedAvatarUrl = avatarId;
uploadedAvatarId = avatarId;
return avatarId;
}
);
}
function validateEditProfileNickname(nickName) {
var nicknameWidth = Array.from(nickName).reduce(function (total, char) {
return total + (/[\u4e00-\u9fa5\uff00-\uffef]/.test(char) ? 2 : 1);
}, 0);
if (nickName.length < 2 || nicknameWidth > 12) {
layui.layer.msg("昵称长度应为2-6个汉字或2-12个英文", { icon: 2 });
$("#editNickName").focus();
return false;
}
return true;
}
function submitEditProfile() {
var nickName = $("#editNickName").val().trim();
var password = $("#editPassword").val();
var passwordConfirm = $("#editPasswordConfirm").val();
var currentNickName = currentUserData
? currentUserData.nickName || currentUserData.username || ""
: "";
if (!nickName) {
layui.layer.msg("昵称不能为空", { icon: 2 });
return;
}
if (nickName !== currentNickName && !validateEditProfileNickname(nickName)) {
return;
}
if ((password || passwordConfirm) && (!password || !passwordConfirm)) {
layui.layer.msg("请输入并确认新密码", { icon: 2 });
return;
}
if (password && password !== passwordConfirm) {
layui.layer.msg("两次密码输入不一致", { icon: 2 });
return;
}
if (password && (password.length < 6 || password.length > 20)) {
layui.layer.msg("密码长度应为6-20位", { icon: 2 });
return;
}
var params = { username: nickName };
var params = {};
if (nickName !== currentNickName) {
params.nickName = nickName;
}
if (password) {
params.password = hex_md5(password);
}
if (!userAvatarFile && Object.keys(params).length === 0) {
layui.layer.msg("暂无修改", { icon: 0 });
return;
}
var $submitBtn = $("#editProfileSubmitBtn");
$submitBtn.prop("disabled", true).text("保存中...");
+48 -44
View File
@@ -1,4 +1,4 @@
<!doctype html>
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
@@ -90,6 +90,11 @@
<div class="table-section-title">
<i class="layui-icon layui-icon-group"></i> 下级用户列表
</div>
<div class="promo-filter" id="distStatusFilter">
<button class="active" type="button" data-status="0">未转余额</button>
<button type="button" data-status="1">已转余额</button>
<button type="button" data-status="2">已提现</button>
</div>
<table id="distTable" lay-filter="distTable" lay-even></table>
<div id="distPage"></div>
</div>
@@ -131,6 +136,9 @@
var $ = layui.$;
var table = layui.table;
var laypage = layui.laypage;
var currentPage = 1;
var pageSize = 20;
var currentStatus = 0;
// Load page config
CommonUtil.loadPageConfig({ ads: false, links: false });
@@ -206,10 +214,11 @@
function loadDistList(pageNum) {
currentPage = pageNum;
ApiClient.get(
ApiClient.API.referralInviteList,
ApiClient.API.referralRebateList,
{
pageNum: pageNum,
pageSize: pageSize,
status: currentStatus,
},
{
headers: CommonUtil.authHeaders(),
@@ -217,8 +226,9 @@
)
.then(function (res) {
if (res.code === 0 && res.data) {
renderTable(res.data.records || []);
renderPagination(res.data.total || 0);
var records = ApiClient.asArray(res.data.records || res.data.rows || res.data.list || res.data);
renderTable(records);
renderPagination(res.data.total || res.data.totalRow || records.length || 0);
} else {
layer.msg(res.msg || "获取数据失败", { icon: 2 });
}
@@ -240,73 +250,60 @@
elem: "#distTable",
cols: [
[
{ field: "id", title: "记录ID", width: 140, align: "center" },
{
field: "nickName",
title: "昵称",
width: 160,
align: "center",
},
{
field: "phoneNumber",
title: "手机号",
title: "订阅用户",
width: 160,
align: "center",
templet: function (d) {
if (d.phoneNumber) {
return d.phoneNumber.replace(
/(\d{3})\d{4}(\d{4})/,
"$1****$2",
);
}
return "--";
return CommonUtil.escapeHtml(d.nickName || d.userName || d.inviteeName || "--");
},
},
{
field: "level",
title: "层级",
width: 100,
field: "articleTitle",
title: "文章/彩种",
minWidth: 180,
align: "center",
templet: function (d) {
return d.level === 1
? '<span style="color:#102b6a;font-weight:600;">一级</span>'
: '<span style="color:#5b2c8e;font-weight:600;">二级</span>';
return CommonUtil.escapeHtml(d.menuName || d.articleTitle || d.title || "--");
},
},
{
field: "createTime",
title: "注册时间",
width: 180,
field: "expertName",
title: "专家",
width: 140,
align: "center",
templet: function (d) {
return d.createTime
? d.createTime.substring(0, 19).replace("T", " ")
: "--";
return CommonUtil.escapeHtml(d.nickNameExpert || d.expertName || "--");
},
},
{
field: "totalRecharge",
title: "累计充值",
field: "amount",
title: "订阅价格",
width: 130,
align: "center",
templet: function (d) {
return (
'<span style="color:#f15a22;font-weight:600;">' +
(d.totalRecharge || 0).toFixed(2) +
"</span>"
);
return CommonUtil.escapeHtml(d.subscriptionPrice || d.orderAmount || d.amount || "0.00");
},
},
{
field: "distributionIncome",
title: "分销收益",
field: "rebateMoney",
title: "分销金额",
width: 130,
align: "center",
templet: function (d) {
return (
'<span style="color:#fdb933;font-weight:600;">' +
(d.distributionIncome || 0).toFixed(2) +
"</span>"
);
return '<span style="color:#f15a22;font-weight:600;">' + CommonUtil.escapeHtml(d.rebateMoney || d.rebateAmount || d.money || "0.00") + "</span>";
},
},
{
field: "status",
title: "状态",
width: 130,
align: "center",
templet: function (d) {
var statusMap = { 0: "未转余额", 1: "已转余额", 2: "已提现" };
return CommonUtil.escapeHtml(d.statusText || statusMap[d.status] || statusMap[d.withdrawStatus] || "--");
},
},
],
@@ -333,6 +330,13 @@
},
});
}
$("#distStatusFilter").on("click", "button", function () {
$("#distStatusFilter button").removeClass("active");
$(this).addClass("active");
currentStatus = Number($(this).data("status")) || 0;
loadDistList(1);
});
});
</script>
</body>
+1 -1
View File
@@ -652,7 +652,7 @@ body {
.draw-card__balls--compact {
gap: 3px;
width: calc(22px * 10 + 3px * 9);
width: calc(25px * 10 + 3px * 9);
max-width: 100%;
}
+2 -2
View File
@@ -470,8 +470,8 @@ select {
.ball--compact {
flex-basis: 22px;
width: 22px;
min-width: 22px;
height: 22px;
min-width: 25px;
height: 25px;
font-size: 11px;
padding: 0;
}
+117 -4
View File
@@ -8,7 +8,7 @@
1. Breadcrumb Bar
---------------------------------------------------------- */
.breadcrumb-bar {
max-width: 1200px;
max-width: 1420px;
margin: 0 auto;
padding: 14px 20px 0;
font-size: 13px;
@@ -40,7 +40,7 @@
2. Page Title Bar & Period Selector
---------------------------------------------------------- */
.page-title-bar {
max-width: 1200px;
max-width: 1420px;
margin: 0 auto;
padding: 10px 20px 14px;
display: flex;
@@ -97,7 +97,7 @@
3. Table Wrapper & Horizontal Scroll
---------------------------------------------------------- */
.table-wrapper {
max-width: 1200px;
max-width: 1420px;
margin: 12px auto 40px;
padding: 0 20px;
overflow-x: auto;
@@ -191,6 +191,10 @@
background: rgba(16, 43, 106, 0.05);
}
.trend-table tbody tr.trend-period-separator td {
border-top: 2px solid rgba(245, 95, 122, 0.82);
}
/* ----------------------------------------------------------
5. Issue & Code Columns (sticky left)
---------------------------------------------------------- */
@@ -243,6 +247,56 @@
background: none;
}
.trend-open-code {
display: inline-flex;
align-items: center;
justify-content: center;
flex-wrap: nowrap;
gap: 6px;
white-space: nowrap;
color: #999;
}
.trend-open-number {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 10px;
height: 18px;
padding: 0;
border-radius: 0;
font-size: 12px;
font-weight: 700;
line-height: 18px;
background: transparent !important;
box-shadow: none !important;
text-shadow: none !important;
}
.trend-open-red,
.trend-open-pos1,
.trend-open-pos2,
.trend-open-pos3,
.trend-open-pos4,
.trend-open-pos5 {
color: rgb(245, 95, 122);
}
.trend-open-blue {
color: rgb(64, 137, 226);
}
.trend-open-separator {
color: var(--primary);
font-weight: 800;
margin: 0 2px;
}
.trend-open-empty {
color: var(--text-muted);
font-weight: 600;
}
/* ----------------------------------------------------------
6. Zone Header Colors
---------------------------------------------------------- */
@@ -357,6 +411,65 @@
box-shadow: 0 2px 6px rgba(241, 90, 34, 0.35);
}
.NumberText {
display: inline-flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
color: #c5c5c5;
font-size: 11px;
font-weight: 400;
}
.NumberText.redok,
.typeText.redok {
border-radius: 50%;
background: linear-gradient(135deg, #f15a22, #e74c3c);
color: #fff;
font-weight: 700;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.18);
box-shadow: 0 2px 6px rgba(241, 90, 34, 0.35);
}
.NumberText.blueok,
.typeText.blueok {
border-radius: 50%;
background: linear-gradient(135deg, #102b6a, #3b6fd4);
color: #fff;
font-weight: 700;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.18);
box-shadow: 0 2px 6px rgba(16, 43, 106, 0.35);
}
.trend-table .hit-hundred,
.trend-table .hit-ten,
.trend-table .hit-unit,
.trend-table .hit-pos1,
.trend-table .hit-pos2,
.trend-table .hit-pos3,
.trend-table .hit-pos4,
.trend-table .hit-pos5,
.trend-table .hit-p1,
.trend-table .hit-p2,
.trend-table .hit-p3,
.trend-table .hit-p4,
.trend-table .hit-p5,
.trend-table .hit-p6,
.trend-table .hit-p7,
.trend-table .hit-num,
.trend-table .hit-num.g2,
.trend-table .hit-main {
background: linear-gradient(135deg, #f15a22, #e74c3c);
box-shadow: 0 2px 6px rgba(241, 90, 34, 0.35);
}
.trend-table .hit-blue,
.trend-table .hit-special {
background: linear-gradient(135deg, #102b6a, #3b6fd4);
box-shadow: 0 2px 6px rgba(16, 43, 106, 0.35);
}
.hit-blue {
display: inline-flex;
align-items: center;
@@ -1010,4 +1123,4 @@
font-size: 9px !important;
padding-left: 4px !important;
}
}
}
+7
View File
@@ -407,6 +407,13 @@
transform: translateY(-2px);
}
.uc-btn:disabled {
cursor: not-allowed;
opacity: 0.65;
transform: none;
box-shadow: none;
}
.uc-btn-publish {
background: var(--bg-light);
color: var(--primary);
+11 -138
View File
@@ -11,7 +11,6 @@ const $ = layui.$;
let isSubmittingPayArticle = false;
let selections = [];
let stepsConfig = [];
const REQUIRED_DAILY_FREE_ARTICLES = 2;
// 页面加载完成后获取首页信息
layui.$(document).ready(function () {
CommonUtil.loadPageConfig({ ads: false, links: false });
@@ -33,149 +32,23 @@ const $ = layui.$;
});
});
// 判断布尔字段,兼容后端可能返回的 1/true/"true"
function isTruthy(value) {
return (
value === true ||
value === 1 ||
value === "1" ||
String(value).toLowerCase() === "true"
);
}
function padDatePart(value) {
return String(value).padStart(2, "0");
}
function formatLocalDate(date) {
return (
date.getFullYear() +
"-" +
padDatePart(date.getMonth() + 1) +
"-" +
padDatePart(date.getDate())
);
}
function getArticleDateText(value) {
if (!value) return "";
if (typeof value === "number") {
const numericDate = new Date(value);
return isNaN(numericDate.getTime())
? ""
: formatLocalDate(numericDate);
}
const text = String(value).trim();
const match = text.match(/^(\d{4})[-/](\d{1,2})[-/](\d{1,2})/);
if (match) {
return (
match[1] + "-" + padDatePart(match[2]) + "-" + padDatePart(match[3])
);
}
const parsedDate = new Date(text.replace(/-/g, "/"));
return isNaN(parsedDate.getTime()) ? "" : formatLocalDate(parsedDate);
}
function isPublishedFreeArticle(item) {
if (!item) return false;
const statusText = String(
item.statusText ||
item.auditStatusText ||
item.publishStatusText ||
item.stateText ||
"",
).toLowerCase();
if (/已发布|审核通过|通过|published|approved|pass/.test(statusText)) {
return true;
}
const status =
item.status != null
? item.status
: item.auditStatus != null
? item.auditStatus
: item.publishStatus;
return status === true || status === 1 || status === "1";
}
function getTodayPublishedFreeCount(rows) {
const today = formatLocalDate(new Date());
return ApiClient.asArray(rows).filter(function (item) {
const articleDate = getArticleDateText(
item.createTime ||
item.create_time ||
item.publishTime ||
item.publish_time ||
item.auditTime ||
item.audit_time ||
item.updateTime ||
item.update_time ||
item.createdAt,
);
return articleDate === today && isPublishedFreeArticle(item);
}).length;
}
function loadTodayPublishedFreeCount() {
return ApiClient.get(ApiClient.API.mineFreeArticleList, {
pageNum: 1,
pageSize: 100,
})
.then(function (res) {
if (!ApiClient.isSuccess(res)) return 0;
const data = res.data || res;
const rows = ApiClient.asArray(
data.rows || data.records || data.list || data.data || data,
);
return getTodayPublishedFreeCount(rows);
})
.catch(function () {
return 0;
});
}
// 付费文章发布权限:专家身份 + 当天 2 篇审核通过/已发布的免费文章。
// 付费文章发布权限以 mine/summary 返回字段为准
function checkPaidPublishPermission() {
return ApiClient.get(ApiClient.API.mineSummary)
.then(function (res) {
if (!ApiClient.isSuccess(res) || !res.data) {
if (
!ApiClient.isSuccess(res) ||
!res.data ||
!window.PublishPermission
) {
return { canPaid: false, reason: "暂时无法获取发布权限。" };
}
const status = String(
res.data.expertStatus || res.data.expert_status || "",
).toLowerCase();
const isRegularExpert =
isTruthy(res.data.is_regular_expert) ||
isTruthy(res.data.isRegularExpert) ||
isTruthy(res.data.regularExpert) ||
/正式|regular|formal/.test(status);
const hasExpertIdentity =
isTruthy(res.data.is_expert) ||
isTruthy(res.data.isExpert) ||
isTruthy(res.data.expert) ||
isRegularExpert ||
/实习|intern|trial|正式|regular|formal/.test(status);
if (!hasExpertIdentity || !isRegularExpert) {
return { canPaid: false, reason: "正式专家才能发布付费文章。" };
}
return loadTodayPublishedFreeCount().then(function (count) {
return {
canPaid: count >= REQUIRED_DAILY_FREE_ARTICLES,
reason:
count >= REQUIRED_DAILY_FREE_ARTICLES
? ""
: "今天需先发布2篇审核通过的免费文章后,才能发布付费文章(当前" +
count +
"篇)。",
};
});
const permission = PublishPermission.parseSummary(res.data);
return {
...permission,
reason: permission.reason || permission.paidReason,
};
})
.catch(function () {
return { canPaid: false, reason: "暂时无法获取发布权限。" };
+16 -2
View File
@@ -768,6 +768,19 @@ const ApiClient = {
const params = this.normalizePageParams(data || {});
const code = params.code || params.suoxie || params.lotteryCode || '';
const pageSize = params.pageSize || params.limit || params.count || 100;
const normalizeResponse = (res) => {
if (!this.isSuccess(res)) return res;
const payload = this.pickData(res, {});
const list = this.asArray(payload);
return Object.assign({}, res, {
code: 0,
success: true,
data: list,
rawData: payload,
rows: list,
records: list,
});
};
if (code && this.API.lotteryTrend) {
try {
const trendRes = await this.get(this.API.lotteryTrend, {
@@ -775,13 +788,14 @@ const ApiClient = {
pageSize,
});
if (this.isSuccess(trendRes) && this.asArray(this.pickData(trendRes, [])).length) {
return trendRes;
return normalizeResponse(trendRes);
}
} catch (e) {
// Fall back to the older result-list endpoint below.
}
}
return this.requestWithMenu('/api/web/lottery/result/list', data);
const listRes = await this.requestWithMenu('/api/web/lottery/result/list', data);
return normalizeResponse(listRes);
},
async loadLegacyExpertDetailCompat(data = {}) {
+333 -2
View File
@@ -3,6 +3,109 @@ const AuthPageUtil = {
slideUuid: '',
slideX: 0,
slideToken: '',
slideCaptchaViewportCleanup: null,
referralInviteSessionKey: 'pendingReferralInviteCode',
getSessionStorage() {
if (typeof sessionStorage !== 'undefined') return sessionStorage;
if (typeof window !== 'undefined' && window.sessionStorage) return window.sessionStorage;
return null;
},
normalizeReferralInviteCode(inviteCode) {
return String(inviteCode || '').trim();
},
getReferralInviteCodeFromUrl(search) {
const query =
search != null
? search
: (typeof window !== 'undefined' && window.location && window.location.search) || '';
const Params =
typeof URLSearchParams !== 'undefined'
? URLSearchParams
: typeof window !== 'undefined' && window.URLSearchParams;
if (!Params) return '';
try {
const params = new Params(query || '');
return this.normalizeReferralInviteCode(
params.get('inviteCode') || params.get('referralCode') || '',
);
} catch (error) {
return '';
}
},
setPendingReferralInviteCode(inviteCode) {
const code = this.normalizeReferralInviteCode(inviteCode);
const storage = this.getSessionStorage();
if (code && storage) {
storage.setItem(this.referralInviteSessionKey, code);
}
return code;
},
getPendingReferralInviteCode() {
const storage = this.getSessionStorage();
if (!storage) return '';
return this.normalizeReferralInviteCode(storage.getItem(this.referralInviteSessionKey));
},
clearPendingReferralInviteCode() {
const storage = this.getSessionStorage();
if (storage) storage.removeItem(this.referralInviteSessionKey);
},
captureReferralInviteFromUrl(search) {
const code = this.getReferralInviteCodeFromUrl(search);
if (code) {
this.setPendingReferralInviteCode(code);
}
return code;
},
bindPendingReferralInviteCode() {
const inviteCode = this.getPendingReferralInviteCode();
if (!inviteCode) {
return Promise.resolve({ skipped: true, reason: 'empty' });
}
if (
typeof ApiClient === 'undefined' ||
!ApiClient.API ||
!ApiClient.API.referralBind ||
typeof ApiClient.post !== 'function'
) {
return Promise.resolve({ skipped: true, reason: 'missing-api' });
}
if (typeof ApiClient.token === 'function' && !ApiClient.token()) {
return Promise.resolve({ skipped: true, reason: 'unauthenticated' });
}
return ApiClient.post(ApiClient.API.referralBind, { inviteCode })
.then((res) => {
const isSuccess =
typeof ApiClient.isSuccess === 'function'
? ApiClient.isSuccess(res)
: !!(res && (res.success === true || Number(res.code) === 0 || Number(res.code) === 200));
if (isSuccess) {
this.clearPendingReferralInviteCode();
return { skipped: false, success: true, res };
}
return { skipped: false, success: false, res };
})
.catch((error) => ({ skipped: false, success: false, error }));
},
isPhone(phone) {
return /^1[3-9]\d{9}$/.test(String(phone || '').trim());
@@ -122,6 +225,234 @@ const AuthPageUtil = {
}
},
blurActiveElement() {
if (typeof document === 'undefined') return;
const activeElement = document.activeElement;
if (activeElement && activeElement !== document.body && typeof activeElement.blur === 'function') {
activeElement.blur();
}
},
ensureSlideCaptchaMobileCenterStyle() {
if (typeof document === 'undefined' || document.getElementById('auth-slide-captcha-mobile-center-style')) {
return;
}
const style = document.createElement('style');
style.id = 'auth-slide-captcha-mobile-center-style';
style.textContent = `
@media (max-width: 640px) {
#rvplus-slide-captcha-root.rvplus-slide-mask {
box-sizing: border-box !important;
align-items: center !important;
justify-content: center !important;
width: 100vw;
max-width: 100vw;
min-height: 100vh;
min-height: 100dvh;
padding: 12px !important;
}
#rvplus-slide-captcha-root .rvplus-slide-modal {
box-sizing: border-box;
max-width: calc(100vw - 24px);
max-height: calc(100vh - 24px);
max-height: calc(100dvh - 24px);
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
}
`;
document.head.appendChild(style);
},
getSlideCaptchaViewport() {
const fallbackWidth =
(typeof window !== 'undefined' && window.innerWidth) ||
(typeof document !== 'undefined' && document.documentElement && document.documentElement.clientWidth) ||
0;
const fallbackHeight =
(typeof window !== 'undefined' && window.innerHeight) ||
(typeof document !== 'undefined' && document.documentElement && document.documentElement.clientHeight) ||
0;
const viewport = typeof window !== 'undefined' ? window.visualViewport : null;
const screenWidth = (typeof window !== 'undefined' && window.screen && window.screen.width) || 0;
const screenHeight = (typeof window !== 'undefined' && window.screen && window.screen.height) || 0;
const width = Math.round((viewport && viewport.width) || fallbackWidth);
const height = Math.round((viewport && viewport.height) || fallbackHeight);
let scale = Number(viewport && viewport.scale) || 1;
if (screenWidth && screenWidth <= 640 && width > screenWidth && scale === 1) {
scale = screenWidth / width;
}
return {
width,
height,
offsetLeft: Math.round((viewport && viewport.offsetLeft) || 0),
offsetTop: Math.round((viewport && viewport.offsetTop) || 0),
scale,
displayWidth: Math.round(width * scale),
displayHeight: Math.round(height * scale),
screenWidth,
screenHeight,
};
},
isCompactSlideCaptchaViewport(viewport) {
return viewport.displayWidth <= 640 || (viewport.screenWidth > 0 && viewport.screenWidth <= 640);
},
centerSlideCaptchaDialog() {
if (typeof document === 'undefined') return false;
const root = document.getElementById('rvplus-slide-captcha-root');
if (!root) return false;
const viewport = this.getSlideCaptchaViewport();
if (!viewport.width || !viewport.height) return true;
const compact = this.isCompactSlideCaptchaViewport(viewport);
const visualPadding = compact ? 12 : 20;
const padding = visualPadding / (viewport.scale || 1);
const modalWidth = Math.max(0, Math.round(viewport.width - padding * 2));
const modalHeight = Math.max(0, Math.round(viewport.height - padding * 2));
root.style.position = 'fixed';
root.style.inset = 'auto';
root.style.left = viewport.offsetLeft + 'px';
root.style.top = viewport.offsetTop + 'px';
root.style.width = viewport.width + 'px';
root.style.height = viewport.height + 'px';
root.style.display = 'flex';
root.style.alignItems = 'center';
root.style.justifyContent = 'center';
root.style.boxSizing = 'border-box';
root.style.padding = padding + 'px';
const modal = root.querySelector && root.querySelector('.rvplus-slide-modal');
if (modal) {
modal.style.boxSizing = 'border-box';
modal.style.maxWidth = modalWidth + 'px';
modal.style.maxHeight = modalHeight + 'px';
modal.style.overflowY = 'auto';
modal.style.webkitOverflowScrolling = 'touch';
modal.style.width = compact ? modalWidth + 'px' : '';
}
return true;
},
clearSlideCaptchaViewportCentering() {
if (typeof this.slideCaptchaViewportCleanup !== 'function') return;
const cleanup = this.slideCaptchaViewportCleanup;
this.slideCaptchaViewportCleanup = null;
cleanup();
},
scheduleSlideCaptchaCentering(callback) {
if (typeof window !== 'undefined' && typeof window.requestAnimationFrame === 'function') {
window.requestAnimationFrame(callback);
return;
}
callback();
},
bindSlideCaptchaViewportCentering() {
if (typeof window === 'undefined') return;
this.clearSlideCaptchaViewportCentering();
const cleanups = [];
const recenter = () => {
if (!this.centerSlideCaptchaDialog()) {
this.clearSlideCaptchaViewportCentering();
}
};
const schedule = () => this.scheduleSlideCaptchaCentering(recenter);
const addListener = (target, type) => {
if (target && typeof target.addEventListener === 'function') {
target.addEventListener(type, schedule);
cleanups.push(() => target.removeEventListener(type, schedule));
}
};
addListener(window.visualViewport, 'resize');
addListener(window.visualViewport, 'scroll');
addListener(window, 'resize');
addListener(window, 'orientationchange');
this.slideCaptchaViewportCleanup = () => {
cleanups.forEach((cleanup) => cleanup());
};
schedule();
if (typeof window.setTimeout === 'function') {
window.setTimeout(schedule, 80);
window.setTimeout(schedule, 260);
}
},
deferClearSlideCaptchaViewportCentering() {
const clear = () => this.clearSlideCaptchaViewportCentering();
if (typeof window !== 'undefined' && typeof window.setTimeout === 'function') {
window.setTimeout(clear, 320);
return;
}
clear();
},
wrapSlideCaptchaCallbacks(options = {}) {
const wrapped = { ...options };
const wrap = (callback) => (...args) => {
try {
if (typeof callback === 'function') {
return callback(...args);
}
} finally {
this.deferClearSlideCaptchaViewportCentering();
}
return undefined;
};
wrapped.onSuccess = wrap(options.onSuccess);
wrapped.onCancel = wrap(options.onCancel);
return wrapped;
},
patchSlideCaptchaMobileCentering(SlideCaptcha) {
if (!SlideCaptcha || SlideCaptcha.__authPageMobileCentered || typeof SlideCaptcha.open !== 'function') {
return SlideCaptcha;
}
const nativeOpen = SlideCaptcha.open.bind(SlideCaptcha);
const nativeClose = typeof SlideCaptcha.close === 'function' ? SlideCaptcha.close.bind(SlideCaptcha) : null;
SlideCaptcha.open = (options = {}) => {
this.blurActiveElement();
this.ensureSlideCaptchaMobileCenterStyle();
const result = nativeOpen(this.wrapSlideCaptchaCallbacks(options));
this.bindSlideCaptchaViewportCentering();
return result;
};
if (nativeClose) {
SlideCaptcha.close = (...args) => {
this.clearSlideCaptchaViewportCentering();
return nativeClose(...args);
};
}
SlideCaptcha.__authPageMobileCentered = true;
return SlideCaptcha;
},
startSmsCountdown(buttonSelector, seconds = 60) {
const $button = layui.$(buttonSelector);
let left = seconds;
@@ -140,7 +471,7 @@ const AuthPageUtil = {
loadSlideCaptchaScript() {
this.installSlideCaptchaTenantHeader();
if (window.SlideCaptcha) return Promise.resolve(window.SlideCaptcha);
if (window.SlideCaptcha) return Promise.resolve(this.patchSlideCaptchaMobileCentering(window.SlideCaptcha));
if (window.__slideCaptchaLoading) return window.__slideCaptchaLoading;
window.__slideCaptchaLoading = new Promise((resolve, reject) => {
const script = document.createElement('script');
@@ -148,7 +479,7 @@ const AuthPageUtil = {
script.dataset.apiBase = ApiClient.baseUrl || '';
script.dataset.tenantId = ApiClient.tenantId || '936208';
script.onload = () => {
if (window.SlideCaptcha) resolve(window.SlideCaptcha);
if (window.SlideCaptcha) resolve(this.patchSlideCaptchaMobileCentering(window.SlideCaptcha));
else reject(new Error('滑动验证组件加载失败'));
};
script.onerror = () => reject(new Error('滑动验证组件加载失败'));
+113
View File
@@ -148,6 +148,70 @@ const CommonUtil = {
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 '正式专家';
@@ -1752,6 +1816,55 @@ const CommonUtil = {
$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();
+139
View File
@@ -0,0 +1,139 @@
(function (global) {
var DEFAULT_REQUIRED_DAILY_FREE_ARTICLES = 2;
function isTruthy(value) {
return (
value === true ||
value === 1 ||
value === '1' ||
String(value).toLowerCase() === 'true'
);
}
function firstDefined(source, keys) {
if (!source || typeof source !== 'object') return undefined;
for (var i = 0; i < keys.length; i += 1) {
var key = keys[i];
if (
Object.prototype.hasOwnProperty.call(source, key) &&
source[key] !== undefined &&
source[key] !== null
) {
return source[key];
}
}
return undefined;
}
function toNumber(value, fallback) {
var numberValue = Number(value);
return Number.isFinite(numberValue) ? numberValue : fallback;
}
function buildQuotaReason(todayCount, requiredCount) {
return (
'今天需先发布' +
requiredCount +
'篇审核通过的免费文章后,才能发布付费文章(当前' +
todayCount +
'篇)'
);
}
function parseSummary(userData, options) {
var data = userData && typeof userData === 'object' ? userData : {};
var config = options || {};
var status = String(
firstDefined(data, ['expertStatus', 'expert_status']) || ''
).toLowerCase();
var isIntern = /实习|intern|trial/.test(status);
var isRegularByStatus = /正式|regular|formal/.test(status);
var isRegularExpert =
isTruthy(
firstDefined(data, [
'is_regular_expert',
'isRegularExpert',
'regularExpert',
])
) || isRegularByStatus;
var hasExpertIdentity =
isTruthy(firstDefined(data, ['is_expert', 'isExpert', 'expert'])) ||
isTruthy(
firstDefined(data, [
'is_regular_expert',
'isRegularExpert',
'regularExpert',
])
) ||
isIntern ||
isRegularByStatus;
var requiredCount = toNumber(
firstDefined(data, [
'requiredDailyApprovedFreeArticleCount',
'required_daily_approved_free_article_count',
]),
config.requiredDailyFreeArticleCount || DEFAULT_REQUIRED_DAILY_FREE_ARTICLES
);
var todayCount = toNumber(
firstDefined(data, [
'todayApprovedFreeArticleCount',
'today_approved_free_article_count',
]),
0
);
var quotaExemptRaw = firstDefined(data, [
'payArticleFreeQuotaExempt',
'pay_article_free_quota_exempt',
]);
var canPublishRaw = firstDefined(data, [
'canPublishPayArticle',
'can_publish_pay_article',
]);
var backendReason = String(
firstDefined(data, [
'payArticlePublishReason',
'pay_article_publish_reason',
]) || ''
);
var quotaExempt = isTruthy(quotaExemptRaw);
var hasBackendPaidDecision = canPublishRaw !== undefined;
var canPaid = false;
var paidReason = '';
if (quotaExempt) {
canPaid = true;
} else if (hasBackendPaidDecision) {
canPaid = isTruthy(canPublishRaw);
paidReason = canPaid
? ''
: backendReason ||
(!hasExpertIdentity || !isRegularExpert
? '正式专家才能发布付费文章'
: buildQuotaReason(todayCount, requiredCount));
} else if (!hasExpertIdentity || !isRegularExpert) {
paidReason = '正式专家才能发布付费文章';
} else {
canPaid = todayCount >= requiredCount;
paidReason = canPaid ? '' : buildQuotaReason(todayCount, requiredCount);
}
return {
hasExpertIdentity: hasExpertIdentity,
isRegularExpert: hasExpertIdentity && isRegularExpert,
canFree: hasExpertIdentity,
canPaid: canPaid,
quotaExempt: quotaExempt,
todayFreeArticleCount: todayCount,
requiredDailyFreeArticleCount: requiredCount,
paidReason: paidReason,
reason: paidReason,
};
}
global.PublishPermission = {
isTruthy: isTruthy,
parseSummary: parseSummary,
};
})(typeof window !== 'undefined' ? window : globalThis);