feat(creator-studio): centered paper layout and scheduled publishing

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
kate522andClaude Opus 5 committed 2026-11-06 21:33:40 +08:00
1 parent 384d2b7aa0
commit 7bdf7cf018
20 files changed
+549 -308

No files matched your search

+3 -3
View File
@@ -267,7 +267,7 @@ class SchemaPostList {
['key' => 'post_article_custom_filter_s', 'label' => '允许选择高级筛选', 'type' => 'switcher', 'default' => true, 'dependsOn' => ['key' => 'post_article_s', 'value' => true], 'description' => '此功能启用后,请先在文章列表/高级筛选中添加高级筛选明细'],
['key' => 'post_article_tag', 'label' => '允许插入标签', 'type' => 'switcher', 'default' => true, 'dependsOn' => ['key' => 'post_article_s', 'value' => true], 'description' => '前台投稿时候允许设置文章标签'],
['key' => 'post_article_user', 'label' => '免登陆投稿发布用户', 'type' => 'user_select', 'default' => 1, 'dependsOn' => ['key' => 'post_article_s', 'value' => true], 'description' => '当选择投稿权限为无需登录就能投稿时,投稿文章的用户'],
['key' => 'post_article_title_strlen_limit', 'label' => '标题字数限制', 'type' => 'between_number', 'default' => ['min' => 5, 'max' => 30], 'unit' => '字', 'description' => '限制标题字数可有效的防止灌水等无意义内容(英文字符按0.5个字计算)'],
['key' => 'post_article_title_strlen_limit', 'label' => '标题字数限制', 'type' => 'between_number', 'default' => ['min' => 5, 'max' => 100], 'unit' => '字', 'description' => '限制标题字数可有效的防止灌水等无意义内容(英文字符按0.5个字计算)'],
['key' => 'post_article_pay_free_s', 'label' => '允许设置为免费资源', 'type' => 'switcher', 'default' => true, 'dependsOn' => ['key' => 'post_article_s', 'value' => true], 'description' => '付费内容金额允许设置为 0'],
['key' => 'post_article_pay_price_limit', 'label' => '付费内容金额限制', 'type' => 'fieldset', 'default' => ['price' => ['min' => 0, 'max' => 99999], 'points' => ['min' => 0, 'max' => 0]], 'description' => '限制用户允许设置的金额,防止金额过高不合理(填0为不限制)', 'fields' => [
['key' => 'price', 'label' => '现金金额限制', 'type' => 'between_number', 'default' => ['min' => 0, 'max' => 99999]],
@@ -276,8 +276,8 @@ class SchemaPostList {
['key' => 'post_article_pay_vip_price_s', 'label' => '付费内容允许设置会员价', 'type' => 'switcher', 'default' => true, 'dependsOn' => ['key' => 'post_article_s', 'value' => true], 'description' => '前台投稿时对拥有设置付费内容权限的用户,是否开启设置会员价格。开启此项,会直接在前台显示设置会员价的选项;关闭此项,则用户只能设置普通价格,会员价则按照下方设置的折扣自动计算'],
['key' => 'post_article_pay_vip_1_discount', 'label' => '一级会员折扣', 'type' => 'number', 'default' => 100, 'min' => 0, 'max' => 100, 'unit' => '%', 'dependsOn' => ['all' => [['key' => 'post_article_s', 'value' => true], ['key' => 'post_article_pay_vip_price_s', 'value' => false]]]],
['key' => 'post_article_pay_vip_2_discount', 'label' => '二级会员折扣', 'type' => 'number', 'default' => 100, 'min' => 0, 'max' => 100, 'unit' => '%', 'dependsOn' => ['all' => [['key' => 'post_article_s', 'value' => true], ['key' => 'post_article_pay_vip_price_s', 'value' => false]]], 'description' => '执行价的百分之多少,0为免费,100为没有折扣,不能高于100'],
['key' => 'post_t_placeholder', 'label' => '投稿标题占位符', 'type' => 'input', 'default' => '请输入标题'],
['key' => 'post_c_placeholder', 'label' => '投稿内容占位符', 'type' => 'input', 'default' => '请输入内容'],
['key' => 'post_t_placeholder', 'label' => '投稿标题占位符', 'type' => 'input', 'default' => '请输入标题(最多 100 个字)'],
['key' => 'post_c_placeholder', 'label' => '投稿内容占位符', 'type' => 'input', 'default' => '请输入正文'],
]],
],
],
+6 -6
View File
@@ -187,11 +187,11 @@ final class CreatorRest {
$payload = [
'title' => (string) $request->get_param('title'),
'content' => (string) $request->get_param('content'),
'excerpt' => (string) $request->get_param('excerpt'),
'categories' => (array) $request->get_param('categories'),
'tags' => (array) $request->get_param('tags'),
'custom' => (array) $request->get_param('custom'),
'featured' => (array) $request->get_param('featured'),
'schedule' => (array) $request->get_param('schedule'),
'pay' => (array) $request->get_param('pay'),
];
@@ -199,7 +199,6 @@ final class CreatorRest {
$result = SubmissionVersion::get_instance()->save_working($post_id, $user_id, [
'title' => $payload['title'] !== '' ? $payload['title'] : null,
'content' => $payload['content'],
'excerpt' => $payload['excerpt'],
'base_rev' => absint($request->get_param('rev')),
'snapshot' => $this->snapshot_from_payload($payload),
], false);
@@ -215,10 +214,10 @@ final class CreatorRest {
'posts_id' => $post_id,
'post_title' => $payload['title'],
'post_content' => $payload['content'],
'excerpt' => $payload['excerpt'],
'category' => $payload['categories'],
'tags' => implode(',', array_map('strval', $payload['tags'])),
'featured_data'=> $payload['featured'],
'schedule' => $payload['schedule'],
];
foreach ($payload['custom'] as $key => $value) {
$data['cf_' . sanitize_key((string) $key)] = $value;
@@ -259,11 +258,11 @@ final class CreatorRest {
$payload = [
'title' => (string) $request->get_param('title'),
'content' => (string) $request->get_param('content'),
'excerpt' => (string) $request->get_param('excerpt'),
'categories' => (array) $request->get_param('categories'),
'tags' => (array) $request->get_param('tags'),
'custom' => (array) $request->get_param('custom'),
'featured' => (array) $request->get_param('featured'),
'schedule' => (array) $request->get_param('schedule'),
'pay' => (array) $request->get_param('pay'),
];
@@ -275,7 +274,6 @@ final class CreatorRest {
$version = SubmissionVersion::get_instance()->save_working($post_id, $user_id, [
'title' => $payload['title'],
'content' => $payload['content'],
'excerpt' => $payload['excerpt'],
'base_rev' => absint($request->get_param('rev')),
'snapshot' => $this->snapshot_from_payload($payload),
], true);
@@ -299,10 +297,10 @@ final class CreatorRest {
'posts_id' => $post_id,
'post_title' => $payload['title'],
'post_content' => $payload['content'],
'excerpt' => $payload['excerpt'],
'category' => $payload['categories'],
'tags' => implode(',', array_map('strval', $payload['tags'])),
'featured_data' => $payload['featured'],
'schedule' => $payload['schedule'],
];
foreach ($payload['custom'] as $key => $value) {
$data['cf_' . sanitize_key((string) $key)] = $value;
@@ -691,6 +689,8 @@ final class CreatorRest {
'canPay' => $cap('new_post_pay'),
'canPayDownload' => $cap('new_post_pay_download'),
'canPublishNow' => $cap('new_post_audit_no'),
// 定时发布只有能直接发布的账号可用;其余账号提交后仍走审核队列。
'canSchedule' => $cap('new_post_audit_no'),
'canEdit' => $post_id > 0 ? $front->can_edit_post($post_id, $user_id) : $front->can_submit($user_id),
'isLoggedIn' => $user_id > 0,
];
+2
View File
@@ -215,6 +215,8 @@ final class CreatorStudio {
'root' => rest_url(),
'ajax' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('wp_rest'),
// 定时发布按站点时区解释;文案里显示给作者,避免 UTC 误解。
'timezone' => (string) wp_timezone_string(),
'captcha' => [
'enabled' => function_exists('jio_is_captcha_enabled_for') && jio_is_captcha_enabled_for('front_submit'),
],
+3 -3
View File
@@ -245,9 +245,9 @@ class FrontSubmit {
'enabled' => true,
'btn_text' => '发布文章',
'title_min' => 5,
'title_max' => 30,
'title_placeholder' => '请输入标题',
'content_placeholder' => '请输入内容',
'title_max' => 100,
'title_placeholder' => '请输入标题(最多 100 个字)',
'content_placeholder' => '请输入正文',
'allowed_cat_ids' => [],
'default_user_id' => 1,
'allow_tags' => true,
+59
View File
@@ -134,8 +134,17 @@ trait FrontSubmitSave {
}
$status = 'draft';
$schedule_at = '';
if (!$is_draft) {
$status = $this->resolve_publish_status($user_id);
if ($status === 'publish') {
// 定时发布:只有能直接发布的账号才排期,否则仍走审核队列。
$schedule = $this->normalize_schedule($data);
if ($schedule !== '') {
$schedule_at = $schedule;
$status = 'future';
}
}
}
// Paid fields are validated before anything is written, so an invalid
@@ -161,6 +170,11 @@ trait FrontSubmitSave {
'post_status' => $status,
'comment_status' => 'open',
];
if ($schedule_at !== '') {
// WordPress 按 post_date 到点把 future 转成 publish。
$postarr['post_date'] = $schedule_at;
$postarr['post_date_gmt'] = get_gmt_from_date($schedule_at);
}
if ($excerpt !== '' || array_key_exists('excerpt', $data) || array_key_exists('post_excerpt', $data)) {
$postarr['post_excerpt'] = $excerpt;
}
@@ -223,6 +237,18 @@ trait FrontSubmitSave {
];
switch ($status) {
case 'future':
$send['message'] = sprintf(
/* translators: %s: scheduled publish time */
__('文章已排期,将在 %s 发布', 'jio'),
$schedule_at
);
$send['scheduled_at'] = $schedule_at;
if ($user_id > 0 && function_exists('jio_get_user_home_url')) {
$send['reload'] = true;
$send['goto'] = jio_get_user_home_url($user_id);
}
break;
case 'pending':
$send['message'] = __('内容已提交,正在等待审核', 'jio');
if ($user_id > 0 && function_exists('jio_get_user_home_url')) {
@@ -247,6 +273,39 @@ trait FrontSubmitSave {
return $send;
}
/**
* 解析定时发布时间(站点本地时间 Y-m-d H:i:s);未开启或不合法时返回空串。
*
* @param array<string, mixed> $data
*/
private function normalize_schedule(array $data): string {
$schedule = $data['schedule'] ?? null;
if (!is_array($schedule) || empty($schedule['enabled'])) {
return '';
}
$raw = trim((string) ($schedule['at'] ?? ''));
if ($raw === '') {
return '';
}
// datetime-local 提交 Y-m-d\TH:i(部分浏览器带秒);统一成站点本地时间。
$normalized = str_replace('T', ' ', $raw);
if (preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/', $normalized) === 1) {
$normalized .= ':00';
}
$timezone = wp_timezone();
$scheduled = \DateTimeImmutable::createFromFormat('Y-m-d H:i:s', $normalized, $timezone);
$errors = \DateTimeImmutable::getLastErrors();
if (!$scheduled instanceof \DateTimeImmutable || (is_array($errors) && (int) $errors['warning_count'] > 0)) {
return '';
}
// 至少晚于当前时间 1 分钟,避免"排期到过去"被 WordPress 立即发布。
$threshold = new \DateTimeImmutable('now', $timezone);
if ($scheduled->getTimestamp() < $threshold->getTimestamp() + MINUTE_IN_SECONDS) {
return '';
}
return $scheduled->format('Y-m-d H:i:s');
}
/**
* Publish status for a non-draft submission.
*
+3 -3
View File
@@ -351,7 +351,7 @@ class PostDisplay {
? $settings['post_article_title_strlen_limit']
: [];
$title_min = max(1, min(100, (int) ($title_limit['min'] ?? 5)));
$title_max = max(5, min(200, (int) ($title_limit['max'] ?? 30)));
$title_max = max(5, min(200, (int) ($title_limit['max'] ?? 100)));
if ($title_max < $title_min) {
$title_max = $title_min;
}
@@ -368,8 +368,8 @@ class PostDisplay {
'btn_text' => sanitize_text_field((string) ($settings['post_article_btn_txte'] ?? '')) ?: __('发布文章', 'jio'),
'title_min' => $title_min,
'title_max' => $title_max,
'title_placeholder' => sanitize_text_field((string) ($settings['post_t_placeholder'] ?? '')) ?: __('请输入标题', 'jio'),
'content_placeholder' => sanitize_textarea_field((string) ($settings['post_c_placeholder'] ?? '')) ?: __('请输入内容', 'jio'),
'title_placeholder' => sanitize_text_field((string) ($settings['post_t_placeholder'] ?? '')) ?: __('请输入标题(最多 100 个字)', 'jio'),
'content_placeholder' => sanitize_textarea_field((string) ($settings['post_c_placeholder'] ?? '')) ?: __('请输入正文', 'jio'),
'allowed_cat_ids' => is_array($settings['post_article_cat'] ?? null) ? array_values(array_filter(array_map('intval', $settings['post_article_cat']))) : [],
'default_user_id' => max(0, (int) ($settings['post_article_user'] ?? 1)),
'allow_tags' => !isset($settings['post_article_tag']) || !empty($settings['post_article_tag']),