Commit Graph
652 Commits
Author SHA1 Message Date
kate522 fd034ff055 fix(widgets): align tabbed post layouts with reference 2026-10-29 20:31:39 +08:00
kate522 8aa116ec43 fix(theme): align reference layouts and restore passing contracts
Align homepage and category discovery, modern widgets, admin controls, quotes, and comments with the reference theme. Remove superseded paths and add rendering and behavior coverage. Update stale asset and finance contracts; all 760 Node tests and 139 widget assertions pass.
2026-10-29 19:16:03 +08:00
kate522 9b27508a5b fix(frontend): align top navigation with reference theme 2026-10-29 18:10:00 +08:00
kate522 1c501ff713 fix(user): align badge modals with reference theme 2026-10-29 17:38:10 +08:00
kate522 8e9d3a7e49 fix(signin): align check-in feedback with reference theme 2026-10-29 17:06:25 +08:00
kate522andClaude Fable 5 1df2822f01 feat(pay): 统一收银台四种模式(站内二维码/当前页跳转/表单提交/JSAPI)
- PaymentOrchestrator 统一返回 mode + code_url/payment_url/form_html/jsapi_params
- 支付宝:当面付二维码 / 电脑网站支付 / H5;微信:Native / H5 / JSAPI(WechatOpenid 静默授权)
- 易支付:PC mapi 二维码,移动端自动提交表单
- 删除旧键 alipay_payment_type / wechat_payment_type / epay_device 与 redirect/qr_content 字段
- 新增 pay-cashier.js,移除 window.open,订单状态接口改为 /pay/order/{n}
- 拆分 credits.js(affiliate / withdraw-history)至 1000 行以内

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-10-29 14:01:28 +08:00
kate522andClaude Fable 5 c3328e7279 fix(pay): 待支付浮窗无刷新同步
- modal-cache 失效时广播 aksrt:cache:invalidated
- 新增 GET /orders/wait-pay-float 返回浮窗片段
- wait-pay-float.js 监听 orders 标签失效并替换/移除浮窗、重启倒计时
- VIP/充值/下载待支付分支触发 orders 失效

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-10-29 11:11:13 +08:00
kate522andClaude Fable 5 d9d1122e9c feat(pay): add wait-pay float window aligned with reference theme
- Render pending orders (max 5, >=3 min left) in a top-right float button
  with hover list and countdown, gated by pay_wait_float_s
