feat(ux): forum pay-hide depth, clogin OAuth, ban appeals admin, posts-navs widget

- Forum compose/save/draft: VIP prices, partial hide, coupons + shortcode tools
- Rainbow clogin aggregate login (rewrite + schema + CLoginClient)
- Native ban appeals tab: approve/reject + lift ban + notify
- WidgetPostsNavs + register missing forum widgets
- Submit editor shortcodes: login/reply/vip/payshow
This commit is contained in:
2026-07-18 23:31:57 +08:00
parent 25aabb71a3
commit 24330653f0
15 changed files with 834 additions and 35 deletions
@@ -24,6 +24,11 @@ export const oauthSchema = {
oauth_weibo_s: false,
oauth_baidu_s: false,
oauth_alipay_s: false,
clogin_s: false,
clogin_url: '',
clogin_id: '',
clogin_key: '',
clogin_oauth_s: ['qq', 'weixin', 'weibo', 'alipay'],
github_appid: '',
github_appkey: '',
gitee_appid: '',
@@ -92,6 +97,7 @@ export const oauthSchema = {
{ key: 'oauth_weibo_s', label: '微博登录', type: 'switch', default: false },
{ key: 'oauth_baidu_s', label: '百度登录', type: 'switch', default: false },
{ key: 'oauth_alipay_s', label: '支付宝登录', type: 'switch', default: false },
{ key: 'clogin_s', label: '彩虹聚合登录', type: 'switch', default: false, help: '通过第三方聚合登录平台统一接入 QQ/微信/微博/支付宝等' },
],
},
{
@@ -174,6 +180,23 @@ export const oauthSchema = {
{ key: 'baidu_appkey', label: 'Secret Key', type: 'input', default: '' },
],
},
{
key: 'clogin',
title: '彩虹聚合登录',
icon: 'fas fa-cloud',
fields: [
{ key: 'clogin_url', label: '接口地址', type: 'input', default: '', help: '如 https://xxx.com 不要带 connect.php', dependsOn: { key: 'clogin_s', value: true } },
{ key: 'clogin_id', label: 'AppID', type: 'input', default: '', dependsOn: { key: 'clogin_s', value: true } },
{ key: 'clogin_key', label: 'AppKey', type: 'input', default: '', dependsOn: { key: 'clogin_s', value: true } },
{ key: 'clogin_oauth_s', label: '启用平台', type: 'checkbox', default: ['qq', 'weixin', 'weibo', 'alipay'], options: [
{ label: 'QQ', value: 'qq' },
{ label: '微信', value: 'weixin' },
{ label: '微博', value: 'weibo' },
{ label: '支付宝', value: 'alipay' },
{ label: '百度', value: 'baidu' },
], dependsOn: { key: 'clogin_s', value: true } },
],
},
{
key: 'alipay',
title: '支付宝配置',
+38
View File
@@ -621,6 +621,44 @@
});
}
function insertForumEditorContent(html) {
if (window.tinymce) {
var ed = window.tinymce.get('compose-content') || window.tinymce.get('post_content');
if (ed && !ed.isHidden()) {
ed.focus();
ed.execCommand('mceInsertContent', false, html);
return;
}
}
var ta = document.getElementById('compose-content') || document.getElementById('post_content');
if (!ta) return;
var start = ta.selectionStart || 0;
var end = ta.selectionEnd || 0;
var val = ta.value || '';
ta.value = val.slice(0, start) + html + val.slice(end);
ta.focus();
}
function bindComposeShortcodeTools(root) {
root = root || document;
root.addEventListener('click', function (e) {
var btn = e.target.closest('[data-insert-shortcode]');
if (!btn) return;
e.preventDefault();
var type = btn.getAttribute('data-insert-shortcode');
if (type === 'hide') {
insertForumEditorContent('[hidecontent type="login"]隐藏内容[/hidecontent]');
} else if (type === 'reply') {
insertForumEditorContent('[hidecontent type="reply"]回复后可见[/hidecontent]');
} else if (type === 'payshow') {
insertForumEditorContent('[hidecontent type="payshow"]付费可见内容[/hidecontent]');
} else if (type === 'vip') {
insertForumEditorContent('[vip]VIP 可见内容[/vip]');
}
});
}
function bindComposeVisibilityToggle(form) {
var visSelect = form.querySelector('#compose-visibility');
var creditsField = form.querySelector('#compose-credits-cost-field');
+6 -2
View File
@@ -543,9 +543,13 @@
e.preventDefault();
var type = btn.getAttribute('data-insert-shortcode');
if (type === 'hide') {
insertEditorContent('[hidecontent type="logged"]hidden[/hidecontent]');
insertEditorContent('[hidecontent type="login"]' + (i18n.hidePlaceholder || '隐藏内容') + '[/hidecontent]');
} else if (type === 'payshow') {
insertEditorContent('[hidecontent type="payshow"]paid content[/hidecontent]');
insertEditorContent('[hidecontent type="payshow"]' + (i18n.payPlaceholder || '付费可见内容') + '[/hidecontent]');
} else if (type === 'reply') {
insertEditorContent('[hidecontent type="reply"]' + (i18n.replyPlaceholder || '回复后可见') + '[/hidecontent]');
} else if (type === 'vip') {
insertEditorContent('[vip]' + (i18n.vipPlaceholder || 'VIP 可见内容') + '[/vip]');
}
});
},
+14
View File
@@ -50,6 +50,20 @@ function aksrt_register_widgets() {
// 一言轮播
register_widget('\AKSRT\Widgets\WidgetYiyan');
// 文章目录树(zibll posts_navs)
register_widget('\AKSRT\Widgets\WidgetPostsNavs');
// Forum widgets
if (class_exists('\AKSRT\Widgets\WidgetForumBoardList')) {
register_widget('\AKSRT\Widgets\WidgetForumBoardList');
}
if (class_exists('\AKSRT\Widgets\WidgetForumHotTopics')) {
register_widget('\AKSRT\Widgets\WidgetForumHotTopics');
}
if (class_exists('\AKSRT\Widgets\WidgetForumRecentTopics')) {
register_widget('\AKSRT\Widgets\WidgetForumRecentTopics');
}
// Shop product widgets (§6.9)
register_widget('\AKSRT\Widgets\WidgetProductList');
register_widget('\AKSRT\Widgets\WidgetProductOneline');
+167
View File
@@ -32,6 +32,7 @@ class UsersMenus {
add_action('admin_post_aksrt_users_update_report', [$this, 'handle_update_report']);
add_action('admin_post_aksrt_users_add_restriction', [$this, 'handle_add_restriction']);
add_action('admin_post_aksrt_users_remove_restriction', [$this, 'handle_remove_restriction']);
add_action('admin_post_aksrt_users_process_appeal', [$this, 'handle_process_appeal']);
add_action('admin_post_aksrt_users_batch_invite_codes', [$this, 'handle_batch_invite_codes']);
add_action('admin_post_aksrt_users_disable_invite_code', [$this, 'handle_disable_invite_code']);
}
@@ -108,6 +109,21 @@ class UsersMenus {
echo '<p><a class="button button-primary" href="' . esc_url($url) . '">' . esc_html__('立即处理', 'aksrt') . '</a></p></div>';
}
}
if ($page !== 'aksrt_user_ban' && class_exists('\AKSRT\User\BanAppeal')) {
$pending_appeals = count(get_users([
'meta_key' => \AKSRT\User\BanAppeal::META_STATUS,
'meta_value' => 'pending',
'number' => 50,
'fields' => 'ID',
]));
if ($pending_appeals > 0) {
$url = admin_url('users.php?page=aksrt_user_ban&tab=appeals&status=pending');
echo '<div class="notice notice-warning is-dismissible"><h3>' . esc_html__('禁封申诉待处理', 'aksrt') . '</h3>';
echo '<p>' . esc_html(sprintf(__('您有 %d 条禁封申诉待处理', 'aksrt'), $pending_appeals)) . '</p>';
echo '<p><a class="button button-primary" href="' . esc_url($url) . '">' . esc_html__('立即处理', 'aksrt') . '</a></p></div>';
}
}
}
private function ensure_cap(): void {
@@ -131,6 +147,9 @@ class UsersMenus {
'restriction_added' => __('限制已添加。', 'aksrt'),
'restriction_removed' => __('限制已移除。', 'aksrt'),
'restriction_failed' => __('限制操作失败。', 'aksrt'),
'appeal_approved' => __('申诉已通过,限制已解除。', 'aksrt'),
'appeal_rejected' => __('申诉已驳回。', 'aksrt'),
'appeal_failed' => __('申诉处理失败。', 'aksrt'),
'invite_batch_ok' => __('邀请码批量生成完成。', 'aksrt'),
'invite_batch_failed' => __('邀请码批量生成失败。', 'aksrt'),
'invite_disabled' => __('邀请码已禁用。', 'aksrt'),
@@ -590,10 +609,13 @@ class UsersMenus {
echo '<h2 class="nav-tab-wrapper">';
printf('<a class="nav-tab%s" href="%s">%s</a>', $tab === 'reports' ? ' nav-tab-active' : '', esc_url(admin_url('users.php?page=aksrt_user_ban&tab=reports')), esc_html__('举报', 'aksrt'));
printf('<a class="nav-tab%s" href="%s">%s</a>', $tab === 'restrictions' ? ' nav-tab-active' : '', esc_url(admin_url('users.php?page=aksrt_user_ban&tab=restrictions')), esc_html__('禁封限制', 'aksrt'));
printf('<a class="nav-tab%s" href="%s">%s</a>', $tab === 'appeals' ? ' nav-tab-active' : '', esc_url(admin_url('users.php?page=aksrt_user_ban&tab=appeals')), esc_html__('禁封申诉', 'aksrt'));
echo '</h2>';
if ($tab === 'restrictions') {
$this->render_restrictions_tab();
} elseif ($tab === 'appeals') {
$this->render_appeals_tab();
} else {
$this->render_reports_tab();
}
@@ -768,6 +790,151 @@ class UsersMenus {
$this->redirect('aksrt_user_ban', $ok ? 'restriction_removed' : 'restriction_failed', ['tab' => 'restrictions']);
}
private function render_appeals_tab(): void {
if (!class_exists('\AKSRT\User\BanAppeal')) {
echo '<div class="notice notice-error"><p>' . esc_html__('申诉模块未初始化。', 'aksrt') . '</p></div>';
return;
}
$status = isset($_GET['status']) ? sanitize_key((string) wp_unslash($_GET['status'])) : 'pending';
if (!in_array($status, ['pending', 'approved', 'rejected', 'all'], true)) {
$status = 'pending';
}
echo '<form method="get" style="margin:12px 0;">';
echo '<input type="hidden" name="page" value="aksrt_user_ban" /><input type="hidden" name="tab" value="appeals" />';
echo '<select name="status">';
foreach (['pending' => __('待处理', 'aksrt'), 'approved' => __('已通过', 'aksrt'), 'rejected' => __('已驳回', 'aksrt'), 'all' => __('全部', 'aksrt')] as $key => $label) {
printf('<option value="%s"%s>%s</option>', esc_attr($key), selected($status, $key, false), esc_html($label));
}
echo '</select> ';
submit_button(__('筛选', 'aksrt'), 'secondary', '', false);
echo '</form>';
$users = get_users([
'meta_key' => \AKSRT\User\BanAppeal::META_STATUS,
'meta_compare' => 'EXISTS',
'number' => 100,
'fields' => ['ID', 'display_name', 'user_login', 'user_email'],
'orderby' => 'ID',
'order' => 'DESC',
]);
echo '<table class="wp-list-table widefat fixed striped"><thead><tr>';
echo '<th>UID</th><th>' . esc_html__('用户', 'aksrt') . '</th><th>' . esc_html__('状态', 'aksrt') . '</th><th>' . esc_html__('类型', 'aksrt') . '</th><th>' . esc_html__('说明', 'aksrt') . '</th><th>' . esc_html__('提交时间', 'aksrt') . '</th><th>' . esc_html__('操作', 'aksrt') . '</th>';
echo '</tr></thead><tbody>';
$shown = 0;
foreach ($users as $u) {
$uid = (int) $u->ID;
$st = \AKSRT\User\BanAppeal::get_status($uid);
if ($st === '') {
continue;
}
if ($status !== 'all' && $st !== $status) {
continue;
}
$payload = \AKSRT\User\BanAppeal::get_payload($uid);
$shown++;
echo '<tr>';
echo '<td>' . esc_html((string) $uid) . '</td>';
echo '<td>' . esc_html($u->display_name . ' (' . $u->user_login . ')') . '<br><span class="description">' . esc_html($u->user_email) . '</span></td>';
echo '<td>' . esc_html($st) . '</td>';
echo '<td>' . esc_html((string) ($payload['reason_key'] ?? '—')) . '</td>';
echo '<td>' . esc_html((string) ($payload['reason'] ?? '')) . '</td>';
echo '<td>' . esc_html((string) ($payload['time'] ?? '—')) . '</td>';
echo '<td>';
if ($st === 'pending') {
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="display:flex;gap:6px;flex-wrap:wrap;align-items:center;">';
echo '<input type="hidden" name="action" value="aksrt_users_process_appeal" />';
echo '<input type="hidden" name="user_id" value="' . esc_attr((string) $uid) . '" />';
wp_nonce_field('aksrt_users_process_appeal_' . $uid);
echo '<input type="text" name="note" placeholder="' . esc_attr__('处理备注', 'aksrt') . '" class="regular-text" />';
echo '<button type="submit" name="decision" value="approve" class="button button-primary">' . esc_html__('通过并解封', 'aksrt') . '</button>';
echo '<button type="submit" name="decision" value="reject" class="button">' . esc_html__('驳回', 'aksrt') . '</button>';
echo '</form>';
} else {
echo '—';
}
echo '</td></tr>';
}
if ($shown < 1) {
echo '<tr><td colspan="7">' . esc_html__('暂无申诉', 'aksrt') . '</td></tr>';
}
echo '</tbody></table>';
}
public function handle_process_appeal(): void {
if (!current_user_can(self::CAPABILITY)) {
wp_die(esc_html__('权限不足', 'aksrt'));
}
$user_id = isset($_POST['user_id']) ? (int) $_POST['user_id'] : 0;
$decision = isset($_POST['decision']) ? sanitize_key((string) wp_unslash($_POST['decision'])) : '';
$note = isset($_POST['note']) ? sanitize_textarea_field((string) wp_unslash($_POST['note'])) : '';
check_admin_referer('aksrt_users_process_appeal_' . $user_id);
if ($user_id <= 0 || !in_array($decision, ['approve', 'reject'], true) || !class_exists('\AKSRT\User\BanAppeal')) {
$this->redirect('aksrt_user_ban', 'appeal_failed', ['tab' => 'appeals']);
}
$status = $decision === 'approve' ? 'approved' : 'rejected';
update_user_meta($user_id, \AKSRT\User\BanAppeal::META_STATUS, $status);
$payload = \AKSRT\User\BanAppeal::get_payload($user_id);
$payload['admin_note'] = $note;
$payload['handled_at'] = current_time('Y-m-d H:i');
$payload['handled_by'] = get_current_user_id();
$payload['decision'] = $status;
update_user_meta($user_id, \AKSRT\User\BanAppeal::META_PAYLOAD, $payload);
$history = get_user_meta($user_id, \AKSRT\User\BanAppeal::META_HISTORY, true);
$history = is_array($history) ? $history : [];
$history[] = $payload;
update_user_meta($user_id, \AKSRT\User\BanAppeal::META_HISTORY, array_slice($history, -30));
if ($decision === 'approve' && class_exists('\AKSRT\User\Restriction')) {
global $wpdb;
$table = class_exists('\AKSRT\Core\DBMigrator') ? \AKSRT\Core\DBMigrator::table('user_restriction') : '';
if ($table !== '') {
$wpdb->query($wpdb->prepare(
"UPDATE {$table} SET status = 'lifted', expires_at = %s WHERE user_id = %d AND type IN ('ban','suspend','mute')",
current_time('mysql'),
$user_id
));
}
$svc = \AKSRT\User\Restriction::get_instance();
if (method_exists($svc, 'clear_user_bans')) {
$svc->clear_user_bans($user_id);
} elseif (method_exists($svc, 'lift_ban')) {
$svc->lift_ban($user_id);
}
}
if (class_exists('\AKSRT\Messages\Message')) {
$msg = $decision === 'approve'
? __('你的禁封申诉已通过,限制已解除。', 'aksrt')
: __('你的禁封申诉未通过,可修改后重新提交。', 'aksrt');
if ($note !== '') {
$msg .= ' ' . sprintf(__('备注:%s', 'aksrt'), $note);
}
\AKSRT\Messages\Message::get_instance()->send(
$user_id,
'ban_appeal',
$msg,
[
'title' => $decision === 'approve' ? __('申诉通过', 'aksrt') : __('申诉驳回', 'aksrt'),
]
);
}
do_action('aksrt_ban_appeal_processed', $user_id, $decision, $note);
$this->redirect(
'aksrt_user_ban',
$decision === 'approve' ? 'appeal_approved' : 'appeal_rejected',
['tab' => 'appeals']
);
}
private function pagination(int $pages, int $page_num): void {
if ($pages <= 1) {
return;
+13
View File
@@ -254,6 +254,7 @@ class SchemaUsers {
['key' => 'oauth_weibo_s', 'label' => '微博登录', 'type' => 'switch', 'default' => false],
['key' => 'oauth_baidu_s', 'label' => '百度登录', 'type' => 'switch', 'default' => false],
['key' => 'oauth_alipay_s', 'label' => '支付宝登录', 'type' => 'switch', 'default' => false],
['key' => 'clogin_s', 'label' => '彩虹聚合登录', 'type' => 'switch', 'default' => false],
]],
['key' => 'github', 'title' => 'GitHub配置', 'fields' => [
['key' => 'github_appid', 'label' => 'Client ID', 'type' => 'input', 'default' => ''],
@@ -302,6 +303,18 @@ class SchemaUsers {
['key' => 'baidu', 'title' => '百度配置', 'fields' => [
['key' => 'baidu_appid', 'label' => 'API Key', 'type' => 'input', 'default' => ''],
['key' => 'baidu_appkey', 'label' => 'Secret Key', 'type' => 'input', 'default' => ''],
]],
['key' => 'clogin', 'title' => '彩虹聚合登录', 'fields' => [
['key' => 'clogin_url', 'label' => '接口地址', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'clogin_s', 'value' => true], 'help' => '如 https://xxx.com'],
['key' => 'clogin_id', 'label' => 'AppID', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'clogin_s', 'value' => true]],
['key' => 'clogin_key', 'label' => 'AppKey', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'clogin_s', 'value' => true]],
['key' => 'clogin_oauth_s', 'label' => '启用平台', 'type' => 'checkbox', 'default' => ['qq', 'weixin', 'weibo', 'alipay'], 'options' => [
['label' => 'QQ', 'value' => 'qq'],
['label' => '微信', 'value' => 'weixin'],
['label' => '微博', 'value' => 'weibo'],
['label' => '支付宝', 'value' => 'alipay'],
['label' => '百度', 'value' => 'baidu'],
], 'dependsOn' => ['key' => 'clogin_s', 'value' => true]],
]],
['key' => 'alipay', 'title' => '支付宝配置', 'fields' => [
['key' => 'alipay_appid', 'label' => 'AppID', 'type' => 'input', 'default' => ''],
+106
View File
@@ -0,0 +1,106 @@
<?php
/**
* CaiHong / rainbow aggregate OAuth client (zibll clogin parity).
*
* @package AKSRT
*/
namespace AKSRT\Auth;
defined('ABSPATH') || exit;
class CLoginClient {
private string $apiurl;
private string $appid;
private string $appkey;
private string $callback;
public string $state = '';
/**
* @param array{apiurl:string,appid:string,appkey:string,callback?:string} $config
*/
public function __construct(array $config) {
$this->apiurl = rtrim((string) ($config['apiurl'] ?? ''), '/') . '/connect.php';
$this->appid = (string) ($config['appid'] ?? '');
$this->appkey = (string) ($config['appkey'] ?? '');
$this->callback = (string) ($config['callback'] ?? '');
}
/**
* @return array<string,mixed>
*/
public function login(string $type): array {
$this->state = md5(uniqid((string) wp_rand(), true));
$params = [
'act' => 'login',
'appid' => $this->appid,
'appkey' => $this->appkey,
'type' => $this->to_api_type($type),
'redirect_uri' => $this->callback,
'state' => $this->state,
];
return $this->request($params);
}
/**
* @return array<string,mixed>
*/
public function callback(string $code): array {
return $this->request([
'act' => 'callback',
'appid' => $this->appid,
'appkey' => $this->appkey,
'code' => $code,
]);
}
public function to_api_type(string $type): string {
switch (sanitize_key($type)) {
case 'weixin':
case 'wechat':
return 'wx';
case 'weibo':
return 'sina';
default:
return sanitize_key($type);
}
}
public function from_api_type(string $type): string {
switch (sanitize_key($type)) {
case 'wx':
return 'weixin';
case 'sina':
return 'weibo';
default:
return sanitize_key($type);
}
}
/**
* @param array<string,string> $params
* @return array<string,mixed>
*/
private function request(array $params): array {
$url = $this->apiurl . '?' . http_build_query($params);
$response = wp_remote_get($url, [
'timeout' => 15,
'headers' => [
'User-Agent' => 'AKSRT-CLogin/1.0',
],
]);
if (is_wp_error($response)) {
return [
'code' => 1,
'msg' => $response->get_error_message(),
];
}
$body = (string) wp_remote_retrieve_body($response);
$data = json_decode($body, true);
return is_array($data) ? $data : [
'code' => 1,
'msg' => __('聚合登录接口返回异常', 'aksrt'),
];
}
}
+12
View File
@@ -24,12 +24,15 @@ class OAuth {
public function init(): void {
add_action('init', [$this, 'add_rewrite_rules']);
add_action('template_redirect', [$this, 'handle_callback']);
add_action('template_redirect', [$this, 'handle_clogin_routes'], 9);
add_action('show_user_profile', [$this, 'profile_bindings'], 10, 1);
add_action('wp_ajax_aksrt_oauth_unbind', [$this, 'ajax_unbind']);
}
public function add_rewrite_rules(): void {
add_rewrite_rule('^oauth/callback/?$', 'index.php?aksrt_oauth=callback', 'top');
add_rewrite_rule('^oauth/clogin/?$', 'index.php?aksrt_oauth=clogin', 'top');
add_rewrite_rule('^oauth/clogin/callback/?$', 'index.php?aksrt_oauth=clogin_callback', 'top');
add_filter('query_vars', function($vars) {
$vars[] = 'aksrt_oauth';
return $vars;
@@ -40,6 +43,15 @@ class OAuth {
* 生成OAuth授权URL
*/
public function get_auth_url(string $provider, string $action = 'login'): string {
$provider = sanitize_key($provider);
if (str_starts_with($provider, 'clogin_') || str_starts_with($provider, 'clogin-')) {
$type = str_replace(['clogin_', 'clogin-'], '', $provider);
return $this->get_clogin_login_url($type, $action);
}
if ($provider === 'clogin') {
return $this->get_clogin_login_url('qq', $action);
}
if (class_exists('\AKSRT\Settings\Settings') && \AKSRT\Settings\Settings::oauth_proxy_mode() === 'client') {
if (class_exists('AKSRT\\Auth\\OAuthProxyClient')) {
return OAuthProxyClient::get_instance()->get_proxy_auth_url($provider, $action);
+242 -29
View File
@@ -217,7 +217,28 @@ trait OAuthPart1 {
}
}
}
return array_unique($enabled);
// Rainbow aggregate (clogin) providers as virtual keys: clogin_qq, clogin_weixin...
if (class_exists('\AKSRT\Settings\Settings')) {
$s = \AKSRT\Settings\Settings::get_all();
if (!empty($s['clogin_s']) && !empty($s['clogin_url']) && !empty($s['clogin_id']) && !empty($s['clogin_key'])) {
$raw = $s['clogin_oauth_s'] ?? $s['clogin_types'] ?? ['qq', 'weixin', 'weibo', 'alipay'];
if (is_string($raw)) {
$raw = preg_split('/[,,\s]+/', $raw) ?: [];
}
if (!is_array($raw)) {
$raw = [];
}
foreach ($raw as $ctype) {
$ctype = sanitize_key((string) $ctype);
if ($ctype === '') {
continue;
}
$enabled[] = 'clogin_' . $ctype;
}
}
}
return array_values(array_unique($enabled));
}
/**
@@ -242,10 +263,20 @@ trait OAuthPart1 {
$url = $this->get_auth_url($key);
if (empty($url)) continue;
$label = $platform_names[$key] ?? '';
$icon_key = $key;
if (str_starts_with($key, 'clogin_')) {
$ctype = substr($key, 7);
$label = ($platform_names[$ctype] ?? strtoupper($ctype)) . ' · 聚合';
$icon_key = $ctype === 'weixin' ? 'weixin' : $ctype;
}
if ($label === '') {
$label = ucfirst($key);
}
$result[] = [
'name' => $key,
'label' => $platform_names[$key] ?? ucfirst($key),
'icon' => function_exists('aksrt_brand_icon') ? aksrt_brand_icon((string) $key) : '',
'label' => $label,
'icon' => function_exists('aksrt_brand_icon') ? aksrt_brand_icon((string) $icon_key) : '',
'url' => esc_url($url),
];
}
@@ -345,42 +376,28 @@ trait OAuthPart1 {
}
public function render_social_buttons(string $title = ''): string {
$providers = get_option('aksrt_oauth', []);
$enabled_providers = [];
foreach ($providers as $key => $cfg) {
if (!empty($cfg['enabled']) && !empty($cfg['client_id']) && in_array($key, ['github', 'gitee', 'qq', 'google', 'weibo', 'weixin', 'baidu', 'alipay'], true)) {
$enabled_providers[$key] = $cfg;
}
$items = $this->get_auth_providers_data();
if (empty($items)) {
return '';
}
if (empty($enabled_providers)) return '';
$icons = [
'github' => aksrt_brand_icon('github'),
'gitee' => aksrt_brand_icon('gitee'),
'qq' => aksrt_brand_icon('qq'),
'google' => aksrt_brand_icon('google'),
'weibo' => aksrt_brand_icon('weibo'),
'weixin' => aksrt_brand_icon('weixin'),
'baidu' => aksrt_brand_icon('baidu'),
'alipay' => aksrt_brand_icon('alipay'),
];
ob_start();
?>
<div class="social-login-divider">
<span><?php echo esc_html($title) ?: esc_html__('or', 'aksrt'); ?></span>
</div>
<div class="oauth-row">
<?php foreach ($enabled_providers as $key => $cfg):
$url = $this->get_auth_url($key);
if (empty($url)) continue;
<?php foreach ($items as $item):
$key = (string) ($item['name'] ?? '');
$url = (string) ($item['url'] ?? '');
if ($key === '' || $url === '') continue;
?>
<a href="<?php echo esc_url($url); ?>"
class="oauth-btn"
aria-label="<?php echo esc_attr(ucfirst($key)); ?>"
title="<?php echo esc_attr(ucfirst($key)); ?>">
<?php echo $icons[$key] ?? ''; ?>
class="oauth-btn oauth-btn--<?php echo esc_attr($key); ?>"
data-provider="<?php echo esc_attr($key); ?>"
aria-label="<?php echo esc_attr((string) ($item['label'] ?? $key)); ?>"
title="<?php echo esc_attr((string) ($item['label'] ?? $key)); ?>">
<?php echo $item['icon'] ?? ''; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
</a>
<?php endforeach; ?>
</div>
@@ -399,4 +416,200 @@ trait OAuthPart1 {
public function register_message(string $message): string {
return $message . self::get_instance()->render_social_buttons(__('或使用社交账号快速注册', 'aksrt'));
}
/**
* @return array{enabled:bool,url:string,id:string,key:string,types:string[]}
*/
private function get_clogin_settings(): array {
$s = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_all() : [];
$raw = $s['clogin_oauth_s'] ?? $s['clogin_types'] ?? ['qq', 'weixin', 'weibo', 'alipay'];
if (is_string($raw)) {
$raw = preg_split('/[,,\s]+/', $raw) ?: [];
}
if (!is_array($raw)) {
$raw = [];
}
$types = [];
foreach ($raw as $t) {
$t = sanitize_key((string) $t);
if ($t !== '') {
$types[] = $t;
}
}
return [
'enabled' => !empty($s['clogin_s']),
'url' => esc_url_raw((string) ($s['clogin_url'] ?? '')),
'id' => (string) ($s['clogin_id'] ?? ''),
'key' => (string) ($s['clogin_key'] ?? ''),
'types' => array_values(array_unique($types)),
];
}
private function get_clogin_login_url(string $type, string $action = 'login'): string {
$cfg = $this->get_clogin_settings();
$type = sanitize_key($type);
if (!$cfg['enabled'] || $cfg['url'] === '' || $cfg['id'] === '' || $cfg['key'] === '' || $type === '') {
return '';
}
if (!empty($cfg['types']) && !in_array($type, $cfg['types'], true)) {
return '';
}
$args = ['type' => $type];
if ($action === 'bind' && is_user_logged_in()) {
$args['bind'] = 1;
}
$rurl = isset($_GET['redirect_to']) ? esc_url_raw(wp_unslash((string) $_GET['redirect_to'])) : '';
if ($rurl !== '') {
$args['rurl'] = $rurl;
}
return add_query_arg($args, home_url('/oauth/clogin/'));
}
public function handle_clogin_routes(): void {
$route = get_query_var('aksrt_oauth');
if ($route === 'clogin') {
$this->handle_clogin_login();
return;
}
if ($route === 'clogin_callback') {
$this->handle_clogin_callback();
}
}
private function handle_clogin_login(): void {
$cfg = $this->get_clogin_settings();
$type = isset($_GET['type']) ? sanitize_key(wp_unslash((string) $_GET['type'])) : '';
if (!$cfg['enabled'] || $cfg['url'] === '' || $cfg['id'] === '' || $cfg['key'] === '' || $type === '') {
wp_die(esc_html__('暂未启用此登录方式', 'aksrt'));
}
if (!empty($cfg['types']) && !in_array($type, $cfg['types'], true)) {
wp_die(esc_html__('此登录方式未启用', 'aksrt'));
}
if (!class_exists(CLoginClient::class)) {
wp_die(esc_html__('聚合登录组件未加载', 'aksrt'));
}
$client = new CLoginClient([
'apiurl' => $cfg['url'],
'appid' => $cfg['id'],
'appkey' => $cfg['key'],
'callback' => home_url('/oauth/clogin/callback'),
]);
$resp = $client->login($type);
if (!empty($resp['code']) && (int) $resp['code'] !== 0 && !empty($resp['msg'])) {
wp_die(esc_html(sprintf(__('处理失败:%s', 'aksrt'), (string) $resp['msg'])));
}
if (empty($resp['url'])) {
wp_die(esc_html__('无法获取聚合登录地址', 'aksrt'));
}
if (!session_id()) {
@session_start();
}
$_SESSION['AKSRT_CLOGIN_STATE'] = $client->state;
$_SESSION['AKSRT_CLOGIN_TYPE'] = $type;
$_SESSION['AKSRT_CLOGIN_RURL'] = !empty($_GET['rurl']) ? esc_url_raw(wp_unslash((string) $_GET['rurl'])) : '';
$_SESSION['AKSRT_CLOGIN_BIND'] = !empty($_GET['bind']) ? 1 : 0;
wp_redirect(esc_url_raw((string) $resp['url']));
exit;
}
private function handle_clogin_callback(): void {
if (!session_id()) {
@session_start();
}
$state = isset($_GET['state']) ? (string) wp_unslash($_GET['state']) : '';
$code = isset($_GET['code']) ? (string) wp_unslash($_GET['code']) : '';
if ($code === '' || $state === '' || empty($_SESSION['AKSRT_CLOGIN_STATE']) || !hash_equals((string) $_SESSION['AKSRT_CLOGIN_STATE'], $state)) {
wp_die(esc_html__('环境异常,state 验证失败,请刷新后重试', 'aksrt'));
}
$cfg = $this->get_clogin_settings();
if (!$cfg['enabled']) {
wp_die(esc_html__('暂未启用此功能', 'aksrt'));
}
$client = new CLoginClient([
'apiurl' => $cfg['url'],
'appid' => $cfg['id'],
'appkey' => $cfg['key'],
]);
$info = $client->callback($code);
if (!empty($info['code']) && (int) $info['code'] !== 0 && !empty($info['msg'])) {
wp_die(esc_html((string) $info['msg']));
}
$openid = (string) ($info['social_uid'] ?? '');
$api_type = (string) ($info['type'] ?? ($_SESSION['AKSRT_CLOGIN_TYPE'] ?? ''));
$provider_type = $client->from_api_type($api_type);
if ($openid === '' || $provider_type === '') {
wp_die(esc_html__('无法获取用户信息', 'aksrt'));
}
$provider = 'clogin_' . sanitize_key($provider_type);
$user_data = [
'provider' => $provider,
'provider_id' => $openid,
'username' => (string) ($info['nickname'] ?? ''),
'name' => (string) ($info['nickname'] ?? ''),
'email' => '',
'avatar_url' => (string) ($info['faceimg'] ?? ''),
];
if ($user_data['avatar_url'] !== '' && str_contains($user_data['avatar_url'], 'no_portrait')) {
$user_data['avatar_url'] = '';
}
$is_bind = !empty($_SESSION['AKSRT_CLOGIN_BIND']);
$rurl = !empty($_SESSION['AKSRT_CLOGIN_RURL']) ? (string) $_SESSION['AKSRT_CLOGIN_RURL'] : '';
unset($_SESSION['AKSRT_CLOGIN_STATE'], $_SESSION['AKSRT_CLOGIN_TYPE'], $_SESSION['AKSRT_CLOGIN_RURL'], $_SESSION['AKSRT_CLOGIN_BIND']);
try {
if ($is_bind) {
$uid = get_current_user_id();
if ($uid <= 0) {
wp_safe_redirect(function_exists('aksrt_get_login_url') ? aksrt_get_login_url() : wp_login_url());
exit;
}
$existing = $this->find_user_by_provider($provider, $openid);
if ($existing && (int) $existing->ID !== $uid) {
throw new \Exception(__('该第三方账号已被其他用户绑定', 'aksrt'));
}
$this->bind_provider_to_user($uid, $provider, $user_data);
$redirect = function_exists('aksrt_get_user_center_url') ? aksrt_get_user_center_url() : home_url('/');
wp_safe_redirect(add_query_arg(['tab' => 'settings', 'oauth_action' => 'bind', 'oauth_bound' => $provider], $redirect));
exit;
}
$user = $this->find_or_create_user($provider, $user_data);
if (is_wp_error($user) && $user->get_error_code() === 'oauth_no_match') {
// Auto-create account for aggregate login when no match (zibll-like).
$login = 'u' . wp_generate_password(10, false, false);
$uid = wp_insert_user([
'user_login' => $login,
'user_pass' => wp_generate_password(24, true, true),
'display_name' => $user_data['name'] !== '' ? $user_data['name'] : $login,
'nickname' => $user_data['name'] !== '' ? $user_data['name'] : $login,
'role' => get_option('default_role', 'subscriber'),
]);
if (is_wp_error($uid)) {
throw new \Exception($uid->get_error_message());
}
$this->bind_provider_to_user((int) $uid, $provider, $user_data);
$user = get_user_by('id', (int) $uid);
}
if (is_wp_error($user) || !$user instanceof \WP_User) {
throw new \Exception(is_wp_error($user) ? $user->get_error_message() : __('登录失败', 'aksrt'));
}
wp_set_current_user($user->ID);
wp_set_auth_cookie($user->ID, true);
do_action('wp_login', $user->user_login, $user);
$redirect = $rurl !== '' ? $rurl : (function_exists('aksrt_get_user_center_url') ? aksrt_get_user_center_url() : home_url('/'));
wp_safe_redirect(add_query_arg(['oauth_action' => 'login', 'oauth_bound' => $provider], $redirect));
exit;
} catch (\Exception $e) {
wp_die(esc_html__('登录失败:', 'aksrt') . '<br>' . esc_html($e->getMessage()));
}
}
}
+35 -1
View File
@@ -78,11 +78,33 @@ class DraftService {
}
if (isset($data['credits_cost'])) {
update_post_meta($post_id, '_aksrt_credits_cost', max(0, (int) $data['credits_cost']));
$cost = max(0, (int) $data['credits_cost']);
update_post_meta($post_id, '_aksrt_credits_cost', $cost);
update_post_meta($post_id, '_aksrt_pay_credits', $cost);
}
if (isset($data['pay_price'])) {
update_post_meta($post_id, '_aksrt_pay_price', sanitize_text_field((string) $data['pay_price']));
}
if (isset($data['credits_vip1']) && (string) $data['credits_vip1'] !== '') {
update_post_meta($post_id, '_aksrt_credits_vip1', max(0, (int) $data['credits_vip1']));
}
if (isset($data['credits_vip2']) && (string) $data['credits_vip2'] !== '') {
update_post_meta($post_id, '_aksrt_credits_vip2', max(0, (int) $data['credits_vip2']));
}
if (isset($data['pay_vip1_price']) && (string) $data['pay_vip1_price'] !== '') {
$v1 = max(0, (float) $data['pay_vip1_price']);
update_post_meta($post_id, '_aksrt_pay_vip1_price', $v1);
update_post_meta($post_id, '_aksrt_pay_vip_price', $v1);
}
if (isset($data['pay_vip2_price']) && (string) $data['pay_vip2_price'] !== '') {
update_post_meta($post_id, '_aksrt_pay_vip2_price', max(0, (float) $data['pay_vip2_price']));
}
if (array_key_exists('pay_hide_part', $data)) {
update_post_meta($post_id, '_aksrt_pay_hide_part', !empty($data['pay_hide_part']) ? 1 : 0);
}
if (array_key_exists('topic_coupon_s', $data)) {
update_post_meta($post_id, '_aksrt_topic_coupon_s', !empty($data['topic_coupon_s']) ? 1 : 0);
}
if (isset($data['visibility_roles']) && is_array($data['visibility_roles'])) {
update_post_meta($post_id, '_aksrt_visibility_roles', array_map('sanitize_key', $data['visibility_roles']));
}
@@ -236,6 +258,12 @@ class DraftService {
'featured_video' => get_post_meta($draft_id, '_aksrt_featured_video', true) ?: '',
'credits_cost' => (int) get_post_meta($draft_id, '_aksrt_credits_cost', true),
'pay_price' => get_post_meta($draft_id, '_aksrt_pay_price', true) ?: '',
'credits_vip1' => get_post_meta($draft_id, '_aksrt_credits_vip1', true),
'credits_vip2' => get_post_meta($draft_id, '_aksrt_credits_vip2', true),
'pay_vip1_price' => get_post_meta($draft_id, '_aksrt_pay_vip1_price', true),
'pay_vip2_price' => get_post_meta($draft_id, '_aksrt_pay_vip2_price', true),
'pay_hide_part' => (int) get_post_meta($draft_id, '_aksrt_pay_hide_part', true) === 1,
'topic_coupon_s' => (int) get_post_meta($draft_id, '_aksrt_topic_coupon_s', true) === 1,
'visibility_roles' => get_post_meta($draft_id, '_aksrt_visibility_roles', true) ?: [],
'topic_cat' => $topic_cat,
'vote_enabled' => get_post_meta($draft_id, '_aksrt_vote_enabled', true) === '1',
@@ -288,6 +316,12 @@ class DraftService {
'featured_video' => esc_url_raw((string) ($_POST['featured_video'] ?? '')),
'credits_cost' => (int) ($_POST['credits_cost'] ?? 0),
'pay_price' => sanitize_text_field((string) ($_POST['pay_price'] ?? '')),
'credits_vip1' => isset($_POST['credits_vip1']) ? sanitize_text_field((string) $_POST['credits_vip1']) : '',
'credits_vip2' => isset($_POST['credits_vip2']) ? sanitize_text_field((string) $_POST['credits_vip2']) : '',
'pay_vip1_price' => isset($_POST['pay_vip1_price']) ? sanitize_text_field((string) $_POST['pay_vip1_price']) : '',
'pay_vip2_price' => isset($_POST['pay_vip2_price']) ? sanitize_text_field((string) $_POST['pay_vip2_price']) : '',
'pay_hide_part' => !empty($_POST['pay_hide_part']),
'topic_coupon_s' => !empty($_POST['topic_coupon_s']),
'visibility_roles' => isset($_POST['visibility_roles']) && is_array($_POST['visibility_roles'])
? array_map('sanitize_key', $_POST['visibility_roles']) : [],
];
+18
View File
@@ -146,10 +146,28 @@ class TopicService {
if ($visibility === Forum::VISIBILITY_CREDITS) {
$credits_cost = max(0, (int) ($data['credits_cost'] ?? 0));
update_post_meta($post_id, '_aksrt_credits_cost', $credits_cost);
update_post_meta($post_id, '_aksrt_pay_credits', $credits_cost);
if (isset($data['credits_vip1']) && (string) $data['credits_vip1'] !== '') {
update_post_meta($post_id, '_aksrt_credits_vip1', max(0, (int) $data['credits_vip1']));
}
if (isset($data['credits_vip2']) && (string) $data['credits_vip2'] !== '') {
update_post_meta($post_id, '_aksrt_credits_vip2', max(0, (int) $data['credits_vip2']));
}
update_post_meta($post_id, '_aksrt_pay_hide_part', !empty($data['pay_hide_part']) ? 1 : 0);
}
if ($visibility === Forum::VISIBILITY_PAY) {
$pay_price = sanitize_text_field((string) ($data['pay_price'] ?? ''));
update_post_meta($post_id, '_aksrt_pay_price', $pay_price);
if (isset($data['pay_vip1_price']) && (string) $data['pay_vip1_price'] !== '') {
$v1 = max(0, (float) $data['pay_vip1_price']);
update_post_meta($post_id, '_aksrt_pay_vip1_price', $v1);
update_post_meta($post_id, '_aksrt_pay_vip_price', $v1);
}
if (isset($data['pay_vip2_price']) && (string) $data['pay_vip2_price'] !== '') {
update_post_meta($post_id, '_aksrt_pay_vip2_price', max(0, (float) $data['pay_vip2_price']));
}
update_post_meta($post_id, '_aksrt_pay_hide_part', !empty($data['pay_hide_part']) ? 1 : 0);
update_post_meta($post_id, '_aksrt_topic_coupon_s', !empty($data['topic_coupon_s']) ? 1 : 0);
}
if ($visibility === Forum::VISIBILITY_ROLE) {
$roles = isset($data['visibility_roles']) && is_array($data['visibility_roles'])
+35 -1
View File
@@ -94,10 +94,38 @@ trait TopicServicePart4 {
if (isset($this->get_visibility_map()[$visibility])) {
update_post_meta($topic_id, '_aksrt_visibility', $visibility);
if ($visibility === self::VISIBILITY_CREDITS) {
update_post_meta($topic_id, '_aksrt_credits_cost', max(0, (int) ($_POST['credits_cost'] ?? 0)));
$cost = max(0, (int) ($_POST['credits_cost'] ?? 0));
update_post_meta($topic_id, '_aksrt_credits_cost', $cost);
update_post_meta($topic_id, '_aksrt_pay_credits', $cost);
if (isset($_POST['credits_vip1']) && (string) $_POST['credits_vip1'] !== '') {
update_post_meta($topic_id, '_aksrt_credits_vip1', max(0, (int) $_POST['credits_vip1']));
} else {
delete_post_meta($topic_id, '_aksrt_credits_vip1');
}
if (isset($_POST['credits_vip2']) && (string) $_POST['credits_vip2'] !== '') {
update_post_meta($topic_id, '_aksrt_credits_vip2', max(0, (int) $_POST['credits_vip2']));
} else {
delete_post_meta($topic_id, '_aksrt_credits_vip2');
}
update_post_meta($topic_id, '_aksrt_pay_hide_part', !empty($_POST['pay_hide_part']) ? 1 : 0);
}
if ($visibility === self::VISIBILITY_PAY) {
update_post_meta($topic_id, '_aksrt_pay_price', sanitize_text_field((string) ($_POST['pay_price'] ?? '')));
if (isset($_POST['pay_vip1_price']) && (string) $_POST['pay_vip1_price'] !== '') {
$v1 = max(0, (float) $_POST['pay_vip1_price']);
update_post_meta($topic_id, '_aksrt_pay_vip1_price', $v1);
update_post_meta($topic_id, '_aksrt_pay_vip_price', $v1);
} else {
delete_post_meta($topic_id, '_aksrt_pay_vip1_price');
delete_post_meta($topic_id, '_aksrt_pay_vip_price');
}
if (isset($_POST['pay_vip2_price']) && (string) $_POST['pay_vip2_price'] !== '') {
update_post_meta($topic_id, '_aksrt_pay_vip2_price', max(0, (float) $_POST['pay_vip2_price']));
} else {
delete_post_meta($topic_id, '_aksrt_pay_vip2_price');
}
update_post_meta($topic_id, '_aksrt_pay_hide_part', !empty($_POST['pay_hide_part']) ? 1 : 0);
update_post_meta($topic_id, '_aksrt_topic_coupon_s', !empty($_POST['topic_coupon_s']) ? 1 : 0);
}
if ($visibility === self::VISIBILITY_ROLE) {
$roles = isset($_POST['visibility_roles']) && is_array($_POST['visibility_roles'])
@@ -227,6 +255,12 @@ trait TopicServicePart4 {
'featured_video' => esc_url_raw((string) ($_POST['featured_video'] ?? '')),
'credits_cost' => (int) ($_POST['credits_cost'] ?? 0),
'pay_price' => sanitize_text_field((string) ($_POST['pay_price'] ?? '')),
'credits_vip1' => isset($_POST['credits_vip1']) ? sanitize_text_field((string) $_POST['credits_vip1']) : '',
'credits_vip2' => isset($_POST['credits_vip2']) ? sanitize_text_field((string) $_POST['credits_vip2']) : '',
'pay_vip1_price' => isset($_POST['pay_vip1_price']) ? sanitize_text_field((string) $_POST['pay_vip1_price']) : '',
'pay_vip2_price' => isset($_POST['pay_vip2_price']) ? sanitize_text_field((string) $_POST['pay_vip2_price']) : '',
'pay_hide_part' => !empty($_POST['pay_hide_part']),
'topic_coupon_s' => !empty($_POST['topic_coupon_s']),
'visibility_roles' => isset($_POST['visibility_roles']) && is_array($_POST['visibility_roles'])
? array_map('sanitize_key', $_POST['visibility_roles']) : [],
];
+82
View File
@@ -0,0 +1,82 @@
<?php
/**
* In-article / topic TOC shell (zibll widget_ui_posts_navs parity).
*
* Frontend JS fills `.posts-nav-box` from heading outline when present.
*
* @package AKSRT
*/
namespace AKSRT\Widgets;
defined('ABSPATH') || exit;
class WidgetPostsNavs extends \WP_Widget {
public function __construct() {
parent::__construct(
'aksrt_posts_navs',
esc_html__('AKSRT 文章目录树', 'aksrt'),
[
'description' => esc_html__('显示当前文章/帖子标题目录,非详情页不输出。标题超过 3 个才显示。', 'aksrt'),
'classname' => 'widget_aksrt_posts_navs',
]
);
}
public function widget($args, $instance): void {
if (!is_singular(['post', 'page', 'aksrt_forum_topic', 'aksrt_product'])) {
return;
}
$title = sanitize_text_field((string) ($instance['title'] ?? __('文章目录', 'aksrt')));
$mini_title = sanitize_text_field((string) ($instance['mini_title'] ?? ''));
$in_affix = !empty($instance['in_affix']);
$data_title = $title;
if ($mini_title !== '') {
$data_title .= ' · ' . $mini_title;
}
echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo '<div class="widget-container">';
printf(
'<div class="posts-nav-box"%s%s></div>',
$in_affix ? ' data-affix="true"' : '',
$data_title !== '' ? ' data-title="' . esc_attr($data_title) . '"' : ''
);
echo '</div>';
echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
public function form($instance): void {
$title = (string) ($instance['title'] ?? __('文章目录', 'aksrt'));
$mini_title = (string) ($instance['mini_title'] ?? '');
$in_affix = !empty($instance['in_affix']);
?>
<p>
<label for="<?php echo esc_attr($this->get_field_id('title')); ?>"><?php esc_html_e('标题:', 'aksrt'); ?></label>
<input class="widefat" type="text" id="<?php echo esc_attr($this->get_field_id('title')); ?>" name="<?php echo esc_attr($this->get_field_name('title')); ?>" value="<?php echo esc_attr($title); ?>">
</p>
<p>
<label for="<?php echo esc_attr($this->get_field_id('mini_title')); ?>"><?php esc_html_e('副标题:', 'aksrt'); ?></label>
<input class="widefat" type="text" id="<?php echo esc_attr($this->get_field_id('mini_title')); ?>" name="<?php echo esc_attr($this->get_field_name('mini_title')); ?>" value="<?php echo esc_attr($mini_title); ?>">
</p>
<p>
<label>
<input type="checkbox" id="<?php echo esc_attr($this->get_field_id('in_affix')); ?>" name="<?php echo esc_attr($this->get_field_name('in_affix')); ?>" value="1" <?php checked($in_affix); ?>>
<?php esc_html_e('侧栏随动(仅侧边栏)', 'aksrt'); ?>
</label>
</p>
<p class="description"><?php esc_html_e('请添加到文章/帖子侧栏。正文 H2–H4 超过 3 个时前端才会填充目录。', 'aksrt'); ?></p>
<?php
}
public function update($new_instance, $old_instance): array {
return [
'title' => sanitize_text_field((string) ($new_instance['title'] ?? '')),
'mini_title' => sanitize_text_field((string) ($new_instance['mini_title'] ?? '')),
'in_affix' => !empty($new_instance['in_affix']) ? 1 : 0,
];
}
}
+35
View File
@@ -154,6 +154,17 @@ $initial_content = $draft_data ? $draft_data['content'] : '';
wp_editor($initial_content, 'compose-content', $editor_settings);
?>
<?php if (!empty($caps['can_set_hide']) || !empty($caps['can_set_paid'])) : ?>
<div class="aksrt-forum-compose__shortcode-tools">
<?php if (!empty($caps['can_set_hide'])) : ?>
<button type="button" class="but" data-insert-shortcode="hide"><i class="fas fa-eye-slash"></i> <?php esc_html_e('登录可见', 'aksrt'); ?></button>
<button type="button" class="but" data-insert-shortcode="reply"><i class="fas fa-comment-dots"></i> <?php esc_html_e('回复可见', 'aksrt'); ?></button>
<?php endif; ?>
<?php if (!empty($caps['can_set_paid'])) : ?>
<button type="button" class="but" data-insert-shortcode="payshow"><i class="fas fa-lock"></i> <?php esc_html_e('付费可见', 'aksrt'); ?></button>
<?php endif; ?>
</div>
<?php endif; ?>
</div>
</div>
@@ -276,6 +287,14 @@ $initial_content = $draft_data ? $draft_data['content'] : '';
<input type="number" id="compose-credits-cost" name="credits_cost" min="1" max="99999"
value="<?php echo esc_attr((string) ($draft_data['credits_cost'] ?? '')); ?>"
placeholder="<?php esc_attr_e('查看需要的积分数', 'aksrt'); ?>">
<label for="compose-credits-vip1" class="mt10"><?php esc_html_e('VIP1 积分价', 'aksrt'); ?></label>
<input type="number" id="compose-credits-vip1" name="credits_vip1" min="0" max="99999"
value="<?php echo esc_attr((string) ($draft_data['credits_vip1'] ?? '')); ?>"
placeholder="<?php esc_attr_e('留空=不设', 'aksrt'); ?>">
<label for="compose-credits-vip2" class="mt10"><?php esc_html_e('VIP2 积分价', 'aksrt'); ?></label>
<input type="number" id="compose-credits-vip2" name="credits_vip2" min="0" max="99999"
value="<?php echo esc_attr((string) ($draft_data['credits_vip2'] ?? '')); ?>"
placeholder="<?php esc_attr_e('留空=不设;0=免费', 'aksrt'); ?>">
</div>
<div class="aksrt-forum-compose__visibility-extra" id="compose-pay-price-field" style="display:none;">
@@ -283,6 +302,22 @@ $initial_content = $draft_data ? $draft_data['content'] : '';
<input type="number" id="compose-pay-price" name="pay_price" min="0.01" step="0.01" max="99999"
value="<?php echo esc_attr((string) ($draft_data['pay_price'] ?? '')); ?>"
placeholder="<?php esc_attr_e('查看需要的金额', 'aksrt'); ?>">
<label for="compose-pay-vip1" class="mt10"><?php esc_html_e('VIP1 价格(¥)', 'aksrt'); ?></label>
<input type="number" id="compose-pay-vip1" name="pay_vip1_price" min="0" step="0.01" max="99999"
value="<?php echo esc_attr((string) ($draft_data['pay_vip1_price'] ?? '')); ?>"
placeholder="<?php esc_attr_e('留空=不设', 'aksrt'); ?>">
<label for="compose-pay-vip2" class="mt10"><?php esc_html_e('VIP2 价格(¥)', 'aksrt'); ?></label>
<input type="number" id="compose-pay-vip2" name="pay_vip2_price" min="0" step="0.01" max="99999"
value="<?php echo esc_attr((string) ($draft_data['pay_vip2_price'] ?? '')); ?>"
placeholder="<?php esc_attr_e('留空=不设;0=免费', 'aksrt'); ?>">
<label class="aksrt-forum-compose__checkbox mt10">
<input type="checkbox" name="pay_hide_part" value="1" <?php checked(!empty($draft_data['pay_hide_part'])); ?>>
<span><?php esc_html_e('仅隐藏部分内容(正文用 [payshow] 包裹)', 'aksrt'); ?></span>
</label>
<label class="aksrt-forum-compose__checkbox mt6">
<input type="checkbox" name="topic_coupon_s" value="1" <?php checked(!empty($draft_data['topic_coupon_s'])); ?>>
<span><?php esc_html_e('允许优惠码', 'aksrt'); ?></span>
</label>
</div>
<div class="aksrt-forum-compose__visibility-extra" id="compose-role-field" style="display:none;">
+8 -2
View File
@@ -69,8 +69,14 @@ $is_can_new = !empty($ctx['is_can_new']);
<?php if (!empty($ctx['can_hide']) || !empty($ctx['can_pay'])) : ?>
<div class="aksrt-submit-shortcode-tools">
<?php if (!empty($ctx['can_hide'])) : ?>
<button type="button" class="but" data-insert-shortcode="hide" title="<?php esc_attr_e('插入隐藏内容', 'aksrt'); ?>">
<i class="fas fa-eye-slash"></i> <?php esc_html_e('隐藏内容', 'aksrt'); ?>
<button type="button" class="but" data-insert-shortcode="hide" title="<?php esc_attr_e('插入登录后可见', 'aksrt'); ?>">
<i class="fas fa-eye-slash"></i> <?php esc_html_e('登录可见', 'aksrt'); ?>
</button>
<button type="button" class="but" data-insert-shortcode="reply" title="<?php esc_attr_e('插入回复后可见', 'aksrt'); ?>">
<i class="fas fa-comment-dots"></i> <?php esc_html_e('回复可见', 'aksrt'); ?>
</button>
<button type="button" class="but" data-insert-shortcode="vip" title="<?php esc_attr_e('插入 VIP 可见', 'aksrt'); ?>">
<i class="fas fa-crown"></i> <?php esc_html_e('VIP 可见', 'aksrt'); ?>
</button>
<?php endif; ?>
<?php if (!empty($ctx['can_pay'])) : ?>