进行了小优化
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
## 目标与边界
|
||||
|
||||
将“全局功能 → Email邮件”收敛为一套真正可用的邮件系统:后台完整展示 SMTP、发件品牌、认证/账号、内容互动、审核、私信、身份/账号处理、举报,以及订单、支付、提现、返佣、分成、转账等商城/资金类邮件;模板主题和正文默认直接显示,不再用空值加 placeholder 代替;所有真实发送统一由同一服务渲染和发送。
|
||||
|
||||
明确排除:
|
||||
|
||||
- 不实现论坛、版主、论坛帖子审核、回答采纳等论坛专属邮件。
|
||||
- 不实现邮箱回滚、撤销链接、回滚令牌或旧邮箱恢复流程;邮箱变更邮件只做安全通知。
|
||||
- 不做旧 option 的数据迁移,不保留新旧双路,不写兼容 adapter。
|
||||
- 不做浏览器或真实视觉验收,只做代码、契约、语法和构建验证。
|
||||
|
||||
## 1. 建立唯一邮件配置与模板目录
|
||||
|
||||
- 以 `aksrt_settings` 的 Email section 作为唯一配置来源;SMTP、品牌、通知开关和模板都通过 `/settings/schema/global/email` 读写。
|
||||
- 在邮件域建立集中式模板目录,统一声明每个非论坛事件的事件键、后台名称、默认开关、默认主题、默认 HTML 正文、占位符及说明。
|
||||
- 邮件运行时和后台 Schema 均从该目录生成,避免默认文案继续散落在业务调用点。
|
||||
- 对每个事件同时声明“开关、主题模板、正文模板”;模板正文在后台直接显示为非空默认值。数据库中已有空主题/正文时,仅在读取响应和表单初始化时解析为目录默认值,不执行迁移。
|
||||
- HTML 模板和 `mail_more_content` 使用 `wp_kses_post()` 清理;主题、SMTP host/username/name 等按文本清理;邮箱地址使用 `sanitize_email()`;端口限制到合法范围;加密方式只允许 `ssl`、`tls` 或空值;SMTP 密码使用 secret 掩码/清除机制。
|
||||
|
||||
## 2. 完整重做 Email 后台 Schema
|
||||
|
||||
在 `src/Admin/Schema/GlobalSchema.php` 中由集中目录生成 Email section;让 `SchemaPage.vue` 对 `global.email` 使用服务端 Schema,删除前端重复的静态 Email schema 定义,避免 PHP/JS 两份字段漂移。
|
||||
|
||||
页面分组与事件如下:
|
||||
|
||||
1. **管理员邮件推送**
|
||||
- 友情链接提交
|
||||
- 新订单
|
||||
- 提现申请
|
||||
- 投稿待审核
|
||||
- 身份认证申请
|
||||
- 账号封禁申诉
|
||||
- 举报提交
|
||||
|
||||
2. **用户邮件推送**
|
||||
- 注册验证码
|
||||
- 注册成功
|
||||
- 免密登录验证码
|
||||
- 密码重置验证码
|
||||
- 邮箱/敏感操作验证码
|
||||
- 邮箱变更安全通知
|
||||
- 新订单/支付结果
|
||||
- 提现处理结果
|
||||
- 返佣到账
|
||||
- 创作分成到账
|
||||
- 收到积分/余额转账
|
||||
- 评论通过审核
|
||||
- 评论回复
|
||||
- 投稿发布/驳回结果
|
||||
- 手机号修改通知
|
||||
- 身份认证审核结果
|
||||
- 账号封禁/解封通知
|
||||
- 举报处理结果
|
||||
- 私信通知
|
||||
- 私信邮件用户组限制
|
||||
- 所有用户
|
||||
- 所有会员
|
||||
- 二级会员及以上
|
||||
- 仅管理员
|
||||
|
||||
3. **自定义发件人/品牌**
|
||||
- `mail_showname`,默认站点名
|
||||
- `mail_title_prefix`,默认 `[站点名]`
|
||||
- `mail_description`,默认“此信为系统邮件,请不要直接回复。”
|
||||
- `mail_more_content`,默认显示“访问网站 / 联系站长”等基础 HTML 链接
|
||||
|
||||
4. **SMTP 配置**(沿用参考主题语义,但使用当前主题唯一字段契约)
|
||||
- `mail_smtps`
|
||||
- `mail_name`
|
||||
- `mail_passwd`
|
||||
- `mail_smtp_name`
|
||||
- `mail_host`
|
||||
- `mail_port`
|
||||
- `mail_smtpauth`
|
||||
- `mail_smtpsecure`
|
||||
- SMTP 风险提示和依赖显示
|
||||
|
||||
5. **测试邮件**
|
||||
- Email 页面内提供测试收件地址和发送操作。
|
||||
- 有未保存修改时禁止测试并提示先保存,确保测试当前生效配置。
|
||||
- 测试邮件走统一 HTML 包装、品牌和 SMTP 主线。
|
||||
|
||||
每个事件组直接显示“启用开关 + 主题模板 + HTML 正文模板 + 可用占位符说明”。论坛事件不进入 Schema、模板目录、默认值或运行时注册。
|
||||
|
||||
## 3. 实现参考主题式统一邮件渲染
|
||||
|
||||
重构 `src/Settings/Mail.php` 和 `inc/core-hooks/part-2.php`:
|
||||
|
||||
- 集中服务完成事件查询、开关判断、占位符替换、主题前缀、HTML 安全处理、品牌包装和 `wp_mail()` 调用。
|
||||
- 复刻参考主题邮件信封:站点 Logo、主体内容、站点名称、底部说明、附加链接及现有 `assets/img/mail-bg.png` 背景图。
|
||||
- 主题前缀只添加一次,并清除 WordPress 默认重复 `[站点名]` 前缀。
|
||||
- 发件人名称保留明确优先级:`mail_showname` → 站点名;SMTP 发件地址和 SMTPAuth 使用同一 Email section 的 `mail_*` 字段。
|
||||
- 将 `send_event_email()` 统一为事件键、收件人、占位符(必要时允许结构化上下文),默认模板不再由调用方重复传入。
|
||||
- 未知事件失败关闭;无效收件地址不发送;事件关闭时不发送。
|
||||
- 统一管理员收件人获取、去重和有效邮箱过滤。
|
||||
|
||||
## 4. 接通所有非论坛真实业务发送点
|
||||
|
||||
新增聚合通知注册类,基于当前主题已有 WordPress/AKSRT action 注册邮件发送;不凭空添加论坛功能。每个事件都复用集中模板目录和统一发送服务。
|
||||
|
||||
### 认证与账号
|
||||
|
||||
- 改造 `UserControllerAuthPhoneOperations.php`:注册验证码、免密登录验证码、密码重置验证码按独立事件发送;失败时删除验证码 transient,维持现有频率限制和安全行为。
|
||||
- 注册成功在用户创建成功且用户对象有效时发送,提供用户名、站点名、个人中心/站点链接等占位符。
|
||||
- 改造 `account-ajax.php`:敏感操作验证码、邮箱变更通知走统一服务;删除直接 `wp_mail()` fallback;不增加邮箱回滚。
|
||||
- 手机号绑定/换绑成功后向有效邮箱发送通知。
|
||||
|
||||
### 内容互动与审核
|
||||
|
||||
- 评论通过审核、评论回复:接入 WordPress 评论状态/回复钩子,避免重复状态流转、自己回复自己和无效邮箱。
|
||||
- 投稿待审核与发布/驳回结果:接入现有 `aksrt_new_posts_pending` 与文章状态转换,只处理当前主题实际支持的投稿内容,防止重复通知。
|
||||
- 友情链接提交:接入当前友情链接写入成功动作,向有效管理员邮箱发送。
|
||||
|
||||
### 身份、账号、举报和私信
|
||||
|
||||
- 身份认证申请/审核、封禁申诉/处理、举报提交/处理:接入现有成功动作;若成功路径缺少动作,只在事务成功点增加明确的 `aksrt_*` action。
|
||||
- 私信通知:接入现有消息/通知主线,只处理 private 类型;实现参考主题的用户组限制,并过滤无效或占位邮箱。
|
||||
|
||||
### 订单与资金业务(保留)
|
||||
|
||||
- 订单创建/支付成功/支付失败或状态变化:接入当前支付订单状态 action,分别通知管理员、买家及适用的订单关联用户。
|
||||
- 提现申请与处理结果:接入当前提现申请、审核/完成/拒绝路径,通知管理员和申请用户。
|
||||
- 返佣通知:接入当前 referral/佣金到账 action,向推荐人发送订单和佣金信息。
|
||||
- 创作分成通知:接入当前 ledger/收益分配成功 action,向分成作者发送到账明细。
|
||||
- 积分/余额转账通知:接入当前资产转账完成 action,向收款用户发送转账信息。
|
||||
- 上述商城/资金事件只使用仓库中已存在的订单、支付、ledger、referral、资产动作和数据结构;若某事件没有可直接监听的成功动作,在对应现有事务完成点补充 action,不复制业务逻辑、不新建第二套交易流程。
|
||||
|
||||
论坛/版主/回答采纳动作不注册任何邮件监听器,也不产生对应模板字段。
|
||||
|
||||
## 5. 删除旧接口、旧存储与绕路代码
|
||||
|
||||
- 删除 `GlobalController` 的 `/settings/mail` 路由和 `rest_handle_mail()`,不再使用 `aksrt_mail_config`。
|
||||
- 保留 `/mail/test` 作为动作接口,但重写为统一邮件服务测试;配置读写只有 Schema 路由。
|
||||
- 删除前端 `getMailSettings()` / `saveMailSettings()`,只保留测试邮件 API 方法。
|
||||
- 删除 `UserController` 的 `/settings/mail-templates` 路由、`handle_mail_templates()` 和 `aksrt_mail_templates` 契约。
|
||||
- 删除 `Settings::get_smtp()` 对 `aksrt_mail_smtp` 的读取,并从 standalone/secret option 注册表移除该旧 option;SMTP 只读 Email section 的统一字段。
|
||||
- 删除认证、账号和各业务模块中邮件服务不存在时的直接 `wp_mail()` fallback;正常业务只允许集中邮件服务内部调用 `wp_mail()`。
|
||||
- 删除旧六事件空模板回退参数、重复前端 Email Schema 及不再使用的旧字段;不读取、不转换旧 option 值。
|
||||
- 不删除数据库中的旧 option 值,但代码完全停止消费它们;不执行迁移脚本或双写。
|
||||
|
||||
## 6. 契约测试与构建验证
|
||||
|
||||
新增邮件系统契约测试/harness,覆盖:
|
||||
|
||||
- 后端 Email Schema 包含认证、账号、内容互动、审核、私信、身份/账号处理、举报、订单、支付、提现、返佣、分成、转账等事件。
|
||||
- Schema 明确不包含 `bbs`、moderator、answer-adopted 及其它论坛字段。
|
||||
- 每个主题和正文默认值非空;空存储读取时后台仍返回可见模板;模板占位符替换后无未知占位符。
|
||||
- HTML 邮件信封包含品牌说明、附加内容、Logo 和背景图;主题前缀不重复;发件人和 SMTP 字段优先级稳定。
|
||||
- SMTP 开关、认证、host、port、encryption、secret 掩码/清除行为正确。
|
||||
- 事件关闭、未知事件、无效收件地址均不发送。
|
||||
- 注册/登录/重置/邮箱变更路径不再绕过集中服务,发送失败时验证码缓存处理正确;不出现邮箱回滚入口或令牌。
|
||||
- 评论、投稿、友情链接、身份认证、封禁、举报、私信及所有订单/资金事件分别绑定真实 action,并覆盖去重、无效邮箱和边界条件。
|
||||
- 论坛事件不注册、不发送、不出现在后台;订单/支付/提现/返佣/分成/转账事件必须存在并可配置。
|
||||
- 旧 `/settings/mail`、`/settings/mail-templates`、`aksrt_mail_config`、`aksrt_mail_templates`、`aksrt_mail_smtp` 和旧前端 API 不再存在。
|
||||
- 全仓库业务代码中的 `wp_mail()` 只剩集中邮件服务内部调用。
|
||||
|
||||
执行验证:
|
||||
|
||||
1. 对所有变更 PHP 文件运行 `php -l`。
|
||||
2. 运行新增邮件契约测试及受影响的 admin/UI 契约测试。
|
||||
3. 在 `assets/admin` 执行 `npm run build`,由 Vite 清理旧 chunk 并更新完整 `assets/admin/build/`。
|
||||
4. 使用 `rg` 做最终静态检查:无论坛邮件字段、无旧 option/API、无散落直接 `wp_mail()`,且订单/支付/提现/返佣/分成/转账事件均有 Schema、模板目录和发送入口。
|
||||
5. 不启动浏览器、不截图、不做真实视觉效果验收。
|
||||
@@ -0,0 +1,142 @@
|
||||
## 目标与边界
|
||||
|
||||
将 AKSRT 前台 DOM 架构一次性硬切换为 `C:\Users\Xing\Desktop\zibll` 的真实架构:
|
||||
|
||||
```text
|
||||
WordPress 配置/_win
|
||||
→ jQuery → Bootstrap → runtime/loader.js
|
||||
→ tbquire(['main'])
|
||||
→ main.js 顶层全局委托 + window.auto_fun()
|
||||
→ 局部 AJAX:post_ajax.ed
|
||||
→ JSON AJAX:zib_ajax.success
|
||||
→ 远程 modal:loaded.bs.modal → auto_fun()
|
||||
```
|
||||
|
||||
不保留 `AksrtFrontend`、不写兼容适配器、不做双路初始化、不保留“迁移中”分支。保留 AKSRT 的业务能力和安全处理,但全部挂到参考主题的唯一 `main/auto_fun` owner 上。按用户要求只做静态、契约、VM、语法和构建验证,不做真实视觉验收。
|
||||
|
||||
## 一、运行时入口硬切换
|
||||
|
||||
1. **重写 `assets/js/runtime/loader.js`**
|
||||
- 以参考主题 loader 为行为基准,恢复 RequireJS 改名运行时、`tbquire.config()` 和最后的 `tbquire(['main'])`。
|
||||
- 根据 AKSRT 实际目录调整 `baseUrl` 与 URL 版本参数,但保持参考主题的加载语义和 module mapping 结构。
|
||||
- 将当前仍有业务消费者的模块纳入 mapping:main、imgbox、page-navs、section-navs、comment、message、input-expand、mini-upload、page-template、page-edit、author、poster-share、captcha、pay、swiper、dplayer、lazySizes、clipboard、svg-icon、enlighter、translate 等;每个 mapping 只指向一个真实文件,不复制第二份实现。
|
||||
- 删除当前仅为 `AksrtFrontend`/`aksrt.tbquire` 服务的映射和 fallback。
|
||||
|
||||
2. **重写 `assets/js/runtime/main.js`**
|
||||
- 以参考主题 `zibll/js/main.js` 的全局顺序为骨架:顶层建立 `_win.bd`/`_win.window`,注册 body/document 委托,定义全局 API 与插件,定义 `z_auto_fun`,暴露 `window.auto_fun = debounce(z_auto_fun, 100)`,顶层首次调用 `auto_fun()`,最后执行单一 `$(document).ready(...)` 首屏逻辑。
|
||||
- 将 AKSRT 当前 `aksrt-runtime.js` 中仍需要的 declarative action、确认、通知、上传/安全/站点功能直接并入该唯一运行时或对应 `tbquire` 模块,不再通过第二个 runtime 注册表调用。
|
||||
- 将所有按 DOM 条件加载逻辑集中到 `z_auto_fun()`:支付、消息、输入扩展、上传、海报分享、验证码、复制、tooltip、popover、affix、Swiper、DPlayer、SVG、代码高亮、灯箱等,严格按参考主题调用 `tbquire()`。
|
||||
- 删除 `AksrtFrontend.register('aksrt-legacy-main', ...)`、`window.auto_fun` 到 `AksrtFrontend.rehydrate()` 的 wrapper、`z_auto_fun()` 中对新 registry 的依赖。
|
||||
- 将原先局部更新后的 `AksrtFrontend.rehydrate(root, 'ajax'/'modal')` 全部改为参考主题调用顺序:注入完成后触发 `post_ajax.ed`(列表场景)或 `loaded.bs.modal`(远程 modal),随后调用防抖 `auto_fun()`。
|
||||
- 保留必要的幂等 class/data 标记(如 `is-on`、`swiper-container-initialized`、`dplayer-initialized`),但删除无法再被调用的 registry 保护代码。
|
||||
|
||||
3. **删除新生命周期核心**
|
||||
- 删除 `assets/js/rehydrate.js`。
|
||||
- 将 `assets/js/aksrt-runtime.js` 中仍需的实际业务行为迁移到 `runtime/main.js` 或对应按需模块后,删除该文件及其作为公共入口的职责;不得留下空壳 `aksrt-runtime` 适配层。
|
||||
- 清理所有 `AksrtFrontend.register/boot/rehydrate`、`aksrt.autoFun`、`aksrt:ready`、`aksrt:rehydrated` 引用和注释。
|
||||
|
||||
## 二、公共构建与 PHP 入队重构
|
||||
|
||||
1. **调整 `tools/build-public-assets.mjs`**
|
||||
- 不再把整个 canonical 模块集合拼成一个 `public.js` 再依赖 `AksrtFrontend`。
|
||||
- 以参考主题的独立 loader + 独立动态模块为构建图;对仍需生成的压缩文件只从唯一源码生成,避免 source/min 双 owner。
|
||||
- 保留 CSS bundle 构建;JS 构建只生成 loader/main 及实际 mapping 所需的 min 产物。
|
||||
- 生成后执行静态顺序检查,确保 loader 先于 main 动态加载,且没有 `tbquire(['main'])` 之外的第二个 boot。
|
||||
|
||||
2. **调整 `src/Core/Scripts.php`、`ScriptsPart1b.php`、`ScriptsPart1c.php` 及 WidgetSystem AssetManager**
|
||||
- 前台基础依赖改为 jQuery → Bootstrap → `runtime/loader.js`,不再将 `build/public.js` 作为全局生命周期入口。
|
||||
- 删除所有 routed script 对 `aksrt-runtime` 的依赖;路由模块改由 loader/main 按 DOM 条件加载,直接遵循参考主题的条件加载方式。
|
||||
- 将 `_win` 作为唯一前台运行时配置对象;把必须保留的 AKSRT 数据字段合并进去,避免同时维护 `_win`、`aksrtData` 两套 DOM 生命周期配置。
|
||||
- WidgetSystem 只保留一个前端 owner:将 widget 的初始化接入 `auto_fun`/对应动态模块,禁止 `assets/widgets/frontend.js` 与旧 widget-set/main 重复初始化同一节点;同步调整第三方资源注册和依赖。
|
||||
- 清理已迁移到 loader/main 的独立 enqueue、重复 inline boot、重复条件判断和无实际动作的 Swiper 注册。
|
||||
|
||||
## 三、局部更新协议统一为参考主题行为
|
||||
|
||||
1. **列表 AJAX:`assets/js/core/ajax-list.js`、`homepage-fixed.js`、`pagination.js`、`comments.js`、`routes/user-center.js`、`author.js`、`widgets/frontend.js` 等**
|
||||
- 保留现有请求、安全、错误提示和业务响应格式。
|
||||
- 删除直接 `AksrtFrontend.rehydrate()` 的调用。
|
||||
- 按场景统一为:before 状态 → HTML parse/replace/append → 触发容器上的 `post_ajax.ed` → `auto_fun()`;保留参考主题的 `ing`/loading/分页/route 顺序。
|
||||
- 对复杂 panel 继续使用现有 destroy 能力(例如订单中心),但 destroy 只是业务实例清理,不再扩展成第二套全局 lifecycle。
|
||||
- 明确旧 `.ajax-next` 与新 `data-aksrt-*` 的唯一 ownership;最终删除重复 click handler,控制只由一个 `post_ajax` 实现处理。
|
||||
|
||||
2. **JSON AJAX:`comment-ajax.js`、`auth.js`、`follow.js`、`credits.js`、`reward.js`、`messages.js` 等**
|
||||
- 统一使用参考主题的 `zib_ajax()` 语义:按钮状态、错误/成功提示、`zib_ajax.success` 事件、业务 callback、可选 modal/reload。
|
||||
- 删除以 `aksrt:rehydrated` 或 registry 事件作为成功信号的路径;只有真正 HTML 注入后才调用 `auto_fun()`。
|
||||
|
||||
3. **远程 modal 与历史路由**
|
||||
- modal HTML 更新改为 Bootstrap `loaded.bs.modal`,由 main 的全局监听调用 `auto_fun()`。
|
||||
- 保留 `history.pushState/replaceState` 与 Tab `popstate` 行为,但删除 `AksrtFrontend` 路由 API 和独立页面 boot;不引入 PJAX、Turbolinks、InstantClick 或全局 MutationObserver 重初始化。
|
||||
|
||||
## 四、灯箱、懒加载和 DOM 标记恢复为参考主题契约
|
||||
|
||||
1. **灯箱硬切换**
|
||||
- 删除 `AksrtLightbox`、`data-aksrt-lightbox*`、`aksrt-lightbox` overlay 和其专用 CSS/配置桥接。
|
||||
- 从参考主题恢复单一 `imgbox` owner:新增/恢复 `assets/js/runtime/imgbox.js`(压缩产物由构建生成),使用 `Miniimgbox`、`imgbox-container`、`a[data-imgbox]`、`imgbox_data` 和参考主题的幂等标记。
|
||||
- `main.js` 只在 `_win.imgbox` 且存在目标 DOM 时 `tbquire(['imgbox'])`;不得再有第二个灯箱监听器。
|
||||
- 更新 `single.php`、`page.php`、gallery/content renderer、图片 helper、相关 CSS 和设置输出,使 DOM 属性、分组、缩略图、zoom/full/play/down 开关与参考主题一致。
|
||||
|
||||
2. **懒加载单一 owner**
|
||||
- 删除 `lazy-image-attr.js` bridge 及其 enqueue/build/test 依赖;lazySizes、`lazybeforeunveil`、`data-src`/`data-srcset`/`data-bg`、兼容属性处理全部由 main/lazySizes owner 负责。
|
||||
- 保留必要的 PHP 图片 URL 生成和占位图能力,但统一输出参考主题可识别的 DOM,不再并行维护第二套 lazy switch 或 IntersectionObserver 初始化。
|
||||
- 保留 lazySizes 自有 MutationObserver;禁止把它扩展成全局组件 rehydrate 机制。
|
||||
|
||||
## 五、所有业务模块去 registry 化
|
||||
|
||||
逐一处理当前已注册的 public/routed 文件(article、comments、comment-fixed-input、pagination、archive、footer-tabbar、misc、follow、enhancements、enhancements2、color-extract、effects、captcha、auth、author、page-navs、section-navs、messages、front-submit、mini-upload、page-template、page-edit、post-share、poster-share、pay-vip、video-player、download-status、ban-appeal、docs-navigation、input-expand、waterfall、yiyan、site-notice、user-badge-actions、widgets 等):
|
||||
|
||||
- 删除 `AksrtFrontend.register()` 包装和对 registry API 的防御性分支。
|
||||
- 将脚本改成参考主题式:被 `tbquire` 加载后立即定义/绑定,或暴露单一模块初始化函数供 main 调用;依赖 DOM 委托和 `is-on`/class/data 标记实现重复调用安全。
|
||||
- 任何需要窗口/文档监听的模块只绑定一次,并使用参考主题的全局 owner;不新增 destroy registry、root 广播或双入口 ready。
|
||||
- 保留 AKSRT 的 nonce、REST 权限、输出转义和业务接口,不改变安全边界。
|
||||
|
||||
## 六、死代码清理
|
||||
|
||||
在改造前后用 `rg/git grep` 建立引用图,仅删除已确认无入口的代码,避免把仍被动态 loader 使用的资源误删:
|
||||
|
||||
- 删除 `assets/js/revenue.js`(确认无 PHP、JS、构建和模板引用后)。
|
||||
- 删除当前 `rehydrate.js`、`aksrt-runtime.js` 生命周期专用代码及其测试夹具。
|
||||
- 删除 `runtime/main.js` 中已迁移功能的 registry 适配、注释掉的 waterfall 分支、未使用 `auto_fun` 转发代码和重复事件派发。
|
||||
- 删除确认无 mapping、enqueue、模板或第三方引用的 `runtime/widget-set.*`、`runtime/mini-touch.*`、`waterfall.*` 等 orphan;若仍被参考主题式 loader 使用,则保留唯一映射,不凭文件名删除。
|
||||
- 删除 `AksrtLightbox`、旧 overlay、runtime/imgbox 的冲突副本和无消费者的 `aksrt.*` API;`_win`、`tbquire`、main、loader、Swiper/DPlayer/lazySizes 只在确认所有消费者迁移后保留。
|
||||
- 同步清理文档、注释、PHP handles、构建清单、测试中的旧 token,确保仓库不存在“旧入口还能被重新接回”的死配置。
|
||||
|
||||
## 七、测试与非视觉验证改造
|
||||
|
||||
1. 删除/重写锁定旧架构的测试:
|
||||
- `global-dom-init-contract.test.cjs` → `legacy-loader-runtime-contract.test.cjs`:验证 loader config、mapping、`tbquire(['main'])`、唯一 boot owner。
|
||||
- `rehydrate-runtime-contract.test.cjs` → `legacy-auto-fun-contract.test.cjs`:VM 验证 `auto_fun` 防抖、重复调用、`auto_fun` 事件、异常隔离和 DOM 标记幂等。
|
||||
- `public-bundle-parity-contract.test.cjs`:改为验证 loader/main 构建图,不再断言 `AksrtFrontend`。
|
||||
- `homepage-dom-contract.test.cjs`:改为验证 HTML 注入后 `post_ajax.ed → auto_fun` 顺序和唯一 delegated owner。
|
||||
- `lightbox-contract.test.cjs`:改为 `Miniimgbox`/`data-imgbox`/`imgbox` mapping 契约。
|
||||
- `lazyload-contract.test.cjs`:改为 main/lazySizes 单 owner 契约。
|
||||
- `legacy-navigation-contract.test.cjs`:验证 loader 的 page-navs/section-navs mapping 和 `auto_fun` 条件加载。
|
||||
|
||||
2. 新增/保留 VM 与静态契约:
|
||||
- loader URL 解析和模块缓存只发生一次。
|
||||
- 同一 DOM 多次 `auto_fun()` 不重复绑定 tooltip、Swiper、DPlayer、imgbox、上传和 widget。
|
||||
- `post_ajax.ed`、`zib_ajax.success`、`loaded.bs.modal` 三种事件不混用。
|
||||
- 全仓库禁止 `AksrtFrontend`、`aksrt.autoFun`、`aksrt:ready`、`aksrt:rehydrated`、第二套 `DOMContentLoaded` 页面 boot。
|
||||
- 静态检查没有 PJAX/InstantClick/全局 MutationObserver reinit 新入口。
|
||||
|
||||
3. 继续运行与改造后的验证:
|
||||
- Node widget/navigation 契约测试。
|
||||
- `tools/test-credits-rehydrate.mjs`(改名或改断言为 auto_fun 重入 harness)。
|
||||
- WidgetSystem PHP audit、asset ledger、业务安全/支付/评论相关测试。
|
||||
- `node --check` 检查所有改动后的 JS 和构建脚本。
|
||||
- `php -l` 检查所有改动后的 PHP。
|
||||
- `node tools/build-public-assets.mjs` 后再次运行 bundle/source parity、旧入口扫描和 `git diff --check`。
|
||||
|
||||
## 八、实施顺序与完成标准
|
||||
|
||||
1. 先完成只读引用图和测试基线,记录当前失败/通过集合。
|
||||
2. 先改 loader/main/构建/PHP 入队,建立唯一运行时入口。
|
||||
3. 再批量去掉所有 registry 调用并统一 AJAX/modal 事件顺序。
|
||||
4. 再恢复参考主题灯箱和 lazySizes owner,更新模板/CSS/配置。
|
||||
5. 最后删除确认死代码与旧测试,重建生成产物。
|
||||
6. 完成标准:
|
||||
- 前台只有 `loader → main → auto_fun` 一条初始 DOM 路径。
|
||||
- 没有任何 `AksrtFrontend` 或其双路兼容代码。
|
||||
- 列表、JSON、modal 三类更新分别遵循参考主题事件协议。
|
||||
- Swiper、DPlayer、lazySizes、imgbox、WidgetSystem 各只有一个 owner。
|
||||
- 所有已确认死代码和旧契约被删除或改写。
|
||||
- 非视觉测试、语法检查、构建和静态引用审计通过;不执行真实视觉验收。
|
||||
@@ -0,0 +1,51 @@
|
||||
## 修复方案
|
||||
|
||||
### 1. 删除废弃的静态 action 入口
|
||||
|
||||
修改 `assets/js/runtime/main.js`:
|
||||
|
||||
- 完整删除全局 `_win.bd.on('click', '[data-action]', ...)` 委托。
|
||||
- 完整删除仅被该委托调用的 `action_ajax()` 函数。
|
||||
- 保留紧随其后的独立 `[ajax-action]` 处理器以及其他合法的 `data-action` 属性读取。
|
||||
- 不创建 `assets/action/action.php`,不增加兼容代理,不把旧协议映射到新接口。
|
||||
|
||||
仓库内文章互动、评论点赞、关注等已有 WordPress AJAX/REST 实现,因此这是单一路径硬切换。
|
||||
|
||||
### 2. 精确同步运行时压缩产物
|
||||
|
||||
修改 `assets/js/runtime/main.min.js`:
|
||||
|
||||
- 从唯一 `_win.bd.on("click","[data-action]"` 起点,精确删除到下一个 `_win.bd.on("click","[ajax-action]"` 起点之前的旧压缩块。
|
||||
- 不执行全量前端构建,以免覆盖或重新格式化其他已有运行时产物。
|
||||
- 删除后确认 `[ajax-action]` 处理器仍存在,`action_ajax` 和 `/action/action.php` 均消失。
|
||||
|
||||
当前该文件虽然出现在状态列表中,但内容与 HEAD 完全一致,因此可安全形成仅针对旧块的内容差异。
|
||||
|
||||
### 3. 增加硬切换契约测试
|
||||
|
||||
新增 `tests/widget-system/runtime-action-hard-cut-contract.test.cjs`:
|
||||
|
||||
- 同时检查 `main.js` 与 `main.min.js`。
|
||||
- 断言旧全局 `[data-action]` 委托不存在。
|
||||
- 断言 `action_ajax` 不存在。
|
||||
- 断言 `/action/action.php` 不存在。
|
||||
- 断言独立 `[ajax-action]` 处理器仍保留且只有一份。
|
||||
- 不禁止整个仓库使用 `data-action`,避免误伤下载、弹窗和其他新业务属性。
|
||||
|
||||
### 4. 验证
|
||||
|
||||
- 运行新增的 runtime action 契约测试。
|
||||
- 运行全部 `tests/widget-system/*.test.cjs`。
|
||||
- 使用 `rg` 检查源码和压缩产物中不存在 `action/action.php`、`action_ajax` 和旧委托。
|
||||
- 执行 `git diff --check`,确认没有格式问题。
|
||||
- 不做真实视觉验收,不恢复静态 PHP 直连入口。
|
||||
|
||||
### 5. 提交
|
||||
|
||||
从当前 `origin/main` 创建 `worktree/fix-legacy-action-404` 分支,只提交:
|
||||
|
||||
- `assets/js/runtime/main.js`
|
||||
- `assets/js/runtime/main.min.js`
|
||||
- `tests/widget-system/runtime-action-hard-cut-contract.test.cjs`
|
||||
|
||||
现有其他 CSS、JavaScript 状态项和 `.zcode/plans` 文件不进入提交。验证通过后推送该分支到远端。
|
||||
@@ -0,0 +1,34 @@
|
||||
## 目标
|
||||
将 `inc/core-hooks/` 下的三个分片收口为唯一实现文件 `inc/core-hooks.php`,不保留兼容 loader、旧分片 stub 或双路加载;同时清理有充分证据确认无效的代码,不做真实浏览器/视觉效果验收。
|
||||
|
||||
## 实施步骤
|
||||
1. **合并实现文件**
|
||||
- 将 `inc/core-hooks/part-1.php`、`part-2.php`、`part-3.php` 的有效实现按现有加载顺序拼接到 `inc/core-hooks.php`。
|
||||
- 保留现有 `defined('ABSPATH') || exit;` 保护和原有 hook 注册顺序,避免改变 WordPress 回调优先级、字符串回调和匿名闭包的行为。
|
||||
- 在合并文件中保留评论过滤、SMTP/邮件、分类 term meta、资源归档/搜索、Enlighter 白名单、后台互动列、Gutenberg、付费内容等仍被运行时代码或测试覆盖的实现。
|
||||
- 去除原文件的 “Shell loader / split into sub-files” 说明和三个 `require_once`,将文件头改为描述单一核心 hooks 文件的注释,避免留下已不存在的拆分结构说明。
|
||||
|
||||
2. **清理明确死代码**
|
||||
- 删除 `part-2.php` 中注册 `wp_head` 调用未定义 `aksrt_print_win_var()` 的 legacy runtime bridge;仓库内只有该调用,没有定义、生产者或测试,且 `function_exists()` 使其当前为空操作。
|
||||
- 删除 `part-3.php` 中原样返回模板的 `single_template` no-op 过滤器;它不改变模板选择,且没有其他实现依赖。
|
||||
- 删除随之失去运行时意义的纯注释占位块(评论提交说明),不触碰实际由 `Moderation` 等模块实现的评论校验。
|
||||
- 不删除后台互动列,尽管它读取旧 meta key;这是可见后台行为,且证据只支持“过时/可能失配”,不足以在本次合并中安全移除或重写。也不删除分类旧字段、资源路由、块注册等看似重复但仍有明确消费者的代码。
|
||||
|
||||
3. **删除旧分片**
|
||||
- 删除 `inc/core-hooks/part-1.php`、`inc/core-hooks/part-2.php`、`inc/core-hooks/part-3.php`,使 `inc/core-hooks.php` 成为唯一实现,不做迁移层或双路兼容。
|
||||
|
||||
4. **同步测试引用**
|
||||
- 将以下契约测试中对分片的直接读取改为 `inc/core-hooks.php`:
|
||||
- `tests/widget-system/email-system-contract.test.cjs`
|
||||
- `tests/widget-system/pay-card-contract.test.cjs`
|
||||
- `tests/widget-system/post-visual-contract.test.cjs`
|
||||
- 不改测试断言语义,仅修正因文件收口导致的路径。
|
||||
|
||||
5. **验证**
|
||||
- 对新的 `inc/core-hooks.php` 运行 `php -l`。
|
||||
- 运行上述三个 Node 契约测试,并运行 `tests/widget-system/run.php` 作为已有回归合同检查(若环境依赖导致失败,如实报告)。
|
||||
- 用 `rg` 检查生产代码和测试中不再残留 `core-hooks/part-*.php` 路径;排除 `.git`、`vendor`、`node_modules` 和历史 `.zcode/plans` 文件。
|
||||
- 不启动浏览器、不执行真实视觉效果验收,符合请求约束。
|
||||
|
||||
## 工作区安全边界
|
||||
当前未提交修改仅涉及 `assets/css/build/*`、`assets/js/runtime/*` 和 `.zcode/plans/*`,不涉及本次目标文件;实施时只修改上述 core hooks 文件及三个测试文件,不覆盖或清理现有其他改动。
|
||||
@@ -0,0 +1,23 @@
|
||||
1. 余额页改为单一路径 DOM 记录切换
|
||||
- 以 `inc/helpers/user-center-renderers/profile-credits.php` 现有 `data-aksrt-asset-tab` / `data-aksrt-asset-pane` 结构为唯一 UI 状态源,保持积分记录、余额记录、积分任务都通过按钮事件切换,不再依赖 URL hash 或 query。
|
||||
- 在 `assets/js/credits.js` 扩展现有 `initAssetTabs()`:拦截记录分页链接,阻止整页导航;读取分页链接的页码/资产类型,调用当前已有的 `/user-center/panel/credits` REST 面板接口,通过响应 HTML 更新 `#panel-credits` 的 DOM;更新后重新绑定 tab,并清理地址栏中 `wallet_page`、`credits_page`、`points_page` 等历史参数。
|
||||
- 将 PHP 记录渲染改为读取由 REST 请求传入的分页参数,而不是直接读取 `$_GET`;移除 `aksrt_render_profile_pagination()` 生成的 query 链接,改为输出带 `data-aksrt-asset-page`、`data-aksrt-asset-key` 的按钮/链接,供 `credits.js` 处理。
|
||||
- 保留服务端 REST 面板的单一 HTML 输出,不新增 JSON/HTML 双协议;分页请求只使用现有 credits panel endpoint,并在响应后触发既有 `post_ajax.ed → auto_fun` 生命周期。
|
||||
- 用户中心初始化时不再把 `reward_tab`、`commission_page`、`user_page` 等记录参数写回用户地址;对 `/user/balance/` 初次加载清理历史记录参数,避免切换后地址继续携带旧参数。
|
||||
|
||||
2. 首页排序固定为 DOM/AJAX,不保留参数刷新分支
|
||||
- `assets/js/homepage-fixed.js` 删除 `SORT_QUERY_KEY`、`getSortFromUrl()`、`normalizeHomepageUrl()`、`replaceUrlSort()` 及对 `window.location.search` 的依赖;当前排序只从活动排序控件或 `aksrtData.homepageDefaultSort` 读取。
|
||||
- `initSortbar()` 点击排序项后只更新 active 状态并调用现有 `requestHomepagePanel()`,不再调用 `history.replaceState()`,因此地址栏保持无 `home_sort` 参数。
|
||||
- `inc/helpers/aksrt-homepage.php` 排序控件改为无参数链接(或按钮化标记,沿用现有 `data-sort`),删除 `add_query_arg('home_sort', ...)`、`data-sort-ajax` 双路开关和服务端 query override;服务端首页排序只使用 canonical 默认/运行时 override,避免生成旧的参数刷新 URL。
|
||||
- 清理 `src/Core/ScriptsPart1c.php`、`src/Settings/HomeCms.php`、`src/Settings/HomepageReader.php` 中仅服务于可选排序 AJAX 开关的 `homepageSortAjax` / `homepage_sort_ajax_enabled` 死代码及数据注入。
|
||||
- 删除首页排序请求中仅为 URL 刷新服务的 `page_url` 参数,或将其固定为当前无 query 的页面路径;保留 REST 请求内部用于确定上下文的必要字段。
|
||||
|
||||
3. 清理历史 query 分支和测试
|
||||
- 通过 `rg` 找到并删除余额记录的 `wallet_page` / `credits_page` 旧 `$_GET` 分支、首页 `home_sort` query 读取/写入分支,以及相关旧注释/配置字段。
|
||||
- 增加/更新 `tests/widget-system` 契约:余额记录控件必须是 DOM data 事件、分页不得生成 `add_query_arg`/query href;首页排序必须调用 `requestHomepagePanel` 且不得使用 `home_sort`、`replaceState`、`location.search`、`data-sort-ajax`。
|
||||
- 只保留真正需要的 `user-center` REST 参数(例如 revenue 内部分页若仍有明确业务需求则保留在请求体/URL 内部,不写浏览器地址);不把现有其他用户中心 tab 的必要内部请求参数误删。
|
||||
|
||||
4. 验证
|
||||
- 仅使用 `rg`、Node 契约测试和 PHP 语法检查,不做真实视觉效果验收。
|
||||
- 运行余额/用户中心、首页、生命周期、资产账本相关测试;对所有改动 PHP/JS 执行 `php -l` / `node --check`。
|
||||
- 运行 `git diff --check`,并确认不覆盖工作树中已有的 VIP、runtime、CSS、imgbox 或计划文件改动。
|
||||
+631
-5
@@ -1,11 +1,637 @@
|
||||
<?php
|
||||
/**
|
||||
* Shell loader — split into sub-files for maintainability.
|
||||
* Original: 1050 lines
|
||||
* AKSRT Theme Core Hooks
|
||||
*
|
||||
* Central registration for the theme's WordPress hooks.
|
||||
*
|
||||
* @package AKSRT
|
||||
* @version 3.2.0
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
require_once __DIR__ . '/core-hooks/part-1.php';
|
||||
require_once __DIR__ . '/core-hooks/part-2.php';
|
||||
require_once __DIR__ . '/core-hooks/part-3.php';
|
||||
// ============================================================
|
||||
// 评论功能开关
|
||||
// ============================================================
|
||||
|
||||
add_filter('comments_open', function ($open, $post_id) {
|
||||
if (class_exists('\AKSRT\Settings\Settings')) {
|
||||
$display = \AKSRT\Settings\Settings::get_comment_display_settings();
|
||||
if (!empty($display['close_comments'])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return $open;
|
||||
}, 10, 2);
|
||||
|
||||
add_filter('comment_text', function ($comment_text, $comment = null, $args = []) {
|
||||
if (function_exists('aksrt_render_comment_content')) {
|
||||
return aksrt_render_comment_content((string) $comment_text);
|
||||
}
|
||||
return $comment_text;
|
||||
}, 20, 3);
|
||||
|
||||
// ============================================================
|
||||
|
||||
// 前端隐藏 WordPress Admin Bar
|
||||
// ============================================================
|
||||
|
||||
add_filter('show_admin_bar', '__return_false');
|
||||
|
||||
// ============================================================
|
||||
// SMTP 邮件配置注入
|
||||
// ============================================================
|
||||
|
||||
add_filter('wp_mail_from', function ($from) {
|
||||
if (!class_exists('\AKSRT\Settings\Settings')) return $from;
|
||||
$mail = \AKSRT\Settings\Settings::get_smtp();
|
||||
if (empty($mail['enabled'])) return $from;
|
||||
$configured = sanitize_email((string) ($mail['from_address'] ?? ''));
|
||||
return $configured !== '' ? $configured : $from;
|
||||
});
|
||||
|
||||
add_filter('wp_mail_from_name', function ($from_name) {
|
||||
if (!class_exists('\AKSRT\Settings\Settings')) return $from_name;
|
||||
$mail = \AKSRT\Settings\Settings::get_smtp();
|
||||
if (empty($mail['enabled'])) return $from_name;
|
||||
$configured = trim((string) ($mail['from_name'] ?? ''));
|
||||
return $configured !== '' ? $configured : $from_name;
|
||||
});
|
||||
|
||||
add_action('phpmailer_init', function ($phpmailer) {
|
||||
$mail = class_exists('\\AKSRT\\Settings\\Settings')
|
||||
? \AKSRT\Settings\Settings::get_smtp()
|
||||
: [];
|
||||
if (empty($mail['enabled']) || empty($mail['host'])) return;
|
||||
|
||||
$phpmailer->isSMTP();
|
||||
$phpmailer->Host = $mail['host'];
|
||||
$phpmailer->Port = (int) ($mail['port'] ?? 465);
|
||||
$phpmailer->SMTPAuth = !empty($mail['auth']) && !empty($mail['username']);
|
||||
$phpmailer->Username = (string) ($mail['username'] ?? '');
|
||||
$phpmailer->Password = (string) ($mail['password'] ?? '');
|
||||
$phpmailer->SMTPSecure = (string) ($mail['encryption'] ?? 'ssl');
|
||||
$phpmailer->Timeout = 10;
|
||||
$phpmailer->Timelimit = 15;
|
||||
});
|
||||
|
||||
// mail_title_prefix is applied by the canonical renderer and by third-party wp_mail callers.
|
||||
add_filter('wp_mail', function ($atts) {
|
||||
if (!class_exists('\AKSRT\Settings\Settings') || empty($atts['subject'])) return $atts;
|
||||
$branding = \AKSRT\Settings\Settings::get_mail_branding();
|
||||
$subject = (string) $atts['subject'];
|
||||
$site = wp_specialchars_decode(get_option('blogname'), ENT_QUOTES);
|
||||
$subject = preg_replace('/^\[' . preg_quote($site, '/') . '\]\s*/u', '', $subject);
|
||||
if ($branding['title_prefix'] !== '' && strpos($subject, $branding['title_prefix']) !== 0) {
|
||||
$atts['subject'] = $branding['title_prefix'] . ' ' . $subject;
|
||||
} else {
|
||||
$atts['subject'] = $subject;
|
||||
}
|
||||
return $atts;
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// 分类 Term Meta 字段(封面图 + 推荐描述)
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* 编辑分类页面 - 显示自定义字段
|
||||
*/
|
||||
function aksrt_category_meta_fields($term) {
|
||||
$cover_id = (int) get_term_meta($term->term_id, 'aksrt_category_cover_id', true);
|
||||
$cover_url = $cover_id > 0 ? wp_get_attachment_url($cover_id) : '';
|
||||
$recommend_desc = (string) get_term_meta($term->term_id, 'aksrt_category_recommend_desc', true);
|
||||
?>
|
||||
<tr class="form-field">
|
||||
<th scope="row"><label for="aksrt_category_cover_id"><?php esc_html_e('分类封面图', 'aksrt'); ?></label></th>
|
||||
<td>
|
||||
<div id="aksrt-cover-preview" style="margin-bottom:8px;">
|
||||
<?php if ($cover_url) : ?>
|
||||
<img src="<?php echo esc_url($cover_url); ?>" style="max-width:200px;height:auto;border-radius:6px;" alt="">
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<input type="hidden" id="aksrt_category_cover_id" name="aksrt_category_cover_id" value="<?php echo esc_attr((string) $cover_id); ?>">
|
||||
<button type="button" class="button" id="aksrt-pick-cover"><?php esc_html_e('选择图片', 'aksrt'); ?></button>
|
||||
<button type="button" class="button" id="aksrt-remove-cover" <?php echo $cover_id > 0 ? '' : 'style="display:none;"'; ?>><?php esc_html_e('移除', 'aksrt'); ?></button>
|
||||
<p class="description"><?php esc_html_e('分类推荐模块的封面图片。', 'aksrt'); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="form-field">
|
||||
<th scope="row"><label for="aksrt_category_recommend_desc"><?php esc_html_e('推荐描述', 'aksrt'); ?></label></th>
|
||||
<td>
|
||||
<textarea id="aksrt_category_recommend_desc" name="aksrt_category_recommend_desc" rows="3" class="large-text"><?php echo esc_textarea($recommend_desc); ?></textarea>
|
||||
<p class="description"><?php esc_html_e('分类推荐模块中显示的描述文字。留空则使用分类默认描述。', 'aksrt'); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增分类页面 - 显示自定义字段
|
||||
*/
|
||||
function aksrt_category_meta_fields_add() {
|
||||
?>
|
||||
<div class="form-field">
|
||||
<label for="aksrt_category_cover_id"><?php esc_html_e('分类封面图', 'aksrt'); ?></label>
|
||||
<div id="aksrt-cover-preview" style="margin-bottom:8px;"></div>
|
||||
<input type="hidden" id="aksrt_category_cover_id" name="aksrt_category_cover_id" value="">
|
||||
<button type="button" class="button" id="aksrt-pick-cover"><?php esc_html_e('选择图片', 'aksrt'); ?></button>
|
||||
<button type="button" class="button" id="aksrt-remove-cover" style="display:none;"><?php esc_html_e('移除', 'aksrt'); ?></button>
|
||||
<p class="description"><?php esc_html_e('分类推荐模块的封面图片。', 'aksrt'); ?></p>
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label for="aksrt_category_recommend_desc"><?php esc_html_e('推荐描述', 'aksrt'); ?></label>
|
||||
<textarea id="aksrt_category_recommend_desc" name="aksrt_category_recommend_desc" rows="3"></textarea>
|
||||
<p class="description"><?php esc_html_e('分类推荐模块中显示的描述文字。留空则使用分类默认描述。', 'aksrt'); ?></p>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
add_action('category_edit_form_fields', 'aksrt_category_meta_fields', 10, 1);
|
||||
add_action('category_add_form_fields', 'aksrt_category_meta_fields_add', 10, 1);
|
||||
|
||||
/**
|
||||
* 保存分类 term meta
|
||||
*/
|
||||
function aksrt_save_category_meta(int $term_id) {
|
||||
if (!current_user_can('manage_categories')) return;
|
||||
|
||||
$cover_id = isset($_POST['aksrt_category_cover_id']) ? absint($_POST['aksrt_category_cover_id']) : 0;
|
||||
$recommend_desc = isset($_POST['aksrt_category_recommend_desc']) ? sanitize_textarea_field(wp_unslash($_POST['aksrt_category_recommend_desc'])) : '';
|
||||
|
||||
if ($cover_id > 0) {
|
||||
update_term_meta($term_id, 'aksrt_category_cover_id', $cover_id);
|
||||
} else {
|
||||
delete_term_meta($term_id, 'aksrt_category_cover_id');
|
||||
}
|
||||
|
||||
if ($recommend_desc !== '') {
|
||||
update_term_meta($term_id, 'aksrt_category_recommend_desc', $recommend_desc);
|
||||
} else {
|
||||
delete_term_meta($term_id, 'aksrt_category_recommend_desc');
|
||||
}
|
||||
|
||||
if (class_exists('\AKSRT\Settings\Settings')) {
|
||||
\AKSRT\Settings\Settings::flush_category_recommend_cache();
|
||||
}
|
||||
}
|
||||
|
||||
add_action('edited_category', 'aksrt_save_category_meta', 10, 1);
|
||||
add_action('created_category', 'aksrt_save_category_meta', 10, 1);
|
||||
|
||||
/**
|
||||
* 分类编辑页加载媒体库脚本(wp_enqueue_media 仅在 AKSRT 面板加载,此处补上)
|
||||
*/
|
||||
add_action('admin_enqueue_scripts', function (string $hook_suffix) {
|
||||
if ($hook_suffix === 'edit-tags.php' || $hook_suffix === 'term.php') {
|
||||
wp_enqueue_media();
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 分类编辑页内联 JS - 媒体选择器
|
||||
*/
|
||||
function aksrt_category_meta_admin_js() {
|
||||
$screen = get_current_screen();
|
||||
if (!$screen || $screen->taxonomy !== 'category') return;
|
||||
?>
|
||||
<script>
|
||||
jQuery(function($) {
|
||||
var frame;
|
||||
|
||||
$(document).on('click', '#aksrt-pick-cover', function(e) {
|
||||
e.preventDefault();
|
||||
if (typeof wp === 'undefined' || !wp.media) return;
|
||||
if (frame) { frame.open(); return; }
|
||||
|
||||
frame = wp.media({
|
||||
title: '<?php echo esc_js(__('选择分类封面图', 'aksrt')); ?>',
|
||||
button: { text: '<?php echo esc_js(__('使用此图片', 'aksrt')); ?>' },
|
||||
multiple: false
|
||||
});
|
||||
|
||||
frame.on('select', function() {
|
||||
var attachment = frame.state().get('selection').first().toJSON();
|
||||
$('#aksrt_category_cover_id').val(attachment.id);
|
||||
$('#aksrt-cover-preview').html('<img src="' + attachment.url + '" style="max-width:200px;height:auto;border-radius:6px;" alt="">');
|
||||
$('#aksrt-remove-cover').show();
|
||||
});
|
||||
|
||||
frame.open();
|
||||
});
|
||||
|
||||
$(document).on('click', '#aksrt-remove-cover', function(e) {
|
||||
e.preventDefault();
|
||||
$('#aksrt_category_cover_id').val('');
|
||||
$('#aksrt-cover-preview').html('');
|
||||
$(this).hide();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<?php
|
||||
}
|
||||
|
||||
add_action('admin_footer', 'aksrt_category_meta_admin_js');
|
||||
|
||||
// ============================================================
|
||||
// 资源列表:/resources 虚拟归档(post + _aksrt_is_resource)
|
||||
// ============================================================
|
||||
|
||||
add_action('init', function () {
|
||||
add_rewrite_rule('^resources/?$', 'index.php?aksrt_is_resource_archive=1', 'top');
|
||||
add_rewrite_rule('^resources/page/([0-9]+)/?$', 'index.php?aksrt_is_resource_archive=1&paged=$matches[1]', 'top');
|
||||
// 旧单篇路径 /resource/{slug} → 301 到 post 永久链接
|
||||
add_rewrite_rule('^resource/([^/]+)/?$', 'index.php?aksrt_legacy_resource=$matches[1]', 'top');
|
||||
});
|
||||
|
||||
add_filter('query_vars', function ($vars) {
|
||||
$vars[] = 'aksrt_is_resource_archive';
|
||||
$vars[] = 'aksrt_legacy_resource';
|
||||
$vars[] = 'type';
|
||||
$vars[] = 'trem';
|
||||
return $vars;
|
||||
});
|
||||
|
||||
add_action('template_redirect', function () {
|
||||
$slug = get_query_var('aksrt_legacy_resource');
|
||||
if (!$slug) {
|
||||
return;
|
||||
}
|
||||
$page = get_page_by_path((string) $slug, OBJECT, 'post');
|
||||
if ($page) {
|
||||
wp_redirect(get_permalink($page->ID), 301);
|
||||
exit;
|
||||
}
|
||||
});
|
||||
|
||||
add_action('pre_get_posts', function ($query) {
|
||||
if (is_admin() || !$query->is_main_query()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ((bool) $query->get('aksrt_is_resource_archive')) {
|
||||
$query->set('post_type', 'post');
|
||||
$query->set('post_status', 'publish');
|
||||
|
||||
$orderby = isset($_GET['orderby']) ? sanitize_key((string) $_GET['orderby']) : 'date';
|
||||
if ($orderby === 'downloads') {
|
||||
$query->set('meta_key', \AKSRT\Core\DomainModel::RESOURCE_DOWNLOAD_COUNT_KEY);
|
||||
$query->set('orderby', 'meta_value_num');
|
||||
$query->set('order', 'DESC');
|
||||
} elseif ($orderby === 'views') {
|
||||
$query->set('meta_key', \AKSRT\Core\DomainModel::RESOURCE_VIEW_COUNT_KEY);
|
||||
$query->set('orderby', 'meta_value_num');
|
||||
$query->set('order', 'DESC');
|
||||
}
|
||||
|
||||
$price_filter = isset($_GET['price']) ? sanitize_key((string) $_GET['price']) : '';
|
||||
$price_min = isset($_GET['price_min']) && $_GET['price_min'] !== '' ? (float) $_GET['price_min'] : null;
|
||||
$price_max = isset($_GET['price_max']) && $_GET['price_max'] !== '' ? (float) $_GET['price_max'] : null;
|
||||
|
||||
$meta_query = [
|
||||
'relation' => 'AND',
|
||||
[
|
||||
'key' => \AKSRT\Core\DomainModel::RESOURCE_MARKER_KEY,
|
||||
'value' => '1',
|
||||
'compare' => '=',
|
||||
],
|
||||
];
|
||||
|
||||
if ($price_filter === 'free') {
|
||||
$meta_query[] = [
|
||||
'relation' => 'OR',
|
||||
[
|
||||
'key' => \AKSRT\Core\DomainModel::PAY_PRICE_KEY,
|
||||
'value' => 0,
|
||||
'compare' => '=',
|
||||
'type' => 'NUMERIC',
|
||||
],
|
||||
[
|
||||
'key' => \AKSRT\Core\DomainModel::PAY_PRICE_KEY,
|
||||
'compare' => 'NOT EXISTS',
|
||||
],
|
||||
];
|
||||
} elseif ($price_filter === 'paid') {
|
||||
$meta_query[] = [
|
||||
'key' => \AKSRT\Core\DomainModel::PAY_PRICE_KEY,
|
||||
'value' => 0,
|
||||
'compare' => '>',
|
||||
'type' => 'NUMERIC',
|
||||
];
|
||||
}
|
||||
|
||||
if ($price_min !== null || $price_max !== null) {
|
||||
if ($price_min !== null && $price_max !== null) {
|
||||
$meta_query[] = [
|
||||
'key' => \AKSRT\Core\DomainModel::PAY_PRICE_KEY,
|
||||
'value' => [$price_min, $price_max],
|
||||
'compare' => 'BETWEEN',
|
||||
'type' => 'NUMERIC',
|
||||
];
|
||||
} elseif ($price_min !== null) {
|
||||
$meta_query[] = [
|
||||
'key' => \AKSRT\Core\DomainModel::PAY_PRICE_KEY,
|
||||
'value' => $price_min,
|
||||
'compare' => '>=',
|
||||
'type' => 'NUMERIC',
|
||||
];
|
||||
} else {
|
||||
$meta_query[] = [
|
||||
'key' => \AKSRT\Core\DomainModel::PAY_PRICE_KEY,
|
||||
'value' => $price_max,
|
||||
'compare' => '<=',
|
||||
'type' => 'NUMERIC',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$existing = $query->get('meta_query');
|
||||
if (!is_array($existing)) {
|
||||
$existing = [];
|
||||
}
|
||||
$query->set('meta_query', array_merge($existing, $meta_query));
|
||||
$query->set('cat', 0);
|
||||
$query->set('tag', '');
|
||||
}
|
||||
|
||||
// 站内搜索:分类/标签名搜索(内容类型始终为 post)
|
||||
if ($query->is_search() && empty($query->get('post_type'))) {
|
||||
$search_type = sanitize_key((string) $query->get('type'));
|
||||
$trem = absint($query->get('trem'));
|
||||
if ($trem > 0) {
|
||||
$query->set('cat', $trem);
|
||||
}
|
||||
|
||||
if (in_array($search_type, ['category', 'tag'], true)) {
|
||||
$taxonomy = $search_type === 'category' ? 'category' : 'post_tag';
|
||||
$term_ids = get_terms([
|
||||
'taxonomy' => $taxonomy,
|
||||
'hide_empty' => false,
|
||||
'fields' => 'ids',
|
||||
'number' => 20,
|
||||
'search' => (string) $query->get('s'),
|
||||
]);
|
||||
|
||||
$query->set('s', '');
|
||||
|
||||
if (is_wp_error($term_ids) || empty($term_ids)) {
|
||||
$query->set('post__in', [0]);
|
||||
} else {
|
||||
$query->set('tax_query', [[
|
||||
'taxonomy' => $taxonomy,
|
||||
'field' => 'term_id',
|
||||
'terms' => array_map('absint', $term_ids),
|
||||
]]);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
add_filter('template_include', function ($template) {
|
||||
if (!get_query_var('aksrt_is_resource_archive')) {
|
||||
return $template;
|
||||
}
|
||||
$candidate = locate_template(['archive-resources.php']);
|
||||
return $candidate ?: $template;
|
||||
});
|
||||
|
||||
add_filter('document_title_parts', function ($parts) {
|
||||
if (get_query_var('aksrt_is_resource_archive')) {
|
||||
$parts['title'] = __('资源', 'aksrt');
|
||||
}
|
||||
return $parts;
|
||||
});
|
||||
|
||||
// EnlighterJS 代码高亮 — 白名单 data-enlighter-* 属性
|
||||
// ============================================================
|
||||
|
||||
add_filter('wp_kses_allowed_html', function ($allowed, $context) {
|
||||
if ($context !== 'post') {
|
||||
return $allowed;
|
||||
}
|
||||
|
||||
// Allow data-enlighter-* attributes on <pre> and <code>
|
||||
$enlighter_attrs = [
|
||||
'data-enlighter-language' => true,
|
||||
'data-enlighter-theme' => true,
|
||||
'data-enlighter-group' => true,
|
||||
'data-enlighter-title' => true,
|
||||
'data-enlighter-linenumbers' => true,
|
||||
'data-enlighter-highlight' => true,
|
||||
'data-enlighter-lineoffset' => true,
|
||||
];
|
||||
|
||||
if (isset($allowed['pre'])) {
|
||||
$allowed['pre'] = array_merge($allowed['pre'], $enlighter_attrs);
|
||||
}
|
||||
if (isset($allowed['code'])) {
|
||||
$allowed['code'] = array_merge($allowed['code'], $enlighter_attrs);
|
||||
}
|
||||
|
||||
return $allowed;
|
||||
}, 10, 2);
|
||||
|
||||
// ============================================================
|
||||
// WP 后台文章列表增强列 — 浏览/点赞/收藏/下载
|
||||
// ============================================================
|
||||
|
||||
add_filter('manage_post_posts_columns', function ($columns) {
|
||||
$new_columns = [];
|
||||
foreach ($columns as $key => $label) {
|
||||
$new_columns[$key] = $label;
|
||||
if ($key === 'title') {
|
||||
$new_columns['aksrt_stats'] = __('互动数据', 'aksrt');
|
||||
}
|
||||
}
|
||||
return $new_columns;
|
||||
});
|
||||
|
||||
add_action('admin_enqueue_scripts', function ($hook) {
|
||||
if ($hook !== 'edit.php') return;
|
||||
wp_enqueue_style(
|
||||
'aksrt-font-awesome',
|
||||
get_template_directory_uri() . '/assets/fontawesome5/css/all.min.css',
|
||||
[],
|
||||
'5.15.4'
|
||||
);
|
||||
});
|
||||
|
||||
add_action('manage_post_posts_custom_column', function ($column, $post_id) {
|
||||
if ($column !== 'aksrt_stats') return;
|
||||
|
||||
$views = (int) get_post_meta($post_id, 'views', true);
|
||||
$likes = (int) get_post_meta($post_id, 'like', true);
|
||||
$favorites = (int) get_post_meta($post_id, 'favorite', true);
|
||||
$downloads = (int) get_post_meta($post_id, 'download_count', true);
|
||||
|
||||
$format = function (int $n): string {
|
||||
if ($n >= 10000) return round($n / 10000, 1) . '万';
|
||||
if ($n >= 1000) return round($n / 1000, 1) . 'k';
|
||||
return (string) $n;
|
||||
};
|
||||
|
||||
$parts = [];
|
||||
$parts[] = '<span title="' . esc_attr__('浏览', 'aksrt') . '" style="margin-right:4px;"><i class="fa fas fa-eye" style="color:#8a909c;font-size:12px;"></i> ' . esc_html($format($views)) . '</span>';
|
||||
$parts[] = '<span title="' . esc_attr__('点赞', 'aksrt') . '" style="margin-right:4px;"><i class="fa fas fa-thumbs-up" style="color:#1890ff;font-size:12px;"></i> ' . esc_html($format($likes)) . '</span>';
|
||||
$parts[] = '<span title="' . esc_attr__('收藏', 'aksrt') . '" style="margin-right:4px;"><i class="fa fas fa-star" style="color:#f39c12;font-size:12px;"></i> ' . esc_html($format($favorites)) . '</span>';
|
||||
|
||||
if ($downloads > 0) {
|
||||
$parts[] = '<span title="' . esc_attr__('下载', 'aksrt') . '"><i class="fa fas fa-download" style="color:#52c41a;font-size:12px;"></i> ' . esc_html($format($downloads)) . '</span>';
|
||||
}
|
||||
|
||||
echo implode(' · ', $parts);
|
||||
}, 10, 2);
|
||||
|
||||
// Support sorting by views/likes/favorites in admin
|
||||
add_action('pre_get_posts', function ($query) {
|
||||
if (!is_admin() || !$query->is_main_query()) return;
|
||||
|
||||
$orderby = $query->get('orderby');
|
||||
$allowed = ['views', 'like', 'favorite', 'download_count'];
|
||||
|
||||
if (in_array($orderby, $allowed, true)) {
|
||||
$query->set('meta_key', $orderby);
|
||||
$query->set('orderby', 'meta_value_num');
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// Gutenberg 自定义块注册
|
||||
// ============================================================
|
||||
|
||||
// Register category server-side so it exists before JS registerBlockType runs.
|
||||
// Keep untyped + defensive: some plugins pass unexpected shapes; typed params can 500 the editor.
|
||||
add_filter('block_categories_all', static function ($categories, $context = null) {
|
||||
if (!is_array($categories)) {
|
||||
return $categories;
|
||||
}
|
||||
|
||||
foreach ($categories as $category) {
|
||||
$slug = '';
|
||||
if (is_array($category)) {
|
||||
$slug = (string) ($category['slug'] ?? '');
|
||||
} elseif (is_object($category) && isset($category->slug)) {
|
||||
$slug = (string) $category->slug;
|
||||
}
|
||||
if ($slug === 'aksrt-blocks') {
|
||||
return $categories;
|
||||
}
|
||||
}
|
||||
|
||||
$categories[] = [
|
||||
'slug' => 'aksrt-blocks',
|
||||
'title' => 'AKSRT',
|
||||
'icon' => null,
|
||||
];
|
||||
|
||||
return $categories;
|
||||
}, 10, 2);
|
||||
|
||||
add_action('enqueue_block_editor_assets', function () {
|
||||
$base_uri = get_template_directory_uri();
|
||||
|
||||
$blocks_js = get_template_directory() . '/assets/js/gutenberg-blocks.js';
|
||||
if (file_exists($blocks_js)) {
|
||||
wp_enqueue_script(
|
||||
'aksrt-gutenberg-blocks',
|
||||
$base_uri . '/assets/js/gutenberg-blocks.js',
|
||||
// wp-media-utils: MediaUpload for carousel block
|
||||
['wp-blocks', 'wp-element', 'wp-components', 'wp-block-editor', 'wp-hooks', 'wp-media-utils', 'wp-data'],
|
||||
(string) filemtime($blocks_js),
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
$blocks_css = get_template_directory() . '/assets/css/gutenberg-blocks.css';
|
||||
if (file_exists($blocks_css)) {
|
||||
wp_enqueue_style(
|
||||
'aksrt-gutenberg-blocks',
|
||||
$base_uri . '/assets/css/gutenberg-blocks.css',
|
||||
[],
|
||||
(string) filemtime($blocks_css)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Front-end styles for Gutenberg blocks
|
||||
add_action('enqueue_block_assets', function () {
|
||||
if (is_admin()) return; // Already loaded in editor
|
||||
|
||||
$posts = [];
|
||||
$queried = get_queried_object();
|
||||
if ($queried instanceof \WP_Post) {
|
||||
$posts[$queried->ID] = $queried;
|
||||
}
|
||||
|
||||
global $wp_query;
|
||||
if ($wp_query instanceof \WP_Query) {
|
||||
foreach ((array) $wp_query->posts as $post) {
|
||||
if ($post instanceof \WP_Post) {
|
||||
$posts[$post->ID] = $post;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$has_carousel = false;
|
||||
foreach ($posts as $post) {
|
||||
if (has_block('aksrt/carousel', $post)) {
|
||||
$has_carousel = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!$has_carousel) return;
|
||||
|
||||
$base_uri = get_template_directory_uri();
|
||||
$blocks_css = get_template_directory() . '/assets/css/gutenberg-blocks.css';
|
||||
if (file_exists($blocks_css)) {
|
||||
wp_enqueue_style(
|
||||
'aksrt-gutenberg-blocks-front',
|
||||
$base_uri . '/assets/css/gutenberg-blocks.css',
|
||||
[],
|
||||
(string) filemtime($blocks_css)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// Gutenberg Carousel Block 渲染回调
|
||||
// ============================================================
|
||||
add_action('init', function () {
|
||||
if (!function_exists('register_block_type')) return;
|
||||
|
||||
register_block_type('aksrt/carousel', [
|
||||
'render_callback' => function (array $attributes): string {
|
||||
$images = $attributes['images'] ?? [];
|
||||
if (empty($images)) return '';
|
||||
|
||||
$autoplay = !empty($attributes['autoplay']) ? 'true' : 'false';
|
||||
$delay = absint($attributes['delay'] ?? 3000);
|
||||
$effect = sanitize_text_field($attributes['effect'] ?? 'slide');
|
||||
|
||||
$slides = '';
|
||||
foreach ($images as $img) {
|
||||
if (empty($img['url'])) continue;
|
||||
$alt = esc_attr($img['alt'] ?? '');
|
||||
$url = esc_url($img['url']);
|
||||
$slides .= '<div class="swiper-slide"><img' . aksrt_get_lazy_attr('lazy_sider', (string) $url, '', aksrt_get_lazy_thumb('lg')) . ' alt="' . $alt . '"></div>';
|
||||
}
|
||||
|
||||
if (empty($slides)) return '';
|
||||
|
||||
return '<div class="aksrt-carousel-block swiper" data-autoplay="' . $autoplay . '" data-delay="' . $delay . '" data-effect="' . $effect . '">'
|
||||
. '<div class="swiper-wrapper">' . $slides . '</div>'
|
||||
. '<div class="swiper-pagination"></div>'
|
||||
. '<div class="swiper-button-prev"></div><div class="swiper-button-next"></div>'
|
||||
. '</div>';
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// 付费内容系统:the_content 过滤器
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* 未购买的图库/视频正文只保留公开预览;购买卡由 single 模板渲染。
|
||||
*/
|
||||
add_filter('the_content', 'aksrt_paid_content_filter', 20);
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT Theme Core Hooks
|
||||
*
|
||||
* 从 functions.php 中抽离的核心功能钩子:
|
||||
* - SMTP 邮件配置注入
|
||||
* - 评论开关控制
|
||||
* - Admin Bar 隐藏
|
||||
*
|
||||
* @package AKSRT
|
||||
* @version 3.2.0
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
// ============================================================
|
||||
// 评论功能开关
|
||||
// ============================================================
|
||||
|
||||
add_filter('comments_open', function ($open, $post_id) {
|
||||
if (class_exists('\AKSRT\Settings\Settings')) {
|
||||
$display = \AKSRT\Settings\Settings::get_comment_display_settings();
|
||||
if (!empty($display['close_comments'])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return $open;
|
||||
}, 10, 2);
|
||||
|
||||
add_filter('comment_text', function ($comment_text, $comment = null, $args = []) {
|
||||
if (function_exists('aksrt_render_comment_content')) {
|
||||
return aksrt_render_comment_content((string) $comment_text);
|
||||
}
|
||||
return $comment_text;
|
||||
}, 20, 3);
|
||||
|
||||
// ============================================================
|
||||
@@ -1,380 +0,0 @@
|
||||
<?php
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
// window._win runtime bridge (legacy aksrt_win_var)
|
||||
add_action('wp_head', static function () {
|
||||
if (function_exists('aksrt_print_win_var')) {
|
||||
aksrt_print_win_var();
|
||||
}
|
||||
}, 1);
|
||||
|
||||
// 前端隐藏 WordPress Admin Bar
|
||||
// ============================================================
|
||||
|
||||
add_filter('show_admin_bar', '__return_false');
|
||||
|
||||
// ============================================================
|
||||
// SMTP 邮件配置注入
|
||||
// ============================================================
|
||||
|
||||
add_filter('wp_mail_from', function ($from) {
|
||||
if (!class_exists('\AKSRT\Settings\Settings')) return $from;
|
||||
$mail = \AKSRT\Settings\Settings::get_smtp();
|
||||
if (empty($mail['enabled'])) return $from;
|
||||
$configured = sanitize_email((string) ($mail['from_address'] ?? ''));
|
||||
return $configured !== '' ? $configured : $from;
|
||||
});
|
||||
|
||||
add_filter('wp_mail_from_name', function ($from_name) {
|
||||
if (!class_exists('\AKSRT\Settings\Settings')) return $from_name;
|
||||
$mail = \AKSRT\Settings\Settings::get_smtp();
|
||||
if (empty($mail['enabled'])) return $from_name;
|
||||
$configured = trim((string) ($mail['from_name'] ?? ''));
|
||||
return $configured !== '' ? $configured : $from_name;
|
||||
});
|
||||
|
||||
add_action('phpmailer_init', function ($phpmailer) {
|
||||
$mail = class_exists('\\AKSRT\\Settings\\Settings')
|
||||
? \AKSRT\Settings\Settings::get_smtp()
|
||||
: [];
|
||||
if (empty($mail['enabled']) || empty($mail['host'])) return;
|
||||
|
||||
$phpmailer->isSMTP();
|
||||
$phpmailer->Host = $mail['host'];
|
||||
$phpmailer->Port = (int) ($mail['port'] ?? 465);
|
||||
$phpmailer->SMTPAuth = !empty($mail['auth']) && !empty($mail['username']);
|
||||
$phpmailer->Username = (string) ($mail['username'] ?? '');
|
||||
$phpmailer->Password = (string) ($mail['password'] ?? '');
|
||||
$phpmailer->SMTPSecure = (string) ($mail['encryption'] ?? 'ssl');
|
||||
$phpmailer->Timeout = 10;
|
||||
$phpmailer->Timelimit = 15;
|
||||
});
|
||||
|
||||
// mail_title_prefix is applied by the canonical renderer and by third-party wp_mail callers.
|
||||
add_filter('wp_mail', function ($atts) {
|
||||
if (!class_exists('\AKSRT\Settings\Settings') || empty($atts['subject'])) return $atts;
|
||||
$branding = \AKSRT\Settings\Settings::get_mail_branding();
|
||||
$subject = (string) $atts['subject'];
|
||||
$site = wp_specialchars_decode(get_option('blogname'), ENT_QUOTES);
|
||||
$subject = preg_replace('/^\[' . preg_quote($site, '/') . '\]\s*/u', '', $subject);
|
||||
if ($branding['title_prefix'] !== '' && strpos($subject, $branding['title_prefix']) !== 0) {
|
||||
$atts['subject'] = $branding['title_prefix'] . ' ' . $subject;
|
||||
} else {
|
||||
$atts['subject'] = $subject;
|
||||
}
|
||||
return $atts;
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// 分类 Term Meta 字段(封面图 + 推荐描述)
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* 编辑分类页面 - 显示自定义字段
|
||||
*/
|
||||
function aksrt_category_meta_fields($term) {
|
||||
$cover_id = (int) get_term_meta($term->term_id, 'aksrt_category_cover_id', true);
|
||||
$cover_url = $cover_id > 0 ? wp_get_attachment_url($cover_id) : '';
|
||||
$recommend_desc = (string) get_term_meta($term->term_id, 'aksrt_category_recommend_desc', true);
|
||||
?>
|
||||
<tr class="form-field">
|
||||
<th scope="row"><label for="aksrt_category_cover_id"><?php esc_html_e('分类封面图', 'aksrt'); ?></label></th>
|
||||
<td>
|
||||
<div id="aksrt-cover-preview" style="margin-bottom:8px;">
|
||||
<?php if ($cover_url) : ?>
|
||||
<img src="<?php echo esc_url($cover_url); ?>" style="max-width:200px;height:auto;border-radius:6px;" alt="">
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<input type="hidden" id="aksrt_category_cover_id" name="aksrt_category_cover_id" value="<?php echo esc_attr((string) $cover_id); ?>">
|
||||
<button type="button" class="button" id="aksrt-pick-cover"><?php esc_html_e('选择图片', 'aksrt'); ?></button>
|
||||
<button type="button" class="button" id="aksrt-remove-cover" <?php echo $cover_id > 0 ? '' : 'style="display:none;"'; ?>><?php esc_html_e('移除', 'aksrt'); ?></button>
|
||||
<p class="description"><?php esc_html_e('分类推荐模块的封面图片。', 'aksrt'); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="form-field">
|
||||
<th scope="row"><label for="aksrt_category_recommend_desc"><?php esc_html_e('推荐描述', 'aksrt'); ?></label></th>
|
||||
<td>
|
||||
<textarea id="aksrt_category_recommend_desc" name="aksrt_category_recommend_desc" rows="3" class="large-text"><?php echo esc_textarea($recommend_desc); ?></textarea>
|
||||
<p class="description"><?php esc_html_e('分类推荐模块中显示的描述文字。留空则使用分类默认描述。', 'aksrt'); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增分类页面 - 显示自定义字段
|
||||
*/
|
||||
function aksrt_category_meta_fields_add() {
|
||||
?>
|
||||
<div class="form-field">
|
||||
<label for="aksrt_category_cover_id"><?php esc_html_e('分类封面图', 'aksrt'); ?></label>
|
||||
<div id="aksrt-cover-preview" style="margin-bottom:8px;"></div>
|
||||
<input type="hidden" id="aksrt_category_cover_id" name="aksrt_category_cover_id" value="">
|
||||
<button type="button" class="button" id="aksrt-pick-cover"><?php esc_html_e('选择图片', 'aksrt'); ?></button>
|
||||
<button type="button" class="button" id="aksrt-remove-cover" style="display:none;"><?php esc_html_e('移除', 'aksrt'); ?></button>
|
||||
<p class="description"><?php esc_html_e('分类推荐模块的封面图片。', 'aksrt'); ?></p>
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label for="aksrt_category_recommend_desc"><?php esc_html_e('推荐描述', 'aksrt'); ?></label>
|
||||
<textarea id="aksrt_category_recommend_desc" name="aksrt_category_recommend_desc" rows="3"></textarea>
|
||||
<p class="description"><?php esc_html_e('分类推荐模块中显示的描述文字。留空则使用分类默认描述。', 'aksrt'); ?></p>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
add_action('category_edit_form_fields', 'aksrt_category_meta_fields', 10, 1);
|
||||
add_action('category_add_form_fields', 'aksrt_category_meta_fields_add', 10, 1);
|
||||
|
||||
/**
|
||||
* 保存分类 term meta
|
||||
*/
|
||||
function aksrt_save_category_meta(int $term_id) {
|
||||
if (!current_user_can('manage_categories')) return;
|
||||
|
||||
$cover_id = isset($_POST['aksrt_category_cover_id']) ? absint($_POST['aksrt_category_cover_id']) : 0;
|
||||
$recommend_desc = isset($_POST['aksrt_category_recommend_desc']) ? sanitize_textarea_field(wp_unslash($_POST['aksrt_category_recommend_desc'])) : '';
|
||||
|
||||
if ($cover_id > 0) {
|
||||
update_term_meta($term_id, 'aksrt_category_cover_id', $cover_id);
|
||||
} else {
|
||||
delete_term_meta($term_id, 'aksrt_category_cover_id');
|
||||
}
|
||||
|
||||
if ($recommend_desc !== '') {
|
||||
update_term_meta($term_id, 'aksrt_category_recommend_desc', $recommend_desc);
|
||||
} else {
|
||||
delete_term_meta($term_id, 'aksrt_category_recommend_desc');
|
||||
}
|
||||
|
||||
if (class_exists('\AKSRT\Settings\Settings')) {
|
||||
\AKSRT\Settings\Settings::flush_category_recommend_cache();
|
||||
}
|
||||
}
|
||||
|
||||
add_action('edited_category', 'aksrt_save_category_meta', 10, 1);
|
||||
add_action('created_category', 'aksrt_save_category_meta', 10, 1);
|
||||
|
||||
/**
|
||||
* 分类编辑页加载媒体库脚本(wp_enqueue_media 仅在 AKSRT 面板加载,此处补上)
|
||||
*/
|
||||
add_action('admin_enqueue_scripts', function (string $hook_suffix) {
|
||||
if ($hook_suffix === 'edit-tags.php' || $hook_suffix === 'term.php') {
|
||||
wp_enqueue_media();
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 分类编辑页内联 JS - 媒体选择器
|
||||
*/
|
||||
function aksrt_category_meta_admin_js() {
|
||||
$screen = get_current_screen();
|
||||
if (!$screen || $screen->taxonomy !== 'category') return;
|
||||
?>
|
||||
<script>
|
||||
jQuery(function($) {
|
||||
var frame;
|
||||
|
||||
$(document).on('click', '#aksrt-pick-cover', function(e) {
|
||||
e.preventDefault();
|
||||
if (typeof wp === 'undefined' || !wp.media) return;
|
||||
if (frame) { frame.open(); return; }
|
||||
|
||||
frame = wp.media({
|
||||
title: '<?php echo esc_js(__('选择分类封面图', 'aksrt')); ?>',
|
||||
button: { text: '<?php echo esc_js(__('使用此图片', 'aksrt')); ?>' },
|
||||
multiple: false
|
||||
});
|
||||
|
||||
frame.on('select', function() {
|
||||
var attachment = frame.state().get('selection').first().toJSON();
|
||||
$('#aksrt_category_cover_id').val(attachment.id);
|
||||
$('#aksrt-cover-preview').html('<img src="' + attachment.url + '" style="max-width:200px;height:auto;border-radius:6px;" alt="">');
|
||||
$('#aksrt-remove-cover').show();
|
||||
});
|
||||
|
||||
frame.open();
|
||||
});
|
||||
|
||||
$(document).on('click', '#aksrt-remove-cover', function(e) {
|
||||
e.preventDefault();
|
||||
$('#aksrt_category_cover_id').val('');
|
||||
$('#aksrt-cover-preview').html('');
|
||||
$(this).hide();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<?php
|
||||
}
|
||||
|
||||
add_action('admin_footer', 'aksrt_category_meta_admin_js');
|
||||
|
||||
// ============================================================
|
||||
// 资源列表:/resources 虚拟归档(post + _aksrt_is_resource)
|
||||
// ============================================================
|
||||
|
||||
add_action('init', function () {
|
||||
add_rewrite_rule('^resources/?$', 'index.php?aksrt_is_resource_archive=1', 'top');
|
||||
add_rewrite_rule('^resources/page/([0-9]+)/?$', 'index.php?aksrt_is_resource_archive=1&paged=$matches[1]', 'top');
|
||||
// 旧单篇路径 /resource/{slug} → 301 到 post 永久链接
|
||||
add_rewrite_rule('^resource/([^/]+)/?$', 'index.php?aksrt_legacy_resource=$matches[1]', 'top');
|
||||
});
|
||||
|
||||
add_filter('query_vars', function ($vars) {
|
||||
$vars[] = 'aksrt_is_resource_archive';
|
||||
$vars[] = 'aksrt_legacy_resource';
|
||||
$vars[] = 'type';
|
||||
$vars[] = 'trem';
|
||||
return $vars;
|
||||
});
|
||||
|
||||
add_action('template_redirect', function () {
|
||||
$slug = get_query_var('aksrt_legacy_resource');
|
||||
if (!$slug) {
|
||||
return;
|
||||
}
|
||||
$page = get_page_by_path((string) $slug, OBJECT, 'post');
|
||||
if ($page) {
|
||||
wp_redirect(get_permalink($page->ID), 301);
|
||||
exit;
|
||||
}
|
||||
});
|
||||
|
||||
add_action('pre_get_posts', function ($query) {
|
||||
if (is_admin() || !$query->is_main_query()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ((bool) $query->get('aksrt_is_resource_archive')) {
|
||||
$query->set('post_type', 'post');
|
||||
$query->set('post_status', 'publish');
|
||||
|
||||
$orderby = isset($_GET['orderby']) ? sanitize_key((string) $_GET['orderby']) : 'date';
|
||||
if ($orderby === 'downloads') {
|
||||
$query->set('meta_key', \AKSRT\Core\DomainModel::RESOURCE_DOWNLOAD_COUNT_KEY);
|
||||
$query->set('orderby', 'meta_value_num');
|
||||
$query->set('order', 'DESC');
|
||||
} elseif ($orderby === 'views') {
|
||||
$query->set('meta_key', \AKSRT\Core\DomainModel::RESOURCE_VIEW_COUNT_KEY);
|
||||
$query->set('orderby', 'meta_value_num');
|
||||
$query->set('order', 'DESC');
|
||||
}
|
||||
|
||||
$price_filter = isset($_GET['price']) ? sanitize_key((string) $_GET['price']) : '';
|
||||
$price_min = isset($_GET['price_min']) && $_GET['price_min'] !== '' ? (float) $_GET['price_min'] : null;
|
||||
$price_max = isset($_GET['price_max']) && $_GET['price_max'] !== '' ? (float) $_GET['price_max'] : null;
|
||||
|
||||
$meta_query = [
|
||||
'relation' => 'AND',
|
||||
[
|
||||
'key' => \AKSRT\Core\DomainModel::RESOURCE_MARKER_KEY,
|
||||
'value' => '1',
|
||||
'compare' => '=',
|
||||
],
|
||||
];
|
||||
|
||||
if ($price_filter === 'free') {
|
||||
$meta_query[] = [
|
||||
'relation' => 'OR',
|
||||
[
|
||||
'key' => \AKSRT\Core\DomainModel::PAY_PRICE_KEY,
|
||||
'value' => 0,
|
||||
'compare' => '=',
|
||||
'type' => 'NUMERIC',
|
||||
],
|
||||
[
|
||||
'key' => \AKSRT\Core\DomainModel::PAY_PRICE_KEY,
|
||||
'compare' => 'NOT EXISTS',
|
||||
],
|
||||
];
|
||||
} elseif ($price_filter === 'paid') {
|
||||
$meta_query[] = [
|
||||
'key' => \AKSRT\Core\DomainModel::PAY_PRICE_KEY,
|
||||
'value' => 0,
|
||||
'compare' => '>',
|
||||
'type' => 'NUMERIC',
|
||||
];
|
||||
}
|
||||
|
||||
if ($price_min !== null || $price_max !== null) {
|
||||
if ($price_min !== null && $price_max !== null) {
|
||||
$meta_query[] = [
|
||||
'key' => \AKSRT\Core\DomainModel::PAY_PRICE_KEY,
|
||||
'value' => [$price_min, $price_max],
|
||||
'compare' => 'BETWEEN',
|
||||
'type' => 'NUMERIC',
|
||||
];
|
||||
} elseif ($price_min !== null) {
|
||||
$meta_query[] = [
|
||||
'key' => \AKSRT\Core\DomainModel::PAY_PRICE_KEY,
|
||||
'value' => $price_min,
|
||||
'compare' => '>=',
|
||||
'type' => 'NUMERIC',
|
||||
];
|
||||
} else {
|
||||
$meta_query[] = [
|
||||
'key' => \AKSRT\Core\DomainModel::PAY_PRICE_KEY,
|
||||
'value' => $price_max,
|
||||
'compare' => '<=',
|
||||
'type' => 'NUMERIC',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$existing = $query->get('meta_query');
|
||||
if (!is_array($existing)) {
|
||||
$existing = [];
|
||||
}
|
||||
$query->set('meta_query', array_merge($existing, $meta_query));
|
||||
$query->set('cat', 0);
|
||||
$query->set('tag', '');
|
||||
}
|
||||
|
||||
// 站内搜索:分类/标签名搜索(内容类型始终为 post)
|
||||
if ($query->is_search() && empty($query->get('post_type'))) {
|
||||
$search_type = sanitize_key((string) $query->get('type'));
|
||||
$trem = absint($query->get('trem'));
|
||||
if ($trem > 0) {
|
||||
$query->set('cat', $trem);
|
||||
}
|
||||
|
||||
if (in_array($search_type, ['category', 'tag'], true)) {
|
||||
$taxonomy = $search_type === 'category' ? 'category' : 'post_tag';
|
||||
$term_ids = get_terms([
|
||||
'taxonomy' => $taxonomy,
|
||||
'hide_empty' => false,
|
||||
'fields' => 'ids',
|
||||
'number' => 20,
|
||||
'search' => (string) $query->get('s'),
|
||||
]);
|
||||
|
||||
$query->set('s', '');
|
||||
|
||||
if (is_wp_error($term_ids) || empty($term_ids)) {
|
||||
$query->set('post__in', [0]);
|
||||
} else {
|
||||
$query->set('tax_query', [[
|
||||
'taxonomy' => $taxonomy,
|
||||
'field' => 'term_id',
|
||||
'terms' => array_map('absint', $term_ids),
|
||||
]]);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
add_filter('template_include', function ($template) {
|
||||
if (!get_query_var('aksrt_is_resource_archive')) {
|
||||
return $template;
|
||||
}
|
||||
$candidate = locate_template(['archive-resources.php']);
|
||||
return $candidate ?: $template;
|
||||
});
|
||||
|
||||
add_filter('document_title_parts', function ($parts) {
|
||||
if (get_query_var('aksrt_is_resource_archive')) {
|
||||
$parts['title'] = __('资源', 'aksrt');
|
||||
}
|
||||
return $parts;
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
@@ -1,252 +0,0 @@
|
||||
<?php
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
// EnlighterJS 代码高亮 — 白名单 data-enlighter-* 属性
|
||||
// ============================================================
|
||||
|
||||
add_filter('wp_kses_allowed_html', function ($allowed, $context) {
|
||||
if ($context !== 'post') {
|
||||
return $allowed;
|
||||
}
|
||||
|
||||
// Allow data-enlighter-* attributes on <pre> and <code>
|
||||
$enlighter_attrs = [
|
||||
'data-enlighter-language' => true,
|
||||
'data-enlighter-theme' => true,
|
||||
'data-enlighter-group' => true,
|
||||
'data-enlighter-title' => true,
|
||||
'data-enlighter-linenumbers' => true,
|
||||
'data-enlighter-highlight' => true,
|
||||
'data-enlighter-lineoffset' => true,
|
||||
];
|
||||
|
||||
if (isset($allowed['pre'])) {
|
||||
$allowed['pre'] = array_merge($allowed['pre'], $enlighter_attrs);
|
||||
}
|
||||
if (isset($allowed['code'])) {
|
||||
$allowed['code'] = array_merge($allowed['code'], $enlighter_attrs);
|
||||
}
|
||||
|
||||
return $allowed;
|
||||
}, 10, 2);
|
||||
|
||||
// ============================================================
|
||||
// WP 后台文章列表增强列 — 浏览/点赞/收藏/下载
|
||||
// ============================================================
|
||||
|
||||
add_filter('manage_post_posts_columns', function ($columns) {
|
||||
$new_columns = [];
|
||||
foreach ($columns as $key => $label) {
|
||||
$new_columns[$key] = $label;
|
||||
if ($key === 'title') {
|
||||
$new_columns['aksrt_stats'] = __('互动数据', 'aksrt');
|
||||
}
|
||||
}
|
||||
return $new_columns;
|
||||
});
|
||||
|
||||
add_action('admin_enqueue_scripts', function ($hook) {
|
||||
if ($hook !== 'edit.php') return;
|
||||
wp_enqueue_style(
|
||||
'aksrt-font-awesome',
|
||||
get_template_directory_uri() . '/assets/fontawesome5/css/all.min.css',
|
||||
[],
|
||||
'5.15.4'
|
||||
);
|
||||
});
|
||||
|
||||
add_action('manage_post_posts_custom_column', function ($column, $post_id) {
|
||||
if ($column !== 'aksrt_stats') return;
|
||||
|
||||
$views = (int) get_post_meta($post_id, 'views', true);
|
||||
$likes = (int) get_post_meta($post_id, 'like', true);
|
||||
$favorites = (int) get_post_meta($post_id, 'favorite', true);
|
||||
$downloads = (int) get_post_meta($post_id, 'download_count', true);
|
||||
|
||||
$format = function (int $n): string {
|
||||
if ($n >= 10000) return round($n / 10000, 1) . '万';
|
||||
if ($n >= 1000) return round($n / 1000, 1) . 'k';
|
||||
return (string) $n;
|
||||
};
|
||||
|
||||
$parts = [];
|
||||
$parts[] = '<span title="' . esc_attr__('浏览', 'aksrt') . '" style="margin-right:4px;"><i class="fa fas fa-eye" style="color:#8a909c;font-size:12px;"></i> ' . esc_html($format($views)) . '</span>';
|
||||
$parts[] = '<span title="' . esc_attr__('点赞', 'aksrt') . '" style="margin-right:4px;"><i class="fa fas fa-thumbs-up" style="color:#1890ff;font-size:12px;"></i> ' . esc_html($format($likes)) . '</span>';
|
||||
$parts[] = '<span title="' . esc_attr__('收藏', 'aksrt') . '" style="margin-right:4px;"><i class="fa fas fa-star" style="color:#f39c12;font-size:12px;"></i> ' . esc_html($format($favorites)) . '</span>';
|
||||
|
||||
if ($downloads > 0) {
|
||||
$parts[] = '<span title="' . esc_attr__('下载', 'aksrt') . '"><i class="fa fas fa-download" style="color:#52c41a;font-size:12px;"></i> ' . esc_html($format($downloads)) . '</span>';
|
||||
}
|
||||
|
||||
echo implode(' · ', $parts);
|
||||
}, 10, 2);
|
||||
|
||||
// Support sorting by views/likes/favorites in admin
|
||||
add_action('pre_get_posts', function ($query) {
|
||||
if (!is_admin() || !$query->is_main_query()) return;
|
||||
|
||||
$orderby = $query->get('orderby');
|
||||
$allowed = ['views', 'like', 'favorite', 'download_count'];
|
||||
|
||||
if (in_array($orderby, $allowed, true)) {
|
||||
$query->set('meta_key', $orderby);
|
||||
$query->set('orderby', 'meta_value_num');
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// 评论提交:单一合同
|
||||
// ------------------------------------------------------------
|
||||
// 主路径:前端 comments.js → REST /comment/submit(aksrt_require_captcha)。
|
||||
// 兜底 / 原生 wp-comments-post.php 经 preprocess_comment 做人机验证,避免绕过。
|
||||
// 列表仍由 aksrt_comment_callback 统一渲染。
|
||||
// ============================================================
|
||||
|
||||
// ============================================================
|
||||
// Gutenberg 自定义块注册
|
||||
// ============================================================
|
||||
|
||||
// Register category server-side so it exists before JS registerBlockType runs.
|
||||
// Keep untyped + defensive: some plugins pass unexpected shapes; typed params can 500 the editor.
|
||||
add_filter('block_categories_all', static function ($categories, $context = null) {
|
||||
if (!is_array($categories)) {
|
||||
return $categories;
|
||||
}
|
||||
|
||||
foreach ($categories as $category) {
|
||||
$slug = '';
|
||||
if (is_array($category)) {
|
||||
$slug = (string) ($category['slug'] ?? '');
|
||||
} elseif (is_object($category) && isset($category->slug)) {
|
||||
$slug = (string) $category->slug;
|
||||
}
|
||||
if ($slug === 'aksrt-blocks') {
|
||||
return $categories;
|
||||
}
|
||||
}
|
||||
|
||||
$categories[] = [
|
||||
'slug' => 'aksrt-blocks',
|
||||
'title' => 'AKSRT',
|
||||
'icon' => null,
|
||||
];
|
||||
|
||||
return $categories;
|
||||
}, 10, 2);
|
||||
|
||||
add_action('enqueue_block_editor_assets', function () {
|
||||
$base_uri = get_template_directory_uri();
|
||||
$version = wp_get_theme()->get('Version');
|
||||
|
||||
$blocks_js = get_template_directory() . '/assets/js/gutenberg-blocks.js';
|
||||
if (file_exists($blocks_js)) {
|
||||
wp_enqueue_script(
|
||||
'aksrt-gutenberg-blocks',
|
||||
$base_uri . '/assets/js/gutenberg-blocks.js',
|
||||
// wp-media-utils: MediaUpload for carousel block
|
||||
['wp-blocks', 'wp-element', 'wp-components', 'wp-block-editor', 'wp-hooks', 'wp-media-utils', 'wp-data'],
|
||||
(string) filemtime($blocks_js),
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
$blocks_css = get_template_directory() . '/assets/css/gutenberg-blocks.css';
|
||||
if (file_exists($blocks_css)) {
|
||||
wp_enqueue_style(
|
||||
'aksrt-gutenberg-blocks',
|
||||
$base_uri . '/assets/css/gutenberg-blocks.css',
|
||||
[],
|
||||
(string) filemtime($blocks_css)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Front-end styles for Gutenberg blocks
|
||||
add_action('enqueue_block_assets', function () {
|
||||
if (is_admin()) return; // Already loaded in editor
|
||||
|
||||
$posts = [];
|
||||
$queried = get_queried_object();
|
||||
if ($queried instanceof \WP_Post) {
|
||||
$posts[$queried->ID] = $queried;
|
||||
}
|
||||
|
||||
global $wp_query;
|
||||
if ($wp_query instanceof \WP_Query) {
|
||||
foreach ((array) $wp_query->posts as $post) {
|
||||
if ($post instanceof \WP_Post) {
|
||||
$posts[$post->ID] = $post;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$has_carousel = false;
|
||||
foreach ($posts as $post) {
|
||||
if (has_block('aksrt/carousel', $post)) {
|
||||
$has_carousel = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!$has_carousel) return;
|
||||
|
||||
$base_uri = get_template_directory_uri();
|
||||
$blocks_css = get_template_directory() . '/assets/css/gutenberg-blocks.css';
|
||||
if (file_exists($blocks_css)) {
|
||||
wp_enqueue_style(
|
||||
'aksrt-gutenberg-blocks-front',
|
||||
$base_uri . '/assets/css/gutenberg-blocks.css',
|
||||
[],
|
||||
(string) filemtime($blocks_css)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// Gutenberg Carousel Block 渲染回调
|
||||
// ============================================================
|
||||
add_action('init', function () {
|
||||
if (!function_exists('register_block_type')) return;
|
||||
|
||||
register_block_type('aksrt/carousel', [
|
||||
'render_callback' => function (array $attributes): string {
|
||||
$images = $attributes['images'] ?? [];
|
||||
if (empty($images)) return '';
|
||||
|
||||
$autoplay = !empty($attributes['autoplay']) ? 'true' : 'false';
|
||||
$delay = absint($attributes['delay'] ?? 3000);
|
||||
$effect = sanitize_text_field($attributes['effect'] ?? 'slide');
|
||||
|
||||
$slides = '';
|
||||
foreach ($images as $img) {
|
||||
if (empty($img['url'])) continue;
|
||||
$alt = esc_attr($img['alt'] ?? '');
|
||||
$url = esc_url($img['url']);
|
||||
$slides .= '<div class="swiper-slide"><img' . aksrt_get_lazy_attr('lazy_sider', (string) $url, '', aksrt_get_lazy_thumb('lg')) . ' alt="' . $alt . '"></div>';
|
||||
}
|
||||
|
||||
if (empty($slides)) return '';
|
||||
|
||||
return '<div class="aksrt-carousel-block swiper" data-autoplay="' . $autoplay . '" data-delay="' . $delay . '" data-effect="' . $effect . '">'
|
||||
. '<div class="swiper-wrapper">' . $slides . '</div>'
|
||||
. '<div class="swiper-pagination"></div>'
|
||||
. '<div class="swiper-button-prev"></div><div class="swiper-button-next"></div>'
|
||||
. '</div>';
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// 付费内容系统:the_content 过滤器
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* 未购买的图库/视频正文只保留公开预览;购买卡由 single 模板渲染。
|
||||
*/
|
||||
add_filter('the_content', 'aksrt_paid_content_filter', 20);
|
||||
|
||||
/**
|
||||
* 付费/资源文章使用统一的 single.php。
|
||||
*/
|
||||
add_filter('single_template', function (string $template): string {
|
||||
return $template;
|
||||
});
|
||||
@@ -9,7 +9,7 @@ const read = (file) => fs.readFileSync(path.join(root, file), 'utf8')
|
||||
const templates = read('src/Settings/MailTemplates.php')
|
||||
const mail = read('src/Settings/Mail.php')
|
||||
const notifications = read('src/Settings/MailNotifications.php')
|
||||
const coreHooks = read('inc/core-hooks/part-2.php')
|
||||
const coreHooks = read('inc/core-hooks.php')
|
||||
const globalController = read('src/Admin/GlobalController.php')
|
||||
const userController = read('src/Admin/UserController.php')
|
||||
const settings = read('src/Settings/Settings.php')
|
||||
|
||||
@@ -13,7 +13,7 @@ test('paid article and sidebar widget use the Zibll purchase-card DOM', () => {
|
||||
const widget = read('src/WidgetSystem/Renderers/ContentRenderer.php');
|
||||
const shell = read('src/WidgetSystem/FrontendRenderer.php');
|
||||
const single = read('inc/helpers/aksrt-single-render.php');
|
||||
const hooks = read('inc/core-hooks/part-3.php');
|
||||
const hooks = read('inc/core-hooks.php');
|
||||
|
||||
assert.match(pay, /zib-widget pay-box pay-widget/);
|
||||
assert.match(pay, /zib-widget pay-box order-type-/);
|
||||
|
||||
@@ -340,7 +340,7 @@ test('AJAX pagination can recover, caps auto loading and rehydrates appended med
|
||||
});
|
||||
|
||||
test('front-end carousel CSS is conditional on carousel block content', () => {
|
||||
const hooks = read('inc/core-hooks/part-3.php');
|
||||
const hooks = read('inc/core-hooks.php');
|
||||
assert.match(hooks, /has_block\('aksrt\/carousel', \$post\)/);
|
||||
assert.match(hooks, /if \(!\$has_carousel\) return;[\s\S]*?'aksrt-gutenberg-blocks-front'/);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user