- Fix order countdown parsing expires_at as UTC
- Drop unused .paid-modal-lists-box thumb rule

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-10-29 10:45:23 +08:00
kate522andClaude Fable 5 b7a910b758 fix(scripts): bust tbquire module cache with newest runtime/*.js mtime
_win.ver only tracked main.min.js, so rebuilding svg-icon.js alone kept
serving the cached sprite and new icons (ticket-color) rendered blank.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-10-29 00:08:05 +08:00
kate522andClaude Fable 5 fcec5b996f refactor(color): use wpColorPicker for page config nav color
Replace the native input[type=color] in the page config meta box with a
wpColorPicker-enhanced text input, enqueued only on page edit screens, and
contract-lock that no native color inputs remain in theme PHP.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-10-28 21:01:54 +08:00
kate522andClaude Fable 5 ed1487d505 fix(tests): repair stale pagination/lazyload contracts and ban defaults script
- pay-pages-pagination asserts the $parent_file base URL added in b74381cf
- Drop native loading="lazy" from message avatars and medal presets per
  the lazyload contract
- verify-ban-settings-defaults requires SchemaUsersTicket and new deps

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-10-28 20:47:42 +08:00
kate522andClaude Fable 5 57ff405691 refactor(color): unify color sanitizing and picking behind shared components
- Add AKSRT\Core\Color::sanitize($value, $allow_alpha) as the single PHP
  color sanitizer; replace sanitize_hex_color calls and per-class regexes
  (FloatButtons, FieldSchema, ThemeSearch, icon cards, TicketStatusService)
- SchemaProvider / ControllerBase sanitize `color` fields as colors;
  alpha kept only when the schema sets showAlpha
- Split SchemaProvider OAuth validation and option matching into sibling
  traits to stay under the 1000-line limit
- Add AksColorPicker.vue + utils/color.js mirroring the PHP rules; AksField,
  FloatBtnCommonFields and TicketStatusPanel use it
- Widget background color input now uses wpColorPicker
- Remove dead AksCompositeField dimensions/spacing/background branches and
  the unused Gutenberg ColorPicker var
- Add color-unification contract test; update harness requires and tests

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-10-28 20:47:41 +08:00
kate522andClaude Fable 5 74021d26bb fix(layout): align color scheme, content width and fonts with reference theme
- Color scheme: zibll field order/labels, theme_skin_custom overrides palette,
  hex palette swatches, dark_theme_skin_custom, light/dark background images,
  close_backdrop via body.fps-accelerat, theme_main_radius drives --main-radius
- Emit --theme-color / --focus-shadow-color / --focus-color-opacity* vars
- Drop border_radius, primary_color readers and legacy value maps
- Content max width: free 1200-2200 number field; fix font family/custom stack

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-10-28 19:36:11 +08:00
kate522andClaude Fable 5 6da08d9d4f refactor(settings): remove filing info config and readers
Drop the 备案信息 group (icp_filing, police_filing, footer_text) from the
global.common PHP/Vue schemas, the FacadeBulk readers and the
get_global_settings() payload. No frontend consumer existed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-10-28 18:36:27 +08:00
kate522andClaude Fable 5 fa89f43ac9 feat(admin): collapse provider/gateway groups by default, mask alipay private key, drop inline notice styles
- OAuth provider and payment gateway groups are collapsible and collapsed by default; remove dead expandWhen logic
- Textarea fields support secret masking; mark alipay_privatekey as secret in JS and PHP schemas
- Extract AksSecretClear shared by input and textarea secret fields
- Replace inline styles in payment/post-list notice HTML with aksrt-hint / aksrt-warn classes

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-10-28 15:50:37 +08:00
kate522andClaude Fable 5 3179754670 feat(admin): polish badge, OAuth and payment gateway settings UX
- Badge page: rewrite as collapsible sortable category/item cards with
  preset medal icon picker, dirty-tracked save bar and duplicate slug check;
  drop user_badge_* dual-read and legacy name/icon_url fallbacks
- OAuth: provider config groups collapsible, expanded when provider is on;
  clogin visibility moved to group-level dependsOn
- Gateway: expandWhen accepts dependsOn-style rules; select-gated groups no
  longer always expand ('null' was truthy); mark five gateway secrets as
  secret/password in JS and PHP; class-based notice list styling
- Remove unused UserRuntime::parse_badge_dictionary

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-10-28 15:33:49 +08:00
kate522andClaude Fable 5 116b6b2223 feat(admin): make all sortable/repeater cards collapsible, collapsed by default
- Add useCollapsibleItems: key-based open state, clickable/keyboard head, aria-expanded
- sortable items with sub-fields and every inline repeater/group row now collapse
- Repeater head summary falls back to the first filled field when no listPreview
- Drop per-schema collapsible/defaultCollapsed for repeater groups (JS + PHP)
- Align FloatBtnItemPanel head with the shared collapse attrs

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-10-28 14:12:13 +08:00
kate522andClaude Fable 5 135fb4eaa4 feat(tickets): allow custom status key in status create/edit
- Validate status_key (^[a-z][a-z0-9_]{1,23}$, unique) on create/update
- Renaming a key moves aksrt_ticket.status in the same transaction
- Default statuses remain immutable

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-10-27 23:22:38 +08:00
kate522andClaude Fable 5 63ab8b8fc9 feat(tickets): make ticket title/content/reply length limits configurable
Add ticket_title_max / ticket_content_max / ticket_reply_max to the
users/ticket schema (PHP + SPA) with clamped TicketSettings readers.
All write paths (front REST, admin REST, note REST, native admin page)
and front-end maxlength/counters now read the configured limits.
Remove the hardcoded TICKET_CONTENT_MAX constant.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-10-27 23:06:28 +08:00
kate522andClaude Fable 5 6681b0c306 feat(messages): redesign native message admin page
- Rebuild users.php?page=aksrt_messages: overview stats, collapsible
  compose card (users list / role recipients), read-state views,
  type/recipient/keyword filters, bulk mark-read/delete, shared
  AdminListPagination
- Extract NotificationAdminQuery / AdminBroadcast / MessageTypes under
  src/Messages; page logic lives in UsersMenusMessages trait
- Page-scoped assets/css/admin/messages.css
- Remove dead aksrt_notification_type_label() and
  Message::get_all_notifications()

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-10-27 22:51:11 +08:00
kate522andClaude Fable 5 bbb41547e2 fix(messages): default message center slug to messages
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-10-27 22:28:57 +08:00
kate522andClaude Fable 5 5febdb35c8 feat(ticket): record urges in thread, show urge count in admin, mail admins
- aksrt_ticket gains urge_count / last_urge_time (schema 3.1.0)
- cooldown moves from transient to an atomic conditional UPDATE
- urge inserts a system reply rendered as a centered notice (not quotable)
- admin list badge + info card row for urge count / last time
- new ticket_urge_admin mail event via aksrt_ticket_urged

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-10-27 22:17:40 +08:00
kate522andClaude Fable 5 ac3ed9c956 feat(upload): pending lifecycle for upload-on-select images
Mark front-end uploads (comment/ticket/report) as pending, return the
attachment id, discard own pending uploads on remove via
DELETE /upload/pending/{id}, claim referenced URLs on successful submit,
and purge unclaimed uploads older than 24h with an hourly cron.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-10-27 21:54:48 +08:00
kate522andClaude Fable 5 a308f96837 fix(ticket): route attachment upload through UserController singleton
The static-array callable pointed at an instance method, which PHP 8
rejects as non-callable, so WP answered every upload with rest_invalid_handler.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-10-27 21:41:38 +08:00
kate522andClaude Fable 5 13bde11dda fix(ticket): rework detail modal layout and restore urge
- Detail modal: fixed-height dialog, thread is the only (thin) scroll area
- Collapsible pinned summary bar (collapsed by default, state kept across re-render)
- Composer: toolbar (image / urge / close) above input, pending image tray with remove
- Paste screenshots to upload; create form uses the same button + tray
- Urge/close now POST (routes are CREATABLE); urge cooldown countdown via urge_wait
- Notify admins on urge when ticket is unassigned
- Split composer/form styles into tickets-compose.css; drop dead uploader code

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-10-27 21:37:03 +08:00
kate522andClaude Fable 5 b74381cfa2 feat(ticket): redesign native ticket admin page UI/UX
- Fix empty status/department selects in the detail panel (printf leak)
- Replace dead 代客提交 button with a working collapsible create form
- Error-aware notices; redirect keeps filters and the open ticket
- Status views with counts, incl. 未关闭 default and 已关闭 view
- Two-column detail: conversation + attachments + prereplies | info,
  status/department/assignee select, internal notes; closed = read-only
- Split detail panel into UsersMenusTicketsDetail trait; page-scoped
  tickets.css / admin-tickets.js replace inline styles
- AdminListPagination accepts parent file so users.php keeps menu highlight
- Drop native loading="lazy" per lazyload contract

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-10-27 14:04:48 +08:00
kate522andClaude Fable 5 3bd15feccb feat(ticket): redesign user-center ticket center UI/UX
- Status stat cards double as quick filters; debounced search with stale-response guard
- Card list with status stripe, windowed pagination, skeleton/empty/error states
- Detail dialog as chat timeline with in-place refresh, quote preview, Ctrl+Enter send
- Create form with department chips, inline validation, multi-image upload, success notice
- Split shared helpers into ticket-center-core.js and enqueue it before the panel script
- Drop dead data-aksrt-ticket-config and legacy colorful modal header

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-10-26 23:06:34 +08:00
kate522andClaude Fable 5 e7ca0ee5df refactor(ticket): move the config tab into the SPA ticket center
users.php → 工单管理 的「配置」Tab 只管理部门 / 状态 / 预设回复三块
DB 字典,与 SPA「工单中心」已有的 8 个标量配置分裂成两处;且同一套
CRUD 在 admin_post 与 TicketConfigRest 里各写了一遍,REST 侧此前
没有任何调用方。

- 删除原生 config tab:UsersMenusTicketsConfig.php 整份移除,
  UsersMenusTicketsCenter 由 512 行降到 251 行(六个写处理方法、
  分发分支、十条提示码与 ticket_delete_button 一并清理)
- SPA /users/ticket 改为 customPage:内嵌 SchemaPage 承载原有 8 字段,
  三块字典改为 n-data-table + n-modal + n-dialog,取色器/管理员多选/
  拖拽排序替代原生手填表单
- TicketConfigRest 成为三块字典的唯一实现,新增 api/modules/tickets.js
  封装 aksrt/v1/admin/tickets*
- 双向深链:原生页跳 SPA 配置页,SPA 页跳回原生工单列表

契约测试同步更新,并新增 ticket-spa-config-contract 守护接线。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-10-26 19:48:58 +08:00
kate522andClaude Opus 5 d32bdc0581 fix(ticket): register the ticket schema inside the users sections array
/users/ticket loaded in the SPA but the form never did: the console showed
"未知的设置分组: ticket" from /settings/schema/users/ticket. The ticket section
was composed one array level too high in SchemaUsers::build() — as a sibling of
'sections' rather than inside it — so SchemaProvider::get_sections(), which reads
only $schema['sections'], never saw it and the REST handler 404'd.

Every other axis was wired correctly: the route, the menu entry, users/ticket.js
and SchemaUsersTicket itself. That is why only this page failed while sibling
/users/* pages worked. Move the entry inside sections.

The contract test could not catch this because it asserted only that the compose
call appears somewhere in the file. Replace it with a bracket-balanced scan that
locates the sections array bounds and asserts the compose call falls inside, so
nesting mistakes are visible.

Also update the two ticket contract tests that still read the trait from
src/Ticket/TicketRepository-traits/, its pre-flattening path. They assert on the
trait's contents, so they were silently skipping the very file the earlier fatal
was about.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-26 14:16:25 +08:00
kate522andClaude Opus 5 ffd8d7d1b7 fix(ticket): flatten TicketRepository traits to PSR-4-visible paths
TicketRepository fatalled on /user/tickets/ with "Trait AKSRT\Ticket\TicketQuery
not found". src/ classes load only via Composer PSR-4 (AKSRT\ => src/) — there is
no bootstrap require list for them — so the autoloader resolved the traits by
namespace to src/Ticket/TicketQuery.php and src/Ticket/TicketStatus.php. Both
files actually sat in src/Ticket/TicketRepository-traits/, one level deeper, and
were therefore invisible: PHP hit the `use` statement with no trait declared and
died the moment the repository compiled.

The nested directory name imitated the old inc/classes/**/*-traits/ layout, which
only worked because inc/bootstrap.php carried an explicit require_once for every
one of those files. No such mechanism exists under src/. Move both traits up to
sit flat as siblings of the class that uses them — the same shape as
GlobalControllerPart1, ModerationPart1 and the rest of src/. No code changes
inside the traits; their namespace was already correct.

