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

+21 -20
View File
@@ -2,31 +2,24 @@
* 付费配置 → 请求体。
*
* 受保护附件只提交 asset_key,不提交任何文件路径或公开 URL;服务端会再次
* 校验附件归属并重新清洗。
* 校验附件归属并重新清洗。字段名与前台投稿表单(jio_pay[...])保持一致,
* 这样两条投稿路径写入的是同一份 pay meta。
*
* @package Jio
*/
type PayState = {
enabled: boolean;
modo: 'cash' | 'points';
price: string;
points: number;
vip1Price: string;
vip2Price: string;
payLimit: number;
downloads: Array<Record<string, any>>;
};
import type { DownloadRow, PayState } from '../draft';
export function normalizePayForRequest(pay: PayState) {
const downloads = (pay.downloads || [])
.map((row) => {
.map((row: DownloadRow) => {
const label = typeof row.label === 'string' ? row.label : '';
const assetKey = typeof row.assetKey === 'string' ? row.assetKey : '';
if (assetKey) {
return {
type: 'protected',
asset_key: assetKey,
label: typeof row.label === 'string' ? row.label : '',
label,
};
}
const url = typeof row.url === 'string' ? row.url.trim() : '';
@@ -34,23 +27,31 @@ export function normalizePayForRequest(pay: PayState) {
return null;
}
return {
type: 'direct',
type: row.type === 'net_disk' ? 'net_disk' : 'direct',
url,
label: typeof row.label === 'string' ? row.label : '',
label,
extract_code: typeof row.extractCode === 'string' ? row.extractCode : '',
copy_key: typeof row.copyKey === 'string' ? row.copyKey : '',
copy_val: typeof row.copyVal === 'string' ? row.copyVal : '',
class: typeof row.class === 'string' ? row.class : '',
attachment_id: Math.max(0, Number(row.attachmentId) || 0),
};
})
.filter(Boolean);
const points = pay.modo === 'points';
return {
enabled: !!pay.enabled,
fields: {
pay_type: pay.enabled ? (downloads.length > 0 ? 'download' : 'reading') : 'no',
pay_modo: pay.modo === 'points' ? 'points' : '0',
price: pay.modo === 'points' ? '0.00' : String(pay.price || '0.00'),
points_price: pay.modo === 'points' ? Math.max(0, Number(pay.points) || 0) : 0,
vip1_price: pay.modo === 'points' ? '0.00' : String(pay.vip1Price || '0.00'),
vip2_price: pay.modo === 'points' ? '0.00' : String(pay.vip2Price || '0.00'),
pay_modo: points ? 'points' : '0',
price: points ? '0.00' : String(pay.price || '0.00'),
points_price: points ? Math.max(0, Number(pay.points) || 0) : 0,
vip1_price: points ? '0.00' : String(pay.vip1Price || '0.00'),
vip2_price: points ? '0.00' : String(pay.vip2Price || '0.00'),
vip1_points: points ? Math.max(0, Number(pay.vip1Points) || 0) : 0,
vip2_points: points ? Math.max(0, Number(pay.vip2Points) || 0) : 0,
pay_limit: Number(pay.payLimit) || 0,
downloads,
},