Compare commits

...
167 Commits
Author SHA1 Message Date
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 Fable 5 ef15416ef7 fix(revenue): keep the poster modal download button circular
The blue download anchor rendered square while the adjacent yellow close
button stayed round. Both carry .but.cir, so the asymmetry had to come
from a rule matching only one of them.

credits.css:334 scopes .uc-content .but.jb-blue to a flat #3b82f6 skin
with border-radius:4px — that rule exists to stop the same class
flattening the top-nav publish chip and the site-wide login/submit/badge
buttons, so it stays. But the poster modal is emitted inside .uc-content,
and at (0,3,0) loading after main.css it out-ranks .poster-share
.but.cir's (0,2,0) on both counts, overriding the 5em pill with 4px on
the blue button only. jb-yellow is not in that selector, which is exactly
why its neighbour survived.

Restate the pill at matching weight, scoped to this modal. Padding comes
along because .uc-content .but.jb-blue also sets 0.45em 1em, which would
otherwise re-inflate the 36px circle. The 36x36 box and drop shadow stay
with .poster-share .but.cir in main.css.

Add a contract test for the radius restatement and the sizing it relies
on; verified it fails when the selector is renamed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-10-21 20:17:50 +08:00
kate522andClaude Fable 5 0cfd4c898b fix(revenue): drop 待偿欠款 from the commission card
The field rendered in two places on /user/revenue/: the 我的佣金 card's
meta line and the 累计佣金 row, both gated on a non-zero balance. Remove
both, along with the now-unused $debt_amount local and the dead
.uc-commission-card__debt rule.

Source trace, since the label is only the surface: CommissionLedger::
overview() reads debt_minor from aksrt_commission_debt and returns it as
'debt'; record_debt() books the shortfall when a refund cannot be clawed
back in full, and repay_debt_from_pool() later clears it out of the
commission pool. That mechanism is untouched — arrears still accrue and
still get repaid, the UI just no longer surfaces them. The reference
theme's rebate card has no equivalent concept, so this is not a parity
target either.

Keep 'debt' in the template's overview() fallback array: it mirrors the
real return shape and the repayment path still depends on the value.

Add a contract test asserting the label, its class and the local are gone
from the display layer while overview() keeps returning the key; verified
it fails if the label is reintroduced.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-10-21 20:08:34 +08:00
kate522andClaude Fable 5 9a22c7eb8b fix(revenue): stop poster modal from blocking the whole revenue page
Two regressions from the previous commit made /user/revenue/ unusable.

The rewrite dropped data-aksrt-affiliate-poster-modal from the modal shell.
That single attribute was both the JS lookup key and the CSS hook, so
initAffiliatePoster() bailed at `if (!button || !modal) return` — which is
why the 推广海报 button never got un-hidden — and no CSS could target the
shell. Restore it.

With the shell back but still visible, main.css's .flex{display:flex} loads
after bootstrap and matches .modal{display:none} on specificity, so the
resting modal rendered as a full-viewport flex box. Combined with .fade's
opacity:0 that is an invisible overlay swallowing every click on the page.
Pin the resting state to display:none and the open state to flex, the same
way credits.css already does for the charge and points-pay modals — that
file documents this exact trap.

Add a contract test asserting the markup, JS selector and CSS hook share
one attribute and that the resting state is explicitly hidden.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-10-21 19:26:48 +08:00
kate522andClaude Fable 5 de0e5cd481 fix(revenue): match reference poster modal UI 1:1
The 推广海报 modal on the revenue page used a bespoke .uc-modal card —
white panel, 12px radius, heavy shadow, title bar, corner × button and a
full-width 保存海报 button — plus a light-gray plate with its own
border-radius around the poster canvas. None of that exists in the
reference theme, so the popup read as bordered and off-spec.

Rebuild the modal on the reference poster-share structure: a borderless
360px .modal-dialog.poster-share centred by .modal.flex.jc, a
.loading-mask overlay while the canvas renders, and the download/close
buttons moved onto a .abs-center.hover-show-con layer that only fades in
on hover. initAffiliatePoster() now drives Bootstrap 3 modal semantics
(.in toggle, .modal-backdrop, body.modal-open, ESC and backdrop-click to
close), mirroring the existing openChargeModal/closeChargeModal pair, and
animates .modal-content from the 220px placeholder to the poster's real
height before clearing the inline styles.

No CSS added — every rule the reference modal needs was already ported.
Drop the now-dead .uc-affiliate-poster rules; the shared .uc-modal shell
and openModal/closeModal stay for the withdraw and transfer modals.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-10-21 16:02:41 +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 7a0445fc4a fix(admin): write group rows in place so drag keys stay stable
The previous fix inherited the drag key onto a replacement row object, but the row data is reactive: Vue hands the renderer a fresh proxy for every new object, so the WeakMap still missed and minted a new key on each keystroke. That remounted the row and dropped input focus after one character, so editing 执行价, 原价, 促销标签, 使用积分, 兑换时长 and 会员有效时间 was unusable.

updateRepeaterItem now writes through the existing row instead of spreading a new object, so the row identity survives across renders and the drag key stays valid. The emitted array is still a fresh array, so parents re-render normally, and rows that are not objects still fall back to a new object.

Verified in a real browser against the actual payment/vip schema: all six fields keep the same DOM node across repeated keystrokes, typed values reach the model, and add/remove still work. The contract test now simulates Vue reactive proxying and asserts row identity is preserved; it fails against the previous implementation. Full suite: 663 tests passing.
2026-10-18 21:59:06 +08:00
kate522 0bc72616e5 fix(admin): keep group row keys stable while editing fields
The inline group/repeater renderer feeds vuedraggable an item key from a WeakMap, because rows loaded from the server carry no _id. updateRepeaterItem rebuilt every row with a fresh object spread on each keystroke, so the WeakMap missed and minted a brand new key, which remounted the whole row and dropped the input focus after a single character.

Inherit the existing drag key onto the replacement row object so the key survives for the whole life of the row. Values that match no option still fall through to the previous coercion, so invalid input keeps reporting a precise error, and rows that already carry an _id continue to key off it.

Covered by a new contract test that extracts the real repeaterDragKey and updateRepeaterItem from AksField.vue and drives them through a simulated typing session. Full suite: 661 tests passing.
2026-10-18 21:15:19 +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 4b5098a753 fix(vip): restore the upgrade-price accordion and space out structured groups
升级价格 declared three reference-theme blocks through the accordion sections option, but the section builder handed the computed the whole field object instead of its sections array, so the guard always saw an empty length and the renderer silently fell back to a flat list. The three blocks ran into each other with no separation at all.

- Move the section expansion into a pure buildFieldsetSections() helper so the mistake is now covered by a real contract test that runs the actual schema through it, instead of a regex over the component source.
- Apply dependsOn to fields inside each block, so 天单价/促销标签 hide when 月费会员升级月费会员 is switched off, matching the flat path.
- Give structured groups (会员商品/续费商品/积分兑换明细/升级价格) their own bottom margin and widen the gap between accordion blocks, so two adjacent groups no longer sit flush against each other.

Verified: 657 Node contract tests passing, the admin SPA builds.
2026-10-18 19:24:00 +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 817ac9c647 fix(user-center): drop icon before unopened VIP status name
The status card always rendered the icon slot, so never-opened members saw a generic certificate glyph in front of 未开通会员 (level 0 has no configured icon and fell back to fa-certificate). Members with a real level keep their icon; only the level-0 case drops the slot.

Render the icon span only when vip_level_number > 0.

Verification: extended tests/widget-system/vip-tab-status-card.test.cjs (3 contract tests); node --test tests/**/*.test.cjs 638/638 pass; php -l clean.
2026-10-18 14:47:26 +08:00
kate522 e94ecdd7b1 fix(user-center): drop expiry row from unopened VIP status card
The status card always rendered the expiry row, so never-opened members saw a redundant 到期时间:未开通 line under the 未开通 state label. The reference theme outputs no expiry info for never-opened users (only active, permanent, and expired states carry one).

Initialize the expiry text empty and render the row only when the text is non-empty; active/permanent/expired branches are unchanged.

Verification: new tests/widget-system/vip-tab-status-card.test.cjs (2 contract tests); node --test tests/**/*.test.cjs 637/637 pass; php -l clean.
2026-10-18 14:42:10 +08:00
kate522 659b77719c fix(vip): keep 开通会员更多内容 inside the modal only
The user-center VIP tab rendered pay_user_vip_more in a muted box below the level cards, which the reference theme does not do: zibpay_user_page_tab_content_vip only renders pay_user_vip_desc in the status box, and pay_user_vip_more appears solely in the two modal builders (purchase and existing-member renew/upgrade). The page block stayed invisible while the setting defaulted to empty and surfaced once the zibll default copy landed.

Remove the page-level block; the modal view model and pay-vip.js right-column rendering are untouched.

Verification: regression test locks vip-tab.php free of the more block while the modal path keeps carrying it; node --test tests/**/*.test.cjs 635/635 pass; php -l clean.
2026-10-18 14:37:35 +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 319ec2f47f fix(nav): use reference theme sprite icons for header wallet tiles
The top-nav user card rendered its balance and points tiles with Font Awesome
glyphs (fa-money-bill-wave, fa-coins) while the reference theme paints the
multi-tone SVGs money-color-2 and points-color, so the two tiles read as flat
monochrome next to the rest of the card.

aksrt_header_user_wallet_mini() now resolves both value icons through
aksrt_icon_markup(), which is the same call the user-center sidebar uses for
the same two figures, so the header and the sidebar cannot drift apart:

  余额  fa-money-bill-wave -> zibsvg-money-color-2 (em14)
  积分  fa-coins          -> zibsvg-points-color  (em14)

The tile arrows stay on fa-angle-right because that is what the reference
renders there; only the two value icons were wrong. The muted-box geometry, the
c-blue-2 / c-yellow value colours and the data-aksrt-* live-update hooks are
untouched.

Verification: node --test across widget-system, navigation, admin,
asset-ledger, dev, front-submit and fixtures (621 tests), plus php -l. Rendered
the tile through a stubbed harness and diffed it against the reference markup:
identical structure, classes and icon. Contract-locked the two keys in
tests/navigation/legacy-navigation-contract.test.cjs and added a cross-surface
parity assertion to the sidebar icon contract.
2026-10-18 13:05:19 +08:00
kate522 4b18f7ef2c fix(user-center): replicate reference theme sidebar icons via SVG sprite
The user-center sidebar rendered Font Awesome glyphs while the reference theme
paints multi-tone inline SVGs, and several glyphs were simply wrong for their
slot: fa-chart-line served both 我的等级 and 推广收益, fa-money-bill served both
余额 and 积分, and fa-pen stood in for 创作收益.

The theme already ships every sprite key the reference sidebar needs in
assets/js/runtime/svg-icon.js, and main.min.js injects the <symbol id="icon-*">
set on every frontend page, so no new icon system was needed. The two grids now
use the reference theme's icon-but-box / user-icon-but-box shell (four per row,
em16 icon, 12px muted label) in place of AKSRT's tinted 38px pill, and every
remaining sidebar glyph and arrow resolves through aksrt_icon_markup():

  我的服务  我的等级 trend-color, 官方认证 user-auth, 我的订单 order-color
  功能设置  消息通知 msg-color, 个人资料 user-color, 账户安全 security-color
  资产卡    余额 money-color-2, 积分 points-color
  中心卡    创作收益 merchant-color, 推广收益 money-color
  其它      VIP 入口 vip_1, 抽屉触发器 menu, 箭头统一 right

Dead code went with the swap: the whole .uc-service-grid block including the
unused --cart and --revenue tones, the count badge (no caller ever set count),
and the tone key. The two card selectors that targeted <i> now also cover <svg>
so the icon sizes survive the element change, and the VIP arrow keeps
uc-sidebar-vip__arrow for its hover animation. level/profile/settings gained
their missing tab keys, which completes the icon-but-* class names and lets the
existing .is-active sync cover all six entries.

Verification: node --test across widget-system, navigation, admin,
asset-ledger, dev, front-submit and fixtures (620 tests), plus php -l on the
five changed PHP files. Contract-locked in
tests/widget-system/user-center-sidebar-icons-contract.test.cjs.
2026-10-18 12:55:30 +08:00
kate522 1461c9fe27 fix(user-center): show brand-colored icons in social bindings
Wrap the provider glyph in the reference theme's .social-login-item circular {type} wrapper on the /user/settings/ bindings list so each row paints the provider brand colour instead of a muted ink glyph. Aggregated clogin_* providers strip the prefix for the colour class while data-provider keeps the full key for the bind/unbind flow. Drops the now-unreferenced .binding-item__icon rules from the user-center settings and profile lists stylesheets.
2026-10-18 11:47:28 +08:00
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&#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 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
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
kate522 bd01199930 fix(navigation): restore header VIP CTA gradient 2026-10-13 17:33:01 +08:00
kate522 7ed739615e fix(navigation): align header wallet tiles with reference 2026-10-11 14:55:23 +08:00
kate522 60e4d67772 fix(frontend): match wallet tile icon colors 2026-10-11 13:49:20 +08:00
kate522 7287cfeaec fix(frontend): restore reference VIP expiry geometry 2026-10-11 13:31:02 +08:00
kate522 f03d72c880 fix(frontend): restore reference user popover button geometry 2026-10-11 13:14:13 +08:00
kate522 6e90a96a15 fix(frontend): align header checkin width 2026-10-11 12:40:37 +08:00
kate522 b368678662 fix(frontend): make header checkin button full width 2026-10-11 11:40:10 +08:00
kate522 40b9a3f36b fix(frontend): align the VIP expiry badge with the reference semantics
到期徽章改成参考主题 zib_get_user_vip_exp_date_text() 的语义:只有真的拿到到期时间才渲染这枚徽章,已过期显示「会员已过期」否则显示裸日期;「永久会员」只留给显式永久会员;拿不到到期数据时参考主题返回 false,整枚徽章都不渲染。

之前只要 expires 为 0 就无条件显示「永久会员」,于是订单来源且没有到期时间的会员也会多出一枚徽章,把会员条撑宽、内容被挤到换行,观感和参考主题对不上。

同时不再依赖 aksrt_get_vip_info():它的 expires_label 是另一套口径,返回值里也没有 is_permanent 字段。改读 aksrt_get_user_vip_state(),和等级判断共用同一个数据源。

用无头浏览器把参考主题卡片和本站卡片并排渲染,逐行量了 offsetHeight:总高 306px,user-info 43、会员条 37、到期徽章 20、签到 31、钱包 53、统计 27、操作行 81,两套完全一致;没有到期数据时会员条也一致。另外确认 vipbg-v2 在参考主题全部 59 个样式表里都没有定义,之前删掉它不影响任何视觉。

新增 3 条断言锁住到期逻辑,573 项测试全部通过。
2026-10-10 17:17:39 +08:00
kate522 6fd80be243 fix(frontend): match the reference check-in icon weight
签到按钮的图标改成参考主题的字重。参考主题用的是 fa fa-calendar-check-o,即 FA4 的轮廓版日历勾选;之前换成了 FA5 的 fas fa-calendar-check,那是实心填充的,观感不一样。现在改成 fa far fa-calendar-check,走 FA5 的 regular 字重恢复轮廓外观,同时仍然满足全局 FA5+ 图标系统的要求。

间距也跟着对齐:弹窗里的两个分支去掉 mr6,改回参考的做法,由 .but > .fa:not(.fa-angle-right) 的 0.25em 外边距加上图标后面的一个空格来撑开。用户卡片徽章分支保留 ml3 mr6,因为它不是 .but,拿不到那条外边距。

改之前核对了字体:解析 fa-regular-400.woff 的 cmap 表,确认 calendar-check(U+F274)确实在 regular 字重里,否则图标会直接消失不见。顺便把卡片用到的全部图标逐个比对了 solid 与 regular 的覆盖情况,包括之前修过的 gem 水印(U+F3A5),都在。

main.css 里本来就有 .fa.far 的 font-weight 400 规则,不会被 solid 权重盖掉。新增 4 条契约断言锁住图标字重与间距,573 项测试全部通过。
2026-10-10 16:58:01 +08:00
kate522 855e85e7db feat(frontend): restore the posts count badge and drop dead vipbg-v1 rules
统计行补回「文章」徽章。上一轮按「项目不做帖子」的要求把它删掉了,现在这个判断反过来了,于是恢复:位置回到参考主题的顺序(文章在最前),计数仍走 count_user_posts,图标改用全局 FA5+ 的 fa-file-alt,与其余徽章(评论 / 点赞 / 人气)保持一致。

删掉 main.css 里 .vipbg-v1 与 .vipbg-v1:before 两条死规则。全站没有任何地方使用这个类,而且 :before 里的 f006 是 FA4 码位,在 FA5 中映射不到任何字形,留着只会让人以为水印能出来。

两个契约测试里原本断言统计行不输出文章徽章,改回断言输出,并补上 fa-file-alt 的校验。

重建 main.min.css;public bundle 本次未变,因为它打包的 header-userbox.css 没有改动。573 项契约测试全部通过。
2026-10-10 16:42:16 +08:00
kate522 7e7bf6db23 fix(frontend): restore reference VIP watermark and action-row geometry
会员条右下角那枚淡淡的 gem 水印一直没渲染出来:.payvip-icon:before 留着参考主题的 FA4 码位 f219,但本站字体栈是 FA5,该码位在 FA5 里映射不到任何字形。改为 f3a5(fa-gem,Solid 字重),这正是参考主题水印的实际内容。

操作行(用户中心 / 我的订单 / 支付中心 / 后台管理)的几何被 header-userbox.css 里的自有规则盖住了,而 main.css 里本就有参考主题的对应规则:瓦片被强制成 flex 1 1 0 加 min-width 0,容器加 4px  gap,标签还被截断。删掉这些覆盖,只保留 max-height none 这一处对全局导航链接 50px 高度上限的豁免,让 .header-user-href > a(inline-block、padding-top 10px、min-width 58px)和 .header-user-href .toggle-radius(36px)重新接管。

同时删掉 360px 断点下把图标瓦片缩到 34px 的规则,参考主题没有对应物;移动端宽度对齐参考主题的 auto。

排查中确认 main.css 整体是对参考主题的忠实移植:93 条布局工具类、8 条 padding 工具类、.sub-user-box / .header-user-href / .msg-news-icon / .payvip-icon 规则以及颜色变量均逐条一致,因此上述两处就是仅有的缺口。另外核对 f3a5 在随包 FA5 Solid 字体中可解析,fa-solid-900.woff2 确实存在。

重建 main.min.css 与 public bundle,仅这三个构建产物发生变化,JS bundle 字节级一致。573 项契约测试全部通过。
2026-10-10 16:37:01 +08:00
kate522 6f815678ec feat(frontend): replicate reference header user box 1:1
顶导航点击小人弹出的用户信息浮层,从 AKSRT 定制结构改为与参考主题 zib_header_user_box() 逐行同构:头像链接包裹、钱包双瓷砖、会员条、签到、消息图标、统计徽章行全部改用参考主题遗留类,渲染交回 main.css(.sub-user-box 242px、.avatar-img 38px、.msg-news-icon、badge.top、.payvip-icon 深色渐变加 gem 水印、.vip-expdate-tag、.muted-box)。

头像改走 aksrt_get_avatar_box(用户ID, avatar-img, true, false),得到参考的 a 链接包裹 avatar-img 结构。消息图标改参考标记 msg-news-icon abs-right 加 toggle-radius msg-icon,未读徽章用 badge.top,avatar_dropdown 位置门控契约保留。管理端行在「后台管理」前插入「支付中心」,跳 admin.php?page=aksrt_pay,即参考主题「商城中心」槽位,图标 fa-credit-card。

钱包重写为参考双瓷砖:余额在前(c-blue-2 加 fa-money-bill-wave)、积分在后(c-yellow 加 fa-coins),分别按充值开关与积分总开关门控,两块皆关则不渲染,并保留 data-aksrt-wallet-value 与 data-aksrt-points-value 两个 JS 刷新钩子。会员条去掉 AKSRT 自创的 vipbg-v2 level-N 金色背景,改参考的 payvip-icon 加 badg jb-yellow vip-expdate-tag,到期文案按参考语义分为已过期、永久会员、裸日期三种。签到入口由 button 改为 a.but block c-yellow,并去掉随之失效的 compact 形参,data-signin 挂载与 wp_footer 验证码不变。统计行删掉文章徽章(本站不做帖子),图标由 SVG sprite 换 FA5+ 的 fa-comment-dots、fa-thumbs-up、fa-fire。

会员等级图标契约不动:aksrt_get_vip_icon 与 User\Level::get_badge_html 的调用与参数保持原样。

顺带清死代码:aksrt_header_user_badges_row 原本取到函数后直接返回空串,现在真正输出统计行;删掉 header-userbox.css 里整套 bespoke 规则与 vip-modal.css 的 vipbg-v2 死规则。

显式偏差:统计行链接保持扁平用户主页 URL(AKSRT 用户主页是干净 URL 加 JS tab,没有 query tab 路由);签到入口沿用积分签到总开关门控,不新增管理端可配置项。

public.css 与 public.min.css 用 tools/build-public-assets.mjs 重建:该 bundle 打包了 header-userbox.css 且作为依赖先于单模块入队,旧的 width 260px 会压过新源码、把卡片钉在 260px 而不是参考的 242px。

验证:node --test tests/**/*.test.cjs 573 项全通过;六个改动 PHP 文件 php -l 无语法错误;卡片用到的 FA5 token 逐个核对 5.15.4 全部命中。未做真实视觉效果验收。
2026-10-10 16:17:01 +08:00
kate522 8ff74f56ee fix(admin): use FA5 long-arrow icons in post list sort
后台文章列表的排序箭头还停留在 FA4 的 fa fa-long-arrow-down / up,而且类名是字符串拼接出来的。契约测试按完整的 fa fas fa-* token 逐个校验,看不穿拼接后的类名,所以这条一直是漏网之鱼。

改为按升降序直接输出完整的 fa fas fa-long-arrow-alt-down / fa-long-arrow-alt-up 两个字面量,走全局 FA5+ 图标系统。

验证:node --test tests/**/*.test.cjs 全量 572 项通过;php -l src/Admin/Native/PostListEnhance.php 无语法错误。
2026-10-10 16:15:48 +08:00
kate522 a3b708f9d4 fix(frontend): keep user center and user home avatar frames rounded
The two large hero avatar frames go back to their legacy rounded-square look: the user center profile header keeps 6px and the public user home hero keeps var(--radius). Both declare an explicit radius because the global avatar primitive (img.avatar, 0,1,1) ties them on specificity and only the later route rule wins the cascade.

Everything else stays: the primitive still circles the bare avatar-mini in the ban and report modals, the VIP modal avatar and the post-card meta avatar, the friends page avatar is circular, and the VIP icon still leads the badge row. The contract test now guards the two hero frames as rounded-square so the primitive cannot silently re-circularize them.
2026-10-10 13:59:47 +08:00
kate522 9cb5e7bea3 feat(frontend): unify avatar shape, move VIP icon to badge row
Make --radius-avatar the single source of truth for frontend user avatar shape. A new primitive in components-ext.css (img.avatar / avatar-mini / avatar-md / avatar-lg / .avatar-img img) forces a circle on every avatar image, which covers the bare avatar-mini in the ban and report modals, the classless img inside the VIP modal's .avatar-img, and the post-card meta avatar that previously depended on post-card.css being enqueued. The user center header frame, the public user home hero and the friends page avatar switch to the same token because route CSS loads after the public bundle and would otherwise override the primitive.

The VIP icon no longer rides the avatar corner. The user card widget, the after-article author card and the sign-in modal stop opting into aksrt_get_avatar_box()'s corner badge, and the icon renders first in the badge/level row instead, since aksrt_get_user_name already orders name -> VIP -> auth -> medal -> level. The sign-in modal appends it after the display name and keeps the legacy precedence where a ban badge takes the VIP slot. The user card widget name row now shrinks its display name so the extra icon cannot overflow a narrow sidebar.

Verified with the new frontend-avatar-shape-contract test plus post-visual, user-card-runtime, circular-button-icon-margin, user-features and page-display contracts, and php -l on every changed PHP file. The public bundle was rebuilt with tools/build-public-assets.mjs.
2026-10-10 13:53:07 +08:00
kate522 16b57dcf1c fix(after-sale): 前台售后不再展示「退款金额」字段
并非每次售后都真的退款——换货、补偿、仅作记录的情况都会把工单标成已完成,
此时退款金额要么是 0 要么没有实际意义,摆在前台只会让人误解钱已经退回去了。
两个前台入口都撤掉该字段:

- 售后详细弹窗:明细行只留「申请时间」,处理记录时间线不受影响
- 订单详情弹窗的售后记录卡片:撤掉退款金额后 meta 的 dl 会空壳,连同
  dl 一起移除;卡片仍保留状态徽章 + 申请时间 + 原因 + 「查看详细」

连带清理死代码:aftersaleAmountText / afterSaleAmount 两个格式化助手各自
只剩这一处调用,orders.css 里 5 条 .aksrt-order-detail__after-sale-meta
规则也随之无人引用。后台售后页与订单明细页的退款金额列不动——那边是运营
核对账目的口径,需要看到金额。

验证:tests/widget-system/ 462 项、tests/admin/ 76 项通过;抽取
buildDetailHtml 跑样例载荷,确认退款金额不再出现、申请时间 / 处理记录 /
再次申请售后按钮都还在。未做浏览器视觉验收。
2026-10-10 13:04:37 +08:00
kate522 1875470ac7 fix(after-sale): 售后工单已完结仍可再次申请,改由订单是否已退款决定
上一个提交把「工单 completed」当成了不能再申请售后的判据,方向搞反了:
能否再售后应该看订单有没有被标「已退款」,而不是看工单状态。订单已退款时
status_group 变成 refunded、enabled 直接为 false,那道闸门本来就够用;用工单
状态再判一次,反而把「工单已完结但订单没退款」的用户堵死,售后详细弹窗底部
也只剩下一个「我知道了」死胡同。

- reapplyable_statuses 补上 STATUS_COMPLETED:rejected / cancelled / completed
  一律放行再次申请,售后详细弹窗底部按钮对已完成订单渲染「再次申请售后」
- can_apply 还原为只看有没有进行中的工单(!open),不再二次校验工单状态
- create() 的重复申请闸门还原为 find_active():只挡仍在流转的工单,
  completed 不挡;订单已退款由上面的 STATUS_PAID 校验先挡掉
- 点「再次申请售后」后清 has_application 落回申请表单,提交走 create()
  新建工单,链路本来就通,无需改动

harness 场景名改为 settled_ticket_still_allows_reapply,断言 completed /
rejected / cancelled 三种已终结状态都能新建工单;待审核工单同因幂等、换因
挡住的场景保留。

验证:tests/widget-system/ 462 项、tests/admin/ 76 项通过;after-sale-log-harness
7/7。未做浏览器视觉验收。
2026-10-09 18:38:18 +08:00
kate522 c955fd35f2 fix(after-sale): 售后工单已完结即不可再申请,不看订单是否另标已退款
原来的重复申请闸门只看「进行中」的工单(find_active = pending/processing),
于是工单已 completed、但订单没被另标「已退款」时,订单还能再发起一次售后。
已退款订单之所以不能再申请,是因为 status_group 变了导致 enabled 为 false,
属于间接效果;工单层面的规则一直是漏的。

- create() 改为以 latest_for_order() 为准:completed 直接挡掉;待审核/处理中
  仍走同人同因幂等(返回原工单),rejected / cancelled 放行再次发起
- after_sale_state() 的 can_apply 同步收紧,与 can_reapply 同一口径:
  有工单时只有 rejected / cancelled 才放行,completed 不再漏过去
- 界面上售后详细弹窗的「再次申请」本就只对 rejected / cancelled 渲染,
  这条改动把服务端与 API 口径补齐,堵住绕过前端直接打 REST 的口子

harness 补两个场景:completed 挡住而 rejected / cancelled 放行;待审核工单
同因幂等返回原单、换因挡住。

验证:tests/widget-system/ 462 项、tests/admin/ 76 项通过;after-sale-log-harness
7/7(新增两场景);dev harness 渲染两个后台页面与 unchecked 处理场景均正常。
未做浏览器视觉验收。
2026-10-09 18:25:02 +08:00
kate522 36b3ba6f45 fix(after-sale): 订单详情弹窗不再渲染售后状态节点进程
订单详情弹窗的「售后记录」卡片把整条处理时间线铺在了卡片里,和点开
「查看详细」后的售后详细弹窗重复,卡片本身也被拉得很长。改为只保留状态
徽章 + 申请时间、原因/申请理由、退款金额与「查看详细」入口;完整处理记录
由售后详细弹窗独占呈现,信息不缺、入口也还在。

- 删掉卡片里的 orderDetailHandlingLogHtml(item.handling_log) 调用
- 连带删除随之无人调用的 orderDetailHandlingLogHtml / handlingEntryTitle /
  AFTER_SALE_STATUS_LABELS 三个助手
- recordPayload 仍带 handling_log,点开详情弹窗照旧能看到时间线
- modal-runtime.css 的时间线规则不动:售后详细弹窗还在用,无需重打包

验证:tests/widget-system/ 462 项、tests/admin/ 76 项通过;契约测试改为断言
订单详情弹窗不再出现 timeline 标记与三个助手,同时锁住「查看详细」载荷仍带
handling_log;node --check 通过。未做浏览器视觉验收。
2026-10-09 18:12:48 +08:00
kate522 039fa6d863 feat(after-sale): 处理记录时间线改为「节点 + 状态 + 时间」同一行
原实现把状态与时间竖排在节点右侧的 88px 固定列里,状态文字一长就折行、
时间被挤到第三行,和右侧备注也对不齐。改为节点独占一列,状态与时间包在
head 里紧随其后,处理备注另起一行:

- [节点][处理状态][时间] 一行,状态色仍按工单状态走 --as-accent
- 竖线改挂到条目的 ::before(原来是 aside 的),top 从 12px 调到 14px,
  与加了 4px 上边距的节点底部对齐,绳子依旧穿过每个节且末项不收口
- head 用 flex + baseline + gap:8px,状态过长时时间自然换行而不重叠
- 售后详细弹窗与订单详情弹窗共用同一套标记,两侧同步改

验证:tests/widget-system/ 462 项、tests/admin/ 76 项通过;用 node 分别抽取
两个 JS 里的时间线构造函数跑样例载荷,确认输出为
[节点][状态][时间] + 另起一行的备注;重建 public.css / public.min.css,
diff 仅限时间线规则。未做浏览器视觉验收。
2026-10-09 18:01:16 +08:00
kate522 26ce9a5904 feat(after-sale): 售后处理记录改为每次提交留痕并新增时间线
工单行内新增 handling_log 列,用户申请、用户撤销与后台每次改状态都落一条
「状态 + 备注 + 来源 + 时间」,按时间正序存放、上限 50 条(对齐
MetaAssetService::MAX_RECORDS)。set_status 因此变成读-改-写,补上
GET_LOCK/RELEASE_LOCK 包裹,并在锁内重读工单快照——锁外取快照仍会被并发
追加覆盖。

三处界面由「只显示最后一条」改为回放整条处理链路:

- 后台订单明细「售后记录」按工单渲染头部(状态/金额/申请时间)+ 原因 +
  完整处理记录
- 售后面板手风琴「处理备注」单独占整行并置于「申请说明」上方,删掉已无
  意义的「审核时间」
- 前台售后详细弹窗与订单详情弹窗的售后记录改为时间线:左列绳结状态条
  (节点 + 状态 + 时间),右列处理备注;样式只在 modal-runtime.css 定义
  一份,两个弹窗共用

清理随之失效的死代码:工单的 admin_note / reviewed_at / completed_at,以及
全仓库无写入方的 gateway_request_id / gateway_refund_id / gateway_result;
连带删掉 gatewayResultText()、uninstall 里从未存在过的 aksrt_after_sale
表项,并把两处后台页面重复的状态标签数组收成 after_sale_status_labels()、
重复的 .aksrt-as-log 样式收成 after_sale_log_css()。

顺带修复 88669f69 重构退款流水线时误删 refund_amount()、却仍在 create()
里调用的问题——此前任何用户提交售后都会直接 fatal。

验证:tests/widget-system/ 462 项、tests/admin/ 76 项通过;新增
after-sale-log-harness.php 驱动真实 AfterSaleService,覆盖首条日志、空备注
也留痕、撤销记录、50 条上限与畸形行规范化;两个后台页面经 dev harness
渲染校验「处理备注」宽行位于「申请说明」之前;未做浏览器视觉验收。
2026-10-09 13:59:50 +08:00
kate522 31e6ba00af fix(after-sale): 售后举证缩略图去掉原生 loading 属性
lazyload 契约测试全仓库禁 loading=lazy/eager,售后服务 accordion 详情里的
举证缩略图带着该属性,是唯一一处违规(80373746 引入,早于本次分页改造)。

去掉它:后台页面不加载 lazySizes 运行时,原生 loading 也不会被主题那套
接管;与 UsersMenusVerify 的后台缩略图保持一致(光 <img src>)。

验证:tests/widget-system/ 459/459 通过(此前 458/459)。
2026-10-09 12:04:48 +08:00
kate522 89ae462610 Merge pull request 'fix(admin): 支付中心分页条改回四按钮式' (#39) from worktree/pay-pages-pagination-style into main
Reviewed-on: #39
2026-10-09 03:56:25 +00:00
kate522 afd08885fc fix(admin): 支付中心分页条改回四按钮式
AdminListPagination 的输出标记从 WP 原生 paginate_links() 换回「卡密管理」
原版的四按钮式:`« ‹ 第 X 页,共 Y 页 › »` +「每页 X 共Y」计数,没有数字页码
链接。七个页面调用点不变,外观统一为卡密管理那一套。

- 删除 $total_pages <= 1 早退:只要总数 > 0 就渲染分页条,单页时四枚按钮全
  disabled —— 与原版 render_pagenavi 行为一致
- 基准 URL 与页码夹取沿用现有实现(显式拼装、夹到 [1, total_pages]),
  这两处缺陷修复不动
- 标题属性由 esc_attr_e() 改为 esc_attr__():输出相同,且同文件与 dev harness
  都只用后者
- 新增私有静态 print_assets(),由 render() 调用并以 $assets_printed 保证每页
  只输出一次;内容只有 .tablenav-pages a.disabled { pointer-events: none; }
  及其注释。这些原生后台页面不加载主题 CSS,故随标记内联

清理 harness 中已无人调用的 number_format_i18n() 与 paginate_links() stub。

验证:tests/admin/ 76 项通过;tests/widget-system/ 458/459(仅剩与本次无关的
lazyload-contract 既有失败);用 git show HEAD~1 取出原 render_pagenavi() 与
新助手在 8 组输入(含单页、总数为 0、per_page 为 0、页码越界)下比对,规范化
空白后输出逐字节一致;harness 渲染售后面板确认四按钮标记与内联样式就位。
2026-10-09 11:49:16 +08:00
kate522 536ac67f3c Merge pull request 'feat(admin): 支付中心列表页统一为 WP 原生分页' (#38) from worktree/pay-pages-pagination into main
Reviewed-on: #38
2026-10-08 14:23:19 +00:00
kate522 7b83893cf6 feat(admin): 支付中心列表页统一为 WP 原生分页
七个支付中心列表页(订单明细 / 售后服务 / 付费文章 / 优惠码 / 创作分成 /
提现审核 / 卡密管理)的分页收敛到共享助手 AdminListPagination,改用 WP 原生
paginate_links() 渲染。

- 基准 URL 显式以 page 参数 + 当前筛选参数拼装,不再让两个参数的
  add_query_arg('paged', ...) 退回 $_SERVER['REQUEST_URI']:链接不再随当前
  请求形态漂移,列条件也不再丢失
- 售后服务从写死的 limit => 100 改为 limit/offset 分页,总数接上原本零调用的
  AfterSaleService::count();after_sale_status 先按状态常量归一,保证 count()
  与 admin_list() 口径一致,分页条不说谎
- 每页 20 条,统一来自 AdminListPagination::DEFAULT_PER_PAGE
- 每页在查询前夹取页码,越界时回退到最后一页并重查,杜绝「空表且没有分页条」
  的死胡同;页码夹取与早退也一并收敛到助手
- 提现审核的翻页链接排除 retry_id / retry_key 两个一次性动作参数
- 订单明细的标签页与翻页链接共用同一份筛选参数,不再各写一份

清理死代码:五个页面里近乎复制的 render_pagination()(其中订单明细与提现审核
两份从未被调用)、两页重复的内联 paginate_links 块、卡密管理的 render_pagenavi()
及其 docblock、PayChargeCards::DEFAULT_PER_PAGE 常量,以及只服务自定义禁用
按钮的 .tablenav-pages a.disabled 规则。

验证:tests/admin/ 76 项通过(新增 pay-pages-pagination.test.cjs,重写
charge-cards-pagination.test.cjs);用 git show HEAD 取出原文件对比,订单明细
与售后服务两页在新旧实现下渲染输出逐字节相同。
2026-10-08 22:22:08 +08:00
kate522 83c164a427 feat(admin): 订单明细页已支付订单新增「退款」按钮
操作栏「详情」右侧为已支付订单渲染「退款」按钮:点击经 aksrt_pay_refund_order 把订单推到已退款。

退款沿既有的受闸路径执行:OrderRepository::update_status 要求 allow_refund,且 can_finalize_order_refund 要求该售后工单仍为待审核/处理中。因此工单号由服务端按订单号反查 find_active 得到,客户端只提交订单号;没有进行中的工单时整次操作中止并给出可操作提示,不会出现订单已退款但无工单依据的状态。

按钮仅在已支付订单上渲染——已退款是已支付的唯一后继状态,其他状态点击也只会在服务端被拒。附带清理 AfterSaleService::find_active 上方一处早已错配的 @param 文档注释(描述的是 \,与该函数签名无关)。
2026-10-08 21:14:56 +08:00
kate522 5fd8c6fc31 feat(after-sale): 售后面板新增「标记订单已退款」勾选项
展开的操作面板在状态下拉与备注旁新增「标记订单已退款」勾选框:勾选后点击确认,会先把关联订单推到已退款,再更新售后工单状态。订单本已退款时勾选框呈现为不可改的已选状态,且不提交该标记。

顺序不能反:OrderRepository::update_status 的退款闸门要求售后工单仍为待审核/处理中,一旦 set_status 先执行就不再放行。因此退款失败时整次操作中止,避免出现「工单已关、订单未退」的半完成状态。订单号由服务端按 after_sale_id 反查,不经客户端传递。

AfterSaleService::get() 由 private 改为 public 以支撑该反查。dev harness 扩展为可驱动 handler 的六个场景,覆盖勾选/未勾选、重复退款、非已支付订单、已关闭工单与非法状态。
2026-10-08 21:03:23 +08:00
kate522 b733e41993 fix(admin): 售后展开面板删除渠道结果并铺满状态面板宽度
展开详情去掉「渠道结果」字段,并清理其已无人读取的 gateway_result / gateway_text 解析。详情字段栅格由 auto-fill 改为按断点固定列数:普通字段 10 项可被 2 / 5 整除,配合整行的「申请说明」,行尾不再留白。状态面板去掉 max-width:720px,改为整行 flex——售后状态下拉与备注横向排布占满左侧,取消/确认贴右端,消除右侧大片空白。
2026-10-08 19:01:56 +08:00
kate522 88669f6989 refactor(after-sale): replace review decisions with direct status panel
售后面板改为状态下拉(待审核/处理中/已完成/已拒绝/已取消)加确认/取消按钮:取消仅收起手风琴,确认为纯状态更新;删除已通过状态、review() 决策机制及其专用退款流水线,无双路无迁移;举证图片并入申请说明列与详情整行。
2026-10-08 11:05:08 +08:00
kate522 80373746f8 feat(admin): 售后列表改为手风琴展开详情并内联审核表单 2026-10-07 23:38:30 +08:00
kate522 d2034f5031 test(admin): 契约覆盖点击展开的操作面板与批量操作 2026-10-07 08:46:43 +00:00
kate522 ad2290cbb7 feat(admin): 售后操作列改为点击「操作」展开快捷面板;新增顶部批量通过/批量拒绝(含复选框与全选) 2026-10-07 08:46:41 +00:00
kate522 c870f08395 test(admin): 契约覆盖售后快捷操作 2026-10-07 08:42:09 +00:00
kate522 80fb4068d1 feat(admin): 售后操作列改为 WP 文章列表式 row-actions 悬停快捷操作(JS POST 提交,拒绝时 prompt 填备注) 2026-10-07 08:42:07 +00:00
kate522 01cd734989 test(admin): 契约覆盖举证图片列 2026-10-07 08:36:05 +00:00
kate522 f44180d06c feat(admin): 售后服务表格新增「举证图片」列,展示申请上传的图片缩略图 2026-10-07 08:36:03 +00:00
kate522 c7d18d426c test(admin): 契约覆盖售后表格新列与策略摘要 2026-10-07 08:32:16 +00:00
kate522 0ec405f566 fix(admin): 售后服务页表头对齐售后策略配置项:原因/申请资料/申请说明分列,页头展示策略摘要 2026-10-07 08:32:14 +00:00
kate522 dd320c99c1 test(admin): 契约测试同步菜单结构调整(无 Tab/独立售后菜单) 2026-10-07 08:28:30 +00:00
kate522 fc75d7b810 chore(admin): 删除统计页专用的 get_admin_order_stats(已无调用) 2026-10-07 08:28:27 +00:00
kate522 b3012b3133 feat(admin): 订单明细页去掉 Tab 分页;售后独立为「售后服务」子菜单;删除统计页 2026-10-07 08:28:25 +00:00
kate522 ab6c2339a0 feat(admin): 支付中心新增「售后服务」子菜单 2026-10-07 08:28:23 +00:00
kate522 3eb9cc739c style(modal): 售后与订单弹窗底部按钮满幅化并优化悬停反馈 2026-10-07 10:18:48 +08:00
kate522 e9a1a75259 fix(styles): 还原弹窗 footer 相邻按钮分界线并补悬停阴影
被平铺规则的 border:0 抹掉的 legacy 分隔线按家族色还原,售后四弹窗与订单详情同款;更新构建产物与契约测试。
2026-10-07 09:53:10 +08:00
kate522 d69e1d33c3 fix(orders): 修复再次申请售后被 has_application 分流回售后详细的问题并对齐底部按钮观感 2026-10-07 09:43:22 +08:00
kate522 8023e2e472 feat(after-sale): 支持撤销售后工单并接入人机验证 2026-10-07 09:20:59 +08:00
kate522 918ca371be chore(repo): 行尾规范化,恢复索引 LF 契约
远端 9 个文件以 CRLF 入库,违反 .gitattributes「索引一律 LF」声明,
在 autocrlf=true 的工作区产生永久假脏并阻塞合并。仅行尾变化,无内容改动。
2026-10-07 09:13:42 +08:00
kate522 9e3267fef4 test(payment): 契约测试同步弹窗底部与记录卡片新行为 2026-10-06 23:50:33 +00:00
kate522 6081e8dcc9 style(payment): 订单详情底部适配主题按钮网格;记录卡片新增样式 2026-10-06 23:50:31 +00:00
kate522 c801ff5047 feat(payment): 订单详情底部改用主题按钮样式;记录卡片加查看详细与原因/申请理由 2026-10-06 23:50:30 +00:00
kate522 8460132feb feat(payment): 售后详细弹窗底部精简为单个主按钮,去掉阻断文案 2026-10-06 23:50:28 +00:00
kate522 732f615a09 style(payment): 炫彩标题区样式扩展到订单详情弹窗 2026-10-06 18:27:27 +00:00
kate522 738abe5102 style(payment): 订单详情弹窗 modal-mini 几何(400px) 2026-10-06 18:27:25 +00:00
kate522 915311dcb6 feat(payment): 订单详情弹窗统一为 modal-mini + 蓝色炫彩标题区 2026-10-06 18:27:23 +00:00
kate522 c200427358 test(payment): 售后契约测试同步结构化表单与可再次申请新行为 2026-10-06 18:08:08 +00:00
kate522 7b5dcf16a1 style(payment): 订单详情弹窗售后记录卡片样式 2026-10-06 18:08:06 +00:00
kate522 2db2414270 style(payment): 订单详情弹窗售后记录改为卡片式 UI 2026-10-06 18:08:04 +00:00
kate522 365b2eafe5 feat(payment): 售后详细弹窗申请表单分行展示(原因/申请资料/申请说明) 2026-10-06 18:08:02 +00:00
kate522 ea88499996 feat(payment): schema 版本升至 2.5.2(apply_form 列迁移) 2026-10-06 18:08:00 +00:00
kate522 f4ff3e16d4 feat(payment): schema 2.5.2 新增 order_aftersale.apply_form 列 2026-10-06 18:07:58 +00:00
kate522 c67d5ae4bc feat(payment): 可再次申请售后按工单状态判定(rejected/cancelled 可再次申请) 2026-10-06 18:07:56 +00:00
kate522 00a087b1a4 feat(payment): 售后申请表单结构化入库(预置原因/申请资料/申请说明独立存储) 2026-10-06 18:07:53 +00:00
kate522 a0f59826fe fix(payment): 售后弹窗滚动容器改用 no-scrollbar,消除弹窗边缘滚动栏
mini-scrollbar 是 3px 可见细滚动条(举报弹窗 600px 宽弹窗用的写法);
modal-mini 这类带炫彩横幅的表单弹窗,主题统一走禁封申诉/记录弹窗的口径:
scroll-y + no-scrollbar 完全隐藏滚动条(含细滚动条)+ 50vh 内层滚动。
申请与售后详细两个滚动容器一并切换,滚动行为不变、只是不再露出滚动栏。
2026-10-06 23:38:18 +08:00
kate522 e7e281f10c fix(payment): 售后入口监听改为脚本加载即挂载,去掉 auto_fun 时序依赖
入口本就是 document 级委托、不需要 DOM 扫描,此前只挂在 auto_fun 上等于
多依赖一次全局重扫的触发时机;脚本加载时立即挂载(auto_fun 钩子保留作幂等
重扫),任何时刻的首次点击都能同步弹窗。
2026-10-06 23:32:19 +08:00
kate522 93579cbc9f fix(payment): 售后弹窗改为按钮载荷同步弹出,修复首次点击无反馈
原实现是「点击 → 先请求 /orders/{n} → 再弹窗」,与项目里所有可用弹窗
(含举报链路 report.js)不同构:举报弹窗把数据放在按钮 data-* 属性里同步
弹出。前置请求会让首次点击在请求返回前毫无反应,体感就是"得二次点击"。

- 按钮 data-aksrt-after-sale-apply / -detail 携带订单与售后状态载荷
  (PHP 模板与 order-center.js 的 actionHtml 同构生成),点击当拍即开
  对应弹窗:申请表单 / 超时效提示 / 售后详细,零前置请求
- 「再次申请售后」同样使用已渲染载荷,不再重新拉取订单
- 提交与图片上传仍走 REST,服务端校验不变;列表刷新后载荷随之更新
- jsdom 多脚本环境实测:三种入口均在点击同一拍弹出且 fetch 计数为 0
2026-10-06 23:29:59 +08:00
kate522 4ea02f25ac fix(payment): 补升 schema 版本使售后举证图片列在已安装站点生效
order_aftersale 新增 images 列时未同步升 SCHEMA_VERSION,而 dbDelta 只在版本
变化时重跑,已安装站点不会执行 ALTER TABLE,提交售后申请直接报
Unknown column 'images'。SCHEMA_VERSION 2.5.0 -> 2.5.1,并在契约测试中
锁定「列变更必须同步升版本」,部署后进入一次 wp-admin 即自动补列。
2026-10-06 22:37:32 +08:00
kate522 1a9a2762f3 feat(payment): 新增「售后策略」配置与前台售后弹窗链路
- 后台「支付&付费 → 售后策略」:售后功能开关、申请时效、超时效提示(HTML)、
  预置原因、申请售后说明、所需材料、申请说明占位文案、可上传图片开关;
  PHP schema 与 SPA schema 1:1 对齐,group 字段支持拖拽排序
- PaymentRuntime 新增售后设置解析(缺键回退 schema 默认、HTML 走 kses、
  中文原因稳定 slug)与 after_sale_expired 时效判定(支付时间起算)
- 售后工单支持举证图片(order_aftersale.images 列,最多 3 张,
  复用评论图片上传校验)与上传路由;订单视图模型输出 after_sale 按钮状态
- 前台申请 / 超时效提示 / 售后详细三个独立弹窗(modal-mini + 蓝色炫彩头),
  订单详情弹窗不再内嵌售后表单并清理相关死代码
- 新增 after-sale-policy 契约测试与运行时 harness,527 项契约测试通过
2026-10-06 22:21:47 +08:00
kate522 b447c41f13 refactor(admin): 清理付费 meta 死字段并补齐商品介绍渲染
延续上一轮 pay_count「只写不读」的排查,把其余写而不读的字段一次
性处理完。按字段性质分两类处理,不是一律删。

【删除】纯数据垃圾,无界面入口、无消费方:
- card_keys / purchase_limit / stock:只有 DomainModel 的默认值与
  sanitize,全仓零引用。契约测试原本还断言它们不得出现在 metabox 上。
- 随 card_keys 一并删除 sanitle_card_keys() 助手。
- video_scale_height「固定长宽比例」:参考主题里这是轮播组件 widget
  的参数,不属于付费视频字段,当初误搬到付费 metabox 上且从未被消费。
  连默认值、sanitize、metabox 保存项一起移除,并同步修正契约测试。

【补齐】有界面入口、管理员填了但不显示的四个字段:
pay_title / pay_doc / pay_details / pay_extra_hide 此前只有 metabox
输入框和 sanitize,前端一个都不读。按参考主题 pages/download.php
的结构接到单篇资源下载页:
- pay_title 有值则替代文章标题作商品标题(回退 get_the_title);
- pay_doc 商品简介、pay_details 更多详情渲染在标题区下方;
- pay_extra_hide 额外隐藏内容只在有下载权限时渲染在下载盒之后。
三处 HTML 输出均走 wp_kses_post,与 sanitize 侧的安全 HTML 口径一致。

同时把 pay_extra_hide 接进文章页付费盒
(aksrt_get_paid_box_html):该函数本身只在有付费权限时执行,天然
就是「付费后才可见」的位置,复用已有的 .pay-extra-hide 样式。

验证:php -l 四个 PHP 文件无语法错误;
node --test tests/widget-system/*.test.cjs 449 项全部通过。
2026-10-06 20:17:58 +08:00
kate522 8e1fd0a30f fix(admin): 让后台「销量浮动」设置真正生效
资源 metabox 和快捷编辑都能设置「销量浮动」(pay_count),但设了完全
不起作用——所有读取方用的是另一个键 virtual_sales(§3.8 电商的虚拟
销量加成),pay_count 只被写、从未被读。

新增统一读取口径 aksrt_get_content_sales_count($post, $pay_meta):
真实订单销量 + pay_count,并把四处各自拼装销量的地方全部接到这个
口径上:
- aksrt_get_download_box_meta() 的 sales_count(内容帖)
- aksrt_get_resource_meta() 的 sales_count(资源帖)
- 付费墙 aksrt_get_paywall_html() 的已售徽标
- 付费盒 aksrt_get_paid_box_html() 的已售徽标
- 后台「所有文章」销售数据列的销量

此前资源帖的 sales_count 走的是「虚拟销量 + 下载量」这条与内容帖
不一致的口径,现统一。

快捷编辑的 pay_count 运算本是整数加减 + max(0,...) 归零,与读取口径
一致,无需改动。

_aksrt_sales_volume 保持不变,它仍是订单流水累加的副本;列表列取
订单表统计与 meta 副本的较大值再加浮动量,避免两条链路数字打架。

新增 sales-volume-float-contract.test.cjs 4 项,锁住「pay_count 必须
被销量读取方消费」这一契约,防止以后又被改成摆设。

验证:node --test tests/widget-system/*.test.cjs 449 项全部通过
(原有 445 项 + 新增 4 项);php -l 三个 PHP 文件无语法错误。
2026-10-06 19:52:03 +08:00
kate522 cf6dc24193 fix(admin): 互动数据列移除下载量统计
「互动数据」列此前有浏览/点赞/收藏/下载四项,现按要求去掉下载量,
只保留前三项。

配套清理:
- render_stats_column() 去掉下载读取、历史 download_count 键回退,
  以及 fa-download 渲染分支;
- apply_query_args() 的排序白名单去掉 download_count;
- stats_meta_key() 映射去掉该维度。

核对参考主题:其互动列(all_count)本来就只有阅读、点赞、收藏三项,
下载量是从 AKSRT 旧实现里搬过来的,不属于参考主题能力,去掉后
与 1:1 复刻目标一致。

_aksrt_res_download_count / RESOURCE_DOWNLOAD_COUNT_KEY 本身保留,
它们仍被资源相关功能使用;此处只是不再在列表列里展示。

契约测试改为断言下载项已消失,并删掉因此不再使用的 domain 变量。

验证:node --test tests/widget-system/*.test.cjs 445 项全部通过;
php -l 无语法错误。
2026-10-06 19:41:25 +08:00
kate522 c8fdd81d6a fix(admin): 修复快捷编辑组描述被 esc_html 转义成可见标签
「付费参数」组的说明文字 `<div class="c-yellow">…</div>` 整段被显示
成了纯文本标签。原因:fields_html() 对 $desc 做了 esc_html(),而该
参数传入的是一段自带 <div> 的 HTML,转义后标签就变成了可见字符。

改为按参考主题的做法原样输出 $desc,责任前移到调用方:
- pay 组:esc_html__() 转义文字,外层 div 由模板给出;
- filter 组:同样自带 <div class="px12 aksrt-bulk-edit-desc"> 包裹,
  以保住原有样式(此前样式靠 fields_html 里的外层 div 提供)。
字段标题仍保持 esc_html(),因为 filter 那组的标题来自设置项。

新增契约断言锁住这个契约:不得再 esc_html($desc),两个调用点必须
各自转义/包裹;同时保留「字段标题仍转义」的断言。

验证:node --test tests/widget-system/*.test.cjs 445 项全部通过
(原有 444 项 + 新增 1 项);php -l 无语法错误。
2026-10-06 19:35:56 +08:00
kate522 27868b6131 fix(admin): 修复销售数据排序图标错位与快捷编辑控件被剥成文本
P1 排序图标跑到「销售数据」文字前面:
原因是注册了 manage_edit-post_sortable_columns。注册后 WordPress 会给
该列套上自己的排序指示器与 <a>,而列头 label 里已经自带一个 <a>,
浏览器解析嵌套 <a> 时把内层内容抛出,箭头因而显示到文字之前。
参考主题不注册该钩子,靠 label 链接 + pre_get_posts 落地排序,现改为
同一做法,同时删掉因此变成死代码的钩子与方法。

P2 快捷/批量编辑新增的配置项不可用,只显示为纯文本:
render_edit_boxes() 用 wp_kses_post() 包裹面板输出。该函数按「文章
内容」白名单过滤,select/option/input/fieldset/legend/label 均不在
白名单内,标签被剥掉只剩文字,于是所有控件退化成纯文本。改为原样
输出,字段值在生成时已逐个 esc_attr/esc_html 转义。

同步更新两份契约测试:sales 列表头排序改为断言「不注册 sortable」,
并为快捷编辑新增一条防回归断言,锁住不得再用 wp_kses_post 包裹面板。

验证:node --test tests/widget-system/*.test.cjs 444 项全部通过
(原有 443 项 + 新增 1 项);php -l 无语法错误。
2026-10-06 19:19:08 +08:00
kate522 ef718042d2 feat(admin): 新增后台文章列表销售数据列与快捷编辑配置项
「所有文章」列表此前只有互动数据列,缺少销售数据表头;快捷/批量编辑
展开后也只有原生三项。本次对照参考主题补齐这两处,不含专题链路。

- 新增 PostListEnhance:统管 sale/stats 两列、付费类型筛选、
  orderby=sales_volume 排序,以及四组快捷编辑字段的渲染与保存
- 新增 PostListEditFields:单独承载字段定义,主类职责保持单一
- 新增 admin-post-list.js:按 pay_type/pay_modo 勾选切换字段显隐
- 从 inc/core-hooks.php 迁出原有列实现(删 74 行),不再保留旧闭包
- 清理 admin-main.css 中确认无引用的 .aksrt-tab-pane 与 .aksrt-modal* 死块
- 新增 post-list-quick-edit-contract 测试,更新 stats/icon-picker 契约

快捷编辑锚点列用互动数据列而非参考主题的专题分类法列(本项目无专题)。
真实销量改为页面级批量查询,避免逐行查库。金额运算统一走 Ledger 的
「分」单位,不用裸浮点。保存走 edit_post 钩子,一次覆盖快捷与批量两条路径。
2026-10-06 15:54:47 +08:00
kate522 194280af72 feat(admin): 移除支付中心的“下载记录”子菜单页面
下载不做记录,支付中心下的“下载记录”页面及相关链路整体删除。

删除的内容:
- src/Admin/Native/PayDownloadLogs.php(页面渲染、批量删除、
  令牌查询)整文件移除。
- PayMenus 中的 download_logs 子菜单注册(概览页快捷入口由同一
  份 pages() 数组生成,随之消失)。
- functions.php 中的 PayDownloadLogs::init() 接线。
- 各入口跳转链接:付费文章列表的操作按钮、用户列表“积分分成”
  列的操作链接、用户列表行操作。
- “保存用户下载记录(条) / 保存文章下载记录(条)”两个设置项及其
  消费链:SchemaPayment.php、assets/admin 侧 knowledge.js 的字段
  与默认值、PaymentRuntime.php 的 user_log / post_log。
- 前台下载中心登录提示里的“管理下载记录”文案(该处本就没有
  可跳转的管理页面)。

未改动内容:
- aksrt_dl_token 数据表仍由 DBMigrator 创建、uninstall.php 仍会
  删除:存量站点可能留有历史令牌行,属数据而非功能,迁移风险
  高于收益。
- 独立下载页面开关(down_alone_page)与远程文件转本地等下载
  行为配置保留。
- 资源下载量(_aksrt_res_download_count)仍是只读口径:下载走
  普通外链,主题内没有写入方,本次不新增下载统计。

download-direct-contract.test.cjs 中原有“下载记录页面展示历史
令牌”用例反向改写为三项断言:页面与接线已删除、记录数设置项
已注销、下载仍不做记录。

验证:node --test "tests/*/*.test.cjs" 517 项全部通过;
php -l 全部改动文件无语法错误;重新构建 admin SPA,确认产物
中不含被移除的字段与菜单。
2026-10-06 14:35:14 +08:00
kate522 50fac36169 fix(admin): 修复后台文章列表互动数据列无数据
后台「文章 → 所有文章」列表的「互动数据」列四项计数(浏览/点赞/
收藏/下载)恒为 0。原因是列回调读取的 meta key 与主题实际写入的
键不一致:读取用的是 'views' / 'like' / 'favorite' /
'download_count',而全仓库的写入方只写 aksrt_post_views /
_aksrt_like_count / _aksrt_favorite_count / _aksrt_res_download_count,
'views' / 'like' / 'favorite' 从未被任何代码写过。

改为复用既有的 aksrt_get_post_views() / aksrt_get_post_likes() /
aksrt_get_post_favorites() 读取(helpers 在 bootstrap 中先于
core-hooks.php 加载,仍保留 function_exists 兜底与原始键回退),
下载量走 DomainModel::RESOURCE_DOWNLOAD_COUNT_KEY,并对历史
download_count 键做一次回退读取。

同时修正两个相邻问题:

- 缩写口径与前台不一致:列内用的是前台从未使用的「万」后缀,
  改为统一的 aksrt_format_number()(≥10000 → 1.2W+,
  ≥1000 → 1.5K),与前台列表、文章详情页保持同一套。
- pre_get_posts 的按计数排序同样拿用户可见的排序键当 meta_key
  使用,现映射到真实存储键。

资源下载走普通外链,主题内没有下载计数的写入方,下载项仍只在
>0 时出现,此为现状而非本次回归。

新增 tests/widget-system/admin-stats-column-contract.test.cjs
(7 项)锁定列注册、读取键与写入键一致、缩写口径、排序键映射,
以及下载键的写入方状态,避免再次改名时静默失效。

验证:node --test "tests/*/*.test.cjs" 515 项全部通过
(原有 508 项 + 新增 7 项);php -l inc/core-hooks.php 无语法错误。
2026-10-06 14:28:44 +08:00
kate522 2595bb02db fix(frontend): 统一计数缩写口径并将流水号改为订单号
前台各处评论、浏览、点赞、收藏、已售数量统一走
aksrt_format_number() 缩写(≥10000 → 1.2W+,≥1000 → 1.5K),
缩写后缀统一为大写 K,与既有 W+ 保持一致。PHP 渲染缩写文本的
同时保留 data-base-count 原始整数,JS 增量计算不受影响;
article.js / comment-ajax.js 内的 formatStatCount / formatCount
与 PHP 规则保持同一套,避免点赞收藏后文本回跳成全量数字。

文章详情页购买信息卡、用户中心创作者收益明细、后台分成与提现
管理页、支付设置页以及四封订单/提现邮件模板中,面向用户展示的
「流水号」字样统一改为「订单号」;代码注释、内部错误码与
WP_Error 文案保留原表述。

验证:node --test "tests/*/*.test.cjs" 508 项全部通过;
php -l 检查全部改动文件无语法错误;重新构建 admin SPA 与
前台 runtime bundle。
2026-10-06 14:13:51 +08:00
kate522 fe57022550 fix(frontend): remove duplicate select arrow and show the WordPress brand icon 2026-10-06 13:23:05 +08:00
kate522 962ab5c290 fix(frontend): match the reference post format options in the page set modal
参考主题把文章格式固定为标准/图像/画廊/视频四项,动态读取
get_theme_support('post-formats') 会让本站多出 aside/quote/link
等注册格式,选项集偏离参考主题,故改为固定列表。
2026-10-06 12:55:04 +08:00
kate522 6dbe1fe4c6 fix(frontend): align page-set hint bar and FA5 icons with the reference
- 齿轮提示横条改为参考主题的 .abs-right 垂直居中几何(top:50% +
  right:.7em + translateY(-50%)),本站 legacy .abs-right 是 top:0/right:0
  导致横条贴到按钮顶角
- 新增 front-page-admin-set.js:横条 2 秒后淡出 1 秒消失(对齐参考
  主题 js/page-edit.js),开关关闭时不出队脚本
- 齿轮与弹窗内图标统一为 FA5 惯例 fa fas fa-*,并采用 FA5 的 glyph
  重命名(sliders→sliders-h、pie-chart→chart-pie)
2026-10-06 12:50:20 +08:00
kate522 12f60897d9 fix(frontend): 修复前台页面编辑弹窗并 1:1 复刻参考主题
远端加载的弹窗在 admin-ajax 上下文里拿不到查询对象,enabled()
恒为假,直接返回 success:false;而 AksrtRequest.responseError
只读 message/data.message,读不到 msg,遂报「请求失败(HTTP 200)」。
改为参考主题原生架构:wp_footer 直出 #modal_admin_set,提交走
vendored main.js 的 aksrt_ajax 契约,不再有 AJAX 取 HTML。

对齐参考主题 zib-frontend-set.php:
- 弹窗补齐 aria-hidden=false、win-ajax-replace、role=document
- 隐藏字段改用 taxonomy + post_type
- 字段顺序与 is_page 分支(标题样式、widgets_register、容器位置)
- 多选 checkbox 改 name="id[]",描述允许富文本
- 模块布局指向定制器小工具面板
- term 保存使用 POST 的 taxonomy 字段

清理不再使用的 assets/js/page-admin-set.js。
新增 frontend-page-edit-harness.php 覆盖门禁、渲染与各保存分支。
2026-10-06 12:22:43 +08:00
kate522 b75bf197a3 feat(extensions): 新增系统工具页面与前台投稿分片上传 2026-10-06 11:09:05 +08:00
kate522 613b8a57a3 style(user-center): 优化手机号脱敏数字框与前缀后缀一体呈现 2026-10-06 09:41:11 +08:00
kate522 af72364078 feat(security): 新增网站安全扩展模块并支持修改手机脱敏验证 2026-10-06 09:26:55 +08:00
kate522 224f86ec08 fix(profile): mask bound phone display 2026-10-06 02:16:22 +08:00
kate522 fc9bf27560 fix(profile): anchor verification code buttons 2026-10-06 02:07:13 +08:00
kate522 d8d0b5f290 test(profile): cover account modal styling 2026-10-06 01:51:02 +08:00
kate522 ce429e1caf fix(profile): align account modal UI 2026-10-06 01:46:48 +08:00
kate522 248912f610 fix(profile): align account dialogs and contracts 2026-10-06 01:09:04 +08:00
kate522 f646fa8573 fix(user): repair phone binding and profile fields 2026-10-05 20:55:20 +08:00
kate522 0cf4639d95 fix(profile): match account submit buttons 2026-10-05 19:54:00 +08:00
kate522 0a52bf36f9 fix(profile): refine settings modal actions 2026-10-05 19:21:42 +08:00
kate522 346146b4fa feat(profile): rebuild account settings page 2026-10-05 18:58:49 +08:00
kate522 f00c3faeaa fix(sms): accept empty HTTP 200 responses 2026-10-05 17:25:33 +08:00
kate522 59e9bbf8a6 fix(payment): restore order statistics cards 2026-10-05 17:25:33 +08:00
kate522 1af51ab172 fix(sms): handle empty Smsbao responses 2026-10-05 16:54:12 +08:00
kate522 7b55cfb190 feat(payment): rebuild order details and asset records 2026-10-05 16:42:25 +08:00
kate522 476e2f961d fix(rewards): load and strengthen amount styles 2026-10-05 11:36:39 +08:00
kate522 979639342a feat(rewards): add toast and captcha flow 2026-10-05 03:51:18 +08:00
kate522 356f7fe5bd fix(frontend): align reward modal with shared runtime 2026-10-05 03:24:42 +08:00
kate522 6037ca184b fix(sms): collapse provider settings by default 2026-10-05 02:29:42 +08:00
kate522 8ef2f7e49f fix(sms): remove obsolete init call 2026-10-05 02:17:25 +08:00
kate522 5091da6a60 feat(sms): align provider and verification flow 2026-10-05 02:13:52 +08:00
kate522 d0a3972ca7 fix(identity): surface real sideload failure reason 2026-10-04 23:26:52 +08:00
kate522 2ae2b2fd07 fix(identity): validate evidence by file content 2026-10-04 23:13:48 +08:00
kate522 90c1f7a62b fix(identity): normalize bare li markers in apply intro 2026-10-04 22:54:58 +08:00
kate522 8fcb136d55 fix(identity): draw intro list markers inside list items 2026-10-04 22:51:21 +08:00
kate522 d6e3b377b4 fix(identity): apply safe list styles in modal runtime 2026-10-04 22:25:15 +08:00
kate522 2b3e325d63 fix(identity): keep modal list markers inside scroll area 2026-10-04 22:15:42 +08:00
kate522 4caf81c9e2 fix(identity): keep intro list markers visible 2026-10-04 22:10:18 +08:00
kate522 88661e1574 fix(identity): preserve HTML in application intro 2026-10-04 22:02:20 +08:00
kate522 67ec3c8aeb fix(identity): align application modal with reference 2026-10-04 21:49:39 +08:00
kate522 59901b9e10 feat(identity): align auth flow with reference theme 2026-10-04 21:10:12 +08:00
kate522 5a17b399a0 fix(user-home): add reference tab loading skeletons 2026-10-04 19:40:24 +08:00
kate522 4c6b02a6b9 fix(user-home): restore desktop comment columns 2026-10-04 19:19:29 +08:00
kate522 56bbb63afd fix(user-home): align profile comments with reference theme 2026-10-04 19:11:27 +08:00
kate522andClaude Fable 5 00817766a6 fix(author): keep the follow sub-tab state out of the other panels
uhSub(粉丝/关注)只属于粉丝面板,loadPanelBody 却把它写在每一个面板上,
翻页时又从被点面板自身读回,于是分组状态跨面板串味:先点粉丝 tab 切到
「关注」,再点文章 tab 翻页,#panel-posts 会被永久打上 data-uh-sub=
"following",之后粉丝面板的翻页读到的是文章面板残留的分组。

翻页改为只读**当前面板内**的 .is-active 子页签,读不到就回落默认分组;
uhPage / uhSub 也只在粉丝面板写入,文章 / 收藏 / 评论面板一次都不碰。
目标面板本身仍由被点面板的 id 反解,跨面板跳转这条路径不存在,本次不动。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-10-04 14:28:43 +08:00
kate522andClaude Fable 5 16d67c8a87 fix(user-home): hide the old panel content while the tab skeleton loads
翻页 / 切 tab 是整页替换语义(author.js 最终就是 panel.innerHTML = html),
但加载态只做了骨架的显示,旧卡片与旧页码仍留在屏上,于是先看到
「第 1 页卡片 + 脚下一摞骨架」,等接口返回才换页。

加载期收起 .aksrt-user-home__post-list 的直接子级 .posts-item 与旧翻页控件,
让骨架独占内容区;对齐参考主题 post_ajax() 的 beforeSend(replace 时
$item.remove() + find(pag).remove())。显隐仍走纯类切换,不写 inline display。

两点刻意的收窄:
- 只收直接子级 .posts-item 而非后代:骨架内层同样带 .posts-item,
  后代选择器会把骨架自己也藏掉,占位就永远出不来。
- 旧页码用 :has(.post_ajax_loader) 限定:关注 / 评论面板没有骨架,
  无条件隐藏会让它们加载时只剩一片空白。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-10-04 13:58:09 +08:00
kate522andClaude Fable 5 45dc909e4c fix(user-home): span the tab pagination skeleton across the post grid
The posts/favorites tabs turn the article list into a 2-column grid at
>=992px. The AJAX pagination skeleton .post_ajax_loader is a direct
child of that grid, so with no grid-column it was laid out as a single
half-width card and the whole placeholder stack collapsed into one
column while paging or switching tabs.

Span the loader across every column and, while loading, give it the
same 2-column template the real cards use so placeholders line up with
them. The skeleton shape now follows the resolved render mode instead
of a hardcoded 'list', so card mode no longer renders list blocks.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-10-04 13:17:02 +08:00
kate522andClaude Fable 5 20fde1d652 fix(skeleton): put the widget gutter relief on the skeleton's inner item
小工具(首页挂载的 / 侧栏的)骨架加载占位比真实卡片窄一截:card 布局下
每侧窄 8px,视觉上就是「骨架小了一些」。

小工具的卡片走 skip_wrapper=true,直接挂在 .widget-ajaxpager 上
(src/WidgetSystem/PostRenderer.php),不经过 .posts-row,因此只受小工具
这条独立的负边距链作用:

  .widget-ajaxpager            margin: -8px
  .posts-item.card(真实卡片)  margin: 8px   ← 卡片自身自带
  .post_ajax_loader(外层)     margin: 8px   ← f61142ee 加的补偿
  .posts-item.card(骨架内层)  margin: 8px   ← 同一条卡片规则

骨架于是拿到 8px(外层 loader)+ 8px(内层卡片)= 16px,而真实卡片只有
8px,内容区每侧少 8px。list 模式没暴露是因为 .posts-item 的 8px 来自
`.widget-main-post.style-list .posts-item`,该规则只覆盖 style-list、
不作用于骨架,外层的 8px 恰好补上 list 卡片应有的一份,刚好对齐。

修法:把水平补偿从外层 .post_ajax_loader 移到骨架内层的 .posts-item /
.posts-mini —— 与真实卡片同一个元素、同一条选择器,于是两者逐像素同宽,
也顺带回答了「补偿链的拥有者是谁」这个一直在打补丁的问题。

- assets/css/main.css、assets/css/frontend/post-card.css:.posts-row 与
  .widget-ajaxpager 两条链、桌面 8px 与移动 5px 两组,共 8 处规则改为
  内层选择器。
- assets/css/routes/user-home.css:面板归零规则同步改到内层,否则它归零
  的是已经失效的外层,旧补偿会重新显形。
- tests/widget-system/asset-ledger-contract.test.cjs:把守卫断言改成内层
  选择器,并修正规则匹配以支持选择器组(.posts-item, .posts-mini)。
  注释说明为何不能补偿外层。

参考主题 zibll 完全没有 .post_ajax_loader 的 margin 规则;AKSRT 需要它,
是因为骨架多包了一层。定位在内层是与参考主题等价的正确解。

Verification:
- node tests/widget-system/asset-ledger-contract.test.cjs  16/16
- node tests/widget-system/post-visual-contract.test.cjs   22/22
- node tests/widget-system/zibll-modern-widgets-contract.test.cjs 10/10
- node tests/widget-system/lazyload-contract.test.cjs      7/7
- node tests/widget-system/frontend-lifecycle-contract.test.cjs 13/13
- node tests/widget-system/page-display-contract.test.cjs  18/18
- node tests/widget-system/message-center-contract.test.cjs 8/8
- php tools/audit-admin-contracts.php  errors=0
- build:public 已重建;旧外层规则在源与产物中均已归零

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-10-04 04:07:19 +08:00
kate522andClaude Fable 5 38bbd917a9 fix(skeleton): align the homepage tab and sort loaders with the reference 1:1
首页「更多栏目」tab 与「排序方式」切换的骨架占位与参考主题 zibll 有实质差异,
根因不是几何宽度,而是三处结构/契约错位:

1. 骨架块自身结构与参考主题不同。card 分支用了 `<posts>` 元素 + `.post-graphic`
   包裹层(列表变体,带 margin-right:20px),而参考主题 card 骨架是
   `<div class="posts-item card">` 且 `.item-thumbnail` 是直接子元素。
   → card 骨架右侧多出一条真实卡片没有的 20px 沟槽。

2. 骨架块被错误地打上 `ajax-item`。参考主题的占位块没有该类。它会让
   `find('.ajax-item')` / ajax-list 的 `inject()` 把骨架当成真实条目。

3. 排序切换的骨架被预渲染节点顶替。aksrt_render_pagination() 在 .ajax-pag 内
   渲染了一个写死 2 条、写死 list 形状的 .post_ajax_loader;runtime post_ajax()
   会先在容器内 find('.post_ajax_loader'),找到就复用并丢弃调用方按视口动态
   拼出的骨架 → 切排序永远是 2 条 list 骨架,card 布局下也是、与视口宽度无关。
   参考主题 zib_paging() 根本没有 loader 兄弟节点。

改动:
- inc/helpers/posts-renderer/markup.php:aksrt_render_placeholder() 的
  card/list 分支改为与 zib_get_post_placeholder() 逐字等价的结构;去掉
  ajax-item 类、card 分支的 .post-graphic 包裹层,元素改用 <div>。
- inc/helpers/template-tags/pagination-permissions.php:删除预渲染 loader,
  与参考主题 zib_paging() 一致 —— 只渲染翻页控件。
- assets/js/core/pagination.js:找不到骨架节点时构造与 runtime/main.js 同款
  的文章骨架(卡片 8/6/4 条、列表 4/3 条,按视口与参考主题同阈值),
  不再退化为「转圈 + 文案」这个第二套加载语言;仅评论等非文章面保留该兜底。
- assets/js/core/pagination.js:为排序菜单(.ajax-option)与 tab 触发器
  (.post_ajax_trigger)增加避让 —— 它们归 legacy runtime 管线,
  否则一次点击发两个请求(与 ajax-list.js 的既有避让对称)。
- assets/js/runtime/main.js:post_ajax 的显隐由 fadeIn()/hide() 改为
  .is-loading 类切换。前者写 inline display,优先级高于类规则,一旦残留
  骨架永远打不开,正是骨架契约明令禁止的失败模式。

归档/分类筛选面原本另有一套独立加载语言(spinner 遮罩 + opacity 淡出),
本次一并统一到同一条 .post_ajax_loader + .is-loading 契约:
- inc/helpers/aksrt-archive.php:删除 show_loading 标记位与遮罩 DOM,
  改为在分页之后渲染文章骨架。
- assets/js/core/archive.js:showLoading/hideLoading 只切 .is-loading;
  fetchPosts() 清空内容时保留骨架与分页节点(与参考主题 success 只 hide
  不删的生命周期一致),原先会连带删掉骨架。
- assets/css/frontend/layout.css:删除 .archive__ajax-wrap.is-loading
  的 opacity 淡出与 spinner 遮罩规则。
- category.php / tag.php:删除已失效的 show_loading 实参。

验证:php -l 全部通过;php tools/audit-admin-contracts.php errors=0;
契约测试 post-visual(22) / asset-ledger(16) / message-center(8) /
lazyload(7) / frontend-lifecycle(13) / zibll-modern-widgets(10) /
page-display(18) / a11y 全绿;已重跑 build:public 刷新产物。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-10-04 03:04:02 +08:00
kate522andClaude Fable 5 f61142ee9a fix(pagination): align the load-more skeleton width with the cards
首页「加载更多」的骨架比列表本身宽出一截:.posts-row 用
margin-left/right:-8px 抵消卡片的水平边距,而卡片边距规则是
`.posts-row > *:not(.post_ajax_loader)`,把骨架排除在补偿链之外,
于是骨架左右各宽出 8px(合计 16px)。移动端的 5px 覆盖块同理。

小工具的 .widget-ajaxpager / tab pane 也是一条独立的负边距补偿链
(margin: -8px 抵消 .posts-item 的 8px),骨架同样溢出。

修法是给骨架补上与卡片等同的水平边距,而不是写 inline display
(会盖过 .is-loading 类规则)。改了 main.css、post-card.css 的
8px / 5px 两组规则,用户中心面板另加一条归零规则,因为那里的
容器 margin 与卡片边距都已清零,不能再叠加补偿。

顺带补一条契约测试:删掉任一 gutter 规则即失败,防止回归。

Verification:
- node --test tests/**/*.test.cjs  471 passed / 0 failed
- php tests/widget-system/run.php  136 assertions
- php tools/audit-widget-system.php
- php tools/audit-admin-contracts.php  errors=0 (56 既有 baseline 警告)
- node tools/build-public-assets.mjs  CSS/JS 产物已重建

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-10-04 00:25:26 +08:00
kate522andClaude Fable 5 cf4554e7c8 fix(ajax): show the reference skeleton on every load-more surface
Clicking 加载更多 in the 多栏目文章 widget showed no skeleton at all — the
list simply stayed still until the response arrived, then the new cards
appeared. The homepage stream, archives and comments already had a working
skeleton; the widget's AJAX load goes through a different client and never
found its loader.

Root cause was DOM placement, not visibility: ContentRenderer emitted
.post_ajax_loader as a *sibling* of .widget-ajaxpager, while the widget REST
client resolves it with container.querySelector('.post_ajax_loader'). The
node existed but could never match, so .is-loading was never added and the
skeleton stayed display:none. Inactive tab panes had the same shape, and the
direct-load branch of ajaxpager() emitted no loader at all.

Changes, all following the reference theme's post_ajax() contract in
zibll/js/main.js (beforeSend anchors the loader to the end of the list;
success only hides it, never removes it):

- ContentRenderer: nest the loader inside .widget-ajaxpager for both the
  direct-load and inactive-tab branches, and drop the sibling append.
- widgets/frontend.js: anchorLoader() moves the loader to the end of the
  list before .ajax-pag on every request; appended cards are inserted before
  it; a replace preserves the node instead of wiping it with innerHTML; a
  fallback loader is created when the DOM has none; loadItems() now returns
  its promise; drop the unreachable is-loading auto-load branch.
- Message centre, user-home panels and the search page switch from spinner /
  opacity feedback to the same .posts-item-shaped skeleton, so all AJAX
  surfaces share one loading contract.
- Remove the second, never-wired skeleton system (.aksrt-skeleton-* in
  core-ui.css) and the dead spinner stylesheets/rules (skeleton.css,
  ajax-spinner.css, .load-more__*, --skeleton-base/--skeleton-shine) plus the
  unregistered assets/js/waterfall.js.
- Extend the widget, message-centre, user-home and asset-ledger contract
  tests to pin the loader inside the container, the class-only visibility
  switch, and the absence of the deleted dead code.

Verified: 470 node tests, 136 PHP widget assertions, admin-contract and
widget-system audits all green; rebuilt public assets after the CSS/JS
removals.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-10-03 22:22:15 +08:00
kate522andClaude Fable 5 271e7f0518 chore(audit): refresh contract report
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-10-03 17:24:16 +08:00
kate522andClaude Fable 5 b5f37d6d46 fix(pagination): align AJAX load-more loading state with reference
Loading indication now matches the reference theme's post_ajax() contract:

- Stop overwriting the 加载更多 button label with 正在加载...; the button
  keeps its own text and the .post_ajax_loader skeleton carries the busy
  state instead.
- Add the reference fallback loader (.loading ring + 加载中...) when no
  skeleton exists in the DOM, and reinsert the skeleton after the last
  card on every page load like beforeSend does.
- Retext the loader string to 加载中... (zib__('loading')) and make the
  ajax_trigger default a translatable string.
- Route ajax-list.js / comments.js / widgets frontend.js through the
  .is-loading class instead of inline display, which outranked the class
  rule and could freeze the skeleton shut.
- Drop the search.php inline display:none that defeated .is-loading, and
  remove the unproduced .post-ajax-loader / [data-aksrt-ajax-loader] shims.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-10-03 17:23:44 +08:00
kate522andClaude Fable 5 e506309434 chore(widgets): re-freeze the drifted widget reference baseline
audit-widget-system.php and validate-widget-golden.php both failed on
"Reference drift: inc/functions/functions.php". The reference theme was
updated from V9.1 to V9.2; its update log records only a message-center
pagination fix, so no widget contract changed.

The file is not a widget source — group_sources in the golden-fixture
generator maps only the five widget-*.php files — so it contributes solely
to the aggregate fingerprint, and re-freezing is safe.

Refresh the pinned SHA256 in .specs/baselines/widget-reference.json and
regenerate tests/fixtures/widget-golden-v1.json so the hash and its
derived fingerprint stay consistent. The fixture diff is those two lines.

Both tools pass again: widget audit (37 widgets, 40 areas, 5 templates)
and golden validation (37 components, 222 screenshot targets).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-10-03 15:44:02 +08:00
kate522andClaude Fable 5 89a9cf24e9 fix(posts): repair the load-more skeleton and align it with the reference
Clicking 加载更多 showed no skeleton at all, and the placeholder shape did
not match the reference theme's post cards.

- Class-name mismatch: pagination.js queried .post-ajax-loader while every
  PHP producer emits .post_ajax_loader, so showSkeleton() never found a
  target and the loader stayed hidden. Accept both spellings.
- Inline display:none on the loader (pagination, comments, widget tab
  panes) outranked .is-loading, so even the correct selector could not
  reveal it. Drop the inline style; the reference uses a bare
  <div class="post_ajax_loader"> and toggles visibility from JS.
- loadNextPage() set indicator.style.display = "flex", which forced the
  .posts-item.card skeleton blocks (inline-block layout) into a single
  row instead of stacking vertically — the visible symptom. Remove those
  inline writes and drive .is-loading from CSS for every surface.
- Scope the hide/show rules on the class itself rather than requiring an
  .ajaxpager ancestor: homepage tab panes, archive streams and comment
  lists sit at three different depths.
- Rebuild aksrt_render_placeholder() on zib_get_post_placeholder's
  structure — list = t1/k1/k2 + two s1, card = t1 item-heading/k2 + two
  s1, thumb wrapped in post-graphic + radius8 item-thumbnail, all carrying
  ajax-item so the loader is not mistaken for a real card.
- Add mini/mini_thumb modes so ContentRenderer::placeholder() stops
  hand-rolling the same markup; normalize its 'lists' arg to 'list'.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-10-03 15:42:18 +08:00
360 changed files with 23694 additions and 13530 deletions

No files matched your search

@@ -5,11 +5,4 @@ export const extensionsApiMethods = {
testIpGeo(ip) { return this.request('/extensions/ip-test', { method: 'POST', data: { ip } }) },
testContentAudit(text) { return this.request('/extensions/audit-test', { method: 'POST', data: { text } }) },
/* AI 辅助 */
getAiStatus() { return this.request('/ai/status') },
updateAiSettings(data) { return this.request('/ai/settings', { method: 'POST', data }) },
generateSeo(data) { return this.request('/ai/seo-generate', { method: 'POST', data }) },
suggestSummary(data) { return this.request('/ai/summary-suggest', { method: 'POST', data }) },
suggestTags(data) { return this.request('/ai/tags-suggest', { method: 'POST', data }) },
}
+1 -8
View File
@@ -5,6 +5,7 @@ export const usersApiMethods = {
getAuthSettings() { return this.request('/settings/auth') },
saveAuthSettings(data) { return this.request('/settings/auth', { method: 'POST', body: JSON.stringify(data) }) },
testSms(phone) { return this.request('/sms/test', { method: 'POST', body: JSON.stringify({ phone }) }) },
getNotificationSettings() { return this.request('/settings/notifications') },
saveNotificationSettings(data) { return this.request('/settings/notifications', { method: 'POST', body: JSON.stringify(data) }) },
@@ -17,14 +18,6 @@ export const usersApiMethods = {
deleteNotification(id) { return this.request(`/notifications/${id}`, { method: 'DELETE' }) },
runNotificationCleanup() { return this.request('/notifications/cleanup', { method: 'POST', body: JSON.stringify({}) }) },
getIdentitySettings() { return this.request('/identity/settings') },
saveIdentitySettings(data) {
return this.request('/identity/settings', { method: 'POST', body: JSON.stringify(data) })
},
testIdentitySettings(data = {}) {
return this.request('/identity/settings/test', { method: 'POST', body: JSON.stringify(data) })
},
getCreditSettings() { return this.request('/assets/settings') },
saveCreditSettings(data) { return this.request('/assets/settings', { method: 'POST', body: JSON.stringify(data) }) },
listCreditUsers(params = {}) {
+323 -39
View File
@@ -476,14 +476,43 @@
<!-- Fieldset (single nested object) -->
<div v-else-if="schema.type === 'fieldset' || schema.type === 'accordion'" class="aksrt-field__fieldset">
<AksField
v-for="field in visibleNestedFields"
:key="field.key"
:schema="field"
:model-value="fieldsetValue(field)"
:form-data="formData"
@update:model-value="updateFieldsetValue(field.key, $event)"
/>
<template v-if="fieldsetSections.length">
<div
v-for="(section, sIndex) in fieldsetSections"
:key="section.title"
class="aksrt-field__accordion-section"
:class="{ 'is-collapsed': !isFieldsetSectionOpen(sIndex) }"
>
<div class="aksrt-field__accordion-section-head" @click="toggleFieldsetSection(sIndex)">
<span class="aksrt-field__accordion-section-title">{{ section.title }}</span>
<FaIcon
:icon="isFieldsetSectionOpen(sIndex) ? 'fa fas fa-chevron-down' : 'fa fas fa-chevron-right'"
:size="12"
class="aksrt-field__accordion-section-toggle"
/>
</div>
<div v-show="isFieldsetSectionOpen(sIndex)" class="aksrt-field__accordion-section-body">
<AksField
v-for="field in sectionFields(section)"
:key="field.key"
:schema="field"
:model-value="fieldsetValue(field)"
:form-data="formData"
@update:model-value="updateFieldsetValue(field.key, $event)"
/>
</div>
</div>
</template>
<template v-if="!fieldsetSections.length">
<AksField
v-for="field in visibleNestedFields"
:key="field.key"
:schema="field"
:model-value="fieldsetValue(field)"
:form-data="formData"
@update:model-value="updateFieldsetValue(field.key, $event)"
/>
</template>
</div>
<div v-else-if="schema.type === 'link'" class="aksrt-field__link">
@@ -507,38 +536,62 @@
</div>
</div>
<!-- Repeater (array of objects) — inline expand (default) -->
<!-- Repeater (array of objects) — inline expand (default, draggable) -->
<div v-else-if="(schema.type === 'repeater' || schema.type === 'group') && schema.ui !== 'list-modal'" class="aksrt-field__repeater">
<div
v-for="(item, index) in repeaterItems"
:key="item._id || index"
class="aksrt-field__repeater-item"
<draggable
v-model="repeaterDragList"
:item-key="repeaterDragKey"
handle=".aksrt-field__repeater-handle"
ghost-class="aksrt-field__repeater-ghost"
animation="180"
>
<div class="aksrt-field__repeater-header">
<span class="aksrt-field__repeater-index">#{{ index + 1 }}</span>
<n-button
size="tiny"
type="error"
text
:disabled="schema.disabled || reachedRepeaterMinimum"
@click="removeRepeaterItem(index)"
>
<FaIcon icon="fa fas fa-trash-alt" :size="12" />
</n-button>
</div>
<div class="aksrt-field__repeater-fields">
<template v-for="field in (schema.fields || [])" :key="field.key">
<AksField
v-if="matchDependency(field.dependsOn, item)"
:schema="field"
:model-value="item[field.key]"
:form-data="formData"
:disabled="schema.disabled"
@update:model-value="updateRepeaterItem(index, field.key, $event)"
/>
</template>
</div>
</div>
<template #item="{ element, index }">
<div class="aksrt-field__repeater-item" :class="{ 'is-collapsible': repeaterCollapsible, 'is-collapsed': repeaterCollapsible && !isRepeaterItemOpen(index) }">
<div
class="aksrt-field__repeater-header"
:class="{ 'is-clickable': repeaterCollapsible }"
@click="repeaterCollapsible ? toggleRepeaterItem(index) : undefined"
>
<div class="aksrt-field__repeater-title">
<i class="fa fas fa-grip-vertical aksrt-field__repeater-handle" aria-hidden="true" @click.stop></i>
<span class="aksrt-field__repeater-index">#{{ index + 1 }}</span>
<span v-if="repeaterCollapsible && repeaterItemSummary(element)" class="aksrt-field__repeater-summary">
{{ repeaterItemSummary(element) }}
</span>
</div>
<div class="aksrt-field__repeater-header-actions" @click.stop>
<FaIcon
v-if="repeaterCollapsible"
:icon="isRepeaterItemOpen(index) ? 'fa fas fa-chevron-down' : 'fa fas fa-chevron-right'"
:size="12"
class="aksrt-field__repeater-toggle"
/>
<n-button
size="tiny"
type="error"
text
:disabled="schema.disabled || reachedRepeaterMinimum"
@click="removeRepeaterItem(index)"
>
<FaIcon icon="fa fas fa-trash-alt" :size="12" />
</n-button>
</div>
</div>
<div v-show="!repeaterCollapsible || isRepeaterItemOpen(index)" class="aksrt-field__repeater-fields">
<template v-for="field in (schema.fields || [])" :key="field.key">
<AksField
v-if="matchDependency(field.dependsOn, element)"
:schema="field"
:model-value="element[field.key]"
:form-data="formData"
:disabled="schema.disabled"
@update:model-value="updateRepeaterItem(index, field.key, $event)"
/>
</template>
</div>
</div>
</template>
</draggable>
<n-button
size="small"
dashed
@@ -688,6 +741,8 @@ import FaIconPicker from './FaIconPicker.vue'
import AksMediaField from './AksMediaField.vue'
import AksGalleryField from './AksGalleryField.vue'
import AksCodeEditor from './AksCodeEditor.vue'
import { buildFieldsetSections } from './fieldset-sections.js'
import draggable from 'vuedraggable'
import api from '@/api'
const props = defineProps({
@@ -720,6 +775,85 @@ const repeaterItems = computed(() => Array.isArray(props.modelValue) ? props.mod
const reachedRepeaterMinimum = computed(() => repeaterLength.value <= Number(props.schema.min || 0))
const reachedRepeaterMaximum = computed(() => Number(props.schema.max || 0) > 0 && repeaterLength.value >= Number(props.schema.max))
/* ------------------------------------------------------------------ *
* 手风琴模式:schema.collapsible 开启后,每一项默认收起,仅显示标题行,
* 点击标题行展开/收起。defaultCollapsed 为 false 时改为默认全部展开。
* ------------------------------------------------------------------ */
const repeaterCollapsible = computed(() => !!props.schema.collapsible)
const repeaterDefaultOpen = computed(() => props.schema.defaultCollapsed === false)
// 只记录「与默认状态相反」的下标,新增/删除项时其余项的开合状态不受影响。
const toggledRepeaterItems = ref(new Set())
function isRepeaterItemOpen(index) {
if (!repeaterCollapsible.value) return true
return repeaterDefaultOpen.value !== toggledRepeaterItems.value.has(index)
}
function toggleRepeaterItem(index) {
const next = new Set(toggledRepeaterItems.value)
if (next.has(index)) {
next.delete(index)
} else {
next.add(index)
}
toggledRepeaterItems.value = next
}
/** 收起态标题行上的一行摘要,取自 listPreview 配置。 */
function repeaterItemSummary(item) {
if (!repeaterCollapsible.value || !item) return ''
const cfg = getRepeaterPreviewConfig()
const titleKey = cfg.titleKey || 'title'
const title = String(item[titleKey] ?? '').trim()
const prefix = String(cfg.prefix || '')
if (title === '') return prefix
const desc = repeaterItemDuration(item)
return desc ? `${prefix}${title} · ${desc}` : `${prefix}${title}`
}
/** time/unit 形态的时长摘要:0 视为永久,其余按 day/month 补单位。 */
function repeaterItemDuration(item) {
const cfg = getRepeaterPreviewConfig()
const descKey = cfg.descKey
if (!descKey) return ''
const raw = item[descKey]
if (raw === '' || raw == null) return ''
const amount = Number(raw)
if (!Number.isFinite(amount)) return String(raw)
if (amount <= 0) return '永久'
const unitKey = cfg.descUnitKey
if (!unitKey) return String(raw)
const unit = String(item[unitKey] || '')
if (unit === 'month') return `${amount} 个月`
if (unit === 'day') return `${amount} 天`
return String(raw)
}
/** 拖拽视图:vuedraggable 以 modelValue 模式工作(内部复制后回传 update:modelValue)。 */
const repeaterDragList = computed({
get: () => (Array.isArray(props.modelValue) ? props.modelValue : []),
set: (value) => emit('update:modelValue', Array.isArray(value) ? value : []),
})
/**
* 行 key:优先用行自身 _id;服务端历史数据没有 _id 时按对象引用派生稳定 key,
* 避免每次输入都重算 key 导致子字段重挂载、输入焦点丢失。
*/
const dragRowKeys = new WeakMap()
let dragRowKeySeed = 0
function repeaterDragKey(item) {
if (item && typeof item === 'object') {
if (item._id) return item._id
let key = dragRowKeys.get(item)
if (!key) {
key = `rpt_drag_${++dragRowKeySeed}`
dragRowKeys.set(item, key)
}
return key
}
return String(item)
}
function fieldsetValue(field) {
const current = props.modelValue && typeof props.modelValue === 'object' && !Array.isArray(props.modelValue)
? props.modelValue
@@ -727,6 +861,35 @@ function fieldsetValue(field) {
return Object.prototype.hasOwnProperty.call(current, field.key) ? current[field.key] : field.default
}
/* ------------------------------------------------------------------ *
* accordion/fieldset 的分块手风琴:schema.sections 声明每块的标题与字段
* key,默认全部收起,点击块标题展开。未声明 sections 时保持平铺渲染。
* ------------------------------------------------------------------ */
const fieldsetSections = computed(() => {
return buildFieldsetSections(props.schema)
})
const openFieldsetSections = ref(new Set())
function isFieldsetSectionOpen(index) {
return openFieldsetSections.value.has(index)
}
/** 分块内字段同样遵循 dependsOn,数据源是本 fieldset 的值对象。 */
function sectionFields(section) {
return (section.fields || []).filter((field) => matchDependency(field.dependsOn, props.modelValue))
}
function toggleFieldsetSection(index) {
const next = new Set(openFieldsetSections.value)
if (next.has(index)) {
next.delete(index)
} else {
next.add(index)
}
openFieldsetSections.value = next
}
function updateFieldsetValue(key, value) {
const current = props.modelValue && typeof props.modelValue === 'object' && !Array.isArray(props.modelValue)
? props.modelValue
@@ -1018,6 +1181,8 @@ const repeaterDraft = ref({})
function createRepeaterItem() {
const newItem = { _id: `rpt_${Date.now()}_${++repeaterIdCounter}` }
for (const field of (props.schema.fields || [])) {
// UI-only chrome rows (notice / alert) are presentation only.
if (field.ui_only) continue
if (field.default !== undefined) {
newItem[field.key] = field.default
} else if (field.type === 'switch') {
@@ -1047,7 +1212,16 @@ function removeRepeaterItem(index) {
function updateRepeaterItem(index, fieldKey, value) {
const current = Array.isArray(props.modelValue) ? [...props.modelValue] : []
current[index] = { ...current[index], [fieldKey]: value }
const row = current[index]
if (row && typeof row === 'object') {
// 必须原地写,不能展开成新对象:drag key 存在以「对象身份」为键的 WeakMap
// 里,而行数据是 reactive 的——换一个新对象后,渲染时读到的是它的一个新
// reactive 代理,WeakMap 认不出它便生成新 key,整行重挂载、输入框失焦,
// 表现为打一个字就退出编辑。经代理原地写既保持身份,也照样触发响应式。
row[fieldKey] = value
} else {
current[index] = { [fieldKey]: value }
}
emit('update:modelValue', current)
}
@@ -1149,6 +1323,15 @@ function confirmRepeaterModal() {
margin-bottom: 0;
}
/* 结构化分组(会员商品/续费商品/积分兑换/升级价格)与相邻字段之间留出更明确的间隔,
* 避免两个分组上下贴在一起,和参考主题的留白一致。 */
.aksrt-field--group,
.aksrt-field--repeater,
.aksrt-field--accordion,
.aksrt-field--fieldset {
margin-bottom: 26px;
}
.aksrt-field__input-wrap {
display: grid;
gap: 8px;
@@ -1414,6 +1597,48 @@ function confirmRepeaterModal() {
background: var(--aksrt-color-canvas);
}
.aksrt-field__accordion-section + .aksrt-field__accordion-section {
margin-top: 14px;
}
.aksrt-field__accordion-section-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
padding: 8px 10px;
border: 1px solid var(--aksrt-color-border);
border-radius: 6px;
background: var(--aksrt-color-surface);
cursor: pointer;
user-select: none;
font-size: 13px;
font-weight: 600;
color: var(--aksrt-color-text);
transition: border-color 0.15s, color 0.15s;
}
.aksrt-field__accordion-section-head:hover {
border-color: var(--aksrt-color-primary);
color: var(--aksrt-color-primary);
}
.aksrt-field__accordion-section-title {
min-width: 0;
}
.aksrt-field__accordion-section-toggle {
flex-shrink: 0;
color: var(--aksrt-color-text-secondary);
}
.aksrt-field__accordion-section-body {
display: flex;
flex-direction: column;
gap: 8px;
padding: 10px 2px 2px;
}
.aksrt-field__repeater {
display: flex;
flex-direction: column;
@@ -1431,9 +1656,68 @@ function confirmRepeaterModal() {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
margin-bottom: 8px;
}
.aksrt-field__repeater-title {
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
}
.aksrt-field__repeater-header.is-clickable {
cursor: pointer;
user-select: none;
border-radius: 6px;
margin: -4px -4px 0;
padding: 4px 4px 0;
}
.aksrt-field__repeater-header.is-clickable:hover .aksrt-field__repeater-index {
color: var(--aksrt-color-primary-hover);
}
.aksrt-field__repeater-header-actions {
display: flex;
align-items: center;
gap: 4px;
flex-shrink: 0;
}
.aksrt-field__repeater-toggle {
color: var(--aksrt-color-text-secondary);
transition: transform 0.15s;
}
.aksrt-field__repeater-summary {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 12px;
color: var(--aksrt-color-text-secondary);
}
.aksrt-field__repeater-item.is-collapsible {
padding: 10px 12px;
}
.aksrt-field__repeater-item.is-collapsed .aksrt-field__repeater-header {
margin-bottom: 0;
}
.aksrt-field__repeater-handle {
cursor: grab;
color: var(--aksrt-color-text-tertiary);
}
.aksrt-field__repeater-ghost {
opacity: 0.45;
border-color: var(--aksrt-color-primary);
}
.aksrt-field__repeater-index {
font-size: 12px;
font-weight: 600;
@@ -64,9 +64,9 @@
<n-tabs type="line" animated>
<n-tab-pane
v-for="tab in field.tabs"
:key="tab.title || tab.key"
:name="tab.title || tab.key"
:tab="tab.title || tab.key"
:key="tab.key || tab.title"
:name="tabName(tab)"
:tab="tabName(tab)"
>
<template v-for="sub in (tab.fields || [])" :key="sub.key">
<AksField
@@ -231,6 +231,18 @@ function isFieldVisible(field) {
return matchDependsOn(field.dependsOn)
}
/**
* Tab label. `titleKey` binds the label to a live form value (e.g. the VIP level
* display name) so renaming a level renames its tab; `titleFallback` is used
* while that value is still empty.
*/
function tabName(tab) {
if (!tab) return ''
const dynamic = tab.titleKey ? props.formData?.[tab.titleKey] : ''
const label = typeof dynamic === 'string' ? dynamic.trim() : ''
return label || tab.titleFallback || tab.title || tab.key || ''
}
/** Whole group can be hidden (e.g. optional advanced blocks). */
function isGroupVisible(group) {
return matchDependsOn(group.dependsOn)
@@ -0,0 +1,24 @@
/**
* 把 accordion / fieldset 的 `schema.sections` 声明展开成「块标题 + 字段定义」。
*
* 参考主题把「升级价格」这类复合配置拆成若干可折叠块,每块用 keys 声明自己
* 包含哪些字段。这里是纯函数,方便在 Node 契约测试里直接用真实 schema 校验,
* 避免渲染层的一个小疏忽让整块手风琴静默退化成平铺。
*/
export function buildFieldsetSections(schema) {
const declared = schema && Array.isArray(schema.sections) ? schema.sections : []
if (!declared.length) return []
const byKey = {}
const nested = Array.isArray(schema.fields) ? schema.fields : []
for (const field of nested) {
if (field && field.key) byKey[field.key] = field
}
return declared.map((section) => ({
title: String((section && section.title) || ''),
fields: (Array.isArray(section && section.keys) ? section.keys : [])
.map((key) => byKey[key])
.filter(Boolean),
}))
}
@@ -1,4 +1,4 @@
import { renderFaIcon, schemaPage, customPage } from './helpers'
import { renderFaIcon, schemaPage } from './helpers'
export const extensionsSection = {
key: 'extensions',
@@ -6,6 +6,16 @@ export const extensionsSection = {
icon: renderFaIcon('fa fas fa-puzzle-piece'),
route: '/extensions',
children: [
schemaPage({
key: 'extensions-system',
name: 'ExtensionsSystem',
label: '系统工具',
route: '/extensions/system',
title: '系统工具',
desc: 'WordPress 优化、上传优化与编辑器显示的系统级开关。',
canonicalDomain: 'extensions',
section: 'system',
}),
schemaPage({
key: 'extensions-ip',
name: 'ExtensionsIp',
@@ -32,19 +42,19 @@ export const extensionsSection = {
label: '百度推送',
route: '/extensions/push',
title: '百度推送',
desc: '配置搜索引擎主动推送、批量提交和状态追踪。',
desc: '配置百度自动推送、收录通道与触发规则;推送结果与「重新提交」在文章/分类编辑页查看。',
canonicalDomain: 'extensions',
section: 'push',
}),
customPage({
key: 'extensions-ai',
name: 'ExtensionAI',
label: 'AI 连接配置',
route: '/extensions/ai',
component: 'ExtensionAI',
status: 'available',
title: 'AI 连接配置',
desc: '配置 AI 生成连接;SEO 在文章独立 SEO 框,摘要/标签在前台投稿页。',
schemaPage({
key: 'extensions-security',
name: 'ExtensionsSecurity',
label: '网站安全',
route: '/extensions/security',
title: '网站安全',
desc: '防刷限制、验证码发送限制与站点安全加固。',
canonicalDomain: 'extensions',
section: 'security',
}),
],
}
+1 -1
View File
@@ -16,7 +16,7 @@ const SECTION_DEFAULT_CHILDREN = {
permissions: 'permissions-comment',
users: 'users-auth',
payment: 'payment-knowledge',
extensions: 'extensions-ip',
extensions: 'extensions-system',
tools: 'tools-import-export',
'theme-info': 'theme-info-dashboard',
docs: 'docs-dashboard',
@@ -88,5 +88,16 @@ export const paymentSection = {
canonicalDomain: 'payment',
section: 'withdrawal',
}),
schemaPage({
key: 'payment-after-sale',
name: 'PaymentAfterSale',
label: '售后策略',
route: '/payment/after-sale',
status: 'available',
title: '售后策略',
desc: '售后功能开关、申请时效、超时效提示与申请表单(预置原因、所需材料、图片上传)配置。',
canonicalDomain: 'payment',
section: 'after_sale',
}),
],
}
+1 -11
View File
@@ -60,7 +60,7 @@ export const usersSection = {
route: '/users/verify',
component: 'UsersVerify',
title: '身份认证',
desc: '配置认证展示、申请类型与申请门槛;审核、驳回和取消认证统一在 WordPress 用户菜单处理。',
desc: '配置通用认证申请规则;申请审核和认证资料管理位于 WordPress 用户菜单。',
canonicalDomain: 'users',
section: 'verify',
}),
@@ -108,16 +108,6 @@ export const usersSection = {
canonicalDomain: 'users',
section: 'sms',
}),
schemaPage({
key: 'users-wechat',
name: 'UsersWechat',
label: '微信通知',
route: '/users/wechat',
title: '微信通知',
desc: '配置微信模板消息、订阅/解绑和站外通知策略。',
canonicalDomain: 'users',
section: 'wechat',
}),
customPage({
key: 'users-wechat-template',
name: 'UsersWechatTemplate',
+37 -44
View File
@@ -23,15 +23,13 @@
</n-form>
</n-card>
<n-card title="统一资产账本" class="aksrt-section-card">
<n-card title="用户近期资产记录" class="aksrt-section-card">
<n-space vertical :size="12">
<n-space wrap>
<n-select v-model:value="ledgerFilters.asset" :options="assetOptions" clearable placeholder="资产" style="width: 130px" />
<n-input v-model:value="ledgerFilters.transaction_id" placeholder="交易号" clearable style="width: 130px" />
<n-input v-model:value="ledgerFilters.operation_key" placeholder="幂等键" clearable style="width: 240px" />
<n-input v-model:value="ledgerFilters.ref_type" placeholder="引用类型" clearable style="width: 130px" />
<n-input v-model:value="ledgerFilters.ref_key" placeholder="引用键" clearable style="width: 180px" />
<n-input v-model:value="ledgerFilters.actor_user_id" placeholder="操作者 ID" clearable style="width: 130px" />
<n-input v-model:value="ledgerFilters.user_id" placeholder="用户 ID" clearable style="width: 130px" />
<n-select v-model:value="ledgerFilters.asset" :options="assetOptions" placeholder="资产" style="width: 130px" />
<n-input v-model:value="ledgerFilters.date_from" type="date" placeholder="开始日期" style="width: 160px" />
<n-input v-model:value="ledgerFilters.date_to" type="date" placeholder="结束日期" style="width: 160px" />
<n-button type="primary" :loading="ledgerLoading" @click="searchLedger">查询</n-button>
<n-button @click="resetLedger">重置</n-button>
</n-space>
@@ -40,13 +38,13 @@
:data="ledgerRows"
:loading="ledgerLoading"
:row-key="row => row.id"
:scroll-x="1600"
:scroll-x="1100"
size="small"
/>
<n-pagination
v-model:page="ledgerPage"
:page-count="ledgerPageCount"
:page-size="20"
:page-size="10"
@update:page="loadLedger"
/>
</n-space>
@@ -169,40 +167,41 @@ const ledgerRows = ref([])
const ledgerPage = ref(1)
const ledgerPageCount = ref(1)
const ledgerFilters = reactive({
asset: null,
transaction_id: '',
operation_key: '',
ref_type: '',
ref_key: '',
actor_user_id: '',
user_id: '',
asset: 'points',
date_from: '',
date_to: '',
})
const assetOptions = [
{ label: '积分', value: 'points' },
{ label: '余额', value: 'balance' },
]
const ledgerColumns = [
{ title: '交易号', key: 'transaction_label', width: 100, fixed: 'left' },
{ title: '幂等键', key: 'operation_key', width: 260, ellipsis: { tooltip: true } },
{ title: '资产', key: 'assets_label', width: 100 },
{ title: '原因', key: 'reason', width: 150, ellipsis: { tooltip: true } },
{ title: '业务引用', key: 'reference_label', width: 200, ellipsis: { tooltip: true } },
{ title: '操作者', key: 'actor_label', width: 100 },
{ title: '增减 / 手续费', key: 'delta_label', width: 180, ellipsis: { tooltip: true } },
{ title: '变动后余额', key: 'balance_label', width: 150, ellipsis: { tooltip: true } },
{ title: '备注', key: 'remark', width: 180, ellipsis: { tooltip: true } },
{ title: 'UTC 时间', key: 'created_at', width: 190 },
{ title: '类型', key: 'reason', width: 150 },
{ title: '订单号', key: 'ref_key', width: 200 },
{ title: '变动 / 变动后', key: 'delta_label', width: 190 },
{ title: '说明', key: 'remark', width: 240, ellipsis: { tooltip: true } },
{ title: '时间', key: 'created_at', width: 170 },
]
function formatAssetValue(asset, value) {
if (asset === 'balance') return typeof value === 'string' && /^[+-]?\d+\.\d{2}$/.test(value) ? value : '0.00'
const raw = String(value ?? '0')
return /^-?\d+$/.test(raw) ? raw : '0'
const raw = Number(value ?? 0)
if (!Number.isFinite(raw)) return asset === 'balance' ? '0.00' : '0'
if (asset !== 'balance') return String(raw)
const negative = raw < 0
const digits = String(Math.abs(Math.trunc(raw))).padStart(3, '0')
const whole = digits.slice(0, -2).replace(/^0+(?=\d)/, '')
return (negative ? '-' : '') + whole + '.' + digits.slice(-2)
}
async function loadLedger() {
ledgerLoading.value = true
try {
const params = { page: ledgerPage.value, per_page: 20 }
if (!/^\d+$/.test(String(ledgerFilters.user_id).trim())) {
message.warning('请输入用户 ID 后查询该用户近期记录')
return
}
const params = { page: ledgerPage.value, per_page: 10 }
Object.entries(ledgerFilters).forEach(([key, value]) => {
if (value !== null && String(value).trim() !== '') params[key] = value
})
@@ -210,23 +209,16 @@ async function loadLedger() {
const data = response?.data || response || {}
ledgerRows.value = (data.items || []).map((row) => {
const entries = Array.isArray(row.entries) ? row.entries : []
const entry = entries[0] || {}
return {
...row,
transaction_label: `#${row.id}`,
assets_label: (row.assets || []).join(' / '),
reference_label: [row.ref_type, row.ref_key].filter(Boolean).join(':') || '—',
actor_label: row.actor_user_id ? `#${row.actor_user_id}` : '系统',
delta_label: entries.map((entry) => {
const value = formatAssetValue(entry.asset, entry.delta)
const signed = entry.asset === 'points' && !value.startsWith('-') && value !== '0' ? `+${value}` : value
return `${entry.asset}:${signed}`
}).join(';'),
balance_label: entries.map((entry) => `${entry.asset}:${formatAssetValue(entry.asset, entry.balance_after)}`).join(';'),
delta_label: `${formatAssetValue(entry.asset, entry.delta)} / ${formatAssetValue(entry.asset, entry.balance_after)}`,
remark: row.remark || '—',
}
})
ledgerPageCount.value = Math.max(1, Math.ceil(Number(data.total || 0) / Number(data.per_page || 20)))
ledgerPageCount.value = Math.max(1, Math.ceil(Number(data.total || 0) / Number(data.per_page || 10)))
} catch (e) {
message.error(e.message || '加载账本失败')
message.error(e.message || '加载资产记录失败')
} finally {
ledgerLoading.value = false
}
@@ -238,8 +230,10 @@ function searchLedger() {
}
function resetLedger() {
Object.assign(ledgerFilters, { asset: null, transaction_id: '', operation_key: '', ref_type: '', ref_key: '', actor_user_id: '' })
searchLedger()
Object.assign(ledgerFilters, { user_id: '', asset: 'points', date_from: '', date_to: '' })
ledgerRows.value = []
ledgerPage.value = 1
ledgerPageCount.value = 1
}
async function loadCfg() {
@@ -270,6 +264,5 @@ async function saveCfg() {
onMounted(() => {
loadCfg()
loadLedger()
})
</script>
+37 -1
View File
@@ -6,7 +6,7 @@
:icon="pageMeta.icon"
:schema="pageMeta.groups"
:form-data="formData"
:dirty="dirty"
:dirty="isSmsPage ? isSmsSettingsDirty : dirty"
:loading="loading"
:saving="saving"
:load-error="loadError"
@@ -131,6 +131,7 @@ const showTestModal = ref(false)
const testLoading = ref(false)
const testResult = ref(null)
const serverSchema = ref(null)
const smsSavedSettings = ref('')
const captchaSecretsToClear = ref(new Set())
const captchaProviders = [
{ key: 'tencent', label: '腾讯云 Captcha', component: TencentConfig },
@@ -144,8 +145,17 @@ const captchaProviderTab = ref('tencent')
const resolvedDomain = computed(() => normalizeSchemaDomain(props.domain || route.meta?.canonicalDomain || route.meta?.schemaDomain || resolveDomainFromPath(route.path)))
const resolvedSection = computed(() => props.section || route.meta?.section || route.meta?.schemaSection || resolveSectionFromPath(route.path))
const isEmailPage = computed(() => resolvedDomain.value === 'global' && resolvedSection.value === 'email')
const isSmsPage = computed(() => resolvedDomain.value === 'users' && resolvedSection.value === 'sms')
const isPermissionsLogin = computed(() => resolvedDomain.value === 'permissions' && resolvedSection.value === 'login')
const isPermissionsModeration = computed(() => resolvedDomain.value === 'permissions' && resolvedSection.value === 'moderation')
const isSmsSettingsDirty = computed(() => smsSavedSettings.value !== '' && smsSavedSettings.value !== smsSettingsSnapshot(formData.value))
function smsSettingsSnapshot(data) {
const settings = { ...(data || {}) }
delete settings.sms_test_phone
delete settings.sms_test_send
return JSON.stringify(settings)
}
function resolveDomainFromPath(path) {
const parts = path.split('/').filter(Boolean)
@@ -216,6 +226,10 @@ const {
delete payload[key]
}
})
if (isSmsPage.value) {
delete payload.sms_test_phone
delete payload.sms_test_send
}
if (clearSecrets.length) payload.__clear_secrets = clearSecrets
if (isPermissionsLogin.value && captchaSecretsToClear.value.size) {
payload.__clear_secrets = [...(payload.__clear_secrets || []), ...captchaSecretsToClear.value]
@@ -226,12 +240,34 @@ const {
return response
},
defaults: defaults.value,
onLoaded: (data) => {
if (isSmsPage.value) smsSavedSettings.value = smsSettingsSnapshot(data)
},
onSaved: () => {
if (isSmsPage.value) smsSavedSettings.value = smsSettingsSnapshot(formData.value)
message.success('设置已保存')
},
})
async function handleFieldAction(key) {
if (key === 'sms_test_send' && isSmsPage.value) {
const phone = String(formData.value?.sms_test_phone || '').trim()
if (!phone) {
message.warning('请先填写测试手机号')
return
}
if (isSmsSettingsDirty.value) {
message.warning('请先保存当前配置,再发送测试短信')
return
}
try {
const response = await api.testSms(phone)
message.success(response?.message || '测试短信已发送')
} catch (error) {
message.error(error?.message || '测试短信发送失败')
}
return
}
if (key !== 'email_test_send' || !isEmailPage.value) return
const to = String(formData.value?.email_test_to || '').trim()
if (!to) {
@@ -1,316 +0,0 @@
<template>
<div class="aksrt-page-container">
<PageHeader title="AI 连接配置" desc="配置 AI 生成能力的连接方式。优先使用 WordPress AI Connectors;也可填写 OpenAI 兼容接口。SEO 生成入口在文章「独立 SEO」框,摘要/标签入口在前台投稿页。" />
<div v-if="loadError" class="aksrt-ai__form-card aksrt-ai__load-error" role="alert">
<strong>加载失败:</strong>{{ loadError }}
</div>
<div class="aksrt-ai__status">
<div class="aksrt-ai__status-card">
<div class="aksrt-ai__status-icon" :class="status.available ? 'aksrt-ai__status-icon--ok' : 'aksrt-ai__status-icon--off'">
<i class="fa fab fa-android"></i>
</div>
<div class="aksrt-ai__status-info">
<div class="aksrt-ai__status-label">可用状态</div>
<div class="aksrt-ai__status-value">{{ status.available ? '可用' : '不可用' }}</div>
</div>
</div>
<div class="aksrt-ai__status-card">
<div class="aksrt-ai__status-icon" :class="status.wp_ai_available ? 'aksrt-ai__status-icon--ok' : 'aksrt-ai__status-icon--warn'">
<i class="fa fas fa-plug"></i>
</div>
<div class="aksrt-ai__status-info">
<div class="aksrt-ai__status-label">WP AI Client</div>
<div class="aksrt-ai__status-value">{{ status.wp_ai_available ? '已就绪' : '不可用' }}</div>
</div>
</div>
<div class="aksrt-ai__status-card">
<div class="aksrt-ai__status-icon" :class="status.theme_available ? 'aksrt-ai__status-icon--ok' : 'aksrt-ai__status-icon--warn'">
<i class="fa fas fa-key"></i>
</div>
<div class="aksrt-ai__status-info">
<div class="aksrt-ai__status-label">主题 Provider</div>
<div class="aksrt-ai__status-value">{{ status.theme_available ? '已配置' : '未配置' }}</div>
</div>
</div>
<div class="aksrt-ai__status-card">
<div class="aksrt-ai__status-icon aksrt-ai__status-icon--info">
<i class="fa fas fa-road"></i>
</div>
<div class="aksrt-ai__status-info">
<div class="aksrt-ai__status-label">当前后端</div>
<div class="aksrt-ai__status-value">{{ backendLabel }}</div>
</div>
</div>
</div>
<div class="aksrt-ai__form-card">
<h3>主题 Provider 配置(回退链路)</h3>
<p class="aksrt-form-hint">已配置主题 Provider 时优先走下方接口;未配置时才尝试 WordPress AI Connectors。NewAPI/兼容网关请填到 <code>/v1</code>(也可只填域名,保存时会自动补全)。</p>
<div class="aksrt-form-group">
<label>
<input type="checkbox" v-model="form.enabled"> 启用主题 AI Provider
</label>
</div>
<div class="aksrt-form-group">
<label>AI Provider</label>
<select v-model="form.provider" class="aksrt-select">
<option value="none">不使用</option>
<option value="openai">OpenAI 兼容接口</option>
</select>
</div>
<div class="aksrt-form-group" v-if="form.provider !== 'none'">
<label>API Key</label>
<input v-model="form.api_key" type="password" class="aksrt-input" placeholder="sk-...">
<p class="aksrt-form-hint">保存后不会回显完整密钥</p>
</div>
<div class="aksrt-form-group" v-if="form.provider === 'openai'">
<label>API Base URL</label>
<input v-model="form.api_base" type="text" class="aksrt-input" placeholder="https://llm.example.com/v1">
<p class="aksrt-form-hint">示例:https://api.openai.com/v1 或 https://你的网关/v1(不要填到 /chat/completions)</p>
</div>
<div class="aksrt-form-group" v-if="form.provider !== 'none'">
<label>模型</label>
<input v-model="form.model" type="text" class="aksrt-input" placeholder="gpt-3.5-turbo">
</div>
<div class="aksrt-form-group" v-if="form.provider !== 'none'">
<label>最大 Token 数</label>
<input v-model.number="form.max_tokens" type="number" min="100" max="4000" class="aksrt-input">
</div>
<div class="aksrt-form-group" v-if="form.provider !== 'none'">
<label>每分钟请求限制</label>
<input v-model.number="form.rate_limit" type="number" min="1" max="100" class="aksrt-input">
</div>
<div class="aksrt-form-actions">
<button class="aksrt-button aksrt-button--primary" :disabled="saving" @click="saveSettings">
{{ saving ? '保存中...' : '保存设置' }}
</button>
</div>
</div>
<div class="aksrt-ai__form-card">
<h3>已接通能力</h3>
<table class="aksrt-table aksrt-table--sm">
<thead>
<tr>
<th>能力</th>
<th>入口</th>
<th>状态</th>
</tr>
</thead>
<tbody>
<tr v-for="feat in features" :key="feat.key">
<td>{{ feat.name }}</td>
<td>{{ feat.scene }}</td>
<td>
<span class="aksrt-badge" :class="feat.ready ? 'aksrt-badge--success' : 'aksrt-badge--warning'">
{{ feat.ready ? '可用' : '未就绪' }}
</span>
</td>
</tr>
</tbody>
</table>
<p class="aksrt-form-hint">SEO 开关与提示词请到「全局 → SEO」配置(seo_ai_enabled / ai_seo_*_prompt)。</p>
</div>
<div class="aksrt-ai__form-card aksrt-ai__notice">
<h3><i class="fa fas fa-exclamation-triangle"></i> 重要说明</h3>
<ul>
<li>AI 输出仅作为<strong>建议</strong>,不会自动写库;需用户点「更新/提交」才保存</li>
<li>主题 API Key 已配置时优先使用;否则回退 WordPress AI Connectors</li>
<li>生成接口要求登录与编辑/投稿权限;配置接口仍需管理员</li>
</ul>
</div>
</div>
</template>
<script>
import { ref, reactive, computed, onMounted } from 'vue';
import PageHeader from '@/components/PageHeader.vue';
import api from '@/api';
export default {
name: 'ExtensionAI',
components: { PageHeader },
setup() {
const status = ref({
enabled: false,
provider: 'none',
configured: false,
model: '',
api_key: '',
rate_limit: 10,
available: false,
backend: 'none',
wp_ai_available: false,
theme_available: false,
seo_ai_enabled: false,
write_assist: false,
});
const form = reactive({
enabled: false,
provider: 'none',
api_key: '',
api_base: 'https://api.openai.com/v1',
model: 'gpt-3.5-turbo',
max_tokens: 1000,
rate_limit: 10,
});
const saving = ref(false);
const loadError = ref('');
const backendLabel = computed(() => {
const map = { wp_ai: 'WP AI Client', theme: '主题 Provider', none: '无' };
return map[status.value.backend] || status.value.backend || '无';
});
const features = computed(() => {
const avail = !!status.value.available;
const seoReady = avail && !!status.value.seo_ai_enabled;
return [
{ key: 'seo', name: 'AI 生成 SEO', scene: '文章/分类「独立 SEO」框', ready: seoReady },
{ key: 'summary', name: 'AI 生成摘要', scene: '前台投稿页', ready: avail },
{ key: 'tags', name: 'AI 建议标签', scene: '前台投稿页', ready: avail },
];
});
function hydrateFromStatus(data) {
if (!data || typeof data !== 'object') return;
status.value = {
enabled: !!data.enabled,
provider: data.provider || 'none',
configured: !!data.configured,
model: data.model || '',
api_key: '',
rate_limit: data.rate_limit || 10,
available: !!data.available,
backend: data.backend || 'none',
wp_ai_available: !!data.wp_ai_available,
theme_available: !!data.theme_available,
seo_ai_enabled: !!data.seo_ai_enabled,
write_assist: !!data.write_assist,
};
form.enabled = !!data.enabled;
form.provider = data.provider || 'none';
form.model = data.model || 'gpt-3.5-turbo';
form.rate_limit = data.rate_limit || 10;
if (data.api_base) form.api_base = data.api_base;
if (typeof data.max_tokens === 'number' && data.max_tokens > 0) {
form.max_tokens = data.max_tokens;
}
}
async function loadStatus() {
loadError.value = '';
try {
const json = await api.getAiStatus();
hydrateFromStatus(json.data || {});
} catch (e) {
console.error('Load AI status failed:', e);
loadError.value = e?.message || '加载 AI 状态失败';
}
}
async function saveSettings() {
saving.value = true;
try {
const payload = {
enabled: form.enabled,
provider: form.provider,
api_base: form.api_base,
model: form.model,
max_tokens: form.max_tokens,
rate_limit: form.rate_limit,
};
// Only send api_key when the user entered a new one.
if (form.api_key && form.api_key !== '***') {
payload.api_key = form.api_key;
}
const json = await api.updateAiSettings(payload);
alert(json.message || '设置已保存');
await loadStatus();
} catch (e) {
console.error('Save failed:', e);
alert(e?.message || '保存失败');
} finally {
saving.value = false;
}
}
onMounted(() => loadStatus());
return { status, form, saving, loadError, features, backendLabel, saveSettings };
},
};
</script>
<style scoped>
.aksrt-ai__status {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
margin-bottom: 24px;
}
.aksrt-ai__status-card {
display: flex;
align-items: center;
gap: 12px;
padding: 16px;
background: #fff;
border-radius: 8px;
border: 1px solid #e8e8e8;
}
.aksrt-ai__status-icon {
width: 40px;
height: 40px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
}
.aksrt-ai__status-icon--ok { background: #d4edda; color: #155724; }
.aksrt-ai__status-icon--warn { background: #fff3cd; color: #856404; }
.aksrt-ai__status-icon--off { background: #f8d7da; color: #721c24; }
.aksrt-ai__status-icon--info { background: #d1ecf1; color: #0c5460; }
.aksrt-ai__status-value { font-size: 18px; font-weight: 600; color: var(--aksrt-color-text); }
.aksrt-ai__status-label { font-size: 12px; color: #888; }
.aksrt-ai__form-card {
background: #fff;
border: 1px solid #e8e8e8;
border-radius: 8px;
padding: 24px;
margin-bottom: 16px;
}
.aksrt-ai__form-card h3 { margin: 0 0 16px; font-size: 16px; font-weight: 600; }
.aksrt-form-hint { font-size: 12px; color: #888; margin-top: 4px; }
.aksrt-ai__load-error {
background: #fdecea;
border-color: #f5c2c0;
color: #842029;
margin-bottom: 16px;
}
.aksrt-ai__notice { background: #fff8e1; border-color: #ffe082; }
.aksrt-ai__notice h3 { color: #e65100; }
.aksrt-ai__notice ul { margin: 0; padding-left: 20px; }
.aksrt-ai__notice li { margin-bottom: 8px; font-size: 14px; }
.aksrt-ai__notice code {
background: rgba(0,0,0,0.06);
padding: 2px 6px;
border-radius: 3px;
font-size: 13px;
}
.aksrt-badge { display: inline-block; padding: 2px 8px; border-radius: 999px; font-size: 12px; }
.aksrt-badge--success { background: #d4edda; color: #155724; }
.aksrt-badge--warning { background: #fff3cd; color: #856404; }
.aksrt-table { width: 100%; border-collapse: collapse; }
.aksrt-table th, .aksrt-table td { border-bottom: 1px solid #eee; padding: 10px 8px; text-align: left; }
.aksrt-form-group { margin-bottom: 14px; }
.aksrt-input, .aksrt-select { width: 100%; max-width: 480px; padding: 8px 10px; }
.aksrt-button { padding: 8px 16px; border: 0; border-radius: 6px; cursor: pointer; }
.aksrt-button--primary { background: #1677ff; color: #fff; }
.aksrt-button:disabled { opacity: .6; cursor: not-allowed; }
</style>
+36 -300
View File
@@ -1,220 +1,52 @@
<template>
<div class="aksrt-page-container">
<PageHeader
title="身份认证"
desc="统一管理认证申请规则与云服务商凭证;审核、驳回和取消认证仍在 WordPress 用户菜单处理。"
/>
<PageHeader title="身份认证" desc="配置通用认证申请规则;申请审核和用户认证资料管理位于 WordPress 用户菜单。" />
<n-spin :show="loading" style="width: 100%; min-height: 240px">
<n-grid :cols="3" :x-gap="12" :y-gap="12" responsive="screen" item-responsive>
<n-grid-item span="3 m:1">
<n-card class="aksrt-status-card" size="small">
<n-statistic label="身份认证功能">
<template #default>{{ verifyForm.user_verify_enabled ? '已启用' : '已停用' }}</template>
</n-statistic>
<n-tag :type="verifyForm.user_verify_enabled ? 'success' : 'default'" round size="small">
{{ verifyForm.user_verify_enabled ? '运行中' : '未运行' }}
</n-tag>
</n-card>
</n-grid-item>
<n-grid-item span="3 m:1" v-for="key in providerKeys" :key="key">
<n-card class="aksrt-status-card" size="small">
<n-statistic :label="providerLabel(key) + '凭证'">
<template #default>{{ providerState(key).configured ? '已配置' : '未配置' }}</template>
</n-statistic>
<n-tag :type="providerState(key).configured ? 'success' : 'warning'" round size="small">
{{ identityForm.provider === key ? '当前服务商' : '备用服务商' }}
</n-tag>
</n-card>
</n-grid-item>
</n-grid>
<n-card class="aksrt-section-card" title="通用认证设置">
<n-form :model="verifyForm" label-placement="left" label-width="190">
<n-form-item label="启用身份认证"><n-switch v-model:value="verifyForm.user_verify_enabled" /></n-form-item>
<n-form-item label="公开页显示认证徽标"><n-switch v-model:value="verifyForm.user_verify_public_display" /></n-form-item>
<n-form-item label="允许用户前台申请"><n-switch v-model:value="verifyForm.user_verify_allow_apply" /></n-form-item>
<n-form-item label="云失败转人工"><n-switch v-model:value="verifyForm.user_verify_allow_manual_fallback" /></n-form-item>
<n-form-item label="认证会话有效期(分钟)"><n-input-number v-model:value="verifyForm.user_verify_session_ttl_minutes" :min="5" :max="1440" /></n-form-item>
<n-form-item label="结果轮询间隔(秒)"><n-input-number v-model:value="verifyForm.user_verify_poll_interval_seconds" :min="3" :max="60" /></n-form-item>
<n-form-item label="个人认证费用"><n-input v-model:value="verifyForm.user_verify_personal_fee" placeholder="0.00" /></n-form-item>
<n-form-item label="企业认证费用"><n-input v-model:value="verifyForm.user_verify_enterprise_fee" placeholder="0.00" /></n-form-item>
<n-form-item label="个人认证徽标文案">
<n-input v-model:value="verifyForm.user_verify_personal_badge_text" placeholder="个人认证" />
<n-card class="aksrt-section-card" title="认证申请设置">
<n-form :model="form" label-placement="left" label-width="190">
<n-form-item label="启用身份认证"><n-switch v-model:value="form.user_auth_s" /></n-form-item>
<n-form-item label="允许用户前台申请"><n-switch v-model:value="form.user_auth_apply_s" :disabled="!form.user_auth_s" /></n-form-item>
<n-form-item v-if="!form.user_auth_apply_s" label="停用申请说明">
<n-input v-model:value="form.user_auth_apply_disable_desc" type="textarea" :rows="2" />
</n-form-item>
<n-form-item label="企业认证徽标文案">
<n-input v-model:value="verifyForm.user_verify_enterprise_badge_text" placeholder="企业认证" />
<n-form-item v-if="form.user_auth_apply_s" label="申请最低用户等级">
<n-input-number v-model:value="form.user_auth_apply_limit_level" :min="0" :max="30" />
<template #feedback>设置为 0 时不限制用户等级。</template>
</n-form-item>
<n-form-item label="停用申请说明">
<n-input v-model:value="verifyForm.user_verify_disable_desc" type="textarea" :rows="2" />
</n-form-item>
<n-form-item label="申请介绍">
<n-input v-model:value="verifyForm.user_verify_desc" type="textarea" :rows="3" />
<n-form-item v-if="form.user_auth_apply_s" label="申请介绍">
<n-input v-model:value="form.user_auth_apply_desc" type="textarea" :rows="3" />
</n-form-item>
</n-form>
</n-card>
<n-card class="aksrt-section-card" title="云服务商">
<n-form :model="identityForm" label-placement="left" label-width="190">
<n-form-item label="当前服务商">
<n-radio-group v-model:value="identityForm.provider">
<n-space>
<n-radio-button value="aliyun">阿里云</n-radio-button>
<n-radio-button value="tencent">腾讯云</n-radio-button>
</n-space>
</n-radio-group>
</n-form-item>
<n-form-item label="回调地址">
<n-input-group>
<n-input :value="callbackUrlForCurrentProvider" readonly />
<n-button @click="copyCallback">复制</n-button>
</n-input-group>
<template #feedback>将此地址填入服务商控制台需要的回调地址配置。</template>
</n-form-item>
</n-form>
</n-card>
<n-tabs v-model:value="activeTab" type="segment" animated class="aksrt-provider-tabs">
<n-tab-pane v-for="key in providerKeys" :key="key" :name="key" :tab="providerLabel(key)">
<n-card class="aksrt-section-card">
<template #header>
<n-space align="center">
<span>{{ providerLabel(key) }}凭证</span>
<n-tag :type="providerState(key).configured ? 'success' : 'warning'" size="small" round>
{{ providerState(key).configured ? '已配置' : '未配置' }}
</n-tag>
</n-space>
</template>
<n-alert type="info" :show-icon="false" style="margin-bottom: 18px">
已保存凭证不会回显。留空表示保留原值;输入新值并保存会替换对应凭证。
</n-alert>
<n-form label-placement="left" label-width="190">
<n-form-item v-if="key === 'aliyun'" label="Region">
<n-input v-model:value="verifyForm.user_verify_aliyun_region" placeholder="cn-hangzhou" />
</n-form-item>
<n-form-item v-if="key === 'aliyun'" label="个人认证 SceneId">
<n-input v-model:value="verifyForm.user_verify_aliyun_personal_flow" placeholder="从阿里云实人认证控制台获取 SceneId" />
</n-form-item>
<n-form-item v-if="key === 'aliyun'" label="个人认证 ProductCode">
<n-select v-model:value="verifyForm.user_verify_aliyun_product_code" :options="aliyunProductOptions" />
</n-form-item>
<n-form-item v-if="key === 'aliyun'" label="企业认证业务标识">
<n-input v-model:value="verifyForm.user_verify_aliyun_enterprise_flow" placeholder="可留空,默认复用个人认证 SceneId" />
</n-form-item>
<n-form-item v-if="key === 'tencent'" label="Region">
<n-input v-model:value="verifyForm.user_verify_tencent_region" placeholder="ap-guangzhou" />
</n-form-item>
<n-form-item v-if="key === 'tencent'" label="个人认证 RuleId">
<n-input v-model:value="verifyForm.user_verify_tencent_personal_flow" placeholder="从腾讯云实名核身控制台获取 RuleId" />
</n-form-item>
<n-form-item v-if="key === 'tencent'" label="企业认证流程标识">
<n-input v-model:value="verifyForm.user_verify_tencent_enterprise_flow" placeholder="可留空,企业四要素后复用个人认证 RuleId" />
</n-form-item>
<n-form-item v-for="field in credentialFields[key]" :key="field.key" :label="field.label">
<n-input
v-model:value="credentials[key][field.key]"
type="password"
show-password-on="click"
:placeholder="credentialPlaceholder(key, field.key)"
autocomplete="new-password"
/>
</n-form-item>
<n-form-item label="显式清除凭证">
<n-checkbox v-model:checked="clearCredentials[key]">
保存时删除 {{ providerLabel(key) }}全部凭证
</n-checkbox>
<template #feedback>清除后该服务商立即变为未配置;此操作不会因输入框留空而触发。</template>
</n-form-item>
<n-form-item label="配置测试">
<n-button :loading="testing === key" :disabled="saving" @click="testProvider(key)">
测试 {{ providerLabel(key) }}配置
</n-button>
</n-form-item>
</n-form>
</n-card>
</n-tab-pane>
</n-tabs>
<div class="aksrt-form-actions">
<n-button :disabled="!dirty" @click="load">放弃更改</n-button>
<n-button type="primary" :loading="saving" @click="save">保存全部配置</n-button>
<n-button :disabled="!dirty || saving" @click="load">放弃更改</n-button>
<n-button type="primary" :disabled="!dirty" :loading="saving" @click="save">保存设置</n-button>
</div>
</n-spin>
</div>
</template>
<script setup>
import { computed, onMounted, reactive, ref, watch } from 'vue'
import { useDialog, useMessage } from 'naive-ui'
import { computed, onMounted, reactive, ref } from 'vue'
import { useMessage } from 'naive-ui'
import PageHeader from '@/components/PageHeader.vue'
import api from '@/api'
const defaults = {
user_auth_s: true,
user_auth_apply_s: true,
user_auth_apply_disable_desc: '由于系统正在维护,暂时无法申请身份认证\n详情请联系管理员',
user_auth_apply_limit_level: 0,
user_auth_apply_desc: '请填写认证身份、简介和申请说明,提交后由管理员审核。',
}
const message = useMessage()
const dialog = useDialog()
const providerKeys = ['aliyun', 'tencent']
const aliyunProductOptions = [
{ label: '实人认证(ID_PRO)', value: 'ID_PRO' },
{ label: '活体人脸验证(PV_FV)', value: 'PV_FV' },
{ label: '活体检测(LR_FR)', value: 'LR_FR' },
]
const credentialFields = {
aliyun: [
{ key: 'access_key_id', label: 'AccessKey ID' },
{ key: 'access_key_secret', label: 'AccessKey Secret' },
],
tencent: [
{ key: 'secret_id', label: 'SecretId' },
{ key: 'secret_key', label: 'SecretKey' },
],
}
const verifyDefaults = {
user_verify_enabled: true,
user_verify_public_display: true,
user_verify_allow_apply: true,
user_verify_allow_manual_fallback: true,
user_verify_session_ttl_minutes: 30,
user_verify_poll_interval_seconds: 5,
user_verify_personal_fee: '0.00',
user_verify_enterprise_fee: '0.00',
user_verify_personal_badge_text: '个人认证',
user_verify_enterprise_badge_text: '企业认证',
user_verify_disable_desc: '由于系统正在维护,暂时无法申请身份认证\n详情请联系管理员',
user_verify_desc: '认证用户会在昵称后显示认证徽章,同时享受多种优先待遇。申请请确保信息属实。',
user_verify_aliyun_region: 'cn-hangzhou',
user_verify_aliyun_personal_flow: '',
user_verify_aliyun_product_code: 'ID_PRO',
user_verify_aliyun_enterprise_flow: '',
user_verify_tencent_region: 'ap-guangzhou',
user_verify_tencent_personal_flow: '',
user_verify_tencent_enterprise_flow: '',
}
const form = reactive({ ...defaults })
const initial = ref('')
const loading = ref(true)
const saving = ref(false)
const testing = ref('')
const activeTab = ref('aliyun')
const callbackUrls = reactive({ aliyun: '', tencent: '' })
const callbackUrlForCurrentProvider = computed(() => callbackUrls[identityForm.provider] || '')
const initialSnapshot = ref('')
const states = reactive({ aliyun: {}, tencent: {} })
const verifyForm = reactive({ ...verifyDefaults })
const identityForm = reactive({ provider: 'aliyun' })
const credentials = reactive({ aliyun: {}, tencent: {} })
const clearCredentials = reactive({ aliyun: false, tencent: false })
const snapshot = computed(() => JSON.stringify(form))
const dirty = computed(() => initial.value !== '' && snapshot.value !== initial.value)
const snapshot = computed(() => JSON.stringify({ verifyForm, identityForm, credentials, clearCredentials }))
const dirty = computed(() => initialSnapshot.value !== '' && snapshot.value !== initialSnapshot.value)
watch(() => identityForm.provider, value => { activeTab.value = value })
function providerLabel(key) {
return key === 'aliyun' ? '阿里云' : '腾讯云'
}
function providerState(key) {
return states[key] || {}
}
function credentialPlaceholder(provider, field) {
return providerState(provider).configured_fields?.[field] ? '已配置,留空则保留' : '尚未配置'
}
function extractData(response) {
return response?.data ?? response ?? {}
}
@@ -222,123 +54,27 @@ function extractData(response) {
async function load() {
loading.value = true
try {
const [verifyResponse, identityResponse] = await Promise.all([
api.getSchemaPage('users', 'verify'),
api.getIdentitySettings(),
])
Object.assign(verifyForm, verifyDefaults, extractData(verifyResponse))
const identity = extractData(identityResponse)
identityForm.provider = providerKeys.includes(identity.provider) ? identity.provider : 'aliyun'
Object.assign(callbackUrls, identity.callback_url || {})
providerKeys.forEach(key => {
states[key] = identity.providers?.[key] || {}
credentials[key] = {}
clearCredentials[key] = false
})
activeTab.value = identityForm.provider
initialSnapshot.value = snapshot.value
Object.assign(form, defaults, extractData(await api.getSchemaPage('users', 'verify')))
initial.value = snapshot.value
} catch (error) {
message.error(error.message || '加载身份认证配置失败')
message.error(error.message || '加载身份认证设置失败')
} finally {
loading.value = false
}
}
function buildProviderPayload(key) {
const replacement = {}
credentialFields[key].forEach(({ key: field }) => {
const value = String(credentials[key]?.[field] || '').trim()
if (value) replacement[field] = value
})
return {
credentials: replacement,
clear_credentials: clearCredentials[key],
}
}
async function persist() {
const [verifyResponse, identityResponse] = await Promise.all([
api.saveSchemaPage('users', 'verify', { ...verifyForm }),
api.saveIdentitySettings({
provider: identityForm.provider,
providers: {
aliyun: buildProviderPayload('aliyun'),
tencent: buildProviderPayload('tencent'),
},
}),
])
const identity = extractData(identityResponse)
Object.assign(verifyForm, extractData(verifyResponse))
providerKeys.forEach(key => {
states[key] = identity.providers?.[key] || states[key]
credentials[key] = {}
clearCredentials[key] = false
})
Object.assign(callbackUrls, identity.callback_url || callbackUrls)
initialSnapshot.value = snapshot.value
}
async function save() {
const hasClear = providerKeys.some(key => clearCredentials[key])
const run = async () => {
saving.value = true
try {
await persist()
message.success('身份认证配置已保存')
} catch (error) {
message.error(error.message || '保存失败')
} finally {
saving.value = false
}
}
if (!hasClear) return run()
dialog.warning({
title: '确认清除凭证',
content: '已勾选的服务商凭证将被永久删除,是否继续保存?',
positiveText: '清除并保存',
negativeText: '取消',
onPositiveClick: run,
})
}
async function testProvider(provider) {
testing.value = provider
saving.value = true
try {
const response = await api.testIdentitySettings({ provider })
message.success(response?.message || `${providerLabel(provider)}配置测试通过`)
Object.assign(form, defaults, extractData(await api.saveSchemaPage('users', 'verify', { ...form })))
initial.value = snapshot.value
message.success('身份认证设置已保存')
} catch (error) {
message.error(error.message || `${providerLabel(provider)}配置测试失败`)
message.error(error.message || '保存身份认证设置失败')
} finally {
testing.value = ''
}
}
async function copyCallback() {
if (!callbackUrlForCurrentProvider.value) return message.warning('回调地址暂不可用')
try {
await navigator.clipboard.writeText(callbackUrlForCurrentProvider.value)
message.success('回调地址已复制')
} catch {
message.error('复制失败,请手动复制')
saving.value = false
}
}
onMounted(load)
</script>
<style scoped>
.aksrt-status-card :deep(.n-card__content) {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 12px;
}
.aksrt-section-card { margin-top: 16px; }
.aksrt-provider-tabs { margin-top: 16px; }
.aksrt-form-actions {
display: flex;
justify-content: flex-end;
gap: 12px;
margin-top: 20px;
}
</style>
@@ -1,3 +0,0 @@
export const extensionsPageLoaders = {
ExtensionAI: () => import('@/pages/extensions/ExtensionAI.vue'),
}
@@ -1,11 +1,9 @@
import { corePageLoaders } from './core'
import { usersPageLoaders } from './users'
import { toolsPageLoaders } from './tools'
import { extensionsPageLoaders } from './extensions'
export const componentLoaders = {
...corePageLoaders,
...usersPageLoaders,
...toolsPageLoaders,
...extensionsPageLoaders,
}
@@ -3,12 +3,16 @@
* Each key corresponds to a sub-page under the "扩展&增强" section.
*/
import { systemSchema } from './system.js'
import { ipSchema } from './ip.js'
import { auditSchema } from './audit.js'
import { pushSchema } from './push.js'
import { securitySchema } from './security.js'
export const extensionsSchemas = {
system: systemSchema,
ip: ipSchema,
audit: auditSchema,
push: pushSchema,
security: securitySchema,
}
@@ -2,21 +2,16 @@ export const pushSchema = {
key: 'extensions.push',
title: '百度推送',
icon: 'fa fas fa-paper-plane',
description: '配置搜索引擎主动推送、批量提交和状态追踪。',
description: '配置百度主动推送、收录通道和推送状态。',
defaults: {
baidu_push_enabled: false,
baidu_push_token: '',
baidu_push_auto: true,
baidu_push_on_publish: true,
baidu_push_on_update: false,
baidu_push_batch_size: 50,
baidu_push_normal: true,
baidu_push_fast: false,
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: [
{
@@ -25,7 +20,7 @@ export const pushSchema = {
icon: 'fa fas fa-rocket',
fields: [
{ 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',
fields: [
{ 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_update', label: '更新时推送', type: 'switch', default: false, 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 }, help: '已发布内容再次保存时,若上次推送未成功则重试;需主动再次提交请在编辑页勾选「重新提交」' },
],
},
{
key: 'xzh',
title: '熊掌号 / JSON-LD',
icon: 'fa fas fa-paw',
key: 'channel',
title: '收录通道',
icon: 'fa fas fa-inbox',
fields: [
{ key: 'xzh_on', label: '启用熊掌号', type: 'switch', default: false },
{ key: 'xzh_appid', label: '百度 AppID', type: 'input', default: '', dependsOn: { key: 'xzh_on', value: true } },
{ 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 } },
{ key: 'baidu_push_normal', label: '普通收录', type: 'switch', default: true, dependsOn: { key: 'baidu_push_enabled', value: true }, help: '每天可提交最多10万条有价值内容,收录速度较慢' },
{ key: 'baidu_push_fast', label: '快速收录', type: 'switch', default: false, dependsOn: { key: 'baidu_push_enabled', value: true }, help: '百度新推出的高效收录接口,目前仅对部分优质站点开放,请确保您的站点已开放快速收录功能' },
],
},
{
@@ -56,8 +47,7 @@ export const pushSchema = {
title: '高级设置',
icon: 'fa fas fa-cog',
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 } },
{ key: 'baidu_push_log_enabled', label: '记录推送日志', type: 'switch', default: true, dependsOn: { key: 'baidu_push_enabled', value: true }, help: 'WP_DEBUG 开启时,将推送结果写入错误日志' },
],
},
],
@@ -0,0 +1,105 @@
export const securitySchema = {
key: 'extensions.security',
title: '网站安全',
icon: 'fa fas fa-umbrella',
description: '防刷限制、验证码发送限制与站点安全加固。',
defaults: {
security_brush_post_10min: 20,
security_brush_post_day: 50,
security_brush_comment_10min: 20,
security_brush_comment_day: 100,
security_brush_upload_10min: 50,
security_brush_upload_day: 200,
security_brush_register_10min: 20,
security_brush_register_day: 100,
security_brush_order_10min: 30,
security_brush_order_day: 100,
security_captcha_send_cooldown: 60,
security_captcha_to_hour: 15,
security_captcha_to_day: 30,
security_captcha_ip_10min: 20,
security_captcha_ip_day: 50,
security_error_frequency_limit: true,
security_disabled_pingback: true,
security_disabled_xmlrpc: true,
security_user_disable_admin: true,
close_attachment_page: true,
},
groups: [
{
key: 'brush_post',
title: '[防刷机制]发布限制',
icon: 'fa fas fa-pen',
description: '限制单个用户发布文章、帖子、板块的总数量(为0则不限制)。',
fields: [
{ key: 'security_brush_post_10min', label: '10分钟内最多发布 (篇)', type: 'number', default: 20, min: 0, max: 200 },
{ key: 'security_brush_post_day', label: '一天内最多发布 (篇)', type: 'number', default: 50, min: 0, max: 2000 },
],
},
{
key: 'brush_comment',
title: '[防刷机制]评论限制',
icon: 'fa fas fa-comments',
description: '限制单个用户发布评论的总数量(为0则不限制)。',
fields: [
{ key: 'security_brush_comment_10min', label: '10分钟内最多评论 (条)', type: 'number', default: 20, min: 0, max: 200 },
{ key: 'security_brush_comment_day', label: '一天内最多评论 (条)', type: 'number', default: 100, min: 0, max: 2000, help: '如果启用了免登陆可评论,则根据用户IP地址判断' },
],
},
{
key: 'brush_upload',
title: '[防刷机制]上传限制',
icon: 'fa fas fa-upload',
description: '限制单个用户上传内容的总数量(为0则不限制)。防刷机制主要为了防止用户恶意多次提交内容,请根据网站的用户量合理设置,此机制不会限制管理员。',
fields: [
{ key: 'security_brush_upload_10min', label: '10分钟内最多上传 (个)', type: 'number', default: 50, min: 0, max: 200 },
{ key: 'security_brush_upload_day', label: '一天内最多上传 (个)', type: 'number', default: 200, min: 0, max: 2000 },
],
},
{
key: 'brush_register',
title: '[防刷机制]注册限制',
icon: 'fa fas fa-user-plus',
description: '限制每个IP地址用户注册数量,此项无法判断管理员身份,所以管理员也会受到限制。',
fields: [
{ key: 'security_brush_register_10min', label: '10分钟内最多注册 (个)', type: 'number', default: 20, min: 0, max: 200 },
{ key: 'security_brush_register_day', label: '一天内最多注册 (个)', type: 'number', default: 100, min: 0, max: 2000, help: '如果您的服务器不能正确的获取用户真实IP地址,请关闭此项(0为不限制)' },
],
},
{
key: 'brush_order',
title: '[防刷机制]下单限制',
icon: 'fa fas fa-shopping-cart',
description: '限制每个用户下单数量,未登录用户按IP地址判断。',
fields: [
{ key: 'security_brush_order_10min', label: '10分钟内最多下单 (个订单)', type: 'number', default: 30, min: 0, max: 200 },
{ key: 'security_brush_order_day', label: '一天内最多下单 (个订单)', type: 'number', default: 100, min: 0, max: 2000, help: '如果您的服务器不能正确的获取用户真实IP地址,请关闭此项(0为不限制)' },
],
},
{
key: 'brush_captcha',
title: '[防刷机制]验证码发送限制',
icon: 'fa fas fa-paper-plane',
description: '限制验证码邮件/短信的发送频率,防止恶意程序批量轰炸特定邮箱或手机号。',
fields: [
{ key: 'security_captcha_send_cooldown', label: '同一收件人/IP发送冷却 (秒)', type: 'number', default: 60, min: 0, max: 600, help: '同一邮箱或手机号、同一IP地址发送验证码后,需间隔此时间才可再次发送(0为不限制)' },
{ key: 'security_captcha_to_hour', label: '同一收件人1小时内最多 (次)', type: 'number', default: 15, min: 0, max: 500 },
{ key: 'security_captcha_to_day', label: '同一收件人一天内最多 (次)', type: 'number', default: 30, min: 0, max: 1000 },
{ key: 'security_captcha_ip_10min', label: '同一IP 10分钟内最多 (次)', type: 'number', default: 20, min: 0, max: 500 },
{ key: 'security_captcha_ip_day', label: '同一IP一天内最多 (次)', type: 'number', default: 50, min: 0, max: 2000, help: '如果您的服务器不能正确的获取用户真实IP地址,请关闭此项(0为不限制)' },
],
},
{
key: 'harden',
title: '安全加固',
icon: 'fa fas fa-shield-alt',
fields: [
{ key: 'security_error_frequency_limit', label: '错误频率限制', type: 'switch', default: true, help: '用户进行密码、邮箱、手机号、验证码等验证时,验证错误次数超过5次后限制30秒验证一次,错误超过10次后,限制3分钟验证一次,有效防止暴力破解' },
{ key: 'security_disabled_pingback', label: '防pingback攻击', type: 'switch', default: true },
{ key: 'security_disabled_xmlrpc', label: '禁用XML-RPC', type: 'switch', default: true, help: '如果无需使用xmlrpc功能,建议直接删除WordPress目录下的xmlrpc.php文件' },
{ key: 'security_user_disable_admin', label: '非管理员禁止进入后台', type: 'switch', default: true, help: '为了安全,如非必要请勿允许非管理员进入后台' },
{ key: 'close_attachment_page', label: '非管理员屏蔽附件页面', type: 'switch', default: true, help: '非管理员前台不显示附件(图片、文件等)页面,直接显示404,能有效提高文件的安全性' },
],
},
],
}
@@ -0,0 +1,91 @@
export const systemSchema = {
key: 'extensions.system',
title: '系统工具',
icon: 'fa fas fa-gavel',
description: 'WordPress 优化、上传优化与编辑器显示的系统级开关。',
defaults: {
hide_admin_bar: true,
remove_emoji: true,
remove_open_sans: true,
remove_more_wp_head: true,
remove_dashboard_widgets: true,
display_wp_update: true,
newfilename: false,
newfilename_type: 'time',
split_upload_s: true,
split_minimum_size: 20,
disabled_large_thumbnail: true,
admin_user_del_fields: true,
admin_frontend_set: true,
close_gutenberg: false,
disabled_autoembed: true,
mce_float_toolbar: ['pc_s', 'm_s'],
time_ago_s: true,
time_format: 'n月j日 H:i',
},
groups: [
{
key: 'optimize',
title: 'WordPress 优化',
icon: 'fa fas fa-bolt',
description: '精简WordPress自带的冗余功能,提升前台与后台的加载速度。',
fields: [
{ key: 'hide_admin_bar', label: '前台关闭顶部admin_bar', type: 'switch', default: true, help: '开启后前台不显示WordPress顶部黑条' },
{ key: 'remove_emoji', label: '删除WordPress自带Emoji表情', type: 'switch', default: true },
{ key: 'remove_open_sans', label: '禁用Google字体', type: 'switch', default: true },
{ key: 'remove_more_wp_head', label: '清理多余的头部Meta标签', type: 'switch', default: true },
{ key: 'remove_dashboard_widgets', label: 'WordPress 后台加速', type: 'switch', default: true, help: '移除仪表盘的站点健康和WordPress活动等不必要功能,并禁止检测浏览器和PHP版本(这两个非常耗时),优化后台加载速度' },
{ key: 'display_wp_update', label: '禁止WordPress检测更新', type: 'switch', default: true, help: '开启后将禁用WordPress核心、插件和主题的自动更新及更新检测' },
],
},
{
key: 'upload',
title: '上传优化',
icon: 'fa fas fa-cloud-upload-alt',
description: '上传文件的命名与缩略图裁剪优化,减少硬盘占用并提高上传效率。',
fields: [
{ key: 'split_upload_s', label: '大文件分片上传', type: 'switch', default: true, help: '分片上传大文件,优化大文件上传效率(支持断点续传)' },
{ key: 'split_minimum_size', label: '分片上传最小文件', type: 'number', default: 20, min: 4, max: 100, unit: 'M', dependsOn: { key: 'split_upload_s', value: true }, help: '大于此大小的文件才会分片上传,请根据服务器性能合理设置;较大文件直接上传会比较耗时且无法突破系统上传大小限制,分片上传可以有效解决此问题;前端上传大小限制在「功能权限 → 上传权限」中设置。注意:能否顺利上传大文件还取决于服务器性能、带宽、CDN等因素,如果网站使用了OSS/COS等第三方云储存,则不建议在网站上传大文件' },
{ key: 'newfilename', label: '上传文件自动重命名', type: 'switch', default: false, help: '上传文件时系统会查询重名文件,开启此功能后可减少重名文件,从而提高上传效率' },
{
key: 'newfilename_type',
label: '重命名格式',
type: 'radio',
buttonStyle: false,
default: 'time',
dependsOn: { key: 'newfilename', value: true },
options: [
{ label: '随机字串符', value: 'random' },
{ label: '时间+原文件名', value: 'time' },
],
},
{ key: 'disabled_large_thumbnail', label: '禁止WP大尺寸缩略图裁剪', type: 'switch', default: true, help: '开启后将禁用WordPress自动裁剪medium_large、1536x1536、2048x2048的缩略图,可减少服务器硬盘压力,同时提高上传效率;如果您使用的插件必须要这些尺寸的缩略图,或使用了云储存的图片处理功能,则请关闭' },
],
},
{
key: 'editor',
title: '编辑器与显示',
icon: 'fa fas fa-edit',
description: '编辑器模式、用户资料页与前台时间显示格式的定制。',
fields: [
{ key: 'admin_user_del_fields', label: '关闭后台用户编辑多余的选项', type: 'switch', default: true, help: '开启后在后台编辑用户资料时将不显示无用的多余选项' },
{ key: 'admin_frontend_set', label: '前端页面编辑', type: 'switch', default: true, help: '管理员可在前台快速修改页面、文章参数' },
{ key: 'close_gutenberg', label: '禁用古腾堡编辑器', type: 'switch', default: false, help: '后台文章编辑器将退回使用经典编辑器;禁用后主题的编辑器增强功能将不能使用,请酌情禁用!' },
{ key: 'disabled_autoembed', label: '禁用Auto Embed', type: 'switch', default: true, help: 'Auto Embed是WordPress自动检测并嵌入链接的一种功能,主要适配海外媒体网站,国内几乎用不上,建议禁用,可大幅度提高页面渲染速度' },
{
key: 'mce_float_toolbar',
label: '经典编辑器增强浮动工具栏',
type: 'checkbox',
default: ['pc_s', 'm_s'],
options: [
{ label: 'PC端开启', value: 'pc_s' },
{ label: '移动端开启', value: 'm_s' },
],
help: '开启后前端编辑器会显示浮动的工具栏,更加方便书写',
},
{ key: 'time_ago_s', label: '倒计时显示', type: 'switch', default: true, help: '时间格式化为:X分钟前,X小时前,X天前....' },
{ key: 'time_format', label: '自定义时间格式', type: 'input', default: 'n月j日 H:i', dependsOn: { key: 'time_ago_s', operator: 'falsy' }, help: '关闭倒计时显示后生效,时间格式接受标准PHP时间格式,请注意控制长度!' },
],
},
],
}
+2 -17
View File
@@ -2,7 +2,7 @@ export const seoSchema = {
key: 'global.seo',
title: 'SEO 优化',
icon: 'fa fas fa-search',
description: '站点标题、描述、关键词与 AI SEO。AI 生成入口在文章「独立 SEO」框;连接配置在「扩展 → AI 连接配置」。优先使用 WordPress AI Connectors。',
description: '站点标题、描述、关键词与 AI SEO。AI 生成入口在文章「独立 SEO」框,连接由 WordPress AI Connectors 提供。',
defaults: {
seo_enabled: true,
seo_ai_enabled: false,
@@ -13,10 +13,6 @@ export const seoSchema = {
ai_seo_title_prompt: '',
ai_seo_keywords_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_default: '',
seo_image_always_default: false,
@@ -48,7 +44,7 @@ export const seoSchema = {
label: '启用 AI SEO 生成',
type: 'switch',
default: false,
help: '开启后文章/分类「独立 SEO」框显示「AI 生成 SEO」按钮。需先配置 AI 连接(WP Connectors 或扩展 → AI 连接配置)。',
help: '开启后文章/分类「独立 SEO」框显示「AI 生成 SEO」按钮。需先在 WordPress 配置 AI Connectors。',
dependsOn: { key: 'seo_enabled', value: true },
},
{ key: 'seo_connector', label: '标题连接符', type: 'input', default: '-', placeholder: '例如:- 或 |', help: 'AI 生成标题时会用此连接符追加站点名。' },
@@ -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',
title: '图片 SEO',
@@ -35,6 +35,8 @@ export const affiliateSchema = {
pay_rebate_poster_img: '',
pay_rebate_poster_title: '发现一个精品网站,快来看一看',
pay_rebate_poster_desc: '网站有超多精品资源和优惠福利,扫码进入网站还可享受诸多优惠哦',
pay_rebate_poster_banner: '',
pay_rebate_poster_desc_bottom: '',
pay_rebate_text_discount: '推荐购买 下单再减%discount%',
},
groups: [
@@ -173,6 +175,8 @@ export const affiliateSchema = {
{ key: 'pay_rebate_poster_img', label: '推广海报顶部图片', type: 'media', default: '', dependsOn: { key: 'pay_rebate_poster_s', value: true } },
{ key: 'pay_rebate_poster_title', label: '推广海报标题(20-30个字)', type: 'input', default: '发现一个精品网站,快来看一看', dependsOn: { key: 'pay_rebate_poster_s', value: true } },
{ key: 'pay_rebate_poster_desc', label: '推广海报简介(40-60个字)', type: 'input', default: '网站有超多精品资源和优惠福利,扫码进入网站还可享受诸多优惠哦', dependsOn: { key: 'pay_rebate_poster_s', value: true } },
{ key: 'pay_rebate_poster_banner', label: '海报顶部备用图片', type: 'media', default: '', dependsOn: { key: 'pay_rebate_poster_s', value: true }, help: '顶部主图加载失败时依次回退到这里,再回退到主题内置图,避免海报顶部出现空白。' },
{ key: 'pay_rebate_poster_desc_bottom', label: '海报底部文案', type: 'input', default: '扫描二维码阅读全文', dependsOn: { key: 'pay_rebate_poster_s', value: true }, help: '画在二维码下方居中位置,留空则使用默认文案。' },
],
},
{
@@ -0,0 +1,80 @@
/**
* 售后策略 — 售后开关、申请时效、超时效提示与申请表单配置。
*
* 字段键、顺序、类型与默认值需与 src/Admin/Schema/SchemaPayment.php 的
* after_sale 分区保持 1:1(tests/widget-system/after-sale-policy-contract.test.cjs 守护)。
*/
export const afterSaleSchema = {
key: 'payment.after_sale',
title: '售后策略',
icon: 'fa fas fa-undo-alt',
description: '售后功能开关、申请时效、超时效提示与申请表单(预置原因、申请说明、所需材料、图片上传)配置。',
defaults: {
after_sale_s: true,
after_sale_time_limit: 7,
after_sale_expired_text: '<div>抱歉,您的订单已超过售后申请时效</div>',
after_sale_reasons: [
{ t: '质量问题' },
{ t: '假冒品牌' },
{ t: '其他原因' },
],
after_sale_desc: '',
after_sale_materials: [
{ t: '联系邮箱' },
{ t: '联系电话' },
],
after_sale_reason_placeholder: '必填,请您详细填写申请原因',
after_sale_img_s: true,
},
groups: [
{
key: 'main',
title: '售后功能',
icon: 'fa fas fa-undo-alt',
fields: [
{ key: 'after_sale_s', label: '售后功能', type: 'switch', default: true, help: '开启后用户可以申请售后;关闭后不再处理售后,前台「我的订单」不再显示申请售后按钮' },
{ key: 'after_sale_time_limit', label: '申请售后时效(天)', type: 'number', default: 7, min: 0, max: 3650, help: '订单支付后多少天内可以申请售后,超过时效将无法申请;0 表示不限制' },
{ key: 'after_sale_expired_text', label: '超过时效提示', type: 'textarea', sanitize_type: 'html', default: '<div>抱歉,您的订单已超过售后申请时效</div>', rows: 3, help: '订单超过售后时效后,用户点击申请售后时弹窗提示的内容(支持HTML代码,请注意代码规范)' },
],
},
{
key: 'form',
title: '申请表单',
icon: 'fa fas fa-file-alt',
fields: [
{
key: 'after_sale_reasons',
label: '预置原因',
type: 'group',
button_title: '添加原因',
help: '用户申请售后时可选择的原因',
default: [
{ t: '质量问题' },
{ t: '假冒品牌' },
{ t: '其他原因' },
],
fields: [
{ key: 't', label: '原因', type: 'input', default: '' },
],
},
{ key: 'after_sale_desc', label: '申请售后说明', type: 'textarea', sanitize_type: 'html', default: '', rows: 5, help: '用户申请售后时显示的内容(支持HTML代码,请注意代码规范)' },
{
key: 'after_sale_materials',
label: '申请所需材料',
type: 'group',
button_title: '添加材料',
help: '用户提交售后申请时需要提供的资料,如联系邮箱、联系电话;此处的信息收集,仅用于售后联系与审核',
default: [
{ t: '联系邮箱' },
{ t: '联系电话' },
],
fields: [
{ key: 't', label: '资料项', type: 'input', default: '' },
],
},
{ key: 'after_sale_reason_placeholder', label: '申请说明占位文案', type: 'input', default: '必填,请您详细填写申请原因', help: '前台申请售后弹窗中「申请说明」输入框的提示文字' },
{ key: 'after_sale_img_s', label: '申请售后可上传图片', type: 'switch', default: true, help: '开启后申请售后弹窗中出现图片上传入口(最多上传3张)' },
],
},
],
}
@@ -9,6 +9,7 @@ import { revenueSchema } from './revenue.js'
import { affiliateSchema } from './affiliate.js'
import { gatewaySchema } from './gateway.js'
import { withdrawalSchema } from './withdrawal.js'
import { afterSaleSchema } from './afterSale.js'
export const paymentSchemas = {
knowledge: knowledgeSchema,
@@ -18,4 +19,5 @@ export const paymentSchemas = {
affiliate: affiliateSchema,
gateway: gatewaySchema,
withdrawal: withdrawalSchema,
after_sale: afterSaleSchema,
}
@@ -25,13 +25,10 @@ export const knowledgeSchema = {
payment_show_pay_time: true,
payment_order_status_pending: '待支付',
payment_order_status_paid: '已支付',
payment_order_status_refunding: '退款中',
payment_order_status_refunded: '已退款',
payment_order_status_closed: '已关闭',
pay_service: [],
down_alone_page: false,
down_user_log: 50,
down_post_log: 50,
down_remote_to_local_s: true,
down_remote_to_local_size: 2048,
down_remote_to_local_ext: 'zip,rar,7z,text,png,jpg,jpeg,gif,mp3,mp4,avi,wmv,flv,swf,doc,docx,xls,xlsx,ppt,pptx,pdf',
@@ -124,7 +121,7 @@ export const knowledgeSchema = {
{ key: 'pay_mark', label: '付费标记', type: 'input', default: '付费' },
{ key: 'payment_show_ip', label: '显示用户IP', type: 'switch', default: false },
{ key: 'payment_show_device', label: '显示设备信息', type: 'switch', default: false },
{ key: 'payment_show_order_no', label: '显示流水号', type: 'switch', default: true },
{ key: 'payment_show_order_no', label: '显示订单号', type: 'switch', default: true },
{ key: 'payment_show_create_time', label: '显示创建时间', type: 'switch', default: true },
{ key: 'payment_show_pay_time', label: '显示支付时间', type: 'switch', default: true },
],
@@ -136,7 +133,6 @@ export const knowledgeSchema = {
fields: [
{ key: 'payment_order_status_pending', label: '待支付状态文案', type: 'input', default: '待支付' },
{ key: 'payment_order_status_paid', label: '已支付状态文案', type: 'input', default: '已支付' },
{ key: 'payment_order_status_refunding', label: '退款中状态文案', type: 'input', default: '退款中' },
{ key: 'payment_order_status_refunded', label: '已退款状态文案', type: 'input', default: '已退款' },
{ key: 'payment_order_status_closed', label: '已关闭状态文案', type: 'input', default: '已关闭' },
],
@@ -174,8 +170,6 @@ export const knowledgeSchema = {
title: '付费下载',
fields: [
{ key: 'down_alone_page', label: '独立下载页面', type: 'switch', default: false, help: '开启后,付费资源的下载链接会跳转独立页面显示' },
{ key: 'down_user_log', label: '保存用户下载记录(条)', type: 'number', default: 50, min: 0, help: '填0则不保存,注意记录量太大可能会影响网站性能' },
{ key: 'down_post_log', label: '保存文章下载记录(条)', type: 'number', default: 50, min: 0, help: '填0则不保存,注意记录量太大可能会影响网站性能' },
{ key: 'down_remote_to_local_s', label: '远程文件转本地文件下载', type: 'switch', default: true, help: '当下载地址为外链文件时,先将远程文件下载到本地服务器,再提供给用户下载,可完全隐藏远程下载地址' },
{ key: 'down_remote_to_local_size', label: '文件大小限制(Kb)', type: 'number', default: 2048, min: 0 },
{ key: 'down_remote_to_local_ext', label: '文件格式限制', type: 'input', default: 'zip,rar,7z,text,png,jpg,jpeg,gif,mp3,mp4,avi,wmv,flv,swf,doc,docx,xls,xlsx,ppt,pptx,pdf', help: '限制使用此功能的文件格式,多个用英文逗号隔开' },
@@ -111,7 +111,7 @@ export const rechargeSchema = {
{ key: 'points_pass_exchange_desc', label: '卡密兑换积分说明', type: 'textarea', default: '', rows: 2, dependsOn: { key: 'points_pass_exchange_s', value: true }, help: '用户使用卡密兑换积分时显示的内容,建议为提醒事项、兑换协议等,支持HTML代码' },
{ key: 'points_pass_exchange_only_password', label: '积分卡密单密码模式', type: 'switch', default: false, dependsOn: { key: 'points_pass_exchange_s', value: true }, help: '积分卡密仅需输入密码,不需要卡号' },
{ key: 'pay_vip_pass_charge_s', label: '启用卡密兑换会员功能', type: 'switch', default: false, help: '开启后用户可在会员购买/续费弹窗选择「卡密」渠道,使用会员卡密直接兑换会员' },
{ key: 'pay_vip_pass_charge_desc', label: '卡密兑换会员说明', type: 'textarea', default: '', rows: 2, dependsOn: { key: 'pay_vip_pass_charge_s', value: true }, help: '用户使用卡密兑换会员时显示的内容,建议为提醒事项、兑换协议等,支持HTML代码' },
{ key: 'pay_vip_pass_charge_desc', label: '卡密兑换会员说明', type: 'textarea', sanitize_type: 'html', default: '', rows: 2, dependsOn: { key: 'pay_vip_pass_charge_s', value: true }, help: '用户使用卡密兑换会员时显示的内容,建议为提醒事项、兑换协议等,支持HTML代码' },
{ key: 'pay_vip_pass_charge_only_password', label: '会员卡密单密码模式', type: 'switch', default: false, dependsOn: { key: 'pay_vip_pass_charge_s', value: true }, help: '会员卡密仅需输入密码,不需要卡号;默认关闭,会员卡需输入卡号和密码(与其他卡密兑换一致)' },
],
},
+342 -63
View File
@@ -1,27 +1,127 @@
/**
* VIP会员 — 参考主题结构:会员等级、会员权益(普通/一级/二级 tab)、
* 会员购买选项(一级/二级/续费/升级 tab)、卡密兑换与积分兑换。
* VIP会员 — 结构 1:1 对照参考主题 Zibll:会员等级(开关)、会员权益
* (普通/一级/二级 tab)、会员购买选项(一级/二级/续费/升级 tab)、
* 积分兑换会员与界面文案。
*
* 会员商品 / 续费商品 / 升级价格 / 积分兑换明细均为结构化 group 或
* accordion,不再使用 `|` 分隔的 textarea。等级 Tab 标题通过 titleKey
* 绑定「会员显示名称」,改名即改 Tab 名。
*/
const VIP_EQUITY_DEFAULT = '<li>全站资源折扣购买</li><li>部分内容免费阅读</li><li>一对一技术指导</li><li>VIP用户专属QQ群</li>';
const vipTagPalette = [
{ label: '#f74b3d', value: 'b-red' },
{ label: '#f3920a', value: 'b-yellow' },
{ label: '#08c4c1', value: 'b-cyan' },
{ label: '#0a8cf3', value: 'b-blue' },
{ label: '#1fd05a', value: 'b-green' },
{ label: '#c133f5', value: 'b-purple' },
{ label: '#121517', value: 'b-black' },
{ label: 'linear-gradient(135deg, #ffbeb4 10%, #f61a1a 100%)', value: 'jb-red' },
{ label: 'linear-gradient(135deg, #ff5e7f 30%, #ff967e 100%)', value: 'jb-pink' },
{ label: 'linear-gradient(135deg, #ffd6b2 10%, #ff651c 100%)', value: 'jb-yellow' },
{ label: 'linear-gradient(140deg, #039ab3 10%, #58dbcf 90%)', value: 'jb-cyan' },
{ label: 'linear-gradient(135deg, #b6e6ff 10%, #198aff 100%)', value: 'jb-blue' },
{ label: 'linear-gradient(135deg, #ccffcd 10%, #52bb51 100%)', value: 'jb-green' },
{ label: 'linear-gradient(135deg, #fec2ff 10%, #d000de 100%)', value: 'jb-purple' },
{ label: 'linear-gradient(25deg, #eab869 10%, #fbecd4 60%, #ffe0ae 100%)', value: 'jb-vip1' },
{ label: 'linear-gradient(317deg, #4d4c4c 30%, #878787 70%, #5f5c5c 100%)', value: 'jb-vip2' },
];
const vipUnitOptions = [
{ label: '天', value: 'day' },
{ label: '月', value: 'month' },
];
// 会员商品/续费商品的手风琴配置:默认收起,标题行显示价格与时长摘要。
const vipProductGroupOptions = {
collapsible: true,
defaultCollapsed: true,
listPreview: { prefix: '价格:', titleKey: 'price', descKey: 'time', descUnitKey: 'unit' },
};
// 积分兑换明细的手风琴配置:默认收起,标题行显示积分与时长摘要。
const vipPointsGroupOptions = {
collapsible: true,
defaultCollapsed: true,
listPreview: { prefix: '积分:', titleKey: 'points', descKey: 'time', descUnitKey: 'unit' },
};
/**
* 会员商品字段集(参考主题 CFS_Module::vip_product)。
*/
function vipProductFields() {
return [
{ key: 'price', label: '执行价', type: 'number', sanitize_type: 'money_decimal', default: '699.00', min: 0, suffix: '元', help: '会员的实际购买价格' },
{ key: 'show_price', label: '原价', type: 'number', sanitize_type: 'money_decimal', default: '999.00', min: 0, suffix: '元', help: '显示在执行价格前面,并划掉' },
{ key: 'tag', label: '促销标签', type: 'textarea', sanitize_type: 'html', default: '', rows: 1, help: '支持HTML,请注意控制长度' },
{ key: 'tag_class', label: '标签颜色', type: 'palette', default: 'jb-yellow', dependsOn: { key: 'tag', operator: 'not_empty' }, options: vipTagPalette },
{ key: 'time', label: '会员有效时间', type: 'spinner', default: 3, min: 0, max: 3600, step: 1, help: '开通会员的时长。填0则为永久会员' },
{ key: 'unit', label: '时长单位', type: 'radio', default: 'month', inline: true, dependsOn: { key: 'time', operator: 'truthy' }, options: vipUnitOptions },
{ key: '_time_permanent_hint', label: '', type: 'alert', alertType: 'success', ui_only: true, dependsOn: { key: 'time', operator: 'eq', value: 0 }, content: '会员有效时间已设置为:永久会员' },
];
}
/**
* 会员等级 Tab 字段集(参考主题 CFS_Module::vip_tab,显示名称并入 Tab)。
*/
function vipLevelTabFields(level) {
const dep = { key: `pay_user_vip_${level}_s`, value: true };
return [
{ key: `pay_user_vip_${level}_name`, label: '会员显示名称', type: 'input', sanitize_type: 'html', default: level === 1 ? '黄金会员' : '钻石会员', dependsOn: dep, help: '支持HTML,请注意控制长度' },
{ key: `pay_user_vip_${level}_desc`, label: '一句话简介', type: 'input', sanitize_type: 'html', default: '尊享专属特权', dependsOn: dep, help: '支持HTML' },
{ key: `pay_user_vip_${level}_equity`, label: '会员权益 HTML', type: 'textarea', sanitize_type: 'html', default: VIP_EQUITY_DEFAULT, rows: 4, dependsOn: dep, help: '支持安全HTML,建议使用 <li> 列表项' },
{ key: `vip_${level}_product_s`, label: '购买会员', type: 'switch', default: true, dependsOn: dep, help: '关闭后则不能付费购买会员,但可通过积分兑换会员,请确保已开启积分以及积分兑换会员功能' },
{
key: `vip_${level}_product`,
label: '会员商品',
type: 'group',
max: 8,
button_title: '添加会员商品',
default: [
{ price: 99, show_price: 199, tag: '<i class="fa fas fa-fw fa-bolt"></i> 限时特惠', time: 3, unit: 'month' },
{ price: 199, show_price: 299, tag: '<i class="fa fas fa-fw fa-bolt"></i> 站长推荐', time: 6, unit: 'month' },
],
...vipProductGroupOptions,
dependsOn: { all: [dep, { key: `vip_${level}_product_s`, value: true }] },
help: '会员购买价格配置:执行价、原价、促销标签、标签颜色与会员有效时间',
fields: vipProductFields(),
},
{ key: `vip_${level}_img_icon`, label: '会员图标', type: 'upload', default: '', dependsOn: dep, help: `留空使用内置 vip-${level}.svg 图标;建议尺寸300x300` },
];
}
export const vipSchema = {
key: 'payment.vip',
title: 'VIP会员',
icon: 'fa fas fa-gem',
description: '双级会员:等级名称、权益(下载次数与限速)、套餐、续费/升级与积分兑换。',
description: '双级会员:等级开关、权益(下载次数与限速)、会员商品、续费/升级与积分兑换。',
defaults: {
nav_pay_vip: true,
nav_user_pay_vip: true,
pay_user_vip_1_s: true,
pay_user_vip_1_name: '一级会员',
pay_user_vip_1_desc: '尊享一级会员特权',
pay_user_vip_1_equity: '<li>免费下载部分资源</li><li>会员专属身份标识</li><li>付费内容会员价</li>',
pay_user_vip_1_name: '黄金会员',
pay_user_vip_1_desc: '尊享专属特权',
pay_user_vip_1_equity: VIP_EQUITY_DEFAULT,
vip_1_product_s: true,
vip_1_product: [
{ price: 99, show_price: 199, tag: '<i class="fa fas fa-fw fa-bolt"></i> 限时特惠', time: 3, unit: 'month' },
{ price: 199, show_price: 299, tag: '<i class="fa fas fa-fw fa-bolt"></i> 站长推荐', time: 6, unit: 'month' },
],
vip_1_img_icon: '',
pay_user_vip_2_s: true,
pay_user_vip_2_name: '二级会员',
pay_user_vip_2_desc: '尊享二级会员特权',
pay_user_vip_2_equity: '<li>更高免费下载额度</li><li>更高等级身份标识</li><li>更多付费内容会员价</li>',
pay_user_vip_2_name: '钻石会员',
pay_user_vip_2_desc: '尊享专属特权',
pay_user_vip_2_equity: VIP_EQUITY_DEFAULT,
vip_2_product_s: true,
vip_2_product: [
{ price: 99, show_price: 199, tag: '<i class="fa fas fa-fw fa-bolt"></i> 限时特惠', time: 3, unit: 'month' },
{ price: 199, show_price: 299, tag: '<i class="fa fas fa-fw fa-bolt"></i> 站长推荐', time: 6, unit: 'month' },
],
vip_2_img_icon: '',
pay_user_vip_desc: '',
pay_user_vip_more: '',
pay_user_vip_desc: '开通VIP会员,享受会员专属折扣以及多项特权',
pay_user_vip_more: '<div>购买后不支持退款</div><div>VIP权益仅适用于本站</div><div>欢迎与站长联系</div>',
pay_download_limit: 0,
pay_download_speed: 0,
pay_download_limit_vip_1: 0,
@@ -29,19 +129,45 @@ export const vipSchema = {
pay_download_limit_vip_2: 0,
pay_download_speed_vip_2: 0,
vip_1_product_s: true,
payment_vip_1_products: '30|59|1|month|限时|月卡\n98|199|3|month|特惠|季卡\n365|599|12|month|超值|年卡\n999|1299|0|day|永久|永久卡',
vip_2_product_s: true,
payment_vip_2_products: '58|99|1|month|限时|月卡\n168|299|3|month|特惠|季卡\n598|999|12|month|超值|年卡\n1299|1999|0|day|永久|永久卡',
vip_renew: true,
vip_renew_desc: '',
vip_renew_desc: '立即续费会员,畅享VIP权益',
vip_renew_price_type: 'discount',
vip_renew_discount: 8,
vip_renew_reduce: '20.00',
vip_1_renew_product: [
{ price: 69, show_price: 199, tag: '<i class="fa fas fa-fw fa-bolt"></i> 限时特惠', time: 3, unit: 'month' },
{ price: 169, show_price: 299, tag: '<i class="fa fas fa-fw fa-bolt"></i> 站长推荐', time: 6, unit: 'month' },
],
vip_2_renew_product: [
{ price: 269, show_price: 599, tag: '<i class="fa fas fa-fw fa-bolt"></i> 限时特惠', time: 3, unit: 'month' },
{ price: 369, show_price: 899, tag: '<i class="fa fas fa-fw fa-bolt"></i> 站长推荐', time: 6, unit: 'month' },
],
vip_2_product_s: true,
vip_renew: true,
vip_renew_discount: 8,
vip_renew_reduce: '20.00',
vip_upgrade: true,
vip_upgrade_desc: '',
vip_upgrade_product: 'unit|1|按剩余天数升级\njump|199|升级永久二级会员\npermanent|299|永久一级升永久二级',
vip_upgrade_desc: '升级VIP会员,享更多会员权益',
vip_upgrade_product: {
month_to_month_s: true,
unit_price: '10.00',
unit_tag: '<i class="fa fas fa-fw fa-bolt"></i> 站长推荐',
unit_tag_class: 'jb-yellow',
jump_s: false,
jump_price: '199.00',
jump_show_price: '299.00',
jump_tag: '<i class="fa fas fa-fw fa-bolt"></i> 升级特惠',
jump_tag_class: 'jb-red',
permanent_price: '199.00',
permanent_show_price: '299.00',
permanent_tag: '<i class="fa fas fa-fw fa-bolt"></i> 升级特惠',
permanent_tag_class: 'jb-red',
},
pay_vip_points_exchange_s: false,
payment_vip_points_packages: '1|500|30|积分月卡\n1|5000|0|积分永久',
pay_vip_points_exchange_product: [
{ points: 500, level: 1, time: 30, unit: 'day' },
{ points: 5000, level: 1, time: 'Permanent' },
],
pay_vip_points_exchange_desc: '',
},
groups: [
@@ -67,17 +193,16 @@ export const vipSchema = {
{ key: 'nav_pay_vip', label: '导航栏购买按钮', type: 'switch', default: true, help: '在顶部导航栏显示开通会员按钮;请注意顶部导航的整体宽度。' },
{ key: 'nav_user_pay_vip', label: '用户框购买按钮', type: 'switch', default: true, help: '在导航栏用户框内显示开通会员按钮。' },
{ key: 'pay_user_vip_1_s', label: '启用一级会员', type: 'switch', default: true },
{ key: 'pay_user_vip_1_name', label: '一级会员显示名称', type: 'input', default: '一级会员', dependsOn: { key: 'pay_user_vip_1_s', value: true } },
{ key: 'pay_user_vip_1_desc', label: '一级会员一句话简介', type: 'input', default: '尊享一级会员特权', dependsOn: { key: 'pay_user_vip_1_s', value: true } },
{ key: 'pay_user_vip_1_equity', label: '一级会员权益 HTML', type: 'textarea', default: '<li>免费下载部分资源</li><li>会员专属身份标识</li><li>付费内容会员价</li>', dependsOn: { key: 'pay_user_vip_1_s', value: true }, help: '建议使用 <li> 列表项' },
{ key: 'vip_1_img_icon', label: '一级会员图标 URL', type: 'input', default: '', dependsOn: { key: 'pay_user_vip_1_s', value: true }, help: '留空使用内置 vip-1.svg 图标' },
{
key: 'vip_level_order_notice',
label: '',
type: 'notice',
ui_only: true,
alertType: 'danger',
dependsOn: { all: [{ key: 'pay_user_vip_2_s', value: true }, { key: 'pay_user_vip_1_s', operator: 'falsy' }] },
content: '<div style="text-align:center"><b>必须先开启一级会员后才能开启二级会员!</b></div>',
},
{ key: 'pay_user_vip_2_s', label: '启用二级会员', type: 'switch', default: true },
{ key: 'pay_user_vip_2_name', label: '二级会员显示名称', type: 'input', default: '二级会员', dependsOn: { key: 'pay_user_vip_2_s', value: true } },
{ key: 'pay_user_vip_2_desc', label: '二级会员一句话简介', type: 'input', default: '尊享二级会员特权', dependsOn: { key: 'pay_user_vip_2_s', value: true } },
{ key: 'pay_user_vip_2_equity', label: '二级会员权益 HTML', type: 'textarea', default: '<li>更高免费下载额度</li><li>更高等级身份标识</li><li>更多付费内容会员价</li>', dependsOn: { key: 'pay_user_vip_2_s', value: true } },
{ key: 'vip_2_img_icon', label: '二级会员图标 URL', type: 'input', default: '', dependsOn: { key: 'pay_user_vip_2_s', value: true }, help: '留空使用内置 vip-2.svg 图标' },
{ key: 'pay_user_vip_desc', label: '弹窗顶部会员说明', type: 'textarea', default: '' },
{ key: 'pay_user_vip_more', label: '弹窗底部补充说明 HTML', type: 'textarea', default: '' },
],
},
{
@@ -101,6 +226,8 @@ export const vipSchema = {
{
key: 'vip1',
title: '一级会员',
titleKey: 'pay_user_vip_1_name',
titleFallback: '一级会员',
fields: [
{ key: 'pay_download_limit_vip_1', label: '付费资源:免费资源每日可下载(个)', type: 'number', default: 0, min: 0, max: 9999, dependsOn: { key: 'pay_user_vip_1_s', value: true }, help: '一级会员每日最多可下载几个免费资源(为0则不限制)' },
{ key: 'pay_download_speed_vip_1', label: '付费资源:下载限速(KB/s)', type: 'number', default: 0, min: 0, dependsOn: { key: 'pay_user_vip_1_s', value: true }, help: '付费下载限制最大下载速度(为0则不限制);只对本地服务器文件有效' },
@@ -109,6 +236,8 @@ export const vipSchema = {
{
key: 'vip2',
title: '二级会员',
titleKey: 'pay_user_vip_2_name',
titleFallback: '二级会员',
fields: [
{ key: 'pay_download_limit_vip_2', label: '付费资源:免费资源每日可下载(个)', type: 'number', default: 0, min: 0, max: 9999, dependsOn: { key: 'pay_user_vip_2_s', value: true }, help: '二级会员每日最多可下载几个免费资源(为0则不限制)' },
{ key: 'pay_download_speed_vip_2', label: '付费资源:下载限速(KB/s)', type: 'number', default: 0, min: 0, dependsOn: { key: 'pay_user_vip_2_s', value: true }, help: '付费下载限制最大下载速度(为0则不限制);只对本地服务器文件有效' },
@@ -131,53 +260,111 @@ export const vipSchema = {
{
key: 'vip1',
title: '一级会员',
titleKey: 'pay_user_vip_1_name',
titleFallback: '一级会员',
fields: [
{ key: 'vip_1_product_s', label: '允许购买一级会员', type: 'switch', default: true, dependsOn: { key: 'pay_user_vip_1_s', value: true } },
{
key: 'payment_vip_1_products',
label: '一级会员套餐',
type: 'textarea',
default: '30|59|1|month|限时|月卡\n98|199|3|month|特惠|季卡\n365|599|12|month|超值|年卡\n999|1299|0|day|永久|永久卡',
dependsOn: { key: 'vip_1_product_s', value: true },
help: '每行:执行价|原价|时长|单位(day|month)|标签|标题;时长 0=永久',
},
...vipLevelTabFields(1),
],
},
{
key: 'vip2',
title: '二级会员',
titleKey: 'pay_user_vip_2_name',
titleFallback: '二级会员',
fields: [
{ key: 'vip_2_product_s', label: '允许购买二级会员', type: 'switch', default: true, dependsOn: { key: 'pay_user_vip_2_s', value: true } },
{
key: 'payment_vip_2_products',
label: '二级会员套餐',
type: 'textarea',
default: '58|99|1|month|限时|月卡\n168|299|3|month|特惠|季卡\n598|999|12|month|超值|年卡\n1299|1999|0|day|永久|永久卡',
dependsOn: { key: 'vip_2_product_s', value: true },
help: '每行:执行价|原价|时长|单位(day|month)|标签|标题;时长 0=永久',
},
...vipLevelTabFields(2),
],
},
{
key: 'renew',
title: '会员续费',
fields: [
{ key: 'vip_renew', label: '允许续费', type: 'switch', default: true, help: '关闭后则不能续费,只能会员到期之后再购买。永久会员不需要续费' },
{ key: 'vip_renew_desc', label: '续费说明', type: 'textarea', default: '', dependsOn: { key: 'vip_renew', value: true } },
{
key: 'vip_renew',
label: '允许续费',
type: 'switch',
default: true,
help: '关闭后则不能续费,只能会员到期之后再购买。永久会员不需要续费',
},
{
key: 'vip_renew_desc',
label: '续费介绍',
type: 'input',
sanitize_type: 'html',
default: '立即续费会员,畅享VIP权益',
dependsOn: { key: 'vip_renew', value: true },
help: '续费会员的一句话简介,支持HTML',
},
{
key: 'vip_renew_price_type',
label: '续费价格类型',
type: 'radio',
type: 'select',
default: 'discount',
dependsOn: { key: 'vip_renew', value: true },
options: [
{ label: '相同价格', value: 'same' },
{ label: '折扣价格', value: 'discount' },
{ label: '优惠立减', value: 'reduce' },
{ label: '自定义商品', value: 'customize' },
],
help: '自定义商品时,按下方「一级会员续费」「二级会员续费」的商品选项计价',
},
{
key: 'vip_renew_discount',
label: '续费折扣',
type: 'slider',
sanitize_type: 'float',
default: 8,
min: 0.1,
max: 9.9,
step: 0.1,
unit: '折',
dependsOn: { key: 'vip_renew_price_type', value: 'discount' },
help: '在会员商品基础上打几折,如 8 = 八折',
},
{
key: 'vip_renew_reduce',
label: '续费减免金额',
type: 'spinner',
sanitize_type: 'money_decimal',
default: '20.00',
min: 0,
max: 500,
step: 5,
unit: '元',
dependsOn: { key: 'vip_renew_price_type', value: 'reduce' },
help: '在会员商品基础上优惠的金额,优惠后总金额不能为0元',
},
{
key: 'vip_1_renew_product',
label: '一级会员续费',
type: 'group',
max: 8,
button_title: '添加续费商品',
default: [
{ price: 69, show_price: 199, tag: '<i class="fa fas fa-fw fa-bolt"></i> 限时特惠', time: 3, unit: 'month' },
{ price: 169, show_price: 299, tag: '<i class="fa fas fa-fw fa-bolt"></i> 站长推荐', time: 6, unit: 'month' },
],
dependsOn: { key: 'vip_renew_price_type', value: 'customize' },
help: '一级会员续费的商品选项',
...vipProductGroupOptions,
fields: vipProductFields(),
},
{
key: 'vip_2_renew_product',
label: '二级会员续费',
type: 'group',
max: 8,
button_title: '添加续费商品',
default: [
{ price: 269, show_price: 599, tag: '<i class="fa fas fa-fw fa-bolt"></i> 限时特惠', time: 3, unit: 'month' },
{ price: 369, show_price: 899, tag: '<i class="fa fas fa-fw fa-bolt"></i> 站长推荐', time: 6, unit: 'month' },
],
dependsOn: { key: 'vip_renew_price_type', value: 'customize' },
help: '二级会员续费的商品选项',
...vipProductGroupOptions,
fields: vipProductFields(),
},
{ key: 'vip_renew_discount', label: '续费折扣(折,如 8=八折)', type: 'number', default: 8, min: 1, max: 100, dependsOn: { key: 'vip_renew_price_type', value: 'discount' } },
{ key: 'vip_renew_reduce', label: '续费减免金额', type: 'input', sanitize_type: 'money_decimal', default: '20.00', min: 0, max: 9999, dependsOn: { key: 'vip_renew_price_type', value: 'reduce' } },
],
},
{
@@ -185,14 +372,50 @@ export const vipSchema = {
title: '会员升级',
fields: [
{ key: 'vip_upgrade', label: '允许升级', type: 'switch', default: true },
{ key: 'vip_upgrade_desc', label: '升级说明', type: 'textarea', default: '', dependsOn: { key: 'vip_upgrade', value: true } },
{ key: 'vip_upgrade_desc', label: '升级介绍', type: 'input', sanitize_type: 'html', default: '升级VIP会员,享更多会员权益', dependsOn: { key: 'vip_upgrade', value: true }, help: '升级会员的一句话简介,支持HTML' },
{
key: 'vip_upgrade_product',
label: '升级产品',
type: 'textarea',
default: 'unit|1|按剩余天数升级\njump|199|升级永久二级会员\npermanent|299|永久一级升永久二级',
label: '升级价格',
type: 'accordion',
default: {
month_to_month_s: true,
unit_price: '10.00',
unit_tag: '<i class="fa fas fa-fw fa-bolt"></i> 站长推荐',
unit_tag_class: 'jb-yellow',
jump_s: false,
jump_price: '199.00',
jump_show_price: '299.00',
jump_tag: '<i class="fa fas fa-fw fa-bolt"></i> 升级特惠',
jump_tag_class: 'jb-red',
permanent_price: '199.00',
permanent_show_price: '299.00',
permanent_tag: '<i class="fa fas fa-fw fa-bolt"></i> 升级特惠',
permanent_tag_class: 'jb-red',
},
dependsOn: { key: 'vip_upgrade', value: true },
help: '每行一条。永久VIP1升级:permanent|价格|标题;按剩余天:unit|每日单价|标题;限时跳转永久:jump|价格|标题',
help: '会员升级的商品选项',
sections: [
{ title: '月费会员升级月费会员', keys: ['_upgrade_unit_notice', 'month_to_month_s', 'unit_price', 'unit_tag', 'unit_tag_class'] },
{ title: '月费会员升级永久会员', keys: ['_upgrade_jump_notice', 'jump_s', 'jump_price', 'jump_show_price', 'jump_tag', 'jump_tag_class'] },
{ title: '永久会员升级永久会员', keys: ['permanent_price', 'permanent_show_price', 'permanent_tag', 'permanent_tag_class'] },
],
fields: [
{ key: '_upgrade_unit_notice', label: '', type: 'notice', ui_only: true, alertType: 'warning', content: '月费会员升级为月费会员,用户会员有效期不会改变;价格按照会员剩余天数计算升级价格,请在下方设置每天的单价' },
{ key: 'month_to_month_s', label: '月费会员升级月费会员', type: 'switch', default: true },
{ key: 'unit_price', label: '天单价', type: 'number', sanitize_type: 'money_decimal', default: '10.00', min: 0, unit: '元', dependsOn: { key: 'month_to_month_s', value: true } },
{ key: 'unit_tag', label: '促销标签', type: 'textarea', sanitize_type: 'html', default: '<i class="fa fas fa-fw fa-bolt"></i> 站长推荐', rows: 1, dependsOn: { key: 'month_to_month_s', value: true }, help: '支持HTML,请注意控制长度' },
{ key: 'unit_tag_class', label: '标签颜色', type: 'palette', default: 'jb-yellow', dependsOn: { all: [{ key: 'month_to_month_s', value: true }, { key: 'unit_tag', operator: 'not_empty' }] }, options: vipTagPalette },
{ key: '_upgrade_jump_notice', label: '', type: 'notice', ui_only: true, alertType: 'warning', content: '如果网站可以购买月费会员和永久会员,则可以开启此项;允许用户由月费会员直接升级到更高一级的永久会员,用户之前还未到期的部分将自动一起转为永久会员,可能会涉及到差价问题,请自行告知用户;如果网站没有永久会员的购买选项请务必关闭此项' },
{ key: 'jump_s', label: '月费会员升级永久会员', type: 'switch', default: false },
{ key: 'jump_price', label: '执行价', type: 'number', sanitize_type: 'money_decimal', default: '199.00', min: 0, unit: '元', dependsOn: { key: 'jump_s', value: true }, help: '月费会员升级永久会员的价格' },
{ key: 'jump_show_price', label: '原价', type: 'number', sanitize_type: 'money_decimal', default: '299.00', min: 0, unit: '元', dependsOn: { key: 'jump_s', value: true }, help: '显示在执行价格前面,并划掉' },
{ key: 'jump_tag', label: '促销标签', type: 'textarea', sanitize_type: 'html', default: '<i class="fa fas fa-fw fa-bolt"></i> 升级特惠', rows: 1, dependsOn: { key: 'jump_s', value: true }, help: '支持HTML,请注意控制长度' },
{ key: 'jump_tag_class', label: '标签颜色', type: 'palette', default: 'jb-red', dependsOn: { all: [{ key: 'jump_s', value: true }, { key: 'jump_tag', operator: 'not_empty' }] }, options: vipTagPalette },
{ key: 'permanent_price', label: '执行价', type: 'number', sanitize_type: 'money_decimal', default: '199.00', min: 0, unit: '元', help: '永久会员升级永久会员的价格' },
{ key: 'permanent_show_price', label: '原价', type: 'number', sanitize_type: 'money_decimal', default: '299.00', min: 0, unit: '元', help: '显示在执行价格前面,并划掉' },
{ key: 'permanent_tag', label: '促销标签', type: 'textarea', sanitize_type: 'html', default: '<i class="fa fas fa-fw fa-bolt"></i> 升级特惠', rows: 1, help: '支持HTML,请注意控制长度' },
{ key: 'permanent_tag_class', label: '标签颜色', type: 'palette', default: 'jb-red', dependsOn: { key: 'permanent_tag', operator: 'not_empty' }, options: vipTagPalette },
],
},
],
},
@@ -205,16 +428,72 @@ export const vipSchema = {
title: '积分兑换会员',
icon: 'fa fas fa-database',
fields: [
{ key: 'pay_vip_points_exchange_s', label: '启用积分兑换', type: 'switch', default: false },
{ key: 'pay_vip_points_exchange_s', label: '启用积分兑换', type: 'switch', default: false, help: '开启后用户可使用积分兑换会员;由于积分可免费获取,所以积分兑换会员功能不支持升级及续费' },
{
key: 'payment_vip_points_packages',
label: '兑换套餐',
type: 'textarea',
default: '1|500|30|积分月卡\n1|5000|0|积分永久',
key: 'pay_vip_points_exchange_product',
label: '积分兑换明细',
type: 'group',
max: 16,
button_title: '添加积分兑换商品',
default: [
{ points: 500, level: 1, time: 30, unit: 'day' },
{ points: 5000, level: 1, time: 'Permanent' },
],
dependsOn: { key: 'pay_vip_points_exchange_s', value: true },
help: '每行:等级|积分|天数|标题(天数 0=永久;仅未开通会员可兑)',
help: '每一行为一个可兑换的会员套餐',
...vipPointsGroupOptions,
fields: [
{ key: 'points', label: '使用积分', type: 'number', default: 100, min: 0, unit: '积分' },
{
key: 'level',
label: '兑换会员',
type: 'radio',
default: 1,
inline: true,
options: [
{ label: '一级会员', value: 1 },
{ label: '二级会员', value: 2 },
],
},
{ key: 'time', label: '兑换时长', type: 'input', default: '', help: '填 Permanent 为永久' },
{
key: 'unit',
label: '时长单位',
type: 'radio',
default: 'day',
inline: true,
dependsOn: { key: 'time', operator: 'neq', value: 'Permanent' },
options: vipUnitOptions,
},
],
},
{ key: 'pay_vip_points_exchange_desc', label: '兑换说明', type: 'textarea', sanitize_type: 'html', default: '', rows: 2, dependsOn: { key: 'pay_vip_points_exchange_s', value: true }, help: '显示在积分兑换弹窗底部,可以为提醒事项、兑换协议等,支持HTML代码' },
],
},
{
key: 'modal-copy',
title: '界面文案',
icon: 'fa fas fa-align-left',
fields: [
{
key: 'pay_user_vip_desc',
label: '会员介绍',
type: 'input',
sanitize_type: 'html',
default: '开通VIP会员,享受会员专属折扣以及多项特权',
dependsOn: { any: [{ key: 'pay_user_vip_1_s', value: true }, { key: 'pay_user_vip_2_s', value: true }] },
help: '显示在开通界面顶部一句话简介,可以为会员权益简介或者活动介绍,支持HTML',
},
{
key: 'pay_user_vip_more',
label: '开通会员更多内容',
type: 'textarea',
sanitize_type: 'html',
default: '<div>购买后不支持退款</div><div>VIP权益仅适用于本站</div><div>欢迎与站长联系</div>',
rows: 4,
dependsOn: { any: [{ key: 'pay_user_vip_1_s', value: true }, { key: 'pay_user_vip_2_s', value: true }] },
help: '显示在开通界面底部位置,可以为提醒事项、用户协议等,支持HTML代码',
},
{ key: 'pay_vip_points_exchange_desc', label: '兑换说明', type: 'textarea', default: '', dependsOn: { key: 'pay_vip_points_exchange_s', value: true } },
],
},
],
@@ -84,6 +84,9 @@ export const permissionsSchemas = {
{ label: '资源下载', value: 'download' },
{ label: '每日签到', value: 'signin' },
{ label: '前台投稿', value: 'front_submit' },
{ label: '打赏用户', value: 'tip_user' },
{ label: '申请售后', value: 'after_sale_apply' },
{ label: '撤销售后', value: 'after_sale_cancel' },
],
},
],
@@ -237,9 +240,6 @@ export const permissionsSchemas = {
{ type: 'admin', pattern: 'allow', val: 'svg=image/svg+xml' },
{ type: 'all', pattern: 'allow', val: '' },
],
newfilename: false,
newfilename_type: 'time',
close_attachment_page: true,
},
groups: [
{
@@ -314,27 +314,6 @@ export const permissionsSchemas = {
},
],
},
{
key: 'behavior',
title: '上传行为',
icon: 'fa fas fa-cogs',
fields: [
{ key: 'newfilename', label: '上传文件自动重命名', type: 'switch', help: '开启后上传文件按规则重命名,减少重名文件查询,提高上传效率。' },
{
key: 'newfilename_type',
label: '重命名格式',
type: 'radio',
buttonStyle: false,
default: 'time',
dependsOn: { key: 'newfilename', value: true },
options: [
{ label: '时间 + 原文件名', value: 'time' },
{ label: '随机字符串', value: 'random' },
],
},
{ key: 'close_attachment_page', label: '非管理员屏蔽附件页面', type: 'switch', help: '开启后非管理员访问附件(图片、文件等)页面将直接显示 404,可有效提高文件的安全性。' },
],
},
],
},
@@ -10,7 +10,6 @@ import { featuresSchema } from './features.js'
import { verifySchema } from './verify.js'
import { levelSchema } from './level.js'
import { badgeSchema } from './badge.js'
import { wechatSchema } from './wechat.js'
import { reportSchema } from './report.js'
import { smsSchema } from './sms.js'
@@ -23,7 +22,6 @@ export const usersSchemas = {
verify: verifySchema,
level: levelSchema,
badge: badgeSchema,
wechat: wechatSchema,
report: reportSchema,
sms: smsSchema,
}
+62 -101
View File
@@ -2,142 +2,103 @@ export const smsSchema = {
key: 'users.sms',
title: '短信接口',
icon: 'fa fas fa-comment',
description: '配置短信验证码和短信通知服务的供应商、模板和限流策略。',
description: '设置手机验证码服务。验证码为 6 位数字,有效期 30 分钟。',
defaults: {
sms_enabled: false,
sms_provider: 'aliyun',
sms_sdk: 'null',
sms_number_limit_ch: true,
sms_access_key: '',
sms_access_secret: '',
sms_sign_name: '',
sms_template_code: '',
sms_code_length: 6,
sms_code_expire: 300,
sms_rate_limit: 60,
sms_daily_limit: 10,
sms_notify_bind: true,
sms_notify_login: false,
sms_notify_register: true,
sms_notify_reset_password: true,
sms_ali_api_type: 'sms',
sms_ali_keyid: '',
sms_ali_keysecret: '',
sms_ali_sign_name: '',
sms_ali_template_code: '',
sms_region_id: 'cn-hangzhou',
sms_tencent_app_id: '',
sms_tencent_app_key: '',
sms_tencent_secret_id: '',
sms_tencent_secret_key: '',
sms_tencent_region: 'ap-guangzhou',
sms_huawei_sender: '',
sms_huawei_signature: '',
sms_huawei_endpoint: '',
sms_custom_url: '',
sms_custom_method: 'POST',
sms_custom_auth_type: 'header',
sms_custom_auth_key: '',
sms_custom_auth_value: '',
sms_custom_headers: '',
sms_custom_body: '',
sms_custom_success_rule: '',
sms_tencent_sign_name: '',
sms_tencent_template_id: '',
sms_smsbao_username: '',
sms_smsbao_password: '',
sms_smsbao_api_key: '',
sms_smsbao_template: '',
},
groups: [
{
key: 'provider',
title: '供应商配置',
title: '短信接口',
icon: 'fa fas fa-plug',
fields: [
{ key: 'sms_enabled', label: '启用短信服务', type: 'switch', default: false, help: '开启后支持短信验证码和短信通知' },
{
key: 'sms_provider',
label: '短信供应商',
key: 'sms_sdk',
label: '设置短信接口',
type: 'select',
default: 'aliyun',
default: 'null',
options: [
{ label: '阿里云短信', value: 'aliyun' },
{ label: '不使用', value: 'null' },
{ label: '阿里云短信', value: 'ali' },
{ label: '腾讯云短信', value: 'tencent' },
{ label: '华为云短信', value: 'huawei' },
{ label: '自定义接口', value: 'custom' },
{ label: '短信宝', value: 'smsbao' },
],
dependsOn: { key: 'sms_enabled', value: true },
},
{ key: 'sms_number_limit_ch', label: '仅发国内', type: 'switch', default: true, dependsOn: { key: 'sms_enabled', value: true }, help: '开启后仅支持发送国内短信,关闭后可发送港澳台及国际短信' },
{ key: 'sms_access_key', label: 'AccessKey / SecretId / AppKey', type: 'input', default: '', dependsOn: { key: 'sms_enabled', value: true }, help: '阿里云填 AccessKey,腾讯云填 SecretId,华为云填 AppKey' },
{ key: 'sms_access_secret', label: 'AccessSecret / SecretKey / AppSecret', type: 'input', inputType: 'password', default: '', dependsOn: { key: 'sms_enabled', value: true } },
{
key: 'sms_number_limit_ch',
label: '限制国内手机号',
type: 'switch',
default: true,
help: '开启后只允许发送到中国大陆手机号。',
},
{ key: 'sms_test_phone', label: '测试手机号', type: 'input', default: '', ui_only: true, help: '保存接口配置后,发送固定验证码 888888 测试短信。' },
{ key: 'sms_test_send', label: '发送测试短信', type: 'button', buttonType: 'primary', buttonText: '发送测试短信', ui_only: true },
],
},
{
key: 'aliyun',
title: '阿里云配置',
title: '阿里云',
icon: 'fa fas fa-cloud',
collapsible: true,
defaultCollapsed: true,
fields: [
{ key: 'sms_ali_api_type', label: '接口类型', type: 'radio', default: 'sms', options: [{ label: '短信服务', value: 'sms' }, { label: '号码认证', value: 'verify' }], dependsOn: { key: 'sms_provider', value: 'aliyun' } },
{ key: 'sms_region_id', label: 'RegionId', type: 'input', default: 'cn-hangzhou', dependsOn: { key: 'sms_provider', value: 'aliyun' } },
{
key: 'sms_ali_api_type',
label: '产品',
type: 'radio',
default: 'sms',
options: [
{ label: '短信服务', value: 'sms' },
{ label: '号码认证:短信认证服务', value: 'verify' },
],
dependsOn: { key: 'sms_sdk', value: 'ali' },
},
{ key: 'sms_ali_keyid', label: 'AccessKey Id', type: 'input', default: '', dependsOn: { key: 'sms_sdk', value: 'ali' } },
{ key: 'sms_ali_keysecret', label: 'AccessKey Secret', type: 'input', inputType: 'password', secret: true, default: '', dependsOn: { key: 'sms_sdk', value: 'ali' } },
{ key: 'sms_ali_sign_name', label: '签名', type: 'input', default: '', dependsOn: { key: 'sms_sdk', value: 'ali' } },
{ key: 'sms_ali_template_code', label: '模板 CODE', type: 'input', default: '', dependsOn: { all: [{ key: 'sms_sdk', value: 'ali' }, { key: 'sms_ali_api_type', not: true, value: 'verify' }] }, help: '短信服务需要填写。号码认证产品固定使用平台模板。' },
{ key: 'sms_region_id', label: 'RegionId', type: 'input', default: 'cn-hangzhou', dependsOn: { key: 'sms_sdk', value: 'ali' } },
],
},
{
key: 'tencent',
title: '腾讯云配置',
title: '腾讯云',
icon: 'fa fas fa-cloud',
collapsible: true,
defaultCollapsed: true,
fields: [
{ key: 'sms_tencent_app_id', label: 'SdkAppId', type: 'input', default: '', dependsOn: { key: 'sms_provider', value: 'tencent' }, help: '腾讯云短信应用 ID' },
{ key: 'sms_tencent_app_key', label: 'AppKey', type: 'input', inputType: 'password', default: '', dependsOn: { key: 'sms_provider', value: 'tencent' } },
{ key: 'sms_tencent_secret_id', label: 'SecretId', type: 'input', default: '', dependsOn: { key: 'sms_provider', value: 'tencent' } },
{ key: 'sms_tencent_secret_key', label: 'SecretKey', type: 'input', inputType: 'password', default: '', dependsOn: { key: 'sms_provider', value: 'tencent' } },
{ key: 'sms_tencent_region', label: '地域', type: 'input', default: 'ap-guangzhou', dependsOn: { key: 'sms_provider', value: 'tencent' } },
{ key: 'sms_tencent_app_id', label: 'SDK AppID', type: 'input', default: '', dependsOn: { key: 'sms_sdk', value: 'tencent' } },
{ key: 'sms_tencent_app_key', label: 'App Key', type: 'input', inputType: 'password', secret: true, default: '', dependsOn: { key: 'sms_sdk', value: 'tencent' } },
{ key: 'sms_tencent_sign_name', label: '签名', type: 'input', default: '', dependsOn: { key: 'sms_sdk', value: 'tencent' } },
{ key: 'sms_tencent_template_id', label: '模板 ID', type: 'input', default: '', dependsOn: { key: 'sms_sdk', value: 'tencent' }, help: '模板包含验证码和有效分钟数两个参数。' },
],
},
{
key: 'huawei',
title: '华为云配置',
key: 'smsbao',
title: '短信宝',
icon: 'fa fas fa-cloud',
collapsible: true,
defaultCollapsed: true,
fields: [
{ key: 'sms_huawei_sender', label: '通道号 (sender)', type: 'input', default: '', dependsOn: { key: 'sms_provider', value: 'huawei' } },
{ key: 'sms_huawei_signature', label: '短信签名', type: 'input', default: '', dependsOn: { key: 'sms_provider', value: 'huawei' }, help: '华为云短信签名内容' },
{ key: 'sms_huawei_endpoint', label: 'API Endpoint', type: 'input', default: '', dependsOn: { key: 'sms_provider', value: 'huawei' }, help: '留空使用默认 endpoint' },
],
},
{
key: 'custom',
title: '自定义接口配置',
icon: 'fa fas fa-plug',
fields: [
{ key: 'sms_custom_url', label: '请求 URL', type: 'input', default: '', dependsOn: { key: 'sms_provider', value: 'custom' }, help: '支持 {phone} {code} {time} 变量' },
{ key: 'sms_custom_method', label: '请求方法', type: 'select', default: 'POST', options: [{ label: 'POST', value: 'POST' }, { label: 'GET', value: 'GET' }, { label: 'PUT', value: 'PUT' }], dependsOn: { key: 'sms_provider', value: 'custom' } },
{ key: 'sms_custom_auth_type', label: '鉴权方式', type: 'select', default: 'header', options: [{ label: 'Header', value: 'header' }], dependsOn: { key: 'sms_provider', value: 'custom' } },
{ key: 'sms_custom_auth_key', label: '鉴权 Header 名', type: 'input', default: '', dependsOn: { key: 'sms_provider', value: 'custom' } },
{ key: 'sms_custom_auth_value', label: '鉴权 Header 值', type: 'input', inputType: 'password', default: '', dependsOn: { key: 'sms_provider', value: 'custom' }, help: '支持 {phone} {code} {time} 变量' },
{ key: 'sms_custom_headers', label: '自定义 Headers', type: 'textarea', default: '', dependsOn: { key: 'sms_provider', value: 'custom' }, help: '每行一个,格式:Key: Value' },
{ key: 'sms_custom_body', label: '请求 Body', type: 'textarea', default: '', dependsOn: { key: 'sms_provider', value: 'custom' }, help: 'JSON 格式,支持 {phone} {code} {time} 变量' },
{ key: 'sms_custom_success_rule', label: '成功判定规则', type: 'input', default: '', dependsOn: { key: 'sms_provider', value: 'custom' }, help: '格式:status:200 或 json:code=0 或 contains:success' },
],
},
{
key: 'template',
title: '短信模板',
icon: 'fa fas fa-comment',
fields: [
{ key: 'sms_sign_name', label: '短信签名', type: 'input', default: '', dependsOn: { key: 'sms_enabled', value: true }, help: '在短信服务商申请的短信签名(所有供应商共用此字段)' },
{ key: 'sms_template_code', label: '验证码模板 ID', type: 'input', default: '', dependsOn: { key: 'sms_enabled', value: true }, help: '所有供应商共用此模板 ID' },
{ key: 'sms_code_length', label: '验证码长度', type: 'number', default: 6, min: 4, max: 8, dependsOn: { key: 'sms_enabled', value: true } },
{ key: 'sms_code_expire', label: '验证码有效期 (秒)', type: 'number', default: 300, min: 60, max: 600, dependsOn: { key: 'sms_enabled', value: true } },
],
},
{
key: 'rate_limit',
title: '限流策略',
icon: 'fa fas fa-shield-alt',
fields: [
{ key: 'sms_rate_limit', label: '发送间隔 (秒)', type: 'number', default: 60, min: 10, max: 300, dependsOn: { key: 'sms_enabled', value: true }, help: '同一手机号两次发送的最小间隔' },
{ key: 'sms_daily_limit', label: '每日发送上限', type: 'number', default: 10, min: 1, max: 100, dependsOn: { key: 'sms_enabled', value: true }, help: '同一手机号每天最多发送条数' },
],
},
{
key: 'triggers',
title: '通知场景',
icon: 'fa fas fa-bell',
fields: [
{ key: 'sms_notify_bind', label: '绑定手机时验证', type: 'switch', default: true, dependsOn: { key: 'sms_enabled', value: true } },
{ key: 'sms_notify_login', label: '短信登录', type: 'switch', default: false, dependsOn: { key: 'sms_enabled', value: true } },
{ key: 'sms_notify_register', label: '注册验证', type: 'switch', default: true, dependsOn: { key: 'sms_enabled', value: true } },
{ key: 'sms_notify_reset_password', label: '找回密码', type: 'switch', default: true, dependsOn: { key: 'sms_enabled', value: true } },
{ key: 'sms_smsbao_username', label: '用户名', type: 'input', default: '', dependsOn: { key: 'sms_sdk', value: 'smsbao' } },
{ key: 'sms_smsbao_password', label: '密码', type: 'input', inputType: 'password', secret: true, default: '', dependsOn: { key: 'sms_sdk', value: 'smsbao' } },
{ key: 'sms_smsbao_api_key', label: 'ApiKey', type: 'input', inputType: 'password', secret: true, default: '', dependsOn: { key: 'sms_sdk', value: 'smsbao' }, help: '可选;填写后以 ApiKey 认证,可与密码任选其一。' },
{ key: 'sms_smsbao_template', label: '模板内容', type: 'input', default: '', dependsOn: { key: 'sms_sdk', value: 'smsbao' }, help: '必须包含 {code},有效时间变量为 {time}。' },
],
},
],
@@ -2,83 +2,52 @@ export const verifySchema = {
key: 'users.verify',
title: '身份认证',
icon: 'fa fas fa-id-card',
description: '配置公开认证徽标、申请类型与申请门槛;审核、驳回和取消认证请前往 WordPress 用户 → 身份认证。',
description: '设置通用身份认证的申请条件;用户提交资料后由管理员审核。',
defaults: {
user_verify_enabled: true,
user_verify_public_display: true,
user_verify_personal_badge_text: '个人认证',
user_verify_personal_fee: '0.00',
user_verify_personal_free_applications: 0,
user_verify_personal_require_real_name: false,
user_verify_enterprise_badge_text: '企业认证',
user_verify_enterprise_fee: '0.00',
user_verify_enterprise_free_applications: 0,
user_verify_enterprise_require_real_name: false,
user_verify_allow_apply: true,
user_verify_disable_desc:
'由于系统正在维护,暂时无法申请身份认证\n详情请联系管理员',
user_verify_desc:
'认证用户会在昵称后显示认证徽章,同时享受多种优先待遇。申请请确保信息属实。',
user_auth_s: true,
user_auth_apply_s: true,
user_auth_apply_disable_desc: '由于系统正在维护,暂时无法申请身份认证\n详情请联系管理员',
user_auth_apply_limit_level: 0,
user_auth_apply_desc: '请填写认证身份、简介和申请说明,提交后由管理员审核。',
},
groups: [
{
key: 'display',
title: '公开展示',
icon: 'fa fas fa-eye',
fields: [
{ key: 'user_verify_enabled', label: '启用身份认证', type: 'switch', default: true },
{
key: 'user_verify_public_display',
label: '公开页显示认证徽标',
type: 'switch',
default: true,
dependsOn: { key: 'user_verify_enabled', value: true },
},
{
key: 'user_verify_personal_badge_text', label: '个人认证徽标文案', type: 'input', default: '个人认证',
dependsOn: { key: 'user_verify_public_display', value: true },
},
{
key: 'user_verify_enterprise_badge_text', label: '企业认证徽标文案', type: 'input', default: '企业认证',
dependsOn: { key: 'user_verify_public_display', value: true },
},
],
},
{
key: 'workflow', title: '认证方式', icon: 'fa fas fa-tasks',
fields: [{ type: 'alert', content: '认证方式固定为个人认证和企业认证,申请人不可自行新增或修改类型。' }],
},
{
key: 'apply',
title: '申请设置',
title: '认证申请',
icon: 'fa fas fa-edit',
fields: [
{ key: 'user_auth_s', label: '启用身份认证', type: 'switch', default: true },
{
key: 'user_verify_allow_apply',
key: 'user_auth_apply_s',
label: '允许用户前台申请',
type: 'switch',
default: true,
dependsOn: { key: 'user_verify_enabled', value: true },
dependsOn: { key: 'user_auth_s', value: true },
},
{
key: 'user_verify_disable_desc',
key: 'user_auth_apply_disable_desc',
label: '停用申请说明',
type: 'textarea',
default: '由于系统正在维护,暂时无法申请身份认证\n详情请联系管理员',
dependsOn: { key: 'user_verify_allow_apply', value: false },
dependsOn: { key: 'user_auth_apply_s', value: false },
},
{ key: 'user_verify_personal_fee', label: '个人认证每次扣余额(元)', type: 'input', default: '0.00', help: '仅接受最多两位小数的十进制字符串。', dependsOn: { key: 'user_verify_allow_apply', value: true } },
{ key: 'user_verify_personal_free_applications', label: '个人认证赠送初始次数', type: 'number', default: 0, min: 0, max: 9999, help: '每位用户前 N 次个人认证申请免费,之后按单次费用扣余额。', dependsOn: { key: 'user_verify_allow_apply', value: true } },
{ key: 'user_verify_personal_require_real_name', label: '个人认证强制填写真实姓名', type: 'switch', default: false, dependsOn: { key: 'user_verify_allow_apply', value: true } },
{ key: 'user_verify_enterprise_fee', label: '企业认证每次扣余额(元)', type: 'input', default: '0.00', help: '仅接受最多两位小数的十进制字符串。', dependsOn: { key: 'user_verify_allow_apply', value: true } },
{ key: 'user_verify_enterprise_free_applications', label: '企业认证赠送初始次数', type: 'number', default: 0, min: 0, max: 9999, help: '每位用户前 N 次企业认证申请免费,之后按单次费用扣余额。', dependsOn: { key: 'user_verify_allow_apply', value: true } },
{ key: 'user_verify_enterprise_require_real_name', label: '企业认证强制填写真实姓名', type: 'switch', default: false, dependsOn: { key: 'user_verify_allow_apply', value: true } },
{
key: 'user_verify_desc',
key: 'user_auth_apply_limit_level',
label: '申请最低用户等级',
type: 'number',
default: 0,
min: 0,
max: 30,
help: '设置为 0 时不限制用户等级。',
dependsOn: { key: 'user_auth_apply_s', value: true },
},
{
key: 'user_auth_apply_desc',
label: '申请介绍',
type: 'textarea',
default: '认证用户会在昵称后显示认证徽章,同时享受多种优先待遇。申请请确保信息属实。',
dependsOn: { key: 'user_verify_allow_apply', value: true },
sanitize_type: 'html',
default: '请填写认证身份、简介和申请说明,提交后由管理员审核。',
dependsOn: { key: 'user_auth_apply_s', value: true },
},
],
},
@@ -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: '在用户中心显示微信绑定/解绑按钮' },
],
},
],
}
+8 -98
View File
@@ -668,95 +668,6 @@ code {
/**---------------------通知结束-------------**/
/**模态框------*/
.aksrt-modal-backdrop,
.aksrt-modal {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 100050;
opacity: 0;
transition: 0.3s;
}
.aksrt-modal {
display: none;
}
.aksrt-modal-backdrop {
background: rgba(0, 0, 0, 0.4);
z-index: 0;
}
.aksrt-modal.in {
opacity: 1;
overflow-x: hidden;
overflow-y: auto;
}
.aksrt-modal.in .aksrt-modal-backdrop {
opacity: 1;
}
.aksrt-modal-dialog {
max-width: calc(100% - 20px);
margin: 0;
transform: scale(0.9);
transition: transform 0.5s cubic-bezier(0.32, 0.85, 0.45, 1.18), width 0.3s;
}
.aksrt-modal-dialog.modal-mini {
width: 400px;
}
.aksrt-modal.in .aksrt-modal-dialog {
transform: scale(1);
position: relative;
}
.aksrt-modal .close.abs-close {
display: none;
}
.aksrt-modal .border-title {
padding-bottom: 12px;
margin-bottom: 12px;
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
}
.aksrt-modal-content {
max-height: calc(100vh - 20px);
overflow: hidden;
overflow-y: hidden;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
border-radius: 6px;
background: #fff;
border: none;
}
.aksrt-modal-body {
padding: 15px;
}
.aksrt-modal .loading-mask {
z-index: 1;
background: #fff;
border-radius: 6px;
}
.aksrt-modal .hide-btn {
position: absolute;
right: 10px;
top: 6px;
font-size: 25px;
opacity: 0.6;
cursor: pointer;
z-index: 9;
}
/**---------批量编辑------------**/
.aksrt-bulk-edit-box {
margin: -1px;
@@ -774,19 +685,18 @@ code {
margin-top: 6px;
}
/**---------------TAB-栏目------------**/
.aksrt-tab-pane {
display: none;
opacity: 0;
transition: 0.15s;
.aksrt-bulk-edit-box select,
.aksrt-bulk-edit-box input[type="text"] {
max-width: 100%;
margin-right: 4px;
}
.aksrt-tab-pane.active {
display: block;
.inline-edit-row fieldset .aksrt-bulk-edit-box {
min-width: 168px;
}
.aksrt-tab-pane.in {
opacity: 1;
.aksrt-bulk-edit-desc {
color: #787c82;
}
/**---------------后台设置项目-修改或隐藏-----------**/
+38 -2
View File
@@ -15,6 +15,17 @@
margin-left: 8px !important;
}
/* 骨架与真实卡片同宽:.posts-row 以 -8px 抵消卡片边距,loader 被上面的
:not() 排除在补偿外,会宽出 16px;这里补上等同的水平边距。
补偿落在骨架内层的 .posts-item 上(与真实卡片同一元素),不落外层
.post_ajax_loader —— 否则内层 .posts-item.card 的 margin:8px 会与外层
补偿叠成 16px,小工具 card 骨架因此窄出 8px。 */
.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;
@@ -448,6 +459,17 @@
margin-left: 8px;
}
/* 骨架与卡片同宽同轴:.widget-ajaxpager / .tab-pane 用负边距抵消卡片的水平
边距。补偿同样落在骨架内层的 .posts-item 上(与真实卡片同一元素),
不落外层 .post_ajax_loader —— 否则内层 .posts-item.card 自带的 margin:8px
会与外层补偿叠成 16px,card 骨架因此窄出 8px。 */
.widget-main-post.style-list .widget-ajaxpager > .post_ajax_loader .posts-item,
.widget-main-post.style-card .widget-ajaxpager > .post_ajax_loader .posts-item,
.widget-tab-post .tab-content > .tab-pane > .widget-ajaxpager > .post_ajax_loader .posts-item {
margin-right: 8px;
margin-left: 8px;
}
.posts-mini:hover img,
.posts-item:hover .item-thumbnail img,
.posts-item.mult-thumb .thumb-items > span > img:hover {
@@ -518,6 +540,12 @@
margin-left: 5px !important;
}
.posts-row > .post_ajax_loader .posts-item,
.posts-row > .post_ajax_loader .posts-mini {
margin-right: 5px;
margin-left: 5px;
}
.posts-item .item-thumbnail {
width: 138px;
}
@@ -564,6 +592,13 @@
margin-left: 5px;
}
.widget-main-post.style-list .widget-ajaxpager > .post_ajax_loader .posts-item,
.widget-main-post.style-card .widget-ajaxpager > .post_ajax_loader .posts-item,
.widget-tab-post .tab-content > .tab-pane > .widget-ajaxpager > .post_ajax_loader .posts-item {
margin-right: 5px;
margin-left: 5px;
}
.swiper-scroll .posts-item {
margin-right: 10px !important;
}
@@ -674,8 +709,9 @@
color: var(--color-primary, #f04494);
}
/* .widget a 的链接色不得覆盖 .but 按钮自身配色(zibll 的 zib-widget 不设置链接色) */
.widget a.but {
/* .widget a 的链接色不得覆盖 .but / .img-badge 自身配色(zibll 的 zib-widget 不设置链接色) */
.widget a.but,
.widget a.img-badge {
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-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.contain>span>img{object-fit:contain;border-radius:0}.posts-item.no_margin{margin:0;border-radius:0;box-shadow:none}.postsLine 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
+635 -1022
View File
File diff suppressed because it is too large. Load diff
+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;--skeleton-base: var(--surface-2);--skeleton-shine: var(--surface-1);--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;--akLine 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
+4 -1
View File
@@ -137,6 +137,7 @@
/* === Full width === */
.btn-block {
display: block;
width: 100%;
}
@@ -182,6 +183,8 @@
.but > .fa:not(.fa-angle-right),
.but > i[class*="fa-"]:not(.fa-angle-right) { margin-right: 0.25em; }
.but.block { display: block; }
/* hover / active */
.but:focus,
.but:hover { opacity: 0.8; color: var(--this-color); }
@@ -313,7 +316,7 @@
/* gradient modifiers (legacy .jb-*) */
.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-green { --this-bg: linear-gradient(135deg, #60e464 10%, #5cb85b 100%); }
.jb-purple { --this-bg: linear-gradient(135deg, #f98dfb 10%, #ea00f9 100%); }
+16
View File
@@ -424,6 +424,22 @@
.aksrt-avatar--lg { width: var(--aksrt-avatar-size-lg); height: var(--aksrt-avatar-size-lg); }
.aksrt-avatar--xl { width: var(--aksrt-avatar-size-xl); height: var(--aksrt-avatar-size-xl); }
/* ============================================================
* 8b. 头像形状统一 — 圆形基底
* --radius-avatar 是前端用户头像形状的唯一来源。任何带头像类名的
* <img>(含未包 .avatar-img 的裸 img)都必须是正圆,供后续头像
* 挂件/装扮叠加。特异性 0,1,1 高于 main.css 的 .avatar{100px},
* 同值为圆;路由 CSS 中显式声明圆角的头像框需各自对齐。
* ============================================================ */
img.avatar,
img.avatar-mini,
img.avatar-md,
img.avatar-lg,
.avatar-img img {
border-radius: var(--radius-avatar, 50%);
object-fit: cover;
}
/* ============================================================
* 9. 涟漪点击效果 — .aksrt-ripple
* 容器加此类,点击时 JS 注入 span.aksrt-ripple__wave
-7
View File
@@ -141,10 +141,6 @@
--card-gap: 16px;
--card-gap-lg: 20px;
/* ===== Skeleton loader ===== */
--skeleton-base: var(--surface-2);
--skeleton-shine: var(--surface-1);
/* ===== Extended tokens (used by components) ===== */
--color-primary-light: var(--color-primary-soft);
--color-surface: var(--surface-1);
@@ -372,9 +368,6 @@ body.dark-theme {
--footer-color: var(--color-ink-4);
--shadow-md: var(--shadow);
--skeleton-base: #3a3b3e;
--skeleton-shine: #444548;
/* Multi-color palette — dark mode: same hue, softer tint backgrounds */
--aksrt-rose-soft: rgba(244, 63, 94, 0.18);
--aksrt-blue-soft: rgba(59, 130, 246, 0.18);
-114
View File
@@ -1,114 +0,0 @@
/* ========== AJAX 加载指示器 — 对齐 legacy loading spinner ========== */
.aksrt-loading {
display: flex;
align-items: center;
justify-content: center;
padding: 40px 20px;
color: var(--color-ink-4, #999);
font-size: 13px;
gap: 8px;
}
.aksrt-loading__spinner {
display: inline-block;
width: 20px;
height: 20px;
border: 2px solid var(--surface-3, #e8e8e8);
border-top-color: var(--color-primary, #f04494);
border-radius: 50%;
animation: aksrt-spin 0.6s linear infinite;
}
@keyframes aksrt-spin {
to { transform: rotate(360deg); }
}
/* 全屏加载遮罩 */
.aksrt-loading-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.8);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
backdrop-filter: blur(2px);
}
.aksrt-loading-overlay__content {
text-align: center;
}
.aksrt-loading-overlay__spinner {
width: 36px;
height: 36px;
border: 3px solid var(--surface-3, #e8e8e8);
border-top-color: var(--color-primary, #f04494);
border-radius: 50%;
animation: aksrt-spin 0.6s linear infinite;
margin: 0 auto 12px;
}
.aksrt-loading-overlay__text {
font-size: 13px;
color: var(--color-ink-3, #777);
}
/* 行内加载(替换内容时) */
.aksrt-loading-inline {
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
color: var(--color-ink-4, #999);
font-size: 12px;
gap: 6px;
}
.aksrt-loading-inline__dot {
width: 6px;
height: 6px;
background: var(--color-primary, #f04494);
border-radius: 50%;
animation: aksrt-dot-pulse 1.2s ease-in-out infinite;
}
.aksrt-loading-inline__dot:nth-child(2) {
animation-delay: 0.2s;
}
.aksrt-loading-inline__dot:nth-child(3) {
animation-delay: 0.4s;
}
@keyframes aksrt-dot-pulse {
0%, 80%, 100% { opacity: 0.3; transform: scale(0.8); }
40% { opacity: 1; transform: scale(1); }
}
/* 页面加载进度条 */
.aksrt-progress-bar {
position: fixed;
top: 0;
left: 0;
height: 2px;
background: var(--color-primary, #f04494);
z-index: 10000;
transition: width 0.3s ease;
}
/* 减弱动画偏好 */
@media (prefers-reduced-motion: reduce) {
.aksrt-loading__spinner,
.aksrt-loading-overlay__spinner {
animation-duration: 1.5s;
}
.aksrt-loading-inline__dot {
animation: none;
opacity: 0.6;
}
}
+2 -1
View File
@@ -242,7 +242,8 @@ main.container,
.user-card.author .user-name > name {
overflow: visible;
}
.user-card.author .user-name > name > .display-name {
.user-card.author .user-name > name > .display-name,
.user-card.widget .user-name > name > .display-name {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
@@ -1,3 +1 @@
@import url("components/base.css");
@import url("components/reward-amounts.css");
@import url("components/custom-amount.css");
@@ -1,147 +0,0 @@
/* ============================================================
* 8. 打赏弹窗
* ============================================================ */
.reward-modal {
position: fixed;
inset: 0;
z-index: 99999;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
visibility: hidden;
transition: opacity 0.3s, visibility 0.3s;
}
.reward-modal.is-open {
opacity: 1;
visibility: visible;
}
.reward-modal__overlay {
position: absolute;
inset: 0;
background: rgba(0,0,0,0.5);
}
.reward-modal__card {
position: relative;
background: var(--surface-1, #fff);
border-radius: var(--radius-lg, 8px);
padding: 32px;
max-width: 360px;
width: 90%;
text-align: center;
box-shadow: 0 8px 32px rgba(0,0,0,0.15);
z-index: 1;
}
.reward-modal__title {
margin: 0 0 8px;
font-size: 18px;
font-weight: 600;
color: var(--color-ink);
}
.reward-modal__desc {
margin: 0 0 20px;
font-size: 14px;
color: var(--color-ink-3);
}
.reward-modal__qr {
width: 200px;
height: 200px;
margin: 0 auto 16px;
border-radius: var(--radius, 10px);
overflow: hidden;
}
.reward-modal__qr img {
width: 100%;
height: 100%;
object-fit: contain;
}
.reward-modal__close {
position: absolute;
top: 12px;
right: 12px;
width: 32px;
height: 32px;
border: none;
border-radius: 50%;
background: var(--surface-2, #F4F5F7);
color: var(--color-ink-3);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.2s;
}
.reward-modal__close:hover {
background: var(--surface-3, #ECEEF1);
}
.reward-modal__content > p {
margin: -4px 0 18px;
}
.reward-amounts {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 10px;
margin: 18px 0;
}
.reward-amount {
min-height: 42px;
border: 1px solid var(--color-line-soft, rgba(15, 23, 42, 0.1));
border-radius: 10px;
background: var(--surface-2);
color: var(--color-ink-2);
cursor: pointer;
font-weight: 700;
}
.reward-amount.is-active {
border-color: var(--color-primary);
background: var(--color-primary-soft);
color: var(--color-primary);
}
.reward-custom {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 16px;
color: var(--color-ink-3);
font-size: 13px;
}
.reward-custom input {
width: 140px;
min-height: 40px;
border: 1px solid var(--color-line-soft, rgba(15, 23, 42, 0.1));
border-radius: 9px;
background: var(--surface-1);
color: var(--color-ink);
padding: 0 12px;
}
.reward-modal__submit {
width: 100%;
min-height: 42px;
}
.reward-modal__message {
min-height: 20px;
margin-top: 10px;
color: var(--color-primary);
font-size: 12px;
text-align: center;
}
@@ -1,54 +1,3 @@
/* ---- custom amount ---- */
.reward-custom {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 16px;
}
.reward-custom__input {
flex: 1;
height: 40px;
padding: 0 12px;
border: 1px solid var(--color-line, #e5e7eb);
border-radius: var(--radius, 10px);
background: var(--surface-2, #F4F5F7);
color: var(--color-ink);
font-size: 14px;
text-align: center;
outline: none;
transition: border-color 0.2s;
-moz-appearance: textfield;
}
.reward-custom__input::-webkit-inner-spin-button,
.reward-custom__input::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}
.reward-custom__input:focus {
border-color: var(--color-primary, #f04494);
}
.reward-custom__confirm {
flex-shrink: 0;
height: 40px;
padding: 0 20px;
border: none;
border-radius: var(--radius, 10px);
background: var(--color-primary, #f04494);
color: #fff;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: opacity 0.2s;
}
.reward-custom__confirm:hover {
opacity: 0.85;
}
/* ============================================================
* 9. 公告跑马灯 Widget
* ============================================================ */
@@ -1,45 +0,0 @@
/* ---- reward amounts ---- */
.reward-amounts {
display: flex;
flex-wrap: wrap;
gap: 8px;
justify-content: center;
margin-bottom: 16px;
}
.reward-amounts__btn {
display: inline-flex;
align-items: baseline;
gap: 2px;
padding: 8px 16px;
border: 1px solid var(--color-line, #e5e7eb);
border-radius: var(--radius, 10px);
background: var(--surface-2, #F4F5F7);
color: var(--color-ink);
font-size: 15px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
user-select: none;
}
.reward-amounts__btn:hover {
border-color: var(--color-primary, #f04494);
background: rgba(240, 68, 148, 0.06);
}
.reward-amounts__btn.is-active {
border-color: var(--color-primary, #f04494);
background: var(--color-primary, #f04494);
color: #fff;
}
.reward-amounts__currency {
font-size: 13px;
font-weight: 400;
}
.reward-amounts__value {
font-size: 15px;
}
+5 -74
View File
@@ -1,8 +1,11 @@
/**
* AKSRT Enhancements — 全局 UI 增强
*
* 包含:骨架屏、悬停动效、暗色过渡、滚动感知
* 包含:悬停动效、暗色过渡、滚动感知
* 所有选择器使用 aksrt- 前缀避免冲突
*
* 骨架屏不在此文件:唯一契约是 .placeholder 块 + .post_ajax_loader 容器
* (inc/helpers/posts-renderer/markup.php 的 aksrt_render_placeholder)。
*/
@keyframes aksrt-spin {
@@ -11,79 +14,7 @@
}
/* ============================================================
* 1. 骨架屏加载动画
* ============================================================ */
.aksrt-skeleton {
background: linear-gradient(
90deg,
rgba(121, 121, 121, 0.08) 25%,
rgba(121, 121, 121, 0.15) 37%,
rgba(121, 121, 121, 0.08) 63%
);
background-size: 400% 100%;
animation: aksrt-skeleton-shimmer 1.5s ease infinite;
border-radius: 4px;
}
.aksrt-skeleton--image {
height: 0;
padding-bottom: 66.67%; /* 3:2 aspect ratio */
}
.aksrt-skeleton--avatar {
width: 40px;
height: 40px;
border-radius: 50%;
}
.aksrt-skeleton--title {
height: 22px;
width: 80%;
}
.aksrt-skeleton--text {
height: 16px;
width: 100%;
}
.aksrt-skeleton--text-short {
height: 16px;
width: 60%;
}
.aksrt-skeleton--button {
height: 36px;
width: 100px;
border-radius: 8px;
}
@keyframes aksrt-skeleton-shimmer {
0% { background-position: 100% 50%; }
100% { background-position: 0 50%; }
}
/* 骨架屏组合:文章卡片骨架 */
.aksrt-skeleton-card {
background: var(--surface-1, #fff);
border: 1px solid var(--color-line);
border-radius: 12px;
overflow: hidden;
padding: 0;
}
.aksrt-skeleton-card .aksrt-skeleton--image {
border-radius: 0;
}
.aksrt-skeleton-card__body {
padding: 16px;
display: flex;
flex-direction: column;
gap: 10px;
}
/* ============================================================
* 2. 悬停交互动效
* 1. 悬停交互动效
* ============================================================ */
/* 卡片放大 */
+16
View File
@@ -14,6 +14,22 @@
transition: 0.3s;
}
/* 前台页面编辑齿轮的提示横条:参考主题共用 .abs-right(top:50% + right:.7em +
* translateY(-50%) 的垂直居中定位)。本站 legacy-utilities 的 .abs-right 是
* top:0/right:0,横条会贴到按钮顶角,故在此恢复参考几何。 */
.float-right .admin-set-page .admin-set-info {
position: absolute;
top: 50%;
right: 0.7em;
transform: translateY(-50%);
white-space: nowrap;
line-height: 1.5;
margin: 0;
box-shadow: none;
text-align: center;
z-index: 1;
}
body:has(.modal.in) .float-right,
.modal-open .float-right,
.imgbox-show .float-right,
+73 -347
View File
@@ -1,31 +1,35 @@
/**
* Header account popover.
*
* The shared legacy navigation styles provide the popover shell. This module
* owns AKSRT-specific account content such as check-in, wallet and VIP rows.
* The shared legacy navigation styles provide the popover shell, and every
* account row (avatar, VIP bar, check-in, wallet tiles, stats badges) renders
* through the reference legacy classes. This module only owns the navigation
* shell anchoring plus the account action-row geometry.
*/
/* The desktop action cluster owns one horizontal alignment context. */
.navbar-top .aksrt-navbar-actions {
display: flex;
align-items: center;
gap: 10px;
min-height: 50px;
margin-top: 0;
margin-bottom: 0;
padding: 0;
}
.navbar-top .aksrt-navbar-actions > *,
.navbar-top .aksrt-navbar-account,
.navbar-top .aksrt-navbar-account > li {
/* The reference keeps the right cluster as three floated .navbar-form groups.
* Bootstrap's base .navbar-form rule carries margin-top: 8px and the >=768px
* override only zeroes the horizontal margins, so every right-cluster group
* starts 8px below the top of .navbar-collapse. The 50px menu row and the logo
* put the navbar midline at 25px, so a 34px cross-size lands a group's midline
* at 8 + 17 = 25px. The publish/VIP chips already reach that line through their
* own 0.4em margin stacked on the group's 8px; the account row reaches it from
* its 34px icon tiles, and the theme group needs the same cross-size because
* 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-account {
display: inline-flex;
align-items: center;
flex: 0 0 auto;
gap: 4px;
margin: 0;
}
.navbar-top .aksrt-navbar-account > li {
position: relative;
display: inline-flex;
align-items: center;
flex: 0 0 auto;
padding: 0;
overflow: visible;
}
@@ -38,39 +42,6 @@
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-avatar {
display: inline-flex !important;
@@ -103,33 +74,34 @@
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;
margin: 0;
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;
}
/* Anchor the VIP preview to the full navigation row instead of relying on
* the absolute panel's static position beside the compact CTA. */
.navbar-top .aksrt-navbar-actions .navbar-vip-hover {
position: relative;
align-self: stretch;
align-items: center;
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;
/* The reference keeps the publish/VIP hover wrapper a plain inline-block, so the
* absolutely positioned .sub-vip-card takes its normal-flow static position.
* The shared action cluster otherwise forces inline-flex on every child, which
* would move that static position to the flex container's cross-start. */
.navbar-top .navbar-but > .hover-show {
display: inline-block;
}
/* Keep the drop-down search panel above a fixed header (z-index: 999). */
@@ -165,85 +137,41 @@
}
.aksrt-header-userbox {
width: 260px;
color: var(--color-ink-2);
line-height: 1.42857143;
}
.aksrt-header-userbox .user-info {
min-height: 44px;
padding: 2px;
/* public.css redeclares .muted-box after the legacy utility rules, which
* otherwise overrides the wallet tiles' padding-6 and radius4 utilities. */
.aksrt-header-userbox .muted-box.padding-6 {
background-color: var(--this-bg, var(--color-line-soft));
border-radius: 4px;
padding: 6px;
}
.aksrt-header-userbox .user-info > .avatar {
flex: 0 0 44px;
width: 44px;
height: 44px;
object-fit: cover;
/* core/buttons.css restates the legacy .but block after main.css, so its
* 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%);
color: #ece0e0;
--this-color: #ece0e0;
border: none;
text-shadow: none;
}
.aksrt-header-userbox .user-right,
.aksrt-header-userbox .user-right > .flex1 {
min-width: 0;
}
.aksrt-header-userbox .user-right b {
display: flex;
align-items: center;
gap: 4px;
max-width: 100%;
overflow: hidden;
white-space: nowrap;
}
.aksrt-header-userbox .display-name {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.aksrt-header-userbox .user-right .img-icon,
.aksrt-header-userbox .user-right .aksrt-user-level-icon,
.aksrt-header-userbox .user-right .aksrt-identity-medal,
.aksrt-header-userbox .user-right .aksrt-header-user-medal,
.aksrt-header-userbox .user-right .aksrt-identity-certification-icon {
flex: 0 0 auto;
height: 1.1em;
}
.aksrt-header-userbox .user-info > .abs-right {
top: 50%;
right: 2px;
transform: translateY(-50%);
display: inline-flex;
align-items: center;
justify-content: center;
width: 30px;
height: 30px;
border-radius: 50%;
background: var(--surface-2);
color: var(--color-ink-3);
}
.aksrt-header-userbox .aksrt-notification-badge {
position: absolute;
top: -4px;
right: -4px;
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 17px;
height: 17px;
padding: 0 4px;
border: 2px solid var(--surface-1);
border-radius: 999px;
background: #e74c3c;
color: #fff;
font-size: 10px;
font-weight: 700;
line-height: 1;
/* Keep the header check-in action flush with the VIP row's padded inner width. */
.aksrt-header-userbox > .mt6.em09 > .but.block {
display: block;
box-sizing: border-box;
width: 100%;
}
/* Top-nav bell badge (the card itself uses the legacy badge.top element). */
.aksrt-nav-notification .aksrt-notification-badge {
position: absolute;
top: 2px;
@@ -263,220 +191,18 @@
line-height: 1;
}
.aksrt-header-wallet {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
width: 100%;
padding: 9px 10px;
border-radius: 8px;
background: var(--surface-2);
color: var(--color-ink-2);
font-size: 12px;
line-height: 1.35;
}
.aksrt-header-wallet:hover {
color: var(--color-ink);
}
.aksrt-header-wallet__item {
min-width: 0;
white-space: nowrap;
}
.aksrt-header-wallet__item i {
margin-right: 2px;
color: var(--color-ink-3);
}
.aksrt-header-wallet__item b {
margin-left: 2px;
color: var(--color-ink);
font-weight: 700;
}
.aksrt-header-checkin {
display: inline-flex !important;
align-items: center;
justify-content: center;
gap: 6px;
width: 100% !important;
min-height: 36px;
margin: 0;
padding: 8px 10px !important;
border: 0 !important;
border-radius: 6px !important;
background: #fff6db !important;
color: #8a5a00 !important;
font-size: 13px !important;
font-weight: 600;
line-height: 1.3;
white-space: nowrap;
}
.aksrt-header-checkin > i {
margin: 0 !important;
color: #b78103 !important;
}
.aksrt-header-checkin:hover {
background: #fff0c2 !important;
color: #8a5a00 !important;
}
/* 已签到态对齐参考主题:保留原配色(参考主题签到后仍为同一彩色按钮),
* 且按钮可点击(打开签到详情弹窗);仅请求进行中的禁用态降不透明度。 */
.aksrt-header-checkin:disabled,
.aksrt-header-checkin:disabled:hover {
cursor: default;
opacity: .85;
}
.aksrt-header-payvip__cta,
.aksrt-header-payvip:not(.is-vip) .payvip-icon,
.aksrt-header-payvip:not(.is-vip) .but {
display: inline-flex !important;
align-items: center !important;
justify-content: center !important;
gap: 6px !important;
width: 100% !important;
min-width: 0 !important;
min-height: 36px;
margin: 0 !important;
padding: 8px 10px !important;
border: 0 !important;
border-radius: 6px !important;
background: linear-gradient(90deg, #ff6b6b, #f04494) !important;
color: #fff !important;
font-size: 13px !important;
font-weight: 600;
line-height: 1.3;
text-align: center;
white-space: nowrap;
}
.aksrt-header-payvip__cta > i,
.aksrt-header-payvip:not(.is-vip) .payvip-icon > i,
.aksrt-header-payvip:not(.is-vip) .but > i {
margin: 0 !important;
color: inherit !important;
}
.aksrt-header-payvip__cta:hover {
color: #fff !important;
opacity: .92;
}
.aksrt-header-payvip__active {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
gap: 8px !important;
width: 100%;
min-height: 42px;
padding: 7px 10px !important;
white-space: nowrap;
}
.aksrt-header-payvip__level {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
}
.aksrt-header-payvip__level > i {
margin: 0 !important;
}
.aksrt-header-payvip__exp {
display: inline;
margin: 0;
padding: 0;
border: 0;
background: none !important;
box-shadow: none;
color: inherit;
font-size: .85em;
font-weight: 400;
line-height: 1.3;
opacity: .9;
white-space: nowrap;
}
/* Navigation links are globally capped at 50px. Account actions are taller
* icon tiles and must opt out or their labels overlap the following row. */
.navbar-top .aksrt-header-userbox .header-user-href {
align-items: flex-start;
gap: 4px;
}
* icon tiles and must opt out or their labels overlap the following row.
* The tile geometry itself stays with the shared legacy rules
* (.header-user-href > a and .header-user-href .toggle-radius in main.css)
* so the action row keeps the reference proportions. */
.navbar-top .aksrt-header-userbox .header-user-href > a,
.aksrt-header-userbox .header-user-href > a {
display: flex;
flex: 1 1 0;
flex-direction: column;
align-items: center;
min-width: 0;
max-height: none;
padding: 10px 2px 2px;
line-height: 1.35;
}
.aksrt-header-userbox .header-user-href > a:hover {
opacity: .75;
}
.aksrt-header-userbox .header-user-href .toggle-radius {
flex: 0 0 36px;
width: 36px;
height: 36px;
}
.aksrt-header-userbox .header-user-href > a > div:last-child {
max-width: 100%;
overflow: hidden;
font-size: 12px;
text-overflow: ellipsis;
white-space: nowrap;
}
.mobile-navbar .aksrt-header-userbox {
width: 100%;
width: auto;
max-width: 400px;
margin-right: auto;
margin-left: auto;
}
body.dark-theme .aksrt-header-checkin {
background: rgba(255, 193, 7, .18) !important;
color: #f0c14b !important;
}
body.dark-theme .aksrt-header-checkin > i {
color: #f0c14b !important;
}
body.dark-theme .aksrt-header-wallet {
background: rgba(255, 255, 255, .06);
}
body.dark-theme .aksrt-header-wallet__item,
body.dark-theme .aksrt-header-wallet__item b {
color: inherit;
}
@media (max-width: 360px) {
.aksrt-header-userbox .header-user-href {
gap: 2px;
}
.aksrt-header-userbox .header-user-href .toggle-radius {
flex-basis: 34px;
width: 34px;
height: 34px;
}
margin: auto;
}
+4 -38
View File
@@ -483,44 +483,10 @@ body.site-layout-3 .sidebar,
overflow: hidden;
}
/* AJAX 筛选加载状态 */
.archive__ajax-wrap {
position: relative;
transition: opacity 0.2s ease;
}
.archive__ajax-wrap.is-loading {
opacity: 0.5;
pointer-events: none;
}
.archive__ajax-loading {
display: none;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
align-items: center;
justify-content: center;
z-index: 5;
background: rgba(255, 255, 255, 0.6);
backdrop-filter: blur(1px);
}
.archive__ajax-loading.is-loading {
display: flex;
}
.archive__ajax-spinner {
display: inline-block;
width: 28px;
height: 28px;
border: 3px solid var(--surface-3, #e8e8e8);
border-top-color: var(--color-primary, #f04494);
border-radius: 50%;
animation: aksrt-spin 0.6s linear infinite;
}
/* 归档面 AJAX 筛选的加载态与首页共用同一套骨架契约:
.post_ajax_loader 容器 + .is-loading 开关(见 frontend/modal-runtime.css)。
原先本文件另有一套独立 spinner 遮罩与 opacity 淡出,已删除 ——
全站只有一套加载语言。 */
@media (max-width: 767px) {
.term-page-header__body {
+39 -19
View File
@@ -106,10 +106,20 @@
}
.messages-center.is-loading .messages-center__content {
opacity: 0.55;
pointer-events: none;
}
/* 加载态由消息骨架承担(与文章列表 .post_ajax_loader 同一契约),
不再让按钮 spinner 与内容透明度抢反馈。 */
.messages-list > .post_ajax_loader {
display: none;
}
.messages-list > .post_ajax_loader.is-loading {
display: block;
animation: aksrtFadeIn 0.2s ease both;
}
.messages-list {
display: flex;
flex-direction: column;
@@ -455,22 +465,36 @@ html.dark .message-detail {
transform: translateY(-1px);
}
.messages-center.is-loading .messages-pagination__more span::before {
display: inline-block;
width: 12px;
height: 12px;
margin-right: 7px;
border: 2px solid currentColor;
border-right-color: transparent;
border-radius: 50%;
content: "";
vertical-align: -2px;
animation: messages-pagination-spin 0.7s linear infinite;
/* 消息骨架:复用真实 .message-item 结构与 .placeholder 动画契约,
只把内容换成灰块(参考主题 post_ajax loader 的同构思路)。 */
.messages-list > .post_ajax_loader .message-item--placeholder {
cursor: default;
}
@keyframes messages-pagination-spin {
to {
transform: rotate(360deg);
.messages-list > .post_ajax_loader .message-item--placeholder .message-item__icon {
color: transparent;
}
.messages-list > .post_ajax_loader .message-item--placeholder .message-item__body {
display: flex;
flex-direction: column;
}
.messages-list > .post_ajax_loader .message-item--placeholder .placeholder {
display: block;
}
.messages-list > .post_ajax_loader .message-item--placeholder .placeholder.s1 {
height: 12px;
}
.messages-list > .post_ajax_loader .message-item--placeholder .message-item__actions {
align-self: center;
}
@media (prefers-reduced-motion: reduce) {
.messages-list > .post_ajax_loader .placeholder {
animation: none;
}
}
@@ -479,10 +503,6 @@ html.dark .message-detail {
.messages-pagination__more {
transition: none;
}
.messages-center.is-loading .messages-pagination__more span::before {
animation: none;
}
}
/* ---- 响应式 ---- */
+324 -9
View File
@@ -257,14 +257,18 @@ body.aksrt-modal-runtime-open {
min-height: 80px;
}
.ajaxpager .post_ajax_loader,
.ajaxpager .post-ajax-loader {
/* 骨架默认收起,由 JS 加 .is-loading 展开(参考主题 post_ajax 的 beforeSend
remove + fadeIn / success .hide() 语义等价物)。
这里不依赖 .ajaxpager 祖先:首页 tab pane、归档流、评论列表的 loader 层级各不
相同,而且并非每个 loader 都在 bootstrap 的 .tab-pane 里(bootstrap 只负责
隐藏 tab-pane 内的骨架),统一按类名自身控制。 */
.post_ajax_loader {
display: none;
}
.ajaxpager .post_ajax_loader.is-loading,
.ajaxpager .post-ajax-loader.is-loading {
.post_ajax_loader.is-loading {
display: block;
animation: aksrtFadeIn 0.2s ease both;
}
.ajax-item.is-fading {
@@ -548,14 +552,15 @@ body.aksrt-modal-runtime-open {
color: var(--color-ink, #111);
}
:is(.order-comment-from, .aksrt-report-modal) .form-upload .preview {
:is(.order-comment-from, .aksrt-report-modal, .aksrt-after-sale-apply-modal) .form-upload .preview {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
:is(.order-comment-from, .aksrt-report-modal) .form-upload .preview .add,
:is(.order-comment-from, .aksrt-report-modal) .form-upload .preview .preview-item {
:is(.order-comment-from, .aksrt-report-modal, .aksrt-after-sale-apply-modal) .form-upload .preview .add,
:is(.order-comment-from, .aksrt-report-modal, .aksrt-after-sale-apply-modal) .form-upload .preview .preview-item {
width: 80px;
height: 80px;
border-radius: 10px;
@@ -571,14 +576,14 @@ body.aksrt-modal-runtime-open {
此前这里的 left/top:50% + translate 版本与该规则叠加导致过约束,
加号被推到 75%/75% 只剩“L”形,故整体删除、统一走主样式包。 */
:is(.order-comment-from, .aksrt-report-modal) .form-upload .preview .preview-item img {
:is(.order-comment-from, .aksrt-report-modal, .aksrt-after-sale-apply-modal) .form-upload .preview .preview-item img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
:is(.order-comment-from, .aksrt-report-modal) .form-upload .preview .preview-item .remove {
:is(.order-comment-from, .aksrt-report-modal, .aksrt-after-sale-apply-modal) .form-upload .preview .preview-item .remove {
position: absolute;
top: 2px;
right: 2px;
@@ -593,7 +598,7 @@ body.aksrt-modal-runtime-open {
cursor: pointer;
}
:is(.order-comment-from, .aksrt-report-modal) .form-upload .hide {
:is(.order-comment-from, .aksrt-report-modal, .aksrt-after-sale-apply-modal) .form-upload .hide {
display: none !important;
}
@@ -608,3 +613,313 @@ body.aksrt-modal-runtime-open {
width: calc(100vw - 20px);
}
}
/* ---- 售后弹窗(申请 / 售后详细 / 超时效提示,参考主题 modal-mini 几何 1:1)----
三个弹窗与禁封申诉弹窗同几何:data_class='modal-mini'(宽 400px),圆角与
底色走 --main-radius / --main-bg-color;移动端由 --bottom 底部抽屉接管。 */
.aksrt-after-sale-apply-modal:not(.aksrt-modal-runtime--bottom) .aksrt-modal-runtime__dialog,
.aksrt-after-sale-detail-modal:not(.aksrt-modal-runtime--bottom) .aksrt-modal-runtime__dialog,
.aksrt-after-sale-expired-modal:not(.aksrt-modal-runtime--bottom) .aksrt-modal-runtime__dialog,
.aksrt-after-sale-withdraw-modal:not(.aksrt-modal-runtime--bottom) .aksrt-modal-runtime__dialog {
width: min(400px, calc(100vw - 32px));
border-radius: var(--main-radius, 8px);
background: var(--main-bg-color, #fff);
}
.aksrt-after-sale-apply-modal .aksrt-modal-runtime__skeleton,
.aksrt-after-sale-detail-modal .aksrt-modal-runtime__skeleton,
.aksrt-after-sale-expired-modal .aksrt-modal-runtime__skeleton,
.aksrt-after-sale-withdraw-modal .aksrt-modal-runtime__skeleton {
padding: 24px 18px;
}
/* 蓝色炫彩标题区(参考主题 zib_get_modal_colorful_header('jb-blue', ...)):120px
绝对定位横幅 + 旋转彩斑,后续内容整体下移 120px,与禁封记录弹窗同构。 */
.aksrt-after-sale-apply-modal .modal-colorful-header,
.aksrt-after-sale-detail-modal .modal-colorful-header,
.aksrt-after-sale-expired-modal .modal-colorful-header,
.aksrt-after-sale-withdraw-modal .modal-colorful-header,
.aksrt-order-detail-modal .modal-colorful-header,
.aksrt-after-sale-apply-modal .colorful-bg,
.aksrt-after-sale-detail-modal .colorful-bg,
.aksrt-after-sale-expired-modal .colorful-bg,
.aksrt-after-sale-withdraw-modal .colorful-bg,
.aksrt-order-detail-modal .colorful-bg {
position: relative;
overflow: hidden;
color: var(--this-color);
background: var(--this-bg);
background-size: 120%;
}
.aksrt-after-sale-apply-modal .modal-colorful-header,
.aksrt-after-sale-detail-modal .modal-colorful-header,
.aksrt-after-sale-expired-modal .modal-colorful-header,
.aksrt-after-sale-withdraw-modal .modal-colorful-header,
.aksrt-order-detail-modal .modal-colorful-header {
position: absolute;
top: 0;
right: 0;
left: 0;
display: flex;
align-items: center;
justify-content: center;
height: 120px;
border-radius: var(--main-radius, 8px) var(--main-radius, 8px) 0 0;
}
.aksrt-after-sale-apply-modal .modal-colorful-header + *,
.aksrt-after-sale-detail-modal .modal-colorful-header + *,
.aksrt-after-sale-expired-modal .modal-colorful-header + *,
.aksrt-after-sale-withdraw-modal .modal-colorful-header + *,
.aksrt-order-detail-modal .modal-colorful-header + * {
margin-top: 120px;
}
.aksrt-after-sale-apply-modal .modal-colorful-header > .close,
.aksrt-after-sale-detail-modal .modal-colorful-header > .close,
.aksrt-after-sale-expired-modal .modal-colorful-header > .close,
.aksrt-after-sale-withdraw-modal .modal-colorful-header > .close,
.aksrt-order-detail-modal .modal-colorful-header > .close {
position: absolute;
top: 10px;
right: 10px;
z-index: 9;
color: var(--this-color);
font-size: 18px;
opacity: 0.6;
}
.aksrt-after-sale-apply-modal .colorful-bg > .colorful-make,
.aksrt-after-sale-apply-modal .colorful-bg > .colorful-make::before,
.aksrt-after-sale-apply-modal .colorful-bg > .colorful-make::after,
.aksrt-after-sale-detail-modal .colorful-bg > .colorful-make,
.aksrt-after-sale-detail-modal .colorful-bg > .colorful-make::before,
.aksrt-after-sale-detail-modal .colorful-bg > .colorful-make::after,
.aksrt-after-sale-expired-modal .colorful-bg > .colorful-make,
.aksrt-after-sale-withdraw-modal .colorful-bg > .colorful-make,
.aksrt-order-detail-modal .colorful-bg > .colorful-make,
.aksrt-after-sale-expired-modal .colorful-bg > .colorful-make::before,
.aksrt-after-sale-withdraw-modal .colorful-bg > .colorful-make::before,
.aksrt-order-detail-modal .colorful-bg > .colorful-make::before,
.aksrt-after-sale-expired-modal .colorful-bg > .colorful-make::after,
.aksrt-after-sale-withdraw-modal .colorful-bg > .colorful-make::after,
.aksrt-order-detail-modal .colorful-bg > .colorful-make::after {
position: absolute;
color: var(--this-color);
background: var(--this-bg);
border-radius: 100%;
content: '';
}
.aksrt-after-sale-apply-modal .colorful-bg > .colorful-make,
.aksrt-after-sale-detail-modal .colorful-bg > .colorful-make,
.aksrt-after-sale-expired-modal .colorful-bg > .colorful-make,
.aksrt-after-sale-withdraw-modal .colorful-bg > .colorful-make,
.aksrt-order-detail-modal .colorful-bg > .colorful-make {
top: 60%;
bottom: 0;
width: 323px;
height: 290px;
animation: aksrt-ban-colorful 120s linear infinite;
}
.aksrt-after-sale-apply-modal .colorful-bg > .colorful-make::before,
.aksrt-after-sale-detail-modal .colorful-bg > .colorful-make::before,
.aksrt-after-sale-expired-modal .colorful-bg > .colorful-make::before,
.aksrt-after-sale-withdraw-modal .colorful-bg > .colorful-make::before,
.aksrt-order-detail-modal .colorful-bg > .colorful-make::before {
top: -325px;
right: -558px;
bottom: 0;
width: 677px;
height: 300px;
opacity: 0.6;
transform: rotate(341deg);
}
.aksrt-after-sale-apply-modal .colorful-bg > .colorful-make::after,
.aksrt-after-sale-detail-modal .colorful-bg > .colorful-make::after,
.aksrt-after-sale-expired-modal .colorful-bg > .colorful-make::after,
.aksrt-after-sale-withdraw-modal .colorful-bg > .colorful-make::after,
.aksrt-order-detail-modal .colorful-bg > .colorful-make::after {
top: -170px;
bottom: 0;
left: -262px;
width: 402px;
height: 214px;
opacity: 0.4;
transform: rotate(326deg);
}
/* 售后原因徽章:与举报弹窗一致,可换行且间距走 mr6/mb6 工具类。 */
.aksrt-after-sale-apply-modal .form-but-radio .badg {
cursor: pointer;
user-select: none;
}
/* 订单摘要与售后明细行 */
.aksrt-after-sale-summary__row,
.aksrt-after-sale-detail__row {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 12px;
margin-bottom: 6px;
}
.aksrt-after-sale-summary__row:last-child,
.aksrt-after-sale-detail__row:last-child {
margin-bottom: 0;
}
.aksrt-after-sale-summary__value,
.aksrt-after-sale-detail__value {
min-width: 0;
text-align: right;
word-break: break-all;
}
.aksrt-after-sale-detail__statusRow {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
}
.aksrt-after-sale-reason-text {
color: var(--color-ink-2, #4e5358);
background: var(--surface-2, #eee);
border-radius: 6px;
padding: 10px 12px;
}
.aksrt-after-sale-images {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.aksrt-after-sale-images img {
width: 72px;
height: 72px;
object-fit: cover;
border-radius: 6px;
border: 1px solid var(--color-line, #e5e7eb);
display: block;
}
.aksrt-after-sale-status.is-pending { color: var(--color-warning, #f59e0b); }
.aksrt-after-sale-status.is-processing { color: var(--color-info, #3b82f6); }
.aksrt-after-sale-status.is-approved,
.aksrt-after-sale-status.is-completed { color: var(--color-success, #10b981); }
.aksrt-after-sale-status.is-rejected,
.aksrt-after-sale-status.is-cancelled { color: var(--color-error, #ef4444); }
/* 售后处理记录时间线:[节点][处理状态][时间] 一行,处理备注另起一行并对齐状态文字。
售后详细弹窗与订单详情弹窗的「售后记录」共用同一套标记;竖线为条目的
::before,末项不收口,避免出现悬空的线段。 */
.aksrt-after-sale-log { display: grid; gap: 6px; }
.aksrt-after-sale-log__empty { color: var(--modal-muted); font-size: 12px; }
.aksrt-after-sale-timeline { display: grid; margin: 0; padding: 0; list-style: none; }
.aksrt-after-sale-timeline__item { --as-accent: var(--modal-muted); position: relative; display: grid; grid-template-columns: 10px minmax(0, 1fr); gap: 10px; padding: 0 0 14px; }
.aksrt-after-sale-timeline__item:last-child { padding-bottom: 0; }
.aksrt-after-sale-timeline__item.is-pending { --as-accent: var(--modal-warning); }
.aksrt-after-sale-timeline__item.is-processing { --as-accent: var(--modal-primary); }
.aksrt-after-sale-timeline__item.is-completed { --as-accent: var(--modal-success); }
.aksrt-after-sale-timeline__item.is-rejected,
.aksrt-after-sale-timeline__item.is-cancelled { --as-accent: var(--modal-danger, #ef4444); }
.aksrt-after-sale-timeline__item::before { content: ""; position: absolute; top: 14px; bottom: -2px; left: 4px; width: 2px; border-radius: 1px; background: color-mix(in srgb, var(--as-accent) 30%, var(--modal-line)); }
.aksrt-after-sale-timeline__item:last-child::before { display: none; }
.aksrt-after-sale-timeline__knot { position: relative; z-index: 1; width: 10px; height: 10px; margin-top: 4px; border: 2px solid var(--as-accent); border-radius: 50%; background: var(--modal-surface, #fff); }
.aksrt-after-sale-timeline__body { display: grid; gap: 3px; min-width: 0; }
.aksrt-after-sale-timeline__head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 8px; }
.aksrt-after-sale-timeline__status { color: var(--as-accent); font-size: 12px; font-weight: 700; line-height: 1.5; }
.aksrt-after-sale-timeline__time { color: var(--modal-muted); font-size: 11px; line-height: 1.5; font-variant-numeric: tabular-nums; }
.aksrt-after-sale-timeline__note { margin: 0; color: var(--modal-ink); font-size: 12.5px; line-height: 1.65; word-break: break-word; }
/* 状态行右侧单元格:状态徽章 + 「撤销售后」入口(仅待审核工单渲染)。 */
.aksrt-after-sale-detail__statusCell { display: inline-flex; align-items: center; gap: 10px; }
.aksrt-after-sale-withdraw { display: inline-flex; align-items: center; gap: 3px; padding: 0; border: 0; background: transparent; color: var(--modal-danger, #ef4444); font-size: 12px; font-weight: 600; line-height: 1.4; cursor: pointer; transition: color .15s ease; }
.aksrt-after-sale-withdraw .fa { font-size: 12px; }
.aksrt-after-sale-withdraw:hover { color: color-mix(in srgb, var(--modal-danger) 78%, var(--modal-ink)); }
/* 售后提示弹窗(撤销售后确认)正文。 */
.aksrt-after-sale-withdraw__body { margin-bottom: 16px; color: var(--color-ink-2, #4e5358); line-height: 1.7; }
/* 售后弹窗底部按钮:全宽平铺贴底(负 margin 抵消 __content 内边距)+ 分隔线,
子按钮去独立背景/边框/圆角(文字色区分主次),覆盖上方全局按钮网格的胶囊观感;
仅在售后弹窗家族生效,其他 runtime 弹窗仍走全局网格样式。
footer 自身不留垂直内边距——上下 12px 并入按钮(min-height 62 = 12+38+12),
按钮盒即整个底部区域(hover 底色/分界线随之满幅)。 */
.aksrt-after-sale-apply-modal .but-average.modal-buts,
.aksrt-after-sale-detail-modal .but-average.modal-buts,
.aksrt-after-sale-expired-modal .but-average.modal-buts,
.aksrt-after-sale-withdraw-modal .but-average.modal-buts {
gap: 0;
margin: 16px -18px -16px;
padding: 0;
border-top: 1px solid var(--modal-line-soft, rgba(50, 50, 50, 0.06));
background: var(--main-bg-color, #fff);
}
.aksrt-after-sale-apply-modal .but-average.modal-buts > .but,
.aksrt-after-sale-detail-modal .but-average.modal-buts > .but,
.aksrt-after-sale-expired-modal .but-average.modal-buts > .but,
.aksrt-after-sale-withdraw-modal .but-average.modal-buts > .but {
min-height: 62px;
border: 0;
border-radius: 0;
box-shadow: none;
background: transparent;
color: var(--modal-muted, #777);
font-weight: 600;
cursor: pointer;
--this-bg: transparent;
--this-border: transparent;
--this-shadow: none;
}
.aksrt-after-sale-apply-modal .but-average.modal-buts > .but.c-blue,
.aksrt-after-sale-detail-modal .but-average.modal-buts > .but.c-blue,
.aksrt-after-sale-expired-modal .but-average.modal-buts > .but.c-blue,
.aksrt-after-sale-withdraw-modal .but-average.modal-buts > .but.c-blue {
background: transparent;
border: 0;
color: var(--modal-primary, #f04494);
}
/* 相邻按钮分界线:legacy footer 自带 .but + .but 左分隔线,上方平铺规则的 border:0
会把它抹掉,这里按家族分隔色还原(单按钮无相邻兄弟,天然无分界线)。
需排在 .c-blue 规则之后:确认提交等主按钮同受 border:0 影响,后者同名优先级。 */
.aksrt-after-sale-apply-modal .but-average.modal-buts > .but + .but,
.aksrt-after-sale-detail-modal .but-average.modal-buts > .but + .but,
.aksrt-after-sale-expired-modal .but-average.modal-buts > .but + .but,
.aksrt-after-sale-withdraw-modal .but-average.modal-buts > .but + .but {
border-left: 1px solid var(--modal-line-soft, rgba(50, 50, 50, 0.06));
}
/* 悬停反馈(参考 Element 卡片 footer 的 hover 语言):整块区域浅灰底 + 文字转主色,
次级按钮 hover 升为主色、主按钮略加深;平铺按钮已满幅,底色即铺满整个区域,
基础 .but 的 0.15s 过渡自动生效,并压掉 compat 层的 hover 半透明。 */
.aksrt-after-sale-apply-modal .but-average.modal-buts > .but:hover,
.aksrt-after-sale-detail-modal .but-average.modal-buts > .but:hover,
.aksrt-after-sale-expired-modal .but-average.modal-buts > .but:hover,
.aksrt-after-sale-withdraw-modal .but-average.modal-buts > .but:hover {
opacity: 1;
background: color-mix(in srgb, var(--modal-ink, #333333) 5%, transparent);
color: var(--modal-primary, #f04494);
}
.aksrt-after-sale-apply-modal .but-average.modal-buts > .but.c-blue:hover,
.aksrt-after-sale-detail-modal .but-average.modal-buts > .but.c-blue:hover,
.aksrt-after-sale-expired-modal .but-average.modal-buts > .but.c-blue:hover,
.aksrt-after-sale-withdraw-modal .but-average.modal-buts > .but.c-blue:hover {
color: color-mix(in srgb, var(--modal-primary, #f04494) 82%, #000000);
}
/* 单按钮 footer 占满整行(全局网格是 1fr 1fr,单按钮会只占一半)。 */
.aksrt-after-sale-apply-modal .but-average.modal-buts > .but:only-child,
.aksrt-after-sale-detail-modal .but-average.modal-buts > .but:only-child,
.aksrt-after-sale-expired-modal .but-average.modal-buts > .but:only-child,
.aksrt-after-sale-withdraw-modal .but-average.modal-buts > .but:only-child { grid-column: 1 / -1; }
-50
View File
@@ -166,56 +166,6 @@ html.dark .archive-timeline__badge {
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 {
text-align: center;
margin-top: 20px;
-44
View File
@@ -155,50 +155,6 @@
border-color: var(--color-primary, #f04494);
}
/* AJAX 加载更多 */
.load-more {
text-align: center;
padding: 20px 0;
}
.load-more__btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 10px 24px;
font-size: 13px;
color: var(--color-ink-2, #4e5358);
background: var(--surface-1, #fff);
border: 1px solid var(--color-line, rgba(50, 50, 50, 0.06));
border-radius: 20px;
cursor: pointer;
transition: all 0.2s;
}
.load-more__btn:hover {
color: var(--color-primary, #f04494);
border-color: var(--color-primary, #f04494);
}
.load-more__btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.load-more__spinner {
display: inline-block;
width: 16px;
height: 16px;
border: 2px solid var(--surface-3, #e8e8e8);
border-top-color: var(--color-primary, #f04494);
border-radius: 50%;
animation: spin 0.6s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* 移动端 */
@media (max-width: 767px) {
.pagination a,
+35
View File
@@ -14,6 +14,17 @@
margin-left: 8px !important;
}
/* 骨架与真实卡片同宽:.posts-row 以 -8px 抵消卡片边距,loader 被上面的
:not() 排除在补偿外,会宽出 16px;这里补上等同的水平边距。
补偿落在骨架内层的 .posts-item 上(与真实卡片同一元素),不落外层
.post_ajax_loader —— 否则内层 .posts-item.card 的 margin:8px 会与外层
补偿叠成 16px,小工具 card 骨架因此窄出 8px。 */
.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;
@@ -447,6 +458,17 @@
margin-left: 8px;
}
/* 骨架与卡片同宽同轴:.widget-ajaxpager / .tab-pane 用负边距抵消卡片的水平
边距。补偿同样落在骨架内层的 .posts-item 上(与真实卡片同一元素),
不落外层 .post_ajax_loader —— 否则内层 .posts-item.card 自带的 margin:8px
会与外层补偿叠成 16px,card 骨架因此窄出 8px。 */
.widget-main-post.style-list .widget-ajaxpager > .post_ajax_loader .posts-item,
.widget-main-post.style-card .widget-ajaxpager > .post_ajax_loader .posts-item,
.widget-tab-post .tab-content > .tab-pane > .widget-ajaxpager > .post_ajax_loader .posts-item {
margin-right: 8px;
margin-left: 8px;
}
.posts-mini:hover img,
.posts-item:hover .item-thumbnail img,
.posts-item.mult-thumb .thumb-items > span > img:hover {
@@ -517,6 +539,12 @@
margin-left: 5px !important;
}
.posts-row > .post_ajax_loader .posts-item,
.posts-row > .post_ajax_loader .posts-mini {
margin-right: 5px;
margin-left: 5px;
}
.posts-item .item-thumbnail {
width: 138px;
}
@@ -563,6 +591,13 @@
margin-left: 5px;
}
.widget-main-post.style-list .widget-ajaxpager > .post_ajax_loader .posts-item,
.widget-main-post.style-card .widget-ajaxpager > .post_ajax_loader .posts-item,
.widget-tab-post .tab-content > .tab-pane > .widget-ajaxpager > .post_ajax_loader .posts-item {
margin-right: 5px;
margin-left: 5px;
}
.swiper-scroll .posts-item {
margin-right: 10px !important;
}
+3 -2
View File
@@ -50,8 +50,9 @@
color: var(--color-primary, #f04494);
}
/* .widget a 的链接色不得覆盖 .but 按钮自身配色(zibll 的 zib-widget 不设置链接色) */
.widget a.but {
/* .widget a 的链接色不得覆盖 .but / .img-badge 自身配色(zibll 的 zib-widget 不设置链接色) */
.widget a.but,
.widget a.img-badge {
color: var(--this-color, #fff);
}
+189
View File
@@ -0,0 +1,189 @@
/* ---- 文章打赏:内容使用 AksrtModalRuntime,几何对齐参考主题 modal-mini ---- */
.aksrt-modal-runtime.aksrt-reward-modal:not(.aksrt-modal-runtime--bottom) .aksrt-modal-runtime__dialog {
width: min(400px, calc(100vw - 32px));
border-radius: var(--main-radius, 8px);
background: var(--main-bg-color, #fff);
}
.aksrt-modal-runtime.aksrt-reward-modal .aksrt-modal-runtime__content {
padding: 0;
}
.aksrt-reward-modal .modal-colorful-header + .aksrt-reward__body {
margin-top: 0;
padding: 135px 15px 15px;
color: var(--main-color, var(--color-ink, #333));
}
.aksrt-reward__desc {
margin: 0 0 16px;
text-align: center;
}
.aksrt-modal-runtime.aksrt-reward-modal .aksrt-reward__amounts {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 8px;
margin: 16px 0;
}
.aksrt-modal-runtime.aksrt-reward-modal .aksrt-reward__amount {
display: inline-flex;
width: 100%;
min-width: 0;
min-height: 46px;
align-items: center;
justify-content: center;
gap: 4px;
margin: 0;
padding: 8px 6px;
border: 1px solid var(--color-line-stronger, rgba(50, 50, 50, 0.12));
border-radius: var(--radius, 8px);
background: var(--surface-2, #f5f6f7);
color: var(--color-ink-2, #4e5358);
cursor: pointer;
font-size: 16px;
font-weight: 700;
line-height: 1.2;
box-shadow: none;
transition: border-color 0.2s ease, background-color 0.2s ease, color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;
}
.aksrt-modal-runtime.aksrt-reward-modal .aksrt-reward__amount:hover {
border-color: var(--focus-color, var(--color-primary, #f04494));
background: var(--color-primary-soft, rgba(240, 68, 148, 0.1));
color: var(--focus-color, var(--color-primary, #f04494));
opacity: 1;
transform: translateY(-1px);
}
.aksrt-modal-runtime.aksrt-reward-modal .aksrt-reward__amount:focus-visible {
outline: 0;
box-shadow: 0 0 0 3px var(--color-primary-soft, rgba(240, 68, 148, 0.2));
}
.aksrt-modal-runtime.aksrt-reward-modal .aksrt-reward__amount.is-active,
.aksrt-modal-runtime.aksrt-reward-modal .aksrt-reward__amount.is-active:hover {
border-color: var(--focus-color, var(--color-primary, #f04494));
background: var(--focus-color, var(--color-primary, #f04494));
color: #fff;
box-shadow: 0 3px 10px var(--color-primary-soft, rgba(240, 68, 148, 0.2));
opacity: 1;
transform: none;
}
.aksrt-reward__currency {
font-size: 13px;
font-weight: 400;
}
.aksrt-reward__value {
font-size: 15px;
}
.aksrt-modal-runtime.aksrt-reward-modal .aksrt-reward__custom {
display: flex;
width: 100%;
align-items: center;
justify-content: space-between;
gap: 12px;
margin: 0 0 16px;
color: var(--color-ink-2, #4e5358);
font-size: 14px;
font-weight: 600;
}
.aksrt-reward__custom-label {
flex: 0 0 auto;
}
.aksrt-modal-runtime.aksrt-reward-modal .aksrt-reward__input-wrap {
display: flex;
width: min(190px, 62%);
min-height: 44px;
align-items: center;
padding: 0 10px 0 12px;
border: 1px solid var(--color-line-stronger, rgba(50, 50, 50, 0.12));
border-radius: var(--radius, 8px);
background: var(--surface-1, #fff);
transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
.aksrt-modal-runtime.aksrt-reward-modal .aksrt-reward__input-wrap:focus-within {
border-color: var(--focus-color, var(--color-primary, #f04494));
box-shadow: 0 0 0 3px var(--color-primary-soft, rgba(240, 68, 148, 0.1));
}
.aksrt-reward__input-currency {
flex: 0 0 auto;
margin-right: 6px;
color: var(--muted-color, var(--color-ink-3, #777));
font-size: 14px;
font-weight: 600;
}
.aksrt-modal-runtime.aksrt-reward-modal .aksrt-reward__input {
width: 100%;
min-width: 0;
min-height: 42px;
padding: 0;
border: 0;
outline: 0;
background: transparent;
color: var(--color-ink, #333);
font-size: 15px;
font-weight: 600;
text-align: right;
-moz-appearance: textfield;
box-shadow: none;
}
.aksrt-reward__input::-webkit-inner-spin-button,
.aksrt-reward__input::-webkit-outer-spin-button {
margin: 0;
-webkit-appearance: none;
}
.aksrt-reward__input::placeholder {
color: var(--muted-color, var(--color-ink-4, #999));
opacity: 1;
}
.aksrt-reward__input:disabled,
.aksrt-reward__amount:disabled {
cursor: wait;
opacity: 0.6;
}
.aksrt-reward__submit {
min-height: 42px;
}
.aksrt-reward__submit:disabled {
cursor: wait;
opacity: 0.7;
}
.aksrt-reward__message {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
.aksrt-reward__captcha {
margin: 0 0 14px;
}
.aksrt-modal-runtime--bottom.aksrt-reward-modal .aksrt-modal-runtime__dialog {
border-radius: 16px 16px 0 0;
}
.aksrt-modal-runtime--bottom.aksrt-reward-modal .modal-colorful-header {
border-radius: 16px 16px 0 0;
}
-141
View File
@@ -1,141 +0,0 @@
/* ========== 骨架屏加载 — 对齐 legacy skeleton loading ========== */
.skeleton {
position: relative;
overflow: hidden;
background: var(--surface-2, #eee);
border-radius: var(--radius, 8px);
}
.skeleton::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(
90deg,
transparent 0%,
rgba(255, 255, 255, 0.4) 50%,
transparent 100%
);
animation: skeleton-shimmer 1.5s infinite;
}
@keyframes skeleton-shimmer {
0% { transform: translateX(-100%); }
100% { transform: translateX(100%); }
}
/* 骨架屏变体 */
.skeleton-text {
height: 14px;
border-radius: 4px;
margin-bottom: 8px;
}
.skeleton-text--title {
height: 20px;
width: 60%;
margin-bottom: 12px;
}
.skeleton-text--short {
width: 40%;
}
.skeleton-text--medium {
width: 70%;
}
.skeleton-avatar {
width: 48px;
height: 48px;
border-radius: 50%;
flex-shrink: 0;
}
.skeleton-avatar--sm {
width: 32px;
height: 32px;
}
.skeleton-avatar--lg {
width: 64px;
height: 64px;
}
.skeleton-thumb {
width: 100%;
aspect-ratio: 16 / 9;
border-radius: var(--radius, 8px);
}
.skeleton-thumb--square {
aspect-ratio: 1;
}
.skeleton-thumb--card {
aspect-ratio: 16 / 10;
border-radius: var(--radius, 8px) var(--radius, 8px) 0 0;
}
/* 卡片骨架屏 */
.skeleton-card {
background: var(--surface-1, #fff);
border-radius: var(--radius, 8px);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
overflow: hidden;
}
.skeleton-card__thumb {
width: 100%;
aspect-ratio: 16 / 10;
}
.skeleton-card__body {
padding: 14px 16px;
}
.skeleton-card__meta {
display: flex;
align-items: center;
gap: 8px;
margin-top: 10px;
}
/* 评论骨架屏 */
.skeleton-comment {
display: flex;
gap: 12px;
padding: 16px 0;
border-bottom: 1px solid var(--color-line-soft, rgba(0, 0, 0, 0.03));
}
.skeleton-comment__body {
flex: 1;
}
.skeleton-comment__header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 10px;
}
/* 通用骨架屏容器 */
.skeleton-list .skeleton-item {
padding: 12px 0;
border-bottom: 1px solid var(--color-line-soft, rgba(0, 0, 0, 0.03));
}
.skeleton-list .skeleton-item:last-child {
border-bottom: none;
}
/* 减弱动画偏好 */
@media (prefers-reduced-motion: reduce) {
.skeleton::after {
animation: none;
}
}
+97 -12
View File
@@ -64,6 +64,27 @@
z-index: 100055;
}
/* public.css redeclares .muted-box after legacy utilities, overriding its
background, radius, and padding utilities. Restore the reference box contract. */
.modal[data-aksrt-vip-points-modal] .muted-box.padding-h10 {
background-color: var(--this-bg, var(--muted-border-color));
border-radius: var(--main-radius);
padding: 10px 15px;
}
.modal[data-aksrt-vip-points-modal] .muted-box.padding-10,
.modal[data-aksrt-vip-points-modal] .aksrt-vip-points-desc {
background-color: var(--this-bg, var(--muted-border-color));
border-radius: var(--main-radius);
padding: 10px;
}
.modal[data-aksrt-vip-points-modal] .aksrt-vip-points-desc {
display: block;
margin-top: 20px;
color: var(--muted-2-color, #999);
}
.pay-vip-form-row {
display: grid;
gap: 12px;
@@ -102,21 +123,85 @@
cursor: wait;
}
.vipbg-v2 {
/* 会员弹窗骨架屏(pay-vip.js vipSkeleton()):仿真实弹窗布局——
用户框 + 等级卡行 + 双列套餐面板。线条/色块与 shimmer 动画复用全局
.aksrt-modal-runtime__skeleton-line/-block 与 @keyframes
aksrt-modal-skeleton(build/public.min.css 已全局入队);
这里只定义 VIP 专属形状(头像圆、tab 卡、双列 grid、按钮条),
与 .pay-vip-form-row 的 640px 双列阈值保持一致。 */
.aksrt-vip-modal-skeleton {
display: flex;
flex-direction: column;
gap: 14px;
padding: 15px;
}
.aksrt-vip-modal-skeleton__user {
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
padding: 8px 10px;
gap: 10px;
}
.aksrt-vip-modal-skeleton__avatar {
width: 42px;
height: 42px;
border-radius: 50%;
flex: none;
}
.aksrt-vip-modal-skeleton__lines {
display: flex;
flex-direction: column;
gap: 6px;
flex: 1;
min-width: 0;
}
.aksrt-vip-modal-skeleton__tabs {
display: flex;
gap: 10px;
}
.aksrt-vip-modal-skeleton__tab {
flex: 1;
height: 56px;
}
.aksrt-vip-modal-skeleton__pane {
display: grid;
gap: 12px;
}
.aksrt-vip-modal-skeleton__info {
display: flex;
flex-direction: column;
gap: 10px;
padding-top: 4px;
}
.aksrt-vip-modal-skeleton__products {
display: flex;
flex-direction: column;
gap: 10px;
}
.aksrt-vip-modal-skeleton__button {
height: 38px;
}
/* 专属形状共用骨架底色与 shimmer(值与 modal-runtime.css 的线条/块一致)。 */
.aksrt-vip-modal-skeleton__avatar,
.aksrt-vip-modal-skeleton__tab,
.aksrt-vip-modal-skeleton__button {
display: block;
border-radius: 8px;
color: #fff;
background: linear-gradient(90deg, #ffb347, #ff6b6b);
background: linear-gradient(90deg, rgba(148, 163, 184, 0.16), rgba(148, 163, 184, 0.3), rgba(148, 163, 184, 0.16));
background-size: 200% 100%;
animation: aksrt-modal-skeleton 1.2s ease-in-out infinite;
}
.vipbg-v2.level-2 {
background: linear-gradient(90deg, #9b59b6, #3f51b5);
}
.vip-expdate-tag {
font-size: 12px;
@media (min-width: 640px) {
.aksrt-vip-modal-skeleton__pane {
grid-template-columns: 1fr 1.2fr;
}
}
+52 -14
View File
@@ -2676,6 +2676,13 @@ select.form-control {
position: relative;
}
/* .form-select::before 已经画了 FA5 箭头,隐藏原生箭头避免出现两个。 */
.form-select select {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
}
.form-select::before {
font: normal normal 900 14px/1 "Font Awesome 5 Free";
content: '\f107';
@@ -5929,7 +5936,7 @@ body:has(.modal.in) .float-btn,
}
.payvip-icon:before {
content: '\f219';
content: '\f3a5';
font: normal normal 900 14px/1 "Font Awesome 5 Free";
font-size: 3.5em;
right: -0.2em !important;
@@ -5937,16 +5944,6 @@ body:has(.modal.in) .float-btn,
opacity: 0.06;
}
.vipbg-v1 {
background: linear-gradient(60deg, #f0bf81 0, #fdefd6 30%, #f2deb9 40%, #fdc277 80%, #f9e1b7 100%);
color: #8c5728;
--this-color: #8c5728;
}
.vipbg-v1:before {
content: '\f006';
}
.vip-texticon {
font-size: 0.85em;
background: transparent;
@@ -7486,6 +7483,17 @@ badge.top,
margin-right: 8px !important;
}
/* 骨架与真实卡片同宽同轴:.posts-row 用 -8px 抵消卡片边距,原 :not() 把骨架
排除在补偿外,骨架于是比卡片左右各宽 8px。补上等同边距。
注意补偿落在骨架**内层**的 .posts-item 上(与真实卡片同一元素、同一选择器),
不能落在外层 .post_ajax_loader 上 —— 骨架内层本身带 .posts-item.card 的
margin:8px,外层再补 8px 就是双重补偿,小工具 card 骨架会窄出 8px。 */
.posts-row > .post_ajax_loader .posts-item,
.posts-row > .post_ajax_loader .posts-mini {
margin-left: 8px;
margin-right: 8px;
}
.posts-item {
padding: 20px;
margin: 15px 0;
@@ -7890,6 +7898,18 @@ badge.top,
margin-right: 8px;
}
/* 骨架与卡片同宽同轴:.widget-ajaxpager / .tab-pane 用负边距抵消卡片的水平
边距。补偿同样落在骨架内层的 .posts-item 上(与真实卡片同一元素),
而不是外层 .post_ajax_loader —— 否则骨架内层自带的 .posts-item.card
margin:8px 会与外层补偿叠成 16px,让 card 骨架窄出 8px。
style-card 的卡片由 .posts-item.card 自带 margin:8px,这里只需覆盖骨架内层。 */
.widget-main-post.style-list .widget-ajaxpager > .post_ajax_loader .posts-item,
.widget-main-post.style-card .widget-ajaxpager > .post_ajax_loader .posts-item,
.widget-tab-post .tab-content > .tab-pane > .widget-ajaxpager > .post_ajax_loader .posts-item {
margin-left: 8px;
margin-right: 8px;
}
.widget-tab-post.index-tab .list-inline {
margin-bottom: 15px;
}
@@ -9635,11 +9655,16 @@ img.alignleft {
padding: 15px;
}
/* AKSRT 适配:站点行高(--leading-normal: 1.6)与参考主题 bootstrap
* 的 1.42857143 不同,用户卡片内的 img-badge(签到书签)钉住参考行高;
* 字号已随 --text-base 对齐参考主题(14px),无需单独声明。 */
/* AKSRT 适配:签到书签排版逐字对齐参考主题 bootstrap 的 body 计算值——
* 行高 1.42857143(站点 --leading-normal 为 1.6)、字体族
* "Helvetica Neue", Helvetica, Arial, sans-serif(站点 --font-body 为
* 现代系统栈),并还原浏览器默认的字体平滑与渲染(站点 html 重置为
* antialiased + optimizeLegibility);字号随 --text-base 已是参考的 14px。 */
.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;
}
.user-card name .display-name {
@@ -11213,6 +11238,13 @@ body.error404 {
margin-bottom: 30px;
}
.widget-main-post.style-list .widget-ajaxpager > .post_ajax_loader .posts-item,
.widget-main-post.style-card .widget-ajaxpager > .post_ajax_loader .posts-item,
.widget-tab-post .tab-content > .tab-pane > .widget-ajaxpager > .post_ajax_loader .posts-item {
margin-left: 5px;
margin-right: 5px;
}
.posts-row {
margin-left: -5px;
margin-right: -5px;
@@ -11223,6 +11255,12 @@ body.error404 {
margin-right: 5px !important;
}
.posts-row > .post_ajax_loader .posts-item,
.posts-row > .post_ajax_loader .posts-mini {
margin-left: 5px;
margin-right: 5px;
}
.posts-item.card {
width: calc(50% - 10px);
margin: 5px;
+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
+1 -1
View File
@@ -34,7 +34,7 @@
.friend-card__avatar {
width: 48px;
height: 48px;
border-radius: var(--radius);
border-radius: var(--radius-avatar, 50%);
object-fit: cover;
flex-shrink: 0;
background: var(--surface-2);
+45 -10
View File
@@ -47,10 +47,10 @@
.aksrt-orders-page__search { display: flex; gap: 8px; align-items: center; margin: 20px 0 12px; padding: 7px; border: 1px solid var(--orders-line); border-radius: 12px; background: var(--surface-1, #fff); }
.aksrt-orders-page__search > i { margin-left: 7px; color: var(--orders-muted); font-size: 13px; }
.aksrt-orders-page__search input { flex: 1; min-width: 0; border: 0; outline: 0; background: transparent; color: var(--orders-ink); font-size: 13px; }
.aksrt-orders-page__search button, .aksrt-orders-page__card-actions .btn-primary, .aksrt-orders-page__card-actions .btn-outline, .aksrt-orders-page__card-actions .btn-ghost, .aksrt-orders-page__empty .btn-outline, .aksrt-order-detail__actions .btn-primary, .aksrt-order-detail__actions .btn-outline, .aksrt-order-detail__actions .btn-ghost { display: inline-flex; align-items: center; justify-content: center; min-height: 34px; padding: 0 13px; border-radius: 8px; border: 1px solid transparent; font-size: 12px; font-weight: 650; text-decoration: none; cursor: pointer; }
.aksrt-orders-page .btn-primary, .aksrt-order-detail__actions .btn-primary { background: var(--color-primary, #3b82f6); color: var(--color-on-primary, #fff); }
.aksrt-orders-page .btn-outline, .aksrt-order-detail__actions .btn-outline { border-color: var(--orders-line); background: var(--surface-1, #fff); color: var(--orders-ink); }
.aksrt-orders-page .btn-ghost, .aksrt-order-detail__actions .btn-ghost { background: transparent; color: var(--orders-muted); }
.aksrt-orders-page__search button, .aksrt-orders-page__card-actions .btn-primary, .aksrt-orders-page__card-actions .btn-outline, .aksrt-orders-page__card-actions .btn-ghost, .aksrt-orders-page__empty .btn-outline { display: inline-flex; align-items: center; justify-content: center; min-height: 34px; padding: 0 13px; border-radius: 8px; border: 1px solid transparent; font-size: 12px; font-weight: 650; text-decoration: none; cursor: pointer; }
.aksrt-orders-page .btn-primary { background: var(--color-primary, #3b82f6); color: var(--color-on-primary, #fff); }
.aksrt-orders-page .btn-outline { border-color: var(--orders-line); background: var(--surface-1, #fff); color: var(--orders-ink); }
.aksrt-orders-page .btn-ghost { background: transparent; color: var(--orders-muted); }
.aksrt-orders-page__filter-toggle { background: var(--surface-2, #f4f6f9); color: var(--orders-ink); white-space: nowrap; }
.aksrt-orders-page__filter-panel { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin: -2px 0 14px; padding: 12px; border-radius: 10px; background: var(--surface-2, #f4f6f9); }
@@ -108,9 +108,16 @@
--detail-success: var(--modal-success);
}
.aksrt-order-detail-modal:not(.aksrt-modal-runtime--bottom) .aksrt-modal-runtime__dialog { width: min(480px, calc(100vw - 32px)); }
.aksrt-order-detail-modal .aksrt-modal-runtime__header { padding: 15px 18px 14px; border-bottom: 1px solid var(--detail-line); }
.aksrt-order-detail-modal .aksrt-modal-runtime__title { font-size: 16px; font-weight: 700; }
/* 订单详情弹窗:与售后弹窗统一为 modal-mini 几何(400px);标题区改用蓝色炫彩头,运行时不再渲染默认标题栏。 */
.aksrt-order-detail-modal:not(.aksrt-modal-runtime--bottom) .aksrt-modal-runtime__dialog {
width: min(400px, calc(100vw - 32px));
border-radius: var(--main-radius, 8px);
background: var(--main-bg-color, #fff);
}
/* 滚动发生在 dialog 本体(modal-runtime 的 overflow:auto),内容超高时右缘会露出
滚动栏;与售后/申诉弹窗口径一致:完全隐藏滚动条(含细滚动条),滚动行为不变。 */
.aksrt-order-detail-modal .aksrt-modal-runtime__dialog { -ms-overflow-style: none; scrollbar-width: none; }
.aksrt-order-detail-modal .aksrt-modal-runtime__dialog::-webkit-scrollbar { display: none; }
.aksrt-order-detail-modal .aksrt-modal-runtime__content { display: flex; flex-direction: column; gap: 18px; padding: 16px 18px 0; }
.aksrt-order-detail__skeleton { display: grid; gap: 12px; padding: 4px 0; }
@@ -156,8 +163,22 @@
.aksrt-order-detail__refresh-state p { margin: 0; }
.aksrt-order-detail__refresh-state button { grid-column: 2; justify-self: start; }
.aksrt-order-detail-modal .aksrt-order-detail__actions { position: sticky; bottom: 0; z-index: 2; flex-direction: row; flex-wrap: wrap; justify-content: flex-end; gap: 8px; margin: 0 -18px; padding: 13px 18px; border-top: 1px solid var(--detail-line); background: var(--detail-surface); }
.aksrt-order-detail-modal .aksrt-order-detail__actions > button { flex: 0 0 auto; min-height: 38px; padding: 0 16px; white-space: nowrap; }
/* 底部操作区:按钮排布沿用主题 .but-average.modal-buts,此处仅保留吸底定位与通栏背景。
主题 modal-buts 自带 margin-top:16px,吸底场景下用更高优先级清掉,避免内容与吸底条之间出现空隙。
footer 不留垂直内边距(水平同理归零)——上下 13px 并入按钮(min-height 64 = 13+38+13),
按钮盒即整个底部区域;安全区留在 footer 底部,避免按钮文字沉入 Home 指示条。 */
.aksrt-order-detail-modal .aksrt-order-detail__actions { position: sticky; bottom: 0; z-index: 2; margin: 0 -18px; padding: 0 0 env(safe-area-inset-bottom, 0); border-top: 1px solid var(--detail-line); background: var(--detail-surface); }
.aksrt-order-detail-modal .aksrt-order-detail__actions.but-average.modal-buts { margin-top: 0; gap: 0; }
/* 底部按钮观感与售后弹窗家族对齐(modal-runtime 同款平铺规则):去胶囊底色/边框/圆角,
文字色区分主次;仅「关闭」单按钮时占满整行,即售后详细「我知道了」的呈现。
分界线与悬停底色需排在 .c-blue 规则之后:主按钮同受 border:0 影响与主色覆盖,同名优先级看先后。 */
.aksrt-order-detail-modal .aksrt-order-detail__actions.but-average.modal-buts > .but { min-height: 64px; border: 0; border-radius: 0; box-shadow: none; background: transparent; color: var(--modal-muted, #777); font-weight: 600; cursor: pointer; --this-bg: transparent; --this-border: transparent; --this-shadow: none; }
.aksrt-order-detail-modal .aksrt-order-detail__actions.but-average.modal-buts > .but.c-blue { background: transparent; border: 0; color: var(--modal-primary, #f04494); }
.aksrt-order-detail-modal .aksrt-order-detail__actions.but-average.modal-buts > .but + .but { border-left: 1px solid var(--detail-line); }
/* 悬停反馈与售后弹窗家族同款:整块区域浅灰底 + 文字转主色(次级升主色、主按钮略加深)。 */
.aksrt-order-detail-modal .aksrt-order-detail__actions.but-average.modal-buts > .but:hover { opacity: 1; background: color-mix(in srgb, var(--modal-ink, #333333) 5%, transparent); color: var(--modal-primary, #f04494); }
.aksrt-order-detail-modal .aksrt-order-detail__actions.but-average.modal-buts > .but.c-blue:hover { color: color-mix(in srgb, var(--modal-primary, #f04494) 82%, #000000); }
.aksrt-order-detail-modal .aksrt-order-detail__actions.but-average.modal-buts > .but:only-child { grid-column: 1 / -1; }
/* Orders extensions */
.aksrt-orders-page__extra-nav { display: flex; flex-wrap: wrap; gap: 8px; margin: 0 0 12px; }
@@ -176,5 +197,19 @@
.aksrt-coupon-card__meta { margin-bottom: 4px; color: var(--muted-2-color, #888); font-size: 12px; }
.aksrt-orders-page__field { margin-bottom: 12px; }
.aksrt-orders-page__input, .aksrt-orders-page__select, .aksrt-orders-page__textarea { box-sizing: border-box; width: 100%; padding: 8px 10px; border: 1px solid var(--main-border-color, #ddd); border-radius: 6px; }
@media (max-width: 767px) { .uc-orders-panel__head { grid-template-columns: 1fr; gap: 17px; }.aksrt-orders-page__stats { max-width: none; }.aksrt-orders-page__search { flex-wrap: wrap; }.aksrt-orders-page__search input { min-width: 150px; }.aksrt-orders-page__search .btn-primary { margin-left: auto; }.aksrt-orders-page__filter-toggle { margin-left: auto; }.aksrt-orders-page__card-rail time { width: 100%; margin-left: 0; order: 3; }.aksrt-orders-page__card-main { grid-template-columns: 48px minmax(0, 1fr); }.aksrt-orders-page__card-img, .aksrt-orders-page__card-art { width: 48px; height: 48px; }.aksrt-orders-page__card-value { grid-column: 2; text-align: left; }.aksrt-orders-page__card-actions > :not(.aksrt-orders-page__countdown) { flex: 1 1 auto; }.aksrt-orders-page__countdown { width: 100%; }.aksrt-order-detail-modal .aksrt-order-detail__actions > button { flex: 1 1 0; min-width: 0; padding: 0 10px; } }
@media (max-width: 767px) { .uc-orders-panel__head { grid-template-columns: 1fr; gap: 17px; }.aksrt-orders-page__stats { max-width: none; }.aksrt-orders-page__search { flex-wrap: wrap; }.aksrt-orders-page__search input { min-width: 150px; }.aksrt-orders-page__search .btn-primary { margin-left: auto; }.aksrt-orders-page__filter-toggle { margin-left: auto; }.aksrt-orders-page__card-rail time { width: 100%; margin-left: 0; order: 3; }.aksrt-orders-page__card-main { grid-template-columns: 48px minmax(0, 1fr); }.aksrt-orders-page__card-img, .aksrt-orders-page__card-art { width: 48px; height: 48px; }.aksrt-orders-page__card-value { grid-column: 2; text-align: left; }.aksrt-orders-page__card-actions > :not(.aksrt-orders-page__countdown) { flex: 1 1 auto; }.aksrt-orders-page__countdown { width: 100%; }}
html.dark .aksrt-orders-page__stat, html.dark .aksrt-orders-page__card { background: var(--surface-1, #1a1d24); } html.dark .aksrt-orders-page__card-rail, html.dark .aksrt-orders-page__filter-panel { background: var(--surface-2, #12151b); }
/* ---- 售后记录卡片(订单详情弹窗) ---- */
.aksrt-order-detail__after-sales { display: grid; gap: 10px; margin: 0; padding: 0; list-style: none; }
.aksrt-order-detail__after-sale { --as-accent: var(--detail-muted); display: grid; gap: 10px; padding: 13px 14px; border: 1px solid var(--detail-line); border-left: 3px solid var(--as-accent); border-radius: 12px; background: var(--detail-surface); }
.aksrt-order-detail__after-sale.is-pending { --as-accent: var(--detail-warning); }
.aksrt-order-detail__after-sale.is-processing { --as-accent: var(--detail-primary); }
.aksrt-order-detail__after-sale.is-approved, .aksrt-order-detail__after-sale.is-completed { --as-accent: var(--detail-success); }
.aksrt-order-detail__after-sale.is-rejected, .aksrt-order-detail__after-sale.is-cancelled { --as-accent: var(--modal-danger, #ef4444); }
.aksrt-order-detail__after-sale-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.aksrt-order-detail__after-sale-badge { padding: 3px 11px; border-radius: 999px; background: color-mix(in srgb, var(--as-accent) 12%, transparent); color: var(--as-accent); font-size: 12px; font-weight: 700; line-height: 1.5; }
.aksrt-order-detail__after-sale-head time { color: var(--detail-muted); font-size: 11.5px; font-variant-numeric: tabular-nums; }
.aksrt-order-detail__after-sale-reason { margin: 0; color: var(--detail-ink); font-size: 13px; line-height: 1.65; white-space: pre-wrap; word-break: break-word; }
.aksrt-order-detail__after-sale-field { color: var(--detail-muted); }
.aksrt-order-detail__after-sale-foot { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 8px; }
-1
View File
@@ -1,3 +1,2 @@
@import url("profile/layout.css");
@import url("profile/lists.css");
@import url("profile/modals.css");
+2
View File
@@ -125,6 +125,8 @@
flex-shrink: 0;
width: 140px;
height: 140px;
/* 用户中心头图卡保持 legacy 圆角方形:显式声明圆角,避免被 public 捆绑里
同特异性(0,1,1)的全局头像原语 img.avatar 抢先。 */
border-radius: 6px;
}
+86 -231
View File
@@ -240,91 +240,93 @@
padding: 0;
display: flex;
flex-direction: column;
gap: 14px;
gap: 0;
}
@media (min-width: 992px) {
.profile-comment-list {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.profile-comment-item {
margin: 8px;
}
}
.profile-comment-item {
position: relative;
padding: 16px 18px;
border: 1px solid var(--color-line);
border-radius: var(--radius-lg);
background: var(--surface-1);
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
transition: background-color var(--dur-fast) var(--ease),
border-color var(--dur-fast) var(--ease),
box-shadow var(--dur-fast) var(--ease);
}
.profile-comment-item::before {
content: none;
}
.profile-comment-item:hover {
background: var(--surface-1);
border-color: var(--color-line-strong, var(--color-line));
box-shadow: 0 2px 6px rgba(15, 23, 42, 0.05);
}
.profile-comment-item__quote-link {
display: flex;
align-items: flex-start;
gap: 6px;
text-decoration: none;
color: var(--color-ink-2);
margin-bottom: 8px;
margin: 0;
padding: 20px 0;
border: 0;
border-radius: 0;
background: transparent;
box-shadow: none;
}
.profile-comment-item__quote-link:hover .profile-comment-item__quote-text {
color: var(--color-ink-2);
}
.profile-comment-item__quote-mark {
flex-shrink: 0;
font-family: var(--font-display);
font-size: 18px;
line-height: 1;
.profile-comment-item__time {
flex: 0 0 20%;
margin-right: 20px;
padding-top: 6px;
color: var(--color-ink-4);
opacity: 0.45;
margin-top: 2px;
text-align: right;
}
.profile-comment-item__quote-text {
flex: 1;
font-family: var(--font-body);
font-size: var(--text-sm, 0.875rem);
line-height: 1.65;
color: var(--color-ink-2);
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
.profile-comment-item__body {
flex: 0 1 calc(80% - 25px);
width: calc(80% - 25px);
min-width: 0;
overflow: hidden;
transition: color var(--dur-fast) var(--ease);
}
.profile-comment-item__meta {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 6px;
margin-left: 0;
font-size: 12px;
color: var(--color-ink-4);
.profile-comment-item__content-wrap {
margin-bottom: 10px;
}
.profile-comment-item__meta-item {
display: inline-flex;
align-items: center;
gap: 4px;
.profile-comment-item__content {
display: -webkit-box;
max-height: 7em;
overflow: hidden;
color: var(--color-ink-2);
line-height: 1.4em;
text-decoration: none;
white-space: normal;
-webkit-box-orient: vertical;
-webkit-line-clamp: 5;
}
.profile-comment-item__meta-item i {
font-size: 11px;
opacity: 0.75;
.profile-comment-item__content:hover {
color: var(--color-ink-2);
}
.profile-comment-item__meta-sep {
opacity: 0.5;
.profile-comment-item__pending {
margin-right: 6px;
}
.profile-comment-item__source {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.profile-comment-item__parent {
margin-right: 10px;
}
.profile-comment-item__post-link {
color: var(--color-ink-2);
font-weight: 500;
text-decoration: none;
transition: color var(--dur-fast) var(--ease);
}
.profile-comment-item__post-link:hover {
color: var(--color-ink);
.profile-comment-more {
margin: 20px 0;
text-align: center;
}
.profile-comment-more button {
padding: 5px 15px;
border: 0;
border-radius: 30px;
color: var(--color-ink-3);
background: transparent;
cursor: pointer;
opacity: .65;
}
.profile-comment-more button:hover {
background: var(--color-line);
opacity: 1;
}
.profile-comment-more button:disabled {
cursor: wait;
}
/* ==================== 分页 ==================== */
@@ -450,49 +452,15 @@
}
/* ==================== 账号设置区 ==================== */
.settings-section {
margin-bottom: 20px;
}
.settings-section__header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 14px;
}
.settings-icon {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border-radius: 8px;
background: var(--color-primary);
color: var(--color-on-primary, #fff);
font-size: 16px;
flex-shrink: 0;
}
.settings-icon i,
.security-action-icon i,
.aksrt-modal__icon i,
.aksrt-modal__close i {
line-height: 1;
}
.settings-hint {
font-size: 13px;
color: var(--color-ink-4);
margin: 4px 0 12px;
}
.settings-hint--top {
margin-top: 0;
margin-bottom: 10px;
line-height: 1.6;
}
.settings-hint--bottom {
margin-top: 8px;
}
.settings-section__title {
margin: 0;
border: 0;
}
.settings-empty {
font-size: 13px;
color: var(--color-ink-4);
@@ -500,22 +468,7 @@
text-align: center;
}
/* ---- 第三方绑定列表 ---- */
.binding-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.binding-item {
display: flex;
align-items: center;
gap: 10px;
padding: 11px 14px;
background: var(--surface-1);
border: 1px solid var(--color-line);
border-radius: 8px;
transition: border-color 0.15s, background 0.15s;
}
/* ---- 第三方绑定列表公共行为 ---- */
.binding-item--linkable {
cursor: pointer;
text-decoration: none;
@@ -525,25 +478,6 @@
border-color: var(--color-primary);
background: var(--color-primary-softer);
}
.binding-item__icon {
display: flex;
align-items: center;
width: 22px;
height: 22px;
color: var(--color-ink);
flex-shrink: 0;
}
.binding-item__icon i {
font-size: 22px;
line-height: 1;
}
.binding-item__meta {
display: flex;
flex: 1;
align-items: center;
gap: 8px;
min-width: 0;
}
.binding-status {
display: inline-flex;
align-items: center;
@@ -556,13 +490,9 @@
.binding-status--idle {
color: var(--color-ink-4);
}
.binding-item__name {
font-size: 14px;
font-weight: 600;
min-width: 0;
.badge__icon {
line-height: 1;
}
/* ---- 徽章 ---- */
.badge {
display: inline-flex;
align-items: center;
@@ -574,18 +504,6 @@
line-height: 1.5;
white-space: nowrap;
}
.badge__icon {
line-height: 1;
}
.badge--bound {
background: #ecfdf5;
color: #059669;
}
.badge--bind {
background: #eff6ff;
color: var(--color-primary);
border: 1px solid var(--color-primary-soft);
}
/* ---- 解绑按钮 ---- */
.binding-unbind-btn {
@@ -603,72 +521,17 @@
background: rgba(239, 68, 68, 0.08);
}
/* ---- 安全设置操作区 ---- */
.security-actions {
display: flex;
flex-direction: column;
gap: 12px;
}
.security-action-item {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 16px;
background: var(--surface-1);
border: 1px solid var(--color-line);
border-radius: 10px;
}
.security-action-info {
display: flex;
align-items: center;
gap: 12px;
flex: 1;
min-width: 0;
}
.security-action-icon {
display: flex;
align-items: center;
justify-content: center;
width: 38px;
height: 38px;
border-radius: 10px;
background: var(--color-primary-soft);
color: var(--color-primary);
flex-shrink: 0;
font-size: 18px;
}
.security-action-text {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
}
.security-action-title {
font-size: 14px;
font-weight: 600;
color: var(--color-ink);
}
.security-action-desc {
font-size: 12px;
color: var(--color-ink-4);
}
/* legacy-style user tabs: one panel outside, flat rows inside. */
.dashboard--public .profile-list,
.dashboard--public .profile-comment-list,
.uc-content #panel-posts .profile-list,
.uc-content #panel-favorites .profile-list,
.uc-content #panel-comments .profile-comment-list {
.uc-content #panel-favorites .profile-list {
gap: 0;
}
.dashboard--public .profile-list-item,
.dashboard--public .profile-comment-item,
.uc-content #panel-posts .profile-list-item,
.uc-content #panel-favorites .profile-list-item,
.uc-content #panel-favorites .profile-list[data-list="favorites"] .profile-list-item,
.uc-content #panel-comments .profile-comment-item {
.uc-content #panel-favorites .profile-list[data-list="favorites"] .profile-list-item {
padding: 16px 0;
background: transparent;
border: 0;
@@ -678,29 +541,23 @@
}
.dashboard--public .profile-list-item:hover,
.dashboard--public .profile-comment-item:hover,
.uc-content #panel-posts .profile-list-item:hover,
.uc-content #panel-favorites .profile-list-item:hover,
.uc-content #panel-favorites .profile-list[data-list="favorites"] .profile-list-item:hover,
.uc-content #panel-comments .profile-comment-item:hover {
.uc-content #panel-favorites .profile-list[data-list="favorites"] .profile-list-item:hover {
background: transparent;
border-color: var(--color-line-soft, rgba(0,0,0,0.06));
box-shadow: none;
}
.dashboard--public .profile-list-item:first-child,
.dashboard--public .profile-comment-item:first-child,
.uc-content #panel-posts .profile-list-item:first-child,
.uc-content #panel-favorites .profile-list-item:first-child,
.uc-content #panel-comments .profile-comment-item:first-child {
.uc-content #panel-favorites .profile-list-item:first-child {
padding-top: 0;
}
.dashboard--public .profile-list-item:last-child,
.dashboard--public .profile-comment-item:last-child,
.uc-content #panel-posts .profile-list-item:last-child,
.uc-content #panel-favorites .profile-list-item:last-child,
.uc-content #panel-comments .profile-comment-item:last-child {
.uc-content #panel-favorites .profile-list-item:last-child {
padding-bottom: 0;
border-bottom: 0;
}
@@ -779,26 +636,24 @@
@media (max-width: 640px) {
.dashboard--public .profile-list-item,
.dashboard--public .profile-comment-item,
.uc-content #panel-posts .profile-list-item,
.uc-content #panel-favorites .profile-list-item,
.uc-content #panel-comments .profile-comment-item {
.uc-content #panel-favorites .profile-list-item {
padding: 14px 0;
}
.dashboard--public .profile-list-item:first-child,
.dashboard--public .profile-comment-item:first-child,
.uc-content #panel-posts .profile-list-item:first-child,
.uc-content #panel-favorites .profile-list-item:first-child,
.uc-content #panel-comments .profile-comment-item:first-child {
.uc-content #panel-favorites .profile-list-item:first-child {
padding-top: 0;
}
.dashboard--public .profile-list-item:last-child,
.dashboard--public .profile-comment-item:last-child,
.uc-content #panel-posts .profile-list-item:last-child,
.uc-content #panel-favorites .profile-list-item:last-child,
.uc-content #panel-comments .profile-comment-item:last-child {
.uc-content #panel-favorites .profile-list-item:last-child {
padding-bottom: 0;
}
.profile-comment-item {
padding: 10px 0;
}
}
-251
View File
@@ -1,251 +0,0 @@
/* ==================== Modal 弹窗 ==================== */
.aksrt-modal {
display: none;
position: fixed;
inset: 0;
z-index: 99998;
align-items: center;
justify-content: center;
}
.aksrt-modal[hidden] {
display: none !important;
}
.aksrt-modal.is-open {
display: flex;
}
.aksrt-modal__overlay {
position: absolute;
inset: 0;
background: rgba(18, 18, 18, 0.48);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
animation: modalFadeIn 0.2s ease forwards;
}
.aksrt-modal__box {
position: relative;
width: 92%;
max-width: 440px;
max-height: 90vh;
overflow-y: auto;
background: var(--surface-1);
border: none;
border-radius: var(--radius-2xl, 16px);
box-shadow: var(--shadow-drawer);
animation: modalSlideUp 0.28s cubic-bezier(0.22, 1, 0.36, 1) forwards;
}
@keyframes modalFadeIn { from { opacity: 0 } to { opacity: 1 } }
@keyframes modalSlideUp {
from { opacity: 0; transform: translateY(24px) scale(0.96); }
to { opacity: 1; transform: translateY(0) scale(1); }
}
/* Modal 头部 */
.aksrt-modal__header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 18px 20px;
border-bottom: 1px solid var(--color-line);
}
.aksrt-modal__title-wrap {
display: flex;
align-items: center;
gap: 8px;
}
.aksrt-modal__icon {
display: flex;
align-items: center;
width: 30px;
height: 30px;
border-radius: 8px;
background: var(--color-primary-soft);
color: var(--color-primary);
font-size: 16px;
}
.aksrt-modal__title-wrap h4 {
margin: 0;
font-size: 16px;
font-weight: 700;
color: var(--color-ink);
}
.aksrt-modal__close {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border: none;
border-radius: 8px;
background: transparent;
color: var(--color-ink-4);
cursor: pointer;
font-size: 18px;
transition: background 0.15s, color 0.15s;
}
.aksrt-modal__close:hover {
background: var(--surface-2);
color: var(--color-ink);
}
/* Modal 表单字段 */
.aksrt-modal__box form {
padding: 20px;
}
.modal-field {
position: relative;
margin-bottom: 16px;
}
.modal-field label {
display: block;
margin-bottom: 6px;
font-size: 13px;
font-weight: 600;
color: var(--color-ink-2);
}
.modal-field input[type="password"],
.modal-field input[type="email"],
.modal-field input[type="tel"],
.modal-field input[type="text"] {
display: block;
width: 100%;
padding: 10px 40px 10px 14px;
font-size: 14px;
line-height: 1.5;
color: var(--color-ink);
background: var(--surface-4, var(--surface-2));
border: 1px solid var(--color-line);
border-radius: 8px;
outline: none;
transition: border-color 0.2s, box-shadow 0.2s;
box-sizing: border-box;
}
.modal-field input:focus {
border-color: var(--color-primary);
box-shadow: 0 0 0 3px var(--color-primary-soft);
background: var(--surface-1);
}
.toggle-pwd {
position: absolute;
right: 10px;
top: 31px;
width: 34px;
height: 34px;
border: none;
border-radius: 6px;
background: transparent;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
color: var(--color-ink-4);
font-size: 16px;
}
.toggle-pwd:hover {
color: var(--color-ink);
}
/* 只读显示(当前邮箱) */
.readonly-display {
display: block;
padding: 10px 14px;
font-size: 14px;
color: var(--color-ink-4);
background: var(--surface-2);
border: 1.5px solid var(--color-line);
border-radius: 8px;
word-break: break-all;
}
/* 验证码输入组 */
.code-input-group {
display: flex;
gap: 8px;
align-items: stretch;
}
.code-input-group input {
flex: 1;
min-width: 0;
padding: 10px 14px !important;
font-family: inherit !important;
letter-spacing: 2px;
font-weight: 600;
}
.code-send-btn {
flex-shrink: 0;
white-space: nowrap;
min-width: 110px;
}
.code-send-btn.is-counting {
pointer-events: none;
opacity: 0.65;
}
/* 密码强度条 */
.pwd-strength {
display: flex;
gap: 4px;
margin-top: 7px;
height: 4px;
border-radius: 2px;
overflow: hidden;
background: var(--surface-3);
}
.pwd-strength__fill {
width: 0%;
height: 100%;
border-radius: 2px;
transition: width 0.3s ease, background-color 0.3s ease;
background-color: var(--color-error);
}
.pwd-strength__fill.level-weak { width: 33%; background-color: var(--color-error); }
.pwd-strength__fill.level-medium { width: 66%; background-color: var(--color-warning); }
.pwd-strength__fill.level-strong { width: 100%; background-color: var(--color-success); }
.pwd-strength__text {
font-size: 11px;
margin-top: 4px;
font-weight: 500;
transition: color 0.2s;
}
.pwd-strength__text.level-weak { color: var(--color-error); }
.pwd-strength__text.level-medium { color: var(--color-warning); }
.pwd-strength__text.level-strong { color: var(--color-success); }
/* 人机验证容器在弹窗中的适配 */
.aksrt-modal .aksrt-captcha-wrap {
margin: 14px 0 8px;
}
.aksrt-modal .aksrt-captcha-box {
display: block !important;
}
/* Modal 底部按钮栏 */
.aksrt-modal__footer {
display: flex;
justify-content: flex-end;
gap: 10px;
margin-top: 20px;
padding-top: 16px;
border-top: 1px solid var(--color-line);
}
/* Modal mobile */
@media (max-width: 640px) {
.aksrt-modal__box {
width: 96%;
max-height: 92vh;
border-radius: 14px;
}
.aksrt-modal__footer {
flex-direction: column-reverse;
}
.aksrt-modal__footer .btn,
.aksrt-modal__footer button {
width: 100%;
}
.code-input-group {
flex-direction: column;
}
.code-send-btn {
width: 100%;
}
}
+41 -10
View File
@@ -190,6 +190,25 @@
display: flex;
}
/* 推广海报弹窗壳与上面两个同构(服务端静态输出 .modal.flex.jc.fade),
静止态同样必须显式隐藏,否则 .flex 工具类会使其铺满全屏形成隐形遮罩,
拦截整页点击。打开态由 initAffiliatePoster 的内联 display:flex 驱动,
关闭撤除内联样式后即回到隐藏规则。 */
.modal[data-aksrt-affiliate-poster-modal] {
display: none;
}
.modal[data-aksrt-affiliate-poster-modal].in {
display: flex;
}
/* 海报弹窗的圆形按钮必须保持圆形。上面的 .uc-content .but.jb-blue 是
(0,3,0) 且后加载,会盖掉 main.css 的 .poster-share .but.cir(0,2,0),
把蓝色下载钮从圆角 5em 压成 4px 方角;黄色关闭钮因为不在该规则内
才幸免。这里按 (0,3,0) 同权重回位参考主题的 pill 形态。 */
.modal[data-aksrt-affiliate-poster-modal] .but.cir {
padding: 0;
border-radius: 5em;
}
.balance-charge-form .zib-widget {
clear: both;
background-color: var(--main-bg-color);
@@ -304,12 +323,24 @@
body.uc-modal-open {
overflow: hidden;
}
.btn-block {
display: block;
width: 100%;
}
.but.jb-blue,
.but.b-blue-2 {
/* Scoped to the user-center content shell. These are the credits panel's own
* button skins, and every element they target renders inside .uc-content (the
* panels and their modals are all emitted through templates/parts/user-center/
* panel.php, which templates/pages/page-profile.php wraps in .uc-content).
*
* 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;
color: #fff;
border: 0;
@@ -317,18 +348,18 @@ body.uc-modal-open {
padding: 0.45em 1em;
cursor: pointer;
}
.but.c-blue-2.hollow,
.but.c-green.hollow {
.uc-content .but.c-blue-2.hollow,
.uc-content .but.c-green.hollow {
background: transparent;
border: 1px solid currentColor;
border-radius: 4px;
padding: 0.4em 0.9em;
cursor: pointer;
}
.but.c-blue-2 {
.uc-content .but.c-blue-2 {
color: #3b82f6;
}
.but.c-green {
.uc-content .but.c-green {
color: #16a34a;
}
@@ -110,13 +110,6 @@
gap: 4px 16px;
}
/* 待偿欠款:退款收回不足挂账,副信息里以描边标签弱提示。 */
.uc-content .uc-commission-card__debt {
padding: 0 8px;
border-radius: 999px;
background: rgba(255, 255, 255, .22);
}
/* 立即提现按钮:贴右居中,对齐参考主题 .abs-right。 */
.uc-content .uc-commission-card__action {
position: absolute;
@@ -214,9 +207,3 @@
.uc-content .uc-revenue-summary-row--explain { grid-template-columns: 1fr; }
.uc-content .uc-revenue-summary-row--explain .uc-reward-richtext { grid-column: 1; }
}
/* 推广海报预览弹窗:舞台居中,canvas 自适应弹窗宽度。 */
.uc-content .uc-affiliate-poster { display: grid; place-items: center; min-height: 120px; }
.uc-content .uc-affiliate-poster img,
.uc-content .uc-affiliate-poster canvas { display: block; max-width: 100%; height: auto; margin: 0 auto; border-radius: 12px; background: var(--surface-2, #f7f8fa); }
.uc-content .uc-affiliate-poster p { margin: 0; text-align: center; }
+510 -54
View File
@@ -144,52 +144,153 @@
min-width: 112px;
}
/* 设置区增强 */
.uc-content .settings-section {
margin-bottom: 24px;
/* 账号设置:按参考主题 account-set / box-body / oauth-bind-box 分区与排布。 */
.uc-content #panel-settings .aksrt-account-settings {
margin: -24px;
overflow: hidden;
border-radius: inherit;
}
.uc-content .settings-section__header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 16px;
.aksrt-account-settings__section {
padding: 15px;
border-bottom: 1px solid var(--color-line-soft, rgba(0, 0, 0, .06));
}
.uc-content .settings-icon {
width: 36px;
height: 36px;
border-radius: 10px;
.aksrt-account-settings__section:last-child { border-bottom: 0; }
.aksrt-account-settings__heading {
display: flex;
align-items: center;
justify-content: center;
background: var(--color-primary);
color: #fff;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
margin-bottom: 20px;
}
.aksrt-account-settings__heading .title-h-left {
margin: 0 0 8px;
font-size: 16px;
flex-shrink: 0;
}
.uc-content .settings-section__title {
.aksrt-account-settings__heading p {
margin: 0;
border: 0;
font-size: 16px;
color: var(--muted-2-color, var(--color-ink-4, #999));
font-size: 14px;
line-height: 1.6;
}
.uc-content .settings-section__title::before {
display: none;
.aksrt-account-list {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px;
}
/* 安全操作项增强 */
.uc-content .security-action-item {
border: 1px solid var(--color-line-soft, rgba(0,0,0,0.06));
border-radius: 10px;
padding: 16px;
transition: border-color 0.2s, box-shadow 0.2s;
.aksrt-account-row {
display: grid;
grid-template-columns: minmax(116px, .85fr) minmax(0, 1fr) auto;
align-items: center;
gap: 12px;
min-width: 0;
padding: 12px 14px;
border-radius: var(--main-radius, 8px);
background: var(--muted-border-color, var(--surface-2, #f5f6f7));
color: inherit;
}
.uc-content .security-action-item:hover {
border-color: var(--color-primary-soft);
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
.aksrt-account-row__identity {
display: flex;
align-items: center;
gap: 6px;
min-width: 0;
}
.aksrt-account-row__icon {
display: grid;
place-items: center;
width: 26px;
height: 26px;
flex: 0 0 26px;
border-radius: 50%;
background: var(--this-bg, rgba(41, 151, 247, .12));
color: var(--this-color, #2997f7);
}
.aksrt-account-row__icon--blue { --this-bg: rgba(41, 151, 247, .12); --this-color: #2997f7; }
.aksrt-account-row__icon--blue-2 { --this-bg: rgba(73, 174, 231, .12); --this-color: #49aee7; }
.aksrt-account-row__icon--purple { --this-bg: rgba(136, 84, 230, .12); --this-color: #8854e6; }
.aksrt-account-row__copy {
display: block;
min-width: 0;
overflow: hidden;
color: var(--color-ink, #333);
font-size: 14px;
font-weight: 400;
text-overflow: ellipsis;
white-space: nowrap;
}
.aksrt-account-row__social-name {
overflow: hidden;
color: var(--color-ink, #333);
font-size: 13px;
font-weight: 400;
text-overflow: ellipsis;
white-space: nowrap;
}
.aksrt-account-row__value {
overflow: hidden;
min-width: 0;
color: var(--muted-2-color, var(--color-ink-3, #777));
font-size: 12px;
text-align: right;
text-overflow: ellipsis;
white-space: nowrap;
}
.aksrt-account-row__action {
flex: 0 0 auto;
margin: 0;
padding: 4px 10px;
border-radius: 999px;
font-size: 12px;
line-height: 1.5;
white-space: nowrap;
}
.aksrt-account-row--simple { grid-template-columns: minmax(0, 1fr) auto; }
.aksrt-account-row__action--unavailable { color: var(--muted-2-color, #999); }
.aksrt-account-settings__section--social .binding-captcha { margin-bottom: 12px; }
.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 .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-unbind-btn { color: var(--this-color, #e4a11b); }
.aksrt-account-list--social .binding-item--linkable:hover { background: var(--color-primary-softer, var(--color-primary-soft, rgba(41, 151, 247, .08))); }
@media (max-width: 640px) {
.aksrt-account-list { grid-template-columns: minmax(0, 1fr); }
}
@media (max-width: 767px) {
.uc-content #panel-settings .aksrt-account-settings { margin: -16px; }
}
@media (max-width: 520px) {
.uc-content #panel-settings .aksrt-account-settings { margin: -16px; }
.aksrt-account-settings__section { padding: 15px; }
.aksrt-account-row { grid-template-columns: minmax(0, 1fr) auto; gap: 8px 10px; padding: 12px; }
.aksrt-account-row__identity { grid-column: 1; }
.aksrt-account-row__value { grid-column: 1; grid-row: 2; padding-left: 38px; text-align: left; }
.aksrt-account-row__action { grid-column: 2; grid-row: 1 / span 2; }
.aksrt-account-list--social .aksrt-account-row { grid-template-columns: 24px minmax(0, 1fr) auto; gap: 6px 8px; }
.aksrt-account-list--social .binding-status { grid-column: 2; grid-row: 2; }
.aksrt-account-list--social .aksrt-account-row__action { grid-column: 3; grid-row: 1 / span 2; }
}
@media (max-width: 480px) {
.uc-content #panel-settings .aksrt-account-settings { margin: -14px; }
}
/* 邀请区增强 */
@@ -502,6 +603,186 @@
margin-top: 4px;
}
/* 账号设置弹窗:使用 AksrtModalRuntime,几何和标题区对齐参考 modal-mini。 */
.aksrt-modal-runtime.aksrt-user-settings-modal:not(.aksrt-modal-runtime--bottom) .aksrt-modal-runtime__dialog {
width: 400px;
max-width: calc(100vw - 20px);
max-height: calc(100vh - 20px);
max-height: calc(100dvh - 20px);
overflow: auto;
border-radius: var(--main-radius, 8px);
box-shadow: 0 10px 30px rgba(0, 0, 0, .15);
}
.aksrt-modal-runtime.aksrt-user-settings-modal.aksrt-modal-runtime--bottom .aksrt-modal-runtime__dialog {
width: 400px;
max-width: calc(100vw - 20px);
max-height: calc(100dvh - 20px - env(safe-area-inset-bottom));
margin: 0 auto calc(10px + env(safe-area-inset-bottom));
border-radius: var(--main-radius, 8px) var(--main-radius, 8px) 0 0;
box-shadow: 0 -10px 50px rgba(0, 0, 0, .15);
}
.aksrt-user-settings-modal .aksrt-modal-runtime__content { padding: 0; }
.aksrt-modal-runtime--bottom.aksrt-user-settings-modal .aksrt-settings-modal {
padding-bottom: env(safe-area-inset-bottom);
border-radius: var(--main-radius, 8px) var(--main-radius, 8px) 0 0;
}
.aksrt-settings-modal {
position: relative;
overflow: hidden;
border-radius: var(--main-radius, 8px);
background: var(--main-bg-color, var(--surface-1, #fff));
}
.aksrt-settings-modal [hidden] { display: none !important; }
.aksrt-settings-modal .modal-colorful-header {
position: absolute;
top: 0;
right: 0;
left: 0;
z-index: 1;
height: 120px;
border-radius: var(--main-radius, 8px) var(--main-radius, 8px) 0 0;
}
.aksrt-settings-modal .modal-colorful-header > .close {
position: absolute;
top: 10px;
right: 10px;
z-index: 9;
width: 30px;
height: 30px;
padding: 0;
border: 0;
background: transparent;
color: var(--this-color);
font-size: 18px;
line-height: 30px;
opacity: .6;
cursor: pointer;
}
.aksrt-settings-modal > form {
position: relative;
z-index: 0;
margin-top: 120px;
padding: 15px;
}
.aksrt-settings-modal__pane {
position: relative;
z-index: 0;
margin-top: 120px;
padding: 15px;
}
.aksrt-settings-modal__pane form,
.aksrt-settings-modal .account-step form {
margin: 0;
padding: 0;
}
.aksrt-settings-modal .account-step-list {
position: relative;
z-index: 0;
margin-top: 120px;
padding: 10px;
}
.aksrt-settings-modal .account-step-panels { position: relative; z-index: 0; }
.aksrt-settings-modal .account-step { display: none; padding: 0 15px 18px; }
.aksrt-settings-modal .account-step.is-active { display: block; }
.aksrt-settings-modal .account-step > p { margin: 0 0 20px; }
.aksrt-settings-modal .account-recovery-toggle { border: 0; background: transparent; cursor: pointer; }
.aksrt-settings-modal__fields { padding: 0; }
.aksrt-settings-modal .modal-line-field--code .line-form-input { padding-right: 7em; }
.aksrt-settings-modal .modal-line-field--code .yztx.abs-right {
top: 50%;
right: .7em;
z-index: 2;
margin-top: 3px;
transform: translateY(-50%);
}
.aksrt-settings-modal .modal-line-field--code .code-send-btn { display: inline-block; margin: 0; }
.aksrt-settings-modal .toggle-pwd { position: absolute; right: 7px; bottom: 4px; width: 30px; height: 30px; padding: 0; border: 0; background: transparent; color: var(--muted-2-color, #999); cursor: pointer; }
.aksrt-settings-modal .toggle-pwd:hover { color: var(--color-ink, #333); }
.aksrt-settings-modal .code-send-btn { white-space: nowrap; }
.aksrt-settings-modal .code-send-btn.is-counting { pointer-events: none; opacity: .6; }
.aksrt-settings-modal .aksrt-captcha-wrap { margin: 12px 0 8px; }
/* 修改手机:脱敏中间位数字框,与前缀/后缀号码一体呈现 */
.aksrt-settings-modal .aksrt-phone-digits {
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
margin-bottom: 12px;
}
.aksrt-settings-modal .aksrt-phone-digit__edge {
padding: 0 2px;
color: var(--color-ink, #333);
font-size: 22px;
font-weight: 600;
line-height: 1;
letter-spacing: 1px;
white-space: nowrap;
}
.aksrt-settings-modal .aksrt-phone-digit {
width: 44px;
height: 50px;
padding: 0;
border: 1.5px solid var(--muted-3-color, #c9d1d9);
border-radius: 10px;
background-color: var(--main-bg-color, #f7f8fa);
color: var(--color-ink, #333);
font-size: 22px;
font-weight: 600;
line-height: 1;
text-align: center;
caret-color: var(--color-primary, #2a7de1);
font-variant-numeric: tabular-nums;
outline: none;
transition: border-color .15s ease, background-color .15s ease, box-shadow .15s ease;
}
.aksrt-settings-modal .aksrt-phone-digit:focus {
border-color: var(--color-primary, #2a7de1);
background-color: var(--main-bg-color, #fff);
box-shadow: 0 0 0 3px rgba(42, 125, 225, 0.18);
}
.aksrt-settings-modal .aksrt-phone-digits.has-error .aksrt-phone-digit {
border-color: var(--color-danger, #e02020);
background-color: rgba(224, 32, 32, 0.06);
box-shadow: 0 0 0 3px rgba(224, 32, 32, 0.12);
}
.aksrt-settings-modal .aksrt-phone-digits__error {
margin: 0 0 12px;
color: var(--color-danger, #e02020);
font-size: 13px;
text-align: center;
}
@media (max-width: 640px) {
.aksrt-modal-runtime.aksrt-user-settings-modal.aksrt-modal-runtime--bottom .aksrt-modal-runtime__dialog {
width: 100%;
max-width: 100%;
max-height: calc(100dvh - env(safe-area-inset-bottom));
margin: 0 0 -1px;
border-radius: 16px 16px 0 0;
}
.aksrt-modal-runtime--bottom.aksrt-user-settings-modal .aksrt-settings-modal {
border-radius: 16px 16px 0 0;
}
}
/* ==================== 官方认证面板 ==================== */
/* 面板内容为 Zibll 同款 colorful-bg hero 卡片(参考主题 1:1),去掉外层白卡壳 */
@@ -519,29 +800,204 @@
margin: 20px 0 0;
}
.uc-auth-modal__dialog { width: min(620px, calc(100vw - 28px)); max-width: 620px; max-height: min(760px, calc(100vh - 40px)); overflow: auto; background: var(--surface-1, #fff); }
.aksrt-identity-modal-shell .aksrt-modal-runtime__dialog { width: min(620px, calc(100vw - 28px)); max-width: 620px; }
.aksrt-identity-modal-shell .uc-auth-modal__dialog { width: auto; max-width: none; max-height: none; overflow: visible; background: transparent; }
.uc-auth-modal__header > div { display: flex; align-items: center; gap: 10px; }
.uc-auth-modal__header > div > span { display: grid; place-items: center; width: 34px; height: 34px; border-radius: 11px; background: var(--color-primary-soft); color: var(--color-primary); }
.uc-auth-modal__body { padding: 22px; }
.uc-auth-form { display: grid; gap: 17px; }
.uc-auth-field { display: grid; gap: 7px; color: var(--color-ink-2); font-size: 13px; font-weight: 600; }
.uc-auth-enterprise-fields { display: grid; gap: 17px; }
.uc-auth-enterprise-fields[hidden] { display: none; }
.uc-auth-form .form-control { width: 100%; min-height: 42px; padding: 9px 11px; border: 1px solid var(--color-line-soft, rgba(15, 23, 42, 0.1)); border-radius: 9px; background: var(--surface-1); color: var(--color-ink); font: inherit; font-weight: 400; box-sizing: border-box; }
.aksrt-identity-modal-shell .aksrt-modal-runtime__dialog {
width: min(600px, calc(100vw - 20px));
max-width: 600px;
max-height: calc(100vh - 20px);
max-height: calc(100dvh - 20px);
overflow: auto;
border-radius: var(--main-radius, 8px);
background: var(--main-bg-color, var(--surface-1, #fff));
}
.aksrt-identity-modal-shell .aksrt-modal-runtime__content { padding: 0; }
.uc-auth-modal {
position: relative;
overflow: hidden;
padding: 1px;
border-radius: var(--main-radius, 8px);
background: var(--main-bg-color, var(--surface-1, #fff));
}
.uc-auth-modal__header,
.uc-auth-modal__header.colorful-bg {
position: absolute;
top: 1px;
right: 1px;
left: 1px;
display: flex;
align-items: center;
justify-content: center;
height: 120px;
overflow: hidden;
border-radius: var(--main-radius, 8px) var(--main-radius, 8px) 0 0;
background: var(--this-bg);
background-size: 120%;
color: var(--this-color);
}
.uc-auth-modal__header > .close {
position: absolute;
top: 10px;
right: 10px;
z-index: 9;
width: 30px;
height: 30px;
padding: 0;
border: 0;
background: transparent;
color: var(--this-color);
font-size: 18px;
line-height: 1;
opacity: .6;
cursor: pointer;
}
.uc-auth-modal__header > .close:hover { opacity: 1; }
.uc-auth-modal__header .colorful-make,
.uc-auth-modal__header .colorful-make::before,
.uc-auth-modal__header .colorful-make::after {
position: absolute;
border-radius: 100%;
background: var(--this-bg);
color: var(--this-color);
content: '';
}
.uc-auth-modal__header .colorful-make {
top: 60%;
bottom: 0;
width: 323px;
height: 290px;
animation: uc-auth-colorful 120s linear infinite;
}
.uc-auth-modal__header .colorful-make::before {
top: -325px;
right: -558px;
bottom: 0;
width: 677px;
height: 300px;
transform: rotate(341deg);
opacity: .6;
}
.uc-auth-modal__header .colorful-make::after {
top: -170px;
bottom: 0;
left: -262px;
width: 402px;
height: 214px;
transform: rotate(326deg);
opacity: .4;
}
.uc-auth-modal__header .colorful-make + * { position: relative; }
.uc-auth-modal__body {
position: relative;
margin-top: 120px;
padding: 15px;
background: var(--main-bg-color, var(--surface-1, #fff));
}
.uc-auth-form { margin: 0; }
.uc-auth-form__scroll { max-height: 50vh; max-height: 50dvh; }
/* 申请介绍的列表:标记随列表项内容一起绘制(悬挂缩进),
不会被外层滚动区的左边界裁掉一半。ul/ol/li 都清原生标记,
因为后台可能直接写裸 li(没有 ul 包裹)。 */
.uc-auth-intro ul,
.uc-auth-intro ol,
.uc-auth-intro li {
list-style: none !important;
}
.uc-auth-intro ul,
.uc-auth-intro ol {
margin: .6em 0 1em;
padding-left: 0;
}
.uc-auth-intro li {
margin-bottom: .35em;
padding-left: 1.25em;
text-indent: -1.25em;
}
.uc-auth-intro li::before {
content: "\2022";
display: inline-block;
width: 1.25em;
text-indent: 0;
}
.uc-auth-intro ol { counter-reset: uc-auth-intro; }
.uc-auth-intro ol li::before {
content: counter(uc-auth-intro) ".";
counter-increment: uc-auth-intro;
width: auto;
min-width: 1.25em;
}
.uc-auth-intro li > ul,
.uc-auth-intro li > ol { margin: .35em 0 0 1.25em; }
.uc-auth-field { display: block; color: var(--muted-2-color); font-size: .9em; font-weight: 400; }
.uc-auth-field__label { display: block; margin-bottom: 6px; }
.uc-auth-form .form-control { width: 100%; box-sizing: border-box; }
.uc-auth-form textarea.form-control { resize: vertical; }
.uc-auth-consent { display: flex; align-items: flex-start; gap: 8px; color: var(--color-ink-3); font-size: 13px; font-weight: 400; line-height: 1.6; }
.uc-auth-consent input { flex: 0 0 auto; margin-top: 3px; }
.uc-auth-submit { display: inline-flex; align-items: center; justify-content: center; gap: 7px; min-height: 44px; }
.uc-auth-form__message { min-height: 18px; color: var(--color-error, #dc2626); font-size: 12px; text-align: center; }
.uc-auth-pending { text-align: center; padding: 4px 0 18px; }
.uc-auth-pending > span { display: grid; place-items: center; width: 58px; height: 58px; margin: 0 auto 12px; border-radius: 18px; background: rgba(245, 158, 11, 0.14); color: #a66e0b; font-size: 24px; }
.uc-auth-pending h3 { margin: 0 0 7px; }
.uc-auth-pending p { margin: 0; color: var(--color-ink-4); font-size: 13px; }
.uc-auth-upload .preview .add {
appearance: none;
padding: 0;
border: 0;
cursor: pointer;
}
.uc-auth-upload .preview-item { overflow: hidden; }
.uc-auth-upload .preview-item img { object-fit: contain; }
.uc-auth-preview-remove {
position: absolute;
top: 5px;
right: 5px;
display: grid;
place-items: center;
width: 24px;
height: 24px;
padding: 0;
border: 0;
border-radius: 50%;
background: #fd2c53;
color: #fff;
font-size: 20px;
line-height: 1;
cursor: pointer;
}
.aksrt-identity-modal-shell .uc-auth-modal .uc-auth-modal__body .uc-auth-modal__footer {
display: flex;
grid-template-columns: none;
gap: 0;
margin: 0 -15px -15px;
overflow: hidden;
border-top: 1px solid var(--main-border-color, var(--color-line, rgba(50, 50, 50, .12)));
border-radius: 0 0 var(--main-radius, 8px) var(--main-radius, 8px);
}
.aksrt-identity-modal-shell .uc-auth-modal .uc-auth-modal__body .uc-auth-modal__footer > .but {
display: flex;
flex: 1 1 50%;
align-items: center;
justify-content: center;
min-height: 48px;
margin: 0 !important;
padding: .8em;
border: 0;
border-radius: 0;
background: transparent;
color: var(--muted-color, #888);
font: inherit;
font-weight: 400;
cursor: pointer;
}
.aksrt-identity-modal-shell .uc-auth-modal .uc-auth-modal__body .uc-auth-modal__footer > .but + .but {
border-left: 1px solid var(--main-border-color, var(--color-line, rgba(50, 50, 50, .12)));
}
.aksrt-identity-modal-shell .uc-auth-modal .uc-auth-modal__body .uc-auth-modal__footer > .but.c-blue {
background: transparent;
color: var(--this-color, #2997f7);
}
.aksrt-identity-modal-shell .uc-auth-modal .uc-auth-modal__body .uc-auth-modal__footer > .but:hover {
background: var(--this-bg, rgba(136, 136, 136, .1));
}
.aksrt-identity-modal-shell .uc-auth-modal .uc-auth-modal__body .uc-auth-modal__footer > .but:disabled { opacity: .5; cursor: wait; }
@keyframes uc-auth-colorful { to { transform: rotate(360deg); } }
@media (max-width: 640px) {
.uc-auth-modal__dialog { position: fixed; inset: auto 0 0; width: 100%; max-width: none; max-height: 88vh; margin: 0; border-radius: 18px 18px 0 0; }
@media (max-width: 768px) {
.aksrt-identity-modal-shell.aksrt-modal-runtime--bottom .aksrt-modal-runtime__dialog {
width: calc(100% - 20px);
max-width: none;
max-height: calc(100dvh - 30px - env(safe-area-inset-bottom));
margin: 0 10px calc(10px + env(safe-area-inset-bottom));
}
}
+19 -66
View File
@@ -622,25 +622,6 @@ body.aksrt-user-center .uc-content,
}
}
.uc-service-grid__icon small {
position: absolute;
top: -5px;
right: -7px;
display: grid;
place-items: center;
min-width: 17px;
height: 17px;
padding: 0 4px;
border: 2px solid var(--surface-1);
border-radius: 99px;
background: var(--color-error, #ef4444);
color: #fff;
font-size: 9px;
font-style: normal;
font-weight: 700;
line-height: 1;
}
.uc-sidebar__pair {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
@@ -684,7 +665,8 @@ body.aksrt-user-center .uc-content,
text-overflow: ellipsis;
}
.uc-summary-card__body > i {
.uc-summary-card__body > i,
.uc-summary-card__body > svg {
font-size: 18px;
}
@@ -716,14 +698,26 @@ body.aksrt-user-center .uc-content,
.uc-center-card strong { font-size: 13px; }
.uc-center-card small { margin-top: 3px; color: var(--color-ink-4); font-size: 10px; }
.uc-center-card > i { color: var(--color-ink-4); font-size: 11px; }
.uc-center-card > i,
.uc-center-card > svg { color: var(--color-ink-4); font-size: 11px; }
.uc-center-card--creation .uc-center-card__icon { color: #6650c7; background: rgba(124, 92, 220, 0.12); }
.uc-center-card--revenue .uc-center-card__icon { color: #ce6b35; background: rgba(234, 112, 55, 0.12); }
.uc-service-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 8px;
/* Services / settings grids, ported from the reference theme sidebar:
four per row, no tinted tile behind the icon. */
.user-icon-but-box > a {
width: calc(25% - 10px);
margin: 5px;
min-width: 50px;
max-width: 100px;
color: var(--color-ink-3);
text-align: center;
text-decoration: none;
}
.user-icon-but-box > a:hover,
.user-icon-but-box > a.is-active {
color: var(--color-primary);
}
.uc-follow-management__list { display: grid; gap: 10px; margin-top: 16px; }
@@ -735,44 +729,3 @@ body.aksrt-user-center .uc-content,
.uc-follow-management__actions { display: flex; flex-wrap: wrap; gap: 8px; margin-left: auto; }
@media (max-width: 560px) { .uc-follow-management__item { align-items: flex-start; flex-wrap: wrap; } .uc-follow-management__actions { width: 100%; margin-left: 60px; } }
.uc-service-grid__item {
display: flex;
flex-direction: column;
align-items: center;
gap: 7px;
min-width: 0;
padding: 4px 2px;
color: var(--color-ink-3);
font-size: 11px;
text-align: center;
text-decoration: none;
}
.uc-service-grid__icon {
position: relative;
display: grid;
place-items: center;
width: 38px;
height: 38px;
border-radius: 14px;
background: var(--surface-2);
font-size: 16px;
}
.uc-service-grid__item--cart .uc-service-grid__icon { color: #dd6941; background: rgba(238, 109, 61, 0.12); }
.uc-service-grid__item--level .uc-service-grid__icon { color: #9b6b0a; background: rgba(234, 179, 8, 0.13); }
.uc-service-grid__item--revenue .uc-service-grid__icon { color: #b8377d; background: rgba(219, 39, 119, 0.12); }
.uc-service-grid__item--auth .uc-service-grid__icon { color: #287bc4; background: rgba(14, 165, 233, 0.12); }
.uc-service-grid__item--orders .uc-service-grid__icon { color: #c05621; background: rgba(234, 88, 12, 0.12); }
.uc-service-grid__item--notice .uc-service-grid__icon { color: #d04b65; background: rgba(244, 63, 94, 0.11); }
.uc-service-grid__item--profile .uc-service-grid__icon { color: #6550c7; background: rgba(124, 92, 220, 0.12); }
.uc-service-grid__item--security .uc-service-grid__icon { color: #16816c; background: rgba(16, 185, 129, 0.12); }
.uc-service-grid__item:hover,
.uc-service-grid__item.is-active {
color: var(--color-primary);
}
.uc-service-grid__item.is-active .uc-service-grid__icon {
box-shadow: 0 0 0 2px var(--color-primary-soft), 0 5px 14px rgba(15, 23, 42, 0.08);
}
+127 -2
View File
@@ -88,6 +88,8 @@
.aksrt-author-hero__avatar img {
width: 120px;
height: 120px;
/* 公开主页 hero 头像保持 legacy 圆角方形:显式声明圆角,避免被 public 捆绑里
同特异性(0,1,1)的全局头像原语 img.avatar 抢先。 */
border-radius: var(--radius);
border: 4px solid var(--surface-1);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
@@ -390,6 +392,34 @@
width: auto; /* 覆盖 vendored site-layout-1 的 width: calc(50% - 16px),让卡片填满网格单元 */
margin: 0 !important; /* 抵消 vendored .posts-row > * 的 8px !important 边距 */
}
/* 骨架是网格容器自己的直接子节点:不显式跨列,它就会被当成一张卡片,
只分到一半宽度,整摞骨架全挤在单列里(翻页 / 切 tab 的占位因此变形)。
跨满全部列,并在加载态把 loader 自身变成与真实卡片同款的双列网格,
骨架行才会与卡片同宽、同轴、同列数。display 只随 is-loading 打开,
未加载时容器仍是 display:none,不会被提前撑开。 */
.aksrt-user-home .aksrt-user-home__post-list[data-view="list"] > .post_ajax_loader {
grid-column: 1 / -1;
}
.aksrt-user-home .dashboard-panel.is-loading .aksrt-user-home__post-list[data-view="list"] > .post_ajax_loader {
display: grid;
gap: 16px;
grid-template-columns: repeat(2, minmax(0, 1fr));
align-items: start;
}
/* 骨架跟随网格走:容器 margin 已归零、卡片边距已清零,
不能再叠加 .posts-row > .post_ajax_loader 内层的 8px 补偿;
宽高沿用真实卡片的网格单元,所以 vendored site-layout-1 的
calc(50% - 16px) 与剩余边距也要一并清零。 */
.aksrt-user-home .aksrt-user-home__post-list[data-view="list"] > .post_ajax_loader .posts-item {
margin-right: 0;
margin-left: 0;
margin-top: 0;
margin-bottom: 0;
width: auto;
}
}
@media (max-width: 767px) {
@@ -488,6 +518,53 @@
text-align: center;
}
.aksrt-user-home .dashboard-panel .uh-follow__list--placeholder {
display: none;
}
.aksrt-user-home .dashboard-panel.is-loading .uh-follow__list--placeholder {
display: block;
}
.aksrt-user-home .dashboard-panel.is-loading .uh-follow__subtabs,
.aksrt-user-home .dashboard-panel.is-loading .uh-follow__list:not(.uh-follow__list--placeholder),
.aksrt-user-home .dashboard-panel.is-loading .uh-pagination,
.aksrt-user-home .dashboard-panel.is-loading .profile-empty {
display: none;
}
.aksrt-user-home .dashboard-panel.is-load-error .uh-follow__list--placeholder {
display: block;
}
.aksrt-user-home .uh-panel-load-error {
display: none;
margin: 12px 0;
color: var(--color-ink-4);
font-size: 13px;
text-align: center;
grid-column: 1 / -1;
}
.aksrt-user-home .dashboard-panel.is-load-error .uh-panel-load-error {
display: block;
}
.aksrt-user-home .uh-panel-load-error button {
margin-left: 8px;
padding: 0;
border: 0;
color: var(--color-primary);
background: transparent;
font: inherit;
cursor: pointer;
}
.aksrt-user-home .uh-follow__placeholder {
display: inline-flex;
margin: 5px 8px;
}
.aksrt-user-home .uh-follow__pagination {
margin-top: 16px;
text-align: center;
@@ -548,7 +625,55 @@
}
.aksrt-user-home .dashboard-panel.is-loading {
opacity: .6;
transition: opacity var(--dur-fast, .15s) var(--ease, ease);
pointer-events: none;
}
/* 面板翻页的加载态由列表骨架承担(与文章列表 .post_ajax_loader 同一契约)。 */
.aksrt-user-home .dashboard-panel .post_ajax_loader {
display: none;
}
/* 加载期收起旧页内容:翻页 / 切 tab 是整页替换语义(author.js 最终就是
panel.innerHTML = html),骨架必须独占内容区,而不是追加在旧卡片下方 ——
否则先看到「第 1 页卡片 + 脚下一摞骨架」,等接口返回才换页。
对齐参考主题 post_ajax() 的 beforeSend:replace 时 $item.remove() +
find(pag).remove()。收 display 而非删节点,仍守住「不写 inline display」契约。
只收 .aksrt-user-home__post-list 的**直接子级** .posts-item:骨架内层同样带
.posts-item,写成后代选择器会连骨架自己一起隐藏掉。 */
.aksrt-user-home .dashboard-panel.is-loading .aksrt-user-home__post-list > .posts-item {
display: none;
}
/* 旧翻页控件同属被替换的旧页内容,且高亮的是旧页码,加载期一并收起。
用 :has() 限定「这个面板真有骨架」:关注 / 评论面板没有 .post_ajax_loader,
无条件隐藏会让它们加载时只剩一片空白。 */
.aksrt-user-home .dashboard-panel.is-loading:has(.post_ajax_loader) .profile-pagination {
display: none;
}
.aksrt-user-home .dashboard-panel.is-loading .post_ajax_loader {
display: block;
animation: aksrtFadeIn 0.2s ease both;
}
.aksrt-user-home .dashboard-panel.is-load-error .post_ajax_loader {
display: block;
}
.aksrt-user-home .dashboard-panel.is-load-error .aksrt-user-home__post-list[data-view="list"] > .post_ajax_loader {
display: grid;
gap: 16px;
grid-template-columns: repeat(2, minmax(0, 1fr));
align-items: start;
}
.aksrt-user-home .dashboard-panel.is-load-error .placeholder {
animation: none;
}
@media (prefers-reduced-motion: reduce) {
.aksrt-user-home .dashboard-panel .post_ajax_loader .placeholder {
animation: none;
}
}
+72
View File
@@ -0,0 +1,72 @@
/**
* Post list quick-edit dependency toggling.
*
* The pay quick-edit box hides fields whose dependency rules no longer match
* the selected 付费模式 / 支付类型, mirroring the reference theme's field
* visibility. Runs on the WP inline-edit row when it is opened or changed.
*/
(function () {
'use strict';
function readControl(row, fieldId) {
var input = row.querySelector(
'[name="aksrt_bulk_edit[pay][' + fieldId + ']"],' +
' [name="aksrt_bulk_edit[pay][' + fieldId + '][operation]"]'
);
if (!input) return null;
if (input.tagName === 'SELECT') return input.value;
var checked = row.querySelector(
'[name="aksrt_bulk_edit[pay][' + fieldId + ']"]:checked'
);
if (input.type === 'radio' && checked) return checked.value;
return input.value;
}
function compare(actual, operator, expected) {
if (actual === null) return true;
if (operator === '==') return actual === expected;
if (operator === '!=') return actual !== expected;
return true;
}
function updateRow(row) {
var boxes = row.querySelectorAll('.aksrt-bulk-edit-box[data-dependency]');
Array.prototype.forEach.call(boxes, function (box) {
var rules;
try {
rules = JSON.parse(box.getAttribute('data-dependency'));
} catch (e) {
return;
}
if (!Array.isArray(rules)) return;
var visible = rules.every(function (rule) {
return compare(readControl(row, rule[0]), rule[1], String(rule[2]));
});
box.style.display = visible ? '' : 'none';
});
}
function syncRows() {
var rows = document.querySelectorAll('.inline-edit-row, #bulk-edit-row');
Array.prototype.forEach.call(rows, function (row) {
if (row.querySelector('[data-dependency]')) updateRow(row);
});
}
document.addEventListener('change', function (event) {
var target = event.target;
if (!target || !target.name || target.name.indexOf('aksrt_bulk_edit[pay]') !== 0) return;
var row = target.closest('.inline-edit-row, #bulk-edit-row');
if (row) updateRow(row);
});
// WP swaps the inline-edit row without a change event.
document.addEventListener('click', function (event) {
var target = event.target;
if (!target || !target.closest) return;
if (target.closest('.editinline, .bulk-action')) window.setTimeout(syncRows, 50);
});
syncRows();
})();
+1 -1
View File
@@ -93,7 +93,7 @@
var url = new URL(window.location.href);
var action = url.searchParams.get('oauth_action');
if (['login', 'bind', 'error'].indexOf(action) === -1) return;
var userCenterOwnsBind = action === 'bind' && /\/user(?:\/settings)?\/?$/.test(url.pathname) && document.querySelector('[data-identity-root], .settings-section');
var userCenterOwnsBind = action === 'bind' && /\/user(?:\/settings)?\/?$/.test(url.pathname) && document.querySelector('[data-identity-root], [data-aksrt-settings-root]');
if (userCenterOwnsBind) return;
document.documentElement.dataset.aksrtOauthResultHandled = 'true';
+83 -7
View File
@@ -60,15 +60,44 @@
});
}
function loadPanelBody(panel, name, page, sub) {
function appendCommentPage(panel, html) {
var fragment = document.createElement('div');
fragment.innerHTML = html;
var currentList = panel.querySelector('.profile-comment-list');
var incomingList = fragment.querySelector('.profile-comment-list');
if (currentList && incomingList) {
Array.prototype.slice.call(incomingList.children).forEach(function (item) {
currentList.appendChild(item);
});
}
var currentMore = panel.querySelector('.profile-comment-more');
var incomingMore = fragment.querySelector('.profile-comment-more');
if (currentMore) currentMore.remove();
if (incomingMore && currentList) currentList.insertAdjacentElement('afterend', incomingMore);
}
function loadPanelBody(panel, name, page, sub, appendComments, trigger) {
var cfg = window.aksrtData || {};
var restBase = cfg.apiUrl || cfg.restUrl || '';
if (!userId || !restBase) return Promise.reject(new Error('rest-unavailable'));
if (panel._aksrtUhRequest) return panel._aksrtUhRequest;
var followSub = sub || panel.dataset.uhSub || 'followers';
panel.dataset.uhPage = String(page);
panel.dataset.uhSub = followSub;
var followSub = sub || 'followers';
// uhPage / uhSub 是**粉丝面板**的私有状态(其它面板没有子页签)。
// 写满所有面板会互相污染:文章面板上的 uhSub 残留会让粉丝翻页读到
// 别人的分组,uhPage 也会让旧页码假活跃。非粉丝面板一次都不写。
if (name === 'follow') {
panel.dataset.uhPage = String(page);
panel.dataset.uhSub = followSub;
}
if (appendComments && trigger) {
trigger.disabled = true;
trigger.setAttribute('aria-busy', 'true');
trigger.textContent = '加载中…';
}
panel.classList.remove('is-load-error');
panel.classList.add('is-loading');
var url = restBase.replace(/\/$/, '') + '/user-home/panel/' + userId + '/' + encodeURIComponent(name)
@@ -84,10 +113,25 @@
if (!response.ok || !payload || !payload.success || !payload.data || typeof payload.data.html !== 'string') {
throw new Error('panel-load-failed');
}
panel.innerHTML = payload.data.html;
if (appendComments && name === 'comments') {
appendCommentPage(panel, payload.data.html);
} else {
panel.innerHTML = payload.data.html;
}
panel.dataset.panelLoaded = 'true';
if (window.jQuery) jQuery(document).trigger('post_ajax.ed', [panel]);
if (typeof window.auto_fun === 'function') window.auto_fun();
});
}).catch(function (error) {
if (panel.dataset.panelLoaded === 'false') {
panel.classList.add('is-load-error');
}
if (appendComments && trigger && trigger.isConnected) {
trigger.disabled = false;
trigger.removeAttribute('aria-busy');
trigger.textContent = '加载更多';
}
throw error;
}).finally(function () {
panel._aksrtUhRequest = null;
panel.classList.remove('is-loading');
@@ -102,7 +146,12 @@
tabs.forEach(function (tab) {
tab.addEventListener('click', function () {
switchTo(tab.getAttribute('data-tab'));
var name = tab.getAttribute('data-tab');
switchTo(name);
var panel = panels[name] && panels[name].panel;
if (panel && panel.dataset.panelLoaded === 'false') {
loadPanelBody(panel, name, 1, 'followers').catch(notifyLoadError);
}
});
});
@@ -122,6 +171,27 @@
return;
}
var retryBtn = target.closest('[data-uh-retry]');
if (retryBtn) {
event.preventDefault();
var retryPanel = retryBtn.closest('.dashboard-panel');
if (!retryPanel) return;
var retryName = panelNameOf(retryPanel);
if (!retryName) return;
loadPanelBody(retryPanel, retryName, 1, 'followers').catch(notifyLoadError);
return;
}
var commentsNext = target.closest('[data-uh-comments-next]');
if (commentsNext) {
event.preventDefault();
var commentsPanel = commentsNext.closest('.dashboard-panel');
if (!commentsPanel) return;
var commentsPage = Math.max(2, parseInt(commentsNext.getAttribute('data-uh-comments-next') || '2', 10) || 2);
loadPanelBody(commentsPanel, 'comments', commentsPage, 'followers', true, commentsNext).catch(notifyLoadError);
return;
}
var pageBtn = target.closest('[data-uh-page]');
if (!pageBtn) return;
event.preventDefault();
@@ -130,7 +200,13 @@
var name = panelNameOf(panel);
if (!name) return;
var page = Math.max(1, parseInt(pageBtn.getAttribute('data-uh-page') || '1', 10) || 1);
loadPanelBody(panel, name, page, panel.dataset.uhSub || 'followers').catch(notifyLoadError);
// 只认**本面板内**的子页签状态:uhSub 是粉丝面板私有的(panel-follow.php
// 的 data-uh-sub),一旦落到别的面板上就会互相串味 —— 先点过粉丝子页签,
// panel.dataset.uhSub 会残留在文章面板上,之后粉丝面板的翻页读到的是
// 文章面板留下的值。只看当前面板,读不到就回落默认分组。
var pageSub = panel.querySelector('[data-uh-sub].is-active');
var sub = pageSub && pageSub.getAttribute('data-uh-sub') === 'following' ? 'following' : 'followers';
loadPanelBody(panel, name, page, sub).catch(notifyLoadError);
});
switchTo(defaultTab);
+13 -1
View File
@@ -67,6 +67,18 @@
}
});
function formatCount(count) {
// 与 PHP aksrt_format_number() 同一套缩写:≥10000 → 1.2W+,≥1000 → 1.5K。
var safeCount = Math.max(0, parseInt(count, 10) || 0);
if (safeCount >= 10000) {
return Math.round((safeCount / 10000) * 10) / 10 + 'W+';
}
if (safeCount >= 1000) {
return Math.round((safeCount / 1000) * 10) / 10 + 'K';
}
return String(safeCount);
}
function handleLike(btn, commentId) {
if (btn.dataset.busy === '1') return;
btn.dataset.busy = '1';
@@ -81,7 +93,7 @@
btn.setAttribute('aria-pressed', active ? 'true' : 'false');
btn.classList.toggle('actived', active);
var countEl = btn.querySelector('count');
if (countEl) countEl.textContent = String(count);
if (countEl) countEl.textContent = formatCount(count);
})
.catch(function () { notyf('网络错误', 'danger'); })
.finally(function () { btn.dataset.busy = ''; });
+5 -10
View File
@@ -48,13 +48,13 @@
} catch (err) {}
}
// 骨架开关统一走 .is-loading(modal-runtime.css),不写 inline display:
// 内联样式优先级高于类规则,一旦残留会让后续骨架永远打不开。
function showLoader(container) {
if (!container) return;
var loader = container.querySelector(".post_ajax_loader, .post-ajax-loader, [data-aksrt-ajax-loader]");
var loader = container.querySelector(".post_ajax_loader");
if (loader) {
loader.classList.add("is-loading");
loader.hidden = false;
loader.style.display = "";
return;
}
container.classList.add("is-ajax-loading");
@@ -62,13 +62,10 @@
function hideLoader(container) {
if (!container) return;
var loader = container.querySelector(".post_ajax_loader, .post-ajax-loader, [data-aksrt-ajax-loader]");
var loader = container.querySelector(".post_ajax_loader");
if (loader) {
loader.classList.remove("is-loading");
if (loader.hasAttribute("data-aksrt-ajax-loader-hide")) {
loader.hidden = true;
loader.style.display = "none";
}
return;
}
container.classList.remove("is-ajax-loading");
}
@@ -188,8 +185,6 @@
Array.prototype.slice.call(container.children).forEach(function (child) {
if (
child.classList.contains("post_ajax_loader") ||
child.classList.contains("post-ajax-loader") ||
child.hasAttribute("data-aksrt-ajax-loader") ||
child.classList.contains("ajax-pag")
) {
return;
+12 -6
View File
@@ -15,7 +15,9 @@
var wrap = scope.querySelector("[data-ajax-wrap]");
if (!wrap) return;
var loading = wrap.querySelector(".archive__ajax-loading");
// 归档面与首页共用同一套骨架契约:.post_ajax_loader + .is-loading,
// 不再使用独立 spinner 遮罩 / opacity 淡出。
var loading = wrap.querySelector(".post_ajax_loader");
var REST_BASE = "/wp-json/aksrt/v1/archive/posts";
var emptyText = archiveType === "tag" ? "该标签下暂无文章" : "该分类下暂无文章";
@@ -73,12 +75,10 @@
function showLoading() {
if (loading) loading.classList.add("is-loading");
wrap.classList.add("is-loading");
}
function hideLoading() {
if (loading) loading.classList.remove("is-loading");
wrap.classList.remove("is-loading");
}
function rebuildEmptyState() {
@@ -102,9 +102,15 @@
.then(function (data) {
hideLoading();
if (data.html !== undefined) {
var children = Array.from(wrap.children);
children.forEach(function (child) {
if (child !== loading) wrap.removeChild(child);
// 只清真实内容;骨架(.post_ajax_loader)与分页控件保留复用 ——
// 与参考主题 post_ajax success 只 hide 不删的生命周期一致。
var keep = function (node) {
return node === loading ||
node.classList.contains("post_ajax_loader") ||
node.classList.contains("ajax-pag");
};
Array.from(wrap.children).forEach(function (child) {
if (!keep(child)) wrap.removeChild(child);
});
if (data.html) {
var temp = document.createElement("div");
+14 -2
View File
@@ -149,15 +149,27 @@
}
}
function formatStatCount(count) {
// 与 PHP aksrt_format_number() 同一套缩写:≥10000 → 1.2W+,≥1000 → 1.5K。
var safeCount = Math.max(0, count);
if (safeCount >= 10000) {
return Math.round((safeCount / 10000) * 10) / 10 + "W+";
}
if (safeCount >= 1000) {
return Math.round((safeCount / 1000) * 10) / 10 + "K";
}
return String(safeCount);
}
function getBaseStatCount(statName) {
var statEl = document.querySelector('[data-stat-count="' + statName + '"][data-base-count]');
return statEl ? (parseInt(statEl.dataset.baseCount, 10) || 0) : 0;
}
function updateStatCounts(statName, count) {
var safeCount = Math.max(0, count);
var label = formatStatCount(count);
document.querySelectorAll('[data-stat-count="' + statName + '"]').forEach(function (statEl) {
statEl.textContent = String(safeCount);
statEl.textContent = label;
});
}
+2 -1
View File
@@ -465,7 +465,8 @@
if (child !== filter && child !== loader) child.remove();
});
if (skeleton) {
skeleton.style.display = "block";
// 骨架开关走 .is-loading 类(modal-runtime.css),克隆时同步清理状态。
skeleton.classList.add("is-loading");
skeleton.removeAttribute("hidden");
list.appendChild(skeleton);
}
+72 -16
View File
@@ -14,7 +14,6 @@
var triggerText = (window.aksrtData && aksrtData.ajaxTrigger) || "加载更多";
var nomoreText = (window.aksrtData && aksrtData.ajaxNomore) || "没有更多了";
var loadingText = (window.aksrtData && aksrtData.ajaxLoading) || "正在加载...";
var errorText = (window.aksrtData && aksrtData.ajaxLoadError) || "加载失败,请重试";
containers.forEach(function (container) {
@@ -31,7 +30,6 @@
var failed = false;
var clickTarget = container.querySelector(".next-page.ajax-next, .ias-btn") || container;
var loadMoreBtn = container.querySelector("[data-aksrt-load-more]") || clickTarget;
var indicator = container.querySelector(".post_ajax_loader");
var successfulAutoLoads = 0;
var iasMax = isComment
? ((window.aksrtData && typeof aksrtData.commentIasMax !== "undefined") ? parseInt(aksrtData.commentIasMax, 10) : 0)
@@ -52,17 +50,78 @@
target = container.parentElement.querySelector(".post-grid, .comment-list, .commentlist, .content-stream__list");
}
// 文章骨架:与 runtime/main.js 的 .ajax-next 处理器、aksrt_render_placeholder()
// 同源同款(1:1 对齐参考主题 zib_get_post_placeholder)。刻意不写 ajax-item:
// 否则会被 inject() / find('.ajax-item') 当成真实条目删除或定位。
var CARD_ITEM =
'<div class="posts-item card"><div class="item-thumbnail">' +
'<div class="radius8 item-thumbnail placeholder"></div> </div>' +
'<div class="item-body "> <h2 class="item-excerpt placeholder t1 item-heading"></h2>' +
' <p class="mt10 placeholder k2"></p><i class="flex jsb"><i class="placeholder s1"></i>' +
'<i class="placeholder s1 ml10"></i></i></div></div>';
var LIST_ITEM =
'<div class="posts-item list flex"><div class="post-graphic">' +
'<div class="radius8 item-thumbnail placeholder"></div> </div>' +
'<div class="item-body flex xx flex1 jsb"> <p class="placeholder t1"></p>' +
' <h4 class="item-excerpt placeholder k1"></h4><p class="placeholder k2"></p>' +
'<i><i class="placeholder s1"></i><i class="placeholder s1 ml10"></i></i></div></div>';
// 视口阈值与参考主题 .ajax-next 处理器一致:宽 + 无侧栏 → 8,宽 + 有侧栏 → 6,
// 窄屏 → 4;list 模式宽屏 4、窄屏 3。
function skeletonCount(style) {
var wide = window.innerWidth > 766;
if (style === "card") {
if (!wide) return 4;
return window.innerWidth > 992 && !document.querySelector(".sidebar") ? 8 : 6;
}
return wide ? 4 : 3;
}
// 从列表首张真实卡片判断形状;无卡片时退化为 list。
function postSkeleton() {
var first = target && target.querySelector
? target.querySelector(".posts-item.card, .posts-item.list")
: null;
var isCard = !!first && /(^|\s)card(\s|$)/.test(first.className || "");
var tpl = isCard ? CARD_ITEM : LIST_ITEM;
var out = "";
for (var i = 0, n = skeletonCount(isCard ? "card" : "list"); i < n; i++) {
out += tpl;
}
return '<div class="mb20">' + out + '</div>';
}
// 兜底(非文章列表面,例如评论区没有 .posts-item 时):转圈 + 文案,
// 绝不用按钮自身文字来表达加载态。
function fallbackLoader() {
var text = (window.aksrtData && aksrtData.ajaxLoading) || "加载中...";
return (
'<div class="mb20"><div class="text-center muted-2-color mt20">' +
'<i class="loading mr10"></i>' + text +
'</div></div>'
);
}
function showSkeleton() {
if (!target) return;
var loader = target.querySelector(".post-ajax-loader");
if (loader) {
loader.classList.add("is-loading");
var loader = target.querySelector(".post_ajax_loader");
if (!loader) {
loader = document.createElement("div");
loader.className = "post_ajax_loader";
loader.innerHTML = postSkeleton() || fallbackLoader();
}
// 参考主题 beforeSend:骨架每次翻页都挪到列表末尾(分页控件之前),
// 紧贴最后一张卡片、位于「加载更多」按钮上方。
target.insertBefore(
loader,
container.parentNode === target ? container : null
);
loader.classList.add("is-loading");
}
function hideSkeleton() {
if (!target) return;
var loader = target.querySelector(".post-ajax-loader");
var loader = target.querySelector(".post_ajax_loader");
if (loader) {
loader.classList.remove("is-loading");
}
@@ -72,13 +131,11 @@
container.classList.remove("ias-pending");
if (markNomore && loadMoreBtn) {
loadMoreBtn.innerHTML = nomoreText;
loadMoreBtn.classList.remove("is-loading");
loadMoreBtn.classList.add("ajax-nomore");
var nextWrap = loadMoreBtn.closest(".next-page");
if (nextWrap) nextWrap.classList.add("no-more");
loadMoreBtn.style.pointerEvents = "none";
} else if (loadMoreBtn && !loadMoreBtn.classList.contains("ajax-nomore")) {
loadMoreBtn.classList.remove("is-loading");
if (failed) {
loadMoreBtn.textContent = errorText;
} else {
@@ -93,7 +150,6 @@
stopInfinite(false);
if (loadMoreBtn) {
loadMoreBtn.textContent = message || errorText;
loadMoreBtn.classList.remove("is-loading");
loadMoreBtn.classList.remove("ajax-nomore");
loadMoreBtn.style.pointerEvents = "";
}
@@ -107,11 +163,8 @@
}
loading = true;
if (indicator) indicator.style.display = "flex";
if (loadMoreBtn) {
loadMoreBtn.classList.add("is-loading");
loadMoreBtn.textContent = loadingText;
}
// 加载态的呈现完全交给列表末尾的骨架(参考主题 post_ajax beforeSend
// 只操作 .post_ajax_loader,不触碰「加载更多」按钮自身的文字)。
showSkeleton();
var action = isComment ? "aksrt_load_more_comments" : "aksrt_load_more_posts";
@@ -197,9 +250,7 @@
.finally(function () {
loading = false;
hideSkeleton();
if (indicator) indicator.style.display = "none";
if (loadMoreBtn && !loadMoreBtn.classList.contains("ajax-nomore") && !failed) {
loadMoreBtn.classList.remove("is-loading");
loadMoreBtn.innerHTML = triggerText;
if (mode === "infinite") {
var lazyTrigger = clickTarget.closest(".next-page") || clickTarget;
@@ -221,6 +272,11 @@
if (loadMoreBtn) {
clickTarget.addEventListener("click", function(e) {
// 排序菜单(.ajax-option)与 tab pane 触发器(.post_ajax_trigger)归 legacy
// runtime post_ajax() 管线;本引擎避开,否则一次点击发两个请求。
if (e.target.closest && e.target.closest(".ajax-option, .post_ajax_trigger")) {
return;
}
e.preventDefault();
if (loadMoreBtn.dataset.jumpComments === "1") {
var jumpInput = container.querySelector(".jump-input");
+218 -100
View File
@@ -38,6 +38,14 @@
return (BigInt(match[1]) * 100n) + BigInt((match[2] || '').padEnd(2, '0') || '0');
}
function formatCutCount(value) {
var number = Number(value);
if (!Number.isFinite(number)) return String(value);
if (number > 999999) return String(Math.round(number / 10000)) + 'W+';
if (number > 9999) return String(Math.round(number / 1000) / 10) + 'W+';
return String(number);
}
function api(path, options) {
return window.AksrtRequest.request(path, options || {}).then(function (response) {
return {
@@ -64,16 +72,18 @@
var walletNumber = Number(wallet);
var walletText = Number.isFinite(walletNumber) ? walletNumber.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 }) : String(wallet);
document.querySelectorAll('[data-aksrt-wallet-value]').forEach(function (element) {
element.textContent = walletText;
element.title = walletText;
var text = element.dataset.aksrtCountFormat === 'cut-count' ? formatCutCount(wallet) : walletText;
element.textContent = text;
element.title = text;
});
}
if (points !== undefined && points !== null && points !== '') {
var pointsNumber = Number(points);
var pointsText = Number.isFinite(pointsNumber) ? pointsNumber.toLocaleString() : String(points);
document.querySelectorAll('[data-aksrt-points-value]').forEach(function (element) {
element.textContent = pointsText;
element.title = pointsText;
var text = element.dataset.aksrtCountFormat === 'cut-count' ? formatCutCount(points) : pointsText;
element.textContent = text;
element.title = text;
});
}
}
@@ -129,7 +139,7 @@
if (!r.ok) return;
var j = r.json || {};
var ratio = root.querySelector('[data-affiliate-ratio]'); if (ratio) ratio.textContent = String(j.current_ratio || 0) + '%';
initAffiliatePoster(root, url || j.url || '', j.poster || {}, j.site_name || document.title);
initAffiliatePoster(root, url || j.url || '', j.poster || {}, j.site_name || document.title, j.referrer_name || '');
}).catch(function () { /* 海报与比例为增强项,静默失败即可。 */ });
if (copy) copy.addEventListener('click', function () {
if (!input || !input.value) return;
@@ -140,19 +150,63 @@
});
}
// ===== Referral poster (canvas preview modal: hero image + copy + QR + link) =====
function initAffiliatePoster(root, url, poster, siteName) {
// ===== Referral poster =====
/* 布局 1:1 复刻参考主题 poster-share.js 的 rebate 分支(非 product):
* 768×1168,顶部主图占上半屏、左上角日期、标题竖线 + 标题、红色推荐人标签层、
* 简介,底部带边框盒内左侧 contain 居中 logo 图 + 右下二维码,最底部居中文案。
* 顶部主图按参考主题的三级回退加载(banner → banner_default → banner_spare)。 */
function initAffiliatePoster(root, url, poster, siteName, referrerName) {
var button = root.querySelector('[data-affiliate-poster]');
var modal = document.querySelector('[data-aksrt-affiliate-poster-modal]');
var stage = document.querySelector('[data-affiliate-poster-stage]');
var download = document.querySelector('[data-affiliate-poster-download]');
var stage = modal ? modal.querySelector('[data-affiliate-poster-stage]') : null;
var loading = modal ? modal.querySelector('[data-affiliate-poster-loading]') : null;
var content = modal ? modal.querySelector('.modal-content') : null;
var download = modal ? modal.querySelector('[data-affiliate-poster-download]') : null;
if (!button || !modal) return;
if (!url || !poster || poster.enabled !== true) return;
button.hidden = false;
[].forEach.call(modal.querySelectorAll('[data-affiliate-poster-close]'), function (closer) {
closer.addEventListener('click', function () { closeModal(modal); });
});
var backdrop = null;
var canvas = null;
function openPosterModal() {
if (modal.classList.contains('in')) return;
modal.style.display = 'flex';
void modal.offsetWidth; // 强制 reflow,让 .fade 过渡从 scale(0.9) 起手
modal.classList.add('in');
modal.setAttribute('aria-hidden', 'false');
if (!backdrop) {
backdrop = document.createElement('div');
backdrop.className = 'modal-backdrop fade in';
backdrop.setAttribute('data-affiliate-poster-backdrop', '1');
document.body.appendChild(backdrop);
}
document.body.classList.add('modal-open');
}
function closePosterModal() {
if (!modal.classList.contains('in')) return;
modal.classList.remove('in');
modal.setAttribute('aria-hidden', 'true');
document.body.classList.remove('modal-open');
if (backdrop && backdrop.parentNode) backdrop.parentNode.removeChild(backdrop);
backdrop = null;
// 等过渡结束再撤 display,复刻 Bootstrap 的 fade-out 收尾
window.setTimeout(function () {
if (!modal.classList.contains('in')) modal.style.display = '';
}, 300);
}
modal.addEventListener('click', function (event) {
// 点遮罩层(.modal 自身)或关闭按钮(data-dismiss="modal")
if (event.target === modal || (event.target.closest && event.target.closest('[data-dismiss="modal"]'))) {
closePosterModal();
}
});
document.addEventListener('keydown', function (event) {
if (event.key === 'Escape' && modal.classList.contains('in')) closePosterModal();
});
if (download) {
download.addEventListener('click', function () {
if (!canvas) return;
@@ -168,20 +222,47 @@
}
});
}
button.addEventListener('click', function () {
if (typeof window.qrcode !== 'function') {
if (stage) { stage.innerHTML = '<p class="muted-2-color em09">' + '二维码组件未加载。' + '</p>'; }
openModal(modal);
openPosterModal();
return;
}
if (stage) { stage.innerHTML = '<p class="muted-2-color em09">' + '正在生成海报…' + '</p>'; }
if (download) download.hidden = true;
openModal(modal);
if (stage) { stage.innerHTML = ''; stage.classList.remove('loaded'); }
if (loading) loading.style.display = '';
if (download) download.classList.add('hide');
openPosterModal();
button.disabled = true;
buildPosterCanvas(url, poster, siteName).then(function (built) {
buildPosterCanvas(url, poster, siteName, referrerName).then(function (built) {
canvas = built;
if (stage) { stage.innerHTML = ''; stage.appendChild(built); }
if (download) download.hidden = false;
if (!stage) return;
// 参考主题:img.loaded-img 直接进 .poster-imgbox,遮罩淡出,
// 内容块高度从 220px 过渡到海报实际高度后清掉内联样式。
var img = document.createElement('img');
img.src = canvas.toDataURL('image/png');
img.alt = '推广海报';
img.className = 'loaded-img';
// 参考主题按 0.7 比例预设尺寸;.poster-imgbox > img 的 max-width:360px
// 会在窄屏把它压回弹窗宽度,height:100% 让 offsetHeight 反映真实渲染高度。
img.width = 768 * 0.7;
img.height = 1168 * 0.7;
stage.appendChild(img);
stage.classList.add('loaded');
if (download) {
download.href = img.src;
download.classList.remove('hide');
}
if (loading) loading.style.display = 'none';
if (content) {
content.style.height = stage.offsetHeight + 'px';
content.style.transition = '.2s';
window.setTimeout(function () {
content.style.height = '';
content.style.overflow = '';
content.style.transition = '';
}, 200);
}
}).catch(function () {
if (stage) { stage.innerHTML = '<p class="muted-2-color em09">' + '海报生成失败,请重试。' + '</p>'; }
}).finally(function () {
@@ -190,89 +271,141 @@
});
}
// 参考主题 poster-share.js 布局 1:1:768×1168,顶部主图占上半屏,
// 左上角日期,标题竖蓝线 + 标题,正文,底部带边框盒(logo/站名 + 二维码 + 链接)。
function buildPosterCanvas(url, poster, siteName) {
// 三级回退加载图片:任一源成功即返回,全部失败才 reject(参考主题 loadCount 0→1→2)。
function loadPosterImage(sources) {
var list = (sources || []).filter(function (src) { return !!src; });
return new Promise(function (resolve, reject) {
var index = 0;
function next() {
if (index >= list.length) { reject(new Error('poster image unavailable')); return; }
var image = new Image();
image.crossOrigin = 'anonymous';
image.onload = function () { resolve(image); };
image.onerror = function () { index += 1; next(); };
image.src = list[index];
}
next();
});
}
// contain 裁切:参考主题 containImg(),等比缩放塞进盒子并居中,不裁切。
function containRect(sx, sy, boxW, boxH, srcW, srcH) {
var dx = sx, dy = sy, dWidth = boxW, dHeight = boxH;
if (srcW > srcH || (srcW === srcH && boxW < boxH)) {
dHeight = (srcH * dWidth) / srcW;
dy = sy + (boxH - dHeight) / 2;
} else if (srcW < srcH || (srcW === srcH && boxW > boxH)) {
dWidth = (srcW * dHeight) / srcH;
dx = sx + (boxW - dWidth) / 2;
}
return { x: dx, y: dy, width: dWidth, height: dHeight };
}
// 参考主题 drawTextOverflow():按 maxWidth-120 换行、x 偏移 38,最多 maxRow 行,溢出补 ...。
// 返回实际测量宽度,供价格/标签层做横向排版。
function drawTextOverflow(ctx, text, maxWidth, maxRow, font, color, lineHeight, x, y) {
var rows = [];
var temp = '';
var limit = maxWidth - 120;
x = x + 38;
text = String(text || '').replace(/[\r\n]/g, '');
ctx.fillStyle = color;
ctx.font = font;
var chars = text.split('');
var measured = 0;
if (ctx.measureText(text).width <= limit) {
rows.push(text);
} else {
for (var i = 0; i < chars.length; i++) {
if (rows.length === maxRow && i < chars.length - 1) {
rows[rows.length - 1] += '...';
break;
}
if (ctx.measureText(temp).width < limit) {
temp += chars[i];
if (i === chars.length - 1) rows.push(temp);
} else {
i--; // 参考主题:回退一格防止字符丢失
rows.push(temp);
temp = '';
}
}
}
for (var r = 0; r < rows.length; r++) {
measured = Math.max(measured, ctx.measureText(rows[r]).width);
ctx.fillText(rows[r], x, y + r * lineHeight, limit);
}
return { width: measured, height: rows.length * lineHeight };
}
// 参考主题 buildPosterCanvas rebate 分支:768×1168,顶部主图占上半屏,左上角日期,
// 标题竖蓝线 + 标题,推荐人标签(红),简介,底部盒(logo + 二维码 + 底部文案)。
function buildPosterCanvas(url, poster, siteName, referrerName) {
var width = 768, height = 1168;
var canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
var ctx = canvas.getContext('2d');
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, width, height);
var loadImage = null;
if (poster.image) {
loadImage = new Promise(function (resolve) {
var image = new Image();
image.crossOrigin = 'anonymous';
image.onload = function () { resolve(image); };
image.onerror = function () { resolve(null); };
image.src = poster.image;
});
} else {
loadImage = Promise.resolve(null);
}
return loadImage.then(function (image) {
var imgHeight = height / 2; // 参考主题 rebate(非 product):顶部主图占上半屏。
// 顶部主图(cover 裁切)或纯色占位。
if (image && image.width && image.height) {
var rect = coverRect(width, imgHeight, image.width, image.height);
ctx.drawImage(image, rect.sx, rect.sy, rect.sWidth, rect.sHeight, 0, 0, width, imgHeight);
} else {
ctx.fillStyle = '#0f172a';
ctx.fillRect(0, 0, width, imgHeight);
}
var imgHeight = height / 2; // rebate 分支:顶部主图占上半屏
// 站点名走「周」前缀,与参考主题 zib__('week_prefix') 一致。
var weekLabel = '周' + '日一二三四五六'.charAt(new Date().getDay());
// 推荐人标签:参考主题 tags = `推荐人: {昵称}`,缺昵称时整层不画。
var tags = referrerName ? '推荐人: ' + String(referrerName) : '';
// 底部文案:优先推广海报配置,回退到分享设置 share_desc,再回退默认文案。
var bottom = String(poster.bottom || '扫描二维码阅读全文');
var title = String(poster.title || siteName || '');
var description = String(poster.description || '');
return Promise.all([
loadPosterImage([poster.image, poster.banner_default, poster.banner_spare]),
poster.logo ? loadPosterImage([poster.logo]).catch(function () { return null; }) : Promise.resolve(null)
]).then(function (assets) {
var banner = assets[0], logo = assets[1];
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, width, height);
// 顶部主图(cover 裁切)。
var rect = coverRect(width, imgHeight, banner.width, banner.height);
ctx.drawImage(banner, rect.sx, rect.sy, rect.sWidth, rect.sHeight, 0, 0, width, imgHeight);
// 左上角日期(白字,覆盖在主图上)。
var now = new Date();
var week = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'];
ctx.textBaseline = 'top';
ctx.textAlign = 'left';
ctx.fillStyle = 'rgba(255,255,255,0.95)';
ctx.font = 'bold 35px Arial, sans-serif';
ctx.fillText(week[now.getDay()], 40, 35);
ctx.font = '28px Arial, sans-serif';
ctx.fillText((now.getMonth() + 1) + ' / ' + now.getDate(), 40, 88);
ctx.fillStyle = 'rgba(255,255,255,1)';
ctx.font = '35px Arial';
ctx.fillText(weekLabel, 30, 37);
ctx.font = '28px Arial';
ctx.fillText((new Date().getMonth() + 1) + ' / ' + new Date().getDate(), 30, 87);
// 标题竖蓝线。
ctx.strokeStyle = '#00a1ff';
ctx.lineWidth = 7;
ctx.lineCap = 'round';
ctx.beginPath();
ctx.lineCap = 'round';
ctx.moveTo(40, imgHeight + 38);
ctx.lineTo(40, imgHeight + 74);
ctx.stroke();
// 标题(粗体,最多 2 行)。
ctx.fillStyle = '#424242';
ctx.font = 'bold 35px Arial, sans-serif';
var titleLines = wrapText(ctx, String(poster.title || siteName || ''), width - 100).slice(0, 2);
var cursorY = imgHeight + 40;
for (var t = 0; t < titleLines.length; t++) {
ctx.fillText(titleLines[t], 60, cursorY);
cursorY += 52;
// 标题层(粗体)。参考 rebate 分支把 $title 子画布置于 (20, img_height+40),
// 子画布内左对齐 x=40、文字基线偏移 2,故绝对坐标 (40, img_height+42);
// drawTextOverflow 内部会 +38,所以这里传 2。
drawTextOverflow(ctx, title, width, 2, 'bold 35px Arial', '#424242', 52, 2, imgHeight + 42);
// 推荐人标签层(红,最多 1 行):$tags 置于 (0, img_height+160)。
if (tags) {
drawTextOverflow(ctx, tags, width, 1, '24px Arial', '#fc4e1f', 36, 2, imgHeight + 162);
}
// 正文(简介,最多 3 行)。
var description = String(poster.description || '');
// 简介层(灰):$content 置于 (0, img_height+220)。
if (description) {
ctx.fillStyle = '#585858';
ctx.font = '25px Arial, sans-serif';
var lines = wrapText(ctx, description, width - 80).slice(0, 3);
var contentY = imgHeight + 160;
for (var i = 0; i < lines.length; i++) {
ctx.fillText(lines[i], 40, contentY);
contentY += 38;
}
drawTextOverflow(ctx, description, width, 4, '25px Arial', 'rgba(88,88,88,1)', 39, 2, imgHeight + 222);
}
// 底部带边框盒。
ctx.strokeStyle = 'rgba(122,122,122,0.2)';
ctx.lineWidth = 2;
ctx.strokeRect(30, height - 230, width - 60, 200);
// 站点名(logo 位,左侧)。
ctx.fillStyle = '#424242';
ctx.font = 'bold 30px Arial, sans-serif';
ctx.fillText(String(siteName || ''), 80, height - 175);
ctx.fillStyle = '#94a3b8';
ctx.font = '20px Arial, sans-serif';
ctx.fillText('长按识别二维码访问', 80, height - 130);
// 二维码(右下)。
ctx.beginPath();
ctx.rect(30, height - 230, width - 60, 200);
ctx.stroke();
// logo 图:contain 居中,缺图时整块不画(参考主题 logo.naturalWidth 守卫)。
if (logo && logo.width && logo.height) {
var logoRect = containRect(80, height - 190, 300, 100, logo.width, logo.height);
ctx.drawImage(logo, logoRect.x, logoRect.y, logoRect.width, logoRect.height);
}
// 二维码(右下角)。
var qr = window.qrcode(0, 'M');
qr.addData(url);
qr.make();
@@ -288,11 +421,11 @@
}
}
}
// 底部链接(居中)。
// 底部文案(居中,盒下方)。
ctx.textAlign = 'center';
ctx.fillStyle = '#b4b4b4';
ctx.font = '22px Arial, sans-serif';
ctx.fillText(String(url).slice(0, 46), width / 2, height - 46);
ctx.font = '22px Arial';
ctx.fillText(String(bottom).slice(0, 46), width / 2, height - 58);
ctx.textAlign = 'left';
return canvas;
});
@@ -311,21 +444,6 @@
return { sx: sx, sy: sy, sWidth: sWidth, sHeight: sHeight };
}
function wrapText(ctx, text, maxWidth) {
var lines = [];
var current = '';
for (var i = 0; i < text.length; i++) {
if (ctx.measureText(current + text[i]).width > maxWidth && current) {
lines.push(current);
current = text[i];
} else {
current += text[i];
}
}
if (current) lines.push(current);
return lines;
}
window.AKSRTAffiliateRevenueInit = initAffiliateRevenue;
// ===== VIP 开通 =====
@@ -1193,7 +1311,7 @@
var number = document.createElement('span');
number.className = 'uc-withdraw-history__number muted-2-color em09';
var numberLabel = document.createElement('span');
numberLabel.textContent = '流水号:';
numberLabel.textContent = '订单号:';
number.appendChild(numberLabel);
var code = document.createElement('code');
code.textContent = text(item && item.withdrawal_number);
+31
View File
@@ -0,0 +1,31 @@
/**
* AKSRT Frontend Page Admin Set — 前台编辑齿轮的提示横条
*
* 齿轮按钮左侧的「在此编辑此页面参数」横条只是首次进入的引导提示,
* 2 秒后淡出 1 秒消失,避免常驻遮挡悬浮按钮栏(对齐参考主题 js/page-edit.js)。
*
* @package AKSRT
*/
(function () {
'use strict';
function fadeHint() {
var hint = document.querySelector('.admin-set-page .admin-set-info');
if (!hint) return;
setTimeout(function () {
hint.style.transition = 'opacity 1s';
hint.style.opacity = '0';
setTimeout(function () {
hint.style.display = 'none';
}, 1000);
}, 2000);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', fadeHint);
} else {
fadeHint();
}
})();
+200 -9
View File
@@ -121,27 +121,100 @@
return true;
}
var SPLIT_CHUNK_BYTES = 2 * 1048576;
function splitUploadConfig() {
var split = cfg.splitUpload || {};
var minSize = parseInt(split.minSize, 10);
if (!isFinite(minSize) || minSize < 4) minSize = 4;
return { enabled: !!split.enabled, minSize: minSize };
}
function requestJson(url, options) {
var init = options || {};
init.credentials = 'same-origin';
init.headers = { 'X-WP-Nonce': cfg.restNonce || '' };
return fetch(url, init).then(function (response) {
return response.json().catch(function () { return {}; }).then(function (payload) {
if (!response.ok) {
throw new Error((payload && (payload.message || (payload.data && payload.data.message))) || i18n.error || '文件上传失败');
}
return payload;
});
});
}
// 分片上传:状态(断点续传)→ 逐片上传 → 合并走标准落库管线,响应与整传一致。
function uploadInChunks(file, type) {
var uploadKey = type === 'file' ? 'resource' : type;
var base = String(cfg.restUrl || '').replace(/\/$/, '');
if (!base) return Promise.reject(new Error(i18n.uploadUnavailable || '当前账号不能上传此类文件'));
var postId = currentPostId();
function chunkUrl(action) {
var target = base + '/front-submit/' + action + '/' + uploadKey;
if (postId) target += (target.indexOf('?') === -1 ? '?' : '&') + 'post_id=' + encodeURIComponent(String(postId));
return target;
}
var total = Math.ceil(file.size / SPLIT_CHUNK_BYTES);
return requestJson(chunkUrl('upload-status')).catch(function () { return { uploaded: [] }; }).then(function (status) {
var uploaded = {};
(Array.isArray(status.uploaded) ? status.uploaded : []).forEach(function (index) { uploaded[index] = true; });
var chain = Promise.resolve();
for (var index = 0; index < total; index++) {
(function (i) {
chain = chain.then(function () {
if (uploaded[i]) return;
var body = new FormData();
body.append('file', file.slice(i * SPLIT_CHUNK_BYTES, Math.min(file.size, (i + 1) * SPLIT_CHUNK_BYTES)));
body.append('chunk', String(i));
body.append('chunks', String(total));
body.append('name', file.name);
body.append('size', String(file.size));
return requestJson(chunkUrl('upload-chunk'), { method: 'POST', body: body });
});
})(index);
}
return chain;
}).then(function () {
var body = new FormData();
body.append('chunks', String(total));
body.append('name', file.name);
body.append('size', String(file.size));
return requestJson(chunkUrl('upload-merge'), { method: 'POST', body: body });
}).then(function (payload) {
if (!payload || !payload.success || !payload.url) {
throw new Error((payload && (payload.message || (payload.data && payload.data.message))) || i18n.error || '文件上传失败');
}
return payload;
});
}
function uploadMedia(file, type) {
var uploadKey = type === 'file' ? 'resource' : type;
var upload = (cfg.uploads || {})[uploadKey] || null;
if (!upload || !upload.url) return Promise.reject(new Error(i18n.uploadUnavailable || '当前账号不能上传此类文件'));
if (!acceptsFile(file, type)) return Promise.reject(new Error(i18n.invalidFile || '请选择有效文件'));
if (upload.maxSize && file.size > upload.maxSize) return Promise.reject(new Error(i18n.fileTooLarge || '文件大小超过限制'));
var split = splitUploadConfig();
if (split.enabled && typeof file.slice === 'function' && file.size > split.minSize * 1048576) {
if (Math.ceil(file.size / SPLIT_CHUNK_BYTES) > 1) {
return uploadInChunks(file, type);
}
}
var body = new FormData();
body.append('file', file);
var uploadUrl = String(upload.url);
if (currentPostId()) {
uploadUrl += (uploadUrl.indexOf('?') === -1 ? '?' : '&') + 'post_id=' + encodeURIComponent(String(currentPostId()));
}
return fetch(uploadUrl, { method: 'POST', credentials: 'same-origin', headers: { 'X-WP-Nonce': cfg.restNonce || '' }, body: body })
.then(function (response) {
return response.json().catch(function () { return {}; }).then(function (payload) {
if (!response.ok || !payload || !payload.success || !payload.url) {
throw new Error((payload && (payload.message || (payload.data && payload.data.message))) || i18n.error || '文件上传失败');
}
return payload;
});
});
return requestJson(uploadUrl, { method: 'POST', body: body }).then(function (payload) {
if (!payload || !payload.success || !payload.url) {
throw new Error((payload && (payload.message || (payload.data && payload.data.message))) || i18n.error || '文件上传失败');
}
return payload;
});
}
function imageBatchLimit() {
@@ -578,6 +651,120 @@
});
}
function toggleBlockFormat(editor, format) {
editor.formatter.toggle(format);
editor.nodeChanged();
}
function removeEditorBlock(editor, node) {
if (!node || node === editor.getBody()) return;
editor.dom.remove(node);
editor.nodeChanged();
}
function replaceButtonIconWithSvg(button, pathData) {
var node = button && button.$el && button.$el[0];
var icon = node ? node.querySelector('.mce-i-c-svg') : null;
if (!icon) return;
var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('viewBox', '0 0 1024 1024');
svg.setAttribute('style', 'width:21px;height:21px;fill:currentColor;');
var path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
path.setAttribute('d', pathData);
svg.appendChild(path);
icon.parentNode.replaceChild(svg, icon);
}
// 经典编辑器增强浮动工具栏(后台「扩展&增强 → 系统工具」mce_float_toolbar):
// 复刻参考主题 editextend.js 的 wptoolbar 上下文工具条。
function registerFloatToolbar(editor) {
editor.addButton('aksrt_h2', {
title: 'Heading 2',
icon: 'c-svg heading2',
stateSelector: 'h2',
onclick: function () { toggleBlockFormat(editor, 'h2'); },
onPostRender: function () {
replaceButtonIconWithSvg(this, 'M143.616 219.648v228.864h278.016V219.648h89.856V768H421.632v-242.688H143.616V768H53.76V219.648h89.856z m660.48-10.752c52.992 0 96.768 15.36 131.328 46.08 33.792 30.72 50.688 69.888 50.688 119.04 0 47.616-18.432 90.624-53.76 129.792-16.554667 17.706667-43.093333 39.082667-78.933333 64.426667l-22.613334 15.701333-12.117333 8.192c-52.309333 34.389333-85.248 64.810667-99.413333 91.178667l-2.730667 5.589333h270.336V768h-382.464c0-56.064 17.664-104.448 54.528-145.92 8.746667-10.069333 21.76-22.186667 38.912-36.352l15.786667-12.586667c5.589333-4.352 11.52-8.874667 17.834666-13.568l19.84-14.506666 21.888-15.488 11.690667-8.106667c35.328-24.576 59.904-45.312 75.264-61.44 23.808-26.88 36.096-56.064 36.096-86.784 0-29.952-8.448-52.224-23.808-66.816-16.128-14.592-39.936-21.504-71.424-21.504-33.792 0-59.136 11.52-76.032 34.56-15.36 19.541333-24.362667 48.64-27.050667 86.058667l-0.597333 11.477333h-89.856c0.768-61.44 18.432-110.592 53.76-148.224 36.096-39.936 83.712-59.904 142.848-59.904z');
},
});
editor.addButton('aksrt_h3', {
title: 'Heading 3',
icon: 'c-svg heading3',
stateSelector: 'h3',
onclick: function () { toggleBlockFormat(editor, 'h3'); },
onPostRender: function () {
replaceButtonIconWithSvg(this, 'M801.024 208.896c55.296 0 100.608 13.056 134.4 39.936 33.024 26.88 49.92 63.744 49.92 111.36 0 59.904-30.72 99.84-91.392 119.808 32.256 9.984 57.6 24.576 74.496 44.544 18.432 20.736 27.648 47.616 27.648 79.872 0 50.688-17.664 92.16-52.992 124.416-36.864 33.024-85.248 49.92-145.152 49.92-56.832 0-102.912-14.592-137.472-43.776-38.4-32.256-59.904-79.872-64.512-141.312h91.392c1.536 35.328 12.288 62.976 33.792 82.176 19.2 17.664 44.544 26.88 76.032 26.88 34.56 0 62.208-9.984 82.176-29.184 17.664-17.664 26.88-39.168 26.88-65.28 0-31.488-9.984-54.528-28.416-69.12-18.432-15.36-45.312-22.272-80.64-22.272h-38.4V449.28h38.4c32.256 0 56.832-6.912 73.728-20.736 16.128-13.824 24.576-34.56 24.576-61.44 0-26.88-7.68-46.848-22.272-60.672-16.128-13.824-39.936-20.736-71.424-20.736-32.256 0-56.832 7.68-74.496 23.808-18.432 16.128-29.184 40.704-32.256 73.728h-88.32c4.608-55.296 24.576-98.304 61.44-129.024 34.56-30.72 79.104-45.312 132.864-45.312z m-657.408 10.752v228.864h278.016V219.648h89.856V768H421.632v-242.688H143.616V768H53.76V219.648h89.856z');
},
});
editor.addButton('aksrt_remove', {
tooltip: i18n.removeBlock || '删除',
icon: 'dashicon dashicons-no-alt',
onclick: function () {
removeEditorBlock(editor, editor.selection.getNode());
},
});
editor.on('wptoolbar', function (event) {
if (event.toolbar) return;
var probe = document.createElement('div');
probe.innerHTML = editor.getContent();
if (probe.querySelectorAll('p,h1,h2,h3,h4,h5,h6,ul,ol,div,blockquote,pre,img,iframe,table,hr').length <= 4) return;
var bars = floatToolbarButtons(editor, event);
if (bars.length && editor.wp && typeof editor.wp._createToolbar === 'function') {
event.toolbar = editor.wp._createToolbar(bars);
}
});
}
// 复刻参考主题 get_all_toolbar:按光标所在块与是否选中文字给出上下文按钮。
function floatToolbarButtons(editor, event) {
var element = event.element;
if (!element) return [];
var nodeName = element.nodeName;
var innerText = element.innerText ? String(element.innerText).trim() : '';
var hasSelection = !!editor.selection.getContent();
var parent = element.parentElement;
var inRoot = !!(parent && parent.id === 'tinymce');
var bars = [];
if (['P', 'H1', 'H2', 'H3', 'H4', 'H5', 'UL', 'LI', 'STRONG', 'SPAN', 'CODE'].indexOf(nodeName) !== -1) {
if (inRoot || (nodeName === 'SPAN' && parent && parent.parentElement && parent.parentElement.id === 'tinymce')) {
bars.push('aksrt_h2', 'aksrt_h3');
}
if (hasSelection) {
bars = ['H1', 'H2', 'H3', 'H4', 'H5'].indexOf(nodeName) === -1 ? ['bold', 'link', 'wp_code'] : [];
bars.push('forecolor');
if (nodeName === 'P' && editorCfg.canHide) bars.push('aksrt_hide');
bars.push('aksrt_remove');
return bars;
}
if (innerText) {
bars.push('bullist', 'numlist', 'aligncenter');
if (nodeName === 'P' && inRoot) {
bars.push('aksrt_quote');
if (editorCfg.canHide) bars.push('aksrt_hide');
}
bars.push('aksrt_remove');
} else {
bars.push('bullist', 'aksrt_quote');
if (editorCfg.canHide) bars.push('aksrt_hide');
if (editorCfg.canUploadImage) bars.push('aksrt_image');
bars.push('aksrt_video', 'aksrt_precode');
}
}
if (!bars.length && (nodeName === 'DIV' || nodeName === 'PRE') && !(element.classList && element.classList.contains('file-download-name'))) {
bars.push('aksrt_remove');
}
return bars;
}
function registerEditorTools() {
if (!window.tinymce || !window.tinymce.PluginManager || window.aksrtSubmitEditorToolsRegistered) {
return false;
@@ -711,6 +898,10 @@
menu: menu,
});
}
if (editorCfg.floatToolbar) {
registerFloatToolbar(editor);
}
});
return true;
}
-62
View File
@@ -103,7 +103,6 @@
this.bindFeatured();
this.bindPay();
this.bindLeaveWarning();
this.bindAi();
this.bindTagActive();
this.loadFeaturedFromDom();
},
@@ -635,67 +634,6 @@
});
},
bindAi: function () {
var ai = cfg.ai || {};
if (!ai.available) {
return;
}
qsa('.aksrt-ai-assist-btn').forEach(function (btn) {
btn.hidden = false;
btn.addEventListener('click', function () {
var action = btn.getAttribute('data-ai-action');
var content = getEditorContent();
if (!content || content.replace(/<[^>]+>/g, '').trim().length < 10) {
notyf(i18n.needContent || '请先填写正文内容', 'warning');
return;
}
var toastId = action === 'tags'
? 'aksrt-front-submit-ai-tags-toast'
: 'aksrt-front-submit-ai-summary-toast';
var old = btn.textContent;
btn.disabled = true;
btn.textContent = i18n.aiGenerating || '...';
notyf(i18n.aiGenerating || '正在生成…', 'load', '', toastId);
fetch((cfg.restUrl || '').replace(/\/$/, '') + '/ai/' + action, {
method: 'POST',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
'X-WP-Nonce': cfg.restNonce || '',
},
body: JSON.stringify({ content: content }),
})
.then(function (r) {
return r.json();
})
.then(function (res) {
var text = (res && (res.data || res).text) || (res && res.result) || '';
if (!text) {
throw new Error(i18n.aiFailed || 'AI failed');
}
if (action === 'summary') {
var ex = qs('[name="excerpt"]');
if (ex) {
ex.value = text;
}
} else if (action === 'tags') {
var tags = qs('[name="tags"]');
if (tags) {
tags.value = text;
}
}
notyf(i18n.aiFilled || '已填入,可继续编辑', 'success', 5, toastId);
})
.catch(function (err) {
notyf(err.message || i18n.aiFailed || 'AI 生成失败', 'danger', 5, toastId);
})
.finally(function () {
btn.disabled = false;
btn.textContent = old;
});
});
});
},
};
jQuery(document).off('auto_fun.aksrt-front-submit').on('auto_fun.aksrt-front-submit', function () { App.init(); });
+25 -1
View File
@@ -70,11 +70,18 @@
currentList.querySelectorAll('[data-notification-id]').forEach(function (item) {
existingIds.add(item.dataset.notificationId);
});
// 骨架常驻列表末尾:新条目一律插到它之前,形状与参考主题一致
// (post_ajax 的 beforeSend 把 loader 挪到最后一张卡片之后)。
var skeleton = currentList.querySelector(':scope > .post_ajax_loader');
var firstAdded = null;
incomingList.querySelectorAll('.message-item').forEach(function (item) {
if (existingIds.has(item.dataset.notificationId)) return;
if (!firstAdded) firstAdded = item;
currentList.appendChild(item);
if (skeleton) {
currentList.insertBefore(item, skeleton);
} else {
currentList.appendChild(item);
}
});
var currentPagination = content.querySelector('.messages-pagination');
@@ -93,6 +100,9 @@
var settings = options || {};
root.classList.add('is-loading');
content.setAttribute('aria-busy', 'true');
// 骨架只在 AJAX 请求期间展开(.is-loading),与文章列表翻页同一契约。
var skeleton = content.querySelector('.messages-list > .post_ajax_loader');
if (skeleton) skeleton.classList.add('is-loading');
return post('aksrt_load_message_center', {
view: requested.view,
@@ -111,7 +121,19 @@
if (settings.append && state.paginateType === 'ajax_lists') {
firstAdded = appendMessagePage(result.data.content);
} else {
// 替换时保留骨架容器(参考主题 success 后只 hide() 不删除),
// 让后续翻页复用同一节点而不是回到无骨架状态。
var keep = content.querySelector('.post_ajax_loader');
content.innerHTML = result.data.content;
var nextSkeleton = content.querySelector('.post_ajax_loader');
if (keep && !nextSkeleton) {
var list = content.querySelector('.messages-list');
if (list) {
list.appendChild(keep);
} else {
content.appendChild(keep);
}
}
}
updateHeaderBadge(result.data.unread);
@@ -131,6 +153,8 @@
}).finally(function () {
root.classList.remove('is-loading');
content.removeAttribute('aria-busy');
var loader = content.querySelector('.messages-list > .post_ajax_loader');
if (loader) loader.classList.remove('is-loading');
});
}
+700
View File
@@ -0,0 +1,700 @@
/**
* AKSRT After-Sale — 前台售后弹窗(申请 / 超时效提示 / 售后详细)
*
* 与「禁封/举报」链路同构(report.js / user-ban.js):
* - [data-aksrt-after-sale-apply] → 申请售后弹窗(超时效则改为超时效提示弹窗)
* - [data-aksrt-after-sale-detail] → 售后详细弹窗(可再次申请 / 不可再申请提示)
* - [data-aksrt-after-sale-withdraw] → 撤销售后二次确认弹窗(仅待审核工单),确认后 DELETE 工单
* 两个入口的 data-* 属性携带订单与售后状态载荷(同 report.js 的 data-aksrt-report),
* 点击即同步弹窗,不做前置请求(避免首次点击无反馈)。
* - 提交走 REST /aksrt/v1/orders/{order_number}/after-sale
* - 撤销走 DELETE 同端点(仅待审核工单,后端置为已取消)
* - 提交与撤销前先过人机验证(after_sale_apply / after_sale_cancel,场景未启用自动放行)
* - 举证图片走 /orders/{order_number}/after-sale/upload-image(复用评论图片上传校验)
*
* 按钮形态由订单视图模型的 after_sale 状态决定(UserOrders::after_sale_state):
* 已申请 → 售后详细;未申请且在时效内 → 申请售后;超时效 → 申请售后(点击弹超时效提示)。
* 弹窗统一经 AksrtModalRuntime.open 直出 HTML:桌面为主题 modal-mini 几何
* (400px)+ 蓝色炫彩标题区(modal-colorful-header colorful-bg jb-blue),
* 移动端由 mobileBottom 底部抽屉接管。
*
* @package AKSRT
*/
(function () {
'use strict';
var API_ROOT = (function () {
var d = window.aksrtData || {};
var raw = d.apiUrl || d.restUrl || d.rest_url || '/index.php?rest_route=/aksrt/v1';
return String(raw).replace(/\/$/, '');
})();
var NONCE = (window.aksrtData && window.aksrtData.restNonce) || '';
var MAX_IMAGES = 3;
var STATUS_LABELS = {
pending: '待审核',
processing: '处理中',
approved: '已通过',
rejected: '已拒绝',
cancelled: '已取消',
completed: '已完成',
};
function settings() {
var d = window.aksrtData && window.aksrtData.afterSale;
return d && typeof d === 'object' ? d : {};
}
function reasonOptions() {
var list = settings().reasons;
if (!Array.isArray(list)) return [];
return list
.map(function (item) {
return {
slug: String((item && (item.slug || item.value)) || ''),
name: String((item && (item.name || item.label || item.t)) || ''),
};
})
.filter(function (item) { return item.name !== ''; });
}
function materialOptions() {
var list = settings().materials;
if (!Array.isArray(list)) return [];
return list.map(function (item) { return String(item == null ? '' : item); })
.filter(function (text) { return text !== ''; });
}
function escapeHtml(value) {
return String(value == null ? '' : value)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function colorfulHeader(title, icon) {
return '<div class="modal-colorful-header colorful-bg jb-blue">' +
'<button type="button" class="close" data-aksrt-modal-close aria-label="关闭"><svg class="ic-close" aria-hidden="true"><use xlink:href="#icon-close"></use></svg></button>' +
'<div class="colorful-make"></div>' +
'<div class="text-center"><div class="em2x"><i class="fa fas ' + escapeHtml(icon) + '" aria-hidden="true"></i></div>' +
'<div class="mt10 em12 padding-w10">' + escapeHtml(title) + '</div></div></div>';
}
function orderAmountText(order) {
var raw = String(order && order.amount != null ? order.amount : '0');
if (order && order.is_points) {
return escapeHtml(String(order.currency_symbol || '积分')) + ' ' + (/^\d+$/.test(raw) ? raw : '0');
}
return '¥' + raw;
}
function orderSummaryHtml(order) {
if (!order) return '';
return '<div class="muted-box padding-10 mb20 em09 aksrt-after-sale-summary">' +
'<div class="aksrt-after-sale-summary__row"><span class="muted-2-color">商品</span>' +
'<span class="aksrt-after-sale-summary__value">' + escapeHtml(order.product_name || '订单') + '</span></div>' +
'<div class="aksrt-after-sale-summary__row"><span class="muted-2-color">订单号</span>' +
'<span class="aksrt-after-sale-summary__value">' + escapeHtml(order.order_number || '') + '</span></div>' +
'<div class="aksrt-after-sale-summary__row"><span class="muted-2-color">实付</span>' +
'<span class="aksrt-after-sale-summary__value">' + orderAmountText(order) + '</span></div>' +
'</div>';
}
function buildApplyHtml(order) {
var cfg = settings();
var reasons = reasonOptions();
var materials = materialOptions();
var html = '<div class="aksrt-after-sale-apply-modal">';
html += colorfulHeader('申请售后', 'fa-undo-alt');
html += '<form class="aksrt-after-sale-form" data-aksrt-after-sale-order="' + escapeHtml((order && order.order_number) || '') + '" novalidate>';
// 滚动容器与禁封申诉弹窗一致:no-scrollbar 完全隐藏滚动条(含细滚动条)+ 50vh 内层滚动。
html += '<div class="scroll-y no-scrollbar max-vh5">';
html += orderSummaryHtml(order);
if (reasons.length) {
html += '<div class="mb20">';
html += '<div class="em09 muted-2-color mb6">售后原因</div>';
html += '<div class="form-radio form-but-radio">';
reasons.forEach(function (reason) {
html += '<span class="mr6 mb6 badg p2-10 pointer" data-aksrt-after-sale-reason="' + escapeHtml(reason.name) + '" data-reason-slug="' + escapeHtml(reason.slug) + '">' + escapeHtml(reason.name) + '</span>';
});
html += '</div></div>';
}
materials.forEach(function (label) {
html += '<div class="mb10"><div class="muted-2-color mb6">' + escapeHtml(label) + '</div>' +
'<input class="form-control" type="text" maxlength="120" data-aksrt-after-sale-material="' + escapeHtml(label) + '" placeholder="请输入' + escapeHtml(label) + '"></div>';
});
var placeholder = String(cfg.reasonPlaceholder || '必填,请您详细填写申请原因');
html += '<div class="mb10"><div class="muted-2-color mb6">申请说明</div>' +
'<textarea class="form-control" name="reason" rows="3" maxlength="2000" required placeholder="' + escapeHtml(placeholder) + '"></textarea></div>';
if (cfg.imgEnabled) {
html += '<div class="form-upload mb10">' +
'<div class="em09 muted-2-color mb6">上传图片</div>' +
'<div class="preview"><div class="add" data-aksrt-after-sale-add role="button" tabindex="0" aria-label="上传图片"></div></div>' +
'<input class="hide" type="file" accept="image/gif,image/jpeg,image/jpg,image/png,image/webp" data-aksrt-after-sale-file>' +
'<div class="muted-2-color em09 mt6">最多上传 ' + MAX_IMAGES + ' 张图片</div>' +
'</div>';
}
// 申请售后说明(后台 after_sale_desc,服务端已 wp_kses_post)。
var desc = String(cfg.desc || '');
if (desc) {
html += '<div class="em09 padding-h10 muted-box mb10">' + desc + '</div>';
}
html += '</div>';
html += '<div class="but-average modal-buts">' +
'<button type="button" class="but" data-aksrt-modal-close>取消</button>' +
'<button type="button" class="but c-blue" data-aksrt-after-sale-submit><i class="fa fas fa-check" aria-hidden="true"></i>确认提交</button>' +
'</div>';
html += '</form></div>';
return html;
}
function detailRow(label, value) {
if (value === '' || value == null) return '';
return '<div class="aksrt-after-sale-detail__row"><span class="muted-2-color">' + escapeHtml(label) + '</span>' +
'<span class="aksrt-after-sale-detail__value">' + escapeHtml(value) + '</span></div>';
}
// 处理记录节点标题:用户侧提交/撤脱离不开工单状态,单独措辞;
// 后台提交直接用目标状态,与后台列表口径一致。
function handlingEntryTitle(entry) {
var status = String((entry && entry.status) || '');
if (String((entry && entry.source) || '') === 'user') {
return status === 'cancelled' ? '用户撤销售后' : '用户提交申请';
}
return STATUS_LABELS[status] || status;
}
// 处理记录时间线:左列绳结状态条(节点 + 状态 + 时间),右列处理备注。
// 数据源为 latest.handling_log(工单行内 JSON,按时间正序)。
function handlingLogHtml(log) {
var entries = Array.isArray(log) ? log : [];
if (!entries.length) {
return '<div class="aksrt-after-sale-log"><div class="aksrt-after-sale-log__empty">暂无处理记录</div></div>';
}
var html = '<div class="aksrt-after-sale-log"><div class="muted-2-color em09 mb6">处理记录</div>';
html += '<ol class="aksrt-after-sale-timeline">';
entries.forEach(function (entry) {
var status = String((entry && entry.status) || '');
var note = String((entry && entry.note) || '');
html += '<li class="aksrt-after-sale-timeline__item is-' + escapeHtml(status) + '">';
html += '<span class="aksrt-after-sale-timeline__knot"></span>';
html += '<div class="aksrt-after-sale-timeline__body">';
// [节点][处理状态][时间] 同一行,处理备注另起一行
html += '<div class="aksrt-after-sale-timeline__head">' +
'<span class="aksrt-after-sale-timeline__status">' + escapeHtml(handlingEntryTitle(entry)) + '</span>' +
'<time class="aksrt-after-sale-timeline__time">' + escapeHtml(String((entry && entry.created_at) || '')) + '</time>' +
'</div>';
if (note !== '') {
html += '<p class="aksrt-after-sale-timeline__note">' + escapeHtml(note).replace(/\r\n|\r|\n/g, '<br>') + '</p>';
}
html += '</div>';
html += '</li>';
});
html += '</ol></div>';
return html;
}
function buildDetailHtml(order) {
var afterSale = (order && order.after_sale) || {};
var latest = afterSale.latest || {};
var status = String(latest.status || '');
var html = '<div class="aksrt-after-sale-detail-modal">';
html += colorfulHeader('售后详细', 'fa-clipboard-list');
html += '<div class="scroll-y no-scrollbar max-vh5 aksrt-after-sale-detail__body">';
html += orderSummaryHtml(order);
html += '<div class="aksrt-after-sale-detail__statusRow">' +
'<span class="muted-2-color em09">售后状态</span>' +
'<span class="aksrt-after-sale-detail__statusCell">' +
'<span class="badg p2-10 aksrt-after-sale-status is-' + escapeHtml(status) + '">' + escapeHtml(STATUS_LABELS[status] || status) + '</span>' +
// 仅「待审核」可撤销(后端 cancel 仅放行 pending):入口随状态徽章渲染。
(status === 'pending'
? '<button type="button" class="aksrt-after-sale-withdraw" data-aksrt-after-sale-withdraw="' + escapeHtml((order && order.order_number) || '') + '">撤销售后<i class="fa fas fa-angle-right" aria-hidden="true"></i></button>'
: '') +
'</span></div>';
html += '<div class="aksrt-after-sale-detail__rows">';
html += detailRow('申请时间', String(latest.created_at || ''));
html += '</div>';
// 处理记录:每次后台提交一条,替代原先单行的「处理备注 / 完成时间」。
html += handlingLogHtml(latest.handling_log);
// 申请表单分行展示:预置原因 / 申请资料 / 申请说明各自独立成行,
// 与上方「申请时间」等明细行同格式;老数据无 apply_form 时兜底展示融合串。
var applyForm = latest.apply_form && typeof latest.apply_form === 'object' ? latest.apply_form : null;
if (applyForm) {
html += detailRow('原因', String(applyForm.reason_label || ''));
(Array.isArray(applyForm.materials) ? applyForm.materials : []).forEach(function (material) {
html += detailRow(String((material && material.label) || ''), String((material && material.value) || ''));
});
html += detailRow('申请说明', String(applyForm.description || ''));
} else if (latest.reason) {
html += '<div class="mb10"><div class="muted-2-color em09 mb6">申请原因</div>' +
'<div class="em09 aksrt-after-sale-reason-text">' + escapeHtml(latest.reason).replace(/\r\n|\r|\n/g, '<br>') + '</div></div>';
}
var images = Array.isArray(latest.images) ? latest.images : [];
if (images.length) {
html += '<div class="mb10"><div class="muted-2-color em09 mb6">举证图片</div><div class="aksrt-after-sale-images">';
images.forEach(function (url) {
html += '<a href="' + escapeHtml(url) + '" target="_blank" rel="noopener noreferrer"><img src="' + escapeHtml(url) + '" alt=""></a>';
});
html += '</div></div>';
}
html += '</div>';
if (afterSale.can_reapply) {
// 可再次申请:仅保留单个主按钮(与禁封申诉弹窗单按钮同式)。
html += '<div class="but-average modal-buts">' +
'<button type="button" class="but c-blue" data-aksrt-after-sale-reapply data-aksrt-after-sale-payload="' + escapeHtml(JSON.stringify(order)) + '"><i class="fa fas fa-undo-alt" aria-hidden="true"></i>再次申请售后</button>' +
'</div>';
} else {
// 不可再申请售后:不再展示阻断文案,仅保留关闭入口。
html += '<div class="but-average modal-buts"><button type="button" class="but c-blue" data-aksrt-modal-close>我知道了</button></div>';
}
html += '</div>';
return html;
}
function buildExpiredHtml() {
var text = String(settings().expiredText || '抱歉,您的订单已超过售后申请时效');
var html = '<div class="aksrt-after-sale-expired-modal">';
html += colorfulHeader('售后提示', 'fa-clock');
// 超过时效提示(后台 after_sale_expired_text,服务端已 wp_kses_post,支持 HTML)。
html += '<div class="aksrt-after-sale-expired__body em09">' + text + '</div>';
html += '<div class="but-average modal-buts"><button type="button" class="but c-blue" data-aksrt-modal-close>我知道了</button></div>';
html += '</div>';
return html;
}
// 撤销售后二次确认弹窗:待审核工单入口点入,确认后 DELETE 售后工单。
function buildWithdrawHtml(orderNumber) {
var html = '<div class="aksrt-after-sale-withdraw-modal">';
html += colorfulHeader('撤销售后', 'fa-undo-alt');
html += '<div class="aksrt-after-sale-withdraw__body em09">撤销后本次售后申请将关闭,订单恢复为可再次申请售后的状态。确定撤销吗?</div>';
html += '<div class="but-average modal-buts">' +
'<button type="button" class="but" data-aksrt-modal-close>取消</button>' +
'<button type="button" class="but c-blue" data-aksrt-after-sale-withdraw-confirm="' + escapeHtml(orderNumber) + '">确认撤销</button>' +
'</div>';
html += '</div>';
return html;
}
function openModal(options) {
if (!window.AksrtModalRuntime || typeof window.AksrtModalRuntime.open !== 'function') {
notyf('售后组件加载失败,请刷新页面后重试', 'danger');
return null;
}
var shell = window.AksrtModalRuntime.open({
html: options.html,
className: options.className,
kind: options.kind,
mobileBottom: true,
});
bindShell(shell);
return shell;
}
function openApply(order) {
return openModal({
html: buildApplyHtml(order),
className: 'aksrt-after-sale-apply-modal',
kind: 'after-sale-apply',
});
}
function openDetail(order) {
return openModal({
html: buildDetailHtml(order),
className: 'aksrt-after-sale-detail-modal',
kind: 'after-sale-detail',
});
}
function openExpired() {
return openModal({
html: buildExpiredHtml(),
className: 'aksrt-after-sale-expired-modal',
kind: 'after-sale-expired',
});
}
function openWithdraw(orderNumber) {
if (!orderNumber) return;
return openModal({
html: buildWithdrawHtml(orderNumber),
className: 'aksrt-after-sale-withdraw-modal',
kind: 'after-sale-withdraw',
});
}
/**
* 解析按钮 data-* 上的售后载荷(与举报链路 data-aksrt-report 同模式):
* 入口数据随列表一起渲染,点击即可同步弹窗,不做前置请求。
*/
function parsePayload(raw) {
if (!raw) return null;
try {
var data = JSON.parse(raw);
return data && typeof data === 'object' ? data : null;
} catch (error) {
return null;
}
}
// 申请售后入口:超时效 → 超时效提示;已申请 → 售后详细;否则 → 申请表单。
function openApplyFlow(payload) {
var afterSale = payload && payload.after_sale ? payload.after_sale : null;
if (!payload || !afterSale || !afterSale.enabled) {
notyf('售后申请暂不可用', 'danger');
return;
}
if (afterSale.expired) { openExpired(); return; }
if (afterSale.has_application) { openDetail(payload); return; }
openApply(payload);
}
function openDetailFlow(payload) {
var afterSale = payload && payload.after_sale ? payload.after_sale : null;
if (!payload || !afterSale || !afterSale.enabled || !afterSale.has_application) {
notyf('暂无售后记录', 'danger');
return;
}
openDetail(payload);
}
function collectImages(shell) {
return Array.prototype.map.call(
shell.querySelectorAll('.preview .preview-item img'),
function (img) { return img.getAttribute('src') || ''; }
).filter(function (url) { return url !== ''; });
}
function uploadImage(shell, file) {
var form = shell.querySelector('[data-aksrt-after-sale-order]');
var preview = shell.querySelector('.form-upload .preview');
var orderNumber = form ? form.getAttribute('data-aksrt-after-sale-order') : '';
if (!preview || !orderNumber) return Promise.resolve();
if (collectImages(shell).length >= MAX_IMAGES) {
notyf('最多上传 ' + MAX_IMAGES + ' 张图片', 'danger');
return Promise.resolve();
}
var body = new FormData();
body.append('image', file);
return window.fetch(API_ROOT + '/orders/' + encodeURIComponent(orderNumber) + '/after-sale/upload-image', {
method: 'POST',
headers: { 'X-WP-Nonce': NONCE },
body: body,
})
.then(function (res) { return res.json(); })
.then(function (result) {
var url = result && result.success && result.data && result.data.url;
if (!url) {
notyf((result && result.message) || '图片上传失败', 'danger');
return;
}
if (collectImages(shell).length >= MAX_IMAGES) {
notyf('最多上传 ' + MAX_IMAGES + ' 张图片', 'danger');
return;
}
var item = document.createElement('div');
item.className = 'preview-item';
var img = document.createElement('img');
img.src = url;
img.alt = '';
var remove = document.createElement('button');
remove.type = 'button';
remove.className = 'remove';
remove.setAttribute('data-aksrt-after-sale-image-remove', '');
remove.setAttribute('aria-label', '删除图片');
remove.innerHTML = '&times;';
remove.addEventListener('click', function () { item.remove(); });
item.appendChild(img);
item.appendChild(remove);
preview.insertBefore(item, preview.querySelector('.add'));
})
.catch(function () {
notyf('图片上传失败,请重试', 'danger');
});
}
function selectedReasonLabel(shell) {
var active = shell.querySelector('[data-aksrt-after-sale-reason].active');
return active ? String(active.getAttribute('data-aksrt-after-sale-reason') || '') : '';
}
function collectMaterials(shell) {
var materials = [];
Array.prototype.forEach.call(shell.querySelectorAll('[data-aksrt-after-sale-material]'), function (input) {
var value = String(input.value || '').trim();
if (value === '') return;
materials.push({
label: String(input.getAttribute('data-aksrt-after-sale-material') || ''),
value: value,
});
});
return materials;
}
function composeReason(shell) {
var parts = [];
var active = shell.querySelector('[data-aksrt-after-sale-reason].active');
if (active) {
var label = active.getAttribute('data-aksrt-after-sale-reason') || '';
if (label) parts.push(label);
}
var materials = [];
Array.prototype.forEach.call(shell.querySelectorAll('[data-aksrt-after-sale-material]'), function (input) {
var value = String(input.value || '').trim();
if (value === '') return;
materials.push(String(input.getAttribute('data-aksrt-after-sale-material') || '') + ':' + value);
});
if (materials.length) parts.push(materials.join('\n'));
var reasonInput = shell.querySelector('[name="reason"]');
var reasonText = reasonInput ? String(reasonInput.value || '').trim() : '';
if (reasonText) parts.push(reasonText);
return parts.join('\n');
}
function clearToast(id) {
var toast = document.getElementById(id);
if (toast) toast.remove();
}
// 提交前的人机验证:场景未启用时无挂载点,run 直接 resolve {};
// 用户中途取消验证时 run resolve null,此时还原按钮并中止提交。
function runCaptcha(scene) {
if (!window.AKSRTCaptcha || typeof window.AKSRTCaptcha.run !== 'function') {
return Promise.resolve({});
}
return window.AKSRTCaptcha.run(scene).then(function (payload) {
return payload === null ? Promise.reject({ cancelled: true }) : (payload || {});
});
}
function submitApply(shell) {
var form = shell.querySelector('[data-aksrt-after-sale-order]');
var orderNumber = form ? form.getAttribute('data-aksrt-after-sale-order') : '';
var reasonInput = shell.querySelector('[name="reason"]');
var reasonText = reasonInput ? String(reasonInput.value || '').trim() : '';
if (!reasonText) {
notyf('请填写申请说明', 'danger');
if (reasonInput) reasonInput.focus();
return;
}
// 预置原因被后台清空时不强制选择;有配置则必须选中一项(与举报弹窗一致)。
if (shell.querySelector('[data-aksrt-after-sale-reason]') && !shell.querySelector('[data-aksrt-after-sale-reason].active')) {
notyf('请选择售后原因', 'danger');
return;
}
var submitBtn = shell.querySelector('[data-aksrt-after-sale-submit]');
if (submitBtn) submitBtn.disabled = true;
notyf('正在提交售后申请…', 'load', 3000, 'aksrt-after-sale-submit');
var restoreButton = function () {
if (submitBtn) submitBtn.disabled = false;
};
var sendApply = function (captchaPayload) {
window.fetch(API_ROOT + '/orders/' + encodeURIComponent(orderNumber) + '/after-sale', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-WP-Nonce': NONCE,
},
body: JSON.stringify(Object.assign({
reason: composeReason(shell),
reason_label: selectedReasonLabel(shell),
materials: collectMaterials(shell),
description: reasonText,
images: collectImages(shell),
}, captchaPayload)),
})
.then(function (res) { return res.json(); })
.then(function (result) {
if (result && result.success) {
window.AksrtModalRuntime.close('submit');
notyf('售后申请已提交', 'success');
if (window.AKSRTOrderCenter && typeof window.AKSRTOrderCenter.refresh === 'function') {
window.AKSRTOrderCenter.refresh(orderNumber);
}
} else {
restoreButton();
notyf((result && result.message) || '售后申请提交失败', 'danger');
}
})
.catch(function () {
restoreButton();
notyf('网络错误,请重试', 'danger');
})
.then(function () { clearToast('aksrt-after-sale-submit'); });
};
runCaptcha('after_sale_apply').then(sendApply).catch(function (error) {
restoreButton();
clearToast('aksrt-after-sale-submit');
if (error && error.cancelled) return;
notyf('人机验证失败,请重试', 'danger');
});
}
// 撤销售后:待审核工单置为已取消(DELETE 售后工单),成功后刷新订单列表与统计。
function submitWithdraw(shell, button) {
var orderNumber = button.getAttribute('data-aksrt-after-sale-withdraw-confirm') || '';
if (!orderNumber || button.disabled) return;
button.disabled = true;
notyf('正在撤销售后申请…', 'load', 3000, 'aksrt-after-sale-withdraw');
var restoreButton = function () {
button.disabled = false;
};
var sendWithdraw = function (captchaPayload) {
window.fetch(API_ROOT + '/orders/' + encodeURIComponent(orderNumber) + '/after-sale', {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'X-WP-Nonce': NONCE,
},
body: JSON.stringify(captchaPayload || {}),
})
.then(function (res) { return res.json(); })
.then(function (result) {
if (result && result.success) {
window.AksrtModalRuntime.close('withdraw');
notyf('售后申请已撤销', 'success');
if (window.AKSRTOrderCenter && typeof window.AKSRTOrderCenter.refresh === 'function') {
window.AKSRTOrderCenter.refresh(orderNumber);
}
} else {
restoreButton();
notyf((result && result.message) || '撤销失败,请重试', 'danger');
}
})
.catch(function () {
restoreButton();
notyf('网络错误,请重试', 'danger');
})
.then(function () { clearToast('aksrt-after-sale-withdraw'); });
};
runCaptcha('after_sale_cancel').then(sendWithdraw).catch(function (error) {
restoreButton();
clearToast('aksrt-after-sale-withdraw');
if (error && error.cancelled) return;
notyf('人机验证失败,请重试', 'danger');
});
}
function bindShell(shell) {
if (!shell) return;
shell.addEventListener('click', function (event) {
// 原因徽章:单选 active(与举报弹窗同语义,重复点击已选徽章保持选中)。
var chip = event.target.closest('[data-aksrt-after-sale-reason]');
if (chip) {
event.preventDefault();
Array.prototype.forEach.call(shell.querySelectorAll('[data-aksrt-after-sale-reason].active'), function (el) {
el.classList.remove('active');
});
chip.classList.add('active');
return;
}
var addBtn = event.target.closest('[data-aksrt-after-sale-add]');
if (addBtn) {
event.preventDefault();
var fileInput = shell.querySelector('[data-aksrt-after-sale-file]');
if (fileInput) fileInput.click();
return;
}
var removeBtn = event.target.closest('[data-aksrt-after-sale-image-remove]');
if (removeBtn) {
event.preventDefault();
var item = removeBtn.closest('.preview-item');
if (item) item.remove();
return;
}
var submitBtn = event.target.closest('[data-aksrt-after-sale-submit]');
if (submitBtn) {
event.preventDefault();
submitApply(shell);
return;
}
// 再次申请售后:载荷已随按钮渲染。can_reapply 必然伴随 has_application=true,
// 直接走 openApplyFlow 会被 has_application 分流回「售后详细」打转;
// 清除标记落回申请表单分支(expired 超时效拦截仍然生效)。
var reapplyBtn = event.target.closest('[data-aksrt-after-sale-reapply]');
if (reapplyBtn) {
event.preventDefault();
var payload = parsePayload(reapplyBtn.getAttribute('data-aksrt-after-sale-payload'));
if (payload && payload.after_sale) payload.after_sale.has_application = false;
window.AksrtModalRuntime.close('replace');
openApplyFlow(payload);
return;
}
// 撤销售后:入口在售后详细弹窗状态行(仅待审核),点击先弹二次确认。
var withdrawBtn = event.target.closest('[data-aksrt-after-sale-withdraw]');
if (withdrawBtn) {
event.preventDefault();
openWithdraw(withdrawBtn.getAttribute('data-aksrt-after-sale-withdraw') || '');
return;
}
var withdrawConfirm = event.target.closest('[data-aksrt-after-sale-withdraw-confirm]');
if (withdrawConfirm) {
event.preventDefault();
submitWithdraw(shell, withdrawConfirm);
}
});
shell.addEventListener('change', function (event) {
var fileInput = event.target.closest('[data-aksrt-after-sale-file]');
if (!fileInput || !fileInput.files || !fileInput.files.length) return;
Array.prototype.forEach.call(fileInput.files, function (file) {
uploadImage(shell, file);
});
fileInput.value = '';
});
}
function initAfterSaleButtons() {
if (document.documentElement.dataset.aksrtAfterSaleInitialized === '1') return;
document.documentElement.dataset.aksrtAfterSaleInitialized = '1';
document.addEventListener('click', function (event) {
var apply = event.target.closest('[data-aksrt-after-sale-apply]');
if (apply) {
event.preventDefault();
openApplyFlow(parsePayload(apply.getAttribute('data-aksrt-after-sale-apply')));
return;
}
var detail = event.target.closest('[data-aksrt-after-sale-detail]');
if (detail) {
event.preventDefault();
openDetailFlow(parsePayload(detail.getAttribute('data-aksrt-after-sale-detail')));
}
});
}
jQuery(document).off('auto_fun.aksrt-after-sale').on('auto_fun.aksrt-after-sale', initAfterSaleButtons);
// 委托监听不依赖 DOM 扫描,脚本加载时立即挂上;auto_fun 钩子保留作幂等重扫。
initAfterSaleButtons();
})();
+126 -12
View File
@@ -7,6 +7,7 @@
"use strict";
var instances = new WeakMap();
var liveStates = [];
function request(path, body, method, operationKey) {
var options = { method: method || "POST" };
@@ -74,7 +75,6 @@
var map = {
pending: { icon: "fa-clock", desc: "订单已生成,请在有效期内完成支付" },
paid: { icon: "fa-check-circle", desc: "支付已完成,相关权益已发放至账户" },
refunding: { icon: "fa-hourglass-half", desc: "退款申请已受理,正在处理中" },
refunded: { icon: "fa-undo-alt", desc: "退款已完成,款项将原路退回" },
closed: { icon: "fa-times-circle", desc: "订单已关闭,如有需要可重新下单" },
};
@@ -106,6 +106,14 @@
return "<div><dt>" + escapeHtml(label) + "</dt><dd>" + cell + "</dd></div>";
}
function colorfulHeader(title, icon) {
return '<div class="modal-colorful-header colorful-bg jb-blue">' +
'<button type="button" class="close" data-aksrt-modal-close aria-label="关闭"><svg class="ic-close" aria-hidden="true"><use xlink:href="#icon-close"></use></svg></button>' +
'<div class="colorful-make"></div>' +
'<div class="text-center"><div class="em2x"><i class="fa fas ' + escapeHtml(icon) + '" aria-hidden="true"></i></div>' +
'<div class="mt10 em12 padding-w10">' + escapeHtml(title) + '</div></div></div>';
}
function renderDetail(state, order, session) {
var allowedActions = order.allowed_actions || {};
var reconciliation = order.reconciliation || {};
@@ -116,6 +124,8 @@
var meta = statusMeta(status);
var timeLeft = Number(order.time_left || 0);
var html = "";
// 炫彩标题区:与售后弹窗统一(modal-mini + jb-blue),运行时不再渲染默认标题栏。
html += colorfulHeader("订单详情", "fa-receipt");
// 状态横幅:整块配色只由 --hero-accent 驱动,换状态只改一处变量。
html += '<section class="aksrt-order-detail__hero is-' + escapeHtml(status) + '">';
@@ -145,15 +155,68 @@
html += '<h4 class="aksrt-order-detail__group-title">交易信息</h4>';
html += '<dl class="aksrt-order-detail__list">';
[
["流水号", order.order_number],
["订单号", order.order_number],
["支付方式", paymentMethodText(order)],
["创建时间", order.created_at],
["支付时间", order.paid_at],
].forEach(function (row) {
html += detailRow(row[0], row[1], row[0] === "流水号");
html += detailRow(row[0], row[1], row[0] === "订单号");
});
html += "</dl></section>";
var afterSales = Array.isArray(order.after_sales) ? order.after_sales : [];
if (afterSales.length) {
html += '<section class="aksrt-order-detail__group"><h4 class="aksrt-order-detail__group-title">售后记录</h4><ul class="aksrt-order-detail__after-sales">';
afterSales.forEach(function (item, index) {
var labels = { pending: "待审核", processing: "处理中", approved: "已通过", rejected: "已拒绝", cancelled: "已取消", completed: "已完成" };
var asStatus = String(item.status || "");
var asLabel = labels[asStatus] || asStatus;
html += '<li class="aksrt-order-detail__after-sale is-' + escapeHtml(asStatus) + '">';
html += '<div class="aksrt-order-detail__after-sale-head"><span class="aksrt-order-detail__after-sale-badge is-' + escapeHtml(asStatus) + '">' + escapeHtml(asLabel) + "</span>";
if (item.created_at) html += "<time>" + escapeHtml(item.created_at) + "</time>";
html += "</div>";
// 原因 / 申请理由:新数据走 apply_form 结构化字段;老数据无 apply_form 时兜底展示融合串。
var applyForm = item.apply_form && typeof item.apply_form === "object" ? item.apply_form : null;
if (applyForm) {
var reasonLabel = String(applyForm.reason_label || "");
var applyDesc = String(applyForm.description || "");
if (reasonLabel !== "") html += '<p class="aksrt-order-detail__after-sale-reason"><span class="aksrt-order-detail__after-sale-field">原因:</span>' + escapeHtml(reasonLabel) + "</p>";
if (applyDesc !== "") html += '<p class="aksrt-order-detail__after-sale-reason"><span class="aksrt-order-detail__after-sale-field">申请理由:</span>' + escapeHtml(applyDesc).replace(/\r\n|\r|\n/g, "<br>") + "</p>";
} else if (item.reason) {
html += '<p class="aksrt-order-detail__after-sale-reason">' + escapeHtml(item.reason).replace(/\r\n|\r|\n/g, "<br>") + "</p>";
}
// 查看详细:逐记录构造售后详细弹窗载荷(after_sale.latest 指向本条记录);
// 再次申请仅对最新一条且订单级可再申请时开放(after_sales 按 id 倒序,首条即最新)。
var recordPayload = escapeHtml(JSON.stringify({
order_number: order.order_number || "",
product_name: order.product_name || "",
amount: order.amount || "0",
is_points: !!order.is_points,
currency_symbol: order.currency_symbol || "",
status_label: order.status_label || "",
after_sale: {
enabled: true,
has_application: true,
can_reapply: index === 0 && !!((order.after_sale || {}).can_reapply),
latest: {
status: asStatus,
reason: String(item.reason || ""),
apply_form: applyForm,
images: Array.isArray(item.images) ? item.images : [],
refund_amount: String(item.refund_amount || "0"),
created_at: String(item.created_at || ""),
handling_log: Array.isArray(item.handling_log) ? item.handling_log : [],
},
},
}));
html += '<div class="aksrt-order-detail__after-sale-foot">';
html += '<button class="btn-ghost" type="button" data-aksrt-after-sale-detail="' + recordPayload + '">查看详细</button>';
html += "</div>";
html += "</li>";
});
html += "</ul></section>";
}
if (reconciliation.state === "refresh_available") {
html += '<div class="aksrt-order-detail__refresh-state" role="status">';
html += '<i class="fa fas fa-info-circle" aria-hidden="true"></i>';
@@ -162,10 +225,17 @@
html += "</div>";
}
html += '<footer class="aksrt-modal__footer aksrt-order-detail__actions">';
html += '<button class="btn-outline" type="button" data-aksrt-modal-close>关闭</button>';
if (allowedActions.can_cancel) html += '<button class="btn-ghost" type="button" data-aksrt-order-cancel="' + escapeHtml(order.order_number) + '">关闭订单</button>';
if (allowedActions.can_pay) html += '<button class="btn-primary" type="button" data-aksrt-order-pay="' + escapeHtml(order.order_number) + '" data-aksrt-order-method="' + escapeHtml(paymentMethod.value || "") + '">继续支付</button>';
// 底部按钮与售后详细弹窗同式:but-average modal-buts;「关闭」与「我知道了」
// 同款(but c-blue),平铺观感由 orders.css 的 footer 按钮规则统一呈现。
html += '<footer class="aksrt-order-detail__actions but-average modal-buts">';
html += '<button type="button" class="but c-blue" data-aksrt-modal-close>关闭</button>';
if (allowedActions.can_cancel) html += '<button type="button" class="but" data-aksrt-order-cancel="' + escapeHtml(order.order_number) + '">关闭订单</button>';
if (allowedActions.can_pay) html += '<button type="button" class="but c-blue" data-aksrt-order-pay="' + escapeHtml(order.order_number) + '" data-aksrt-order-method="' + escapeHtml(paymentMethod.value || "") + '">继续支付</button>';
// 售后入口:未申请时底部保留「申请售后」;已有申请时入口收拢到售后记录卡片的「查看详细」。
var detailAfterSale = order.after_sale || {};
if (detailAfterSale.enabled && !detailAfterSale.has_application) {
html += '<button type="button" class="but c-blue" data-aksrt-after-sale-apply="' + afterSalePayload(order) + '">申请售后</button>';
}
html += "</footer>";
session.content.innerHTML = html;
@@ -179,7 +249,6 @@
method: refresh ? "POST" : "GET",
headers: actionKey ? { "Idempotency-Key": state.operationKeys[actionKey] } : {},
kind: "orders",
title: "订单详情",
className: "aksrt-order-detail-modal",
responseType: "json",
cache: !refresh,
@@ -241,9 +310,40 @@
function actionHtml(order) {
var allowedActions = order.allowed_actions || {};
var afterSale = order.after_sale || {};
var html = '<button class="btn-outline" type="button" data-aksrt-order-detail="' + escapeHtml(order.order_number) + '">订单详情</button>';
if (allowedActions.can_cancel) html += '<button class="btn-ghost" type="button" data-aksrt-order-cancel="' + escapeHtml(order.order_number) + '">关闭订单</button>';
if (allowedActions.can_pay) html += '<button class="btn-primary" type="button" data-aksrt-order-pay="' + escapeHtml(order.order_number) + '" data-aksrt-order-method="' + escapeHtml((order.payment_method || {}).value || "") + '">继续支付</button>';
// 售后入口:申请售后 / 售后详细走 order-after-sale.js 的独立弹窗。
if (afterSale.enabled) html += afterSaleButtons(order);
return html;
}
// 售后按钮(列表卡片与订单详情弹窗共用):已申请 → 售后详细,否则 → 申请售后。
// 载荷与 PHP 模板同构(order-after-sale.js 同步消费),点击即弹窗、无前置请求。
function afterSalePayload(order) {
var afterSale = order.after_sale || {};
return escapeHtml(JSON.stringify({
order_number: order.order_number || "",
product_name: order.product_name || "",
amount: order.amount || "0",
is_points: !!order.is_points,
currency_symbol: order.currency_symbol || "",
status_label: order.status_label || "",
after_sale: afterSale,
}));
}
function afterSaleButtons(order) {
var afterSale = order.after_sale || {};
var html = "";
if (!afterSale.enabled) return html;
var payload = afterSalePayload(order);
if (afterSale.has_application) {
html += '<button class="btn-outline" type="button" data-aksrt-after-sale-detail="' + payload + '">售后详细</button>';
return html;
}
html += '<button class="btn-outline" type="button" data-aksrt-after-sale-apply="' + payload + '">申请售后</button>';
return html;
}
@@ -256,7 +356,7 @@
: "";
return '<article class="aksrt-orders-page__card" data-aksrt-order-card="' + escapeHtml(order.order_number) + '" data-order-status="' + escapeHtml(order.status_group || "") + '">' +
'<div class="aksrt-orders-page__card-rail"><span>' + escapeHtml(order.product_type_label || "") + '</span><time>' + escapeHtml(order.created_at || "") + '</time><span class="aksrt-orders-page__card-status is-' + escapeHtml(order.status_group || "") + '">' + escapeHtml(order.status_label || "") + "</span></div>" +
'<div class="aksrt-orders-page__card-main">' + image + '<div class="aksrt-orders-page__card-copy"><h3>' + escapeHtml(order.product_name || "订单") + "</h3><p>流水号 " + escapeHtml(order.order_number) + "</p></div><div class=\"aksrt-orders-page__card-value\"><strong>" + formatMoney(order) + "</strong><span>实付</span></div></div>" +
'<div class="aksrt-orders-page__card-main">' + image + '<div class="aksrt-orders-page__card-copy"><h3>' + escapeHtml(order.product_name || "订单") + "</h3><p>订单号 " + escapeHtml(order.order_number) + "</p></div><div class=\"aksrt-orders-page__card-value\"><strong>" + formatMoney(order) + "</strong><span>实付</span></div></div>" +
'<footer class="aksrt-orders-page__card-actions">' + countdown + actionHtml(order) + "</footer></article>";
}
@@ -265,7 +365,10 @@
if (!root) return;
root.innerHTML = '<div class="aksrt-orders-page__stat"><span>全部</span><strong>' + Number(stats.total || 0) + "</strong></div>" +
'<div class="aksrt-orders-page__stat"><span>待支付</span><strong class="is-pending">' + Number(stats.pending || 0) + "</strong></div>" +
'<div class="aksrt-orders-page__stat"><span>已支付</span><strong class="is-paid">' + Number(stats.paid || 0) + "</strong></div>";
'<div class="aksrt-orders-page__stat"><span>已支付</span><strong class="is-paid">' + Number(stats.paid || 0) + "</strong></div>" +
'<div class="aksrt-orders-page__stat"><span>已退款</span><strong>' + Number(stats.refunded || 0) + "</strong></div>" +
'<div class="aksrt-orders-page__stat"><span>已关闭</span><strong>' + Number(stats.closed || 0) + "</strong></div>" +
'<div class="aksrt-orders-page__stat"><span>售后待处理</span><strong>' + Number(stats.after_sale_pending || 0) + "</strong></div>";
var pendingCount = state.root.querySelector('[data-aksrt-order-filter="status"][data-aksrt-order-filter-value="pending"] .aksrt-orders-page__filter-count');
if (pendingCount) {
pendingCount.textContent = String(Number(stats.pending || 0));
@@ -428,7 +531,7 @@
if (!value) return;
var icon = button.querySelector("i");
var settle = function (ok) {
notyf(ok ? "流水号已复制" : "复制失败,请手动选择复制", ok ? "success" : "danger");
notyf(ok ? "订单号已复制" : "复制失败,请手动选择复制", ok ? "success" : "danger");
if (!ok) return;
button.classList.add("is-copied");
if (icon) icon.className = "fa fas fa-check";
@@ -533,6 +636,7 @@
state.detailReplacementSession = null;
};
state.onDocumentClick = function (event) {
// 取消/撤销售后已移入售后详细弹窗(order-after-sale.js 的「撤销售后」入口)。
if (!state.detailSession || window.AksrtModalRuntime.getActive() !== state.detailSession.shell) return;
var copy = closest(event.target, "[data-aksrt-order-copy]");
if (copy) {
@@ -565,6 +669,7 @@
document.addEventListener("click", state.onDocumentClick);
state.countdownTimer = window.setInterval(function () { updateCountdowns(state); }, 1000);
instances.set(root, state);
liveStates.push(state);
}
function rootsIn(root) {
@@ -590,9 +695,18 @@
document.removeEventListener("click", state.onDocumentClick);
window.clearInterval(state.countdownTimer);
instances.delete(item);
liveStates = liveStates.filter(function (entry) { return entry !== state; });
});
}
window.AKSRTOrderCenter = { init: init, destroy: destroy };
// refresh 供售后弹窗(order-after-sale.js)提交/取消后刷新列表与统计。
function refresh(orderNumber) {
invalidateOrder(orderNumber);
liveStates.forEach(function (state) {
refreshView(state);
});
}
window.AKSRTOrderCenter = { init: init, destroy: destroy, refresh: refresh };
jQuery(document).off("auto_fun.aksrt-order-center").on("auto_fun.aksrt-order-center", function () { init(document); });
}());
+110 -23
View File
@@ -17,6 +17,14 @@
return el;
}
// Server-side schema sanitization limits these configured rich-text fields to post-safe HTML.
function htmlNode(tag, className, value) {
var el = document.createElement(tag);
if (className) el.className = className;
if (value !== undefined && value !== null) el.innerHTML = text(value);
return el;
}
function imageNode(src, className) {
if (!src) return null;
var image = document.createElement("img");
@@ -61,7 +69,7 @@
if (levelInfo && levelInfo.img_icon) {
var img = node("img", "img-icon");
img.src = text(levelInfo.img_icon);
img.alt = text(levelInfo.name || "");
img.alt = text(levelInfo.name_text || levelInfo.name || "");
img.decoding = "async";
return img;
}
@@ -115,7 +123,8 @@
// Zibll 套餐卡:zib-widget vip-product relative text-center product-box
// (右上 tag + 价格行 + 时长 badge,无标题行)。
var product = node("div", "zib-widget vip-product relative text-center product-box");
if (item.discount_tag) append(product, node("div", "abs-right vip-tag badg jb-yellow", item.discount_tag));
// 标签颜色取后台「标签颜色」配置,未配置时回退参考主题默认的 jb-yellow。
if (item.discount_tag) append(product, htmlNode("div", "abs-right vip-tag badg " + (item.tag_class || "jb-yellow"), item.discount_tag));
append(product, priceRow(mark, item.price_label || "", item.show_price_label || ""));
if (item.days_label) append(product, node("div", "muted-2-color c-blue em09 p2-10 badg radius", item.days_label));
append(label, product);
@@ -179,7 +188,7 @@
box.setAttribute("data-condition", "==");
box.setAttribute("data-value", "card_pass");
if (defaultMethod !== "card_pass") box.style.display = "none";
if (cardPass.desc) append(box, node("div", "muted-box muted-2-color padding-10 mb10 em09", cardPass.desc));
if (cardPass.desc) append(box, htmlNode("div", "muted-box muted-2-color padding-10 mb10 em09", cardPass.desc));
append(box, node("div", "muted-2-color em09 mb6", cardPass.only_password ? "请输入卡密" : "请输入卡号和密码"));
if (!cardPass.only_password) {
var cardInput = node("input", "form-control mb6");
@@ -384,12 +393,7 @@
var avatarItem = node("li");
var avatarWrap = node("div", "avatar-img");
if (member.avatar_url) {
var avatar = node("img");
avatar.src = text(member.avatar_url);
avatar.alt = "";
avatar.width = 42;
avatar.height = 42;
append(avatarWrap, avatar);
append(avatarWrap, imageNode(text(member.avatar_url), "avatar"));
}
append(avatarItem, avatarWrap);
append(userBox, avatarItem);
@@ -403,7 +407,7 @@
} else if (member.state === "expired") {
append(userInfo, node("div", "muted-2-color em09", "已过期"));
} else if (catalog.desc) {
append(userInfo, node("div", "muted-2-color em09", catalog.desc));
append(userInfo, htmlNode("div", "muted-2-color em09", catalog.desc));
}
append(userBox, userInfo);
append(modal, userBox);
@@ -439,8 +443,8 @@
append(miniIcon, vipIcon(levelInfo));
append(miniIconbox, miniIcon);
var miniInfo = node("div", "v-info");
append(miniInfo, node("div", "v-name", actionLabel(action) + (levelInfo.name || group.name || "VIP" + level)));
append(miniInfo, node("div", "v-desc em09", levelInfo.desc || ""));
append(miniInfo, htmlNode("div", "v-name", actionLabel(action) + (levelInfo.name || group.name || "VIP" + level)));
append(miniInfo, htmlNode("div", "v-desc em09", levelInfo.desc || ""));
append(miniIconbox, miniInfo);
append(miniCard, miniIconbox);
append(tab, miniCard);
@@ -466,15 +470,12 @@
append(vicon, vipIcon(levelInfo));
append(iconbox, vicon);
var vinfo = node("div", "v-info");
append(vinfo, node("div", "v-name", levelInfo.name || group.name || "VIP" + level));
append(vinfo, node("div", "v-desc em09", levelInfo.desc || ""));
append(vinfo, htmlNode("div", "v-name", levelInfo.name || group.name || "VIP" + level));
append(vinfo, htmlNode("div", "v-desc em09", levelInfo.desc || ""));
append(iconbox, vinfo);
append(info, iconbox);
if (levelInfo.equity) {
var equity = document.createElement("ul");
equity.className = "v-equity";
equity.innerHTML = text(levelInfo.equity);
append(info, equity);
append(info, htmlNode("div", "", levelInfo.equity));
}
var infoBa = node("div", "abs-center vip-baicon");
append(infoBa, vipIcon(levelInfo));
@@ -497,7 +498,7 @@
+ '<i class="fa fas fa-angle-right em12 ml10" aria-hidden="true"></i>';
append(products, pointsLink);
}
if (catalog.more) append(products, node("div", "muted-2-color mb10 em09 muted-box padding-10 mini-radius", catalog.more));
if (catalog.more) append(products, htmlNode("div", "muted-2-color mb10 em09 muted-box padding-10 mini-radius", catalog.more));
renderMethods(products, methods, cardPass, wallet);
var actionName = actionKey(action);
var actionAllowed = allowed[actionName] === true;
@@ -524,8 +525,6 @@
append(modal, tabs);
append(modal, content);
if (catalog.help_text) append(modal, node("div", "muted-2-color em09 mb6", catalog.help_text));
// 卡密渠道初始显隐(选中 card_pass 时隐藏套餐、显示卡密输入盒)。
modal.querySelectorAll(".aksrt-vip-pane").forEach(function (pane) {
applyVipMethodVisibility(pane);
@@ -703,6 +702,46 @@
});
}
// 骨架屏:仿弹窗真实布局(用户框 + 等级卡 + 双列套餐面板)。
// 线条/色块复用全局骨架类(build/public.min.css),VIP 专属形状见 vip-modal.css。
function vipSkeleton() {
var skeleton = node("div", "aksrt-vip-modal-skeleton");
skeleton.setAttribute("aria-live", "polite");
skeleton.setAttribute("aria-busy", "true");
// 用户框:头像圆 + 两行文字。
var user = node("div", "aksrt-vip-modal-skeleton__user");
append(user, node("span", "aksrt-vip-modal-skeleton__avatar"));
var userLines = node("span", "aksrt-vip-modal-skeleton__lines");
append(userLines, node("span", "aksrt-modal-runtime__skeleton-line is-wide"));
append(userLines, node("span", "aksrt-modal-runtime__skeleton-line is-short"));
append(user, userLines);
append(skeleton, user);
// 等级卡行(vip-cardminis):两张 tab 占位。
var tabs = node("div", "aksrt-vip-modal-skeleton__tabs");
append(tabs, node("span", "aksrt-vip-modal-skeleton__tab"));
append(tabs, node("span", "aksrt-vip-modal-skeleton__tab"));
append(skeleton, tabs);
// 双列面板:左权益信息 + 右套餐区(套餐卡、支付方式行、提交按钮条)。
var pane = node("div", "aksrt-vip-modal-skeleton__pane");
var info = node("div", "aksrt-vip-modal-skeleton__info");
append(info, node("span", "aksrt-modal-runtime__skeleton-line is-wide"));
append(info, node("span", "aksrt-modal-runtime__skeleton-line"));
append(info, node("span", "aksrt-modal-runtime__skeleton-line is-short"));
append(pane, info);
var products = node("div", "aksrt-vip-modal-skeleton__products");
append(products, node("span", "aksrt-modal-runtime__skeleton-block"));
append(products, node("span", "aksrt-modal-runtime__skeleton-block"));
append(products, node("span", "aksrt-modal-runtime__skeleton-line is-short"));
append(products, node("span", "aksrt-vip-modal-skeleton__button"));
append(pane, products);
append(skeleton, pane);
return skeleton.outerHTML;
}
function vipPay(trigger) {
var level = trigger
? parseInt(trigger.getAttribute("vip-level") || trigger.getAttribute("data-vip-level") || "1", 10) || 1
@@ -711,11 +750,11 @@
url: "/pay/vip/modal",
query: { vip_level: level },
className: "aksrt-vip-modal-shell",
skeleton: '<div class="aksrt-vip-modal-panel"><div class="aksrt-vip-modal-content" data-aksrt-vip-modal-body><div class="text-center padding-20 muted-2-color">加载中…</div></div></div>',
skeleton: vipSkeleton(),
telemetry: "vip",
kind: "vip",
responseType: "json",
cacheTtl: 15000,
cacheTtl: 60000,
cacheTags: ["vip"],
renderData: function (data, session) {
renderModalView(session.content, data);
@@ -733,6 +772,53 @@
});
}
var prefetchedLevels = Object.create(null);
// 与 openRemote 同一 cacheKey(method + AksrtRequest.url),命中同一份缓存/flight。
function vipModalCacheKey(level) {
return "GET:" + window.AksrtRequest.url("/pay/vip/modal", { vip_level: level });
}
// 意图预热:悬停/聚焦开通入口时按等级预热弹窗数据(不打开弹窗,失败静默)。
// 点击打开时若预热请求在途,cache.acquire 的 flight 去重会自动合并。
function prefetchVipModal(trigger) {
if (!trigger || !window.AksrtModalCache || !window.AksrtRequest) return;
var level = parseInt(trigger.getAttribute("vip-level") || trigger.getAttribute("data-vip-level") || "1", 10) || 1;
if (level < 1 || level > 2) level = 1;
if (prefetchedLevels[level]) return;
prefetchedLevels[level] = true;
var cache = window.AksrtModalCache;
if (cache.get && cache.get(vipModalCacheKey(level)).hit) return;
try {
cache.acquire(vipModalCacheKey(level), function () {
return window.AksrtRequest.request("/pay/vip/modal", {
method: "GET",
query: { vip_level: level },
});
}, {
ttl: 60000,
tags: ["vip"],
});
} catch (error) {
// 预热失败不影响点击后的正常打开路径。
}
}
function vipTriggerFrom(event) {
var target = event.target;
if (!target || !target.closest) return null;
return target.closest(".pay-vip, a.pay-vip, .float-btn.pay-vip, [data-aksrt-pay-vip]");
}
function bindPrefetch() {
// pointerenter 不冒泡但走捕获阶段;focusin 冒泡,两者互补覆盖鼠标与键盘。
["pointerenter", "focusin"].forEach(function (type) {
document.addEventListener(type, function (event) {
prefetchVipModal(vipTriggerFrom(event));
}, true);
});
}
function onDocClick(event) {
var trigger = event.target.closest(".pay-vip, a.pay-vip, .float-btn.pay-vip, [data-aksrt-pay-vip]");
if (!trigger) return;
@@ -773,6 +859,7 @@
if (window.__aksrtPayVipBound) return;
window.__aksrtPayVipBound = true;
document.addEventListener("click", onDocClick, true);
bindPrefetch();
bindPointsModal();
bindPointsEntry();
}
+219 -95
View File
@@ -1,127 +1,251 @@
/**
* On-site wallet tipping modal.
* On-site wallet tipping content for the shared modal runtime.
*
* @package AKSRT
*/
;(function () {
;(function (window, document) {
'use strict';
var cfg = window.aksrtData || {};
var titleSequence = 0;
var toastId = 'aksrt-reward-toast';
function initReward() {
var activeTrigger = null;
function escapeHtml(value) {
return String(value == null ? '' : value).replace(/[&<>"']/g, function (character) {
return {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;',
}[character];
});
}
function operationKey(modal) {
if (!modal.dataset.aksrtOperationKey) {
modal.dataset.aksrtOperationKey = window.AksrtRequest.createIdempotencyKey();
}
return modal.dataset.aksrtOperationKey;
function notify(message, type, time) {
if (typeof window.notyf === 'function') {
window.notyf(escapeHtml(message), type || 'info', time, toastId);
}
}
function rewardModal(target) {
return target.closest('.aksrt-modal-runtime.aksrt-reward-modal');
}
function rewardTemplate(modal) {
return modal ? document.getElementById(modal.dataset.templateId || '') : null;
}
function operationKey(template) {
if (!template.dataset.aksrtOperationKey) {
template.dataset.aksrtOperationKey = window.AksrtRequest.createIdempotencyKey();
}
return template.dataset.aksrtOperationKey;
}
function runCaptcha(modal) {
var mount = modal.querySelector('[data-aksrt-captcha-config][data-scene="tip_user"]');
if (!mount) return Promise.resolve({});
if (!window.AKSRTCaptcha || typeof window.AKSRTCaptcha.run !== 'function') {
return Promise.reject(new Error('人机验证正在加载,请稍后重试'));
}
function closeAll() {
document.querySelectorAll('.reward-modal.is-open').forEach(function (modal) {
modal.classList.remove('is-open');
modal.setAttribute('aria-hidden', 'true');
return window.AKSRTCaptcha.run('tip_user', modal).then(function (payload) {
if (!payload) return null;
if (!payload.aksrt_captcha_proof) throw new Error('人机验证凭证未就绪,请重试');
return payload;
});
}
function setSubmitting(modal, submit, submitting, succeeded) {
submit.disabled = submitting || succeeded;
submit.textContent = submitting ? '验证中…' : '余额赞赏';
modal.querySelectorAll('[data-reward-amount], [data-aksrt-reward-custom]').forEach(function (control) {
control.disabled = submitting || succeeded;
});
}
function openReward(trigger) {
var runtime = window.AksrtModalRuntime;
if (!runtime || !window.AksrtRequest) {
if (trigger.dataset.aksrtRewardWaiting !== '1') {
trigger.dataset.aksrtRewardWaiting = '1';
document.addEventListener('aksrt:modal-runtime:ready', function () {
delete trigger.dataset.aksrtRewardWaiting;
if (document.contains(trigger)) openReward(trigger);
}, { once: true });
}
return;
}
var templateId = trigger.getAttribute('data-template-id') || '';
var template = document.getElementById(templateId);
if (!template || !template.content) return;
var shell = runtime.open({
html: template.innerHTML,
className: 'aksrt-reward-modal modal-mini full-sm',
mobileBottom: true,
kind: 'generic',
});
shell.dataset.templateId = template.id;
shell.dataset.userId = trigger.getAttribute('data-user-id') || template.dataset.userId || '';
shell.dataset.postId = trigger.getAttribute('data-post-id') || template.dataset.postId || '0';
var title = shell.querySelector('.aksrt-reward__title');
if (title) {
title.id = 'aksrt-reward-title-' + (++titleSequence);
shell.setAttribute('aria-labelledby', title.id);
}
var initialFocus = shell.querySelector('[data-reward-amount].is-active, [data-aksrt-reward-custom]');
if (initialFocus) initialFocus.focus();
}
document.addEventListener('click', function (event) {
var opener = event.target.closest('[data-aksrt-reward-open]');
if (opener) {
event.preventDefault();
openReward(opener);
return;
}
var amountButton = event.target.closest('[data-reward-amount]');
if (amountButton) {
var amountModal = rewardModal(amountButton);
if (!amountModal) return;
var amountTemplate = rewardTemplate(amountModal);
if (amountTemplate) delete amountTemplate.dataset.aksrtOperationKey;
amountModal.querySelectorAll('[data-reward-amount]').forEach(function (button) {
var isActive = button === amountButton;
button.classList.toggle('is-active', isActive);
button.setAttribute('aria-pressed', isActive ? 'true' : 'false');
});
document.body.style.overflow = '';
if (activeTrigger) activeTrigger.focus();
activeTrigger = null;
var customInput = amountModal.querySelector('[data-aksrt-reward-custom]');
if (customInput) customInput.value = '';
return;
}
function open(trigger) {
var userId = trigger.getAttribute('data-user-id') || '';
var modal = document.getElementById('reward-modal-' + userId);
if (!modal) return;
closeAll();
activeTrigger = trigger;
modal.dataset.postId = trigger.getAttribute('data-post-id') || modal.dataset.postId || '0';
modal.classList.add('is-open');
modal.setAttribute('aria-hidden', 'false');
document.body.style.overflow = 'hidden';
var first = modal.querySelector('[data-reward-amount], [data-aksrt-reward-custom]');
if (first) first.focus();
var submit = event.target.closest('[data-aksrt-reward-submit]');
if (!submit || submit.disabled) return;
var modal = rewardModal(submit);
var template = rewardTemplate(modal);
var runtime = window.AksrtModalRuntime;
if (!modal || !template || !runtime) return;
var message = modal.querySelector('[data-aksrt-reward-message]');
var custom = modal.querySelector('[data-aksrt-reward-custom]');
var selected = modal.querySelector('[data-reward-amount].is-active');
var amount = custom && custom.value ? custom.value : (selected ? selected.dataset.rewardAmount : '');
if (!amount) {
if (message) message.textContent = '请选择或输入赞赏金额';
notify('请选择或输入赞赏金额', 'warning');
return;
}
if (custom && custom.value && !custom.checkValidity()) {
custom.reportValidity();
return;
}
var key;
try {
key = operationKey(template);
} catch (error) {
if (message) message.textContent = error.message || '无法创建安全请求,请稍后重试';
notify(error.message || '无法创建安全请求,请稍后重试', 'danger');
return;
}
document.addEventListener('click', function (event) {
var opener = event.target.closest('[data-aksrt-reward-open]');
if (opener) {
event.preventDefault();
open(opener);
return;
}
setSubmitting(modal, submit, true, false);
if (message) message.textContent = '正在进行人机验证…';
if (event.target.closest('[data-aksrt-reward-close]')) {
event.preventDefault();
closeAll();
return;
}
var body = new URLSearchParams();
body.set('action', 'aksrt_submit_tip');
body.set('nonce', cfg.nonce || '');
body.set('recipient_id', modal.dataset.userId || '');
body.set('post_id', modal.dataset.postId || '0');
body.set('amount', amount);
var succeeded = false;
var amountButton = event.target.closest('[data-reward-amount]');
if (amountButton) {
var modal = amountButton.closest('.reward-modal');
delete modal.dataset.aksrtOperationKey;
modal.querySelectorAll('[data-reward-amount]').forEach(function (button) {
button.classList.toggle('is-active', button === amountButton);
});
var custom = modal.querySelector('[data-aksrt-reward-custom]');
if (custom) custom.value = '';
return;
runCaptcha(modal).then(function (captchaPayload) {
if (!captchaPayload) {
if (window.AKSRTCaptcha && typeof window.AKSRTCaptcha.reset === 'function') {
window.AKSRTCaptcha.reset('tip_user', modal);
}
return { cancelled: true };
}
var submit = event.target.closest('[data-aksrt-reward-submit]');
if (!submit || submit.disabled) return;
var tipModal = submit.closest('.reward-modal');
var message = tipModal.querySelector('[data-aksrt-reward-message]');
var customInput = tipModal.querySelector('[data-aksrt-reward-custom]');
var selected = tipModal.querySelector('[data-reward-amount].is-active');
var amount = customInput && customInput.value ? customInput.value : (selected ? selected.dataset.rewardAmount : '');
if (customInput && customInput.dataset.aksrtIdempotencyBound !== '1') {
customInput.dataset.aksrtIdempotencyBound = '1';
customInput.addEventListener('input', function () { delete tipModal.dataset.aksrtOperationKey; });
if (runtime.getActive() !== modal) {
if (window.AKSRTCaptcha && typeof window.AKSRTCaptcha.reset === 'function') {
window.AKSRTCaptcha.reset('tip_user', modal);
}
return { cancelled: true };
}
submit.disabled = true;
Object.keys(captchaPayload).forEach(function (name) {
var value = captchaPayload[name];
if (value != null && typeof value !== 'object') body.set(name, String(value));
});
submit.textContent = '赞赏中…';
if (message) message.textContent = '';
var body = new URLSearchParams();
body.set('action', 'aksrt_submit_tip');
body.set('nonce', cfg.nonce || '');
body.set('recipient_id', tipModal.dataset.userId || '');
body.set('post_id', tipModal.dataset.postId || '0');
body.set('amount', amount);
fetch(cfg.ajaxUrl || '/wp-admin/admin-ajax.php', {
if (message) message.textContent = '正在提交赞赏…';
notify('正在提交赞赏…', 'load', '');
return window.fetch(cfg.ajaxUrl || '/wp-admin/admin-ajax.php', {
method: 'POST',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
'Idempotency-Key': operationKey(tipModal)
'Idempotency-Key': key,
},
body: body.toString()
body: body.toString(),
}).then(function (response) {
return response.json();
}).then(function (response) {
var text = response && response.data && response.data.message ? response.data.message : '赞赏失败';
if (response && response.success) {
delete tipModal.dataset.aksrtOperationKey;
if (message) message.textContent = text;
setTimeout(closeAll, 900);
} else if (message) {
message.textContent = text;
}
}).catch(function () {
if (message) message.textContent = '网络错误,请稍后重试';
}).finally(function () {
submit.disabled = false;
submit.textContent = '余额赞赏';
});
}).then(function (response) {
if (runtime.getActive() !== modal) return;
if (response && response.cancelled) {
if (message) message.textContent = '请完成人机验证后再试';
notify('请完成人机验证后再试', 'warning', 5);
return;
}
var text = response && response.data && response.data.message ? response.data.message : '赞赏失败';
if (response && response.success) {
succeeded = true;
delete template.dataset.aksrtOperationKey;
if (message) message.textContent = text;
notify(text, 'success', 5);
window.setTimeout(function () {
if (runtime.getActive() === modal) runtime.close('success');
}, 900);
} else {
if (message) message.textContent = text;
notify(text, 'danger', 5);
if (window.AKSRTCaptcha && typeof window.AKSRTCaptcha.reset === 'function') {
window.AKSRTCaptcha.reset('tip_user', modal);
}
}
}).catch(function (error) {
if (runtime.getActive() !== modal) return;
var errorMessage = error && error.message ? error.message : '网络错误,请稍后重试';
if (message) message.textContent = errorMessage;
notify(errorMessage, 'danger', 5);
if (window.AKSRTCaptcha && typeof window.AKSRTCaptcha.reset === 'function') {
window.AKSRTCaptcha.reset('tip_user', modal);
}
}).finally(function () {
setSubmitting(modal, submit, false, succeeded);
});
});
document.addEventListener('keydown', function (event) {
if (event.key === 'Escape') closeAll();
document.addEventListener('input', function (event) {
if (!event.target.matches('[data-aksrt-reward-custom]')) return;
var modal = rewardModal(event.target);
var template = rewardTemplate(modal);
if (template) delete template.dataset.aksrtOperationKey;
if (!modal) return;
var hasCustomAmount = event.target.value !== '';
var firstAmount = modal.querySelector('[data-reward-amount]');
modal.querySelectorAll('[data-reward-amount]').forEach(function (button) {
var isActive = hasCustomAmount ? false : button === firstAmount;
button.classList.toggle('is-active', isActive);
button.setAttribute('aria-pressed', isActive ? 'true' : 'false');
});
}
jQuery(document).off('auto_fun.aksrt-reward').on('auto_fun.aksrt-reward', initReward);
})();
});
})(window, document);
+203 -353
View File
@@ -1,364 +1,235 @@
(function (window, document) {
'use strict';
function parseConfig(root) {
function configFor(root) {
try { return JSON.parse(root.getAttribute('data-identity-config') || '{}'); } catch (_) { return {}; }
}
function uuid() {
if (window.crypto && crypto.randomUUID) return crypto.randomUUID();
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
var r = Math.random() * 16 | 0;
return (c === 'x' ? r : (r & 3 | 8)).toString(16);
});
}
function request(path, config, options) {
options = Object.assign({}, options || {});
options.credentials = 'same-origin';
options.headers = Object.assign({ 'X-WP-Nonce': config.nonce || '' }, options.headers || {});
return window.AksrtRequest.request(path, options).then(function (response) {
return response.data !== undefined ? response.data : response.json;
});
function parseResponse(response) {
var payload = response && (response.data !== undefined ? response.data : response.json);
return payload && payload.data !== undefined ? payload.data : payload;
}
function errorMessage(error, fallback) {
if (error && error.json) {
return error.json.message || (error.json.data && error.json.data.message) || fallback;
}
if (error && error.json) return error.json.message || fallback;
return error && error.message ? error.message : fallback;
}
function loadScript(src) {
return new Promise(function (resolve, reject) {
var existing = document.querySelector('script[src="' + src + '"]');
if (existing) {
if (existing.dataset.loaded === '1') return resolve();
existing.addEventListener('load', resolve, { once: true });
existing.addEventListener('error', reject, { once: true });
return;
}
var script = document.createElement('script');
script.src = src;
script.async = true;
script.onload = function () { script.dataset.loaded = '1'; resolve(); };
script.onerror = reject;
document.head.appendChild(script);
});
}
function aliyunMetaInfo(config) {
if (config.provider !== 'aliyun') return Promise.resolve('');
return loadScript('https://o.alicdn.com/yd-cloudauth/cloudauth-cdn/jsvm_all.js').then(function () {
if (typeof window.getMetaInfo !== 'function') throw new Error('阿里云 MetaInfo 组件未加载成功');
var meta = window.getMetaInfo();
return typeof meta === 'string' ? meta : JSON.stringify(meta || {});
});
function toast(message, type, time, id) {
if (message && typeof window.notyf === 'function') window.notyf(message, type || 'success', time, id);
}
function init(root) {
if (!root || root.dataset.identityBound === '1') return;
root.dataset.identityBound = '1';
var config = parseConfig(root);
var config = configFor(root);
var template = root.querySelector('[data-auth-modal-template]');
var runtime = window.AksrtModalRuntime;
if (!template) return;
var activeShell = null;
var currentUuid = config.currentUuid || '';
var currentStage = config.currentStage || '';
var pollTimer = null;
var pollBackoff = 0;
var currentController = null;
var refreshController = null;
var refreshTimeout = null;
var refreshInFlight = false;
var openSequence = 0;
var pendingCreateKey = '';
var pendingCreateSignature = '';
var pendingRefreshKey = '';
var pendingManualKey = '';
var pendingManualSignature = '';
function isOpen(shell) {
return !!shell && shell.classList.contains('is-open') && document.contains(shell);
}
function stopPolling() {
if (pollTimer) window.clearTimeout(pollTimer);
pollTimer = null;
}
function abortCurrent() {
if (currentController) {
try { currentController.abort(); } catch (_) {}
}
currentController = null;
}
function abortRefresh() {
if (refreshTimeout) window.clearTimeout(refreshTimeout);
refreshTimeout = null;
if (refreshController) {
try { refreshController.abort(); } catch (_) {}
}
refreshController = null;
refreshInFlight = false;
}
function setMessage(shell, message) {
var node = shell && shell.querySelector('[data-auth-status-message]');
if (node) node.textContent = message || '';
}
function updateSummary(data) {
if (!data || data.status !== 'verified') return;
var trigger = root.querySelector('[data-auth-modal-open]');
if (trigger) {
trigger.disabled = true;
trigger.textContent = '已认证';
}
runtime.invalidate(['identity']);
document.dispatchEvent(new CustomEvent('aksrt:identity:changed', { detail: { status: data.status } }));
}
function schedulePoll(delay) {
stopPolling();
if (!currentUuid || !isOpen(activeShell) || document.visibilityState !== 'visible') return;
var base = Math.max(5, Number(delay || config.pollInterval || 5));
var seconds = Math.min(60, base * Math.pow(2, Math.min(4, pollBackoff)));
pollTimer = window.setTimeout(function () {
pollTimer = null;
refresh(false);
}, seconds * 1000);
}
function render(data, shell) {
if (!shell || shell !== activeShell || !isOpen(shell)) return;
data = data || {};
var previousStage = currentStage;
currentUuid = data.uuid || currentUuid;
currentStage = data.stage || currentStage;
if (data.stage && data.stage !== previousStage) runtime.invalidate(['identity']);
var lock = ['created', 'provider_pending', 'provider_processing', 'manual_pending'].indexOf(currentStage) !== -1;
var retryable = data.next_action === 'manual_or_retry';
var statusBox = shell.querySelector('[data-auth-status]');
var createForm = shell.querySelector('[data-auth-create]');
var stageNode = shell.querySelector('[data-auth-stage]');
var messageNode = shell.querySelector('[data-auth-status-message]');
var continueLink = shell.querySelector('[data-auth-continue]');
var manualButton = shell.querySelector('[data-auth-manual-open]');
if (statusBox) statusBox.hidden = !data.uuid && !retryable;
if (createForm) createForm.hidden = lock || data.status === 'verified';
if (stageNode) stageNode.textContent = data.stage || data.status || '准备认证';
if (messageNode) messageNode.textContent = data.message || '认证结果以站点服务端主动查询为准。';
if (continueLink) {
var canContinue = data.next_action === 'poll' && data.redirect_url;
continueLink.hidden = !canContinue;
if (canContinue) continueLink.href = data.redirect_url;
}
if (manualButton) manualButton.hidden = !retryable;
if (data.status === 'verified' || data.next_action !== 'poll') {
pollBackoff = 0;
stopPolling();
} else if (isOpen(shell) && document.visibilityState === 'visible') {
schedulePoll(data.poll_interval || config.pollInterval);
}
updateSummary(data);
}
function refresh(explicit) {
if (!currentUuid || !isOpen(activeShell) || document.visibilityState !== 'visible' || refreshInFlight) {
return Promise.resolve(null);
}
stopPolling();
abortRefresh();
refreshInFlight = true;
refreshController = window.AbortController ? new window.AbortController() : null;
var timedOut = false;
var shell = activeShell;
var timeoutMs = Math.max(5000, Number(config.refreshTimeout || 15000));
if (!pendingRefreshKey) pendingRefreshKey = uuid();
refreshTimeout = window.setTimeout(function () {
timedOut = true;
if (refreshController) {
try { refreshController.abort(); } catch (_) {}
}
}, timeoutMs);
return request(config.restBase + '/' + encodeURIComponent(currentUuid) + '/refresh', config, {
method: 'POST',
headers: { 'Idempotency-Key': pendingRefreshKey },
signal: refreshController ? refreshController.signal : undefined,
}).then(function (data) {
pendingRefreshKey = '';
if (shell !== activeShell || !isOpen(shell)) return data;
pollBackoff = explicit ? 0 : Math.min(4, pollBackoff + 1);
render(data, shell);
return data;
}).catch(function (error) {
var aborted = error && (error.name === 'AbortError' || error.code === 'ABORT_ERR');
if (shell !== activeShell || !isOpen(shell) || (aborted && !timedOut)) return null;
setMessage(shell, timedOut ? '云端刷新超时,请稍后重试。' : errorMessage(error, '刷新失败,请稍后重试。'));
pollBackoff = Math.min(4, pollBackoff + 1);
schedulePoll(config.pollInterval || 5);
return null;
}).finally(function () {
if (refreshTimeout) window.clearTimeout(refreshTimeout);
refreshTimeout = null;
refreshController = null;
refreshInFlight = false;
});
}
function loadCurrent(shell, sequence) {
abortCurrent();
currentController = window.AbortController ? new window.AbortController() : null;
var controller = currentController;
return request(config.restBase + '/current', config, {
method: 'GET',
signal: controller ? controller.signal : undefined,
}).then(function (snapshot) {
if (sequence !== openSequence || shell !== activeShell || !isOpen(shell)) return;
var data = snapshot && (snapshot.current || snapshot.active || snapshot.latest);
if (!data) {
currentUuid = '';
currentStage = '';
var statusBox = shell.querySelector('[data-auth-status]');
var createForm = shell.querySelector('[data-auth-create]');
if (statusBox) statusBox.hidden = true;
if (createForm) createForm.hidden = false;
stopPolling();
return;
}
render(data, shell);
}).catch(function (error) {
var aborted = error && (error.name === 'AbortError' || error.code === 'ABORT_ERR');
if (aborted || sequence !== openSequence || shell !== activeShell || !isOpen(shell)) return;
setMessage(shell, errorMessage(error, '状态加载失败,请稍后重试。'));
}).finally(function () {
if (currentController === controller) currentController = null;
});
}
function bindForm(shell) {
var createForm = shell.querySelector('[data-auth-create]');
var manualForm = shell.querySelector('[data-auth-manual]');
var statusBox = shell.querySelector('[data-auth-status]');
var refreshButton = shell.querySelector('[data-auth-refresh]');
var manualButton = shell.querySelector('[data-auth-manual-open]');
var typeSelect = createForm && createForm.querySelector('[name="type"]');
var enterprise = createForm && createForm.querySelector('[data-enterprise-fields]');
var personLabel = createForm && createForm.querySelector('[data-auth-person-label]');
var idLabel = createForm && createForm.querySelector('[data-auth-id-label]');
function syncEnterprise() {
var enabled = typeSelect && typeSelect.value === 'enterprise';
if (enterprise) enterprise.hidden = !enabled;
if (enterprise) enterprise.querySelectorAll('input').forEach(function (input) { input.required = enabled; });
if (personLabel) personLabel.textContent = enabled ? '法定代表人姓名' : '真实姓名';
if (idLabel) idLabel.textContent = enabled ? '法定代表人身份证号' : '身份证号码';
}
if (typeSelect) {
typeSelect.addEventListener('change', syncEnterprise);
syncEnterprise();
}
if (refreshButton) refreshButton.addEventListener('click', function () { refresh(true); });
if (manualButton) manualButton.addEventListener('click', function () { if (manualForm) manualForm.hidden = false; });
if (createForm) createForm.addEventListener('submit', function (event) {
event.preventDefault();
var button = createForm.querySelector('[type="submit"]');
var message = createForm.querySelector('[data-auth-message]');
var form = new FormData(createForm);
var payload = {};
form.forEach(function (value, key) { payload[key] = value; });
payload.consent = payload.consent === '1';
var signature = JSON.stringify(payload);
if (!pendingCreateKey || pendingCreateSignature !== signature) {
pendingCreateKey = uuid();
pendingCreateSignature = signature;
}
if (button) button.disabled = true;
if (message) message.textContent = '';
aliyunMetaInfo(config).then(function (metaInfo) {
if (metaInfo) payload.meta_info = metaInfo;
return request(config.restBase, config, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Idempotency-Key': pendingCreateKey },
body: JSON.stringify(payload),
});
}).then(function (data) {
pendingCreateKey = '';
pendingCreateSignature = '';
render(data, shell);
}).catch(function (error) {
if (message) message.textContent = errorMessage(error, '创建认证会话失败,请重试。');
}).finally(function () {
if (button) button.disabled = false;
});
});
if (manualForm) manualForm.addEventListener('submit', function (event) {
event.preventDefault();
if (!currentUuid) return;
var files = manualForm.querySelector('[name="evidence[]"]');
if (files && files.files.length > 3) {
setMessage(shell, '证明材料最多上传 3 张');
return;
}
var button = manualForm.querySelector('[type="submit"]');
var body = new FormData();
Array.prototype.forEach.call((files && files.files) || [], function (file) { body.append('evidence[]', file); });
var signature = Array.prototype.map.call((files && files.files) || [], function (file) {
return [file.name, file.size, file.lastModified].join(':');
}).join('|');
if (!pendingManualKey || pendingManualSignature !== signature) {
pendingManualKey = uuid();
pendingManualSignature = signature;
}
if (button) button.disabled = true;
request(config.restBase + '/' + encodeURIComponent(currentUuid) + '/manual', config, {
method: 'POST',
headers: { 'Idempotency-Key': pendingManualKey },
body: body,
}).then(function (data) {
pendingManualKey = '';
pendingManualSignature = '';
manualForm.hidden = true;
render(data, shell);
}).catch(function (error) {
setMessage(shell, errorMessage(error, '人工审核提交失败,请重试。'));
}).finally(function () {
if (button) button.disabled = false;
});
});
if (statusBox && statusBox.hidden && createForm) createForm.hidden = false;
}
if (!template || !runtime || !window.AksrtRequest) return;
function open() {
stopPolling();
abortCurrent();
abortRefresh();
openSequence += 1;
var sequence = openSequence;
var shell = runtime.open({
runtime.open({
html: template.innerHTML,
height: 320,
kind: 'identity',
className: 'aksrt-identity-modal-shell',
mobileBottom: true,
onContent: function (_, session) {
activeShell = session.shell;
bindForm(session.shell);
var shell = session && session.shell;
var form = shell && shell.querySelector('[data-auth-create]');
var input = form && form.querySelector('[data-auth-evidence]');
var preview = form && form.querySelector('[data-auth-preview]');
if (!form || !input || !preview) return;
shell.setAttribute('aria-labelledby', 'uc-auth-modal-title');
var selectedFiles = [];
function renderPreview() {
preview.replaceChildren();
selectedFiles.forEach(function (entry, index) {
var item = document.createElement('div');
item.className = 'preview-item';
var image = document.createElement('img');
image.className = 'fit-cover';
image.src = entry.url;
image.alt = entry.file.name;
var remove = document.createElement('button');
remove.type = 'button';
remove.className = 'uc-auth-preview-remove';
remove.dataset.authRemove = String(index);
remove.setAttribute('aria-label', '移除 ' + entry.file.name);
remove.textContent = '×';
item.appendChild(image);
item.appendChild(remove);
preview.appendChild(item);
});
if (selectedFiles.length < 3) {
var add = document.createElement('button');
add.type = 'button';
add.className = 'add';
add.dataset.authFilePicker = '';
add.setAttribute('aria-label', '选择证明材料图片');
preview.appendChild(add);
}
}
function releasePreviewUrls() {
selectedFiles.forEach(function (entry) { window.URL.revokeObjectURL(entry.url); });
selectedFiles = [];
}
function onModalClose(event) {
if (!event.detail || event.detail.shell !== shell) return;
document.removeEventListener('aksrt:modal:close', onModalClose);
releasePreviewUrls();
}
document.addEventListener('aksrt:modal:close', onModalClose);
preview.addEventListener('click', function (event) {
var remove = event.target.closest('[data-auth-remove]');
if (remove) {
var index = Number(remove.dataset.authRemove);
var entry = selectedFiles[index];
if (entry) window.URL.revokeObjectURL(entry.url);
selectedFiles.splice(index, 1);
renderPreview();
return;
}
if (event.target.closest('[data-auth-file-picker]')) input.click();
});
input.addEventListener('change', function () {
var files = Array.prototype.slice.call(input.files || []);
input.value = '';
if (!files.length) return;
var unsupported = files.some(function (file) { return !/^image\/(gif|jpe?g|png|webp)$/i.test(file.type); });
if (unsupported) {
toast('证明材料仅支持 JPG、PNG、GIF 或 WebP 图片', 'danger');
return;
}
if (files.some(function (file) { return file.size > 2 * 1024 * 1024; })) {
toast('单张证明材料不能超过 2MB', 'danger');
return;
}
var uniqueFiles = files.filter(function (file, index) {
var key = [file.name, file.size, file.lastModified].join(':');
return !selectedFiles.some(function (entry) { return entry.key === key; }) &&
files.findIndex(function (candidate) {
return [candidate.name, candidate.size, candidate.lastModified].join(':') === key;
}) === index;
});
if (selectedFiles.length + uniqueFiles.length > 3) {
toast('证明材料最多上传 3 张', 'danger');
return;
}
uniqueFiles.forEach(function (file) {
selectedFiles.push({
file: file,
key: [file.name, file.size, file.lastModified].join(':'),
url: window.URL.createObjectURL(file),
});
});
renderPreview();
});
var header = shell.querySelector('.uc-auth-modal__header');
if (header) {
var touchStartY = null;
header.addEventListener('touchstart', function (event) {
touchStartY = event.touches.length ? event.touches[0].clientY : null;
}, { passive: true });
header.addEventListener('touchend', function (event) {
var touchEndY = event.changedTouches.length ? event.changedTouches[0].clientY : null;
if (touchStartY !== null && touchEndY !== null && touchEndY - touchStartY > 60 &&
runtime.getActive && runtime.getActive() === shell) {
runtime.close('swipe');
}
touchStartY = null;
}, { passive: true });
}
form.addEventListener('submit', function (event) {
event.preventDefault();
var invalidField = form.querySelector(':invalid');
if (!form.checkValidity()) {
toast((invalidField && invalidField.validationMessage) || '请检查申请信息是否填写完整', 'danger');
if (invalidField && typeof invalidField.focus === 'function') invalidField.focus();
return;
}
var button = form.querySelector('[type="submit"]');
var formData = new FormData(form);
selectedFiles.forEach(function (entry) {
formData.append('evidence[]', entry.file, entry.file.name);
});
var submitLabel = button ? button.innerHTML : '';
if (button) {
button.disabled = true;
button.innerHTML = '<i class="fa fas fa-spinner fa-spin mr6" aria-hidden="true"></i>正在提交…';
}
toast('正在提交申请…', 'load', '', 'aksrt_identity_submit');
window.AksrtRequest.request(config.restBase, {
method: 'POST',
credentials: 'same-origin',
headers: { 'X-WP-Nonce': config.nonce || '' },
body: formData,
}).then(function (response) {
var result = parseResponse(response);
if (!result || result.success === false) throw new Error((result && result.message) || '申请提交失败,请稍后重试');
toast('申请已提交,请耐心等待管理员审核。', 'success', '', 'aksrt_identity_submit');
var summary = root.querySelector('[data-auth-summary]');
if (summary) {
summary.replaceChildren();
var pending = document.createElement('div');
pending.className = 'c-red mb10';
pending.textContent = '您的申请正在审核中,请耐心等待';
var name = document.createElement('div');
name.textContent = result.name || '';
summary.appendChild(pending);
summary.appendChild(name);
if (result.description) {
var description = document.createElement('div');
description.className = 'muted-2-color mt10';
description.textContent = result.description;
summary.appendChild(description);
}
if (result.message) {
var applicationMessage = document.createElement('div');
applicationMessage.className = 'muted-2-color mt10';
applicationMessage.textContent = '申请说明:' + result.message;
summary.appendChild(applicationMessage);
}
(Array.isArray(result.evidence) ? result.evidence : []).forEach(function (item) {
if (!item || !item.url || !item.thumbnail_url) return;
var link = document.createElement('a');
link.href = item.url;
link.target = '_blank';
link.rel = 'noopener noreferrer';
var image = document.createElement('img');
image.src = item.thumbnail_url;
image.alt = '身份认证证明材料';
image.width = 64;
image.height = 64;
image.style.objectFit = 'cover';
image.style.borderRadius = '4px';
link.appendChild(image);
summary.appendChild(link);
});
}
if (runtime.getActive && runtime.getActive() === shell) runtime.close('submit');
}).catch(function (error) {
toast(errorMessage(error, '申请提交失败,请稍后重试'), 'danger', '', 'aksrt_identity_submit');
}).finally(function () {
if (button) {
button.disabled = false;
button.innerHTML = submitLabel;
}
});
});
},
});
activeShell = shell;
loadCurrent(shell, sequence);
}
root.querySelectorAll('[data-auth-modal-open]').forEach(function (button) {
@@ -367,27 +238,6 @@
open();
});
});
document.addEventListener('visibilitychange', function () {
if (!isOpen(activeShell)) return;
if (document.visibilityState !== 'visible') {
stopPolling();
abortCurrent();
abortRefresh();
return;
}
if (currentUuid && ['provider_pending', 'provider_processing'].indexOf(currentStage) !== -1) {
schedulePoll(config.pollInterval || 5);
}
});
document.addEventListener('aksrt:modal:close', function (event) {
if (!activeShell || !event.detail || event.detail.shell !== activeShell) return;
stopPolling();
abortCurrent();
abortRefresh();
activeShell = null;
});
}
window.AKSRTUserCenterAuth = { init: init };
+470 -312
View File
@@ -40,6 +40,11 @@
if (window.AKSRTCaptcha) window.AKSRTCaptcha.reset(scene, scope);
}
function setSettingsSubmitLabel(button, text) {
var label = button && button.querySelector('[data-aksrt-settings-submit-label]');
if (label) label.textContent = text;
}
/* ==================== Dashboard 导航 ==================== */
function initDashboardNavigation() {
if (document.documentElement.dataset.aksrtUserCenterNavigationInitialized === 'true') return;
@@ -709,236 +714,493 @@
if (!ajaxUrl || !settingsNonce) return;
root = root || document;
function openModal(id) {
var m = document.getElementById(id);
if (!m) return;
m.hidden = false;
m.classList.add('is-open');
document.body.style.overflow = 'hidden';
var firstInput = m.querySelector('input:not([type=hidden])');
if (firstInput) setTimeout(function() { firstInput.focus(); }, 200);
function openSettingsModal(kind) {
var runtime = window.AksrtModalRuntime;
var template = document.querySelector('template[data-aksrt-settings-modal-template="' + kind + '"]');
if (!runtime || typeof runtime.open !== 'function' || !template) return;
runtime.open({
html: template.innerHTML,
kind: 'account-settings',
className: 'aksrt-user-settings-modal modal-mini full-sm',
mobileBottom: true,
onContent: function (_, session) {
var shell = session && session.shell;
if (!shell) return;
var dialog = shell.querySelector('[data-account-dialog]');
var title = dialog && dialog.querySelector('[data-account-password-title], .modal-colorful-header .text-center > div:last-child');
if (title && title.textContent.trim()) shell.setAttribute('aria-label', title.textContent.trim());
bindDialog(shell);
},
});
}
function closeModal(m) {
if (typeof m === 'string') m = document.getElementById(m);
if (!m) return;
m.classList.remove('is-open');
m.hidden = true;
document.body.style.overflow = '';
var f = m.querySelector('form');
var sceneMap = { 'pwd-modal': 'change_password', 'email-modal': 'change_email', 'phone-modal': 'bind_phone' };
var scene = m.id && sceneMap[m.id] ? sceneMap[m.id] : '';
if (f) {
f.reset();
var bar = document.getElementById('pwd-strength-bar');
var txt = document.getElementById('pwd-strength-text');
if (bar && bar.firstChild) { bar.style.width = '0'; bar.firstChild.className = 'pwd-strength__fill'; }
if (txt) { txt.textContent = ''; txt.className = 'pwd-strength__text'; }
function setStep(dialog, key) {
dialog.querySelectorAll('[data-account-step]').forEach(function (panel) {
panel.classList.toggle('is-active', panel.getAttribute('data-account-step') === key);
});
var panels = Array.prototype.slice.call(dialog.querySelectorAll('[data-account-step]'));
var index = Math.max(0, panels.findIndex(function (panel) { return panel.getAttribute('data-account-step') === key; }));
dialog.querySelectorAll('.account-step-list li').forEach(function (item, itemIndex) {
item.classList.toggle('active', itemIndex === index);
item.classList.toggle('done', itemIndex < index);
});
}
function resetCountdown(button) {
if (!button) return;
if (button.__aksrtCountdownTimer) window.clearInterval(button.__aksrtCountdownTimer);
button.__aksrtCountdownTimer = null;
button.disabled = false;
button.classList.remove('is-counting');
button.textContent = button.getAttribute('data-idle-label') || '发送验证码';
}
function startCountdown(button, seconds) {
var idleText = button.textContent;
var left = Number(seconds || 60);
if (button.__aksrtCountdownTimer) window.clearInterval(button.__aksrtCountdownTimer);
button.disabled = true;
button.classList.add('is-counting');
button.setAttribute('data-idle-label', idleText);
button.textContent = left + 's';
button.__aksrtCountdownTimer = window.setInterval(function () {
left -= 1;
if (left <= 0) { resetCountdown(button); return; }
button.textContent = left + 's';
}, 1000);
}
function settingsRestRoot() {
return String((window.aksrtData && (window.aksrtData.restUrl || window.aksrtData.apiUrl)) || cfg.restUrl || '/wp-json/aksrt/v1').replace(/\/$/, '');
}
function settingsRestNonce() {
return (window.aksrtData && (window.aksrtData.restNonce || window.aksrtData.nonce)) || cfg.restNonce || '';
}
function formHasRequiredValues(form) {
var required = form.querySelectorAll('[required]');
for (var i = 0; i < required.length; i++) {
if (required[i].type === 'checkbox' && !required[i].checked) {
required[i].focus();
notyf(i18n.agreeTerms || '请先同意相关协议', 'warning');
return false;
}
if (required[i].type !== 'checkbox' && !String(required[i].value || '').trim()) {
required[i].focus();
notyf(i18n.fillRequired || '请填写必填项', 'warning');
return false;
}
}
if (scene) resetCaptcha(scene, f || m);
return true;
}
root.querySelectorAll('.security-trigger').forEach(function(btn) {
if (btn.dataset.aksrtSettingsInitialized === 'true') return;
btn.dataset.aksrtSettingsInitialized = 'true';
btn.addEventListener('click', function() { openModal(this.getAttribute('data-modal')); });
});
root.querySelectorAll('[data-close-modal], [data-modal-close]').forEach(function(el) {
if (el.dataset.aksrtSettingsInitialized === 'true') return;
el.dataset.aksrtSettingsInitialized = 'true';
el.addEventListener('click', function() { closeModal(this.closest('.aksrt-modal')); });
});
if (document.documentElement.dataset.aksrtSettingsKeyboardInitialized !== 'true') {
document.documentElement.dataset.aksrtSettingsKeyboardInitialized = 'true';
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
var openM = document.querySelector('.aksrt-modal.is-open');
if (openM) closeModal(openM);
function sendEmailCode(button, form, scene) {
if (!form || button.disabled) return;
if (scene === 'change_email' && !String((form.querySelector('[name="new_email"]') || {}).value || '').trim()) {
notyf('请输入邮箱', 'warning');
return;
}
runCaptcha('change_email', form).then(function (captchaPayload) {
if (captchaPayload === null) return null;
var data = new URLSearchParams();
data.set('action', 'aksrt_send_account_email_code');
data.set('_settings_nonce', settingsNonce);
data.set('scene', scene);
new FormData(form).forEach(function (value, key) { if (value !== '' && !data.has(key)) data.set(key, value); });
Object.keys(captchaPayload || {}).forEach(function (key) { data.set(key, captchaPayload[key]); });
button.disabled = true;
return fetch(ajaxUrl, { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: data.toString() });
}).then(function (response) { return response ? response.json() : null; }).then(function (payload) {
if (!payload) return;
if (!payload.success) throw new Error((payload.data && payload.data.message) || '发送失败');
notyf((payload.data && payload.data.message) || '验证码已发送', 'success');
startCountdown(button, payload.data && payload.data.countdown);
}).catch(function (error) {
resetCountdown(button);
notyf(error.message || '发送失败', 'danger');
}).finally(function () { resetCaptcha('change_email', form); });
}
function sendPhoneCode(button, form, stage) {
if (!form || button.disabled) return;
var payload = { stage: stage };
if (stage === 'verify_old_phone') {
var dialog = form.closest('[data-account-dialog="phone"]');
var boxes = dialog ? phoneDigitBoxes(dialog) : [];
if (boxes.length) {
var hiddenLength = parseInt(dialog.querySelector('[data-aksrt-phone-digits]').getAttribute('data-hidden-length'), 10) || boxes.length;
var middle = phoneDigitsValue(dialog);
if (middle.length !== hiddenLength) { notyf('请先补全手机号缺失的数字', 'warning'); return; }
payload.middle = middle;
}
} else {
var phone = String((form.querySelector('[name="phone"]') || {}).value || '').trim();
if (!phone) { notyf('请输入手机号', 'warning'); return; }
payload.phone = phone;
}
runCaptcha('bind_phone', form).then(function (captchaPayload) {
if (captchaPayload === null) return null;
var restRoot = settingsRestRoot();
var nonce = settingsRestNonce();
Object.keys(captchaPayload || {}).forEach(function (key) { payload[key] = captchaPayload[key]; });
var request = fetch(restRoot + '/user/phone-code', {
method: 'POST', credentials: 'same-origin',
headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': nonce },
body: JSON.stringify(payload)
});
button.disabled = true;
return request;
}).then(function (response) { return response ? response.json().then(function (payload) { return { ok: response.ok, payload: payload }; }) : null; }).then(function (result) {
if (!result) return;
if (!result.ok || !result.payload.success) throw new Error(result.payload.message || '发送失败');
notyf(result.payload.message || '验证码已发送', 'success');
startCountdown(button, result.payload.data && result.payload.data.countdown);
}).catch(function (error) {
resetCountdown(button);
notyf(error.message || '发送失败', 'danger');
}).finally(function () { resetCaptcha('bind_phone', form); });
}
function postAjaxForm(form, action, scene, success) {
if (!formHasRequiredValues(form)) return;
var button = form.querySelector('[type="submit"]');
var label = button && button.querySelector('[data-aksrt-settings-submit-label]');
var original = label ? label.textContent : '';
if (button) { button.disabled = true; setSettingsSubmitLabel(button, i18n.submitting || '提交中...'); }
runCaptcha(scene, form).then(function (captchaPayload) {
if (captchaPayload === null) {
if (button) { button.disabled = false; setSettingsSubmitLabel(button, original); }
return null;
}
var data = new FormData(form);
data.set('action', action);
data.set('_settings_nonce', settingsNonce);
Object.keys(captchaPayload || {}).forEach(function (key) { data.set(key, captchaPayload[key]); });
return fetch(ajaxUrl, { method: 'POST', credentials: 'same-origin', body: data });
}).then(function (response) { return response ? response.json() : null; }).then(function (payload) {
if (!payload) return;
if (button) { button.disabled = false; setSettingsSubmitLabel(button, original); }
if (!payload.success) throw new Error((payload.data && payload.data.message) || '操作失败');
notyf((payload.data && payload.data.message) || '操作成功', 'success');
if (typeof success === 'function') success(payload);
}).catch(function (error) {
if (button) { button.disabled = false; setSettingsSubmitLabel(button, original); }
notyf(error.message || '操作失败', 'danger');
}).finally(function () { resetCaptcha(scene, form); });
}
function postPhoneForm(form, endpoint, success) {
if (!formHasRequiredValues(form)) return;
var button = form.querySelector('[type="submit"]');
var label = button && button.querySelector('[data-aksrt-settings-submit-label]');
var original = label ? label.textContent : '';
runCaptcha('bind_phone', form).then(function (captchaPayload) {
if (captchaPayload === null) return null;
var restRoot = settingsRestRoot();
var nonce = settingsRestNonce();
if (button) { button.disabled = true; setSettingsSubmitLabel(button, i18n.submitting || '提交中...'); }
var payload = {};
new FormData(form).forEach(function (value, key) { payload[key] = value; });
Object.keys(captchaPayload || {}).forEach(function (key) { payload[key] = captchaPayload[key]; });
return fetch(restRoot + endpoint, { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': nonce }, body: JSON.stringify(payload) });
}).then(function (response) { return response ? response.json().then(function (payload) { return { ok: response.ok, payload: payload }; }) : null; }).then(function (result) {
if (!result) return;
if (!result.ok || !result.payload.success) throw new Error(result.payload.message || '操作失败');
if (button) { button.disabled = false; setSettingsSubmitLabel(button, original); }
notyf(result.payload.message || '操作成功', 'success');
if (typeof success === 'function') success(result.payload);
}).catch(function (error) {
if (button) { button.disabled = false; setSettingsSubmitLabel(button, original); }
notyf(error.message || '操作失败', 'danger');
}).finally(function () { resetCaptcha('bind_phone', form); });
}
/* ---- 修改手机:脱敏中间位数字框(格数随号码长度变化) ---- */
function phoneDigitBoxes(dialog) {
return Array.prototype.slice.call(dialog.querySelectorAll('[data-aksrt-phone-digits] .aksrt-phone-digit'));
}
function phoneDigitsValue(dialog) {
return phoneDigitBoxes(dialog).map(function (input) { return String(input.value || '').replace(/\D/g, ''); }).join('');
}
function setPhoneDigitsError(dialog, message) {
var group = dialog.querySelector('[data-aksrt-phone-digits]');
var error = dialog.querySelector('[data-aksrt-phone-digits-error]');
if (error) {
error.textContent = message || '';
error.hidden = !message;
}
if (group) group.classList.toggle('has-error', !!message);
}
function hidePhoneCodeArea(dialog, options) {
var area = dialog.querySelector('[data-aksrt-phone-code-area]');
if (!area) return;
var wasVisible = !area.hidden;
area.hidden = true;
area.querySelectorAll('.account-code-send,[data-aksrt-sms-trigger]').forEach(resetCountdown);
if (wasVisible && options && options.destroyCode) {
// 中间位被修改:销毁服务端已发放的老号验证码状态。
fetch(settingsRestRoot() + '/user/phone-code', {
method: 'POST', credentials: 'same-origin',
headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': settingsRestNonce() },
body: JSON.stringify({ stage: 'reset_old_phone' })
}).catch(function () {});
}
}
function verifyPhoneMiddle(dialog) {
var group = dialog.querySelector('[data-aksrt-phone-digits]');
var boxes = phoneDigitBoxes(dialog);
if (!group || !boxes.length) return;
var hiddenLength = parseInt(group.getAttribute('data-hidden-length'), 10) || boxes.length;
var middle = phoneDigitsValue(dialog);
if (middle.length !== hiddenLength) return;
dialog.__aksrtPhoneMatchTicket = (dialog.__aksrtPhoneMatchTicket || 0) + 1;
var ticket = dialog.__aksrtPhoneMatchTicket;
fetch(settingsRestRoot() + '/user/phone-match', {
method: 'POST', credentials: 'same-origin',
headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': settingsRestNonce() },
body: JSON.stringify({ middle: middle })
}).then(function (response) { return response.json().then(function (payload) { return { ok: response.ok, payload: payload }; }); }).then(function (result) {
if (ticket !== dialog.__aksrtPhoneMatchTicket) return;
if (result.ok && result.payload.success) {
var area = dialog.querySelector('[data-aksrt-phone-code-area]');
if (area) area.hidden = false;
setPhoneDigitsError(dialog, '');
} else {
hidePhoneCodeArea(dialog, { destroyCode: true });
setPhoneDigitsError(dialog, (result.payload && result.payload.message) || '填写的号码与绑定手机号不一致');
}
}).catch(function () {
if (ticket === dialog.__aksrtPhoneMatchTicket) {
hidePhoneCodeArea(dialog, { destroyCode: false });
setPhoneDigitsError(dialog, '网络错误,请重试');
}
});
}
// 密码显隐切换
root.querySelectorAll('#pwd-modal .toggle-pwd, #email-modal .toggle-pwd').forEach(function(btn) {
if (btn.dataset.aksrtSettingsInitialized === 'true') return;
btn.dataset.aksrtSettingsInitialized = 'true';
btn.addEventListener('click', function() {
var inp = document.getElementById(this.getAttribute('data-target'));
if (!inp) return;
inp.type = (inp.type === 'password') ? 'text' : 'password';
this.classList.toggle('is-visible');
});
});
function initPhoneDigits(dialog) {
var boxes = phoneDigitBoxes(dialog);
if (!boxes.length || dialog.dataset.aksrtDigitsBound === 'true') return;
dialog.dataset.aksrtDigitsBound = 'true';
// 密码强度
var pInput = document.getElementById('pwd-new');
var sBar = document.getElementById('pwd-strength-bar');
var sTxt = document.getElementById('pwd-strength-text');
if (pInput && sBar && sTxt && pInput.dataset.aksrtSettingsInitialized !== 'true') {
pInput.dataset.aksrtSettingsInitialized = 'true';
pInput.addEventListener('input', function() {
var v = this.value;
var f = sBar.querySelector('.pwd-strength__fill');
if (!v) { f.className = 'pwd-strength__fill'; sTxt.className = 'pwd-strength__text'; sTxt.textContent = ''; return; }
var s = 0;
if (v.length >= 6) s++;
if (v.length >= 10) s++;
if (/[a-z]/.test(v) && /[A-Z]/.test(v)) s++;
if (/\d/.test(v)) s++;
if (/[^a-zA-Z0-9]/.test(v)) s++;
var lv = '', tx = '';
if (s <= 1) { lv = 'weak'; tx = '弱'; }
else if (s <= 3) { lv = 'medium'; tx = '中'; }
else { lv = 'strong'; tx = '强'; }
f.className = 'pwd-strength__fill level-' + lv;
sTxt.className = 'pwd-strength__text level-' + lv;
sTxt.textContent = '密码强度: ' + tx;
boxes.forEach(function (input, index) {
input.addEventListener('input', function () {
var value = String(input.value || '').replace(/\D/g, '');
input.value = value === '' ? '' : value.charAt(value.length - 1);
hidePhoneCodeArea(dialog, { destroyCode: true });
setPhoneDigitsError(dialog, '');
if (input.value !== '' && index < boxes.length - 1) boxes[index + 1].focus();
verifyPhoneMiddle(dialog);
});
input.addEventListener('keydown', function (event) {
if (event.key === 'Backspace' && !input.value && index > 0) {
event.preventDefault();
boxes[index - 1].value = '';
boxes[index - 1].focus();
hidePhoneCodeArea(dialog, { destroyCode: true });
setPhoneDigitsError(dialog, '');
} else if (event.key === 'ArrowLeft' && index > 0) {
boxes[index - 1].focus();
} else if (event.key === 'ArrowRight' && index < boxes.length - 1) {
boxes[index + 1].focus();
}
});
input.addEventListener('paste', function (event) {
var text = String((event.clipboardData || window.clipboardData).getData('text') || '');
var digits = text.replace(/\D/g, '');
if (!digits) return;
event.preventDefault();
for (var offset = 0; offset < boxes.length - index && offset < digits.length; offset++) {
boxes[index + offset].value = digits.charAt(offset);
}
boxes[Math.min(index + digits.length, boxes.length - 1)].focus();
hidePhoneCodeArea(dialog, { destroyCode: true });
setPhoneDigitsError(dialog, '');
verifyPhoneMiddle(dialog);
});
});
}
// 验证码发送
function getSendBtnIdleText(btnId) {
if (btnId === 'pwd-send-code') return i18n.sendToCurrentEmail || '发送到当前邮箱';
if (btnId === 'email-send-code') return i18n.sendToNewEmail || '发送到新邮箱';
return i18n.sendCode || '发送验证码';
function sendRecoveryCode(button, form) {
if (!form || button.disabled) return;
var target = String((form.querySelector('[name="target"]') || {}).value || '').trim();
if (!target) { notyf('请输入邮箱或手机号', 'warning'); return; }
var dialog = form.closest('[data-account-dialog="password"]');
button.disabled = true;
runCaptcha('lostpassword', form).then(function (captchaPayload) {
if (captchaPayload === null) { notyf('请先完成人机验证', 'warning'); resetCountdown(button); return null; }
var payload = { scene: 'resetpassword', target: target, channel: dialog ? (dialog.getAttribute('data-recovery-channel') || '') : '' };
Object.keys(captchaPayload || {}).forEach(function (key) { payload[key] = captchaPayload[key]; });
return fetch(settingsRestRoot() + '/auth/code', {
method: 'POST', credentials: 'same-origin',
headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': settingsRestNonce() },
body: JSON.stringify(payload)
});
}).then(function (response) { return response ? response.json().then(function (payload) { return { ok: response.ok, payload: payload }; }) : null; }).then(function (result) {
if (!result) return;
if (!result.ok || !result.payload.success) throw new Error(result.payload.message || '发送失败');
notyf((result.payload.data && result.payload.data.message) || result.payload.message || '验证码已发送', 'success');
startCountdown(button, (result.payload.data && result.payload.data.countdown) || result.payload.countdown);
}).catch(function (error) {
resetCountdown(button);
notyf(error.message || '发送失败', 'danger');
}).finally(function () { resetCaptcha('lostpassword', form); });
}
function bindSend(btnId, formId, scene) {
var b = document.getElementById(btnId);
if (!b) return;
if (b.dataset.aksrtSettingsInitialized === 'true') return;
b.dataset.aksrtSettingsInitialized = 'true';
b.addEventListener('click', function() {
if (this.classList.contains('is-counting')) return;
var self = this;
var form = document.getElementById(formId);
var toastId = scene === 'change_password'
? 'aksrt-settings-password-code-toast'
: 'aksrt-settings-email-code-toast';
runCaptcha(scene, form).then(function(captchaPayload) {
if (captchaPayload === null) return;
self.disabled = true;
self.textContent = i18n.sending || '发送中...';
notyf(i18n.sending || '正在发送验证码…', 'load', '', toastId);
var data = new URLSearchParams();
data.set('action', 'aksrt_send_email_code');
data.set('_settings_nonce', settingsNonce);
data.set('scene', scene || '');
if (form) {
new FormData(form).forEach(function(value, key) {
if (data.has(key) || value === '' || value == null) return;
data.set(key, value);
});
}
Object.keys(captchaPayload || {}).forEach(function(key) { data.set(key, captchaPayload[key]); });
fetch(ajaxUrl, { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: data.toString() })
.then(function(r) { return r.json(); }).then(function(res) {
if (res.success) {
notyf((res.data && res.data.message) || i18n.codeSent || '验证码已发送', 'success', 5, toastId);
var sec = 60;
self.classList.add('is-counting');
self.textContent = sec + 's 后重发';
var t = setInterval(function() {
sec--;
if (sec <= 0) {
clearInterval(t);
self.disabled = false;
self.classList.remove('is-counting');
self.textContent = getSendBtnIdleText(btnId);
} else {
self.textContent = sec + 's 后重发';
}
}, 1000);
} else {
notyf((res.data && res.data.message) || i18n.sendFailed || '发送失败', 'danger', 5, toastId);
self.disabled = false;
self.textContent = getSendBtnIdleText(btnId);
}
})
.catch(function() {
notyf(i18n.networkError || '网络错误', 'danger', 5, toastId);
self.disabled = false;
self.textContent = getSendBtnIdleText(btnId);
})
.finally(function() {
resetCaptcha(scene, form);
function postRecoveryForm(form) {
if (!formHasRequiredValues(form)) return;
var password = form.querySelector('[name="password"]');
var confirm = form.querySelector('[name="password_confirm"]');
if (password && confirm && password.value !== confirm.value) { notyf('两次密码输入不一致', 'warning'); confirm.focus(); return; }
var button = form.querySelector('[type="submit"]');
var label = button && button.querySelector('[data-aksrt-settings-submit-label]');
var original = label ? label.textContent : '';
var dialog = form.closest('[data-account-dialog="password"]');
if (button) { button.disabled = true; setSettingsSubmitLabel(button, i18n.submitting || '提交中...'); }
runCaptcha('lostpassword', form).then(function (captchaPayload) {
if (captchaPayload === null) {
if (button) { button.disabled = false; setSettingsSubmitLabel(button, original); }
return null;
}
var payload = {};
new FormData(form).forEach(function (value, key) { payload[key] = value; });
payload.channel = dialog ? (dialog.getAttribute('data-recovery-channel') || '') : '';
Object.keys(captchaPayload || {}).forEach(function (key) { payload[key] = captchaPayload[key]; });
return fetch(settingsRestRoot() + '/auth/reset-password', {
method: 'POST', credentials: 'same-origin',
headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': settingsRestNonce() },
body: JSON.stringify(payload)
});
}).then(function (response) { return response ? response.json().then(function (payload) { return { ok: response.ok, payload: payload }; }) : null; }).then(function (result) {
if (!result) return;
if (!result.ok || !result.payload.success) throw new Error(result.payload.message || '重设密码失败');
if (button) { button.disabled = false; setSettingsSubmitLabel(button, original); }
notyf(result.payload.message || (result.payload.data && result.payload.data.message) || '密码已重置', 'success');
window.setTimeout(function () {
var redirect = result.payload.redirect || (result.payload.data && result.payload.data.redirect);
if (redirect) window.location.assign(redirect);
else window.location.reload();
}, 700);
}).catch(function (error) {
if (button) { button.disabled = false; setSettingsSubmitLabel(button, original); }
notyf(error.message || '重设密码失败', 'danger');
}).finally(function () { resetCaptcha('lostpassword', form); });
}
function bindDialog(shell) {
var dialog = shell.querySelector('[data-account-dialog]');
if (!dialog || dialog.dataset.aksrtBound === 'true') return;
dialog.dataset.aksrtBound = 'true';
dialog.querySelectorAll('.toggle-pwd').forEach(function (button) {
button.addEventListener('click', function () {
var input = dialog.querySelector('#' + button.getAttribute('data-target'));
if (!input) return;
input.type = input.type === 'password' ? 'text' : 'password';
var icon = button.querySelector('i');
if (icon) { icon.classList.toggle('fa-eye', input.type === 'password'); icon.classList.toggle('fa-eye-slash', input.type === 'text'); }
});
});
var type = dialog.getAttribute('data-account-dialog');
if (type === 'password') {
var recoveryTarget = dialog.querySelector('#pwd-recovery-target');
if (recoveryTarget && dialog.dataset.recoveryTarget) recoveryTarget.value = dialog.dataset.recoveryTarget;
dialog.querySelectorAll('[data-account-recovery-open],[data-account-recovery-back]').forEach(function (button) {
button.addEventListener('click', function () {
var showRecovery = button.hasAttribute('data-account-recovery-open');
dialog.querySelectorAll('[data-account-password-pane]').forEach(function (pane) {
pane.hidden = pane.getAttribute('data-account-password-pane') === 'recovery' ? !showRecovery : showRecovery;
});
}).catch(function() {
resetCaptcha(scene, form);
notyf('人机验证失败,请重试', 'danger', 5, toastId);
});
});
}
bindSend('pwd-send-code', 'pwd-form', 'change_password');
bindSend('email-send-code', 'email-form', 'change_email');
// 表单提交
function submitForm(fid, act) {
var fm = document.getElementById(fid);
if (!fm) return;
if (fm.dataset.aksrtSettingsInitialized === 'true') return;
fm.dataset.aksrtSettingsInitialized = 'true';
fm.addEventListener('submit', function(e) {
e.preventDefault();
var btn = fm.querySelector('[type=submit]');
var orig = btn ? btn.textContent : '';
var toastId = fid === 'pwd-form'
? 'aksrt-settings-password-toast'
: 'aksrt-settings-email-toast';
var reqs = fm.querySelectorAll('[required]');
for (var i = 0; i < reqs.length; i++) {
if (!reqs[i].value.trim()) { reqs[i].focus(); notyf(i18n.fillRequired || '请填写必填项', 'warning'); return; }
}
if (fid === 'pwd-form') {
var pwdNew = document.getElementById('pwd-new');
var pwdConfirm = document.getElementById('pwd-new-confirm');
if (pwdNew && pwdConfirm && pwdNew.value !== pwdConfirm.value) {
pwdConfirm.focus();
notyf(i18n.passwordMismatch || '两次输入的新密码不一致', 'warning');
return;
}
}
if (btn) { btn.disabled = true; btn.textContent = i18n.submitting || '提交中...'; }
notyf(i18n.submitting || '正在提交…', 'load', '', toastId);
var scene = fid === 'pwd-form' ? 'change_password' : 'change_email';
runCaptcha(scene, fm).then(function(captchaPayload) {
if (captchaPayload === null) {
if (btn) { btn.disabled = false; btn.textContent = orig; }
return null;
var title = dialog.querySelector('[data-account-password-title]');
if (title) {
title.textContent = title.getAttribute(showRecovery ? 'data-recovery-label' : 'data-change-label') || title.textContent;
var shell = dialog.closest('.aksrt-user-settings-modal');
if (shell) shell.setAttribute('aria-label', title.textContent);
}
var d = new FormData(fm);
d.set('action', act);
d.set('_settings_nonce', settingsNonce);
Object.keys(captchaPayload || {}).forEach(function(key) { d.set(key, captchaPayload[key]); });
return fetch(ajaxUrl, { method: 'POST', credentials: 'same-origin', body: d });
})
.then(function(r) { return r ? r.json() : null; })
.then(function(r) {
if (!r) return;
if (btn) { btn.disabled = false; btn.textContent = orig; }
if (r.success) {
notyf((r.data && r.data.message) || '操作成功', 'success', 5, toastId);
closeModal(fm.closest('.aksrt-modal'));
if (r.data && r.data.redirect_to) setTimeout(function() { window.location.href = r.data.redirect_to; }, 2500);
} else {
notyf((r.data && r.data.message) || i18n.operationFailed || '操作失败', 'danger', 5, toastId);
}
})
.catch(function() {
if (btn) { btn.disabled = false; btn.textContent = orig; }
notyf(i18n.networkError || '网络错误', 'danger', 5, toastId);
})
.finally(function() {
resetCaptcha(scene, fm);
var input = dialog.querySelector(showRecovery ? '#pwd-recovery-target' : '#pwd-old');
if (input) input.focus();
});
});
var passwordForm = dialog.querySelector('#pwd-form');
if (passwordForm) passwordForm.addEventListener('submit', function (event) {
event.preventDefault();
var password = passwordForm.querySelector('[name="password"]');
var confirm = passwordForm.querySelector('[name="password2"]');
if (password && confirm && password.value !== confirm.value) { notyf('两次密码输入不一致', 'warning'); confirm.focus(); return; }
postAjaxForm(passwordForm, 'aksrt_change_password', 'change_password', function () { window.setTimeout(function () { window.location.reload(); }, 700); });
});
var recoveryForm = dialog.querySelector('#pwd-recovery-form');
var recoveryCodeButton = dialog.querySelector('[data-account-recovery-send]');
if (recoveryCodeButton && recoveryForm) recoveryCodeButton.addEventListener('click', function () { sendRecoveryCode(recoveryCodeButton, recoveryForm); });
if (recoveryForm) recoveryForm.addEventListener('submit', function (event) { event.preventDefault(); postRecoveryForm(recoveryForm); });
} else if (type === 'email') {
dialog.querySelectorAll('.account-code-send').forEach(function (button) {
button.addEventListener('click', function () { sendEmailCode(button, button.closest('form'), button.getAttribute('data-account-code-scene')); });
});
var oldForm = dialog.querySelector('#email-old-verify-form');
var newForm = dialog.querySelector('#email-new-form');
if (oldForm) oldForm.addEventListener('submit', function (event) {
event.preventDefault();
postAjaxForm(oldForm, 'aksrt_verify_account_email', 'change_email', function () { setStep(dialog, 'email-new'); });
});
if (newForm) newForm.addEventListener('submit', function (event) {
event.preventDefault();
postAjaxForm(newForm, 'aksrt_change_email', 'change_email', function () {
if (dialog.querySelector('[data-account-step="email-success"]')) setStep(dialog, 'email-success');
window.setTimeout(function () { window.location.reload(); }, 700);
});
});
} else if (type === 'phone') {
initPhoneDigits(dialog);
dialog.querySelectorAll('[data-aksrt-sms-trigger]').forEach(function (button) {
button.addEventListener('click', function () { sendPhoneCode(button, button.closest('form'), button.getAttribute('data-aksrt-sms-scene')); });
});
var phoneOldForm = dialog.querySelector('#phone-old-verify-form');
var phoneNewForm = dialog.querySelector('#aksrt-bind-phone-form');
if (phoneOldForm) phoneOldForm.addEventListener('submit', function (event) {
event.preventDefault();
postPhoneForm(phoneOldForm, '/user/phone-verify', function () { setStep(dialog, 'phone-new'); });
});
if (phoneNewForm) phoneNewForm.addEventListener('submit', function (event) {
event.preventDefault();
postPhoneForm(phoneNewForm, '/user/phone', function () {
if (dialog.querySelector('[data-account-step="phone-success"]')) setStep(dialog, 'phone-success');
window.setTimeout(function () { window.location.reload(); }, 700);
});
});
}
}
function resetSettingsModal(shell) {
if (!shell || !shell.classList.contains('aksrt-user-settings-modal')) return;
shell.querySelectorAll('.account-code-send,[data-aksrt-sms-trigger],[data-account-recovery-send]').forEach(resetCountdown);
shell.querySelectorAll('form').forEach(function (form) { form.reset(); });
shell.querySelectorAll('[data-account-dialog="phone"]').forEach(function (dialog) {
dialog.querySelectorAll('.aksrt-phone-digit').forEach(function (input) { input.value = ''; });
var group = dialog.querySelector('[data-aksrt-phone-digits]');
if (group) group.classList.remove('has-error');
var error = dialog.querySelector('[data-aksrt-phone-digits-error]');
if (error) { error.hidden = true; error.textContent = ''; }
var area = dialog.querySelector('[data-aksrt-phone-code-area]');
if (area) area.hidden = !!group;
});
}
submitForm('pwd-form', 'aksrt_change_password');
submitForm('email-form', 'aksrt_change_email');
if (document.documentElement.dataset.aksrtSettingsModalCleanupInitialized !== 'true') {
document.documentElement.dataset.aksrtSettingsModalCleanupInitialized = 'true';
document.addEventListener('aksrt:modal:close', function (event) { resetSettingsModal(event.detail && event.detail.shell); });
}
root.querySelectorAll('.binding-item--linkable').forEach(function(link) {
root.querySelectorAll('[data-aksrt-settings-modal]').forEach(function (button) {
if (button.dataset.aksrtSettingsInitialized === 'true') return;
button.dataset.aksrtSettingsInitialized = 'true';
button.addEventListener('click', function () { openSettingsModal(button.getAttribute('data-aksrt-settings-modal')); });
});
root.querySelectorAll('.binding-item--linkable').forEach(function(link) {
if (link.dataset.aksrtBindCaptchaInitialized === 'true') return;
link.dataset.aksrtBindCaptchaInitialized = 'true';
link.addEventListener('click', function(event) {
@@ -1100,113 +1362,9 @@
initNotifications();
initNotificationPreferences();
initSettings();
initBindPhone();
initSettingsCallbackToast();
});
// 绑定手机
function initBindPhone(root) {
root = root || document;
var form = root.id === 'aksrt-bind-phone-form' ? root : root.querySelector('#aksrt-bind-phone-form');
if (!form) return;
if (form.dataset.aksrtSettingsInitialized === 'true') return;
form.dataset.aksrtSettingsInitialized = 'true';
var sendButton = form.querySelector('[data-aksrt-sms-trigger]');
if (sendButton) sendButton.addEventListener('click', function () {
var phone = String((form.querySelector('[name="phone"]') || {}).value || '').trim();
if (!/^1[3-9]\d{9}$/.test(phone)) { notyf('请输入正确的手机号', 'warning'); return; }
var toastId = 'aksrt-settings-phone-code-toast';
var restRoot = (window.aksrtData && aksrtData.apiUrl) || '/wp-json/aksrt/v1';
var nonce = (window.aksrtData && aksrtData.restNonce) || '';
sendButton.disabled = true;
notyf('正在发送验证码…', 'load', '', toastId);
runCaptcha('bind_phone', form).then(function(captchaPayload) {
if (captchaPayload === null) {
sendButton.disabled = false;
return null;
}
return fetch(String(restRoot).replace(/\/$/, '') + '/user/phone-code', {
method: 'POST', credentials: 'same-origin',
headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': nonce },
body: JSON.stringify(Object.assign({ phone: phone }, captchaPayload || {}))
});
}).then(function (r) { return r ? r.json().then(function (j) { return { ok: r.ok, json: j }; }) : null; }).then(function (res) {
if (!res) return;
if (!res.ok || !res.json.success) throw new Error(res.json.message || '发送失败');
notyf(res.json.message || '验证码已发送', 'success', 5, toastId);
var left = Number((res.json.data && res.json.data.countdown) || 60);
var timer = setInterval(function () { left -= 1; sendButton.textContent = left + 's'; if (left <= 0) { clearInterval(timer); sendButton.disabled = false; sendButton.textContent = '获取验证码'; } }, 1000);
}).catch(function (error) { sendButton.disabled = false; notyf(error.message || '发送失败', 'danger', 5, toastId); })
.finally(function () { resetCaptcha('bind_phone', form); });
});
form.addEventListener('submit', function (e) {
e.preventDefault();
var phoneEl = form.querySelector('[name="phone"]');
var codeEl = form.querySelector('[name="code"]');
var phone = phoneEl ? String(phoneEl.value || '').trim() : '';
var code = codeEl ? String(codeEl.value || '').trim() : '';
if (!/^1[3-9]\d{9}$/.test(phone)) {
notyf('请输入正确的手机号', 'warning');
return;
}
if (!code) {
notyf('请输入短信验证码', 'warning');
return;
}
var submitBtn = form.querySelector('[type="submit"]');
if (submitBtn) {
submitBtn.disabled = true;
submitBtn.textContent = '绑定中...';
}
function doBind(captcha) {
var toastId = 'aksrt-settings-phone-bind-toast';
notyf('正在绑定手机…', 'load', '', toastId);
var restRoot = (window.aksrtData && aksrtData.apiUrl) || '/wp-json/aksrt/v1';
var nonce = (window.aksrtData && aksrtData.restNonce) || '';
return fetch(String(restRoot).replace(/\/$/, '') + '/user/phone', {
method: 'POST',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
'X-WP-Nonce': nonce
},
body: JSON.stringify(Object.assign({ phone: phone, code: code }, captcha))
})
.then(function (r) { return r.json().then(function (j) { return { ok: r.ok, json: j }; }); })
.then(function (res) {
if (res.ok && res.json && res.json.success) {
notyf(res.json.message || '绑定成功', 'success', 5, toastId);
setTimeout(function () { window.location.reload(); }, 600);
return;
}
notyf((res.json && (res.json.message || res.json.error)) || '绑定失败', 'danger', 5, toastId);
if (submitBtn) {
submitBtn.disabled = false;
submitBtn.textContent = '确认绑定';
}
})
.catch(function () {
notyf('网络错误', 'danger', 5, toastId);
if (submitBtn) {
submitBtn.disabled = false;
submitBtn.textContent = '确认绑定';
}
});
}
runCaptcha('bind_phone', form).then(function(captchaPayload) {
if (captchaPayload === null) {
if (submitBtn) {
submitBtn.disabled = false;
submitBtn.textContent = '确认绑定';
}
return null;
}
return doBind(captchaPayload);
}).finally(function() {
resetCaptcha('bind_phone', form);
});
});
}
})();
+5 -2
View File
@@ -1781,7 +1781,9 @@ function post_ajax(_this, con, jcon, item, loader, pag, next, trigger, replace,
$con.append($loader);
}
$loader.fadeIn();
// 显隐只走 .is-loading 类(modal-runtime.css);fadeIn()/hide() 会写
// inline display,而内联优先级高于类规则,残留后骨架永远打不开。
$loader.addClass('is-loading');
swiper_tab_AutoHeight($con);
},
success: function (a) {
@@ -1837,7 +1839,8 @@ function post_ajax(_this, con, jcon, item, loader, pag, next, trigger, replace,
}
$con.removeAttr('ing').trigger('post_ajax.ed', jQuery(a));
find('.post_ajax_loader', $con).hide();
// 同 beforeSend:只切换 .is-loading,不写 inline display。
find('.post_ajax_loader', $con).removeClass('is-loading');
if (!_this.attr('no-scroll')) {
var _scroll_To = replace ? $con : jQuery(c_c[0]);
+1 -1
View File
@@ -1 +1 @@
(function($,window2,document2){"use strict";function closeNotyf($element){$element.addClass("notyn-out"),window2.setTimeout(function(){$element.remove()},1e3)}function notyf2(message,type,time,id){var $body=jQuery(document2.body);jQuery(".notyn").length||$body.append('<div class="notyn"></div>'),type=type||"success",type==="error"&&(type="danger"),time=time||5e3,time=time<100?time*1e3:time;var idAttribute=id?' id="'+id+'"':"",$notification=jQuery('<div class="noty1"'+idAttribute+'><div class="notyf '+type+'">'+message+"</div></div>"),shouldClose=!id;id&&jQuery("#"+id).length?(jQuery("#"+id).find(".notyf").removeClass().addClass("notyf "+type).html(message),$notification=jQuery("#"+id),shouldClose=!0):jQuery(".notyn").append($notification),shouldClose&&window2.setTimeout(function(){closeNotyf($notification)},time)}jQuery(document2.body).on("click",".noty1",function(){closeNotyf(jQuery(this))}),window2.notyf=notyf2})(jQuery,window,document),(function(window2){"use strict";var allowedKinds={identity:!0,badges:!0,orders:!0,vip:!0},allowedEvents={click:!0,shell:!0,"first-content":!0,ready:!0,"action-start":!0,"action-end":!0,unauthorized:!0,error:!0},sessions=Object.create(null),sequence=0,maxSessions=100;function normalize(value,fallback){var normalized=String(value||fallback||"").toLowerCase().replace(/[^a-z0-9-]/g,"");return normalized||fallback}function now(){return window2.performance&&typeof window2.performance.now=="function"?window2.performance.now():0}function markName(session,event){return"aksrt-modal-"+session.kind+"-"+session.id+"-"+event}function emit(session,event){if(!(!window2.performance||typeof window2.performance.mark!="function")){var name=markName(session,event);if(window2.performance.mark(name),session.marks[event]=name,typeof window2.performance.measure=="function"&&session.marks.click&&event!=="click")try{window2.performance.measure("aksrt-modal-"+session.kind+"-"+session.id+"-click-to-"+event,session.marks.click,name)}catch{}}}function start(kind){var safeKind=normalize(kind,"unknown");allowedKinds[safeKind]||(safeKind="unknown"),sequence+=1;var id=String(sequence),session={id,kind:safeKind,startedAt:now(),marks:Object.create(null)};sessions[id]=session,emit(session,"click");for(var ids=Object.keys(sessions);ids.length>maxSessions;)delete sessions[ids.shift()];return id}function mark(id,event){var session=sessions[String(id||"")],safeEvent=normalize(event,"error");!session||!allowedEvents[safeEvent]||emit(session,safeEvent)}function finish(id){mark(id,"ready")}function fail(id){mark(id,"error")}window2.AksrtModalTelemetry={start,mark,finish,error:fail}})(window),(function(window2){"use strict";function data(){return window2.aksrtData||window2.aksrt||{}}function createIdempotencyKey(){if(window2.crypto&&typeof window2.crypto.randomUUID=="function")return window2.crypto.randomUUID();if(!window2.crypto||typeof window2.crypto.getRandomValues!="function")throw new Error("当前浏览器无法生成安全的操作标识");var bytes=new Uint8Array(16);window2.crypto.getRandomValues(bytes),bytes[6]=bytes[6]&15|64,bytes[8]=bytes[8]&63|128;var hex=Array.prototype.map.call(bytes,function(value){return value.toString(16).padStart(2,"0")}).join("");return hex.slice(0,8)+"-"+hex.slice(8,12)+"-"+hex.slice(12,16)+"-"+hex.slice(16,20)+"-"+hex.slice(20)}function restBase(){var config=data(),raw=config.restUrl||config.apiUrl||config.rest_url||"",siteUrl=config.siteUrl||config.homeUrl||window2.location.origin+"/";return window2.location.protocol==="https:"&&siteUrl.substring(0,5)==="http:"&&(siteUrl="https:"+siteUrl.substring(5)),raw?String(raw).replace(/\/$/,""):String(siteUrl).replace(/\/$/,"")+"/index.php?rest_route=/aksrt/v1"}function appendQuery(url2,query){if(!query)return url2;var parts=[];return typeof query=="string"?parts=query.replace(/^\?/,"").split("&").filter(Boolean):typeof query=="object"&&Object.keys(query).forEach(function(key){query[key]===void 0||query[key]===null||parts.push(encodeURIComponent(key)+"="+encodeURIComponent(String(query[key])))}),parts.length?url2+(url2.indexOf("?")>=0?"&":"?")+parts.join("&"):url2}function url(path,query){path=String(path||"");var inlineQuery="",queryIndex=path.indexOf("?");if(queryIndex>=0&&(inlineQuery=path.slice(queryIndex+1),path=path.slice(0,queryIndex)),/^https?:\/\//i.test(path))return appendQuery(path,query||inlineQuery);path.charAt(0)!=="/"&&(path="/"+path);var base=restBase(),result=(base.indexOf("rest_route=")>=0,base+path);return appendQuery(result,query||inlineQuery)}function headers(options){var config=data(),source=options&&options.headers||{},result={},nonce=config.restNonce||config.nonce||"";return Object.keys(source).forEach(function(key){result[key]=source[key]}),result.Accept||(result.Accept="application/json, text/html;q=0.9, */*;q=0.8"),nonce&&!result["X-WP-Nonce"]&&(result["X-WP-Nonce"]=nonce),options&&options.body!==void 0&&options.body!==null&&!(window2.FormData&&options.body instanceof window2.FormData)&&!result["Content-Type"]&Line truncated
(function($,window2,document2){"use strict";function closeNotyf($element){$element.addClass("notyn-out"),window2.setTimeout(function(){$element.remove()},1e3)}function notyf2(message,type,time,id){var $body=jQuery(document2.body);jQuery(".notyn").length||$body.append('<div class="notyn"></div>'),type=type||"success",type==="error"&&(type="danger"),time=time||5e3,time=time<100?time*1e3:time;var idAttribute=id?' id="'+id+'"':"",$notification=jQuery('<div class="noty1"'+idAttribute+'><div class="notyf '+type+'">'+message+"</div></div>"),shouldClose=!id;id&&jQuery("#"+id).length?(jQuery("#"+id).find(".notyf").removeClass().addClass("notyf "+type).html(message),$notification=jQuery("#"+id),shouldClose=!0):jQuery(".notyn").append($notification),shouldClose&&window2.setTimeout(function(){closeNotyf($notification)},time)}jQuery(document2.body).on("click",".noty1",function(){closeNotyf(jQuery(this))}),window2.notyf=notyf2})(jQuery,window,document),(function(window2){"use strict";var allowedKinds={identity:!0,badges:!0,orders:!0,vip:!0},allowedEvents={click:!0,shell:!0,"first-content":!0,ready:!0,"action-start":!0,"action-end":!0,unauthorized:!0,error:!0},sessions=Object.create(null),sequence=0,maxSessions=100;function normalize(value,fallback){var normalized=String(value||fallback||"").toLowerCase().replace(/[^a-z0-9-]/g,"");return normalized||fallback}function now(){return window2.performance&&typeof window2.performance.now=="function"?window2.performance.now():0}function markName(session,event){return"aksrt-modal-"+session.kind+"-"+session.id+"-"+event}function emit(session,event){if(!(!window2.performance||typeof window2.performance.mark!="function")){var name=markName(session,event);if(window2.performance.mark(name),session.marks[event]=name,typeof window2.performance.measure=="function"&&session.marks.click&&event!=="click")try{window2.performance.measure("aksrt-modal-"+session.kind+"-"+session.id+"-click-to-"+event,session.marks.click,name)}catch{}}}function start(kind){var safeKind=normalize(kind,"unknown");allowedKinds[safeKind]||(safeKind="unknown"),sequence+=1;var id=String(sequence),session={id,kind:safeKind,startedAt:now(),marks:Object.create(null)};sessions[id]=session,emit(session,"click");for(var ids=Object.keys(sessions);ids.length>maxSessions;)delete sessions[ids.shift()];return id}function mark(id,event){var session=sessions[String(id||"")],safeEvent=normalize(event,"error");!session||!allowedEvents[safeEvent]||emit(session,safeEvent)}function finish(id){mark(id,"ready")}function fail(id){mark(id,"error")}window2.AksrtModalTelemetry={start,mark,finish,error:fail}})(window),(function(window2){"use strict";function data(){return window2.aksrtData||window2.aksrt||{}}function createIdempotencyKey(){if(window2.crypto&&typeof window2.crypto.randomUUID=="function")return window2.crypto.randomUUID();if(!window2.crypto||typeof window2.crypto.getRandomValues!="function")throw new Error("当前浏览器无法生成安全的操作标识");var bytes=new Uint8Array(16);window2.crypto.getRandomValues(bytes),bytes[6]=bytes[6]&15|64,bytes[8]=bytes[8]&63|128;var hex=Array.prototype.map.call(bytes,function(value){return value.toString(16).padStart(2,"0")}).join("");return hex.slice(0,8)+"-"+hex.slice(8,12)+"-"+hex.slice(12,16)+"-"+hex.slice(16,20)+"-"+hex.slice(20)}function restBase(){var config=data(),raw=config.restUrl||config.apiUrl||config.rest_url||"",siteUrl=config.siteUrl||config.homeUrl||window2.location.origin+"/";return window2.location.protocol==="https:"&&siteUrl.substring(0,5)==="http:"&&(siteUrl="https:"+siteUrl.substring(5)),raw?String(raw).replace(/\/$/,""):String(siteUrl).replace(/\/$/,"")+"/index.php?rest_route=/aksrt/v1"}function appendQuery(url2,query){if(!query)return url2;var parts=[];return typeof query=="string"?parts=query.replace(/^\?/,"").split("&").filter(Boolean):typeof query=="object"&&Object.keys(query).forEach(function(key){query[key]===void 0||query[key]===null||parts.push(encodeURIComponent(key)+"="+encodeURIComponent(String(query[key])))}),parts.length?url2+(url2.indexOf("?")>=0?"&":"?")+parts.join("&"):url2}function url(path,query){path=String(path||"");var inlineQuery="",queryIndex=path.indexOf("?");if(queryIndex>=0&&(inlineQuery=path.slice(queryIndex+1),path=path.slice(0,queryIndex)),/^https?:\/\//i.test(path))return appendQuery(path,query||inlineQuery);path.charAt(0)!=="/"&&(path="/"+path);var base=restBase(),result=(base.indexOf("rest_route=")>=0,base+path);return appendQuery(result,query||inlineQuery)}function headers(options){var config=data(),source=options&&options.headers||{},result={},nonce=config.restNonce||config.nonce||"";return Object.keys(source).forEach(function(key){result[key]=source[key]}),result.Accept||(result.Accept="application/json, text/html;q=0.9, */*;q=0.8"),nonce&&!result["X-WP-Nonce"]&&(result["X-WP-Nonce"]=nonce),options&&options.body!==void 0&&options.body!==null&&!(window2.FormData&&options.body instanceof window2.FormData)&&!result["Content-Type"]&Line truncated
+1 -1
View File
@@ -377,7 +377,7 @@
if (data.redirect && (data.payment_url || data.qr_content)) {
callback(options, "onPending", data);
if (!data.order_number) {
var missing = { ok: false, status: 500, json: { message: "流水号缺失" } };
var missing = { ok: false, status: 500, json: { message: "订单号缺失" } };
callback(options, "onError", missing);
return missing;
}
-173
View File
@@ -1,173 +0,0 @@
/**
* AKSRT Waterfall — 瀑布流/异步列表
*
* 提供纯 CSS column-based 瀑布流布局和无限滚动加载能力。
* 无外部依赖。
*
* @package AKSRT
*/
(function () {
'use strict';
/**
* Initialize waterfall layout on a container.
*
* @param {HTMLElement} container - The container element
* @param {Object} options
* @param {number} options.columns - Number of columns (default: auto from CSS)
* @param {string} options.itemSelector - Child item selector (default: '.waterfall-item')
* @param {number} options.gap - Gap between items in px (default: 16)
*/
function Waterfall(container, options) {
if (!container) return;
this.container = container;
this.options = Object.assign({
columns: 0,
itemSelector: '.waterfall-item',
gap: 16,
}, options || {});
this._resizeHandler = this.layout.bind(this);
window.addEventListener('resize', this._resizeHandler);
this.layout();
}
Waterfall.prototype.layout = function () {
var items = this.container.querySelectorAll(this.options.itemSelector);
if (items.length === 0) return;
var cols = this.options.columns;
if (cols <= 0) {
var containerWidth = this.container.offsetWidth;
cols = Math.max(1, Math.floor(containerWidth / 280));
}
var gap = this.options.gap;
var colWidth = (this.container.offsetWidth - gap * (cols - 1)) / cols;
var colHeights = new Array(cols).fill(0);
for (var i = 0; i < items.length; i++) {
var item = items[i];
item.style.width = colWidth + 'px';
item.style.position = 'absolute';
// Find shortest column
var minHeight = Infinity;
var minCol = 0;
for (var c = 0; c < cols; c++) {
if (colHeights[c] < minHeight) {
minHeight = colHeights[c];
minCol = c;
}
}
item.style.left = (minCol * (colWidth + gap)) + 'px';
item.style.top = colHeights[minCol] + 'px';
colHeights[minCol] += item.offsetHeight + gap;
}
// Set container height to tallest column
var maxHeight = Math.max.apply(null, colHeights);
this.container.style.height = maxHeight + 'px';
this.container.style.position = 'relative';
};
Waterfall.prototype.relayout = function () {
this.layout();
};
Waterfall.prototype.destroy = function () {
window.removeEventListener('resize', this._resizeHandler);
};
/**
* Infinite scroll loader.
*
* @param {Object} options
* @param {string} options.containerSelector - List container selector
* @param {string} options.loadMoreSelector - Load-more button selector
* @param {Function} options.loadMore - Async function that loads more items
* @param {number} options.threshold - Distance from bottom to trigger (default: 200)
*/
function InfiniteScroll(options) {
this.options = Object.assign({
containerSelector: '.aksrt-infinite-list',
loadMoreSelector: '.aksrt-load-more',
loadMore: null,
threshold: 200,
}, options || {});
this.loading = false;
this.hasMore = true;
this._scrollHandler = this._check.bind(this);
window.addEventListener('scroll', this._scrollHandler);
this._check();
}
InfiniteScroll.prototype._check = function () {
if (this.loading || !this.hasMore) return;
var loadMoreBtn = document.querySelector(this.options.loadMoreSelector);
if (!loadMoreBtn) {
this.hasMore = false;
return;
}
var rect = loadMoreBtn.getBoundingClientRect();
if (rect.top < window.innerHeight + this.options.threshold) {
this._load();
}
};
InfiniteScroll.prototype._load = function () {
if (!this.options.loadMore) return;
this.loading = true;
var self = this;
this.options.loadMore().then(function (hasMore) {
self.hasMore = hasMore !== false;
self.loading = false;
if (self.hasMore) {
self._check();
}
}).catch(function () {
self.loading = false;
});
};
InfiniteScroll.prototype.destroy = function () {
window.removeEventListener('scroll', this._scrollHandler);
};
// Auto-init
var waterfallInstances = [];
function relayoutAll() {
waterfallInstances.forEach(function (instance) {
if (instance && typeof instance.relayout === 'function') {
instance.relayout();
}
});
}
function autoInit() {
document.querySelectorAll('[data-aksrt-waterfall]:not([data-aksrt-waterfall-bound])').forEach(function (el) {
el.dataset.aksrtWaterfallBound = '1';
var cols = parseInt(el.dataset.aksrtWaterfallColumns) || 0;
var gap = parseInt(el.dataset.aksrtWaterfallGap) || 16;
waterfallInstances.push(new Waterfall(el, { columns: cols, gap: gap }));
});
relayoutAll();
}
jQuery(document).off('auto_fun.aksrt-waterfall').on('auto_fun.aksrt-waterfall', autoInit);
jQuery(document).off('lazyloaded.aksrt-waterfall').on('lazyloaded.aksrt-waterfall', relayoutAll);
// Export to global
window.AksrtWaterfall = Waterfall;
window.AksrtInfiniteScroll = InfiniteScroll;
})();
+57 -9
View File
@@ -109,23 +109,50 @@
}
function loaderOf(container) {
if (!container) return null;
return container.querySelector('.post_ajax_loader');
}
function fallbackLoader() {
var text = (window.aksrtData && aksrtData.ajaxLoading) || '加载中...';
var node = document.createElement('div');
node.className = 'post_ajax_loader';
node.setAttribute('role', 'status');
node.innerHTML = '<div class="mb20"><div class="text-center muted-2-color mt20"><i class="loading mr10"></i>' + text + '</div></div>';
return node;
}
function setBusy(container, busy) {
if (!container) return;
if (busy) {
container.dataset.loading = '1';
container.setAttribute('aria-busy', 'true');
var loader = loaderOf(container);
if (loader) loader.style.display = '';
// 骨架开关走 .is-loading 类(modal-runtime.css),不写 inline display。
var loader = loaderOf(container) || container.appendChild(fallbackLoader());
loader.classList.add('is-loading');
} else {
delete container.dataset.loading;
container.setAttribute('aria-busy', 'false');
var loader = loaderOf(container);
if (loader) loader.style.display = 'none';
if (loader) loader.classList.remove('is-loading');
}
}
// 参考主题 post_ajax 的 beforeSend 语义:骨架每次请求都挪到列表末尾
// (最后一个条目之后、分页控件之前),请求成功后只隐藏而不删除,
// 下一轮翻页复用同一个节点。
function anchorLoader(container) {
var pag = container.querySelector(':scope > .ajax-pag');
var loader = loaderOf(container);
if (!loader) return null;
if (pag) {
pag.parentNode.insertBefore(loader, pag);
} else {
container.appendChild(loader);
}
return loader;
}
function loadItems(container, page, append) {
var owner = container.closest('[data-widget]') || container;
var widget = owner.dataset.widget || container.dataset.widget;
@@ -133,28 +160,49 @@
var segment = container.dataset.segment || owner.dataset.segment || '';
if (!widget || !instance || !config.restUrl || container.dataset.loading === '1') return;
setBusy(container, true);
fetch(itemsUrl(widget, instance, page, segment), { credentials: 'same-origin', headers: { Accept: 'application/json' } })
anchorLoader(container);
return fetch(itemsUrl(widget, instance, page, segment), { credentials: 'same-origin', headers: { Accept: 'application/json' } })
.then(function (response) { if (!response.ok) throw new Error('items'); return response.json(); })
.then(function (data) {
var fragment = document.createElement('div');
fragment.innerHTML = String(data.html || '');
Array.prototype.slice.call(container.querySelectorAll(':scope > .ajax-pag')).forEach(function (node) { node.remove(); });
if (append) {
Array.prototype.slice.call(fragment.children).forEach(function (node) { container.appendChild(node); });
// 追加方向为底部:新条目插到骨架之前,骨架始终贴住最后一张卡片。
var anchor = loaderOf(container) || container.querySelector(':scope > .ajax-pag');
Array.prototype.slice.call(fragment.children).forEach(function (node) {
if (anchor && anchor.parentNode === container) {
container.insertBefore(node, anchor);
} else {
container.appendChild(node);
}
});
} else {
container.innerHTML = fragment.innerHTML;
// 替换时保留骨架节点本身(参考主题 success 后只 hide() 不删除)。
var loader = loaderOf(container);
Array.prototype.slice.call(container.children).forEach(function (child) {
if (child !== loader && !child.classList.contains('ajax-pag')) child.remove();
});
Array.prototype.slice.call(fragment.children).forEach(function (node) { container.appendChild(node); });
}
Array.prototype.slice.call(container.querySelectorAll(':scope > .ajax-pag')).forEach(function (node) { node.remove(); });
if (data.paginate_html) container.insertAdjacentHTML('beforeend', data.paginate_html);
container.dataset.page = String(data.page || page);
anchorLoader(container);
scan(container);
})
.catch(function () { container.innerHTML = errorMarkup(); })
.catch(function () {
var loader = loaderOf(container);
if (loader && loader.parentNode === container) {
loader.insertAdjacentHTML('beforebegin', errorMarkup());
} else {
container.insertAdjacentHTML('afterbegin', errorMarkup());
}
})
.finally(function () { setBusy(container, false); });
}
function initItems(container) {
if (!once(container, 'Items')) return;
if (container.classList.contains('is-loading')) loadItems(container, 1, false);
container.addEventListener('click', function (event) {
var retry = event.target.closest('[data-widget-retry]');
var trigger = event.target.closest('[data-next-page]');
-1
View File
@@ -139,7 +139,6 @@ if (function_exists('aksrt_archive_open_shell')) {
'stream_class' => 'archive__ajax-wrap content-stream--archive content-stream--category',
'empty_text' => __('该分类下暂无文章', 'aksrt'),
'ajax_enabled' => $ajax_filter,
'show_loading' => $ajax_filter,
]);
}
?>
+23 -6
View File
@@ -167,6 +167,15 @@ if (class_exists('\AKSRT\Settings\MailNotifications')) {
\AKSRT\Auth\Router::get_instance()->init();
\AKSRT\Core\Templates::get_instance()->init();
\AKSRT\Auth\Moderation::get_instance()->init();
if (class_exists('\AKSRT\Extensions\SecurityHardening')) {
\AKSRT\Extensions\SecurityHardening::get_instance()->init();
}
if (class_exists('\AKSRT\Extensions\SystemTools')) {
\AKSRT\Extensions\SystemTools::get_instance()->init();
}
if (class_exists('\AKSRT\Frontend\PageAdminSet')) {
\AKSRT\Frontend\PageAdminSet::get_instance()->init();
}
// 短代码统一注册入口(registry)
if (class_exists('\AKSRT\Core\Shortcodes')) {
@@ -230,6 +239,9 @@ if (class_exists('\AKSRT\Payment\ChargeCard')) {
if (class_exists('\AKSRT\Payment\OrderCenterRest')) {
\AKSRT\Payment\OrderCenterRest::get_instance()->init();
}
if (class_exists('\AKSRT\Payment\AfterSaleService')) {
\AKSRT\Payment\AfterSaleService::get_instance()->init();
}
if (class_exists('\AKSRT\Credits\BalanceService')) {
\AKSRT\Credits\BalanceService::get_instance()->init();
}
@@ -243,9 +255,6 @@ if (class_exists('\AKSRT\Core\DocsPresenter')) {
if (class_exists('\AKSRT\Content\Visibility')) {
\AKSRT\Content\Visibility::get_instance()->init();
}
if (class_exists('\AKSRT\Sms\SMS')) {
\AKSRT\Sms\SMS::get_instance()->init();
}
if (class_exists('\AKSRT\Ai\AI')) {
\AKSRT\Ai\AI::get_instance(); // config-gated; available when admin enables
}
@@ -286,6 +295,9 @@ if (class_exists('\AKSRT\Extensions\MeiliSearch')) {
if (class_exists('\AKSRT\Extensions\SearchPush')) {
\AKSRT\Extensions\SearchPush::get_instance()->init();
}
if (class_exists('\AKSRT\Extensions\SearchPushMetaBox')) {
\AKSRT\Extensions\SearchPushMetaBox::get_instance()->init();
}
if (class_exists('\AKSRT\Core\SEOHead')) {
\AKSRT\Core\SEOHead::get_instance()->init();
}
@@ -374,6 +386,9 @@ if (class_exists('\AKSRT\User\Level')) {
if (class_exists('\AKSRT\User\LevelAdmin')) {
\AKSRT\User\LevelAdmin::get_instance()->init();
}
if (class_exists('\AKSRT\User\ProfileAdmin')) {
\AKSRT\User\ProfileAdmin::init();
}
if (class_exists('\AKSRT\User\Metrics')) {
\AKSRT\User\Metrics::get_instance()->init();
}
@@ -394,9 +409,6 @@ if (is_admin() && class_exists('\AKSRT\Admin\Native\PayOrders')) {
if (is_admin() && class_exists('\AKSRT\Admin\Native\PayProducts')) {
\AKSRT\Admin\Native\PayProducts::get_instance()->init();
}
if (is_admin() && class_exists('\AKSRT\Admin\Native\PayDownloadLogs')) {
\AKSRT\Admin\Native\PayDownloadLogs::get_instance()->init();
}
if (is_admin() && class_exists('\AKSRT\Admin\Native\PayWithdrawals')) {
\AKSRT\Admin\Native\PayWithdrawals::get_instance()->init();
}
@@ -404,6 +416,11 @@ if (is_admin() && class_exists('\AKSRT\Admin\Native\PayIncome')) {
\AKSRT\Admin\Native\PayIncome::get_instance()->init();
}
// 后台文章列表增强:互动/销售数据列 + 快捷编辑配置项
if (is_admin() && class_exists('\AKSRT\Admin\Native\PostListEnhance')) {
\AKSRT\Admin\Native\PostListEnhance::get_instance()->init();
}
// WordPress 原生用户菜单:消息/推广码/认证/禁封(legacy users.php 子菜单对齐)
if (is_admin() && class_exists('\AKSRT\Admin\Native\UsersMenus')) {
\AKSRT\Admin\Native\UsersMenus::get_instance()->init();
+2
View File
@@ -19,6 +19,7 @@ require_once AKSRT_DIR . '/inc/helpers/aksrt-avatar.php';
require_once AKSRT_DIR . '/inc/helpers/aksrt-domain-model.php';
require_once AKSRT_DIR . '/inc/helpers/aksrt-template-tags.php';
require_once AKSRT_DIR . '/inc/helpers/aksrt-frontend-tags.php';
require_once AKSRT_DIR . '/inc/helpers/aksrt-phone.php';
// Shared comment kernel.
require_once AKSRT_DIR . '/inc/helpers/aksrt-ip-geo.php';
require_once AKSRT_DIR . '/inc/helpers/comments/content.php';
@@ -55,6 +56,7 @@ require_once AKSRT_DIR . '/inc/helpers/aksrt-null.php';
require_once AKSRT_DIR . '/inc/helpers/user-ban.php';
require_once AKSRT_DIR . '/inc/helpers/aksrt-ajax-list.php';
require_once AKSRT_DIR . '/inc/helpers/aksrt-links.php';
require_once AKSRT_DIR . '/inc/helpers/message-placeholder.php';
require_once AKSRT_DIR . '/inc/helpers/search/aksrt-search-posts.php';
require_once AKSRT_DIR . '/inc/helpers/search/aksrt-search-box.php';
require_once AKSRT_DIR . '/inc/helpers/aksrt-header-userbox.php';
+2 -62
View File
@@ -62,11 +62,10 @@ add_action('save_post', static function ($post_id, $post = null): void {
// ============================================================
// 前端隐藏 WordPress Admin Bar
// 前端隐藏 WordPress Admin Bar:由 SystemTools 按「扩展&增强 → 系统工具」
// 的 hide_admin_bar 设置门控(默认开启),不再无条件注入。
// ============================================================
add_filter('show_admin_bar', '__return_false');
// ============================================================
// SMTP 邮件配置注入
// ============================================================
@@ -460,65 +459,6 @@ add_filter('wp_kses_allowed_html', function ($allowed, $context) {
return $allowed;
}, 10, 2);
// ============================================================
// WP 后台文章列表增强列 — 浏览/点赞/收藏/下载
// ============================================================
add_filter('manage_post_posts_columns', function ($columns) {
$new_columns = [];
foreach ($columns as $key => $label) {
$new_columns[$key] = $label;
if ($key === 'title') {
$new_columns['aksrt_stats'] = __('互动数据', 'aksrt');
}
}
return $new_columns;
});
add_action('admin_enqueue_scripts', function ($hook) {
if ($hook !== 'edit.php') return;
aksrt_enqueue_fontawesome();
});
add_action('manage_post_posts_custom_column', function ($column, $post_id) {
if ($column !== 'aksrt_stats') return;
$views = (int) get_post_meta($post_id, 'views', true);
$likes = (int) get_post_meta($post_id, 'like', true);
$favorites = (int) get_post_meta($post_id, 'favorite', true);
$downloads = (int) get_post_meta($post_id, 'download_count', true);
$format = function (int $n): string {
if ($n >= 10000) return round($n / 10000, 1) . '万';
if ($n >= 1000) return round($n / 1000, 1) . 'k';
return (string) $n;
};
$parts = [];
$parts[] = '<span title="' . esc_attr__('浏览', 'aksrt') . '" style="margin-right:4px;"><i class="fa fas fa-eye" style="color:#8a909c;font-size:12px;"></i> ' . esc_html($format($views)) . '</span>';
$parts[] = '<span title="' . esc_attr__('点赞', 'aksrt') . '" style="margin-right:4px;"><i class="fa fas fa-thumbs-up" style="color:#1890ff;font-size:12px;"></i> ' . esc_html($format($likes)) . '</span>';
$parts[] = '<span title="' . esc_attr__('收藏', 'aksrt') . '" style="margin-right:4px;"><i class="fa fas fa-star" style="color:#f39c12;font-size:12px;"></i> ' . esc_html($format($favorites)) . '</span>';
if ($downloads > 0) {
$parts[] = '<span title="' . esc_attr__('下载', 'aksrt') . '"><i class="fa fas fa-download" style="color:#52c41a;font-size:12px;"></i> ' . esc_html($format($downloads)) . '</span>';
}
echo implode(' &middot; ', $parts);
}, 10, 2);
// Support sorting by views/likes/favorites in admin
add_action('pre_get_posts', function ($query) {
if (!is_admin() || !$query->is_main_query()) return;
$orderby = $query->get('orderby');
$allowed = ['views', 'like', 'favorite', 'download_count'];
if (in_array($orderby, $allowed, true)) {
$query->set('meta_key', $orderby);
$query->set('orderby', 'meta_value_num');
}
});
// ============================================================
// Gutenberg 自定义块注册
// ============================================================
Loaded 100 of 360 files, more files were not shown because too many files have changed in this diff. Show more