From 8a6a8acab708b76f4e87b6c4ef6df77d43e98ed4 Mon Sep 17 00:00:00 2001 From: rain Date: Tue, 7 Jul 2026 17:07:34 +0800 Subject: [PATCH] =?UTF-8?q?=E6=A0=B7=E5=BC=8F=E4=BF=AE=E6=94=B9=E5=AE=8C?= =?UTF-8?q?=E6=88=90=EF=BC=8C=E5=BE=85=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...erence-project-function-diff-2026-07-07.md | 334 +++++++++++ .../2026-07-07-functional-parity-plan.md | 567 ++++++++++++++++++ html/apply.html | 24 +- html/c.html | 3 + html/caiinfo.html | 38 +- html/chongzhi.html | 3 +- html/cjdlt.html | 13 +- html/fabumianfeiwenzhang.html | 160 +---- html/fc3d.html | 13 +- html/fufei.html | 1 + html/kl8.html | 17 +- html/login.html | 30 +- html/mianfei.html | 30 +- html/mianfeilist.html | 99 ++- html/pl3.html | 13 +- html/pl5.html | 13 +- html/qlc.html | 51 +- html/qxc.html | 34 +- html/reg.html | 58 +- html/ssq.html | 13 +- html/tixian.html | 8 +- html/usercenter.html | 100 ++- html/wodefenxiao.html | 92 +-- public/css/index.css | 2 +- public/css/public.css | 4 +- public/css/trend.css | 121 +++- public/css/usercenter.css | 7 + public/js/fufei-3dyjs-publish.js | 149 +---- utils/ApiClient.js | 18 +- utils/AuthPageUtil.js | 335 ++++++++++- utils/CommonUtil.js | 113 ++++ utils/PublishPermission.js | 139 +++++ 32 files changed, 2097 insertions(+), 505 deletions(-) create mode 100644 docs/reference-project-function-diff-2026-07-07.md create mode 100644 docs/superpowers/plans/2026-07-07-functional-parity-plan.md create mode 100644 utils/PublishPermission.js diff --git a/docs/reference-project-function-diff-2026-07-07.md b/docs/reference-project-function-diff-2026-07-07.md new file mode 100644 index 0000000..58666ee --- /dev/null +++ b/docs/reference-project-function-diff-2026-07-07.md @@ -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`。 diff --git a/docs/superpowers/plans/2026-07-07-functional-parity-plan.md b/docs/superpowers/plans/2026-07-07-functional-parity-plan.md new file mode 100644 index 0000000..382bc43 --- /dev/null +++ b/docs/superpowers/plans/2026-07-07-functional-parity-plan.md @@ -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 + +``` + +- [ ] 步骤 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` 保持一致。 +- 范围检查:这是多批次功能等价修复,每个任务都可以独立验证,并且必须按顺序执行。 diff --git a/html/apply.html b/html/apply.html index d5921be..d94bdf1 100644 --- a/html/apply.html +++ b/html/apply.html @@ -140,6 +140,7 @@ diff --git a/html/caiinfo.html b/html/caiinfo.html index 5d13056..603718c 100644 --- a/html/caiinfo.html +++ b/html/caiinfo.html @@ -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: diff --git a/html/chongzhi.html b/html/chongzhi.html index 3730b83..0e28179 100644 --- a/html/chongzhi.html +++ b/html/chongzhi.html @@ -1,4 +1,4 @@ - + @@ -563,7 +563,6 @@ status === "success" || status === "paid" || status === "2" || - status === "1" || Number(data.status) === 2 || Number(data.payStatus) === 2 ); diff --git a/html/cjdlt.html b/html/cjdlt.html index b55f512..34dc8ff 100644 --- a/html/cjdlt.html +++ b/html/cjdlt.html @@ -66,9 +66,10 @@
大乐透走势图
+
10期
+
20期
30期
60期
-
100期
@@ -216,9 +217,12 @@ backSet[n] = true; }); - html += ""; + html += + r > 0 && r % 10 === 0 + ? '' + : ""; html += '' + item.issue + ""; - html += '' + item.openCode + ""; + html += '' + CommonUtil.renderTrendOpenCode(item.openCode, { mode: "dlt" }) + ""; 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); } }); diff --git a/html/fabumianfeiwenzhang.html b/html/fabumianfeiwenzhang.html index fe707eb..115cb1f 100644 --- a/html/fabumianfeiwenzhang.html +++ b/html/fabumianfeiwenzhang.html @@ -7,6 +7,7 @@ + @@ -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, }; }); } diff --git a/html/fc3d.html b/html/fc3d.html index c567305..01d1d58 100644 --- a/html/fc3d.html +++ b/html/fc3d.html @@ -66,9 +66,10 @@
福彩3D走势图
+
10期
+
20期
30期
60期
-
100期
@@ -207,9 +208,12 @@ var parsed = parseCode(item.openCode); var type = getType(parsed.hundred, parsed.ten, parsed.unit); - html += ""; + html += + r > 0 && r % 10 === 0 + ? '' + : ""; html += '' + item.issue + ""; - html += '' + item.openCode + ""; + html += '' + CommonUtil.renderTrendOpenCode(item.openCode, { mode: "position" }) + ""; // 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); } }); diff --git a/html/fufei.html b/html/fufei.html index 5068733..e2454d7 100644 --- a/html/fufei.html +++ b/html/fufei.html @@ -196,6 +196,7 @@ + diff --git a/html/kl8.html b/html/kl8.html index d93b20d..00ce8b7 100644 --- a/html/kl8.html +++ b/html/kl8.html @@ -66,9 +66,10 @@
快乐8走势图
+
10期
+
20期
30期
60期
-
100期
@@ -77,7 +78,6 @@ 期号 - 开奖号 1-40 41-80 @@ -198,13 +198,15 @@ hitSet[n] = true; }); - html += ""; + html += + r > 0 && r % 10 === 0 + ? '' + : ""; html += '' + item.issue + ""; - html += '' + item.openCode + ""; 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 += '' + + '">' + labels[li] + ""; 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); } }); diff --git a/html/login.html b/html/login.html index c6a66da..1f3f2a9 100644 --- a/html/login.html +++ b/html/login.html @@ -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(); diff --git a/html/mianfei.html b/html/mianfei.html index 5006a5f..518c249 100644 --- a/html/mianfei.html +++ b/html/mianfei.html @@ -9,6 +9,7 @@ + @@ -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 '

暂无内容

'; } + if (typeof DOMPurify !== "undefined") { + html = DOMPurify.sanitize(html); + } + var scopedStyles = []; html = html.replace(/]*>([\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 || "无标题"; diff --git a/html/mianfeilist.html b/html/mianfeilist.html index 9ff56f1..b26d439 100644 --- a/html/mianfeilist.html +++ b/html/mianfeilist.html @@ -110,7 +110,7 @@ diff --git a/public/css/index.css b/public/css/index.css index 26e41de..8e22850 100644 --- a/public/css/index.css +++ b/public/css/index.css @@ -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%; } diff --git a/public/css/public.css b/public/css/public.css index 3922c84..07a0f98 100644 --- a/public/css/public.css +++ b/public/css/public.css @@ -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; } diff --git a/public/css/trend.css b/public/css/trend.css index f071516..8f983b1 100644 --- a/public/css/trend.css +++ b/public/css/trend.css @@ -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; } -} \ No newline at end of file +} diff --git a/public/css/usercenter.css b/public/css/usercenter.css index 8869e16..70c757b 100644 --- a/public/css/usercenter.css +++ b/public/css/usercenter.css @@ -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); diff --git a/public/js/fufei-3dyjs-publish.js b/public/js/fufei-3dyjs-publish.js index 4bff43f..8e7f247 100644 --- a/public/js/fufei-3dyjs-publish.js +++ b/public/js/fufei-3dyjs-publish.js @@ -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: "暂时无法获取发布权限。" }; diff --git a/utils/ApiClient.js b/utils/ApiClient.js index 569707a..6eda8e0 100644 --- a/utils/ApiClient.js +++ b/utils/ApiClient.js @@ -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 = {}) { diff --git a/utils/AuthPageUtil.js b/utils/AuthPageUtil.js index dacfb1d..2a17a43 100644 --- a/utils/AuthPageUtil.js +++ b/utils/AuthPageUtil.js @@ -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('滑动验证组件加载失败')); diff --git a/utils/CommonUtil.js b/utils/CommonUtil.js index 6e166c0..b131b9a 100644 --- a/utils/CommonUtil.js +++ b/utils/CommonUtil.js @@ -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 '--'; + + 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) => + `${this.escapeHtml(pad(String(num), shouldPad))}`; + const renderGroup = (list, className, shouldPad = true) => + list.map((num) => renderNum(num, className, shouldPad)).join(''); + const renderPlus = () => '+'; + 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 `${body || this.escapeHtml(value)}`; + }, + sanitizeHTML(html) { if (!html) return ''; const parser = new DOMParser(); diff --git a/utils/PublishPermission.js b/utils/PublishPermission.js new file mode 100644 index 0000000..7e795d3 --- /dev/null +++ b/utils/PublishPermission.js @@ -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);