Compare commits

...
15 Commits
Author SHA1 Message Date
kate522 5641c40958 fix(header): stop the nav midline chip rule from collapsing the quick-login buttons
The top-nav guest popover rendered its quick-login buttons as 29px circles
with zero gap: github, gitee and qq were touching. The user-card widget, which
uses the same .social_loginbar markup, was fine at 32px with 10px gaps.

header-userbox.css carries an AKSRT-specific rule, absent from the reference
theme, that gives every .toggle-radius inside .navbar-form a 29px midline
circle:

  .navbar-top .navbar-form .toggle-radius { flex: 0 0 29px; margin: 0; }

It exists for the account-row .badg chips, the theme toggle and the language
toggle. The popover social buttons are .toggle-radius too and also sit inside
.navbar-form, so they were caught by it. Two independent mechanisms broke the
geometry: flex-basis outranks width in a flex container, so flex: 0 0 29px beat
the .social_loginbar .social-login-item width: 32px !important; and the rule
(0,3,0) specificity outranked the (0,2,0) margin: 3px 5px.

Exempt the social buttons with :not(). The chips, the theme toggle and the
language toggle still match, and the popover buttons fall back to the
.social_loginbar geometry ported 1:1 from the reference.

Verified on the live site by measuring computed styles in headless Edge: the
popover buttons go from 29px/0px margins/0px gap to 32px/5px margins/10px gap,
the user-card widget is unchanged, and the account-row chips keep 29px (theme
toggle) and 36px (.badg). Rebuilt the public bundle so the exemption survives
the build, and contract-locked the selector plus the bundle copy.

Verification: node --test across widget-system, navigation, admin,
asset-ledger, dev, front-submit and visual (615 tests).
2026-10-17 17:56:56 +08:00
kate522 0bee2c605e refactor(auth): stop pre-escaping the OAuth provider URLs
get_auth_providers_data() returned esc_url()-ed URLs while all four of its
consumers (the modal, the user card, render_social_buttons and the comment
form) escaped again on output. Its sibling get_frontend_bindings_data() stores
the raw URL and lets the binding list escape it, so the class was inconsistent
with itself.

I first reported this as a double-escaping bug that broke ampersand-bearing
URLs. That was wrong: I had verified it with a stub that modelled esc_url() as
htmlspecialchars(). The real esc_url() runs wp_kses_normalize_entities()
first, which explicitly decodes double-encoded character references, so
esc_url() is idempotent on its own output. Re-checked against the WordPress
source with a faithful transcription, and re-ran the popover markup: the
WeChat href is ?tab=oauth&oauth_intent=login, one entity, unchanged.

So the escaping was redundant rather than broken. The producer now hands out
raw URLs and each surface escapes exactly once, which is where WordPress
expects it and matches get_frontend_bindings_data(). Rendered output is
byte-identical.

Extended the auth source contract to pin the invariant: the producer stays
raw and all four surfaces escape on output.

Verification: node --test across widget-system, navigation, admin,
asset-ledger, dev, front-submit and visual (614 tests), plus php -l.
2026-10-17 14:54:10 +08:00
kate522 c472045ce4 fix(auth): align the social-login surfaces with the reference theme
The "other login methods" block drifted from zibll on three axes. Gitee was
mapped to fa-code-branch because FA5 Free ships no Gitee brand glyph, so the
button showed a generic branch icon; Google showed the monochrome FA glyph
instead of the reference's multi-colour mark. The modal also printed an
implicit agreement line under the social buttons, which the reference guest
card does not have. And the top-nav user popover built its own
.social-login-divider / .oauth-row / .oauth-btn markup and then wrapped that
in a second separator and .social_loginbar, so the block rendered nested and
unlike the reference.

aksrt_brand_icon() now routes zibsvg-/customsvg- values through the shared
icon renderer, and the brand map resolves gitee to zibsvg-gitee and google to
zibsvg-google-color. Both sprite keys already exist in the runtime symbol
sheet, which the loader pulls in on every frontend page. Because
get_auth_providers_data() and binding_row() both go through
aksrt_brand_icon(), one map change fixes the icon on the modal, the user card,
the top-nav popover, the user-center binding list and the profile social
links.

render_social_buttons() is rewritten to the reference shape: an optional
.social-separator, a .social_loginbar, and one .social-login-item <type>
toggle-radius anchor per enabled provider with rel="nofollow" and a
"<label>登录" title. oauth_button_lg switches to the button-lg variant with a
label, and the WeChat entry carries qrcode-signin so auth.js opens the QR
panel instead of navigating, matching the reference. The navbar and the link
apply modal now append the block verbatim instead of re-wrapping it, and the
user-card loop adopts the same item attributes. The retired
.social-login-divider / .oauth-row / .oauth-btn rules are dropped from
base-1.css, and the binding-list icon rule gains an svg selector so the
sprite icons keep the 22px the FA glyphs had.

Verification: node --test across widget-system, navigation, admin,
asset-ledger, dev, front-submit and visual (613 tests), plus php -l on every
changed PHP file. Extended the auth source contract with the agreement-line,
brand-icon and popover-structure cases, and pinned the user-card guest
markup.
2026-10-17 14:42:42 +08:00
kate522 6dc9265260 fix(frontend): keep the widget link color off the check-in badge
sidebar.css paints .widget a with --color-ink-2 (specificity 0,1,1), which
out-specifies the .jb-* color group (0,1,0) the badge relies on for its white
ink. The user-card widget carries the .widget class, so the check-in bookmark
rendered dark-gray text on the orange gradient on widget pages. zibll sets no
link color on zib-widget anchors at all.

sidebar.css already restores .but colors for exactly this reason; extend that
restoration to .img-badge so the badge keeps its own --this-color. Rebuild the
homepage bundle and extend the badge contract test.
2026-10-17 13:02:46 +08:00
kate522 008e63524d fix(frontend): pin the check-in badge typography to the reference values
The badge inherited AKSRT's modern system font stack (--font-body) and the
html reset's antialiased/optimizeLegibility rendering, while the reference
theme renders its body in bootstrap's "Helvetica Neue", Helvetica, Arial,
sans-serif with browser defaults. Same 14px size but a different typeface
and smoothing, so the badge text still read as off-theme after the gradient
fix.

Pin font-family, -webkit-font-smoothing and text-rendering to the reference
body values on the existing scoped badge rule (alongside the line-height
pin) and extend the contract test.
2026-10-17 12:40:42 +08:00
kate522 cff3083a91 fix(frontend): align the user-card check-in badge with the reference theme
The legacy .jb-* compat layer in core/buttons.css restated .jb-yellow as #f99d4d -> #f7631d, and because the public bundle loads after main.min.css that value overrode the reference-accurate #f59f54 -> #ff6922 gradient, so the check-in bookmark (and every other .jb-yellow surface) painted the wrong orange. Correct the gradient and rebuild the public bundle.

Also drop the dead is-done class and the attribute-less span wrapper from the signed card badge, which the reference theme does not have. The header dropdown variant keeps its class, locked by the navigation contract.
2026-10-16 17:32:06 +08:00
kate522 4d69aaee50 fix(frontend): scope the credits button skins to the user-center shell
The user-center route rendered a 4px top-nav publish chip while every other
route kept the 50px pill. assets/css/routes/user-center/credits.css restated
border-radius on bare legacy colour modifiers (.but.jb-blue, .but.b-blue-2,
.but.c-blue-2.hollow, .but.c-green.hollow). Those rules are (0,2,0) and load
after public.css, so they out-rank .but.radius's 50px and flatten the publish
chip, which is newadd-btns but jb-blue radius, down to 4px.

The same leak also repainted every site-wide .but.jb-blue outside the panel
with the flat #3b82f6 skin instead of the legacy gradient, so login, submit,
badge and cashier buttons lost their reference surface on that route too.

Scope the four rules to .uc-content, the shell every credits panel and its
modals render inside (templates/pages/page-profile.php wraps
templates/parts/user-center/panel.php). The panel keeps its own button skins
and the top-nav chips get the reference pill back.

The .btn-block restatement that sat in the same block is byte-identical to
the one in core/buttons.css, which loads earlier on every page, so it could
never win; it is dropped rather than scoped.

Verified: node --test across tests/ (604 passing), plus a stubbed navbar
rendered against each route's real stylesheet set measuring the computed
border-radius of the publish chip and of the panel's own buttons. A new
contract test fails on the previous stylesheet and passes on this one.
2026-10-15 23:22:18 +08:00
kate522 ee39024549 fix(header): put the top-nav clusters on one horizontal line
The top-nav account row carried min-height: 50px on top of the 8px
margin-top that Bootstrap base .navbar-form rule keeps at >=768px (the
override only zeroes the horizontal margins). The 8px was counted twice,
so the row ran from 8px to 58px: its icon tiles landed 8px below the 25px
midline that the 50px menu row and the logo define, and the navbar grew
from 50px to 58px, which also broke the body.nav-fixed offset.

Drop the min-height so the row takes its natural 34px height from the
icon tiles, and give the theme/language group the same 34px cross-size so
its 29px toggle shares that midline instead of sitting 2.5px above it.
Measured against the reference theme, the navbar goes from 66px to
50.05px (reference 50.28px) and the logo, menu, publish chip, VIP chip,
theme toggle and account icons all centre on 25px.

The desktop navbar stays visible below 991px (only the menu folds into a
dropdown there), so the same fix applies at the mobile and tablet
breakpoints: the top bar drops from 68px to 52.05px.

Verified: node --test across tests/ (603 passing), and a stubbed navbar
render measured in a headless browser against the reference theme
stylesheet.
2026-10-15 21:03:27 +08:00
kate522 5810693e01 fix(header): restore the payvip surface on the top-nav VIP chip
The 开通会员 chip rendered in the .jb-red orange gradient instead of the
legacy metallic .payvip-icon gradient. The markup always carried
payvip-icon, but core/buttons.css restates the legacy .but block and is
bundled into public.css, which loads after main.css. Same specificity,
later in the cascade, so its background / color / border / text-shadow
declarations outrank .payvip-icon and replace the payvip surface with
whatever .jb-* skin happens to be on the element.

The reference theme has no such compatibility layer, so there its
.payvip-icon declarations are the last word and the chip shows the
metallic gradient with #ece0e0 text, no border and no text shadow.

A previous fix restored the surface only for the user-card CTA, scoped to
.aksrt-header-userbox .pay-vip.but.payvip-icon, which left the top-nav
chip (and the guest CTA, and the user-card status bar) on the wrong
gradient. Re-assert the legacy .payvip-icon declarations unscoped in
header-userbox.css, which loads after core/buttons.css, and complete the
property set with border: none and text-shadow: none so the chip no
longer carries the 1px transparent border the .but block adds.

Verified with a cascade resolver that replays the real stylesheet load
order (bootstrap, main, public, header-userbox) against the reference
theme's own order: background, color, --this-color, border, text-shadow,
border-radius, padding, margin, font-size, display, overflow and position
now resolve identically for both the publish and the VIP chip.
2026-10-15 13:44:01 +08:00
kate522 0722c4f00e fix(header): restore reference VIP chip geometry and cluster split
The VIP chip never received the reference .but geometry. Commit f03d72c8
narrowed the legacy selector from a descendant to a child combinator as a
side effect of an unrelated user-popover fix, and the contract test then
pinned the narrowed form. Because the VIP anchor lives inside the
hover-show wrapper rather than being a direct child of .navbar-but, it
fell back to the bare .but defaults: 15px type instead of 0.8em, no
0.4em margin, and no padding override.

Restore the descendant selector so both chips share one geometry:

- .navbar-top .navbar-but .but is byte-identical to the reference again,
  giving the VIP chip font-size 0.8em, padding 0.3em 1em !important and
  margin 0.4em
- the publish and VIP anchors are plain inline-block .but elements again
  (the merged flex container no longer blockifies them)
- ml10 is back on the VIP anchor; .navbar-top .navbar-but .but overrides
  it exactly as it does in the reference, so it stays markup parity
  without changing the computed margin

The right cluster was also merged into one .aksrt-navbar-actions flex
container with a 10px gap and zero padding, while the reference emits
three floated .navbar-form.navbar-right groups whose 15px padding is what
produces the inter-cluster spacing. Split them again: publish/VIP in
.navbar-but, theme toggle plus language switcher in a flex ac group, and
the account row in its own group. Measured against the reference, the
publish-to-VIP gap was 19.6px instead of 9.6px, and VIP-to-theme-toggle
was 14.8px instead of 34.8px.

The container rules in header-userbox.css are re-anchored onto the
reference structure: .aksrt-navbar-account owns the only remaining flex
context, .toggle-radius and .aksrt-navbar-language are scoped through
.navbar-form, and the hover-show rule targets .navbar-but. Coverage of
those selectors is unchanged, so the theme toggle, language switcher and
message icon keep their current geometry.

The .aksrt-navbar-actions wrapper and its gap are gone; a negative
assertion now guards against reintroducing them.

Verified: node --test across tests/ (603 passing), php -l on the changed
PHP files, and a stubbed render of aksrt_menu_button() for guest,
logged-in and layout 2.
2026-10-15 12:19:24 +08:00
kate522 4f2edebab6 fix(header): align publish and VIP CTAs with reference theme
The top-nav publish chip and open-membership chip carried an AKSRT-only
class layer (navbar-cta / navbar-cta-text / navbar-newposts /
navbar-vip-hover) plus a matching set of CSS overrides that replaced the
legacy .but geometry: margin 0.4em -> 0, an extra min-height and flex gap,
an FA5 pencil glyph that is not the reference pencil, and label text
wrapped in a span. The VIP hover card also had its own positioning
override instead of the reference static position.

Drop that layer and emit the reference class combination verbatim so the
already byte-identical legacy stylesheet owns the geometry again:

- publish chip renders newadd-btns but nowave <skin> [radius] btn-newadd
  with the label echoed as saved (no injected icon, no span)
- VIP chip renders pay-vip|signin-loader but jb-red radius payvip-icon
  with the vip_1 SVG at em12 mr3, inside span.hover-show.inline-block
- user-card CTA uses the vip_1 SVG (em12 mr6) instead of fa-gem and drops
  vip-level, matching zib_get_header_payvip_icon()
- both buttons now honour Settings::close_sign(), matching the reference
  zib_is_close_sign() gate and the floating publish button
- header-userbox.css keeps one rule that restores the hover wrapper to a
  plain block container so .sub-vip-card takes its reference static
  position; every other override is gone
- nav_newposts_text default drops the duplicated fas and uses the FA5
  spelling of the reference pencil (fa-pencil-alt, not fa-pen)

Dead code removed: aksrt_normalize_header_cta_label(), the unreachable
multi-type dropdown branch of aksrt_render_new_add_btns() (only 'post' is
supported and only ['post'] is ever passed), and the
newadd-btns__toggle / newadd-btns__item / drop-newadd markup it emitted.

ml10 is no longer set on the VIP anchor: .aksrt-navbar-actions already
supplies the 10px gap the reference gets from ml10, and keeping both
would double it to 20px.

No migration for sites that already stored the old nav_newposts_text.

Verified: node --test across tests/ (603 passing), php -l on the changed
PHP files, and a stubbed render of every publish/VIP branch.
2026-10-15 11:54:54 +08:00
kate522 ce6fbb4e39 chore(tools): refresh admin contract report
重新生成契约审计报告:微信 REST 回调补齐后 errors 归零,build 清单与 schema 域同步到当前树。
2026-10-13 21:01:12 +08:00
kate522 cf39e72126 feat(extensions): align Baidu push with reference theme
与参考主题(zibll)1:1 对齐百度推送,并清掉存量双路与死代码:

- 新增普通/快速收录通道开关,快速收录走 &type=daily;自动推送 JS 与参考实现一致

- 分类法 saved_term 推送(可推送类型推导公开分类法,排除 post_format,aksrt_baidu_push_taxonomies 可过滤)

- 推送结果写入 meta 去重,成功后不重复提交;编辑页 Metabox 展示结果并提供「重新提交」(nonce 校验后强制推送)

- 配置面收敛到扩展页单一 canonical 键:删除 SEO 页 seo_baidu_* 重复面与两处合并逻辑,不写迁移兜底

- 清理死代码:push_batch(零调用)及 batch_size、从不写入的 baidu_push_verified、零消费的熊掌号 xzh_* 七键;baidu_push_log_enabled 接入 log()

新增 PHP harness 驱动真实逻辑(通道/去重/重试/重新提交/术语/面板渲染)+ 参考主题契约测试;tests/admin 与 tests/widget-system 全量通过。
2026-10-13 21:01:03 +08:00
kate522 740bd48f42 refactor(users): drop dead WeChat notify settings surface
users/wechat schema 页写入 aksrt_settings 的 wechat_notify_* / wechat_app_* / wechat_template_* 等 15 个键全仓无任何消费方,与活配置面 aksrt_wechat_settings(WeChatNotify 读取、/users/wechat-template 页管理)构成双路;PHP 与 JS 两份 schema 的字段集早已不一致(15 vs 10),是死面腐烂的直接证据。

移除 SchemaUsers 的 wechat 分组、JS schema 文件及其注册、菜单 schema 页条目,微信配置收敛为单一自定义页入口;契约测试锁定死键零残留与活配置面唯一。
2026-10-13 21:00:49 +08:00
kate522 18e579a8c6 fix(admin): restore WeChat template message REST callbacks
/wechat/status 与 /wechat/settings 自引入起只注册了路由、从未实现回调方法,后台「微信模板消息」页调用即 500。

按既定设计补齐:状态读取委托 WeChatNotify::get_status()(其既定用途即 REST 与 AJAX 复用),写入委托 Settings::update_wechat_settings() 并由 Integration 收敛键白名单;前端空密钥下发掩码 *** 时保留已存密钥,避免把密钥写成字面量。

新增 PHP harness 驱动真实 UserController + Integration + WeChatNotify 验证状态形状、掩码保留、缺省沿用、未知键丢弃与输入消毒。
2026-10-13 21:00:35 +08:00
46 changed files with 1858 additions and 624 deletions

No files matched your search

