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

+66 -9
View File
@@ -482,29 +482,36 @@ final class CreatorRest {
if (!is_array($row)) {
continue;
}
// 资源行里的展示字段与前台投稿的资源编辑器一一对应,缺一个就会在
// 审核应用后丢掉提取码 / 复制内容 / 按钮样式。
$display = [
'label' => (string) ($row['label'] ?? ''),
'extract_code' => (string) ($row['extract_code'] ?? ''),
'copy_key' => (string) ($row['copy_key'] ?? ''),
'copy_val' => (string) ($row['copy_val'] ?? ''),
'class' => (string) ($row['class'] ?? ''),
'attachment_id' => max(0, (int) ($row['attachment_id'] ?? 0)),
];
$key = PrivateAssets::sanitize_asset_key((string) ($row['asset_key'] ?? ''));
if ($key !== '') {
$asset = PrivateAssets::get($key);
if ($asset === null || $asset['owner_id'] !== get_current_user_id()) {
continue;
}
$downloads[] = [
$downloads[] = array_merge($display, [
'type' => 'protected',
'asset_key' => $key,
'file_name' => $asset['file_name'],
'size_bytes' => $asset['size_bytes'],
'label' => (string) ($row['label'] ?? ''),
];
]);
continue;
}
$url = esc_url_raw((string) ($row['url'] ?? ''));
if ($url !== '') {
$downloads[] = [
'type' => 'direct',
'url' => $url,
'label' => (string) ($row['label'] ?? ''),
'extract_code' => (string) ($row['extract_code'] ?? ''),
];
$downloads[] = array_merge($display, [
'type' => (string) ($row['type'] ?? 'direct') === 'net_disk' ? 'net_disk' : 'direct',
'url' => $url,
]);
}
}
@@ -653,9 +660,17 @@ final class CreatorRest {
'label' => (string) ($row['label'] ?? ''),
'fileName' => (string) ($row['file_name'] ?? ''),
'sizeBytes' => (int) ($row['size_bytes'] ?? 0),
// 前台投稿的资源编辑器字段,缺一个就会在再次编辑时丢配置。
'extractCode' => (string) ($row['extract_code'] ?? ''),
'copyKey' => (string) ($row['copy_key'] ?? ''),
'copyVal' => (string) ($row['copy_val'] ?? ''),
'class' => (string) ($row['class'] ?? ''),
'attachmentId' => (int) ($row['attachment_id'] ?? 0),
];
}
$vip = $this->vip_labels();
return [
'enabled' => (bool) $enabled,
'canPay' => $front->user_can_cap('new_post_pay', $user),
@@ -665,9 +680,21 @@ final class CreatorRest {
'points' => (int) ($pay['points_price'] ?? 0),
'vip1Price' => (string) ($pay['vip1_price'] ?? '0.00'),
'vip2Price' => (string) ($pay['vip2_price'] ?? '0.00'),
'vip1Points' => (int) ($pay['vip1_points'] ?? 0),
'vip2Points' => (int) ($pay['vip2_points'] ?? 0),
'payLimit' => (int) ($pay['pay_limit'] ?? 0),
'downloads' => $downloads,
'freeAllowed' => !empty($settings['pay_free_s']),
// 会员价:手动填写或按折扣自动计算,决定前端是否渲染会员价输入框。
'vipManual' => !empty($settings['pay_vip_price_s']),
'vip1Enabled' => !empty($settings['pay_vip_1_enabled']),
'vip2Enabled' => !empty($settings['pay_vip_2_enabled']),
'vip1Name' => $vip[1],
'vip2Name' => $vip[2],
// 积分支付只有站点开启积分体系时才可选。
'pointsEnabled' => class_exists('\Jio\Settings\Settings') && \Jio\Settings\Settings::credits_enabled(),
'downloadClasses' => DomainModel::DOWNLOAD_BUTTON_CLASSES,
'currencySymbol' => $this->currency_symbol(),
'priceMin' => (int) ($settings['pay_price_min'] ?? 0),
'priceMax' => (int) ($settings['pay_price_max'] ?? 0),
'pointsMin' => (int) ($settings['pay_points_min'] ?? 0),
@@ -676,6 +703,36 @@ final class CreatorRest {
];
}
/**
* 会员等级名称:与前台付费卡片保持同一来源。
*
* @return array{1:string,2:string}
*/
private function vip_labels(): array {
$names = [1 => __('VIP1', 'jio'), 2 => __('VIP2', 'jio')];
if (!class_exists('\Jio\Settings\PaymentRuntime')) {
return $names;
}
$vip = \Jio\Settings\PaymentRuntime::get_payment_vip_settings();
foreach ([1, 2] as $level) {
$name = trim((string) ($vip['levels'][$level]['name'] ?? ''));
if ($name !== '') {
$names[$level] = $name;
}
}
return $names;
}
/** 站点货币符号,用于价格输入框的前缀。 */
private function currency_symbol(): string {
if (!class_exists('\Jio\Settings\PaymentRuntime')) {
return '¥';
}
$paid = \Jio\Settings\PaymentRuntime::get_paid_content_settings();
$symbol = trim((string) ($paid['display']['currency_symbol'] ?? ''));
return $symbol !== '' ? $symbol : '¥';
}
/**
* @return array<string, bool>
*/