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.
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.
升级价格 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.
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.
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.
The sorter field was rendered in the template but never imported, so
the compiler treated it as an unknown element and share_items was
invisible on the social sharing page.
- Rewrite post_list/filter schema (PHP + JS) with the reference theme's
field set, order, defaults and dependencies; topics fields excluded
(no topics taxonomy), AKSRT private filter_* toggles dropped
- Support dynamic checkbox options in AksField (options as formData key)
so cat_custom_filter.custom_filters lists the custom_filter dimensions
- Map PostDisplay filter settings to the new keys without legacy fallback;
cat_custom_filter now carries lists/dropdown fieldsets and array filters
- Wire the archive chain to reference semantics: cat_custom_filter
overrides per-category filter projects, custom_filter_show gates the
default display, dimension chip groups render on category/tag archives
- Filter archive queries (main query + AJAX endpoint) by _aksrt_cf_{key}
meta with exact-quoted LIKE matching
- Add advanced-filter settings reference contract test
The server catalog ships bare glyph names, but the SPA grid preview
composed "fas home" instead of "fas fa-home" — every FA icon rendered
blank (the old curated lists carried the fa- prefix, so the regression
appeared with the catalog switch).
Item clicks move from per-element @click to a delegated panel handler
reading data-icon (immunity to DOM-replacing extensions/userscripts,
matching the PHP picker's pattern), and selecting now closes the
popover so the choice is visible in the field immediately.
手机底部Tab and 右侧悬浮按钮 settings pages swap their icon text inputs
for the shared FaIconPicker (with a new IconValue preview component that
renders FA/sprite/custom SVG values in lists and previews). footer.php
tabbar/floating/sub-item icons and the VIP float render through
aksrt_icon_markup so every stored format displays; GlobalControllerPart1
and the VIP helper drop the FA-only regex gate and sanitize icon values
through aksrt_sanitize_icon_value. The picker popover tabs now wrap
instead of overflowing, and FA grid previews use style-only classes.
FaIconPicker drops its duplicated hard-coded FA subset and loads
GET /aksrt/v1/icons/catalog: full solid/regular/brands sets, the
built-in zibsvg sprite tab and an inline custom SVG tab. Props/emit
surface is unchanged; the system-notice title icon schema drift
(input vs icon) is fixed on the JS side.
Capture real client IPs behind CDN, improve provider fallbacks and
negative cache, fix admin credential dependsOn for polling mode, and
map country labels to ISO codes for moderation GeoIP blocks.
Render icon cards as a compact list with edit/delete actions and
preview on the right; field data is edited in a modal. Opt-in via
schema ui=list-modal so other repeaters stay inline.