@@ -42,7 +42,7 @@ export const extensionsSection = {
label: '百度推送', label: '百度推送',
route: '/extensions/push', route: '/extensions/push',
title: '百度推送', title: '百度推送',
desc: '配置搜索引擎主动推送、批量提交和状态追踪。', desc: '配置百度自动推送、收录通道与触发规则;推送结果与「重新提交」在文章/分类编辑页查看。',
canonicalDomain: 'extensions', canonicalDomain: 'extensions',
section: 'push', section: 'push',
}), }),
@@ -108,16 +108,6 @@ export const usersSection = {
canonicalDomain: 'users', canonicalDomain: 'users',
section: 'sms', section: 'sms',
}), }),
schemaPage({
key: 'users-wechat',
name: 'UsersWechat',
label: '微信通知',
route: '/users/wechat',
title: '微信通知',
desc: '配置微信模板消息、订阅/解绑和站外通知策略。',
canonicalDomain: 'users',
section: 'wechat',
}),
customPage({ customPage({
key: 'users-wechat-template', key: 'users-wechat-template',
name: 'UsersWechatTemplate', name: 'UsersWechatTemplate',
@@ -2,21 +2,16 @@ export const pushSchema = {
key: 'extensions.push', key: 'extensions.push',
title: '百度推送', title: '百度推送',
icon: 'fa fas fa-paper-plane', icon: 'fa fas fa-paper-plane',
description: '配置搜索引擎主动推送、批量提交和状态追踪。', description: '配置百度主动推送、收录通道和推送状态。',
defaults: { defaults: {
baidu_push_enabled: false, baidu_push_enabled: false,
baidu_push_token: '', baidu_push_token: '',
baidu_push_auto: true, baidu_push_auto: true,
baidu_push_on_publish: true, baidu_push_on_publish: true,
baidu_push_on_update: false, baidu_push_on_update: false,
baidu_push_batch_size: 50, baidu_push_normal: true,
baidu_push_fast: false,
baidu_push_log_enabled: true, baidu_push_log_enabled: true,
xzh_on: false,
xzh_appid: '',
xzh_render_tail: true,
xzh_jsonld_single: true,
xzh_jsonld_page: false,
xzh_jsonld_img: false,
}, },
groups: [ groups: [
{ {
@@ -25,7 +20,7 @@ export const pushSchema = {
icon: 'fa fas fa-rocket', icon: 'fa fas fa-rocket',
fields: [ fields: [
{ key: 'baidu_push_enabled', label: '启用百度推送', type: 'switch', default: false, help: '开启后将自动向百度搜索推送页面链接' }, { key: 'baidu_push_enabled', label: '启用百度推送', type: 'switch', default: false, help: '开启后将自动向百度搜索推送页面链接' },
{ key: 'baidu_push_token', label: '推送 Token', type: 'input', default: '', dependsOn: { key: 'baidu_push_enabled', value: true }, help: '在百度搜索资源平台获取的推送准入 Token' }, { key: 'baidu_push_token', label: '推送 Token', type: 'input', default: '', dependsOn: { key: 'baidu_push_enabled', value: true }, help: '在百度搜索资源平台获取的推送准入 Token(仅填 Token,勿填完整链接),并确保平台中的站点为本站' },
], ],
}, },
{ {
@@ -34,21 +29,17 @@ export const pushSchema = {
icon: 'fa fas fa-bolt', icon: 'fa fas fa-bolt',
fields: [ fields: [
{ key: 'baidu_push_auto', label: '自动推送 (页面加载时)', type: 'switch', default: true, dependsOn: { key: 'baidu_push_enabled', value: true }, help: '在前台页面中嵌入百度自动推送 JS 代码' }, { key: 'baidu_push_auto', label: '自动推送 (页面加载时)', type: 'switch', default: true, dependsOn: { key: 'baidu_push_enabled', value: true }, help: '在前台页面中嵌入百度自动推送 JS 代码' },
{ key: 'baidu_push_on_publish', label: '发布时主动推送', type: 'switch', default: true, dependsOn: { key: 'baidu_push_enabled', value: true } }, { key: 'baidu_push_on_publish', label: '发布时主动推送', type: 'switch', default: true, dependsOn: { key: 'baidu_push_enabled', value: true }, help: '文章 / 分类首次发布时推送链接;推送成功后会记录,不再重复提交' },
{ key: 'baidu_push_on_update', label: '更新时推送', type: 'switch', default: false, dependsOn: { key: 'baidu_push_enabled', value: true } }, { key: 'baidu_push_on_update', label: '更新时推送', type: 'switch', default: false, dependsOn: { key: 'baidu_push_enabled', value: true }, help: '已发布内容再次保存时,若上次推送未成功则重试;需主动再次提交请在编辑页勾选「重新提交」' },
], ],
}, },
{ {
key: 'xzh', key: 'channel',
title: '熊掌号 / JSON-LD', title: '收录通道',
icon: 'fa fas fa-paw', icon: 'fa fas fa-inbox',
fields: [ fields: [
{ key: 'xzh_on', label: '启用熊掌号', type: 'switch', default: false }, { key: 'baidu_push_normal', label: '普通收录', type: 'switch', default: true, dependsOn: { key: 'baidu_push_enabled', value: true }, help: '每天可提交最多10万条有价值内容,收录速度较慢' },
{ key: 'xzh_appid', label: '百度 AppID', type: 'input', default: '', dependsOn: { key: 'xzh_on', value: true } }, { key: 'baidu_push_fast', label: '快速收录', type: 'switch', default: false, dependsOn: { key: 'baidu_push_enabled', value: true }, help: '百度新推出的高效收录接口,目前仅对部分优质站点开放,请确保您的站点已开放快速收录功能' },
{ key: 'xzh_render_tail', label: '文章底部显示熊掌号尾部', type: 'switch', default: true, dependsOn: { key: 'xzh_on', value: true } },
{ key: 'xzh_jsonld_single', label: '文章页 JSON-LD', type: 'switch', default: true, dependsOn: { key: 'xzh_on', value: true } },
{ key: 'xzh_jsonld_page', label: '页面 JSON-LD', type: 'switch', default: false, dependsOn: { key: 'xzh_on', value: true } },
{ key: 'xzh_jsonld_img', label: '不添加图片', type: 'switch', default: false, dependsOn: { key: 'xzh_on', value: true } },
], ],
}, },
{ {
@@ -56,8 +47,7 @@ export const pushSchema = {
title: '高级设置', title: '高级设置',
icon: 'fa fas fa-cog', icon: 'fa fas fa-cog',
fields: [ fields: [
{ key: 'baidu_push_batch_size', label: '批量推送条数', type: 'number', default: 50, min: 1, max: 500, dependsOn: { key: 'baidu_push_enabled', value: true } }, { key: 'baidu_push_log_enabled', label: '记录推送日志', type: 'switch', default: true, dependsOn: { key: 'baidu_push_enabled', value: true }, help: 'WP_DEBUG 开启时,将推送结果写入错误日志' },
{ key: 'baidu_push_log_enabled', label: '记录推送日志', type: 'switch', default: true, dependsOn: { key: 'baidu_push_enabled', value: true } },
], ],
}, },
], ],
@@ -13,10 +13,6 @@ export const seoSchema = {
ai_seo_title_prompt: '', ai_seo_title_prompt: '',
ai_seo_keywords_prompt: '', ai_seo_keywords_prompt: '',
ai_seo_description_prompt: '', ai_seo_description_prompt: '',
seo_baidu_push: false,
seo_baidu_normal: false,
seo_baidu_fast: false,
seo_baidu_token: '',
seo_image_optimize: true, seo_image_optimize: true,
seo_image_default: '', seo_image_default: '',
seo_image_always_default: false, seo_image_always_default: false,
@@ -95,17 +91,6 @@ export const seoSchema = {
}, },
], ],
}, },
{
key: 'baidu',
title: '百度推送',
icon: 'fa fas fa-paper-plane',
fields: [
{ key: 'seo_baidu_push', label: '启用百度自动推送', type: 'switch', default: false },
{ key: 'seo_baidu_normal', label: '普通收录推送', type: 'switch', default: false },
{ key: 'seo_baidu_fast', label: '快速收录推送', type: 'switch', default: false },
{ key: 'seo_baidu_token', label: '百度推送 Token', type: 'input', default: '', placeholder: '百度资源平台 Token' },
],
},
{ {
key: 'image', key: 'image',
title: '图片 SEO', title: '图片 SEO',
@@ -10,7 +10,6 @@ import { featuresSchema } from './features.js'
import { verifySchema } from './verify.js' import { verifySchema } from './verify.js'
import { levelSchema } from './level.js' import { levelSchema } from './level.js'
import { badgeSchema } from './badge.js' import { badgeSchema } from './badge.js'
import { wechatSchema } from './wechat.js'
import { reportSchema } from './report.js' import { reportSchema } from './report.js'
import { smsSchema } from './sms.js' import { smsSchema } from './sms.js'
@@ -23,7 +22,6 @@ export const usersSchemas = {
verify: verifySchema, verify: verifySchema,
level: levelSchema, level: levelSchema,
badge: badgeSchema, badge: badgeSchema,
wechat: wechatSchema,
report: reportSchema, report: reportSchema,
sms: smsSchema, sms: smsSchema,
} }
@@ -1,51 +0,0 @@
export const wechatSchema = {
key: 'users.wechat',
title: '微信通知',
icon: 'fa fab fa-weixin',
description: '配置微信模板消息、订阅/解绑和站外通知策略。',
defaults: {
wechat_notify_enabled: false,
wechat_app_id: '',
wechat_app_secret: '',
wechat_template_id: '',
wechat_token: '',
wechat_encoding_aes_key: '',
wechat_notify_comment: true,
wechat_notify_reply: true,
wechat_notify_system: true,
wechat_bind_entry: true,
},
groups: [
{
key: 'account',
title: '公众号配置',
icon: 'fa fab fa-weixin',
fields: [
{ key: 'wechat_notify_enabled', label: '启用微信通知', type: 'switch', default: false, help: '开启后用户可绑定微信接收通知' },
{ key: 'wechat_app_id', label: 'AppID', type: 'input', default: '', dependsOn: { key: 'wechat_notify_enabled', value: true } },
{ key: 'wechat_app_secret', label: 'AppSecret', type: 'input', default: '', dependsOn: { key: 'wechat_notify_enabled', value: true } },
{ key: 'wechat_template_id', label: '模板消息 ID', type: 'input', default: '', dependsOn: { key: 'wechat_notify_enabled', value: true }, help: '在微信公众平台申请的模板消息 ID' },
],
},
{
key: 'verify',
title: '服务器验证',
icon: 'fa fas fa-check-circle',
fields: [
{ key: 'wechat_token', label: 'Token', type: 'input', default: '', dependsOn: { key: 'wechat_notify_enabled', value: true }, help: '微信公众平台服务器配置中的 Token' },
{ key: 'wechat_encoding_aes_key', label: 'EncodingAESKey', type: 'input', default: '', dependsOn: { key: 'wechat_notify_enabled', value: true } },
],
},
{
key: 'triggers',
title: '通知触发器',
icon: 'fa fas fa-bell',
fields: [
{ key: 'wechat_notify_comment', label: '新评论通知', type: 'switch', default: true, dependsOn: { key: 'wechat_notify_enabled', value: true } },
{ key: 'wechat_notify_reply', label: '评论回复通知', type: 'switch', default: true, dependsOn: { key: 'wechat_notify_enabled', value: true } },
{ key: 'wechat_notify_system', label: '系统通知', type: 'switch', default: true, dependsOn: { key: 'wechat_notify_enabled', value: true } },
{ key: 'wechat_bind_entry', label: '显示绑定入口', type: 'switch', default: true, dependsOn: { key: 'wechat_notify_enabled', value: true }, help: '在用户中心显示微信绑定/解绑按钮' },
],
},
],
}
+3 -2
View File
@@ -709,8 +709,9 @@
color: var(--color-primary, #f04494); color: var(--color-primary, #f04494);
} }
/* .widget a 的链接色不得覆盖 .but 按钮自身配色(zibll 的 zib-widget 不设置链接色) */ /* .widget a 的链接色不得覆盖 .but / .img-badge 自身配色(zibll 的 zib-widget 不设置链接色) */
.widget a.but { .widget a.but,
.widget a.img-badge {
color: var(--this-color, #fff); color: var(--this-color, #fff);
} }
+1 -1
View File
@@ -1 +1 @@
.posts-row{margin-right:-8px;margin-bottom:15px;margin-left:-8px}.posts-row>*:not(.post_ajax_loader){margin-right:8px!important;margin-left:8px!important}.posts-row>.post_ajax_loader .posts-item,.posts-row>.post_ajax_loader .posts-mini{margin-right:8px;margin-left:8px}.posts-item{margin:15px 0;padding:20px;overflow:hidden;background:var(--main-bg-color, #fff);border-radius:var(--main-radius, 8px);box-shadow:0 0 10px var(--main-shadow, rgba(116, 116, 116, .08));transition:.2s}.posts-item.flex,.posts-item .flex,.posts-mini .flex{display:flex}.posts-item .flex.xx,.posts-mini .flex.xx{flex-direction:column}.posts-item .flex.ac,.posts-mini .flex.ac{align-items:center}.posts-item .flex.jsb,.posts-mini .flex.jsb{justify-content:space-between}.posts-item .flex1,.posts-mini .flex1{flex:1;overflow:hidden}.posts-item .post-graphic:not(.order1){margin-right:20px}.posts-item .order1.post-graphic{margin-right:0;margin-left:20px}.posts-item .item-thumbnail{position:relative;display:block;width:190px;height:0;padding-bottom:var(--posts-list-scale, 70%);overflow:hidden;border-radius:var(--main-radius, 8px)}.item-thumbnail.contain img{object-fit:contain}.swiper-slide.gradient-bg .dplayer-poster img,.swiper-slide.gradient-bg>*>img{object-fit:contain!important;border-radius:0!important}.item-thumbnail .swiper-wrapper{height:100%!important}.list .item-thumbnail .aksrt-slider,.list .item-thumbnail.new-swiper{position:absolute;width:100%;height:100%;margin:0!important}.item-thumbnail .dplayer.dplayer-thumb{position:absolute;inset:0;z-index:1;padding-bottom:var(--posts-list-scale, 70%);background:#000;opacity:0;transition:opacity .3s}.card .item-thumbnail .dplayer.dplayer-thumb{padding-bottom:var(--posts-card-scale, 70%)}.item-thumbnail .dplayer-thumb video,.item-thumbnail .swiper-wrapper,.item-thumbnail img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}.item-thumbnail .swiper-pagination{right:0!important;bottom:0!important;left:0!important;padding:0 2px!important;text-align:center!important}.posts-item.thumb-dplayer-playing .dplayer-thumb{opacity:1}.posts-item.thumb-dplayer-playing .abs-center{opacity:0}.posts-item .item-heading{display:-webkit-box;max-height:2.8em;margin-top:0;margin-bottom:5px;overflow:hidden;font-size:18px;line-height:1.4em;white-space:normal;-webkit-box-orient:vertical;-webkit-line-clamp:2}.font-bold{font-weight:700}.list.style2 .item-thumbnail{width:130px}.list.style2 .item-heading{display:unset;max-height:1.4em;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.posts-item .item-heading>a{color:var(--key-color, #333)}.posts-item .item-heading>a:hover{color:var(--focus-color, #f04494)}.posts-item .item-excerpt{margin:0 0 6px;overflow:hidden;color:var(--muted-color, #777);text-overflow:ellipsis;white-space:nowrap}.posts-item .item-meta item{margin-right:8px;font-size:13px}.posts-item .item-meta svg.icon,.posts-mini .item-meta svg.icon{display:inline-block;width:1em;height:1em;margin-right:2px;fill:currentColor;overflow:hidden;vertical-align:-.15em}.posts-item .meta-right item:last-child{margin-right:0}.item-meta .avatar-mini{width:20px;height:20px;border-radius:50%;transform:translateY(-1px)}.item-meta .meta-pay{padding:0 4px}.item-meta .meta-pay img{width:1em;height:1em;object-fit:contain;vertical-align:-.15em}.posts-item .item-meta a,.posts-mini .item-meta a{color:inherit}.posts-item .item-tags a{margin-right:5px;padding:2px 5px;font-size:11px}.posts-item .item-tags a .fa{font-size:1em}.posts-item.card{display:inline-block;width:calc(33.333% - 16px);margin:8px;padding:10px;vertical-align:top;transition:.3s}.posts-item.card:last-child{margin-bottom:20px}.posts-item.card .item-body{width:100%;margin:10px 0 0;padding:0}.posts-item.card .item-meta item{font-size:12px}.posts-mini-con .item-heading,.posts-item.card .item-heading{font-size:14px}.posts-item.card .item-heading{min-height:2.8em}.posts-item.card .item-tags{min-height:23px}.posts-item.card .item-thumbnail{width:100%;padding-bottom:var(--posts-card-scale, 70%)}.posts-item.card.style2{background:transparent;box-shadow:none}.posts-item.card.style2:hover{background:var(--main-bg-color, #fff);box-shadow:0 0 10px var(--main-shadow, rgba(116, 116, 116, .08))}.posts-item.card.style3{padding:0}.posts-item.card.style3 .item-thumbnail,.posts-item.card.style3 .item-thumbnail img{border-radius:var(--main-radius, 8px) var(--main-radius, 8px) 0 0}.posts-item.card.style3 .item-body{margin:0;padding:10px}.posts-item.mult-thumb .thumb-items{margin:0 -3px 6px}.posts-item.mult-thumb .thumb-items>span{position:relative;display:inline-block;width:calc(25% - 6px);height:0;margin:0 3px;padding-bottom:calc(var(--posts-list-scale, 70%) / 4)}.posts-item.mult-thumb .thumb-items>span>img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}.posts-item.mult-thumb .thumb-items.contain>span{overflow:hidden;background:var(--main-border-color, rgba(50, 50, 50, .06));border-radius:var(--main-radius, 8px)}.posts-item.mult-thumb .thumb-items.contaiLine truncated .posts-row{margin-right:-8px;margin-bottom:15px;margin-left:-8px}.posts-row>*:not(.post_ajax_loader){margin-right:8px!important;margin-left:8px!important}.posts-row>.post_ajax_loader .posts-item,.posts-row>.post_ajax_loader .posts-mini{margin-right:8px;margin-left:8px}.posts-item{margin:15px 0;padding:20px;overflow:hidden;background:var(--main-bg-color, #fff);border-radius:var(--main-radius, 8px);box-shadow:0 0 10px var(--main-shadow, rgba(116, 116, 116, .08));transition:.2s}.posts-item.flex,.posts-item .flex,.posts-mini .flex{display:flex}.posts-item .flex.xx,.posts-mini .flex.xx{flex-direction:column}.posts-item .flex.ac,.posts-mini .flex.ac{align-items:center}.posts-item .flex.jsb,.posts-mini .flex.jsb{justify-content:space-between}.posts-item .flex1,.posts-mini .flex1{flex:1;overflow:hidden}.posts-item .post-graphic:not(.order1){margin-right:20px}.posts-item .order1.post-graphic{margin-right:0;margin-left:20px}.posts-item .item-thumbnail{position:relative;display:block;width:190px;height:0;padding-bottom:var(--posts-list-scale, 70%);overflow:hidden;border-radius:var(--main-radius, 8px)}.item-thumbnail.contain img{object-fit:contain}.swiper-slide.gradient-bg .dplayer-poster img,.swiper-slide.gradient-bg>*>img{object-fit:contain!important;border-radius:0!important}.item-thumbnail .swiper-wrapper{height:100%!important}.list .item-thumbnail .aksrt-slider,.list .item-thumbnail.new-swiper{position:absolute;width:100%;height:100%;margin:0!important}.item-thumbnail .dplayer.dplayer-thumb{position:absolute;inset:0;z-index:1;padding-bottom:var(--posts-list-scale, 70%);background:#000;opacity:0;transition:opacity .3s}.card .item-thumbnail .dplayer.dplayer-thumb{padding-bottom:var(--posts-card-scale, 70%)}.item-thumbnail .dplayer-thumb video,.item-thumbnail .swiper-wrapper,.item-thumbnail img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}.item-thumbnail .swiper-pagination{right:0!important;bottom:0!important;left:0!important;padding:0 2px!important;text-align:center!important}.posts-item.thumb-dplayer-playing .dplayer-thumb{opacity:1}.posts-item.thumb-dplayer-playing .abs-center{opacity:0}.posts-item .item-heading{display:-webkit-box;max-height:2.8em;margin-top:0;margin-bottom:5px;overflow:hidden;font-size:18px;line-height:1.4em;white-space:normal;-webkit-box-orient:vertical;-webkit-line-clamp:2}.font-bold{font-weight:700}.list.style2 .item-thumbnail{width:130px}.list.style2 .item-heading{display:unset;max-height:1.4em;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.posts-item .item-heading>a{color:var(--key-color, #333)}.posts-item .item-heading>a:hover{color:var(--focus-color, #f04494)}.posts-item .item-excerpt{margin:0 0 6px;overflow:hidden;color:var(--muted-color, #777);text-overflow:ellipsis;white-space:nowrap}.posts-item .item-meta item{margin-right:8px;font-size:13px}.posts-item .item-meta svg.icon,.posts-mini .item-meta svg.icon{display:inline-block;width:1em;height:1em;margin-right:2px;fill:currentColor;overflow:hidden;vertical-align:-.15em}.posts-item .meta-right item:last-child{margin-right:0}.item-meta .avatar-mini{width:20px;height:20px;border-radius:50%;transform:translateY(-1px)}.item-meta .meta-pay{padding:0 4px}.item-meta .meta-pay img{width:1em;height:1em;object-fit:contain;vertical-align:-.15em}.posts-item .item-meta a,.posts-mini .item-meta a{color:inherit}.posts-item .item-tags a{margin-right:5px;padding:2px 5px;font-size:11px}.posts-item .item-tags a .fa{font-size:1em}.posts-item.card{display:inline-block;width:calc(33.333% - 16px);margin:8px;padding:10px;vertical-align:top;transition:.3s}.posts-item.card:last-child{margin-bottom:20px}.posts-item.card .item-body{width:100%;margin:10px 0 0;padding:0}.posts-item.card .item-meta item{font-size:12px}.posts-mini-con .item-heading,.posts-item.card .item-heading{font-size:14px}.posts-item.card .item-heading{min-height:2.8em}.posts-item.card .item-tags{min-height:23px}.posts-item.card .item-thumbnail{width:100%;padding-bottom:var(--posts-card-scale, 70%)}.posts-item.card.style2{background:transparent;box-shadow:none}.posts-item.card.style2:hover{background:var(--main-bg-color, #fff);box-shadow:0 0 10px var(--main-shadow, rgba(116, 116, 116, .08))}.posts-item.card.style3{padding:0}.posts-item.card.style3 .item-thumbnail,.posts-item.card.style3 .item-thumbnail img{border-radius:var(--main-radius, 8px) var(--main-radius, 8px) 0 0}.posts-item.card.style3 .item-body{margin:0;padding:10px}.posts-item.mult-thumb .thumb-items{margin:0 -3px 6px}.posts-item.mult-thumb .thumb-items>span{position:relative;display:inline-block;width:calc(25% - 6px);height:0;margin:0 3px;padding-bottom:calc(var(--posts-list-scale, 70%) / 4)}.posts-item.mult-thumb .thumb-items>span>img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}.posts-item.mult-thumb .thumb-items.contain>span{overflow:hidden;background:var(--main-border-color, rgba(50, 50, 50, .06));border-radius:var(--main-radius, 8px)}.posts-item.mult-thumb .thumb-items.contaiLine truncated
+48 -70
View File
@@ -1095,7 +1095,7 @@ strong, b {
/* gradient modifiers (legacy .jb-*) */ /* gradient modifiers (legacy .jb-*) */
.jb-red { --this-bg: linear-gradient(135deg, #fd7a64 10%, #fb2d2d 100%); } .jb-red { --this-bg: linear-gradient(135deg, #fd7a64 10%, #fb2d2d 100%); }
.jb-yellow { --this-bg: linear-gradient(135deg, #f99d4d 10%, #f7631d 100%); } .jb-yellow { --this-bg: linear-gradient(135deg, #f59f54 10%, #ff6922 100%); }
.jb-blue { --this-bg: linear-gradient(135deg, #59c3fb 10%, #268df7 100%); } .jb-blue { --this-bg: linear-gradient(135deg, #59c3fb 10%, #268df7 100%); }
.jb-green { --this-bg: linear-gradient(135deg, #60e464 10%, #5cb85b 100%); } .jb-green { --this-bg: linear-gradient(135deg, #60e464 10%, #5cb85b 100%); }
.jb-purple { --this-bg: linear-gradient(135deg, #f98dfb 10%, #ea00f9 100%); } .jb-purple { --this-bg: linear-gradient(135deg, #f98dfb 10%, #ea00f9 100%); }
@@ -2714,27 +2714,29 @@ main.container,
* shell anchoring plus the account action-row geometry. * shell anchoring plus the account action-row geometry.
*/ */
/* The desktop action cluster owns one horizontal alignment context. */ /* The reference keeps the right cluster as three floated .navbar-form groups.
.navbar-top .aksrt-navbar-actions { * Bootstrap's base .navbar-form rule carries margin-top: 8px and the >=768px
display: flex; * override only zeroes the horizontal margins, so every right-cluster group
align-items: center; * starts 8px below the top of .navbar-collapse. The 50px menu row and the logo
gap: 10px; * put the navbar midline at 25px, so a 34px cross-size lands a group's midline
min-height: 50px; * at 8 + 17 = 25px. The publish/VIP chips already reach that line through their
margin-top: 0; * own 0.4em margin stacked on the group's 8px; the account row reaches it from
margin-bottom: 0; * its 34px icon tiles, and the theme group needs the same cross-size because
padding: 0; * its 29px toggle is otherwise 2.5px high. Nothing here may restate a 50px
} * min-height: it would double-count the 8px and push that group's content 8px
* below the line while inflating the navbar from 50px to 58px. */
.navbar-top .aksrt-navbar-actions > *, .navbar-top .aksrt-navbar-account {
.navbar-top .aksrt-navbar-account,
.navbar-top .aksrt-navbar-account > li {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
flex: 0 0 auto; gap: 4px;
margin: 0;
} }
.navbar-top .aksrt-navbar-account > li { .navbar-top .aksrt-navbar-account > li {
position: relative; position: relative;
display: inline-flex;
align-items: center;
flex: 0 0 auto;
padding: 0; padding: 0;
overflow: visible; overflow: visible;
} }
@@ -2747,39 +2749,6 @@ main.container,
margin: 0; margin: 0;
} }
.navbar-top .aksrt-navbar-account {
gap: 4px;
min-height: 50px;
margin: 0;
}
.navbar-top .aksrt-navbar-account.navbar-text {
float: none;
}
.navbar-top .aksrt-navbar-actions .navbar-cta {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 4px;
min-height: 29px;
margin: 0;
padding: .3em 1em !important;
border-radius: var(--radius-btn, 4px);
font-size: .8em;
line-height: 1.44;
}
.navbar-top .aksrt-navbar-actions .navbar-cta.radius {
border-radius: 50px;
}
.navbar-top .aksrt-navbar-actions .navbar-cta > .icon {
width: 1.2em;
height: 1.2em;
flex: 0 0 auto;
}
.navbar-top .aksrt-nav-icon, .navbar-top .aksrt-nav-icon,
.navbar-top .aksrt-nav-avatar { .navbar-top .aksrt-nav-avatar {
display: inline-flex !important; display: inline-flex !important;
@@ -2812,33 +2781,34 @@ main.container,
object-fit: cover; object-fit: cover;
} }
.navbar-top .aksrt-navbar-actions .toggle-radius { /* The account-row chips, the theme and the language toggles all want the 29px
* midline circle. The popover's quick-login buttons are .toggle-radius too and
* also live inside .navbar-form, but they must keep the .social_loginbar
* geometry from main.css: in a flex container flex-basis outranks width, so
* without this exemption the buttons collapse to 29px and lose the 5px side
* margins that space them apart. */
.navbar-top .navbar-form .toggle-radius:not(.social-login-item):not(.xh-social-item) {
flex: 0 0 29px; flex: 0 0 29px;
margin: 0; margin: 0;
vertical-align: middle; vertical-align: middle;
} }
.navbar-top .aksrt-navbar-language { /* Match the account row's 34px cross-size so the 29px toggle shares the 25px
* midline instead of sitting 2.5px above it. */
.navbar-top .navbar-form.flex.ac {
min-height: 34px;
}
.navbar-top .navbar-form .aksrt-navbar-language {
margin: 0; margin: 0;
} }
/* Anchor the VIP preview to the full navigation row instead of relying on /* The reference keeps the publish/VIP hover wrapper a plain inline-block, so the
* the absolute panel's static position beside the compact CTA. */ * absolutely positioned .sub-vip-card takes its normal-flow static position.
.navbar-top .aksrt-navbar-actions .navbar-vip-hover { * The shared action cluster otherwise forces inline-flex on every child, which
position: relative; * would move that static position to the flex container's cross-start. */
align-self: stretch; .navbar-top .navbar-but > .hover-show {
align-items: center; display: inline-block;
overflow: visible;
}
.navbar-top .aksrt-navbar-actions .navbar-vip-hover > .sub-vip-card {
top: 100%;
right: 0;
left: auto;
width: max-content;
max-width: calc(100vw - 24px);
margin: 0;
z-index: 1002;
} }
/* Keep the drop-down search panel above a fixed header (z-index: 999). */ /* Keep the drop-down search panel above a fixed header (z-index: 999). */
@@ -2886,11 +2856,19 @@ main.container,
padding: 6px; padding: 6px;
} }
/* The later .but compatibility layer otherwise replaces the legacy payvip surface. */ /* core/buttons.css restates the legacy .but block after main.css, so its
.aksrt-header-userbox .pay-vip.but.payvip-icon { * background / color / border / text-shadow declarations outrank .payvip-icon
* and replace the payvip surface with whatever .jb-* skin is on the element.
* The reference theme has no such compatibility layer, so its .payvip-icon
* declarations are the last word in the cascade. Re-assert them here, which
* loads after core/buttons.css, so every payvip surface (top-nav chip, user
* card status bar and CTA, guest CTA) keeps the legacy metallic gradient. */
.payvip-icon {
background: linear-gradient(300deg, #4c4d51, #2a2a31 15%, #85858a 40%, #393a3c 60%, #393838 80%, #5e5f62 100%); background: linear-gradient(300deg, #4c4d51, #2a2a31 15%, #85858a 40%, #393a3c 60%, #393838 80%, #5e5f62 100%);
color: #ece0e0; color: #ece0e0;
--this-color: #ece0e0; --this-color: #ece0e0;
border: none;
text-shadow: none;
} }
/* Keep the header check-in action flush with the VIP row's padded inner width. */ /* Keep the header check-in action flush with the VIP row's padded inner width. */
+1 -1
View File
@@ -1 +1 @@
:root{--color-primary: #f04494;--color-primary-hover: #e0307e;--color-primary-active: #c9206d;--color-primary-soft: rgba(240, 68, 148, .1);--color-primary-softer: rgba(240, 68, 148, .05);--color-on-primary: #FFFFFF;--color-ink: #333333;--color-ink-2: #4e5358;--color-ink-3: #777777;--color-ink-4: #999999;--surface-page: #f5f6f7;--surface-1: #FFFFFF;--surface-2: #eeeeee;--surface-3: #e9ecef;--surface-4: #f8f8f8;--color-line: rgba(50, 50, 50, .06);--color-line-soft: rgba(0, 0, 0, .03);--color-line-stronger: rgba(50, 50, 50, .12);--color-success: #10B981;--color-warning: #F59E0B;--color-error: #EF4444;--color-info: #3B82F6;--color-accent: #18a058;--color-accent-soft: rgba(24, 160, 88, .12);--color-accent-shadow: rgba(24, 160, 88, .28);--font-display: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans SC", "PingFang SC", "Microsoft YaHei", sans-serif;--font-body: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans SC", "PingFang SC", "Microsoft YaHei", sans-serif;--font-ui: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans SC", "PingFang SC", "Microsoft YaHei", sans-serif;--font-mono: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace;--text-xs: .75rem;--text-sm: .875rem;--text-base: .875rem;--text-md: 1.125rem;--text-lg: 1.25rem;--text-xl: 1.5rem;--text-2xl: 1.875rem;--text-3xl: 2.25rem;--text-4xl: 3rem;--text-5xl: 3.75rem;--leading-tight: 1.2;--leading-snug: 1.4;--leading-normal: 1.6;--leading-relaxed: 1.6;--space-1: 4px;--space-2: 8px;--space-3: 12px;--space-4: 16px;--space-5: 20px;--space-6: 24px;--space-7: 32px;--space-8: 40px;--space-9: 48px;--space-10: 64px;--space-11: 80px;--space-12: 96px;--radius-xs: 3px;--radius-sm: 4px;--radius: 8px;--radius-lg: 8px;--radius-xl: 12px;--radius-2xl: 16px;--radius-pill: 100px;--radius-avatar: 50%;--shadow-xs: 0 1px 2px rgba(0, 0, 0, .03);--shadow-sm: 0 0 10px rgba(116, 116, 116, .08);--shadow: 0 0 10px rgba(116, 116, 116, .12);--shadow-lg: 0 12px 32px rgba(18, 18, 18, .1);--shadow-drawer: 0 18px 60px rgba(0, 0, 0, .2);--nav-height: 60px;--container: 1200px;--container-wide: 1440px;--container-read: 760px;--container-page: 880px;--sidebar-width: 311px;--content-gap: 24px;--section-gap: 24px;--sidebar-bp: 992px;--ease: cubic-bezier(.4, 0, .2, 1);--ease-out: cubic-bezier(.16, 1, .3, 1);--ease-spring: cubic-bezier(.34, 1.56, .64, 1);--dur-fast: .15s;--dur: .25s;--dur-slow: .4s;--focus-ring: 0 0 2px 3px rgba(253, 83, 161, .4);--z-dropdown: 100;--z-sticky: 200;--z-fixed: 300;--z-overlay: 400;--z-modal: 500;--z-popover: 600;--z-toast: 700;--z-tooltip: 800;--radius-btn: 4px;--btn-sm-h: 28px;--btn-md-h: 34px;--btn-lg-h: 44px;--card-padding: 15px;--card-padding-sm: 10px;--card-gap: 16px;--card-gap-lg: 20px;--color-primary-light: var(--color-primary-soft);--color-surface: var(--surface-1);--color-line-strong: var(--color-line);--color-bg-hover: var(--surface-2);--color-border-light: var(--color-line-soft);--color-fill: var(--surface-2);--color-warning-bg: #fffbe6;--color-warning-text: #ad6800;--text-secondary: var(--color-ink-3);--text-tertiary: var(--color-ink-4);--surface-active: var(--surface-2);--surface-card: var(--surface-1);--surface-hover: var(--surface-2);--surface-secondary: var(--surface-2);--radius-code: var(--radius);--radius-full: 100px;--shadow-xl: 0 0 10px rgba(116, 116, 116, .12);--space-16: 64px;--z-floating: 900;--transition-fast: .15s ease;--transition-base: .25s ease;--header-bg: var(--blur-bg);--header-color: var(--color-ink-2);--footer-bg: var(--surface-1);--footer-color: var(--color-ink-4);--blur-bg: rgba(255, 255, 255, .8);--float-btn-bg: rgba(200, 200, 200, .4);--font-heading: var(--font-ui);--radius-card: var(--radius);--radius-md: var(--radius);--color-primary-50: var(--color-primary-soft);--color-primary-dark: var(--color-primary-active);--shadow-md: var(--shadow);--mini-radius: calc(var(--radius) / 1.4);--aksrt-avatar-size-xs: 24px;--aksrt-avatar-size-sm: 32px;--aksrt-avatar-size: 40px;--aksrt-avatar-size-lg: 56px;--aksrt-avatar-size-xl: 80px;--aksrt-card-thumb-scale: 56.25%;--aksrt-rose: #f43f5e;--aksrt-rose-soft: rgba(244, 63, 94, .12);--aksrt-blue: #3b82f6;--aksrt-blue-soft: rgba(59, 130, 246, .12);--aksrt-green: #10b981;--aksrt-green-soft: rgba(16, 185, 129, .12);--aksrt-orange: #f97316;--aksrt-orange-soft:rgba(249, 115, 22, .12);--aksrt-purple: #8b5cf6;--aksrt-purple-soft:rgba(139, 92, 246, .12);--aksrt-cyan: #06b6d4;--aksrt-cyan-soft: rgba(6, 182, 212, .12);--aksrt-yellow: #eab308;--aksrt-yellow-soft:rgba(234, 179, 8, .12);--aksrt-red: #ef4444;--aksrt-red-soft: rgba(239, 68, 68, .12);--aksrt-teal: #14b8a6;--aksrt-teal-soft: rgba(20, 184, 166, .12);--aksrt-indigo: #6366f1;--aksrt-indigo-soft:rgba(99, 102, 241, .12);--aksrt-pink: #ec4899;--aksrt-pink-soft: rgba(236, 72, 153, .12);--aksrt-vip-gold-1: #f6d365;--aksrt-vip-gold-2: #fda085;--aksrt-grad-sunset-1: #f093fb;--aksrt-grad-sunset-2: #f5576c;--aksrt-graLine truncated :root{--color-primary: #f04494;--color-primary-hover: #e0307e;--color-primary-active: #c9206d;--color-primary-soft: rgba(240, 68, 148, .1);--color-primary-softer: rgba(240, 68, 148, .05);--color-on-primary: #FFFFFF;--color-ink: #333333;--color-ink-2: #4e5358;--color-ink-3: #777777;--color-ink-4: #999999;--surface-page: #f5f6f7;--surface-1: #FFFFFF;--surface-2: #eeeeee;--surface-3: #e9ecef;--surface-4: #f8f8f8;--color-line: rgba(50, 50, 50, .06);--color-line-soft: rgba(0, 0, 0, .03);--color-line-stronger: rgba(50, 50, 50, .12);--color-success: #10B981;--color-warning: #F59E0B;--color-error: #EF4444;--color-info: #3B82F6;--color-accent: #18a058;--color-accent-soft: rgba(24, 160, 88, .12);--color-accent-shadow: rgba(24, 160, 88, .28);--font-display: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans SC", "PingFang SC", "Microsoft YaHei", sans-serif;--font-body: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans SC", "PingFang SC", "Microsoft YaHei", sans-serif;--font-ui: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans SC", "PingFang SC", "Microsoft YaHei", sans-serif;--font-mono: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace;--text-xs: .75rem;--text-sm: .875rem;--text-base: .875rem;--text-md: 1.125rem;--text-lg: 1.25rem;--text-xl: 1.5rem;--text-2xl: 1.875rem;--text-3xl: 2.25rem;--text-4xl: 3rem;--text-5xl: 3.75rem;--leading-tight: 1.2;--leading-snug: 1.4;--leading-normal: 1.6;--leading-relaxed: 1.6;--space-1: 4px;--space-2: 8px;--space-3: 12px;--space-4: 16px;--space-5: 20px;--space-6: 24px;--space-7: 32px;--space-8: 40px;--space-9: 48px;--space-10: 64px;--space-11: 80px;--space-12: 96px;--radius-xs: 3px;--radius-sm: 4px;--radius: 8px;--radius-lg: 8px;--radius-xl: 12px;--radius-2xl: 16px;--radius-pill: 100px;--radius-avatar: 50%;--shadow-xs: 0 1px 2px rgba(0, 0, 0, .03);--shadow-sm: 0 0 10px rgba(116, 116, 116, .08);--shadow: 0 0 10px rgba(116, 116, 116, .12);--shadow-lg: 0 12px 32px rgba(18, 18, 18, .1);--shadow-drawer: 0 18px 60px rgba(0, 0, 0, .2);--nav-height: 60px;--container: 1200px;--container-wide: 1440px;--container-read: 760px;--container-page: 880px;--sidebar-width: 311px;--content-gap: 24px;--section-gap: 24px;--sidebar-bp: 992px;--ease: cubic-bezier(.4, 0, .2, 1);--ease-out: cubic-bezier(.16, 1, .3, 1);--ease-spring: cubic-bezier(.34, 1.56, .64, 1);--dur-fast: .15s;--dur: .25s;--dur-slow: .4s;--focus-ring: 0 0 2px 3px rgba(253, 83, 161, .4);--z-dropdown: 100;--z-sticky: 200;--z-fixed: 300;--z-overlay: 400;--z-modal: 500;--z-popover: 600;--z-toast: 700;--z-tooltip: 800;--radius-btn: 4px;--btn-sm-h: 28px;--btn-md-h: 34px;--btn-lg-h: 44px;--card-padding: 15px;--card-padding-sm: 10px;--card-gap: 16px;--card-gap-lg: 20px;--color-primary-light: var(--color-primary-soft);--color-surface: var(--surface-1);--color-line-strong: var(--color-line);--color-bg-hover: var(--surface-2);--color-border-light: var(--color-line-soft);--color-fill: var(--surface-2);--color-warning-bg: #fffbe6;--color-warning-text: #ad6800;--text-secondary: var(--color-ink-3);--text-tertiary: var(--color-ink-4);--surface-active: var(--surface-2);--surface-card: var(--surface-1);--surface-hover: var(--surface-2);--surface-secondary: var(--surface-2);--radius-code: var(--radius);--radius-full: 100px;--shadow-xl: 0 0 10px rgba(116, 116, 116, .12);--space-16: 64px;--z-floating: 900;--transition-fast: .15s ease;--transition-base: .25s ease;--header-bg: var(--blur-bg);--header-color: var(--color-ink-2);--footer-bg: var(--surface-1);--footer-color: var(--color-ink-4);--blur-bg: rgba(255, 255, 255, .8);--float-btn-bg: rgba(200, 200, 200, .4);--font-heading: var(--font-ui);--radius-card: var(--radius);--radius-md: var(--radius);--color-primary-50: var(--color-primary-soft);--color-primary-dark: var(--color-primary-active);--shadow-md: var(--shadow);--mini-radius: calc(var(--radius) / 1.4);--aksrt-avatar-size-xs: 24px;--aksrt-avatar-size-sm: 32px;--aksrt-avatar-size: 40px;--aksrt-avatar-size-lg: 56px;--aksrt-avatar-size-xl: 80px;--aksrt-card-thumb-scale: 56.25%;--aksrt-rose: #f43f5e;--aksrt-rose-soft: rgba(244, 63, 94, .12);--aksrt-blue: #3b82f6;--aksrt-blue-soft: rgba(59, 130, 246, .12);--aksrt-green: #10b981;--aksrt-green-soft: rgba(16, 185, 129, .12);--aksrt-orange: #f97316;--aksrt-orange-soft:rgba(249, 115, 22, .12);--aksrt-purple: #8b5cf6;--aksrt-purple-soft:rgba(139, 92, 246, .12);--aksrt-cyan: #06b6d4;--aksrt-cyan-soft: rgba(6, 182, 212, .12);--aksrt-yellow: #eab308;--aksrt-yellow-soft:rgba(234, 179, 8, .12);--aksrt-red: #ef4444;--aksrt-red-soft: rgba(239, 68, 68, .12);--aksrt-teal: #14b8a6;--aksrt-teal-soft: rgba(20, 184, 166, .12);--aksrt-indigo: #6366f1;--aksrt-indigo-soft:rgba(99, 102, 241, .12);--aksrt-pink: #ec4899;--aksrt-pink-soft: rgba(236, 72, 153, .12);--aksrt-vip-gold-1: #f6d365;--aksrt-vip-gold-2: #fda085;--aksrt-grad-sunset-1: #f093fb;--aksrt-grad-sunset-2: #f5576c;--aksrt-graLine truncated
+1 -1
View File
@@ -316,7 +316,7 @@
/* gradient modifiers (legacy .jb-*) */ /* gradient modifiers (legacy .jb-*) */
.jb-red { --this-bg: linear-gradient(135deg, #fd7a64 10%, #fb2d2d 100%); } .jb-red { --this-bg: linear-gradient(135deg, #fd7a64 10%, #fb2d2d 100%); }
.jb-yellow { --this-bg: linear-gradient(135deg, #f99d4d 10%, #f7631d 100%); } .jb-yellow { --this-bg: linear-gradient(135deg, #f59f54 10%, #ff6922 100%); }
.jb-blue { --this-bg: linear-gradient(135deg, #59c3fb 10%, #268df7 100%); } .jb-blue { --this-bg: linear-gradient(135deg, #59c3fb 10%, #268df7 100%); }
.jb-green { --this-bg: linear-gradient(135deg, #60e464 10%, #5cb85b 100%); } .jb-green { --this-bg: linear-gradient(135deg, #60e464 10%, #5cb85b 100%); }
.jb-purple { --this-bg: linear-gradient(135deg, #f98dfb 10%, #ea00f9 100%); } .jb-purple { --this-bg: linear-gradient(135deg, #f98dfb 10%, #ea00f9 100%); }
+47 -69
View File
@@ -7,27 +7,29 @@
* shell anchoring plus the account action-row geometry. * shell anchoring plus the account action-row geometry.
*/ */
/* The desktop action cluster owns one horizontal alignment context. */ /* The reference keeps the right cluster as three floated .navbar-form groups.
.navbar-top .aksrt-navbar-actions { * Bootstrap's base .navbar-form rule carries margin-top: 8px and the >=768px
display: flex; * override only zeroes the horizontal margins, so every right-cluster group
align-items: center; * starts 8px below the top of .navbar-collapse. The 50px menu row and the logo
gap: 10px; * put the navbar midline at 25px, so a 34px cross-size lands a group's midline
min-height: 50px; * at 8 + 17 = 25px. The publish/VIP chips already reach that line through their
margin-top: 0; * own 0.4em margin stacked on the group's 8px; the account row reaches it from
margin-bottom: 0; * its 34px icon tiles, and the theme group needs the same cross-size because
padding: 0; * its 29px toggle is otherwise 2.5px high. Nothing here may restate a 50px
} * min-height: it would double-count the 8px and push that group's content 8px
* below the line while inflating the navbar from 50px to 58px. */
.navbar-top .aksrt-navbar-actions > *, .navbar-top .aksrt-navbar-account {
.navbar-top .aksrt-navbar-account,
.navbar-top .aksrt-navbar-account > li {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
flex: 0 0 auto; gap: 4px;
margin: 0;
} }
.navbar-top .aksrt-navbar-account > li { .navbar-top .aksrt-navbar-account > li {
position: relative; position: relative;
display: inline-flex;
align-items: center;
flex: 0 0 auto;
padding: 0; padding: 0;
overflow: visible; overflow: visible;
} }
@@ -40,39 +42,6 @@
margin: 0; margin: 0;
} }
.navbar-top .aksrt-navbar-account {
gap: 4px;
min-height: 50px;
margin: 0;
}
.navbar-top .aksrt-navbar-account.navbar-text {
float: none;
}
.navbar-top .aksrt-navbar-actions .navbar-cta {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 4px;
min-height: 29px;
margin: 0;
padding: .3em 1em !important;
border-radius: var(--radius-btn, 4px);
font-size: .8em;
line-height: 1.44;
}
.navbar-top .aksrt-navbar-actions .navbar-cta.radius {
border-radius: 50px;
}
.navbar-top .aksrt-navbar-actions .navbar-cta > .icon {
width: 1.2em;
height: 1.2em;
flex: 0 0 auto;
}
.navbar-top .aksrt-nav-icon, .navbar-top .aksrt-nav-icon,
.navbar-top .aksrt-nav-avatar { .navbar-top .aksrt-nav-avatar {
display: inline-flex !important; display: inline-flex !important;
@@ -105,33 +74,34 @@
object-fit: cover; object-fit: cover;
} }
.navbar-top .aksrt-navbar-actions .toggle-radius { /* The account-row chips, the theme and the language toggles all want the 29px
* midline circle. The popover's quick-login buttons are .toggle-radius too and
* also live inside .navbar-form, but they must keep the .social_loginbar
* geometry from main.css: in a flex container flex-basis outranks width, so
* without this exemption the buttons collapse to 29px and lose the 5px side
* margins that space them apart. */
.navbar-top .navbar-form .toggle-radius:not(.social-login-item):not(.xh-social-item) {
flex: 0 0 29px; flex: 0 0 29px;
margin: 0; margin: 0;
vertical-align: middle; vertical-align: middle;
} }
.navbar-top .aksrt-navbar-language { /* Match the account row's 34px cross-size so the 29px toggle shares the 25px
* midline instead of sitting 2.5px above it. */
.navbar-top .navbar-form.flex.ac {
min-height: 34px;
}
.navbar-top .navbar-form .aksrt-navbar-language {
margin: 0; margin: 0;
} }
/* Anchor the VIP preview to the full navigation row instead of relying on /* The reference keeps the publish/VIP hover wrapper a plain inline-block, so the
* the absolute panel's static position beside the compact CTA. */ * absolutely positioned .sub-vip-card takes its normal-flow static position.
.navbar-top .aksrt-navbar-actions .navbar-vip-hover { * The shared action cluster otherwise forces inline-flex on every child, which
position: relative; * would move that static position to the flex container's cross-start. */
align-self: stretch; .navbar-top .navbar-but > .hover-show {
align-items: center; display: inline-block;
overflow: visible;
}
.navbar-top .aksrt-navbar-actions .navbar-vip-hover > .sub-vip-card {
top: 100%;
right: 0;
left: auto;
width: max-content;
max-width: calc(100vw - 24px);
margin: 0;
z-index: 1002;
} }
/* Keep the drop-down search panel above a fixed header (z-index: 999). */ /* Keep the drop-down search panel above a fixed header (z-index: 999). */
@@ -179,11 +149,19 @@
padding: 6px; padding: 6px;
} }
/* The later .but compatibility layer otherwise replaces the legacy payvip surface. */ /* core/buttons.css restates the legacy .but block after main.css, so its
.aksrt-header-userbox .pay-vip.but.payvip-icon { * background / color / border / text-shadow declarations outrank .payvip-icon
* and replace the payvip surface with whatever .jb-* skin is on the element.
* The reference theme has no such compatibility layer, so its .payvip-icon
* declarations are the last word in the cascade. Re-assert them here, which
* loads after core/buttons.css, so every payvip surface (top-nav chip, user
* card status bar and CTA, guest CTA) keeps the legacy metallic gradient. */
.payvip-icon {
background: linear-gradient(300deg, #4c4d51, #2a2a31 15%, #85858a 40%, #393a3c 60%, #393838 80%, #5e5f62 100%); background: linear-gradient(300deg, #4c4d51, #2a2a31 15%, #85858a 40%, #393a3c 60%, #393838 80%, #5e5f62 100%);
color: #ece0e0; color: #ece0e0;
--this-color: #ece0e0; --this-color: #ece0e0;
border: none;
text-shadow: none;
} }
/* Keep the header check-in action flush with the VIP row's padded inner width. */ /* Keep the header check-in action flush with the VIP row's padded inner width. */
-50
View File
@@ -166,56 +166,6 @@ html.dark .archive-timeline__badge {
font-size: 13px; font-size: 13px;
} }
/* 社交登录分隔线 */
.social-login-divider {
display: flex;
align-items: center;
margin: 24px 0 16px;
color: var(--color-ink-3, #94a3b8);
font-size: 13px;
}
.social-login-divider::before,
.social-login-divider::after {
content: '';
flex: 1;
height: 1px;
background: var(--color-line, #e2e8f0);
}
.social-login-divider span {
padding: 0 16px;
white-space: nowrap;
}
.oauth-row {
display: flex;
flex-wrap: wrap;
gap: 10px;
justify-content: center;
margin-top: 16px;
}
.oauth-btn {
width: 44px;
height: 44px;
border-radius: var(--radius-btn);
border: 1px solid var(--color-line);
display: flex;
align-items: center;
justify-content: center;
color: var(--color-ink-3);
transition: border-color 0.2s, color 0.2s;
}
.oauth-btn i {
font-size: 22px;
line-height: 1;
display: block;
}
.oauth-btn:hover {
border-color: var(--color-primary);
color: var(--color-primary);
}
.auth-footer-link { .auth-footer-link {
text-align: center; text-align: center;
margin-top: 20px; margin-top: 20px;
+3 -2
View File
@@ -50,8 +50,9 @@
color: var(--color-primary, #f04494); color: var(--color-primary, #f04494);
} }
/* .widget a 的链接色不得覆盖 .but 按钮自身配色(zibll 的 zib-widget 不设置链接色) */ /* .widget a 的链接色不得覆盖 .but / .img-badge 自身配色(zibll 的 zib-widget 不设置链接色) */
.widget a.but { .widget a.but,
.widget a.img-badge {
color: var(--this-color, #fff); color: var(--this-color, #fff);
} }
+9 -4
View File
@@ -6838,7 +6838,7 @@ badge.top,
padding-right: 5px; padding-right: 5px;
} }
.navbar-top .navbar-but > .but { .navbar-top .navbar-but .but {
font-size: 0.8em; font-size: 0.8em;
padding: 0.3em 1em !important; padding: 0.3em 1em !important;
margin: 0.4em; margin: 0.4em;
@@ -9655,11 +9655,16 @@ img.alignleft {
padding: 15px; padding: 15px;
} }
/* AKSRT 适配:站点行高(--leading-normal: 1.6)与参考主题 bootstrap /* AKSRT 适配:签到书签排版逐字对齐参考主题 bootstrap 的 body 计算值——
* 的 1.42857143 不同,用户卡片内的 img-badge(签到书签)钉住参考行高; * 行高 1.42857143(站点 --leading-normal 为 1.6)、字体族
* 字号已随 --text-base 对齐参考主题(14px),无需单独声明。 */ * "Helvetica Neue", Helvetica, Arial, sans-serif(站点 --font-body 为
* 现代系统栈),并还原浏览器默认的字体平滑与渲染(站点 html 重置为
* antialiased + optimizeLegibility);字号随 --text-base 已是参考的 14px。 */
.user-card .card-content .img-badge { .user-card .card-content .img-badge {
line-height: 1.42857143; line-height: 1.42857143;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
-webkit-font-smoothing: auto;
text-rendering: auto;
} }
.user-card name .display-name { .user-card name .display-name {
+1 -1
View File
@@ -1 +1 @@
body{--mian-max-width: 1200px;--posts-card-scale: 70%;--posts-list-scale: 70%;--theme-color: #f04494;--focus-color: var(--theme-color);--focus-shadow-color: rgba(253, 83, 161, .4);--key-color: #333;--main-color: #4e5358;--main-shadow: rgba(116, 116, 116, .08);--muted-color: #777;--muted-2-color: #999;--muted-3-color: #b1b1b1;--muted-4-color: #d2d2d2;--body-bg-color: #f5f6f7;--main-bg-color: #fff;--muted-bg-color: #eee;--muted-bg-2-color: #f6f7f8;--main-border-color: rgba(50, 50, 50, .06);--muted-border-color: rgba(0, 0, 0, .03);--main-radius: 8px;--mini-radius: calc(var(--main-radius) / 1.4);--blur-bg: rgba(255, 255, 255, .8);--float-btn-bg: rgba(200, 200, 200, .4);--header-bg: var(--blur-bg);--header-color: var(--main-color);--footer-bg: var(--main-bg-color);--footer-color: var(--muted-2-color)}.dark-theme{--key-color: #f8fafc;--main-color: #e5eef7;--main-shadow: rgba(24, 24, 24, .1);--muted-color: #b4b6bb;--muted-2-color: #888a8f;--muted-3-color: #636469;--muted-4-color: #43454a;--body-bg-color: #292a2d;--main-bg-color: #323335;--muted-bg-color: #2d2e31;--muted-bg-2-color: #37383a;--main-border-color: rgba(114, 114, 114, .1);--muted-border-color: rgba(184, 184, 184, .02);--blur-bg: rgba(50, 51, 53, .8);--float-btn-bg: rgba(70, 70, 70, .4);--header-bg: var(--blur-bg);--header-color: var(--main-color);--footer-bg: var(--main-bg-color);--footer-color: var(--muted-2-color)}::view-transition-old(root),::view-transition-new(root){animation:none;mix-blend-mode:normal}.dark::view-transition-old(root){z-index:1}.dark::view-transition-new(root){z-index:2147483646}::view-transition-old(root){z-index:2147483646}::view-transition-new(root){z-index:1}@supports not ((-webkit-backdrop-filter: blur(20px)) or (backdrop-filter: blur(20px))){body{--blur-bg: #ffffff}body.dark-theme{--blur-bg: #37383a}}body:has(.modal.in),body.modal-open{--blur-bg: #ffffff}body.dark-theme:has(.modal.in),body.dark-theme.modal-open{--blur-bg: #37383a}body.fps-accelerat{--blur-bg: #ffffff}body.fps-accelerat.dark-theme{--blur-bg: #37383a}body.fps-accelerat *{-webkit-backdrop-filter:unset!important;backdrop-filter:unset!important}a[href],.relates-thumb li a>.post-info,.more-posts a>span{transition:color .2s,background .3s,opacity .3s,box-shadow .3s,transform .3s}.main-shadow{transition:box-shadow .3s}.but:focus,.but:hover,.progress,.navbar-nav>li:before{transition:.3s}.navbar-top .sub-menu,.theme-popover,.hover-show .hover-show-con{opacity:0;transform:translateY(6px);transition:.4s}.navbar-top li:hover>.sub-menu,.hover-show:hover .hover-show-con,.hover-show:hover~.hover-show-con,.hover-show:hover~.sub-menu{opacity:1;transform:translateY(0)}.hover-show{position:relative;cursor:pointer;z-index:10}.hover-show .hover-show-con{position:absolute;visibility:hidden}.hover-show:hover .hover-show-con,.hover-show:hover~.hover-show-con,.hover-show:hover~.sub-menu{visibility:unset}.match-ok,.navbar-top .sub-menu li>a,.ic-close{transition:.3s}.ic-close:hover{transform:rotate(270deg);color:inherit}.social_loginbar a,.item-thumbnail img,.dosc-nav .fa{transition:.2s ease-out}.hover-zoom:hover,.hover-zoom-img:hover img,.social_loginbar .xh-social-item:hover,.social_loginbar .social-login-item:hover{transform:scale(1.05)}.hover-zoom-sm:hover,.hover-zoom-img-sm:hover img,.posts-mini:hover img,.posts-item:hover .item-thumbnail img,.posts-item.mult-thumb .thumb-items>span>img:hover{transform:scale(1.02)}.main-min-height{min-height:calc(100vh - 280px)}.main-shadow{box-shadow:0 0 10px var(--main-shadow)}.key-color{color:var(--key-color)}.focus-color{--this-color: var(--focus-color);color:var(--focus-color)}.muted-color{color:var(--muted-color)}.muted-2-color{color:var(--muted-2-color)}.muted-3-color{color:var(--muted-3-color)}.main-bg{background:var(--main-bg-color)}.placeholder{background:linear-gradient(118deg,#7979791a 25%,#79797933 37%,#7979791a 63%);opacity:1;border-radius:4px;-webkit-animation:placeholder 1.5s ease infinite!important;animation:placeholder 1.5s ease infinite!important;background-size:400% 100%!important}.placeholder.img{height:119px}.placeholder.t1{height:24px;width:80%}.placeholder.k1{height:20px;width:60%}.placeholder.k2{height:20px}.placeholder.k3{height:15px}.placeholder.s1{width:70px;height:15px;display:inline-block}@-webkit-keyframes placeholder{0%{background-position:100% 50%}to{background-position:0 50%}}@keyframes placeholder{0%{background-position:100% 50%}to{background-position:0 50%}}@-webkit-keyframes fade{0%{opacity:0}to{opacity:1}}@keyframes fade{0%{opacity:0}to{opacity:1}}.ani.shake{-webkit-animation:shake .3s ease-in!important;animation:shake .3s ease-in!important}@-webkit-keyframes shake{20%{transform:translate(2px)}40%{transform:translate(-4px)}60%{transform:translate(5px)}80%{transform:translate(3px)}to{transform:translate(0)}}@keyframes shake{20%{transform:translate(2px)}40%{transform:translate(-4px)}60%{transform:translate(5px)}80%{transform:translate(3px)}to{transform:translate(0)}}.obs-animate{opacity:0;transition:opacity .6sLine truncated body{--mian-max-width: 1200px;--posts-card-scale: 70%;--posts-list-scale: 70%;--theme-color: #f04494;--focus-color: var(--theme-color);--focus-shadow-color: rgba(253, 83, 161, .4);--key-color: #333;--main-color: #4e5358;--main-shadow: rgba(116, 116, 116, .08);--muted-color: #777;--muted-2-color: #999;--muted-3-color: #b1b1b1;--muted-4-color: #d2d2d2;--body-bg-color: #f5f6f7;--main-bg-color: #fff;--muted-bg-color: #eee;--muted-bg-2-color: #f6f7f8;--main-border-color: rgba(50, 50, 50, .06);--muted-border-color: rgba(0, 0, 0, .03);--main-radius: 8px;--mini-radius: calc(var(--main-radius) / 1.4);--blur-bg: rgba(255, 255, 255, .8);--float-btn-bg: rgba(200, 200, 200, .4);--header-bg: var(--blur-bg);--header-color: var(--main-color);--footer-bg: var(--main-bg-color);--footer-color: var(--muted-2-color)}.dark-theme{--key-color: #f8fafc;--main-color: #e5eef7;--main-shadow: rgba(24, 24, 24, .1);--muted-color: #b4b6bb;--muted-2-color: #888a8f;--muted-3-color: #636469;--muted-4-color: #43454a;--body-bg-color: #292a2d;--main-bg-color: #323335;--muted-bg-color: #2d2e31;--muted-bg-2-color: #37383a;--main-border-color: rgba(114, 114, 114, .1);--muted-border-color: rgba(184, 184, 184, .02);--blur-bg: rgba(50, 51, 53, .8);--float-btn-bg: rgba(70, 70, 70, .4);--header-bg: var(--blur-bg);--header-color: var(--main-color);--footer-bg: var(--main-bg-color);--footer-color: var(--muted-2-color)}::view-transition-old(root),::view-transition-new(root){animation:none;mix-blend-mode:normal}.dark::view-transition-old(root){z-index:1}.dark::view-transition-new(root){z-index:2147483646}::view-transition-old(root){z-index:2147483646}::view-transition-new(root){z-index:1}@supports not ((-webkit-backdrop-filter: blur(20px)) or (backdrop-filter: blur(20px))){body{--blur-bg: #ffffff}body.dark-theme{--blur-bg: #37383a}}body:has(.modal.in),body.modal-open{--blur-bg: #ffffff}body.dark-theme:has(.modal.in),body.dark-theme.modal-open{--blur-bg: #37383a}body.fps-accelerat{--blur-bg: #ffffff}body.fps-accelerat.dark-theme{--blur-bg: #37383a}body.fps-accelerat *{-webkit-backdrop-filter:unset!important;backdrop-filter:unset!important}a[href],.relates-thumb li a>.post-info,.more-posts a>span{transition:color .2s,background .3s,opacity .3s,box-shadow .3s,transform .3s}.main-shadow{transition:box-shadow .3s}.but:focus,.but:hover,.progress,.navbar-nav>li:before{transition:.3s}.navbar-top .sub-menu,.theme-popover,.hover-show .hover-show-con{opacity:0;transform:translateY(6px);transition:.4s}.navbar-top li:hover>.sub-menu,.hover-show:hover .hover-show-con,.hover-show:hover~.hover-show-con,.hover-show:hover~.sub-menu{opacity:1;transform:translateY(0)}.hover-show{position:relative;cursor:pointer;z-index:10}.hover-show .hover-show-con{position:absolute;visibility:hidden}.hover-show:hover .hover-show-con,.hover-show:hover~.hover-show-con,.hover-show:hover~.sub-menu{visibility:unset}.match-ok,.navbar-top .sub-menu li>a,.ic-close{transition:.3s}.ic-close:hover{transform:rotate(270deg);color:inherit}.social_loginbar a,.item-thumbnail img,.dosc-nav .fa{transition:.2s ease-out}.hover-zoom:hover,.hover-zoom-img:hover img,.social_loginbar .xh-social-item:hover,.social_loginbar .social-login-item:hover{transform:scale(1.05)}.hover-zoom-sm:hover,.hover-zoom-img-sm:hover img,.posts-mini:hover img,.posts-item:hover .item-thumbnail img,.posts-item.mult-thumb .thumb-items>span>img:hover{transform:scale(1.02)}.main-min-height{min-height:calc(100vh - 280px)}.main-shadow{box-shadow:0 0 10px var(--main-shadow)}.key-color{color:var(--key-color)}.focus-color{--this-color: var(--focus-color);color:var(--focus-color)}.muted-color{color:var(--muted-color)}.muted-2-color{color:var(--muted-2-color)}.muted-3-color{color:var(--muted-3-color)}.main-bg{background:var(--main-bg-color)}.placeholder{background:linear-gradient(118deg,#7979791a 25%,#79797933 37%,#7979791a 63%);opacity:1;border-radius:4px;-webkit-animation:placeholder 1.5s ease infinite!important;animation:placeholder 1.5s ease infinite!important;background-size:400% 100%!important}.placeholder.img{height:119px}.placeholder.t1{height:24px;width:80%}.placeholder.k1{height:20px;width:60%}.placeholder.k2{height:20px}.placeholder.k3{height:15px}.placeholder.s1{width:70px;height:15px;display:inline-block}@-webkit-keyframes placeholder{0%{background-position:100% 50%}to{background-position:0 50%}}@keyframes placeholder{0%{background-position:100% 50%}to{background-position:0 50%}}@-webkit-keyframes fade{0%{opacity:0}to{opacity:1}}@keyframes fade{0%{opacity:0}to{opacity:1}}.ani.shake{-webkit-animation:shake .3s ease-in!important;animation:shake .3s ease-in!important}@-webkit-keyframes shake{20%{transform:translate(2px)}40%{transform:translate(-4px)}60%{transform:translate(5px)}80%{transform:translate(3px)}to{transform:translate(0)}}@keyframes shake{20%{transform:translate(2px)}40%{transform:translate(-4px)}60%{transform:translate(5px)}80%{transform:translate(3px)}to{transform:translate(0)}}.obs-animate{opacity:0;transition:opacity .6sLine truncated
+22 -10
View File
@@ -304,12 +304,24 @@
body.uc-modal-open { body.uc-modal-open {
overflow: hidden; overflow: hidden;
} }
.btn-block { /* Scoped to the user-center content shell. These are the credits panel's own
display: block; * button skins, and every element they target renders inside .uc-content (the
width: 100%; * panels and their modals are all emitted through templates/parts/user-center/
} * panel.php, which templates/pages/page-profile.php wraps in .uc-content).
.but.jb-blue, *
.but.b-blue-2 { * Left unscoped they are (0,2,0) rules that load after public.css, so they
* out-rank the legacy .but.radius pill and flatten every .but.jb-blue on the
* page down to 4px. The top-nav publish chip is .but.jb-blue.radius, so on the
* user-center route it lost its 50px pill and no longer matched the homepage.
* Site-wide .but.jb-blue callers outside the panel (login, submit, badge and
* cashier buttons) also picked up the flat #3b82f6 skin instead of the legacy
* gradient.
*
* The .btn-block restatement that used to sit here was byte-identical to the
* one in core/buttons.css, which loads earlier on every page, so it could never
* win and is dropped rather than scoped. */
.uc-content .but.jb-blue,
.uc-content .but.b-blue-2 {
background: #3b82f6; background: #3b82f6;
color: #fff; color: #fff;
border: 0; border: 0;
@@ -317,18 +329,18 @@ body.uc-modal-open {
padding: 0.45em 1em; padding: 0.45em 1em;
cursor: pointer; cursor: pointer;
} }
.but.c-blue-2.hollow, .uc-content .but.c-blue-2.hollow,
.but.c-green.hollow { .uc-content .but.c-green.hollow {
background: transparent; background: transparent;
border: 1px solid currentColor; border: 1px solid currentColor;
border-radius: 4px; border-radius: 4px;
padding: 0.4em 0.9em; padding: 0.4em 0.9em;
cursor: pointer; cursor: pointer;
} }
.but.c-blue-2 { .uc-content .but.c-blue-2 {
color: #3b82f6; color: #3b82f6;
} }
.but.c-green { .uc-content .but.c-green {
color: #16a34a; color: #16a34a;
} }
+2 -1
View File
@@ -265,7 +265,8 @@
.aksrt-account-list--social .binding-item { border: 0; } .aksrt-account-list--social .binding-item { border: 0; }
.aksrt-account-list--social .aksrt-account-row { display: grid; grid-template-columns: 28px minmax(0, 1fr) auto auto; text-decoration: none; } .aksrt-account-list--social .aksrt-account-row { display: grid; grid-template-columns: 28px minmax(0, 1fr) auto auto; text-decoration: none; }
.aksrt-account-list--social .binding-item__icon { display: grid; place-items: center; width: 24px; height: 24px; color: var(--color-ink, #333); } .aksrt-account-list--social .binding-item__icon { display: grid; place-items: center; width: 24px; height: 24px; color: var(--color-ink, #333); }
.aksrt-account-list--social .binding-item__icon i { font-size: 22px; line-height: 1; } .aksrt-account-list--social .binding-item__icon i,
.aksrt-account-list--social .binding-item__icon svg { font-size: 22px; line-height: 1; }
.aksrt-account-list--social .binding-status { display: inline-flex; align-items: center; gap: 4px; color: var(--color-success, #16a34a); font-size: 11px; white-space: nowrap; } .aksrt-account-list--social .binding-status { display: inline-flex; align-items: center; gap: 4px; color: var(--color-success, #16a34a); font-size: 11px; white-space: nowrap; }
.aksrt-account-list--social .binding-status--idle { color: var(--muted-2-color, var(--color-ink-4, #999)); } .aksrt-account-list--social .binding-status--idle { color: var(--muted-2-color, var(--color-ink-4, #999)); }
.aksrt-account-list--social .binding-unbind-btn { color: var(--this-color, #e4a11b); } .aksrt-account-list--social .binding-unbind-btn { color: var(--this-color, #e4a11b); }
+3
View File
@@ -295,6 +295,9 @@ if (class_exists('\AKSRT\Extensions\MeiliSearch')) {
if (class_exists('\AKSRT\Extensions\SearchPush')) { if (class_exists('\AKSRT\Extensions\SearchPush')) {
\AKSRT\Extensions\SearchPush::get_instance()->init(); \AKSRT\Extensions\SearchPush::get_instance()->init();
} }
if (class_exists('\AKSRT\Extensions\SearchPushMetaBox')) {
\AKSRT\Extensions\SearchPushMetaBox::get_instance()->init();
}
if (class_exists('\AKSRT\Core\SEOHead')) { if (class_exists('\AKSRT\Core\SEOHead')) {
\AKSRT\Core\SEOHead::get_instance()->init(); \AKSRT\Core\SEOHead::get_instance()->init();
} }
+30 -107
View File
@@ -37,102 +37,33 @@ if (!function_exists('aksrt_get_new_add_actions')) :
} }
endif; endif;
if (!function_exists('aksrt_normalize_header_cta_label')) :
/**
* Normalize CTA label HTML: map FA5 classes and keep a single leading icon.
*/
function aksrt_normalize_header_cta_label(string $label, string $fallback_icon = 'fa fas fa-pen'): string {
$label = trim($label);
if ($label === '') {
return '<i class="' . esc_attr($fallback_icon) . '" aria-hidden="true"></i> ' . esc_html__('发布', 'aksrt');
}
if (stripos($label, '<i') === false) {
// No space between icon and label — spacing via CSS margin (legacy .but > .fa).
$label = '<i class="' . esc_attr($fallback_icon) . '" aria-hidden="true"></i><span class="navbar-cta-text">' . esc_html(wp_strip_all_tags($label)) . '</span>';
} else {
$label = preg_replace(
'/(<\/i>)\s*([^<].*)$/u',
'$1<span class="navbar-cta-text">$2</span>',
$label
) ?? $label;
}
return $label;
}
endif;
if (!function_exists('aksrt_render_new_add_btns')) : if (!function_exists('aksrt_render_new_add_btns')) :
/** /**
* Multi-type publish button (legacy newadd-btns). * Publish chip (legacy newadd-btns, single action).
*
* Mirrors the single-button branch of the reference zib_get_new_add_btns():
* the paint classes ride on the anchor itself and the label is emitted
* verbatim. Only the post action exists, so there is no dropdown branch.
* *
* @param string[] $types * @param string[] $types
*/ */
function aksrt_render_new_add_btns(array $types, string $class = '', string $label = ''): string { function aksrt_render_new_add_btns(array $types, string $class = '', string $label = ''): string {
if (class_exists('\AKSRT\Settings\Settings') && \AKSRT\Settings\Settings::close_sign()) {
return '';
}
$actions = aksrt_get_new_add_actions($types); $actions = aksrt_get_new_add_actions($types);
if (!$actions) { if (!$actions) {
// Last resort: keep a write-post entry so the top-nav chip is never blank. return '';
$fallback = function_exists('aksrt_get_new_post_url') ? (string) aksrt_get_new_post_url() : '';
if ($fallback === '') {
$fallback = admin_url('post-new.php');
}
$actions = [[
'type' => 'post',
'url' => $fallback,
'label' => __('写文章', 'aksrt'),
'icon' => 'fa fas fa-file-alt',
]];
} }
$a = $actions[0];
if ($label === '') { if ($label === '') {
$label = __('发布', 'aksrt'); $label = __('发布', 'aksrt');
} }
$label_html = aksrt_normalize_header_cta_label((string) $label, 'fa fas fa-pen');
// Split paint classes vs wrapper tokens. return '<a rel="nofollow" class="newadd-btns ' . esc_attr($class) . ' btn-newadd" href="' . esc_url($a['url']) . '">'
$class_tokens = preg_split('/\s+/', trim($class), -1, PREG_SPLIT_NO_EMPTY) ?: []; . $label . '</a>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Saved through sanitize_type 'html' (wp_kses_post).
$cta_tokens = [];
$wrap_tokens = ['newadd-btns'];
foreach ($class_tokens as $token) {
if ($token === 'newadd-btns') {
continue;
}
if ($token === 'btn-primary' || $token === 'but' || $token === 'navbar-cta' || $token === 'radius' || $token === 'main-radius' || $token === 'mini-radius' || str_starts_with($token, 'jb-') || str_starts_with($token, 'b-')) {
$cta_tokens[] = $token;
continue;
}
$wrap_tokens[] = $token;
}
if (!$cta_tokens) {
$cta_tokens[] = 'jb-blue';
}
if (!in_array('but', $cta_tokens, true)) {
$cta_tokens[] = 'but';
}
$cta_class = trim(implode(' ', array_unique($cta_tokens)));
if (count($actions) === 1) {
$a = $actions[0];
return '<a rel="nofollow" class="' . esc_attr(trim(implode(' ', $wrap_tokens) . ' ' . $cta_class . ' navbar-cta')) . '" href="' . esc_url($a['url']) . '">'
. $label_html
. '</a>';
}
// legacy: span.newadd-btns.hover-show > toggle chip + .hover-show-con.drop-newadd
// Paint classes (jb-*, but, radius) MUST live on the toggle — wrapper is layout-only
// (background forced transparent). Bare navbar-but has no .navbar-but-cluster shell.
// data-aksrt-newadd: JS may portal menu to body so chip width cannot track menu.
$html = '<span class="' . esc_attr(trim(implode(' ', $wrap_tokens) . ' hover-show')) . '" data-aksrt-newadd="1">';
$html .= '<span class="newadd-btns__toggle navbar-cta ' . esc_attr($cta_class) . '">' . $label_html
. '<i class="fa fas fa-caret-down" aria-hidden="true"></i></span>';
$html .= '<div class="hover-show-con dropdown-menu drop-newadd" role="menu">';
foreach ($actions as $a) {
$html .= '<a rel="nofollow" class="btn-newadd newadd-btns__item" role="menuitem" href="' . esc_url($a['url']) . '">';
$html .= '<i class="' . esc_attr($a['icon']) . '" aria-hidden="true"></i> '
. '<text>' . esc_html($a['label']) . '</text>';
$html .= '</a>';
}
$html .= '</div></span>';
return $html;
} }
endif; endif;
@@ -142,26 +73,19 @@ if (!function_exists('aksrt_render_header_menu_buttons')) :
*/ */
function aksrt_render_header_menu_buttons(): string { function aksrt_render_header_menu_buttons(): string {
$settings = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::read_domain('page_display', 'header-nav') : []; $settings = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::read_domain('page_display', 'header-nav') : [];
$cfg = [ $enabled = !isset($settings['nav_newposts']) || !empty($settings['nav_newposts']);
'enabled' => !isset($settings['nav_newposts']) || !empty($settings['nav_newposts']), $text = trim((string) ($settings['nav_newposts_text'] ?? ''));
'text' => trim((string) ($settings['nav_newposts_text'] ?? __('发布', 'aksrt'))), $pill = !isset($settings['nav_newposts_radius']) || !empty($settings['nav_newposts_radius']);
'radius_pill' => !isset($settings['nav_newposts_radius']) || !empty($settings['nav_newposts_radius']), $skin = sanitize_html_class((string) (($settings['nav_newposts_class'] ?? '') ?: 'jb-blue'));
'class' => sanitize_html_class((string) (($settings['nav_newposts_class'] ?? '') ?: 'jb-blue')),
];
$html = ''; $html = '';
if (!empty($cfg['enabled'])) { if ($enabled) {
// legacy default: but nowave jb-blue (+ radius). // legacy default: but nowave jb-blue (+ radius).
$class = 'but nowave navbar-newposts'; $class = 'but nowave ' . $skin;
$skin = trim((string) ($cfg['class'] ?? ''));
if ($skin === '') {
$skin = 'jb-blue';
}
$class .= ' ' . $skin;
$pill = !empty($cfg['radius_pill']);
if ($pill && !str_contains($class, 'radius')) { if ($pill && !str_contains($class, 'radius')) {
$class .= ' radius'; $class .= ' radius';
} }
$label = (string) ($cfg['text'] ?? __('发布', 'aksrt')); $label = $text !== '' ? $text : __('发布', 'aksrt');
$btn = aksrt_render_new_add_btns(['post'], $class, $label); $btn = aksrt_render_new_add_btns(['post'], $class, $label);
if ($btn !== '') { if ($btn !== '') {
$html .= $btn; $html .= $btn;
@@ -173,7 +97,8 @@ if (!function_exists('aksrt_render_header_menu_buttons')) :
$show_vip = false; $show_vip = false;
if (class_exists('\AKSRT\Settings\Settings')) { if (class_exists('\AKSRT\Settings\Settings')) {
$vs = \AKSRT\Settings\Settings::get_payment_vip_settings(); $vs = \AKSRT\Settings\Settings::get_payment_vip_settings();
$show_vip = !empty($vs['enabled']) && !empty($vs['nav_pay_vip']); $show_vip = !empty($vs['enabled']) && !empty($vs['nav_pay_vip'])
&& !\AKSRT\Settings\Settings::close_sign();
} }
if ($show_vip) { if ($show_vip) {
$is_vip = false; $is_vip = false;
@@ -188,17 +113,15 @@ if (!function_exists('aksrt_render_header_menu_buttons')) :
if (method_exists($svc, 'render_vip_card')) { if (method_exists($svc, 'render_vip_card')) {
$cards = $svc->render_vip_card(1, $user_id) . $svc->render_vip_card(2, $user_id); $cards = $svc->render_vip_card(1, $user_id) . $svc->render_vip_card(2, $user_id);
if ($cards !== '') { if ($cards !== '') {
$hover = '<div class="hover-show-con sub-menu sub-vip-card">' . $cards . '</div>'; $hover = '<div class="sub-menu hover-show-con sub-vip-card">' . $cards . '</div>';
} }
} }
} }
// legacy: span.hover-show wraps button + .hover-show-con.sub-vip-card // legacy: span.hover-show wraps button + .sub-menu.sub-vip-card
$html .= '<span class="hover-show inline-block navbar-vip-hover">'; $html .= '<span class="hover-show inline-block">';
$html .= '<a class="but jb-red radius payvip-icon ml10 navbar-cta ' . esc_attr($cls) . '" href="javascript:;"' $html .= '<a class="' . esc_attr($cls) . ' but jb-red radius payvip-icon ml10" href="javascript:;">'
. ($user_id > 0 ? ' vip-level="1"' : '') . '>'; . '<svg class="em12 mr3" aria-hidden="true"><use xlink:href="#icon-vip_1"></use></svg>'
$html .= '<svg class="icon em12 mr3" aria-hidden="true"><use xlink:href="#icon-vip_1"></use></svg>' . esc_html__('开通会员', 'aksrt') . '</a>' . $hover . '</span>';
. '<span class="navbar-cta-text">' . esc_html__('开通会员', 'aksrt') . '</span>';
$html .= '</a>' . $hover . '</span>';
} }
} }
+7 -7
View File
@@ -56,11 +56,11 @@ if (!function_exists('aksrt_header_user_checkin_btn')) :
// 已签到:与参考主题一致,入口按钮变为签到详情弹窗触发链接(可点击)。 // 已签到:与参考主题一致,入口按钮变为签到详情弹窗触发链接(可点击)。
if ($signed) { if ($signed) {
if ($class !== null) { if ($class !== null) {
// 用户卡片签到书签:保留书签类与 is-done 视觉,仅将行为切换为打开详情弹窗。 // 用户卡片签到书签:与参考主题一致,已签到态仅切换为详情弹窗触发链接,书签类名与文本不变。
return aksrt_get_user_checkin_details_link( return aksrt_get_user_checkin_details_link(
$class . ' is-done', $class,
'<i class="fa far fa-calendar-check ml3 mr6" aria-hidden="true"></i>' '<i class="fa far fa-calendar-check ml3 mr6" aria-hidden="true"></i>'
. '<span>' . esc_html__('已签到', 'aksrt') . '</span>' . esc_html__('已签到', 'aksrt')
); );
} }
return aksrt_get_user_checkin_details_link( return aksrt_get_user_checkin_details_link(
@@ -230,11 +230,11 @@ if (!function_exists('aksrt_header_payvip_bar')) :
return ''; return '';
} }
// 几何对齐参考主题 CTA(通栏、radius4)——不是顶导 navbar-cta chips。 // 1:1 对齐参考主题 zib_get_header_payvip_icon():通栏、radius4、vip_1 SVG 图标、裸文案。
return '<div class="mt10 em09" style="padding:2px;">' return '<div class="mt10 em09" style="padding:2px;">'
. '<a class="pay-vip but radius4 payvip-icon btn-block" href="javascript:;" vip-level="1">' . '<a class="pay-vip but radius4 payvip-icon btn-block" href="javascript:;">'
. '<i class="fa fas fa-gem em12 mr6" aria-hidden="true"></i>' . '<svg class="em12 mr6" aria-hidden="true"><use xlink:href="#icon-vip_1"></use></svg>'
. '<span>' . esc_html__('开通会员 尊享会员权益', 'aksrt') . '</span>' . esc_html__('开通会员 尊享会员权益', 'aksrt')
. '</a></div>'; . '</a></div>';
} }
endif; endif;
+24 -9
View File
@@ -42,7 +42,7 @@ endif;
if (!function_exists('aksrt_brand_icon_map')) : if (!function_exists('aksrt_brand_icon_map')) :
/** /**
* Short key → FA5 class string (allowlisted). * Short key → FA5 class string or built-in SVG sprite key (allowlisted).
* *
* @return array<string, string> * @return array<string, string>
*/ */
@@ -52,10 +52,12 @@ function aksrt_brand_icon_map(): array {
return $map; return $map;
} }
// FA5 Free has no bilibili/gitee brands — use solid stand-ins. // FA5 Free ships no Gitee brand glyph and no multi-colour Google mark, so
// those two resolve to the theme's built-in SVG sprite (zibsvg-*) instead of
// a solid stand-in; every other key stays on FA5.
$map = [ $map = [
'github' => 'fa fab fa-github', 'github' => 'fa fab fa-github',
'google' => 'fa fab fa-google', 'google' => 'zibsvg-google-color',
'x' => 'fa fab fa-twitter', 'x' => 'fa fab fa-twitter',
'twitter' => 'fa fab fa-twitter', 'twitter' => 'fa fab fa-twitter',
'email' => 'fa fas fa-envelope', 'email' => 'fa fas fa-envelope',
@@ -73,7 +75,7 @@ function aksrt_brand_icon_map(): array {
'instagram' => 'fa fab fa-instagram', 'instagram' => 'fa fab fa-instagram',
'web' => 'fa fas fa-globe', 'web' => 'fa fas fa-globe',
'globe' => 'fa fas fa-globe', 'globe' => 'fa fas fa-globe',
'gitee' => 'fa fas fa-code-branch', 'gitee' => 'zibsvg-gitee',
'baidu' => 'fa fas fa-paw', 'baidu' => 'fa fas fa-paw',
'alipay' => 'fa fab fa-alipay', 'alipay' => 'fa fab fa-alipay',
]; ];
@@ -127,10 +129,10 @@ endif;
if (!function_exists('aksrt_brand_icon_class')) : if (!function_exists('aksrt_brand_icon_class')) :
/** /**
* Resolve a short key or FA class string to an allowlisted FA5 class. * Resolve a short key, FA class string or sprite key to its icon value.
* *
* @param string $name Short key or full FA class string. * @param string $name Short key, full FA class string or zibsvg-/customsvg- value.
* @return string e.g. 'fa fab fa-github', or '' if unknown. * @return string e.g. 'fa fab fa-github' or 'zibsvg-gitee', or '' if unknown.
*/ */
function aksrt_brand_icon_class(string $name): string { function aksrt_brand_icon_class(string $name): string {
$raw = trim($name); $raw = trim($name);
@@ -138,6 +140,12 @@ function aksrt_brand_icon_class(string $name): string {
return ''; return '';
} }
// Built-in SVG sprite and inline SVG values pass straight through; they are
// not FA class strings and must survive to the markup renderer untouched.
if (preg_match('/^(?:zibsvg|customsvg)-/i', $raw)) {
return $raw;
}
// Pass through already-valid full FA class strings (footer contact_more). // Pass through already-valid full FA class strings (footer contact_more).
if (preg_match('/^fa\s+(?:fas|far|fab)\s+fa-[\w-]+(?:\s+fa-[\w-]+)*$/i', $raw)) { if (preg_match('/^fa\s+(?:fas|far|fab)\s+fa-[\w-]+(?:\s+fa-[\w-]+)*$/i', $raw)) {
// Prefer mapped short key when strip matches our catalog (normalizes weixin etc.). // Prefer mapped short key when strip matches our catalog (normalizes weixin etc.).
@@ -164,14 +172,21 @@ endif;
if (!function_exists('aksrt_brand_icon')) : if (!function_exists('aksrt_brand_icon')) :
/** /**
* Escaped FA <i> markup for a brand/contact icon. * Escaped markup for a brand/contact icon (FA5 <i> or built-in SVG sprite).
* *
* @param string $name Short key or FA class string. * @param string $name Short key, FA class string or zibsvg-/customsvg- value.
* @param array|string $extra_classes Optional extra classes. * @param array|string $extra_classes Optional extra classes.
* @return string Safe HTML or empty string when unknown. * @return string Safe HTML or empty string when unknown.
*/ */
function aksrt_brand_icon(string $name, $extra_classes = []): string { function aksrt_brand_icon(string $name, $extra_classes = []): string {
$class = aksrt_brand_icon_class($name); $class = aksrt_brand_icon_class($name);
// Sprite and inline-SVG values carry their own element, so the shared icon
// renderer owns the markup (it emits <svg class="icon"><use …>).
if (strpos($class, 'zibsvg-') === 0 || strpos($class, 'customsvg-') === 0) {
return aksrt_icon_markup($class, $extra_classes);
}
if ($class === '') { if ($class === '') {
return ''; return '';
} }
+1 -4
View File
@@ -428,10 +428,7 @@ if (!function_exists('aksrt_submit_links_modal')) :
$body .= '<p><a href="' . esc_url($sign_url) . '" class="signin-loader but c-blue padding-lg">' $body .= '<p><a href="' . esc_url($sign_url) . '" class="signin-loader but c-blue padding-lg">'
. '<i class="fa fas fa-fw fa-sign-in-alt mr10" aria-hidden="true"></i>' . esc_html__('登录', 'aksrt') . '</a></p>'; . '<i class="fa fas fa-fw fa-sign-in-alt mr10" aria-hidden="true"></i>' . esc_html__('登录', 'aksrt') . '</a></p>';
if (class_exists('\AKSRT\Auth\OAuth') && method_exists(\AKSRT\Auth\OAuth::get_instance(), 'render_social_buttons')) { if (class_exists('\AKSRT\Auth\OAuth') && method_exists(\AKSRT\Auth\OAuth::get_instance(), 'render_social_buttons')) {
$social = (string) \AKSRT\Auth\OAuth::get_instance()->render_social_buttons(__('使用以下方式快速登录', 'aksrt')); $body .= (string) \AKSRT\Auth\OAuth::get_instance()->render_social_buttons(__('使用以下方式快速登录', 'aksrt'));
if ($social !== '') {
$body .= '<div class="social_loginbar">' . $social . '</div>';
}
} }
$body .= '</div>'; $body .= '</div>';
} else { } else {
+20 -8
View File
@@ -286,11 +286,7 @@ if (!function_exists('aksrt_header_user_box_html')) :
$con = '<div class="text-center">' . $href . '</div>'; $con = '<div class="text-center">' . $href . '</div>';
if (class_exists('\AKSRT\Auth\OAuth') && method_exists(\AKSRT\Auth\OAuth::get_instance(), 'render_social_buttons')) { if (class_exists('\AKSRT\Auth\OAuth') && method_exists(\AKSRT\Auth\OAuth::get_instance(), 'render_social_buttons')) {
$oauth_html = (string) \AKSRT\Auth\OAuth::get_instance()->render_social_buttons(__('快速登录', 'aksrt')); $con .= (string) \AKSRT\Auth\OAuth::get_instance()->render_social_buttons(__('快速登录', 'aksrt'));
if ($oauth_html !== '') {
$con .= '<p class="social-separator separator muted-3-color em09 mt10">' . esc_html__('快速登录', 'aksrt') . '</p>';
$con .= '<div class="social_loginbar">' . $oauth_html . '</div>';
}
} }
} }
@@ -303,6 +299,12 @@ if (!function_exists('aksrt_menu_button')) :
/** /**
* Right cluster: fold, publish/VIP, theme, user, search (legacy). * Right cluster: fold, publish/VIP, theme, user, search (legacy).
* *
* The reference splits the cluster into three floated .navbar-form
* groups (publish/VIP, theme + language, account row). Each group
* keeps its own .navbar-form padding, which is what produces the
* reference inter-cluster spacing, so the groups are emitted
* separately instead of being merged into one flex container.
*
* @param array<string,mixed> $ctx * @param array<string,mixed> $ctx
*/ */
function aksrt_menu_button(array $ctx): void { function aksrt_menu_button(array $ctx): void {
@@ -371,14 +373,24 @@ if (!function_exists('aksrt_menu_button')) :
$account = '<ul class="list-inline splitters relative aksrt-navbar-account' $account = '<ul class="list-inline splitters relative aksrt-navbar-account'
. ($user_id <= 0 && '2' === $layout ? ' navbar-text' : '') . '">' . ($user_id <= 0 && '2' === $layout ? ' navbar-text' : '') . '">'
. $notification_but . $sign_but . $search_but . '</ul>'; . $notification_but . $sign_but . $search_but . '</ul>';
$button = '<div class="aksrt-navbar-actions navbar-form navbar-right navbar-but">'
. $actions . $utility_buttons . $account . '</div>'; // Reference cluster split: publish/VIP, theme + language, account row.
$button = '';
if ($actions !== '') {
$button = '<div class="navbar-form navbar-right navbar-but">' . $actions . '</div>';
}
if ($utility_buttons !== '') {
$button .= '<div class="navbar-form navbar-right flex ac">' . $utility_buttons . '</div>';
}
$right_but = '<div class="navbar-form navbar-right'
. ($user_id <= 0 && '2' === $layout ? ' navbar-text' : '') . '">'
. $account . '</div>';
$fold = '<div class="navbar-form navbar-right hide show-nav-but" style="margin-right:-10px;">' $fold = '<div class="navbar-form navbar-right hide show-nav-but" style="margin-right:-10px;">'
. '<a data-toggle-class data-target=".nav.navbar-nav" href="javascript:;" class="aksrt-nav-icon" aria-label="' . '<a data-toggle-class data-target=".nav.navbar-nav" href="javascript:;" class="aksrt-nav-icon" aria-label="'
. esc_attr__('展开菜单', 'aksrt') . '"><svg class="icon" viewBox="0 0 1024 1024" aria-hidden="true"><use xlink:href="#icon-menu_2"></use></svg></a></div>'; . esc_attr__('展开菜单', 'aksrt') . '"><svg class="icon" viewBox="0 0 1024 1024" aria-hidden="true"><use xlink:href="#icon-menu_2"></use></svg></a></div>';
$html = $fold . $button; $html = $fold . $button . $right_but;
echo $html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo $html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
} }
-6
View File
@@ -79,12 +79,6 @@ class GlobalSchema {
['key' => 'ai_seo_keywords_prompt', 'label' => '关键词提示词', 'type' => 'textarea', 'default' => '', 'dependsOn' => ['key' => 'seo_ai_enabled', 'value' => true], 'help' => '留空使用内置默认。'], ['key' => 'ai_seo_keywords_prompt', 'label' => '关键词提示词', 'type' => 'textarea', 'default' => '', 'dependsOn' => ['key' => 'seo_ai_enabled', 'value' => true], 'help' => '留空使用内置默认。'],
['key' => 'ai_seo_description_prompt', 'label' => '描述提示词', 'type' => 'textarea', 'default' => '', 'dependsOn' => ['key' => 'seo_ai_enabled', 'value' => true], 'help' => '留空使用内置默认。'], ['key' => 'ai_seo_description_prompt', 'label' => '描述提示词', 'type' => 'textarea', 'default' => '', 'dependsOn' => ['key' => 'seo_ai_enabled', 'value' => true], 'help' => '留空使用内置默认。'],
]], ]],
['key' => 'baidu', 'title' => '百度SEO', 'fields' => [
['key' => 'seo_baidu_push', 'label' => '百度链接自动提交', 'type' => 'switch', 'default' => false],
['key' => 'seo_baidu_normal', 'label' => '百度普通收录', 'type' => 'switch', 'default' => false],
['key' => 'seo_baidu_fast', 'label' => '百度快速收录', 'type' => 'switch', 'default' => false],
['key' => 'seo_baidu_token', 'label' => '百度准入密钥Token', 'type' => 'input', 'default' => ''],
]],
['key' => 'image', 'title' => '图片SEO', 'fields' => [ ['key' => 'image', 'title' => '图片SEO', 'fields' => [
['key' => 'seo_image_optimize', 'label' => '搜索引擎图片展示优化', 'type' => 'switch', 'default' => true], ['key' => 'seo_image_optimize', 'label' => '搜索引擎图片展示优化', 'type' => 'switch', 'default' => true],
['key' => 'seo_image_default', 'label' => '默认图像', 'type' => 'media', 'default' => ''], ['key' => 'seo_image_default', 'label' => '默认图像', 'type' => 'media', 'default' => ''],
+3 -8
View File
@@ -25,16 +25,11 @@ class SchemaExtensionsPush {
['key' => 'baidu_push_on_publish', 'label' => '发布时主动推送', 'type' => 'switch', 'default' => true], ['key' => 'baidu_push_on_publish', 'label' => '发布时主动推送', 'type' => 'switch', 'default' => true],
['key' => 'baidu_push_on_update', 'label' => '更新时推送', 'type' => 'switch', 'default' => false], ['key' => 'baidu_push_on_update', 'label' => '更新时推送', 'type' => 'switch', 'default' => false],
]], ]],
['key' => 'xzh', 'title' => '熊掌号 / JSON-LD', 'fields' => [ ['key' => 'channel', 'title' => '收录通道', 'fields' => [
['key' => 'xzh_on', 'label' => '启用熊掌号', 'type' => 'switch', 'default' => false], ['key' => 'baidu_push_normal', 'label' => '普通收录', 'type' => 'switch', 'default' => true],
['key' => 'xzh_appid', 'label' => '百度 AppID', 'type' => 'input', 'default' => ''], ['key' => 'baidu_push_fast', 'label' => '快速收录', 'type' => 'switch', 'default' => false],
['key' => 'xzh_render_tail', 'label' => '文章底部显示熊掌号尾部', 'type' => 'switch', 'default' => true],
['key' => 'xzh_jsonld_single', 'label' => '文章页 JSON-LD', 'type' => 'switch', 'default' => true],
['key' => 'xzh_jsonld_page', 'label' => '页面 JSON-LD', 'type' => 'switch', 'default' => false],
['key' => 'xzh_jsonld_img', 'label' => '不添加图片', 'type' => 'switch', 'default' => false],
]], ]],
['key' => 'advanced', 'title' => '高级设置', 'fields' => [ ['key' => 'advanced', 'title' => '高级设置', 'fields' => [
['key' => 'baidu_push_batch_size', 'label' => '批量推送条数', 'type' => 'number', 'default' => 50, 'min' => 1, 'max' => 500],
['key' => 'baidu_push_log_enabled', 'label' => '记录推送日志', 'type' => 'switch', 'default' => true], ['key' => 'baidu_push_log_enabled', 'label' => '记录推送日志', 'type' => 'switch', 'default' => true],
]], ]],
], ],
+1 -1
View File
@@ -53,7 +53,7 @@ class SchemaPageDisplay {
self::field('nav_newposts', 'Add发布按钮', 'switcher', true, ['description' => '顶部导航显示文章投稿按钮。']), self::field('nav_newposts', 'Add发布按钮', 'switcher', true, ['description' => '顶部导航显示文章投稿按钮。']),
self::field('nav_newposts_radius', '按钮圆角', 'switcher', true, ['dependsOn' => self::enabled('nav_newposts')]), self::field('nav_newposts_radius', '按钮圆角', 'switcher', true, ['dependsOn' => self::enabled('nav_newposts')]),
self::field('nav_newposts_class', '按钮颜色', 'palette', 'jb-blue', ['dependsOn' => self::enabled('nav_newposts'), 'options' => self::aksrt_palette()]), self::field('nav_newposts_class', '按钮颜色', 'palette', 'jb-blue', ['dependsOn' => self::enabled('nav_newposts'), 'options' => self::aksrt_palette()]),
self::field('nav_newposts_text', '按钮文案', 'textarea', '<i class="fa fas fa-fw fas fa-pencil-alt"></i>发布', ['dependsOn' => self::enabled('nav_newposts'), 'sanitize_type' => 'html', 'rows' => 1]), self::field('nav_newposts_text', '按钮文案', 'textarea', '<i class="fa fas fa-fw fa-pencil-alt"></i>发布', ['dependsOn' => self::enabled('nav_newposts'), 'sanitize_type' => 'html', 'rows' => 1]),
]); ]);
} }
-30
View File
@@ -611,36 +611,6 @@ class SchemaUsers {
]], ]],
], ],
], ],
'wechat' => [
'key' => 'wechat',
'title' => '微信通知',
'version' => '1.0.0',
'option_key' => 'aksrt_settings',
'groups' => [
['key' => 'account', 'title' => '公众号配置', 'fields' => [
['key' => 'wechat_notify_enabled', 'label' => '启用微信通知', 'type' => 'switch', 'default' => false],
['key' => 'wechat_app_id', 'label' => 'AppID', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'wechat_notify_enabled', 'value' => true]],
['key' => 'wechat_app_secret', 'label' => 'AppSecret', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'wechat_notify_enabled', 'value' => true]],
]],
['key' => 'verify', 'title' => '服务器验证', 'fields' => [
['key' => 'wechat_token', 'label' => 'Token', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'wechat_notify_enabled', 'value' => true]],
['key' => 'wechat_encoding_aes_key', 'label' => 'EncodingAESKey', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'wechat_notify_enabled', 'value' => true]],
]],
['key' => 'templates', 'title' => '模板消息', 'fields' => [
['key' => 'wechat_template_comment', 'label' => '评论通知模板', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'wechat_notify_enabled', 'value' => true]],
['key' => 'wechat_template_reply', 'label' => '回复通知模板', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'wechat_notify_enabled', 'value' => true]],
['key' => 'wechat_template_system', 'label' => '系统通知模板', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'wechat_notify_enabled', 'value' => true]],
['key' => 'wechat_template_order', 'label' => '订单通知模板', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'wechat_notify_enabled', 'value' => true]],
]],
['key' => 'triggers', 'title' => '通知触发器', 'fields' => [
['key' => 'wechat_notify_comment', 'label' => '新评论通知', 'type' => 'switch', 'default' => true, 'dependsOn' => ['key' => 'wechat_notify_enabled', 'value' => true]],
['key' => 'wechat_notify_reply', 'label' => '评论回复通知', 'type' => 'switch', 'default' => true, 'dependsOn' => ['key' => 'wechat_notify_enabled', 'value' => true]],
['key' => 'wechat_notify_system', 'label' => '系统通知', 'type' => 'switch', 'default' => true, 'dependsOn' => ['key' => 'wechat_notify_enabled', 'value' => true]],
['key' => 'wechat_notify_order', 'label' => '订单通知', 'type' => 'switch', 'default' => true, 'dependsOn' => ['key' => 'wechat_notify_enabled', 'value' => true]],
['key' => 'wechat_bind_entry', 'label' => '显示绑定入口', 'type' => 'switch', 'default' => true, 'dependsOn' => ['key' => 'wechat_notify_enabled', 'value' => true]],
]],
],
],
], ],
]; ];
} }
+48
View File
@@ -325,6 +325,54 @@ class UserController extends ControllerBase {
} }
/**
* GET /wechat/status — 微信模板消息配置状态。
*
* 状态形状与 WeChatNotify::get_status() 对齐(该方法的既定用途即 REST 与 AJAX 复用)。
*/
public function get_wechat_status(\WP_REST_Request $request): \WP_REST_Response {
if (!class_exists('\AKSRT\Payment\WeChatNotify')) {
return $this->error(__('微信通知组件未加载', 'aksrt'), 500);
}
return $this->success(['data' => \AKSRT\Payment\WeChatNotify::get_instance()->get_status()]);
}
/**
* POST /wechat/settings — 更新微信模板消息配置。
*
* 前端在密钥留空且已配置时发送掩码 ***,此时保留已存密钥;
* 未提供的字段沿用当前值。键白名单由 Integration::update_wechat_settings() 收敛。
*/
public function update_wechat_settings(\WP_REST_Request $request): \WP_REST_Response {
$params = $this->get_json_params($request);
$current = \AKSRT\Settings\Settings::get_wechat_settings();
$enabled = array_key_exists('enabled', $params) ? !empty($params['enabled']) : !empty($current['enabled']);
$app_id = array_key_exists('app_id', $params)
? sanitize_text_field((string) $params['app_id'])
: (string) ($current['app_id'] ?? '');
$template_id = array_key_exists('template_id', $params)
? sanitize_text_field((string) $params['template_id'])
: (string) ($current['template_id'] ?? '');
// 掩码 *** 表示「不修改密钥」;仅在显式传入其他值时才覆盖。
if (array_key_exists('app_secret', $params) && (string) $params['app_secret'] !== '***') {
$app_secret = sanitize_text_field((string) $params['app_secret']);
} else {
$app_secret = (string) ($current['app_secret'] ?? '');
}
\AKSRT\Settings\Settings::update_wechat_settings([
'enabled' => $enabled,
'app_id' => $app_id,
'app_secret' => $app_secret,
'template_id' => $template_id,
]);
return $this->success(['message' => __('微信通知配置已保存', 'aksrt')]);
}
public function rest_handle_credit_settings(\WP_REST_Request $request): \WP_REST_Response { public function rest_handle_credit_settings(\WP_REST_Request $request): \WP_REST_Response {
if ($request->get_method() === 'GET') { if ($request->get_method() === 'GET') {
$config = \AKSRT\Settings\CreditsRuntime::get_credit_settings(); $config = \AKSRT\Settings\CreditsRuntime::get_credit_settings();
+41 -19
View File
@@ -219,35 +219,57 @@ final class OAuth {
'name' => $provider, 'name' => $provider,
'label' => OAuthProviderCatalog::label($provider), 'label' => OAuthProviderCatalog::label($provider),
'icon' => function_exists('aksrt_brand_icon') ? aksrt_brand_icon(OAuthProviderCatalog::icon_key($provider)) : '', 'icon' => function_exists('aksrt_brand_icon') ? aksrt_brand_icon(OAuthProviderCatalog::icon_key($provider)) : '',
'url' => esc_url($url), // Raw, matching get_frontend_bindings_data(): every surface escapes
// once on output. esc_url() is idempotent on its own output, so
// escaping here too was redundant rather than broken.
'url' => $url,
]; ];
} }
return $result; return $result;
} }
/**
* Reference-parity social login block (legacy zib_social_login()).
*
* Emits the muted separator plus a .social_loginbar of .social-login-item
* buttons, one per enabled provider. Returns '' when nothing is enabled.
*
* @param string $title Separator label; an empty title drops the separator.
*/
public function render_social_buttons(string $title = ''): string { public function render_social_buttons(string $title = ''): string {
$items = $this->get_auth_providers_data(); $items = $this->get_auth_providers_data();
if ($items === []) { if ($items === []) {
return ''; return '';
} }
ob_start();
?> $large = (bool) \AKSRT\Settings\Settings::get_value('oauth_button_lg', false);
<div class="social-login-divider"> $html = $title !== ''
<span><?php echo esc_html($title) ?: esc_html__('or', 'aksrt'); ?></span> ? '<p class="social-separator separator muted-3-color em09 mt10">' . esc_html($title) . '</p>'
</div> : '';
<div class="oauth-row"> $html .= '<div class="social_loginbar">';
<?php foreach ($items as $item) : ?> foreach ($items as $item) {
<a href="<?php echo esc_url((string) $item['url']); ?>" $name = sanitize_html_class((string) ($item['name'] ?? ''));
class="oauth-btn oauth-btn--<?php echo esc_attr((string) $item['name']); ?>" $type = str_starts_with($name, 'clogin_') ? substr($name, 7) : $name;
data-provider="<?php echo esc_attr((string) $item['name']); ?>" $label = trim((string) ($item['label'] ?? ''));
aria-label="<?php echo esc_attr((string) $item['label']); ?>" $url = (string) ($item['url'] ?? '');
title="<?php echo esc_attr((string) $item['label']); ?>"> if ($name === '' || $type === '' || $label === '' || $url === '') {
<?php echo $item['icon']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> continue;
</a> }
<?php endforeach; ?> $class = 'social-login-item ' . $type . ($large ? ' button-lg' : ' toggle-radius');
</div> if ($name === 'weixingzh') {
<?php // Reference parity: the WeChat entry opens the QR panel instead of
return (string) ob_get_clean(); // navigating, so auth.js routes it to the modal's oauth tab.
$class .= ' qrcode-signin';
}
$html .= '<a rel="nofollow" href="' . esc_url($url) . '" class="' . esc_attr($class) . '"'
. ' data-provider="' . esc_attr($name) . '"'
. ' aria-label="' . esc_attr(sprintf(__('%s登录', 'aksrt'), $label)) . '"'
. ' title="' . esc_attr(sprintf(__('%s登录', 'aksrt'), $label)) . '">'
. $item['icon'] // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
. ($large ? '<span>' . esc_html(sprintf(__('%s登录', 'aksrt'), $label)) . '</span>' : '')
. '</a>';
}
return $html . '</div>';
} }
public function get_frontend_bindings_data(int $user_id): array { public function get_frontend_bindings_data(int $user_id): array {
+279 -63
View File
@@ -9,6 +9,14 @@ defined('ABSPATH') || exit;
class SearchPush { class SearchPush {
/** 推送结果 meta 键(编辑页面板与去重逻辑共用)。 */
public const META_KEY = '_aksrt_baidu_push_back';
/** 「重新提交」nonce 与复选框字段名(与 SearchPushMetaBox 共用)。 */
public const NONCE_ACTION = 'aksrt_push_meta_save';
public const NONCE_FIELD = 'aksrt_push_meta_nonce';
public const RESUBMIT_FIELD = 'aksrt_push_resubmit';
private static $instance = null; private static $instance = null;
public static function get_instance(): self { public static function get_instance(): self {
@@ -17,16 +25,20 @@ class SearchPush {
} }
/** /**
* 绑定运行时钩子:发布/更新主动推送 + 前台自动推送 JS。 * 绑定运行时钩子:发布/更新主动推送 + 分类法保存推送 + 前台自动推送 JS。
*/ */
public function init(): void { public function init(): void {
add_action('transition_post_status', [$this, 'on_transition_post_status'], 10, 3); add_action('transition_post_status', [$this, 'on_transition_post_status'], 10, 3);
add_action('saved_term', [$this, 'on_saved_term'], 10, 3);
add_action('wp_footer', [$this, 'output_auto_push_script'], 99); add_action('wp_footer', [$this, 'output_auto_push_script'], 99);
} }
/** /**
* 文章发布/更新时主动推送链接到百度。 * 文章发布/更新时主动推送链接到百度。
* *
* 与参考主题一致:推送结果写入 meta,成功后不再重复提交;
* 编辑页勾选「重新提交」可清空记录并强制再次推送。
*
* @param string $new_status 新状态。 * @param string $new_status 新状态。
* @param string $old_status 旧状态。 * @param string $old_status 旧状态。
* @param \WP_Post $post 文章对象。 * @param \WP_Post $post 文章对象。
@@ -42,27 +54,230 @@ class SearchPush {
return; return;
} }
// 仅推送公开可索引的内容类型。 // 仅推送公开可索引的内容类型。
$allowed = function_exists('aksrt_searchable_post_types') if (!in_array($post->post_type, self::pushable_post_types(), true)) {
? aksrt_searchable_post_types()
: ['post'];
if (!in_array($post->post_type, $allowed, true)) {
return; return;
} }
$config = $this->get_config(); $config = $this->get_config();
if (!$this->should_push($config)) {
return;
}
if ($this->resubmit_requested()) {
self::clear_push_record($post->ID);
} else {
$is_new = $old_status !== 'publish';
if ($is_new && empty($config['baidu_push_on_publish'])) {
return;
}
if (!$is_new && empty($config['baidu_push_on_update'])) {
return;
}
// 已成功提交过的链接不再重复推送。
if ($this->record_is_complete(self::get_push_record($post->ID), $config)) {
return;
}
}
$this->push_and_record((string) get_permalink($post), $post->ID, false, $config);
}
/**
* 分类法术语保存时主动推送链接到百度。
*
* 术语无发布/更新状态,沿用参考主题的 saved_term 触发,
* 与文章推送共用 meta 记录、去重与「重新提交」。
*
* @param int $term_id 术语 ID。
* @param int $tt_id 术语分类法 ID。
* @param string $taxonomy 分类法名称。
*/
public function on_saved_term(int $term_id, int $tt_id, string $taxonomy): void {
if (!in_array($taxonomy, self::pushable_taxonomies(), true)) {
return;
}
$config = $this->get_config();
if (!$this->should_push($config)) {
return;
}
if ($this->resubmit_requested()) {
self::clear_push_record($term_id, true);
} elseif ($this->record_is_complete(self::get_push_record($term_id, true), $config)) {
return;
}
$url = get_term_link($term_id, $taxonomy);
if (is_wp_error($url) || $url === '') {
return;
}
$this->push_and_record((string) $url, $term_id, true, $config);
}
/**
* 可推送的文章类型(与站内搜索 / MeiliSearch 索引共用同一策略)。
*
* @return string[]
*/
public static function pushable_post_types(): array {
$types = function_exists('aksrt_searchable_post_types') ? aksrt_searchable_post_types() : ['post'];
return array_values(array_unique(array_filter((array) $types)));
}
/**
* 可推送的分类法:可推送文章类型上的公开分类法(排除文章形式等)。
*
* @return string[]
*/
public static function pushable_taxonomies(): array {
$taxonomies = get_object_taxonomies(self::pushable_post_types(), 'names');
$taxonomies = array_diff((array) $taxonomies, ['post_format', 'nav_menu']);
/**
* 过滤百度推送覆盖的分类法。
*
* @param string[] $taxonomies 分类法 slug 列表。
*/
return array_values(array_unique(apply_filters('aksrt_baidu_push_taxonomies', $taxonomies)));
}
/**
* 推送功能是否已启用且配置完整(决定编辑页面板是否展示)。
*/
public function is_ready(): bool {
return $this->should_push($this->get_config());
}
/**
* 读取文章或术语的推送记录。
*
* @param int $object_id 文章 / 术语 ID。
* @param bool $is_term 是否为术语。
* @return array 记录为空时返回 []。
*/
public static function get_push_record(int $object_id, bool $is_term = false): array {
$record = $is_term ? get_term_meta($object_id, self::META_KEY, true) : get_post_meta($object_id, self::META_KEY, true);
return is_array($record) ? $record : [];
}
/**
* 写入推送记录。
*
* @param int $object_id 文章 / 术语 ID。
* @param array $record 推送记录。
* @param bool $is_term 是否为术语。
*/
public static function save_push_record(int $object_id, array $record, bool $is_term = false): void {
if ($is_term) {
update_term_meta($object_id, self::META_KEY, $record);
} else {
update_post_meta($object_id, self::META_KEY, $record);
}
}
/**
* 清除推送记录(「重新提交」时调用)。
*
* @param int $object_id 文章 / 术语 ID。
* @param bool $is_term 是否为术语。
*/
public static function clear_push_record(int $object_id, bool $is_term = false): void {
if ($is_term) {
delete_term_meta($object_id, self::META_KEY);
} else {
delete_post_meta($object_id, self::META_KEY);
}
}
/**
* 推送是否可用:总开关开启、Token 已配置且至少启用一个收录通道。
*
* @param array $config 推送配置。
*/
private function should_push(array $config): bool {
if (empty($config['baidu_push_enabled'])) { if (empty($config['baidu_push_enabled'])) {
return false;
}
if ((string) ($config['baidu_push_token'] ?? '') === '') {
return false;
}
return !empty($config['baidu_push_normal']) || !empty($config['baidu_push_fast']);
}
/**
* 推送 URL 并把结果写入 meta(成功记录用于去重)。
*
* @param string $url 待推送链接。
* @param int $object_id 文章 / 术语 ID。
* @param bool $is_term 是否为术语。
* @param array $config 推送配置。
*/
private function push_and_record(string $url, int $object_id, bool $is_term, array $config): void {
if ($url === '') {
return; return;
} }
$is_new = $old_status !== 'publish'; $outcome = $this->push_url($url);
if ($is_new && empty($config['baidu_push_on_publish'])) { self::save_push_record($object_id, $this->build_record($outcome, $config), $is_term);
return; }
/**
* 组装推送记录:通道结果 + 更新时间(结构与参考主题 meta 对齐)。
*
* @param array $outcome push_url() 返回值。
* @param array $config 推送配置。
*/
private function build_record(array $outcome, array $config): array {
$record = ['update_time' => current_time('Y-m-d H:i:s')];
$result = isset($outcome['result']) && is_array($outcome['result']) ? $outcome['result'] : [];
if (!empty($config['baidu_push_normal'])) {
$channel = $result['normal'] ?? null;
$record['normal_push'] = is_array($channel) && !empty($channel['success']);
$record['normal_result'] = $channel;
} }
if (!$is_new && empty($config['baidu_push_on_update'])) { if (!empty($config['baidu_push_fast'])) {
return; $channel = $result['daily'] ?? null;
$record['daily_push'] = is_array($channel) && !empty($channel['success']);
$record['daily_result'] = $channel;
} }
$this->push_url((string) get_permalink($post)); return $record;
}
/**
* 推送记录是否已覆盖全部启用通道(去重判断)。
*
* @param array $record 推送记录。
* @param array $config 推送配置。
*/
private function record_is_complete(array $record, array $config): bool {
if (!empty($config['baidu_push_normal']) && empty($record['normal_push'])) {
return false;
}
if (!empty($config['baidu_push_fast']) && empty($record['daily_push'])) {
return false;
}
return true;
}
/**
* 编辑页是否勾选了「重新提交」(需通过 metabox nonce 校验)。
*/
private function resubmit_requested(): bool {
if (empty($_POST[self::RESUBMIT_FIELD])) {
return false;
}
$nonce = isset($_POST[self::NONCE_FIELD]) ? sanitize_text_field(wp_unslash($_POST[self::NONCE_FIELD])) : '';
return (bool) wp_verify_nonce($nonce, self::NONCE_ACTION);
} }
/** /**
@@ -96,7 +311,50 @@ class SearchPush {
return ['success' => false, 'message' => __('推送 Token 未配置', 'aksrt'), 'result' => []]; return ['success' => false, 'message' => __('推送 Token 未配置', 'aksrt'), 'result' => []];
} }
$api_url = "http://data.zz.baidu.com/urls?site=" . rawurlencode(home_url('/')) . "&token={$token}"; // 收录通道与百度接口 type 参数对应:普通收录不带参数,快速收录为 daily。
$channels = [];
if (!empty($config['baidu_push_normal'])) {
$channels['normal'] = '';
}
if (!empty($config['baidu_push_fast'])) {
$channels['daily'] = '&type=daily';
}
if (!$channels) {
return ['success' => false, 'message' => __('未启用普通收录或快速收录通道', 'aksrt'), 'result' => []];
}
$success = false;
$results = [];
$messages = [];
foreach ($channels as $channel => $suffix) {
$outcome = $this->submit($url, $token, $suffix, $config);
$results[$channel] = $outcome['result'];
if ($outcome['success']) {
$success = true;
} else {
$messages[] = $outcome['message'];
}
}
if ($success) {
return ['success' => true, 'message' => __('推送成功', 'aksrt'), 'result' => $results];
}
return ['success' => false, 'message' => implode('; ', $messages), 'result' => $results];
}
/**
* 向百度链接提交接口发送单条 URL。
*
* @param string $url 待推送的完整 URL。
* @param string $token 百度准入密钥。
* @param string $suffix 接口附加参数(快速收录为 &type=daily)。
* @param array $config 推送配置。
* @return array{success: bool, message: string, result: array}
*/
private function submit(string $url, string $token, string $suffix, array $config): array {
$api_url = 'http://data.zz.baidu.com/urls?site=' . rawurlencode(home_url('/')) . '&token=' . $token . $suffix;
$response = wp_remote_post($api_url, [ $response = wp_remote_post($api_url, [
'timeout' => 10, 'timeout' => 10,
@@ -105,7 +363,7 @@ class SearchPush {
]); ]);
if (is_wp_error($response)) { if (is_wp_error($response)) {
$this->log('error', $url, $response->get_error_message()); $this->log($config, 'error', $url, $response->get_error_message());
return ['success' => false, 'message' => $response->get_error_message(), 'result' => []]; return ['success' => false, 'message' => $response->get_error_message(), 'result' => []];
} }
@@ -113,59 +371,15 @@ class SearchPush {
$status = wp_remote_retrieve_response_code($response); $status = wp_remote_retrieve_response_code($response);
if ($status === 200 && isset($body['success'])) { if ($status === 200 && isset($body['success'])) {
$this->log('success', $url, wp_json_encode($body)); $this->log($config, 'success', $url, wp_json_encode($body));
return ['success' => true, 'message' => __('推送成功', 'aksrt'), 'result' => $body]; return ['success' => true, 'message' => __('推送成功', 'aksrt'), 'result' => $body];
} }
$err_msg = $body['message'] ?? ($body['error'] ?? sprintf('HTTP %d', $status)); $err_msg = $body['message'] ?? ($body['error'] ?? sprintf('HTTP %d', $status));
$this->log('error', $url, $err_msg); $this->log($config, 'error', $url, $err_msg);
return ['success' => false, 'message' => $err_msg, 'result' => $body ?? []]; return ['success' => false, 'message' => $err_msg, 'result' => $body ?? []];
} }
/**
* 批量推送 URL
*
* @param string[] $urls
* @return array{success: int, failed: int, results: array}
*/
public function push_batch(array $urls): array {
$config = $this->get_config();
$batch_size = max(1, min(500, (int) ($config['baidu_push_batch_size'] ?? 50)));
$success = 0;
$failed = 0;
$results = [];
$chunks = array_chunk($urls, $batch_size);
foreach ($chunks as $chunk) {
$token = (string) ($config['baidu_push_token'] ?? '');
$api_url = "http://data.zz.baidu.com/urls?site=" . rawurlencode(home_url('/')) . "&token={$token}";
$body_str = implode("\n", $chunk);
$response = wp_remote_post($api_url, [
'timeout' => 15,
'headers' => ['Content-Type' => 'text/plain'],
'body' => $body_str,
]);
if (is_wp_error($response)) {
$failed += count($chunk);
$results[] = ['error' => $response->get_error_message()];
continue;
}
$body = json_decode(wp_remote_retrieve_body($response), true);
if (isset($body['success'])) {
$success += (int) ($body['success'] ?? 0);
$failed += (int) ($body['not_same_site'] ?? 0) + (int) ($body['not_valid'] ?? 0);
} else {
$failed += count($chunk);
}
$results[] = $body ?? [];
}
return ['success' => $success, 'failed' => $failed, 'results' => $results];
}
/** /**
* 获取自动推送 JS 代码 * 获取自动推送 JS 代码
*/ */
@@ -175,7 +389,6 @@ class SearchPush {
return ''; return '';
} }
$site_url = home_url('/');
return <<<SCRIPT return <<<SCRIPT
<script> <script>
(function(){ (function(){
@@ -211,13 +424,15 @@ SCRIPT;
'provider' => 'baidu', 'provider' => 'baidu',
'configured' => $enabled && $token !== '', 'configured' => $enabled && $token !== '',
'available' => $enabled && $token !== '', 'available' => $enabled && $token !== '',
'verified' => !empty($config['baidu_push_verified']), // 百度链接提交接口无单独校验握手,Token 配置完成即可推送。
'verified' => false,
'transport' => 'http_api', 'transport' => 'http_api',
'missing_fields' => $missing, 'missing_fields' => $missing,
'auto_push' => !empty($config['baidu_push_auto']), 'auto_push' => !empty($config['baidu_push_auto']),
'normal_push' => !empty($config['baidu_push_normal']),
'fast_push' => !empty($config['baidu_push_fast']),
'on_publish' => !empty($config['baidu_push_on_publish']), 'on_publish' => !empty($config['baidu_push_on_publish']),
'on_update' => !empty($config['baidu_push_on_update']), 'on_update' => !empty($config['baidu_push_on_update']),
'batch_size' => max(1, (int) ($config['baidu_push_batch_size'] ?? 50)),
]; ];
} }
@@ -228,7 +443,8 @@ SCRIPT;
return ['baidu_push_enabled' => false]; return ['baidu_push_enabled' => false];
} }
private function log(string $level, string $url, string $message): void { private function log(array $config, string $level, string $url, string $message): void {
if (empty($config['baidu_push_log_enabled'])) return;
if (!defined('WP_DEBUG') || !WP_DEBUG) return; if (!defined('WP_DEBUG') || !WP_DEBUG) return;
error_log(sprintf('[AKSRT Search Push] [%s] URL: %s — %s', $level, $url, $message)); error_log(sprintf('[AKSRT Search Push] [%s] URL: %s — %s', $level, $url, $message));
} }
+190
View File
@@ -0,0 +1,190 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Extensions;
defined('ABSPATH') || exit;
/**
* 百度推送编辑页面板(对齐参考主题「百度资源提交」Metabox)。
*
* 文章:侧栏 Metabox;分类法:编辑 / 新增表单行。
* 展示推送结果与更新时间,并提供「重新提交」勾选——
* 勾选后保存会清空去重记录并强制再次推送。
*/
class SearchPushMetaBox {
private static $instance = null;
public static function get_instance(): self {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
public function init(): void {
add_action('add_meta_boxes', [$this, 'register_post_meta_box']);
// 分类法可能由插件延后注册,面板在 admin_init 统一挂载。
add_action('admin_init', [$this, 'register_term_panels']);
}
/**
* 为可推送文章类型注册侧栏 Metabox(推送功能就绪后才展示)。
*/
public function register_post_meta_box(): void {
if (!$this->is_ready()) {
return;
}
foreach (SearchPush::pushable_post_types() as $post_type) {
add_meta_box(
'aksrt_baidu_push',
__('百度资源提交', 'aksrt'),
[$this, 'render_post_panel'],
$post_type,
'side',
'high'
);
}
}
/**
* 为可推送分类法注册编辑 / 新增表单面板。
*/
public function register_term_panels(): void {
if (!$this->is_ready()) {
return;
}
foreach (SearchPush::pushable_taxonomies() as $taxonomy) {
add_action("{$taxonomy}_edit_form_fields", [$this, 'render_term_panel'], 20);
add_action("{$taxonomy}_add_form_fields", [$this, 'render_term_panel'], 20);
}
}
/**
* 渲染文章 Metabox。
*
* @param mixed $post WP_Post 或 null。
*/
public function render_post_panel($post): void {
if (!$post instanceof \WP_Post) {
return;
}
$this->render_panel((int) $post->ID, false);
}
/**
* 渲染分类法表单行(新增表单无术语记录,仅展示提示)。
*
* @param mixed $term WP_Term、分类名字符串或 null。
*/
public function render_term_panel($term = null): void {
$term_id = 0;
if ($term instanceof \WP_Term) {
$term_id = (int) $term->term_id;
} elseif (isset($_GET['tag_ID'])) {
$term_id = (int) $_GET['tag_ID'];
}
// 新增表单无表格结构,仅编辑表单以表单行包裹。
$this->render_panel($term_id, true, $term_id > 0);
}
private function is_ready(): bool {
return class_exists(SearchPush::class) && SearchPush::get_instance()->is_ready();
}
/**
* 渲染推送结果面板。
*
* @param int $object_id 文章 / 术语 ID(0 表示新增表单)。
* @param bool $is_term 是否为术语。
* @param bool $as_row 是否以分类法表单行包裹。
*/
private function render_panel(int $object_id, bool $is_term, bool $as_row = false): void {
ob_start();
wp_nonce_field(SearchPush::NONCE_ACTION, SearchPush::NONCE_FIELD);
$record = $object_id > 0 ? SearchPush::get_push_record($object_id, $is_term) : [];
$lines = $this->result_lines($record);
?>
<div class="aksrt-push-meta-box">
<?php if ($lines) : ?>
<p class="aksrt-push-meta-box__label"><strong><?php esc_html_e('提交结果:', 'aksrt'); ?></strong></p>
<ul class="aksrt-push-meta-box__results">
<?php foreach ($lines as $line) : ?>
<li><strong><?php echo esc_html($line[0]); ?></strong> <span><?php echo esc_html($line[1]); ?></span></li>
<?php endforeach; ?>
</ul>
<?php else : ?>
<p class="description"><?php esc_html_e('发布、更新后刷新页面后可查看提交结果', 'aksrt'); ?></p>
<?php endif; ?>
<?php if (!empty($record['update_time'])) : ?>
<p class="aksrt-push-meta-box__resubmit">
<label>
<input type="checkbox" name="<?php echo esc_attr(SearchPush::RESUBMIT_FIELD); ?>" value="1" />
<?php esc_html_e('重新提交', 'aksrt'); ?>
</label>
<span class="description"><?php esc_html_e('勾选后保存将清空提交记录并再次推送', 'aksrt'); ?></span>
</p>
<?php endif; ?>
</div>
<?php
$body = (string) ob_get_clean();
if ($as_row) {
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- 面板 HTML 已在输出前逐项转义。
echo '<tr class="form-field"><th scope="row">' . esc_html(__('百度资源提交', 'aksrt')) . '</th><td>' . $body . '</td></tr>';
return;
}
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- 面板 HTML 已在输出前逐项转义。
echo $body;
}
/**
* 汇总推送结果行(展示项与参考主题对齐)。
*
* @param array $record 推送记录。
* @return array<int,array{0:string,1:string}>
*/
private function result_lines(array $record): array {
$lines = [];
if (!empty($record['normal_push'])) {
$lines[] = [__('普通收录:成功', 'aksrt'), $this->format_result($record['normal_result'] ?? null)];
} elseif (array_key_exists('normal_push', $record)) {
$lines[] = [__('普通收录:失败', 'aksrt'), $this->format_result($record['normal_result'] ?? null)];
}
if (!empty($record['daily_push'])) {
$lines[] = [__('快速收录:成功', 'aksrt'), $this->format_result($record['daily_result'] ?? null)];
} elseif (array_key_exists('daily_push', $record)) {
$lines[] = [__('快速收录:失败', 'aksrt'), $this->format_result($record['daily_result'] ?? null)];
}
if (!empty($record['update_time'])) {
$lines[] = [__('更新时间:', 'aksrt'), (string) $record['update_time']];
}
return $lines;
}
/**
* 格式化接口返回结果。
*
* @param mixed $result 百度接口返回的解码数据。
*/
private function format_result($result): string {
if (is_array($result) || is_object($result)) {
return (string) wp_json_encode($result);
}
return (string) $result;
}
}
-4
View File
@@ -24,10 +24,6 @@ trait DisplayPageGroups {
'ai_seo_title_prompt' => trim((string) ($s['ai_seo_title_prompt'] ?? '')), 'ai_seo_title_prompt' => trim((string) ($s['ai_seo_title_prompt'] ?? '')),
'ai_seo_keywords_prompt' => trim((string) ($s['ai_seo_keywords_prompt'] ?? '')), 'ai_seo_keywords_prompt' => trim((string) ($s['ai_seo_keywords_prompt'] ?? '')),
'ai_seo_description_prompt' => trim((string) ($s['ai_seo_description_prompt'] ?? '')), 'ai_seo_description_prompt' => trim((string) ($s['ai_seo_description_prompt'] ?? '')),
'seo_baidu_push' => !empty($s['seo_baidu_push']) || !empty($s['baidu_push_enabled']),
'seo_baidu_normal' => !empty($s['seo_baidu_normal']),
'seo_baidu_fast' => !empty($s['seo_baidu_fast']),
'seo_baidu_token' => (string) (($s['seo_baidu_token'] ?? '') ?: ($s['baidu_push_token'] ?? '')),
'seo_image_optimize' => !isset($s['seo_image_optimize']) || !empty($s['seo_image_optimize']), 'seo_image_optimize' => !isset($s['seo_image_optimize']) || !empty($s['seo_image_optimize']),
'seo_image_default' => esc_url_raw((string) ($s['seo_image_default'] ?? '')), 'seo_image_default' => esc_url_raw((string) ($s['seo_image_default'] ?? '')),
'seo_image_always_default'=> !empty($s['seo_image_always_default']), 'seo_image_always_default'=> !empty($s['seo_image_always_default']),
+4 -18
View File
@@ -128,29 +128,15 @@ class ExtensionRuntime {
public static function get_search_push_settings(): array { public static function get_search_push_settings(): array {
$settings = Settings::get_all(); $settings = Settings::get_all();
$enabled = !empty($settings['baidu_push_enabled']) || !empty($settings['seo_baidu_push']);
$token = (string) ($settings['baidu_push_token'] ?? '');
if ($token === '' && !empty($settings['seo_baidu_token'])) {
$token = (string) $settings['seo_baidu_token'];
}
return [ return [
'baidu_push_enabled' => $enabled, 'baidu_push_enabled' => !empty($settings['baidu_push_enabled']),
'baidu_push_token' => $token, 'baidu_push_token' => (string) ($settings['baidu_push_token'] ?? ''),
'baidu_push_auto' => !isset($settings['baidu_push_auto']) || !empty($settings['baidu_push_auto']), 'baidu_push_auto' => !isset($settings['baidu_push_auto']) || !empty($settings['baidu_push_auto']),
'baidu_push_normal' => !isset($settings['baidu_push_normal']) || !empty($settings['baidu_push_normal']),
'baidu_push_fast' => !empty($settings['baidu_push_fast']),
'baidu_push_on_publish' => !isset($settings['baidu_push_on_publish']) || !empty($settings['baidu_push_on_publish']), 'baidu_push_on_publish' => !isset($settings['baidu_push_on_publish']) || !empty($settings['baidu_push_on_publish']),
'baidu_push_on_update' => !empty($settings['baidu_push_on_update']), 'baidu_push_on_update' => !empty($settings['baidu_push_on_update']),
'baidu_push_batch_size' => max(1, min(500, (int) ($settings['baidu_push_batch_size'] ?? 50))),
'baidu_push_log_enabled' => !isset($settings['baidu_push_log_enabled']) || !empty($settings['baidu_push_log_enabled']), 'baidu_push_log_enabled' => !isset($settings['baidu_push_log_enabled']) || !empty($settings['baidu_push_log_enabled']),
'baidu_push_verified' => !empty($settings['baidu_push_verified']),
'xzh_on' => !empty($settings['xzh_on']),
'xzh_appid' => (string) ($settings['xzh_appid'] ?? ''),
'xzh_render_tail' => !isset($settings['xzh_render_tail']) || !empty($settings['xzh_render_tail']),
'xzh_jsonld_single' => !isset($settings['xzh_jsonld_single']) || !empty($settings['xzh_jsonld_single']),
'xzh_jsonld_page' => !empty($settings['xzh_jsonld_page']),
'xzh_jsonld_img' => !empty($settings['xzh_jsonld_img']),
'seo_baidu_normal' => !empty($settings['seo_baidu_normal']),
'seo_baidu_fast' => !empty($settings['seo_baidu_fast']),
]; ];
} }
+1 -1
View File
@@ -125,7 +125,7 @@ final class UserRenderer {
if ($name === '' || $style_name === '' || $label === '' || $url === '') { if ($name === '' || $style_name === '' || $label === '' || $url === '') {
continue; continue;
} }
$html .= '<a rel="nofollow" href="' . esc_url($url) . '" class="social-login-item ' . esc_attr($style_name) . '" data-provider="' . esc_attr($name) . '" aria-label="' . esc_attr(sprintf(__('%s登录', 'aksrt'), $label)) . '" title="' . esc_attr($label) . '">' . (string) ($item['icon'] ?? '') . '</a>'; $html .= '<a rel="nofollow" href="' . esc_url($url) . '" class="social-login-item ' . esc_attr($style_name) . ' toggle-radius" data-provider="' . esc_attr($name) . '" aria-label="' . esc_attr(sprintf(__('%s登录', 'aksrt'), $label)) . '" title="' . esc_attr(sprintf(__('%s登录', 'aksrt'), $label)) . '">' . (string) ($item['icon'] ?? '') . '</a>';
} }
return $html . '</div>'; return $html . '</div>';
} }
-1
View File
@@ -147,6 +147,5 @@ $oauth_bind_pass = $oauth_intent === 'bind'
<?php foreach ($oauth_providers as $provider) : ?><?php if (($provider['name'] ?? '') === 'weixingzh') continue; ?><?php $provider_label = (string) ($provider['label'] ?? $provider['name'] ?? ''); ?><a class="social-login-item <?php echo esc_attr((string) ($provider['name'] ?? '')); ?>" href="<?php echo esc_url((string) ($provider['url'] ?? '#')); ?>" data-provider="<?php echo esc_attr((string) ($provider['name'] ?? '')); ?>" aria-label="<?php echo esc_attr(sprintf(__('%s登录', 'aksrt'), $provider_label)); ?>" title="<?php echo esc_attr($provider_label); ?>"><?php echo $provider['icon'] ?? ''; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?><?php if ($oauth_large) : ?><span><?php printf(esc_html__('%s登录', 'aksrt'), esc_html($provider_label)); ?></span><?php endif; ?></a><?php endforeach; ?> <?php foreach ($oauth_providers as $provider) : ?><?php if (($provider['name'] ?? '') === 'weixingzh') continue; ?><?php $provider_label = (string) ($provider['label'] ?? $provider['name'] ?? ''); ?><a class="social-login-item <?php echo esc_attr((string) ($provider['name'] ?? '')); ?>" href="<?php echo esc_url((string) ($provider['url'] ?? '#')); ?>" data-provider="<?php echo esc_attr((string) ($provider['name'] ?? '')); ?>" aria-label="<?php echo esc_attr(sprintf(__('%s登录', 'aksrt'), $provider_label)); ?>" title="<?php echo esc_attr($provider_label); ?>"><?php echo $provider['icon'] ?? ''; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?><?php if ($oauth_large) : ?><span><?php printf(esc_html__('%s登录', 'aksrt'), esc_html($provider_label)); ?></span><?php endif; ?></a><?php endforeach; ?>
<?php if ($wechat_qr) : ?><button type="button" class="social-login-item weixingzh" data-auth-tab="oauth" aria-label="<?php esc_attr_e('微信扫码登录', 'aksrt'); ?>" title="<?php esc_attr_e('微信扫码登录', 'aksrt'); ?>"><i class="fa fab fa-weixin" aria-hidden="true"></i><?php if ($oauth_large) : ?><span><?php esc_html_e('微信登录', 'aksrt'); ?></span><?php endif; ?></button><?php endif; ?> <?php if ($wechat_qr) : ?><button type="button" class="social-login-item weixingzh" data-auth-tab="oauth" aria-label="<?php esc_attr_e('微信扫码登录', 'aksrt'); ?>" title="<?php esc_attr_e('微信扫码登录', 'aksrt'); ?>"><i class="fa fab fa-weixin" aria-hidden="true"></i><?php if ($oauth_large) : ?><span><?php esc_html_e('微信登录', 'aksrt'); ?></span><?php endif; ?></button><?php endif; ?>
</div></div> </div></div>
<?php if (!empty($agreement['enabled'])) : ?><div class="sign-agreement sign-agreement-implicit"><?php esc_html_e('使用社交账号登录即表示', 'aksrt'); ?><?php echo $agreement['label_html']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></div><?php endif; ?>
<?php endif; ?> <?php endif; ?>
</div> </div>
+265
View File
@@ -0,0 +1,265 @@
<?php
/**
* Baidu push harness.
*
* Drives the REAL SearchPush / SearchPushMetaBox against WP stubs so the
* reference-parity contract is covered end to end:
* - normal / daily channels hit data.zz.baidu.com (daily appends type=daily);
* - successful pushes are recorded in meta and never re-submitted;
* - failed pushes retry on update when baidu_push_on_update is on;
* - the 「重新提交」checkbox forces a push only with a valid metabox nonce;
* - terms push via saved_term (post_format and other taxonomies excluded);
* - the edit panel renders status lines, update time and the resubmit checkbox.
*
* Usage: php baidu-push-harness.php -> prints "Baidu push harness passed".
*/
namespace {
define('ABSPATH', __DIR__ . '/');
define('WP_DEBUG', false);
final class WP_Error {
public function __construct(private string $code, private string $message) {}
public function get_error_message(): string { return $this->message; }
}
final class WP_Post {
public int $ID = 0;
public string $post_type = 'post';
public string $post_status = 'publish';
}
final class WP_Term {
public int $term_id = 0;
public string $taxonomy = 'category';
}
// ---------- WP function stubs ----------
function add_action(...$args) {}
function apply_filters($tag, $value) { return $value; }
function __($s, $d = null) { return (string) $s; }
function esc_html($s) { return htmlspecialchars((string) $s, ENT_QUOTES); }
function esc_attr($s) { return htmlspecialchars((string) $s, ENT_QUOTES); }
function esc_html_e($s, $d = null) { echo esc_html($s); }
function wp_json_encode($v, $f = 0) { return (string) json_encode($v, $f); }
function wp_nonce_field($action, $name) { echo '<input type="hidden" name="' . $name . '" value="nonce" />'; }
function wp_unslash($s) { return $s; }
function sanitize_text_field($s) { return trim(strip_tags((string) $s)); }
function wp_verify_nonce($nonce, $action) { return $nonce === 'valid-nonce' && $action === 'aksrt_push_meta_save'; }
function is_wp_error($t) { return $t instanceof WP_Error; }
function home_url($path = '/') { return 'https://example.test' . $path; }
function current_time($type = 'mysql') { return '2026-10-13 12:00:00'; }
function get_permalink($post) { return 'https://example.test/?p=' . (int) $post->ID; }
function get_term_link($term, $taxonomy = '') {
$id = is_object($term) ? (int) $term->term_id : (int) $term;
return 'https://example.test/?cat=' . $id;
}
function wp_is_post_revision($id) { return false; }
function wp_is_post_autosave($id) { return false; }
function aksrt_searchable_post_types(): array { return ['post']; }
function get_object_taxonomies($types, $output = 'names') { return ['post_tag', 'category', 'post_format']; }
// Outgoing HTTP: every call is captured so channel/URL/body can be asserted.
$GLOBALS['push_requests'] = [];
function wp_remote_post($url, $args = []) {
$GLOBALS['push_requests'][] = ['url' => (string) $url, 'body' => (string) ($args['body'] ?? '')];
return ['response' => ['code' => 200], 'body' => (string) json_encode(['success' => 1, 'remain' => 99])];
}
function wp_remote_retrieve_body($r) { return is_array($r) ? (string) ($r['body'] ?? '') : ''; }
function wp_remote_retrieve_response_code($r) { return is_array($r) ? (int) ($r['response']['code'] ?? 0) : 0; }
// In-memory meta stores.
$GLOBALS['push_post_meta'] = [];
$GLOBALS['push_term_meta'] = [];
function get_post_meta($id, $key, $single = false) { return $GLOBALS['push_post_meta'][$id][$key] ?? ''; }
function update_post_meta($id, $key, $value) { $GLOBALS['push_post_meta'][$id][$key] = $value; return true; }
function delete_post_meta($id, $key) { unset($GLOBALS['push_post_meta'][$id][$key]); return true; }
function get_term_meta($id, $key, $single = false) { return $GLOBALS['push_term_meta'][$id][$key] ?? ''; }
function update_term_meta($id, $key, $value) { $GLOBALS['push_term_meta'][$id][$key] = $value; return true; }
function delete_term_meta($id, $key) { unset($GLOBALS['push_term_meta'][$id][$key]); return true; }
}
namespace AKSRT\Settings {
class Settings {
public static array $config = [];
public static function get_search_push_settings(): array { return self::$config; }
}
}
namespace {
require_once dirname(__DIR__, 2) . '/src/Extensions/SearchPush.php';
require_once dirname(__DIR__, 2) . '/src/Extensions/SearchPushMetaBox.php';
use AKSRT\Extensions\SearchPush;
use AKSRT\Extensions\SearchPushMetaBox;
$failures = [];
$check = static function (string $label, bool $ok) use (&$failures): void {
if (!$ok) {
$failures[] = $label;
}
};
$reset = static function (array $config): void {
\AKSRT\Settings\Settings::$config = $config;
$GLOBALS['push_requests'] = [];
$GLOBALS['push_post_meta'] = [];
$GLOBALS['push_term_meta'] = [];
$_POST = [];
};
$post = static function (int $id): WP_Post {
$p = new WP_Post();
$p->ID = $id;
return $p;
};
$defaults = [
'baidu_push_enabled' => true,
'baidu_push_token' => 'TOKEN',
'baidu_push_auto' => true,
'baidu_push_on_publish' => true,
'baidu_push_on_update' => false,
'baidu_push_normal' => true,
'baidu_push_fast' => false,
'baidu_push_log_enabled' => true,
];
$push = SearchPush::get_instance();
$api = 'http://data.zz.baidu.com/urls?site=' . rawurlencode('https://example.test/') . '&token=TOKEN';
// 1) First publish pushes once through the normal-collection endpoint.
$reset($defaults);
$push->on_transition_post_status('publish', 'draft', $post(11));
$check('首次发布推送一次', count($GLOBALS['push_requests']) === 1);
$check('普通收录请求地址正确', ($GLOBALS['push_requests'][0]['url'] ?? '') === $api);
$check('普通收录请求体为文章链接', ($GLOBALS['push_requests'][0]['body'] ?? '') === 'https://example.test/?p=11');
$record = SearchPush::get_push_record(11);
$check('推送记录写入 meta', !empty($record['normal_push']));
$check('推送记录带更新时间', !empty($record['update_time']));
$check('推送记录不含快速收录', !array_key_exists('daily_push', $record));
// 2) A completed push is never re-submitted on save.
$push->on_transition_post_status('publish', 'publish', $post(11));
$check('成功后更新不重复推送', count($GLOBALS['push_requests']) === 1);
// 3) Failed push retries on update when the update switch is on.
$reset(array_merge($defaults, ['baidu_push_on_update' => true]));
SearchPush::save_push_record(12, ['normal_push' => false, 'normal_result' => null, 'update_time' => '2026-10-13 09:00:00']);
$push->on_transition_post_status('publish', 'publish', $post(12));
$check('失败后更新重试一次', count($GLOBALS['push_requests']) === 1);
$check('重试后记录为成功', !empty(SearchPush::get_push_record(12)['normal_push']));
$push->on_transition_post_status('publish', 'publish', $post(12));
$check('重试成功后不重复推送', count($GLOBALS['push_requests']) === 1);
// 4) 「重新提交」with a valid nonce forces a push even after success.
$reset($defaults);
$push->on_transition_post_status('publish', 'draft', $post(13));
$_POST[SearchPush::RESUBMIT_FIELD] = '1';
$_POST[SearchPush::NONCE_FIELD] = 'valid-nonce';
$push->on_transition_post_status('publish', 'publish', $post(13));
$check('重新提交强制再次推送', count($GLOBALS['push_requests']) === 2);
// 5) A forged nonce is ignored, dedup still applies.
$reset($defaults);
$push->on_transition_post_status('publish', 'draft', $post(14));
$_POST[SearchPush::RESUBMIT_FIELD] = '1';
$_POST[SearchPush::NONCE_FIELD] = 'forged-nonce';
$push->on_transition_post_status('publish', 'publish', $post(14));
$check('无效 nonce 的重新提交被忽略', count($GLOBALS['push_requests']) === 1);
// 6) Fast collection appends type=daily and records both channels.
$reset(array_merge($defaults, ['baidu_push_fast' => true]));
$push->on_transition_post_status('publish', 'draft', $post(15));
$check('双通道推送两次', count($GLOBALS['push_requests']) === 2);
$check('快速收录走 type=daily', str_contains($GLOBALS['push_requests'][1]['url'] ?? '', '&type=daily'));
$record15 = SearchPush::get_push_record(15);
$check('双通道记录完整', !empty($record15['normal_push']) && !empty($record15['daily_push']));
$push->on_transition_post_status('publish', 'publish', $post(15));
$check('双通道成功后不重复推送', count($GLOBALS['push_requests']) === 2);
// 7) No channel enabled: nothing pushed, nothing recorded.
$reset(array_merge($defaults, ['baidu_push_normal' => false]));
$push->on_transition_post_status('publish', 'draft', $post(16));
$check('通道全关不推送', count($GLOBALS['push_requests']) === 0);
$check('通道全关不写记录', SearchPush::get_push_record(16) === []);
// 8) Feature off / token missing: nothing pushed, nothing recorded.
$reset(array_merge($defaults, ['baidu_push_enabled' => false]));
$push->on_transition_post_status('publish', 'draft', $post(17));
$check('未启用不推送', count($GLOBALS['push_requests']) === 0);
$reset(array_merge($defaults, ['baidu_push_token' => '']));
$push->on_transition_post_status('publish', 'draft', $post(18));
$check('缺少 Token 不推送', count($GLOBALS['push_requests']) === 0);
$check('缺少 Token 不写记录', SearchPush::get_push_record(18) === []);
// 9) Terms push via saved_term with the same dedup / resubmit rules.
$reset($defaults);
$push->on_saved_term(21, 21, 'category');
$check('术语推送一次', count($GLOBALS['push_requests']) === 1);
$check('术语推送链接正确', ($GLOBALS['push_requests'][0]['body'] ?? '') === 'https://example.test/?cat=21');
$check('术语记录写入', !empty(SearchPush::get_push_record(21, true)['normal_push']));
$push->on_saved_term(21, 21, 'category');
$check('术语成功后不重复推送', count($GLOBALS['push_requests']) === 1);
$push->on_saved_term(22, 22, 'post_format');
$check('post_format 术语不推送', count($GLOBALS['push_requests']) === 1);
$_POST[SearchPush::RESUBMIT_FIELD] = '1';
$_POST[SearchPush::NONCE_FIELD] = 'valid-nonce';
$push->on_saved_term(21, 21, 'category');
$check('术语重新提交强制推送', count($GLOBALS['push_requests']) === 2);
// 10) Shared type / taxonomy policy.
$check('可推送文章类型', SearchPush::pushable_post_types() === ['post']);
$check('可推送分类法排除 post_format', SearchPush::pushable_taxonomies() === ['post_tag', 'category']);
// 11) Edit panel rendering.
$reset($defaults);
SearchPush::save_push_record(31, [
'normal_push' => true,
'normal_result' => ['success' => 1],
'daily_push' => false,
'daily_result' => null,
'update_time' => '2026-10-13 09:00:00',
]);
SearchPush::save_push_record(31, [
'normal_push' => true,
'normal_result' => ['success' => 1],
'daily_push' => false,
'daily_result' => null,
'update_time' => '2026-10-13 09:00:00',
], true);
$box = SearchPushMetaBox::get_instance();
ob_start();
$box->render_post_panel($post(31));
$html = (string) ob_get_clean();
$check('面板含普通收录成功', str_contains($html, '普通收录:成功'));
$check('面板含快速收录失败', str_contains($html, '快速收录:失败'));
$check('面板含更新时间', str_contains($html, '2026-10-13 09:00:00'));
$check('面板含重新提交勾选', str_contains($html, 'name="aksrt_push_resubmit"'));
$check('面板含 nonce 字段', str_contains($html, 'name="aksrt_push_meta_nonce"'));
$check('面板转义接口 JSON', str_contains($html, '{&quot;success&quot;:1}'));
ob_start();
$box->render_term_panel(null);
$addHtml = (string) ob_get_clean();
$check('新增术语表单仅提示', str_contains($addHtml, '发布、更新后刷新页面后可查看提交结果') && !str_contains($addHtml, '<tr'));
$term = new WP_Term();
$term->term_id = 31;
ob_start();
$box->render_term_panel($term);
$termHtml = (string) ob_get_clean();
$check('术语编辑表单为表单行', str_contains($termHtml, '<tr class="form-field">'));
$check('术语编辑表单含重新提交', str_contains($termHtml, 'name="aksrt_push_resubmit"'));
if ($failures) {
echo "Baidu push harness FAILED\n";
foreach ($failures as $failure) {
echo ' - ' . $failure . "\n";
}
exit(1);
}
echo "Baidu push harness passed\n";
}
@@ -0,0 +1,203 @@
'use strict';
/**
* 「百度推送」后台设置页契约(对齐参考主题 zibll):
* 1) PHP schema(SchemaExtensionsPush.php)与 JS schema(push.js)字段键、顺序、默认值一致;
* 2) 参考主题字段全集存在:自动推送 JS、普通收录、快速收录、准入 Token、发布/更新触发;
* 3) 双路/迁移字段(SEO 页 seo_baidu_*、熊掌号 xzh_*、批量推送 batch_*)已彻底移除,无双路兼容;
* 4) 运行时(SearchPush + ExtensionRuntime)只读取 canonical 键,快速收录走 type=daily;
* 5) 分类法 saved_term 推送、meta 去重、「重新提交」nonce 与编辑页面板均已接入,
* 并由 PHP harness 驱动真实逻辑验证。
*/
const assert = require('node:assert/strict');
const { execFileSync } = require('node:child_process');
const fs = require('node:fs');
const path = require('node:path');
const test = require('node:test');
const root = path.resolve(__dirname, '..', '..');
const php = fs.readFileSync(path.join(root, 'src/Admin/Schema/SchemaExtensionsPush.php'), 'utf8');
const js = fs.readFileSync(path.join(root, 'assets/admin/src/schema/sections/extensions/push.js'), 'utf8');
const runtime = fs.readFileSync(path.join(root, 'src/Extensions/SearchPush.php'), 'utf8');
const extensionRuntime = fs.readFileSync(path.join(root, 'src/Settings/ExtensionRuntime.php'), 'utf8');
const globalPhp = fs.readFileSync(path.join(root, 'src/Admin/Schema/GlobalSchema.php'), 'utf8');
const seoJs = fs.readFileSync(path.join(root, 'assets/admin/src/schema/sections/global/seo.js'), 'utf8');
const displayGroups = fs.readFileSync(path.join(root, 'src/Settings/DisplayPageGroups.php'), 'utf8');
const metaBox = fs.readFileSync(path.join(root, 'src/Extensions/SearchPushMetaBox.php'), 'utf8');
const functions = fs.readFileSync(path.join(root, 'functions.php'), 'utf8');
// 参考主题(zibll)百度推送字段全集,AKSRT 单一 canonical 配置面(扩展&增强 → 百度推送)。
const REFERENCE_KEYS = [
'baidu_push_enabled',
'baidu_push_token',
'baidu_push_auto',
'baidu_push_on_publish',
'baidu_push_on_update',
'baidu_push_normal',
'baidu_push_fast',
'baidu_push_log_enabled',
];
// 双路 / 迁移 / 死代码字段:SEO 页 seo_baidu_*、熊掌号 xzh_*、批量推送与校验占位。
const DUAL_KEYS = [
'seo_baidu_push',
'seo_baidu_normal',
'seo_baidu_fast',
'seo_baidu_token',
'xzh_on',
'xzh_appid',
'xzh_render_tail',
'xzh_jsonld_single',
'xzh_jsonld_page',
'xzh_jsonld_img',
'baidu_push_batch_size',
'baidu_push_verified',
'push_batch',
'batch_size',
];
function phpFieldKeys(src) {
const found = [];
const re = /'key'\s*=>\s*'([a-z0-9_]+)',\s*'label'/g;
let m;
while ((m = re.exec(src)) !== null) found.push(m[1]);
return found;
}
function jsFieldKeys(src) {
const found = [];
const re = /key:\s*'([a-z0-9_]+)',\s*label:/g;
let m;
while ((m = re.exec(src)) !== null) found.push(m[1]);
return found;
}
test('PHP push schema 字段键与顺序对齐参考主题', () => {
assert.deepEqual(phpFieldKeys(php), REFERENCE_KEYS);
});
test('JS push schema 字段键与顺序对齐参考主题', () => {
assert.deepEqual(jsFieldKeys(js), REFERENCE_KEYS);
});
test('PHP schema 默认值对齐', () => {
assert.match(php, /'baidu_push_enabled',\s*'label'\s*=>\s*'启用百度推送',\s*'type'\s*=>\s*'switch',\s*'default'\s*=>\s*false/);
assert.match(php, /'baidu_push_token',\s*'label'\s*=>\s*'推送 Token',\s*'type'\s*=>\s*'input',\s*'default'\s*=>\s*''/);
assert.match(php, /'baidu_push_auto',\s*'label'\s*=>\s*'自动推送',\s*'type'\s*=>\s*'switch',\s*'default'\s*=>\s*true/);
assert.match(php, /'baidu_push_on_publish',\s*'label'\s*=>\s*'发布时主动推送',\s*'type'\s*=>\s*'switch',\s*'default'\s*=>\s*true/);
assert.match(php, /'baidu_push_on_update',\s*'label'\s*=>\s*'更新时推送',\s*'type'\s*=>\s*'switch',\s*'default'\s*=>\s*false/);
assert.match(php, /'baidu_push_normal',\s*'label'\s*=>\s*'普通收录',\s*'type'\s*=>\s*'switch',\s*'default'\s*=>\s*true/);
assert.match(php, /'baidu_push_fast',\s*'label'\s*=>\s*'快速收录',\s*'type'\s*=>\s*'switch',\s*'default'\s*=>\s*false/);
assert.match(php, /'baidu_push_log_enabled',\s*'label'\s*=>\s*'记录推送日志',\s*'type'\s*=>\s*'switch',\s*'default'\s*=>\s*true/);
});
test('JS schema 默认值对齐', () => {
assert.match(js, /baidu_push_enabled:\s*false/);
assert.match(js, /baidu_push_token:\s*''/);
assert.match(js, /baidu_push_auto:\s*true/);
assert.match(js, /baidu_push_on_publish:\s*true/);
assert.match(js, /baidu_push_on_update:\s*false/);
assert.match(js, /baidu_push_normal:\s*true/);
assert.match(js, /baidu_push_fast:\s*false/);
assert.match(js, /baidu_push_log_enabled:\s*true/);
});
test('双路/迁移/死代码字段在 schema、运行时与展示层均已移除(无双路)', () => {
for (const legacy of DUAL_KEYS) {
assert.ok(!php.includes(legacy), `SchemaExtensionsPush 残留: ${legacy}`);
assert.ok(!js.includes(legacy), `push.js 残留: ${legacy}`);
assert.ok(!runtime.includes(legacy), `SearchPush 残留: ${legacy}`);
assert.ok(!extensionRuntime.includes(legacy), `ExtensionRuntime 残留: ${legacy}`);
assert.ok(!globalPhp.includes(legacy), `GlobalSchema 残留: ${legacy}`);
assert.ok(!seoJs.includes(legacy), `seo.js 残留: ${legacy}`);
assert.ok(!displayGroups.includes(legacy), `DisplayPageGroups 残留: ${legacy}`);
assert.ok(!metaBox.includes(legacy), `SearchPushMetaBox 残留: ${legacy}`);
assert.ok(!functions.includes(legacy), `functions.php 残留: ${legacy}`);
}
assert.ok(!globalPhp.includes("'baidu'"), 'GlobalSchema SEO 分组残留百度重复配置面');
assert.ok(!seoJs.includes("key: 'baidu'"), 'seo.js 残留百度重复配置面');
});
test('普通/快速收录通道接入百度链接提交接口', () => {
assert.match(runtime, /data\.zz\.baidu\.com\/urls\?site=/);
assert.match(runtime, /'baidu_push_normal'/);
assert.match(runtime, /'baidu_push_fast'/);
assert.match(runtime, /&type=daily/);
assert.match(extensionRuntime, /'baidu_push_normal'\s*=>/);
assert.match(extensionRuntime, /'baidu_push_fast'\s*=>/);
});
test('前台自动推送 JS 与参考主题一致', () => {
assert.match(runtime, /zz\.bdstatic\.com\/linksubmit\/push\.js/);
assert.match(runtime, /push\.zhanzhang\.baidu\.com\/push\.js/);
assert.match(runtime, /add_action\('wp_footer'/);
});
test('发布/更新触发沿用 transition_post_status 且仅限可检索内容类型', () => {
assert.match(runtime, /transition_post_status/);
assert.match(runtime, /aksrt_searchable_post_types/);
assert.match(runtime, /wp_is_post_revision/);
assert.match(runtime, /wp_is_post_autosave/);
});
test('推送日志开关接入运行时', () => {
assert.match(runtime, /baidu_push_log_enabled/);
assert.match(extensionRuntime, /'baidu_push_log_enabled'/);
});
test('服务状态契约保留扩展总览消费的键', () => {
for (const key of ['enabled', 'provider', 'configured', 'available', 'verified', 'transport', 'missing_fields']) {
assert.ok(runtime.includes(`'${key}'`), `get_status 缺少 ${key}`);
}
});
test('分类法推送接入 saved_term 且排除文章形式', () => {
assert.match(runtime, /add_action\('saved_term'/);
assert.match(runtime, /public static function pushable_taxonomies/);
assert.match(runtime, /array_diff\(\(array\) \$taxonomies, \['post_format', 'nav_menu'\]\)/);
assert.match(runtime, /aksrt_baidu_push_taxonomies/);
assert.match(runtime, /get_term_link\(\$term_id, \$taxonomy\)/);
});
test('推送结果写入 meta 并按记录去重', () => {
assert.match(runtime, /META_KEY\s*=\s*'_aksrt_baidu_push_back'/);
assert.match(runtime, /private function record_is_complete/);
assert.match(runtime, /private function build_record/);
assert.match(runtime, /'normal_push'/);
assert.match(runtime, /'daily_push'/);
assert.match(runtime, /'update_time'/);
assert.match(runtime, /update_term_meta\(/);
assert.match(runtime, /delete_term_meta\(/);
});
test('「重新提交」勾选需通过 metabox nonce 校验', () => {
assert.match(runtime, /NONCE_ACTION\s*=\s*'aksrt_push_meta_save'/);
assert.match(runtime, /NONCE_FIELD\s*=\s*'aksrt_push_meta_nonce'/);
assert.match(runtime, /RESUBMIT_FIELD\s*=\s*'aksrt_push_resubmit'/);
assert.match(runtime, /wp_verify_nonce\(\$nonce, self::NONCE_ACTION\)/);
});
test('编辑页面板与参考主题「百度资源提交」Metabox 对齐', () => {
assert.match(metaBox, /add_meta_box\(\s*'aksrt_baidu_push',\s*__\('百度资源提交', 'aksrt'\)/);
assert.match(metaBox, /'side',\s*'high'/);
assert.match(metaBox, /\{\$taxonomy\}_edit_form_fields/);
assert.match(metaBox, /\{\$taxonomy\}_add_form_fields/);
assert.match(metaBox, /普通收录:成功/);
assert.match(metaBox, /快速收录:失败/);
assert.match(metaBox, /更新时间:/);
assert.match(metaBox, /重新提交/);
assert.match(metaBox, /发布、更新后刷新页面后可查看提交结果/);
assert.match(metaBox, /wp_nonce_field\(SearchPush::NONCE_ACTION, SearchPush::NONCE_FIELD\)/);
assert.match(metaBox, /esc_html\(\$line\[1\]\)/, '接口结果需转义后输出');
});
test('functions.php 接线推送面板', () => {
assert.match(functions, /SearchPushMetaBox::get_instance\(\)->init\(\)/);
});
test('百度推送行为 harness 通过', () => {
const harness = path.join(root, 'tests', 'admin', 'baidu-push-harness.php');
const output = execFileSync('php', [harness], { encoding: 'utf8' });
assert.match(output, /Baidu push harness passed/);
});
+151
View File
@@ -0,0 +1,151 @@
<?php
/**
* WeChat template-message settings harness.
*
* Drives the REAL UserController REST callbacks (get_wechat_status /
* update_wechat_settings) against WP stubs so the previously-broken routes are
* covered end to end:
* - GET /wechat/status returns the WeChatNotify status shape (masked secret);
* - POST /wechat/settings persists through Integration's key whitelist;
* - the *** mask keeps the stored secret instead of overwriting it;
* - omitted fields fall back to current values; unknown keys are dropped;
* - inputs are sanitized before storage.
*
* Usage: php wechat-settings-harness.php -> prints "WeChat settings harness passed".
*/
namespace {
define('ABSPATH', __DIR__ . '/');
final class WP_REST_Response {
public $data;
public $status;
public function __construct($data = null, int $status = 200) {
$this->data = $data;
$this->status = $status;
}
}
final class WP_REST_Request {
private array $json;
public function __construct(array $json = []) { $this->json = $json; }
public function get_json_params(): array { return $this->json; }
public function get_method(): string { return 'POST'; }
}
// ---------- WP function stubs ----------
function __($s, $d = null) { return (string) $s; }
function sanitize_text_field($s) { return trim(strip_tags((string) $s)); }
function current_user_can($c) { return true; }
function wp_send_json_success($d) { echo (string) json_encode($d); }
function get_transient($k) { return $GLOBALS['wechat_transient'] ?? false; }
function get_option($k, $default = false) { return $GLOBALS['wechat_options'][$k] ?? $default; }
function update_option($k, $v) { $GLOBALS['wechat_options'][$k] = $v; return true; }
$GLOBALS['wechat_options'] = ['aksrt_wechat_settings' => [
'enabled' => true,
'app_id' => 'wxAPPID',
'app_secret' => 'SECRET123',
'template_id' => 'TPL1',
]];
$GLOBALS['wechat_transient'] = 'ACCESS_TOKEN';
$GLOBALS['wpdb'] = new class {
public $usermeta = 'wp_usermeta';
public function get_var($sql) { return 7; }
};
}
namespace AKSRT\Settings {
// Settings 层仅为薄委托,harness 直接对接真实 Integration。
class Settings {
public static function get_wechat_settings(): array { return Integration::get_wechat_settings(); }
public static function update_wechat_settings(array $settings): void { Integration::update_wechat_settings($settings); }
}
}
namespace {
require_once dirname(__DIR__, 2) . '/src/Admin/ControllerBase.php';
require_once dirname(__DIR__, 2) . '/src/Admin/UserControllerReportOperations.php';
require_once dirname(__DIR__, 2) . '/src/Admin/UserControllerAuthPhoneOperations.php';
require_once dirname(__DIR__, 2) . '/src/Admin/UserControllerRestrictionOperations.php';
require_once dirname(__DIR__, 2) . '/src/Admin/UserControllerFollowOperations.php';
require_once dirname(__DIR__, 2) . '/src/Admin/UserControllerProfileMediaOperations.php';
require_once dirname(__DIR__, 2) . '/src/Admin/UserController.php';
require_once dirname(__DIR__, 2) . '/src/Settings/Integration.php';
require_once dirname(__DIR__, 2) . '/src/Settings/DomainGroups.php';
require_once dirname(__DIR__, 2) . '/src/Payment/WeChatNotify.php';
$failures = [];
$check = static function (string $label, bool $ok) use (&$failures): void {
if (!$ok) {
$failures[] = $label;
}
};
$stored = static function (): array {
$v = get_option('aksrt_wechat_settings', []);
return is_array($v) ? $v : [];
};
$controller = \AKSRT\Admin\UserController::get_instance();
// 1) GET /wechat/status 返回完整状态形状(密钥掩码)。
$response = $controller->get_wechat_status(new WP_REST_Request());
$data = is_array($response->data) ? ($response->data['data'] ?? []) : [];
$check('status 含 enabled', array_key_exists('enabled', $data) && !empty($data['enabled']));
$check('status 含 appid', ($data['appid'] ?? '') === 'wxAPPID');
$check('status configured', !empty($data['configured']));
$check('status has_token', !empty($data['has_token']));
$check('status has_template', !empty($data['has_template']));
$check('status bound_users', ($data['bound_users'] ?? 0) === 7);
$check('status 掩码 app_secret', ($data['app_secret'] ?? '') === '***');
$check('status allowed_types 非空', !empty($data['allowed_types']));
// 2) 完整更新写入全部字段。
$response = $controller->update_wechat_settings(new WP_REST_Request([
'enabled' => true,
'app_id' => 'wxNEW',
'app_secret' => 'NEWSECRET',
'template_id' => 'TPL2',
]));
$check('更新写入 app_id', ($stored()['app_id'] ?? '') === 'wxNEW');
$check('更新写入 app_secret', ($stored()['app_secret'] ?? '') === 'NEWSECRET');
$check('更新写入 template_id', ($stored()['template_id'] ?? '') === 'TPL2');
$check('更新返回成功消息', str_contains((string) ($response->data['message'] ?? ''), '已保存'));
// 3) 掩码 *** 保留已存密钥。
$controller->update_wechat_settings(new WP_REST_Request([
'enabled' => true,
'app_id' => 'wxNEW',
'app_secret' => '***',
]));
$check('掩码保留原密钥', ($stored()['app_secret'] ?? '') === 'NEWSECRET');
// 4) 缺省字段沿用当前值,开关可关闭。
$controller->update_wechat_settings(new WP_REST_Request(['enabled' => false]));
$check('缺省字段沿用当前值', ($stored()['app_id'] ?? '') === 'wxNEW' && ($stored()['template_id'] ?? '') === 'TPL2');
$check('enabled 可关闭', empty($stored()['enabled']));
// 5) 输入消毒 + 未知键被白名单丢弃。
$controller->update_wechat_settings(new WP_REST_Request([
'app_id' => '<b>wxXSS</b>',
'evil' => 'x',
]));
$check('app_id 经消毒', ($stored()['app_id'] ?? '') === 'wxXSS');
$check('未知键被忽略', !array_key_exists('evil', $stored()));
// 6) 未配置时空密钥可显式清空。
update_option('aksrt_wechat_settings', ['enabled' => true, 'app_id' => 'a', 'app_secret' => '', 'template_id' => '']);
$controller->update_wechat_settings(new WP_REST_Request(['app_secret' => '']));
$check('空密钥可清存', ($stored()['app_secret'] ?? '') === '');
if ($failures) {
echo "WeChat settings harness FAILED\n";
foreach ($failures as $failure) {
echo ' - ' . $failure . "\n";
}
exit(1);
}
echo "WeChat settings harness passed\n";
}
@@ -0,0 +1,84 @@
'use strict';
/**
* 微信模板消息 REST 契约(修复存量审计错误):
* 1) /wechat/status 与 /wechat/settings 的路由回调方法在 UserController 中真实存在;
* 2) 状态读取委托 WeChatNotify::get_status(),写入委托 Settings::update_wechat_settings();
* 3) app_secret 掩码 *** 不覆盖已存密钥,输入经 sanitize_text_field 消毒;
* 4) PHP harness 驱动真实控制器验证上述行为;
* 5) aksrt_settings 上的微信通知死键(wechat_notify_* 等)已彻底移除,
* 活配置面仅剩 aksrt_wechat_settings(/users/wechat-template 自定义页)。
*/
const assert = require('node:assert/strict');
const { execFileSync } = require('node:child_process');
const fs = require('node:fs');
const path = require('node:path');
const test = require('node:test');
const root = path.resolve(__dirname, '..', '..');
const controller = fs.readFileSync(path.join(root, 'src/Admin/UserController.php'), 'utf8');
const schemaUsers = fs.readFileSync(path.join(root, 'src/Admin/Schema/SchemaUsers.php'), 'utf8');
const usersSchemaIndex = fs.readFileSync(path.join(root, 'assets/admin/src/schema/sections/users/index.js'), 'utf8');
const usersSections = fs.readFileSync(path.join(root, 'assets/admin/src/constants/sections/users.js'), 'utf8');
// 死双路面:写进 aksrt_settings、全仓无消费方的微信通知键。
const DEAD_WECHAT_KEYS = [
'wechat_notify_enabled',
'wechat_app_id',
'wechat_app_secret',
'wechat_template_id',
'wechat_token',
'wechat_encoding_aes_key',
'wechat_template_comment',
'wechat_template_reply',
'wechat_template_system',
'wechat_template_order',
'wechat_notify_comment',
'wechat_notify_reply',
'wechat_notify_system',
'wechat_notify_order',
'wechat_bind_entry',
];
test('路由与回调方法配对存在', () => {
assert.match(controller, /register_rest_route\(self::API_NAMESPACE, '\/wechat\/status'[\s\S]*?'callback'\s*=>\s*\[\$this, 'get_wechat_status'\]/);
assert.match(controller, /register_rest_route\(self::API_NAMESPACE, '\/wechat\/settings'[\s\S]*?'callback'\s*=>\s*\[\$this, 'update_wechat_settings'\]/);
assert.match(controller, /public function get_wechat_status\(\\WP_REST_Request \$request\)/);
assert.match(controller, /public function update_wechat_settings\(\\WP_REST_Request \$request\)/);
});
test('状态与写入委托既有服务层', () => {
assert.match(controller, /WeChatNotify::get_instance\(\)->get_status\(\)/);
assert.match(controller, /Settings::update_wechat_settings\(/);
});
test('密钥掩码与输入消毒', () => {
assert.match(controller, /\$params\['app_secret'\] !== '\*\*\*'/);
assert.match(controller, /sanitize_text_field\(\(string\) \$params\['app_id'\]\)/);
assert.match(controller, /sanitize_text_field\(\(string\) \$params\['app_secret'\]\)/);
assert.match(controller, /sanitize_text_field\(\(string\) \$params\['template_id'\]\)/);
});
test('微信设置行为 harness 通过', () => {
const harness = path.join(root, 'tests', 'admin', 'wechat-settings-harness.php');
const output = execFileSync('php', [harness], { encoding: 'utf8' });
assert.match(output, /WeChat settings harness passed/);
});
test('死双路面(aksrt_settings 微信通知键)已彻底移除', () => {
for (const key of DEAD_WECHAT_KEYS) {
assert.ok(!schemaUsers.includes(key), `SchemaUsers 残留: ${key}`);
assert.ok(!usersSections.includes(key), `users 菜单常量残留: ${key}`);
}
assert.ok(!usersSchemaIndex.includes('wechatSchema'), 'users schema index 残留 wechatSchema 注册');
assert.ok(!usersSections.includes("route: '/users/wechat',"), '用户菜单残留微信通知 schema 页');
assert.ok(!fs.existsSync(path.join(root, 'assets/admin/src/schema/sections/users/wechat.js')), 'wechat.js 未删除');
});
test('活配置面保持唯一(aksrt_wechat_settings + 自定义页)', () => {
assert.match(usersSections, /key: 'users-wechat-template'/);
assert.match(usersSections, /route: '\/users\/wechat-template'/);
assert.match(controller, /WeChatNotify::get_instance\(\)->get_status\(\)/);
assert.match(controller, /Settings::update_wechat_settings\(/);
});
@@ -180,6 +180,52 @@ test('guest account member VIP and publish contracts share reference header DOM'
assert.match(userboxCss, /\.aksrt-header-userbox\s*>\s*\.mt6\.em09\s*>\s*\.but\.block\s*\{[^}]*display:\s*block;[^}]*box-sizing:\s*border-box;[^}]*width:\s*100%;/s); assert.match(userboxCss, /\.aksrt-header-userbox\s*>\s*\.mt6\.em09\s*>\s*\.but\.block\s*\{[^}]*display:\s*block;[^}]*box-sizing:\s*border-box;[^}]*width:\s*100%;/s);
}); });
test('the top-nav midline chip rule leaves the popover social buttons alone', () => {
const userboxCss = read('assets/css/frontend/header-userbox.css');
const publicCss = read('assets/css/build/public.css');
// The account-row chips, the theme toggle and the language toggle all want a
// 29px midline circle. The popover's quick-login buttons are .toggle-radius
// too and also live inside .navbar-form, but they must keep the
// .social_loginbar geometry from main.css. flex-basis outranks width in a
// flex container and the bare rule is (0,3,0) against (0,2,0), so an
// unscoped selector collapses the buttons to 29px and zeroes their margins.
assert.match(userboxCss, /\.navbar-top \.navbar-form \.toggle-radius:not\(\.social-login-item\):not\(\.xh-social-item\)\s*\{[^}]*flex:\s*0 0 29px;[^}]*margin:\s*0;/s);
assert.doesNotMatch(userboxCss, /\.navbar-top \.navbar-form \.toggle-radius\s*\{/);
// header-userbox.css is also folded into the public bundle, so the exemption
// has to survive the build.
assert.match(publicCss, /\.navbar-top \.navbar-form \.toggle-radius:not\(\.social-login-item\):not\(\.xh-social-item\)/);
});
test('route and feature modules do not restyle the legacy top-nav chips', () => {
// Route and feature modules may not restyle the legacy .but colour modifiers
// without an enclosing scope. The top-nav chips ride the legacy geometry
// verbatim (newadd-btns but <skin> radius and pay-vip but jb-red radius
// payvip-icon), so their pill comes from .but.radius at (0,2,0). A later,
// equally specific rule that restates border-radius on a bare .but.<colour>
// out-ranks it and flattens the pill: user-center/credits.css did exactly that
// with .but.jb-blue, so the user-center route rendered a 4px publish chip while
// every other route kept the 50px one. Such rules must be scoped to their own
// shell, as credits.css now is with .uc-content.
const moduleCss = [
...listFiles('assets/css/routes', new Set(['.css'])),
...listFiles('assets/css/frontend', new Set(['.css'])),
].filter((file) => file !== 'assets/css/frontend/header-userbox.css');
for (const file of moduleCss) {
const css = read(file);
for (const [, selector, declarations] of css.matchAll(/([^{}]+)\{([^{}]*)\}/g)) {
if (!/border-radius/.test(declarations)) continue;
for (const part of selector.split(',')) {
assert.doesNotMatch(
part.trim(),
/^\.but\./,
`${file} restates border-radius on an unscoped legacy .but modifier (${part.trim()}); scope it to the module's own shell so the top-nav chips keep the reference pill`
);
}
}
}
})
test('the account popover rows match the reference user box structure', () => { test('the account popover rows match the reference user box structure', () => {
const identity = read('inc/helpers/aksrt-identity.php'); const identity = read('inc/helpers/aksrt-identity.php');
const legacyMainCss = read('assets/css/main.css'); const legacyMainCss = read('assets/css/main.css');
@@ -217,7 +263,12 @@ test('the account popover rows match the reference user box structure', () => {
assert.match(header, /radius4 payvip-icon btn-block text-center/); assert.match(header, /radius4 payvip-icon btn-block text-center/);
assert.match(header, /vip-expdate-tag/); assert.match(header, /vip-expdate-tag/);
assert.match(header, /pay-vip but radius4 payvip-icon btn-block/); assert.match(header, /pay-vip but radius4 payvip-icon btn-block/);
assert.match(userboxCss, /\.aksrt-header-userbox \.pay-vip\.but\.payvip-icon\s*\{[\s\S]*?background:\s*linear-gradient\(300deg,[\s\S]*?color:\s*#ece0e0;[\s\S]*?--this-color:\s*#ece0e0;/s); // core/buttons.css restates .but after main.css, which would otherwise replace
// the legacy .payvip-icon surface with the .jb-* skin. The restoration must
// cover background, color, border and text-shadow, and must not be scoped to
// one call site or the top-nav chip keeps the wrong gradient.
assert.match(userboxCss, /\.payvip-icon\s*\{[\s\S]*?background:\s*linear-gradient\(300deg,[\s\S]*?color:\s*#ece0e0;[\s\S]*?--this-color:\s*#ece0e0;[\s\S]*?border:\s*none;[\s\S]*?text-shadow:\s*none;/s);
assert.doesNotMatch(userboxCss, /\.aksrt-header-userbox \.pay-vip\.but\.payvip-icon/)
assert.doesNotMatch(header, /vipbg-v2/); assert.doesNotMatch(header, /vipbg-v2/);
// Check-in entry is an anchor with the reference button classes. // Check-in entry is an anchor with the reference button classes.
@@ -225,8 +276,11 @@ test('the account popover rows match the reference user box structure', () => {
assert.match(header, /but block c-yellow is-done/); assert.match(header, /but block c-yellow is-done/);
assert.match(buttonsCss, /\.but\.block\s*\{\s*display:\s*block;\s*\}/); assert.match(buttonsCss, /\.but\.block\s*\{\s*display:\s*block;\s*\}/);
assert.match(buttonsCss, /\.btn-block\s*\{\s*display:\s*block;\s*width:\s*100%;\s*\}/); assert.match(buttonsCss, /\.btn-block\s*\{\s*display:\s*block;\s*width:\s*100%;\s*\}/);
assert.match(legacyMainCss, /\.navbar-top \.navbar-but > \.but\s*\{[^}]*font-size:\s*0\.8em;[^}]*padding:\s*0\.3em 1em !important;[^}]*margin:\s*0\.4em;/s); // The reference selector is a descendant, so the VIP chip inside its hover-show
assert.doesNotMatch(legacyMainCss, /\.navbar-top \.navbar-but \.but\s*\{/); // wrapper receives the same .but geometry (font-size, padding, margin) as the
// publish chip instead of falling back to the bare .but defaults.
assert.match(legacyMainCss, /\.navbar-top \.navbar-but \.but\s*\{[^}]*font-size:\s*0\.8em;[^}]*padding:\s*0\.3em 1em !important;[^}]*margin:\s*0\.4em;/s);
assert.doesNotMatch(legacyMainCss, /\.navbar-top \.navbar-but > \.but\s*\{/);
assert.doesNotMatch(header, /but block c-yellow[^"']*btn-block/); assert.doesNotMatch(header, /but block c-yellow[^"']*btn-block/);
assert.match(header, /<div class="mt6 em09" style="padding:2px;">' \. \$checkin \. '<\/div>/); assert.match(header, /<div class="mt6 em09" style="padding:2px;">' \. \$checkin \. '<\/div>/);
assert.match(header, /data-signin-done-label[\s\S]{0,120}?今日已签到/); assert.match(header, /data-signin-done-label[\s\S]{0,120}?今日已签到/);
@@ -239,3 +239,77 @@ test('OAuth frontend is accessible, redirect-aware, and removes retired provider
assert.doesNotMatch(signCss + mainCss, /social-login-item[^\n{]*(?:baidu|alipay)|social-login-item\.(?:baidu|alipay)/i); assert.doesNotMatch(signCss + mainCss, /social-login-item[^\n{]*(?:baidu|alipay)|social-login-item\.(?:baidu|alipay)/i);
assert.match(mainCss, /\.pay-box \.but\.baidu/); assert.match(mainCss, /\.pay-box \.but\.baidu/);
}); });
test('the social-login area drops the implicit agreement line', () => {
const shared = read('templates/parts/auth-content.php');
const userRenderer = read('src/WidgetSystem/Renderers/UserRenderer.php');
// The reference shows no agreement copy under the social buttons; only the
// registration panel keeps its own, and it stays reachable.
assert.doesNotMatch(shared, /使用社交账号登录即表示/);
assert.match(shared, /注册即表示/);
assert.match(shared, /name="agree_terms"[\s\S]*?required/);
assert.doesNotMatch(userRenderer, /使用社交账号登录即表示|我已阅读并同意/);
});
test('brand icons resolve Gitee and Google to the reference SVG sprite', () => {
const icons = read('inc/helpers/aksrt-icons.php');
const sprites = read('assets/js/runtime/svg-icon.js');
// FA5 Free ships no Gitee brand glyph and no multi-colour Google mark, so both
// resolve to the theme's own symbol sheet exactly like the reference does.
assert.match(icons, /'gitee'\s*=>\s*'zibsvg-gitee'/);
assert.match(icons, /'google'\s*=>\s*'zibsvg-google-color'/);
assert.match(icons, /'github'\s*=>\s*'fa fab fa-github'/);
assert.match(icons, /'wechat'\s*=>\s*'fa fab fa-weixin'/);
assert.match(sprites, /aksrt_svgs\['gitee'\]/);
assert.match(sprites, /aksrt_svgs\['google-color'\]/);
// Sprite values must reach the shared icon renderer instead of being wrapped
// in a bogus <i class="zibsvg-…">.
assert.match(icons, /strpos\(\$class, 'zibsvg-'\) === 0[\s\S]*?aksrt_icon_markup\(\$class, \$extra_classes\)/);
});
test('the top-nav user popover reproduces the reference quick-login block', () => {
const oauth = read('src/Auth/OAuth.php');
const header = read('inc/helpers/header/aksrt-header-navbar.php');
const links = read('inc/helpers/aksrt-links.php');
const legacyCss = read('assets/css/frontend/pages/base-1.css');
assert.match(oauth, /social-separator separator muted-3-color em09 mt10/);
assert.match(oauth, /'<div class="social_loginbar">'/);
assert.match(oauth, /'social-login-item ' \. \$type/);
assert.match(oauth, /' button-lg' : ' toggle-radius'/);
assert.match(oauth, /rel="nofollow"/);
assert.match(oauth, /qrcode-signin/);
assert.match(oauth, /oauth_button_lg/);
// Callers append the block verbatim — no hand-rolled separator or wrapper.
assert.match(header, /render_social_buttons\(__\('快速登录', 'aksrt'\)\)/);
assert.doesNotMatch(header, /social-separator|social_loginbar/);
assert.match(links, /render_social_buttons\(__\('使用以下方式快速登录', 'aksrt'\)\)/);
assert.doesNotMatch(links, /social_loginbar/);
// The retired .social-login-divider / .oauth-row / .oauth-btn markup and its
// stylesheet are gone.
assert.doesNotMatch(oauth, /oauth-row|oauth-btn|social-login-divider/);
assert.doesNotMatch(legacyCss, /\.oauth-row|\.oauth-btn|\.social-login-divider/);
});
test('provider URLs stay raw in the data layer and are escaped once per surface', () => {
const oauth = read('src/Auth/OAuth.php');
const shared = read('templates/parts/auth-content.php');
const userRenderer = read('src/WidgetSystem/Renderers/UserRenderer.php');
const comments = read('inc/helpers/aksrt-comments.php');
// get_auth_providers_data() hands out raw URLs, exactly like its sibling
// get_frontend_bindings_data() does. esc_url() is idempotent on its own
// output, so escaping here as well was redundant rather than broken.
assert.doesNotMatch(oauth, /'url'\s*=>\s*esc_url\(\$url\)/);
assert.match(oauth, /'url'\s*=>\s*\$url,/);
// Every consumer still escapes on output, so the rendered href is unchanged.
assert.match(shared, /esc_url\(\(string\) \(\$provider\['url'\] \?\? '#'\)\)/);
assert.match(userRenderer, /href="' \. esc_url\(\$url\)/);
assert.match(oauth, /'<a rel="nofollow" href="' \. esc_url\(\$url\)/);
assert.match(comments, /<a href="<\?php echo esc_url\(\$url\); \?\>" class="but hollow social-login-item ml10">/);
});
@@ -120,6 +120,10 @@ test('defaults, nested structures, dependencies, and excluded product families a
} }
assert.equal(headerMenuButtons.includes('nav_newposts_btns'), false) assert.equal(headerMenuButtons.includes('nav_newposts_btns'), false)
assert.match(headerMenuButtons, /aksrt_render_new_add_btns\(\['post'\], \$class, \$label\)/) assert.match(headerMenuButtons, /aksrt_render_new_add_btns\(\['post'\], \$class, \$label\)/)
// FA5 spelling of the reference '<i class="fa fa-fw fa-pencil"></i>发布'
// default: fa-pencil does not exist in the bundled FA 5.15.4 free set.
assert.match(schema, /'nav_newposts_text', '按钮文案', 'textarea', '<i class="fa fas fa-fw fa-pencil-alt"><\/i>发布'/)
assert.doesNotMatch(schema, /fa fas fa-fw fas /)
}) })
test('VIP navigation purchase switches are saved and consumed end to end', () => { test('VIP navigation purchase switches are saved and consumed end to end', () => {
@@ -136,8 +140,18 @@ test('VIP navigation purchase switches are saved and consumed end to end', () =>
}) })
test('header VIP hover cards match the legacy markup and style contract', () => { test('header VIP hover cards match the legacy markup and style contract', () => {
assert.match(headerMenuButtons, /<span class="hover-show inline-block navbar-vip-hover">/) assert.match(headerMenuButtons, /<span class="hover-show inline-block">/)
assert.match(headerMenuButtons, /<div class="hover-show-con sub-menu sub-vip-card">/) assert.match(headerMenuButtons, /<div class="sub-menu hover-show-con sub-vip-card">/)
assert.match(headerMenuButtons, /'<a class="' \. esc_attr\(\$cls\) \. ' but jb-red radius payvip-icon ml10" href="javascript:;">'/)
// The payvip surface must survive the .but compatibility layer for the top-nav
// chip too, not just the user-card CTA.
assert.match(headerCss, /\.payvip-icon\s*\{[\s\S]*?background:\s*linear-gradient\(300deg,[\s\S]*?--this-color:\s*#ece0e0;/s)
assert.match(headerMenuButtons, /<svg class="em12 mr3" aria-hidden="true"><use xlink:href="#icon-vip_1"><\/use><\/svg>'/)
assert.match(headerMenuButtons, /'<a rel="nofollow" class="newadd-btns ' \. esc_attr\(\$class\) \. ' btn-newadd"/)
assert.equal((headerMenuButtons.match(/Settings::close_sign\(\)/g) || []).length, 2)
for (const dead of ['navbar-cta', 'navbar-cta-text', 'navbar-newposts', 'navbar-vip-hover', 'newadd-btns__', 'vip-level', 'fa-gem']) {
assert.equal(headerMenuButtons.includes(dead), false, `AKSRT-only header CTA token remains: ${dead}`)
}
assert.match(vipCardRenderer, /aksrt_get_lazy_attr\('lazy_other', \$icon_url, 'img-icon'\)/) assert.match(vipCardRenderer, /aksrt_get_lazy_attr\('lazy_other', \$icon_url, 'img-icon'\)/)
assert.match(vipCardRenderer, /<div class=\"abs-center vip-baicon\"[^>]*>/) assert.match(vipCardRenderer, /<div class=\"abs-center vip-baicon\"[^>]*>/)
assert.match(vipCardRenderer, /<div class=\"vip-img abs-right\">/) assert.match(vipCardRenderer, /<div class=\"vip-img abs-right\">/)
@@ -154,8 +168,21 @@ test('header VIP hover cards match the legacy markup and style contract', () =>
assert.match(legacyMainCss, /\.sub-vip-card \.vip-card\s*\{[^}]*display:\s*inline-block;[^}]*width:\s*240px;[^}]*margin:\s*5px;/s) assert.match(legacyMainCss, /\.sub-vip-card \.vip-card\s*\{[^}]*display:\s*inline-block;[^}]*width:\s*240px;[^}]*margin:\s*5px;/s)
assert.match(legacyMainCss, /\.sub-vip-card \.vip-baicon\.abs-center\s*\{[^}]*font-size:\s*10em;[^}]*left:\s*55%;/s) assert.match(legacyMainCss, /\.sub-vip-card \.vip-baicon\.abs-center\s*\{[^}]*font-size:\s*10em;[^}]*left:\s*55%;/s)
assert.match(legacyMainCss, /\.sub-vip-card \.vip-img\s*\{[^}]*display:\s*none;/s) assert.match(legacyMainCss, /\.sub-vip-card \.vip-img\s*\{[^}]*display:\s*none;/s)
assert.match(headerCss, /\.navbar-top \.aksrt-navbar-actions \.navbar-vip-hover\s*\{[^}]*align-self:\s*stretch;[^}]*overflow:\s*visible;/s) // The hover wrapper must stay a plain block container so the legacy static
assert.match(headerCss, /\.navbar-top \.aksrt-navbar-actions \.navbar-vip-hover > \.sub-vip-card\s*\{[^}]*top:\s*100%;[^}]*right:\s*0;[^}]*left:\s*auto;[^}]*max-width:\s*calc\(100vw - 24px\);/s) // position of .sub-vip-card matches the reference theme.
assert.match(headerCss, /\.navbar-top \.navbar-but > \.hover-show\s*\{[^}]*display:\s*inline-block;/s)
assert.doesNotMatch(headerCss, /\.navbar-cta|\.navbar-vip-hover/)
assert.match(legacyMainCss, /\.navbar-top \.navbar-right \.sub-menu\s*\{[^}]*right:\s*0;/s)
})
test('user card VIP CTA matches the reference zib_get_header_payvip_icon()', () => {
assert.match(headerUserbox, /'<a class="pay-vip but radius4 payvip-icon btn-block" href="javascript:;">'/)
assert.match(headerUserbox, /<svg class="em12 mr6" aria-hidden="true"><use xlink:href="#icon-vip_1"><\/use><\/svg>'/)
assert.match(headerUserbox, /esc_html__\('开通会员 尊享会员权益', 'aksrt'\)/)
for (const dead of ['fa-gem', 'vip-level']) {
assert.equal(headerUserbox.includes(dead), false, `AKSRT-only user card CTA token remains: ${dead}`)
}
assert.doesNotMatch(headerUserbox, /<span>' \. esc_html__\('开通会员 尊享会员权益'/)
}) })
test('VIP purchase modal matches legacy desktop-center and mobile-bottom positioning', () => { test('VIP purchase modal matches legacy desktop-center and mobile-bottom positioning', () => {
@@ -322,13 +349,30 @@ test('user profile and author widgets render the reference user-card contract',
}) })
test('desktop header actions share one row without generic button masks', () => { test('desktop header actions share one row without generic button masks', () => {
assert.match(headerNavbar, /aksrt-navbar-actions navbar-form navbar-right/) assert.match(headerNavbar, /navbar-form navbar-right navbar-but/)
assert.match(headerNavbar, /navbar-form navbar-right flex ac/)
assert.match(headerNavbar, /\$fold \. \$button \. \$right_but/)
assert.match(headerNavbar, /class="aksrt-nav-icon nav-search-btn main-search-btn"/) assert.match(headerNavbar, /class="aksrt-nav-icon nav-search-btn main-search-btn"/)
assert.doesNotMatch(headerNavbar, /class="btn nav-search-btn main-search-btn"/) assert.doesNotMatch(headerNavbar, /class="btn nav-search-btn main-search-btn"/)
assert.match(headerCss, /\.navbar-top \.aksrt-navbar-actions\s*\{[^}]*display:\s*flex;[^}]*align-items:\s*center;/s) assert.match(headerCss, /\.navbar-top \.aksrt-navbar-account\s*\{[^}]*display:\s*inline-flex;[^}]*align-items:\s*center;/s)
assert.match(headerCss, /\.navbar-top \.aksrt-nav-icon,[\s\S]*?background:\s*transparent !important;/) assert.match(headerCss, /\.navbar-top \.aksrt-nav-icon,[\s\S]*?background:\s*transparent !important;/)
assert.match(headerCss, /\.navbar-top \.aksrt-navbar-actions \.navbar-cta\.radius\s*\{[^}]*border-radius:\s*50px;/s) // Bootstrap's base .navbar-form rule keeps margin-top: 8px at >=768px (the
assert.match(headerCss, /\.navbar-top \.aksrt-navbar-actions \.navbar-cta > \.icon\s*\{[^}]*width:\s*1\.2em;[^}]*height:\s*1\.2em;[^}]*flex:\s*0 0 auto;/s) // override only zeroes the horizontal margins), so every right-cluster group
// starts 8px below the top of .navbar-collapse. A 34px cross-size therefore
// puts each group's midline on the navbar's 25px line, where the 50px menu row
// and the logo already sit. A 50px min-height double-counts that 8px: the
// group's content drops 8px below the line and the navbar grows from 50px to
// 58px, which also breaks the body.nav-fixed offset.
assert.doesNotMatch(headerCss, /\.navbar-top \.aksrt-navbar-account\s*\{[^}]*min-height/s)
assert.match(headerCss, /\.navbar-top \.navbar-form\.flex\.ac\s*\{[^}]*min-height:\s*34px;/s)
// The publish and VIP chips ride the legacy .but geometry verbatim; nothing in
// the header module may restate their padding, margin, radius or icon size.
// Descendant, not child: the VIP chip lives inside the hover-show wrapper.
assert.match(legacyMainCss, /\.navbar-top \.navbar-but \.but\s*\{[^}]*font-size:\s*0\.8em;[^}]*padding:\s*0\.3em 1em !important;[^}]*margin:\s*0\.4em;/s)
assert.doesNotMatch(headerCss, /\.navbar-cta|\.navbar-vip-hover/)
// No merged flex container: the three groups keep their own .navbar-form
// padding, which is what produces the reference inter-cluster spacing.
assert.doesNotMatch(headerCss, /\.aksrt-navbar-actions/)
assert.match(headerCss, /\.navbar-top \.aksrt-navbar-account > li\s*\{[^}]*position:\s*relative;/s) assert.match(headerCss, /\.navbar-top \.aksrt-navbar-account > li\s*\{[^}]*position:\s*relative;/s)
assert.match(headerCss, /\.navbar-top \.aksrt-navbar-account > li > \.sub-menu\s*\{[^}]*top:\s*calc\(100% \+ 5px\);[^}]*right:\s*0;/s) assert.match(headerCss, /\.navbar-top \.aksrt-navbar-account > li > \.sub-menu\s*\{[^}]*top:\s*calc\(100% \+ 5px\);[^}]*right:\s*0;/s)
}) })
@@ -0,0 +1,100 @@
'use strict'
const assert = require('node:assert/strict')
const fs = require('node:fs')
const path = require('node:path')
const test = require('node:test')
const root = path.resolve(__dirname, '..', '..')
const read = (file) => fs.readFileSync(path.join(root, file), 'utf8')
// Reference theme (zibll css/main.css): .jb-yellow sets --this-bg-b: #f59f54 and
// --this-bg: linear-gradient(135deg, #f59f54 10%, #ff6922 100%). build/public.min.css
// is enqueued after main.min.css, so the legacy .jb-* compat layer in core/buttons.css
// overrides main.css there; the two must agree or every .jb-yellow surface (the
// user-card check-in badge first of all) paints the wrong gradient.
const referenceGradient = /--this-bg:\s*linear-gradient\(135deg,\s*#f59f54 10%,\s*#ff6922 100%\)/
const staleHexes = /#f99d4d|#f7631d/
test('buttons.css legacy .jb-yellow gradient matches the reference theme', () => {
const buttons = read('assets/css/core/buttons.css')
const start = buttons.indexOf('.jb-yellow {')
assert.notEqual(start, -1, 'missing the legacy .jb-yellow rule')
const block = buttons.slice(start, buttons.indexOf('}', start))
assert.match(block, referenceGradient)
assert.doesNotMatch(block, staleHexes)
})
test('the built public bundle carries the reference .jb-yellow gradient', () => {
const bundle = read('assets/css/build/public.min.css')
assert.match(
bundle,
/\.jb-yellow\{--this-bg: linear-gradient\(135deg, #f59f54 10%, #ff6922 100%\)\}/
)
assert.doesNotMatch(
bundle,
/\.jb-yellow\{--this-bg: linear-gradient\(135deg, #f99d4d 10%, #f7631d 100%\)\}/
)
})
test('main.css still owns the reference .jb-yellow gradient', () => {
const main = read('assets/css/main.css')
assert.match(main, referenceGradient)
assert.doesNotMatch(main, staleHexes)
})
test('the badge typography is pinned to the reference bootstrap body values', () => {
const main = read('assets/css/main.css')
const start = main.indexOf('.user-card .card-content .img-badge')
assert.notEqual(start, -1, 'missing the user-card img-badge adaptation rule')
const block = main.slice(start, main.indexOf('}', start))
assert.match(block, /line-height:\s*1\.42857143/)
// Reference body font stack (zibll bootstrap.min.css; zibll main.css does not
// override it), while AKSRT's reset imposes the modern --font-body stack.
assert.match(block, /font-family:\s*"Helvetica Neue", Helvetica, Arial, sans-serif/)
// The AKSRT html reset forces antialiased + optimizeLegibility; the badge
// restores the browser defaults the reference renders with.
assert.match(block, /-webkit-font-smoothing:\s*auto/)
assert.match(block, /text-rendering:\s*auto/)
const min = read('assets/css/main.min.css')
assert.match(
min,
/\.user-card \.card-content \.img-badge\{line-height:1\.42857143;font-family:Helvetica Neue,Helvetica,Arial,sans-serif;-webkit-font-smoothing:auto;text-rendering:auto\}/
)
})
test('the sidebar widget link color does not recolor the check-in badge', () => {
// .widget a (sidebar.css, shipped in the homepage bundle) out-specifies the
// .jb-* color group, so the badge would paint --color-ink-2 instead of the
// white --this-color; the established .but restoration must cover img-badge.
const sidebar = read('assets/css/frontend/sidebar.css')
const start = sidebar.indexOf('.widget a.but')
assert.notEqual(start, -1, 'missing the .widget a color restoration')
const block = sidebar.slice(start, sidebar.indexOf('}', start))
assert.match(block, /\.widget a\.but,/)
assert.match(block, /\.widget a\.img-badge/)
assert.match(block, /color:\s*var\(--this-color/)
const bundle = read('assets/css/build/homepage.min.css')
assert.match(bundle, /\.widget a\.but,\.widget a\.img-badge\{color:var\(--this-color, #fff\)\}/)
})
test('the user-card check-in badge renders without the dead is-done class and bare span', () => {
const userbox = read('inc/helpers/aksrt-header-userbox.php')
const fnStart = userbox.indexOf('function aksrt_header_user_checkin_btn')
assert.notEqual(fnStart, -1, 'missing aksrt_header_user_checkin_btn')
const fnEnd = userbox.indexOf('if (!function_exists', fnStart)
assert.notEqual(fnEnd, -1)
const fn = userbox.slice(fnStart, fnEnd)
// Signed card badge: the badge class passes through untouched and the done
// label is bare text after the icon, matching the reference markup.
assert.match(fn, /aksrt_get_user_checkin_details_link\(\s*\$class,/)
assert.doesNotMatch(fn, /\$class \. ' is-done'/)
assert.doesNotMatch(fn, /'<span>'/)
// The idle badge keeps the data-signin-label hook the sign-in controller
// uses to swap labels in place.
assert.match(fn, /<span data-signin-label>/)
})
@@ -23,9 +23,13 @@ test('user and author widgets render the reference state contract', () => {
assert.match(data.guest, /signup-loader ml10 but jb-yellow padding-lg/) assert.match(data.guest, /signup-loader ml10 but jb-yellow padding-lg/)
assert.match(data.guest, /social-separator separator muted-3-color em09 mt20 mb20/) assert.match(data.guest, /social-separator separator muted-3-color em09 mt20 mb20/)
assert.match(data.guest, /social_loginbar/) assert.match(data.guest, /social_loginbar/)
assert.match(data.guest, /class="social-login-item github"/) assert.match(data.guest, /class="social-login-item github toggle-radius"/)
assert.match(data.guest, /class="social-login-item qq" data-provider="clogin_qq"/) assert.match(data.guest, /class="social-login-item qq toggle-radius" data-provider="clogin_qq"/)
assert.match(data.guest, /title="GitHub登录"/)
assert.doesNotMatch(data.guest, /aksrt-widget|aksrt-panel-wrap|user-card__auth-btns|oauth-row|oauth-btn/) assert.doesNotMatch(data.guest, /aksrt-widget|aksrt-panel-wrap|user-card__auth-btns|oauth-row|oauth-btn/)
// The social-login area carries no agreement copy; the reference guest card
// only shows the separator, the .social_loginbar and the provider buttons.
assert.doesNotMatch(data.guest, /我已阅读并同意|使用社交账号登录即表示|注册即表示/)
assert.match(data.guest_no_registration, /signin-loader but jb-blue padding-lg/) assert.match(data.guest_no_registration, /signin-loader but jb-blue padding-lg/)
assert.doesNotMatch(data.guest_no_registration, /signup-loader/) assert.doesNotMatch(data.guest_no_registration, /signup-loader/)
+63 -12
View File
@@ -1,5 +1,5 @@
{ {
"generated_at": "2026-10-03T20:06:15+00:00", "generated_at": "2026-10-13T12:53:18+00:00",
"errors": [], "errors": [],
"warnings": [ "warnings": [
"customPage tools-update section 'tools/update' not in PHP schema (ok if pure operational page)", "customPage tools-update section 'tools/update' not in PHP schema (ok if pure operational page)",
@@ -45,6 +45,8 @@
"enum incomplete parse for 'search_type_in' (php subset of js): php=[post] js=[post,user]", "enum incomplete parse for 'search_type_in' (php subset of js): php=[post] js=[post,user]",
"enum incomplete parse for 'sign_card_position' (php subset of js): php=[left] js=[left,center,right]", "enum incomplete parse for 'sign_card_position' (php subset of js): php=[left] js=[left,center,right]",
"enum incomplete parse for 'sign_theme_mode' (php subset of js): php=[no] js=[no,white-theme,dark-theme]", "enum incomplete parse for 'sign_theme_mode' (php subset of js): php=[no] js=[no,white-theme,dark-theme]",
"enum incomplete parse for 'sms_ali_api_type' (php subset of js): php=[sms] js=[sms,verify]",
"enum incomplete parse for 'sms_sdk' (php subset of js): php=[null] js=[null,ali,tencent,smsbao]",
"enum incomplete parse for 'style' (php subset of js): php=[null] js=[null,style2]", "enum incomplete parse for 'style' (php subset of js): php=[null] js=[null,style2]",
"enum incomplete parse for 'system_notice_btn1_color' (php subset of js): php=[primary] js=[primary,success,warning,danger,info,purple,dark]", "enum incomplete parse for 'system_notice_btn1_color' (php subset of js): php=[primary] js=[primary,success,warning,danger,info,purple,dark]",
"enum incomplete parse for 'system_notice_btn2_color' (php subset of js): php=[primary] js=[primary,success,warning,danger,info,purple,dark]", "enum incomplete parse for 'system_notice_btn2_color' (php subset of js): php=[primary] js=[primary,success,warning,danger,info,purple,dark]",
@@ -57,12 +59,12 @@
"enum incomplete parse for 'user_sign_type' (php subset of js): php=[modal] js=[modal,page]", "enum incomplete parse for 'user_sign_type' (php subset of js): php=[modal] js=[modal,page]",
"enum incomplete parse for 'user_signin_nopas_active' (php subset of js): php=[nopas] js=[nopas,pas,only_nopas]", "enum incomplete parse for 'user_signin_nopas_active' (php subset of js): php=[nopas] js=[nopas,pas,only_nopas]",
"enum incomplete parse for 'wechat_payment_type' (php subset of js): php=[native] js=[native,h5]", "enum incomplete parse for 'wechat_payment_type' (php subset of js): php=[native] js=[native,h5]",
"enum incomplete parse for 'weixingzh_gzh_type' (php subset of js): php=[service] js=[service,subscribe]", "enum incomplete parse for 'weixingzh_gzh_type' (php subset of js): php=[service] js=[service,subscribe]"
"assets/admin/build/ missing — run: cd assets/admin && npm run build"
], ],
"info": [ "info": [
"PHP schema domains: global, page_display, post_list, permissions, users, payment, extensions, tools, ExtensionsAudit, ExtensionsIP, ExtensionsPush, ToolsBackup, ToolsHealth, ToolsImportExport", "PHP schema domains: global, page_display, post_list, permissions, users, payment, extensions, tools, ExtensionsAudit, ExtensionsIP, ExtensionsPush, ExtensionsSecurity, ExtensionsSystem, ToolsBackup, ToolsHealth, ToolsImportExport",
"Menu pages discovered: 60" "Menu pages discovered: 61",
"admin build ok; chunks=30"
], ],
"domains": { "domains": {
"global": { "global": {
@@ -136,8 +138,7 @@
"oauth", "oauth",
"report", "report",
"sms", "sms",
"verify", "verify"
"wechat"
], ],
"files": [ "files": [
"src/Admin/Schema/SchemaUsers.php" "src/Admin/Schema/SchemaUsers.php"
@@ -146,6 +147,7 @@
"payment": { "payment": {
"sections": [ "sections": [
"affiliate", "affiliate",
"after_sale",
"gateway", "gateway",
"knowledge", "knowledge",
"recharge", "recharge",
@@ -161,7 +163,9 @@
"sections": [ "sections": [
"audit", "audit",
"ip", "ip",
"push" "push",
"security",
"system"
], ],
"files": [ "files": [
"src/Admin/Schema/SchemaExtensions.php" "src/Admin/Schema/SchemaExtensions.php"
@@ -201,6 +205,22 @@
"src/Admin/Schema/SchemaExtensionsPush.php" "src/Admin/Schema/SchemaExtensionsPush.php"
] ]
}, },
"ExtensionsSecurity": {
"sections": [
"security"
],
"files": [
"src/Admin/Schema/SchemaExtensionsSecurity.php"
]
},
"ExtensionsSystem": {
"sections": [
"system"
],
"files": [
"src/Admin/Schema/SchemaExtensionsSystem.php"
]
},
"ToolsBackup": { "ToolsBackup": {
"sections": [ "sections": [
"backup" "backup"
@@ -226,11 +246,42 @@
] ]
} }
}, },
"page_count": 60, "page_count": 61,
"build": { "build": {
"present": false, "present": true,
"entry": false, "entry": true,
"missing_chunks": [], "missing_chunks": [],
"chunks": [] "chunks": [
"SchemaPage.BrFz57tJ.js",
"vendor-vue.BqIbVHit.js",
"FaIcon.BMA0TNWc.js",
"AksMediaField.C9RRDn2V.js",
"FaIconPicker.CA3w9r30.js",
"shared-api.Cak1GAri.js",
"vendor-misc.Dr7TL_YE.js",
"vendor-draggable.CjYjXbZD.js",
"level.E3ANSDAX.js",
"badge.CKTsmH1z.js",
"vendor-naive.Dzr6v856.js",
"DeferredPage.Bo6-Z82H.js",
"NativeOpsRedirect.wMMcdVjx.js",
"FloatingButtonsSettings.DM1NXEcr.js",
"PageHeader.UhySXCpq.js",
"FooterTabbarSettings.8D3P8zNv.js",
"PostPageSettings.B5Sm-lcm.js",
"CommentSettings.DiQrTo4p.js",
"PermissionsCapabilities.CnQfjtxC.js",
"CmsCredits.Dh2b9uHb.js",
"ThemeInfoPage.zYktV7qz.js",
"ThemeAuthorizationPage.e-ksjN6X.js",
"DocsPage.Dtd_-3fP.js",
"UsersVerify.CAJUfACd.js",
"UsersLevel.CBoqr3ux.js",
"UsersBadge.B_lwRfdl.js",
"UsersWechatTemplate.F0aCC5C-.js",
"ToolsImportExport.CN30A6Ge.js",
"ToolsHealth.B0GkAdJR.js",
"ToolsUpdate.DHtpNLfB.js"
]
} }
} }