Add tools/audit-psr4-paths.php, which checks every symbol under src/ against the
path Composer maps it to and exits 1 on mismatch. It flags this exact layout, so
the nesting cannot come back unnoticed. Document it in CLAUDE.md next to the
existing admin-contract audit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-26 13:04:46 +08:00
kate522andClaude Opus 5 dbb7de41e9 feat(ticket): replace the after-sale flow with a general ticket system
The old 售后 flow was an order-refund appendage: tickets were hard-bound to an
order number, users could only file them from 「我的订单」against a paid order,
and the admin action bottomed out at 「标记订单已退款」. It could not carry
account questions or feature requests that have nothing to do with an order.

Replace it with a general ticket system modelled on 魔方财务云's idcsmart_ticket:
title + department + description are required, linking an order is optional
(user's own paid orders only). Add departments, custom statuses, preset replies
and internal admin notes as four configuration surfaces. Conversation history
moves off the ticket row's JSON log into dedicated ticket_reply rows, which
unlocks quoted replies. Front end gets a 工单中心 panel under 我的服务; back end
moves from 支付中心 → 售后服务 to users.php → 工单管理.

Per CLAUDE.md's rewrite-over-compatibility rule: drop the old table, build new
ones, delete the old code. No dual paths, no data migration. The upstream
plugin's upstream/downstream delivery and internal-ticket conversion are
deliberately left out — both are multi-provider platform concerns with no
meaning on a single WordPress site.

Data: order_aftersale becomes six aksrt_ticket* tables (ticket, ticket_reply,
ticket_status, ticket_type, ticket_prereply, ticket_note). SCHEMA_VERSION
2.6.0 -> 3.0.0. Status and department seeds are idempotent; the default status
「待接单」is flagged as non-editable.

Domain: new src/Ticket/ (13 files, all under 460 lines). Attachments reuse
UserController::rest_upload_comment_image with a ticket-specific permission
callback, mirroring the existing /report/upload-image route rather than
growing a second upload path.

Notifications go through AKSRT\Messages\Message::send() and deep-link to the
right surface; email/SMS stay out of scope since the theme's mail and SMS
stacks are separate implementations.

Refund gate opens up: marking an order refunded no longer requires an in-flight
ticket, only that the order is paid.

Also generalise a hardcoded special case in tools/audit-admin-contracts.php —
schema domain inference now reads the parent directory instead of matching
"PostList" by name, so new leaf schema files need no per-name exception.

Verification:
  php tools/audit-admin-contracts.php   exit 0, errors=0
  node --test (widget-system + admin)   664 pass / 0 fail
  php -l over all new/changed files     clean
  node --check over all JS              clean
  npm run build + build:public          clean

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-25 23:29:51 +08:00
kate522andClaude Opus 5 761c84d70f feat(orders): replace order stat cards with a 售后中 filter
/user/orders 顶部的「已退款」「售后待处理」统计卡移除(统计行改为
全部 / 待支付 / 已支付 / 已关闭 四格,栅格同步由 3 列调为 4 列),
筛选的订单状态新增「售后中」,命中带有「待审核 / 处理中」工单的订单。

售后中与订单自身状态正交(售后中的订单可能仍是 paid,也可能已
refunded),因此不过滤 status 列,而是由 OrderCenterQuery 通过
repository_args 的 after_sale_open 下传,在 get_user_order_query_parts
中作为独立谓词叠加工单子查询,与类型、订单号条件自然组合。

工单流转口径复用 AfterSaleService 的 STATUS_PENDING / STATUS_PROCESSING,
与 find_active() 的退款闸门同一套判定;已完结(completed)与已撤销
(cancelled)不算售后中。

同时清理随之失效的死代码:UserOrders::get_user_order_stats()、
AfterSaleService::count_user_open()、aksrt_get_order_status_labels(),
以及两处 summary 查询与空视图中的 refunded / after_sale_pending 统计键。
不写双路与数据迁移,旧键直接替换。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-25 18:58:06 +08:00
kate522andClaude Opus 5 81b60eb427 fix(revenue): replicate the reference rebate poster canvas layers
The poster modal shell already matched the reference theme, but the canvas
drawing diverged from zibll's poster-share.js rebate branch in four places:

- No 推荐人 tags layer. The reference paints a red #fc4e1f 24px line at
  img_height+160; AKSRT dropped the layer entirely, so the poster lost the
  referrer attribution that identifies whose link it is.
- The logo slot painted text. The reference contains the real logo image
  into (80, height-190, 300, 100); AKSRT drew the site name as a 30px string
  plus a hardcoded 长按识别二维码访问 caption in that slot.
- Bottom copy was hardcoded. The reference reads share_desc; now sourced
  from the new pay_rebate_poster_desc_bottom field with share_desc fallback.
- No banner fallback. The reference walks banner → banner_default →
  banner_spare; a single broken URL left the top half of the poster black.

Also aligns the week label to the reference's 周 prefix (was 星期日) and ports
drawTextOverflow for wrapping/truncation, which retires the local wrapText.

Adds pay_rebate_poster_banner and pay_rebate_poster_desc_bottom to both the
Vue and PHP schemas; rest_me now returns logo, compatible and referrer_name.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-21 21:07:48 +08:00
kate522andClaude Opus 5 a120f61e13 fix(vip): render points exchange description
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-19 14:04:30 +08:00
kate522andClaude Opus 5 0d51beeaad fix(vip): add spacing before points exchange notice
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-19 13:22:04 +08:00
kate522andClaude Opus 5 573040381d fix(vip): preserve safe HTML in VIP copy
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-19 12:52:23 +08:00
kate522 064f4e0955 fix(admin): keep enum values in their declared type when sanitizing
The 积分兑换明细.level radio declares its options as the integers 1 and 2, but sanitize_field_value() ran every radio value through sanitize_text_field(), so the sanitized payload held the string 1. validate() then compared with in_array(..., true) and rejected both default rows, which made the VIP payment section impossible to save.

Resolve enum values to the value declared by their option during sanitization, covering select, radio, palette and image_select. A value that matches no option still falls through to the previous string coercion, so invalid input keeps producing a precise validation error. This also keeps the stored type aligned with the schema, which the naive-ui radio group compares with === when it re-renders a saved setting.

Verified with an end-to-end sanitize, validate, write and read round-trip, and locked in with a contract test in the VIP structured-settings suite. The widget-system, admin, front-submit and navigation suites all pass (658 tests).
2026-10-18 19:45:57 +08:00
kate522 05f16d5fd7 feat(vip): collapse the structured membership groups into accordions
The structured groups introduced in the previous commit rendered every row
fully expanded, which buried the fields an admin actually came to edit.
They now behave like accordions and start collapsed.

- 会员商品, 一级/二级会员续费 and 积分兑换明细 opt in via `collapsible`
  on the group schema. Each row shows only its header, carrying a summary
  line built from `listPreview` (价格:99.00 · 3 个月 / 积分:500 · 30 天),
  and expands on click. `defaultCollapsed` keeps the collapsed default;
  setting it to false restores the previous always-open behaviour.
- 升级价格 splits into its three reference-theme blocks
  (月费升月费 / 月费升永久 / 永久升永久) through a new `sections` option
  on the accordion type, each block independently collapsible.
- AksField keeps the existing open state keyed by index so adding or
  removing a row does not disturb the others, and the drag handle, delete
  button and chevron are excluded from the header toggle.
- Non-collapsible groups are untouched: the accordion markup only renders
  when the schema opts in.

Verified: php -l on the changed PHP, 656 Node contract tests passing
(including new assertions that every collapsible group is opted in, that
the upgrade sections cover exactly the declared fields, and that the SPA
mirror matches), and the admin SPA builds.
2026-10-18 17:51:28 +08:00
kate522 af8dccdc45 feat(vip): restructure membership settings into structured groups
Replace the pipe-delimited VIP configuration with structured groups and
accordions, matching the reference theme (Zibll) 1:1.

Settings:
- 会员等级 keeps only the nav buttons and the two level switches, plus the
  "must enable level 1 first" guard. Level identity fields (display name,
  intro, benefits HTML, icon) and 会员商品 move into the 一级会员/二级会员
  tabs of 会员购买选项, so each level is configured in one place.
- Level tab titles bind to the configured display name via a new
  titleKey/titleFallback hook in AksFormRenderer.
- 会员商品 is a repeater with exactly the reference fields: 执行价/原价/
  促销标签/标签颜色/会员有效时间/时长单位. The tag colour uses the full
  b+jb palette and now reaches the product card instead of a hardcoded
  jb-yellow.
- 会员续费 gains the 自定义商品 type backed by per-level renew-product
  groups; the discount becomes a 0.1-9.9 slider with float sanitizing.
- 会员升级 becomes the reference three-block accordion (month-to-month
  day-rate, jump to permanent, permanent to permanent), each with its own
  price, strikethrough price, promo tag and tag colour.
- 积分兑换明细 is a repeater (points/level/duration/unit) where the
  duration accepts the literal Permanent.

Runtime:
- PaymentRuntime normalizes the structured groups into purchasable rows
  (money normalization, palette validation, zero-price and out-of-range
  rows dropped) and no longer emits any *_raw strings.
- The catalog builder carries tag_class through and routes renewals to the
  renew-product group under 自定义商品.
- The points-exchange order note carries time/unit so month durations use
  calendar math instead of a 30-day approximation.

Dead code removed: parse_level_products, parse_packages,
parse_upgrade_product_line and their pipe-splitting, plus nine zero-call
PaymentRuntime proxy methods in HomeCms. No dual-format or migration path
was added; SchemaProvider already falls back to defaults when a stored
group value is not an array, so existing pipe strings degrade safely.

Verified: php -l on every changed PHP file, 655 Node contract tests passing
(14 new ones in vip-settings-structured-contract.test.cjs exercising the
real schema build and runtime normalization via a PHP harness), and the
admin SPA builds.
2026-10-18 17:14:00 +08:00
kate522 dcb80cff6e fix(vip): let 积分兑换说明 support HTML like the reference theme
The points-exchange desc was stripped at all three layers: no sanitize_type on the schema fields (save ran sanitize_textarea_field), the runtime re-ran the same strip, and the points modal escaped it with esc_html. The reference theme declares sanitize=false and concatenates the value raw into the modal.

- Schema (SPA + PHP): sanitize_type html, rows 2, help text noting HTML support

- PaymentRuntime: output via wp_kses_post

- VipServiceModal points modal: render via wp_kses_post instead of esc_html

Verification: new contract test covers schema/runtime/modal layers; node --test tests/**/*.test.cjs 641/641 pass; php -l clean on both PHP files.
2026-10-18 15:06:29 +08:00
kate522 2e2558e5ce fix(vip): render 开通会员更多内容 as HTML in the modal
Three layers were dropping the markup: the save path was already relaxed to wp_kses_post via schema sanitize_type html, but the runtime output re-filtered more through the narrow equity whitelist (h/img/hr/table stripped), and pay-vip.js inserted it with node() whose textContent showed the tags as literal text. The card-pass desc had the same output-side strip despite its save-side fix.

- PaymentRuntime: more and pay_vip_pass_charge_desc now output via wp_kses_post, matching the save-time level (repo precedent: revenue note, after-sale desc)

- pay-vip.js: new htmlNode() helper (innerHTML) for server-sanitized rich text; more and cardPass.desc use it, plain-text fields keep node()

Verification: 3 new contract tests cover output-level kses and innerHTML rendering; node --test tests/**/*.test.cjs 640/640 pass; php -l and node --check clean.
2026-10-18 14:53:42 +08:00
kate522 7f556892ad fix(vip): align modal copy fields with reference theme backend
Rename and reposition the two VIP modal copy settings to match the zibll reference backend, drop dead renew/upgrade desc config, and align copy defaults.

- pay_user_vip_desc: modal top intro -> 会员介绍, textarea -> input (one-line), zibll help text, default copy from zibll

- pay_user_vip_more: bottom note -> 开通会员更多内容, rows 4 + sanitize_type html (HTML was stripped on save), zibll help text and three-div default

- Both fields move from the member-level group to a new 界面文案 (modal-copy) group at the end of the VIP section; dependsOn any-of-two-levels adapted from zibll's pay_user_vip_1_s dependency

- Copy defaults synced in three places (SPA schema, PHP schema, PaymentRuntime fallbacks, since Settings::get_all() does not merge schema defaults): 黄金会员/钻石会员, 尊享专属特权, zibll four-item equity list

- Remove vip_renew_desc / vip_upgrade_desc: AKSRT has no renew/upgrade card render surface (render_vip_card takes no type) and zero consumers repo-wide; zibll renders them on page-level cards only

- pay_user_vip_1/2_equity and pay_vip_pass_charge_desc gain sanitize_type html so admins no longer lose list/HTML markup on save

Verification: new tests/widget-system/vip-modal-copy-fields.test.cjs (6 contract tests); node --test tests/**/*.test.cjs 634/634 pass; php -l clean on both changed PHP files; assets/admin npm run build succeeds
2026-10-18 14:30:15 +08:00
kate522 cb76b95b01 feat(vip): skeleton loading state and faster modal/site startup
The VIP purchase/renew modal opened with a single "loading..." line, which
reads as a hang while /pay/vip/modal round-trips. Every frontend pageview
also paid an option write inside wp_head, and the header user-card CSS was
enqueued even for guests who never see the card.

pay-vip.js now builds a full-layout skeleton (user box, level tabs, the
two-column package panel) from vipSkeleton(), reusing the global
aksrt-modal-runtime__skeleton line/block classes; the VIP-specific shapes
live in the conditionally loaded vip-modal.css, and the 640px two-column
threshold matches .pay-vip-form-row. The modal cache TTL goes 15s -> 60s
(purchase, card-pass redeem and points exchange already invalidate the
vip tag), and hover/focus on a .pay-vip trigger now prefetches the payload
through the same cacheKey and flight openRemote uses, so a warm click
opens instantly.

The bottom note and the whole help_text pipeline behind it are removed; the
admin-editable pay_user_vip_desc still renders at the top of the modal, and
the recharge/gateway help_text keys are untouched. get_catalog() reads
points and wallet balance through one new LedgerService::points_and_balance()
call. The StatsTracker counter write moves to shutdown so wp_head only does
the visitor check and setcookie, and header-userbox.css is enqueued for
logged-in users only.

Verification: node --test across the suite (628 tests, including the 7 new
contract assertions in tests/widget-system/vip-modal-skeleton.test.cjs) and
php -l on every changed PHP file. No visual acceptance performed, per request.
2026-10-18 13:44:01 +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&#038;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 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 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
kate522 cdab7a5e5f refactor(admin): drop AI connection config page for WP AI Connectors
Remove the "AI 连接配置" page under 扩展&增强 together with the theme's
own OpenAI-compatible provider path. AI generation (SEO, summary, tags)
now runs solely on the WordPress AI Client (wp_ai_client_prompt), with
connections configured through WordPress AI Connectors.

- AI.php keeps only the WP AI Client path: no provider config storage,
  no theme fallback, no rate limiting
- Drop /ai/status, /ai/settings and legacy /ai/seo-suggest routes plus
  their handlers
- Remove dead compat code: AISEO::suggest, AIPrompt::build_seo_prompt
  and the front-submit AI assist flow (its buttons were never rendered
  and its endpoints were never registered)
- Update SEO schema/meta-box guidance that pointed at the deleted page
- Add ai-config-removal contract test and refresh the toast and
  user-features contracts affected by the cleanup
2026-10-13 19:32:34 +08:00