feat(creator-studio): full pay settings and paid resources in the publish drawer

The drawer only had a pay toggle and one bare price field, so several
options from the front-submit pay card could not be set at all.

- New PayPanel mirrors the front-submit card: cash / points segment
  (points only when the site has credits enabled), price or points with
  the currency suffix, and VIP prices. VIP fields render only when the
  site asks for manual VIP pricing and the level is enabled, and now
  submit vip1_points / vip2_points as well.
- New ResourceDialog restores "添加资源": download URL with an attachment
  picker, resource note (extraction code), copy name / value, custom
  button label and button style swatches. Pasting a net-disk share link
  splits the URL and the extraction code.
- pay.ts sends every download display field and tags net-disk links.
- CreatorRest exposes vipManual / vip*Enabled / vip names / pointsEnabled
  / downloadClasses / currencySymbol, and the readback carries
  extractCode / copyKey / copyVal / class / attachmentId.
- Fixes an existing data loss: snapshot_from_payload dropped copy_key,
  copy_val, class and attachment_id, so approving a revision erased them.

Files split to stay within budget: publish-drawer 332 lines, pay-panel
343, resource-dialog 254. 886 widget-system tests pass.
This commit is contained in:
kate522 committed 2026-11-09 15:24:30 +08:00
1 parent c5bdbc0457
commit 0e4e4670dd
12 files changed
+1022 -224

No files matched your search

@@ -144,7 +144,8 @@ test('studio app uses TinyMCE while preserving autosave, recovery, and publishin
assert.match(api, /'X-WP-Nonce'/);
assert.match(drawer, /canPay/);
assert.match(drawer, /canPayDownload/);
assert.match(drawer, /私有附件存储尚未配置/);
assert.match(drawer, /<PayPanel/);
assert.match(read('assets/creator-studio/src/panels/pay-panel.tsx'), /私有附件存储尚未配置/);
assert.match(drawer, /onSubmit\(false\)/);
assert.match(drawer, /立即发布/);
});
@@ -454,3 +455,75 @@ test('character count lives in the fixed bottom bar, not a reading-time footer',
assert.doesNotMatch(appSource, /分钟阅读/);
assert.match(read('assets/creator-studio/src/components/status-bar.tsx'), /字数/);
});
// 发布抽屉的付费配置必须与前台投稿的「付费内容」卡片等价。
test('publish drawer pay settings match the front-submit pay card', () => {
const payPanel = read('assets/creator-studio/src/panels/pay-panel.tsx');
const resource = read('assets/creator-studio/src/components/resource-dialog.tsx');
const payUtil = read('assets/creator-studio/src/utils/pay.ts');
const draft = read('assets/creator-studio/src/draft.ts');
const rest = read('src/Core/CreatorRest.php');
// 现金 / 积分两种计价方式,且积分只在站点开启积分体系时出现。
assert.match(payPanel, /现金支付/);
assert.match(payPanel, /积分支付/);
assert.match(payPanel, /bootstrap\.pointsEnabled/);
assert.match(rest, /'pointsEnabled'\s*=>/);
// 会员价:手动填写模式与自动折扣模式由服务端决定。
assert.match(payPanel, /bootstrap\.vipManual/);
assert.match(payPanel, /bootstrap\.vip1Enabled/);
assert.match(payPanel, /bootstrap\.vip2Enabled/);
assert.match(payPanel, /vip1Points/);
assert.match(payPanel, /vip2Points/);
assert.match(rest, /'vipManual'\s*=>/);
assert.match(rest, /'vip1Name'\s*=>/);
assert.match(draft, /vip1Points: Number\(pay\.vip1Points/);
// 请求体带齐服务端会校验的会员价字段。
assert.match(payUtil, /vip1_points:/);
assert.match(payUtil, /vip2_points:/);
assert.match(payUtil, /vip1_price:/);
assert.match(payUtil, /vip2_price:/);
// 资源编辑器:与前台投稿一致的七个字段。
for (const field of ['extract_code', 'copy_key', 'copy_val', 'class', 'attachment_id']) {
assert.ok(payUtil.includes(field), `missing download field in request: ${field}`);
}
for (const label of ['下载地址', '资源备注', '复制名称', '复制内容', '自定义按钮文案', '按钮样式']) {
assert.ok(resource.includes(label), `missing resource field: ${label}`);
}
assert.match(resource, /上传附件/);
assert.match(resource, /提取码/);
assert.match(resource, /onPaste/);
assert.match(payPanel, /添加资源/);
assert.match(payPanel, /<ResourceDialog/);
// 按钮样式色块与保存端的白名单同源。
assert.match(rest, /'downloadClasses'\s*=>\s*DomainModel::DOWNLOAD_BUTTON_CLASSES/);
assert.match(payPanel, /bootstrap\.downloadClasses/);
// 受保护附件仍然只提交 asset_key。
assert.match(payUtil, /asset_key: assetKey/);
assert.doesNotMatch(payUtil, /wp-content|file_path/);
});
// 审核 / 发布路径要把资源的展示字段一并带进快照,否则会丢提取码与按钮样式。
test('creator snapshot keeps every download display field', () => {
const rest = read('src/Core/CreatorRest.php');
const snapshot = rest.slice(
rest.indexOf('private function snapshot_from_payload'),
rest.indexOf('private function claim_private_assets')
);
assert.match(snapshot, /'extract_code'\s*=>/);
assert.match(snapshot, /'copy_key'\s*=>/);
assert.match(snapshot, /'copy_val'\s*=>/);
assert.match(snapshot, /'class'\s*=>/);
assert.match(snapshot, /'attachment_id'\s*=>/);
assert.match(snapshot, /'net_disk'/);
// bootstrap 回读时字段名要与前端模型一致。
assert.match(rest, /'extractCode'\s*=>/);
assert.match(rest, /'copyKey'\s*=>/);
assert.match(rest, /'copyVal'\s*=>/);
});