refactor: rename the theme brand and every identifier from AKSRT to Jio

Full-project rename. `git grep -i aksrt` is now empty apart from the
DBMigrator 3.3.0 changelog note that documents the rename itself.

Code identifiers
- PHP namespace AKSRT\ -> Jio\ (composer PSR-4 + 378 declarations)
- global constants AKSRT_* -> JIO_* (VERSION/DIR/URI, PAY_TYPE_*, CAPTCHA_*)
- procedural functions, hooks, options, meta keys, table names,
  cookies, query vars, ajax actions and nonces: aksrt_ -> jio_
- text domain aksrt -> jio; Theme Name -> Jio
- JS globals: aksrtData/aksrtAdminData/... -> jioData/jioAdminData/...,
  window.AKSRTCaptcha & friends -> window.JioCaptcha, Aksrt* -> Jio*
- Vue components Aks*.vue -> Jio*.vue (15 files renamed + imports)
- CSS classes/data-attrs/custom properties: aksrt- -> jio-
- REST namespace aksrt/v1 -> jio/v1 (incl. payment gateway callbacks)

Storage layer
- DB tables {prefix}aksrt_* -> {prefix}jio_*; SCHEMA_VERSION 3.2.0 -> 3.3.0
  so dbDelta creates the new names on install
- widget id_base aksrt_widget_* -> jio_widget_*
- uninstall.php drop list rebuilt from the actual migrator tables
  (dropped 3 stale entries, added the 17 missing ones)

Removed
- hardcoded https://api.aksrt.com/version.json default update endpoint;
  get_endpoint() now reads the 「更新端点」 setting and do_check() no-ops
  when it is empty

Files renamed: inc/helpers/aksrt-*.php -> jio-*.php (54),
assets/admin/src/components/form/Aks*.vue -> Jio*.vue (15).
Build artifacts regenerated: assets/css/build, assets/js/runtime,
assets/admin/build.

Verified: audit-psr4-paths errors=0, audit-admin-contracts errors=0
(warnings=56, unchanged baseline), 813/813 widget-system tests,
44/44 phpunit, php -l clean across all PHP.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
kate522andClaude Opus 5 committed 2026-11-01 20:05:04 +08:00
1 parent 5b16bc6e15
commit 35bb96e82f
997 files changed
+28216 -28188

No files matched your search

+8 -8
View File
@@ -1,6 +1,6 @@
<?php
/**
* @package AKSRT
* @package Jio
*
* 后台列表页分页条 —— 1:1 复刻「卡密管理」页的 zibpay_admin_pagenavi 结构。
*
@@ -20,7 +20,7 @@
* 这样的口径,四枚按钮全 disabled 也明确了「没有更多页」。
*/
namespace AKSRT\Admin\Native;
namespace Jio\Admin\Native;
defined('ABSPATH') || exit;
@@ -78,13 +78,13 @@ class AdminListPagination {
?>
<div class="tablenav bottom">
<div class="tablenav-pages">
<span class="displaying-num"><?php printf(esc_html__('每页 %1$s 共%2$s', 'aksrt'), esc_html((string) $per_page), esc_html((string) $total_count)); ?></span>
<span class="displaying-num"><?php printf(esc_html__('每页 %1$s 共%2$s', 'jio'), esc_html((string) $per_page), esc_html((string) $total_count)); ?></span>
<span class="pagination-links">
<a class="first-page button<?php echo $on_first ? ' disabled' : ''; ?>" title="<?php echo esc_attr__('前往第一页', 'aksrt'); ?>" href="<?php echo esc_url($first_page_url); ?>">«</a>
<a class="prev-page button<?php echo $on_first ? ' disabled' : ''; ?>" title="<?php echo esc_attr__('前往上一页', 'aksrt'); ?>" href="<?php echo esc_url($prev_page_url); ?>">‹</a>
<span class="paging-input"><?php printf(esc_html__('第 %1$s 页,共 %2$s 页', 'aksrt'), esc_html((string) $current_page), '<span class="total-pages">' . esc_html((string) $total_pages) . '</span>'); ?></span>
<a class="next-page button<?php echo $on_last ? ' disabled' : ''; ?>" title="<?php echo esc_attr__('前往下一页', 'aksrt'); ?>" href="<?php echo esc_url($next_page_url); ?>">›</a>
<a class="last-page button<?php echo $on_last ? ' disabled' : ''; ?>" title="<?php echo esc_attr__('前往最后一页', 'aksrt'); ?>" href="<?php echo esc_url($last_page_url); ?>">»</a>
<a class="first-page button<?php echo $on_first ? ' disabled' : ''; ?>" title="<?php echo esc_attr__('前往第一页', 'jio'); ?>" href="<?php echo esc_url($first_page_url); ?>">«</a>
<a class="prev-page button<?php echo $on_first ? ' disabled' : ''; ?>" title="<?php echo esc_attr__('前往上一页', 'jio'); ?>" href="<?php echo esc_url($prev_page_url); ?>">‹</a>
<span class="paging-input"><?php printf(esc_html__('第 %1$s 页,共 %2$s 页', 'jio'), esc_html((string) $current_page), '<span class="total-pages">' . esc_html((string) $total_pages) . '</span>'); ?></span>
<a class="next-page button<?php echo $on_last ? ' disabled' : ''; ?>" title="<?php echo esc_attr__('前往下一页', 'jio'); ?>" href="<?php echo esc_url($next_page_url); ?>">›</a>
<a class="last-page button<?php echo $on_last ? ' disabled' : ''; ?>" title="<?php echo esc_attr__('前往最后一页', 'jio'); ?>" href="<?php echo esc_url($last_page_url); ?>">»</a>
</span>
</div>
</div>
+49 -49
View File
@@ -2,16 +2,16 @@
/**
* 原生后台运营页共享 UI。
*
* 支付中心(aksrt_pay 菜单)与用户子菜单(users.php)的运营页统一使用这里的
* 支付中心(jio_pay 菜单)与用户子菜单(users.php)的运营页统一使用这里的
* 页面外壳、页头、统计卡、卡片、徽标与空状态。样式在 assets/css/admin/native-ui.css,
* 只在 SLUGS 列出的页面入队,规则全部限定在 .aksrt-native 下。
* 只在 SLUGS 列出的页面入队,规则全部限定在 .jio-native 下。
*
* 输出方法直接 echo,参数在内部转义;$html 形参约定为调用方已转义的片段。
*
* @package AKSRT
* @package Jio
*/
namespace AKSRT\Admin\Native;
namespace Jio\Admin\Native;
defined('ABSPATH') || exit;
@@ -19,22 +19,22 @@ final class NativeUi {
/** 使用共享 UI 的 admin page 参数。 */
public const SLUGS = [
'aksrt_pay',
'aksrt_orders',
'aksrt_pay_products',
'aksrt_coupons',
'aksrt_charge_cards',
'aksrt_income',
'aksrt_withdraw',
'aksrt_invite_codes',
'aksrt_messages',
'aksrt_user_vip',
'aksrt_user_auth',
'aksrt_user_ban',
'aksrt_tickets',
'jio_pay',
'jio_orders',
'jio_pay_products',
'jio_coupons',
'jio_charge_cards',
'jio_income',
'jio_withdraw',
'jio_invite_codes',
'jio_messages',
'jio_user_vip',
'jio_user_auth',
'jio_user_ban',
'jio_tickets',
];
public const STYLE_HANDLE = 'aksrt-admin-native-ui';
public const STYLE_HANDLE = 'jio-admin-native-ui';
private const TONES = ['success', 'info', 'warning', 'danger', 'neutral', 'primary'];
@@ -59,8 +59,8 @@ final class NativeUi {
return;
}
$css = '/assets/css/admin/native-ui.css';
if (is_file(AKSRT_DIR . $css)) {
wp_enqueue_style(self::STYLE_HANDLE, AKSRT_URI . $css, [], (string) filemtime(AKSRT_DIR . $css));
if (is_file(JIO_DIR . $css)) {
wp_enqueue_style(self::STYLE_HANDLE, JIO_URI . $css, [], (string) filemtime(JIO_DIR . $css));
}
}
@@ -70,7 +70,7 @@ final class NativeUi {
/** 打开 .wrap 外壳。$modifier 追加到 class(如页面专属根类)。 */
public static function page_open(string $modifier = ''): void {
$class = trim('wrap aksrt-native ' . $modifier);
$class = trim('wrap jio-native ' . $modifier);
echo '<div class="' . esc_attr($class) . '">';
}
@@ -86,19 +86,19 @@ final class NativeUi {
* @param array<int, array{label:string,url?:string,dialog?:string,primary?:bool,icon?:string}> $actions
*/
public static function header(string $title, string $description = '', string $icon = '', array $actions = []): void {
echo '<div class="aksrt-native-header"><div class="aksrt-native-header__main">';
echo '<div class="jio-native-header"><div class="jio-native-header__main">';
// h1 让 WP 把 admin_notices 挪到标题之后。
echo '<h1 class="aksrt-native-header__title">';
echo '<h1 class="jio-native-header__title">';
if ($icon !== '') {
echo '<span class="aksrt-native-header__icon dashicons ' . esc_attr($icon) . '" aria-hidden="true"></span>';
echo '<span class="jio-native-header__icon dashicons ' . esc_attr($icon) . '" aria-hidden="true"></span>';
}
echo esc_html($title) . '</h1>';
if ($description !== '') {
echo '<p class="aksrt-native-header__desc">' . esc_html($description) . '</p>';
echo '<p class="jio-native-header__desc">' . esc_html($description) . '</p>';
}
echo '</div>';
if ($actions !== []) {
echo '<div class="aksrt-native-header__actions">';
echo '<div class="jio-native-header__actions">';
foreach ($actions as $action) {
echo isset($action['dialog'])
? self::button_dialog($action['label'], (string) $action['dialog'], !empty($action['primary']), (string) ($action['icon'] ?? ''))
@@ -117,11 +117,11 @@ final class NativeUi {
return '<a class="' . esc_attr($class) . '" href="' . esc_url($url) . '">' . $icon_html . esc_html($label) . '</a>';
}
/** 打开页面内 <dialog id="$dialog_id"> 的按钮;点击行为由页面脚本按 data-aksrt-dialog-open 绑定。 */
/** 打开页面内 <dialog id="$dialog_id"> 的按钮;点击行为由页面脚本按 data-jio-dialog-open 绑定。 */
public static function button_dialog(string $label, string $dialog_id, bool $primary = false, string $icon = ''): string {
$class = $primary ? 'button button-primary' : 'button';
$icon_html = $icon !== '' ? '<span class="dashicons ' . esc_attr($icon) . '" aria-hidden="true"></span>' : '';
return '<button type="button" class="' . esc_attr($class) . '" data-aksrt-dialog-open="' . esc_attr($dialog_id) . '" aria-haspopup="dialog" aria-controls="' . esc_attr($dialog_id) . '">'
return '<button type="button" class="' . esc_attr($class) . '" data-jio-dialog-open="' . esc_attr($dialog_id) . '" aria-haspopup="dialog" aria-controls="' . esc_attr($dialog_id) . '">'
. $icon_html . esc_html($label) . '</button>';
}
@@ -136,7 +136,7 @@ final class NativeUi {
if ($items === []) {
return;
}
echo '<div class="aksrt-native-stats">';
echo '<div class="jio-native-stats">';
foreach ($items as $item) {
self::stat($item);
}
@@ -150,19 +150,19 @@ final class NativeUi {
$tag = $url !== '' ? 'a' : 'div';
$attr = $url !== '' ? ' href="' . esc_url($url) . '"' : '';
echo '<' . $tag . ' class="aksrt-native-stat aksrt-native-stat--' . esc_attr($tone) . '"' . $attr . '>';
echo '<span class="aksrt-native-stat__label">';
echo '<' . $tag . ' class="jio-native-stat jio-native-stat--' . esc_attr($tone) . '"' . $attr . '>';
echo '<span class="jio-native-stat__label">';
if (!empty($item['icon'])) {
echo '<span class="dashicons ' . esc_attr((string) $item['icon']) . '" aria-hidden="true"></span>';
}
echo esc_html($item['label']) . '</span>';
echo '<span class="aksrt-native-stat__value">';
echo '<span class="jio-native-stat__value">';
if (!empty($item['unit'])) {
echo '<span class="aksrt-native-stat__unit">' . esc_html((string) $item['unit']) . '</span>';
echo '<span class="jio-native-stat__unit">' . esc_html((string) $item['unit']) . '</span>';
}
echo esc_html((string) $item['value']) . '</span>';
if (!empty($item['hint'])) {
echo '<span class="aksrt-native-stat__hint">' . esc_html((string) $item['hint']) . '</span>';
echo '<span class="jio-native-stat__hint">' . esc_html((string) $item['hint']) . '</span>';
}
echo '</' . $tag . '>';
}
@@ -176,19 +176,19 @@ final class NativeUi {
* $flush=true 时正文无内边距(承载整表)。
*/
public static function card_open(string $title = '', string $actions_html = '', bool $flush = false, string $modifier = ''): void {
$class = 'aksrt-native-card' . ($flush ? ' aksrt-native-card--flush' : '') . ($modifier !== '' ? ' ' . $modifier : '');
$class = 'jio-native-card' . ($flush ? ' jio-native-card--flush' : '') . ($modifier !== '' ? ' ' . $modifier : '');
echo '<section class="' . esc_attr($class) . '">';
if ($title !== '' || $actions_html !== '') {
echo '<div class="aksrt-native-card__head">';
echo '<div class="jio-native-card__head">';
if ($title !== '') {
echo '<h2 class="aksrt-native-card__title">' . esc_html($title) . '</h2>';
echo '<h2 class="jio-native-card__title">' . esc_html($title) . '</h2>';
}
if ($actions_html !== '') {
echo '<div class="aksrt-native-card__actions">' . $actions_html . '</div>';
echo '<div class="jio-native-card__actions">' . $actions_html . '</div>';
}
echo '</div>';
}
echo '<div class="aksrt-native-card__body">';
echo '<div class="jio-native-card__body">';
}
public static function card_close(): void {
@@ -198,7 +198,7 @@ final class NativeUi {
/** 表格卡:整表铺满卡片,带横向滚动容器。 */
public static function table_card_open(string $title = '', string $actions_html = ''): void {
self::card_open($title, $actions_html, true);
echo '<div class="aksrt-native-table-wrap">';
echo '<div class="jio-native-table-wrap">';
}
/** 关闭表格卡;分页条应在 table-wrap 关闭后、卡片关闭前输出,故拆成两步。 */
@@ -211,7 +211,7 @@ final class NativeUi {
* ----------------------------------------------------------------*/
public static function badge(string $label, string $tone = 'neutral', bool $dot = true): string {
$class = 'aksrt-native-badge aksrt-native-badge--' . self::tone($tone) . ($dot ? ' has-dot' : '');
$class = 'jio-native-badge jio-native-badge--' . self::tone($tone) . ($dot ? ' has-dot' : '');
return '<span class="' . esc_attr($class) . '">' . esc_html($label) . '</span>';
}
@@ -228,9 +228,9 @@ final class NativeUi {
}
public static function empty_state(string $title, string $hint = '', string $icon = 'dashicons-portfolio'): string {
$html = '<div class="aksrt-native-empty">';
$html = '<div class="jio-native-empty">';
$html .= '<span class="dashicons ' . esc_attr($icon) . '" aria-hidden="true"></span>';
$html .= '<span class="aksrt-native-empty__title">' . esc_html($title) . '</span>';
$html .= '<span class="jio-native-empty__title">' . esc_html($title) . '</span>';
if ($hint !== '') {
$html .= '<span>' . esc_html($hint) . '</span>';
}
@@ -239,7 +239,7 @@ final class NativeUi {
/** 表格内的空状态整行。 */
public static function empty_row(int $colspan, string $title, string $hint = ''): string {
return '<tr class="no-items"><td class="aksrt-native-empty-cell" colspan="' . esc_attr((string) max(1, $colspan)) . '">'
return '<tr class="no-items"><td class="jio-native-empty-cell" colspan="' . esc_attr((string) max(1, $colspan)) . '">'
. self::empty_state($title, $hint)
. '</td></tr>';
}
@@ -248,13 +248,13 @@ final class NativeUi {
public static function user_cell(int $user_id, string $fallback = ''): string {
$user = $user_id > 0 ? get_userdata($user_id) : false;
if (!$user) {
return '<span class="aksrt-native-muted">' . esc_html($fallback !== '' ? $fallback : ($user_id > 0 ? '#' . $user_id : '—')) . '</span>';
return '<span class="jio-native-muted">' . esc_html($fallback !== '' ? $fallback : ($user_id > 0 ? '#' . $user_id : '—')) . '</span>';
}
$avatar = function_exists('aksrt_get_avatar_url') ? (string) aksrt_get_avatar_url($user_id, 28) : (string) get_avatar_url($user_id, ['size' => 28]);
return '<a class="aksrt-native-user" href="' . esc_url(get_edit_user_link($user_id)) . '">'
$avatar = function_exists('jio_get_avatar_url') ? (string) jio_get_avatar_url($user_id, 28) : (string) get_avatar_url($user_id, ['size' => 28]);
return '<a class="jio-native-user" href="' . esc_url(get_edit_user_link($user_id)) . '">'
. ($avatar !== '' ? '<img src="' . esc_url($avatar) . '" width="28" height="28" alt="">' : '')
. '<span><span class="aksrt-native-user__name">' . esc_html($user->display_name) . '</span>'
. '<span class="aksrt-native-user__meta">#' . esc_html((string) $user_id) . '</span></span></a>';
. '<span><span class="jio-native-user__name">' . esc_html($user->display_name) . '</span>'
. '<span class="jio-native-user__meta">#' . esc_html((string) $user_id) . '</span></span></a>';
}
private static function tone(string $tone): string {
+53 -53
View File
@@ -1,22 +1,22 @@
<?php
/**
* @package AKSRT
* @package Jio
*
* 后台「卡密管理」页面 — 1:1 复刻参考主题 zibll 的 zibpay/page/charge-card.php。
*
* 结构:列表(过滤/搜索/排序/批量删除/分页) + 添加卡密(自动生成 / 导入) + 导出卡密(text / xls)。
* 卡密类型与参考主题一致:余额充值 / 会员兑换 / 积分兑换 / 自定义卡密;
* 本主题内部类型映射:balance / vip / credits / custom。
* 差异点:AKSRT 无商城发货(shipped)子系统,故不含「已售」标记与链接;删除取代参考主题的批量操作语义一致。
* 差异点:Jio 无商城发货(shipped)子系统,故不含「已售」标记与链接;删除取代参考主题的批量操作语义一致。
*
* 外壳与配色走 NativeUi(.aksrt-native),页面专属交互样式仍由 print_assets() 输出。
* 外壳与配色走 NativeUi(.jio-native),页面专属交互样式仍由 print_assets() 输出。
* 类体按关注点拆成 flat sibling traits(PSR-4 要求 trait 与宿主类同目录,不能放子目录):
* - PayChargeCardsList 列表渲染 + 翻页参数
* - PayChargeCardsAssets 页面专属内联样式/脚本
* - PayChargeCardsForms 添加/导出表单 + 依赖显隐助手
*/
namespace AKSRT\Admin\Native;
namespace Jio\Admin\Native;
defined('ABSPATH') || exit;
@@ -27,9 +27,9 @@ class PayChargeCards {
use PayChargeCardsForms;
public const CAPABILITY = 'manage_options';
public const PAGE_SLUG = 'aksrt_charge_cards';
public const NONCE = 'aksrt_card_pass_page';
public const EXPORT_AJAX_ACTION = 'aksrt_card_pass_export';
public const PAGE_SLUG = 'jio_charge_cards';
public const NONCE = 'jio_card_pass_page';
public const EXPORT_AJAX_ACTION = 'jio_card_pass_export';
private static $instance = null;
@@ -54,17 +54,17 @@ class PayChargeCards {
$this->ensure_cap();
$this->page_url = admin_url('admin.php?page=' . self::PAGE_SLUG);
if (!class_exists('\AKSRT\Payment\ChargeCard')) {
if (!class_exists('\Jio\Payment\ChargeCard')) {
NativeUi::page_open();
echo '<h1 class="wp-heading-inline">' . esc_html__('卡密管理', 'aksrt') . '</h1>';
echo '<div class="notice notice-error"><p>' . esc_html__('卡密模块未初始化。', 'aksrt') . '</p></div>';
echo '<h1 class="wp-heading-inline">' . esc_html__('卡密管理', 'jio') . '</h1>';
echo '<div class="notice notice-error"><p>' . esc_html__('卡密模块未初始化。', 'jio') . '</p></div>';
NativeUi::page_close();
return;
}
$tab = isset($_GET['tab']) ? sanitize_key((string) wp_unslash($_GET['tab'])) : '';
$s = isset($_REQUEST['s']) ? sanitize_text_field((string) wp_unslash($_REQUEST['s'])) : '';
$page_title = __('卡密管理', 'aksrt');
$page_title = __('卡密管理', 'jio');
$head_but = '';
// 页面级动作(参考主题在渲染前处理 add / delete)。
@@ -78,20 +78,20 @@ class PayChargeCards {
// 标题右侧按钮是页面头部的一部分,只在此处输出一次(列表页同样如此,由 render_list 负责列表本体)。
switch ($tab) {
case 'add':
$page_title = __('添加卡密', 'aksrt');
$head_but = '<a href="' . esc_url($this->page_url) . '" class="page-title-action">' . esc_html__('返回列表', 'aksrt') . '</a>';
$page_title = __('添加卡密', 'jio');
$head_but = '<a href="' . esc_url($this->page_url) . '" class="page-title-action">' . esc_html__('返回列表', 'jio') . '</a>';
break;
case 'export':
$page_title = __('导出卡密', 'aksrt');
$head_but = '<a href="' . esc_url($this->page_url) . '" class="page-title-action">' . esc_html__('返回列表', 'aksrt') . '</a>';
$page_title = __('导出卡密', 'jio');
$head_but = '<a href="' . esc_url($this->page_url) . '" class="page-title-action">' . esc_html__('返回列表', 'jio') . '</a>';
break;
default:
$head_but = '<a href="' . esc_url(add_query_arg('tab', 'add', $this->page_url)) . '" class="page-title-action">' . esc_html__('添加卡密', 'aksrt') . '</a>';
$head_but .= '<a href="' . esc_url(add_query_arg('tab', 'export', $this->page_url)) . '" class="page-title-action">' . esc_html__('导出卡密', 'aksrt') . '</a>';
$head_but = '<a href="' . esc_url(add_query_arg('tab', 'add', $this->page_url)) . '" class="page-title-action">' . esc_html__('添加卡密', 'jio') . '</a>';
$head_but .= '<a href="' . esc_url(add_query_arg('tab', 'export', $this->page_url)) . '" class="page-title-action">' . esc_html__('导出卡密', 'jio') . '</a>';
if ($s !== '') {
$head_but .= '<div><div class="update-nag" style="margin: 10px 0 0;">' . sprintf(esc_html__('搜索 “%s” 的内容', 'aksrt'), esc_html($s)) . ' </div></div>';
$head_but .= '<div><div class="update-nag" style="margin: 10px 0 0;">' . sprintf(esc_html__('搜索 “%s” 的内容', 'jio'), esc_html($s)) . ' </div></div>';
}
break;
}
@@ -118,7 +118,7 @@ class PayChargeCards {
*/
public function handle_export(): void {
if (!current_user_can(self::CAPABILITY)) {
wp_die(esc_html__('暂无此权限', 'aksrt'));
wp_die(esc_html__('暂无此权限', 'jio'));
}
@set_time_limit(0);
@@ -133,9 +133,9 @@ class PayChargeCards {
$status = $status_raw === '0' ? 'active' : $status_raw;
}
$rows = \AKSRT\Payment\ChargeCard::get_instance()->get_export_rows($type, $status);
$rows = \Jio\Payment\ChargeCard::get_instance()->get_export_rows($type, $status);
if (!$rows) {
wp_die(esc_html__('暂无可导出的内容', 'aksrt'));
wp_die(esc_html__('暂无可导出的内容', 'jio'));
}
$filename = $type . '_' . gmdate('d_m_Y');
@@ -150,24 +150,24 @@ class PayChargeCards {
$meta = is_array($decoded) ? $decoded : [];
}
$status_text = $row['status'] === 'used'
? __('已使用', 'aksrt')
: ($row['status'] === 'expired' ? __('已过期', 'aksrt') : ($row['status'] === 'disabled' ? __('已禁用', 'aksrt') : __('未使用', 'aksrt')));
? __('已使用', 'jio')
: ($row['status'] === 'expired' ? __('已过期', 'jio') : ($row['status'] === 'disabled' ? __('已禁用', 'jio') : __('未使用', 'jio')));
switch ($type) {
case 'credits': // 积分兑换
$title = [__('卡号', 'aksrt'), __('密码', 'aksrt'), __('面额', 'aksrt'), __('备注', 'aksrt'), __('状态', 'aksrt')];
$title = [__('卡号', 'jio'), __('密码', 'jio'), __('面额', 'jio'), __('备注', 'jio'), __('状态', 'jio')];
$data[] = [(string) $row['card'], (string) $row['password'], (string) (int) ($meta['points'] ?? 0), (string) $row['note'], $status_text];
break;
case 'vip': // 会员兑换
$title = [__('卡号', 'aksrt'), __('密码', 'aksrt'), __('兑换会员', 'aksrt'), __('备注', 'aksrt'), __('状态', 'aksrt')];
$title = [__('卡号', 'jio'), __('密码', 'jio'), __('兑换会员', 'jio'), __('备注', 'jio'), __('状态', 'jio')];
$data[] = [(string) $row['card'], (string) $row['password'], $this->vip_meta_query($meta), (string) $row['note'], $status_text];
break;
case 'custom': // 自定义
$title = [__('卡号', 'aksrt'), __('密码', 'aksrt'), __('备注', 'aksrt'), __('状态', 'aksrt')];
$title = [__('卡号', 'jio'), __('密码', 'jio'), __('备注', 'jio'), __('状态', 'jio')];
$data[] = [(string) $row['card'], (string) $row['password'], (string) $row['note'], ''];
break;
default: // 余额充值
$title = [__('卡号', 'aksrt'), __('密码', 'aksrt'), __('面额', 'aksrt'), __('备注', 'aksrt'), __('状态', 'aksrt')];
$title = [__('卡号', 'jio'), __('密码', 'jio'), __('面额', 'jio'), __('备注', 'jio'), __('状态', 'jio')];
$data[] = [(string) $row['card'], (string) $row['password'], $this->balance_amount_text($meta), (string) $row['note'], $status_text];
}
}
@@ -237,7 +237,7 @@ class PayChargeCards {
$add_type = isset($_REQUEST['add_type']) ? sanitize_key((string) wp_unslash($_REQUEST['add_type'])) : 'auto';
$pass_type = isset($_REQUEST['type']) ? sanitize_key((string) wp_unslash($_REQUEST['type'])) : '';
if (!in_array($pass_type, ['balance', 'vip', 'credits', 'custom'], true)) {
$this->page_notice(__('错误!', 'aksrt'), __('参数传入错误', 'aksrt'), 'error');
$this->page_notice(__('错误!', 'jio'), __('参数传入错误', 'jio'), 'error');
return;
}
@@ -250,7 +250,7 @@ class PayChargeCards {
// 一并落在区间外,因此用一个区间判定即可覆盖全部非法输入,不另设分支。
$auto_num = isset($_REQUEST['auto_num']) ? (int) $_REQUEST['auto_num'] : 0;
if ($auto_num < 1 || $auto_num > 1000) {
$this->page_notice(__('错误!', 'aksrt'), __('生成数量需为 1 - 1000 的整数', 'aksrt'), 'error');
$this->page_notice(__('错误!', 'jio'), __('生成数量需为 1 - 1000 的整数', 'jio'), 'error');
return;
}
@@ -263,7 +263,7 @@ class PayChargeCards {
// 阻止表单提交),因此下限由服务端兜底。
$auto_price = max(0.0, $auto_price);
if (!$auto_price) {
$this->page_notice(__('错误!', 'aksrt'), __('请输入卡密的面额', 'aksrt'), 'error');
$this->page_notice(__('错误!', 'jio'), __('请输入卡密的面额', 'jio'), 'error');
return;
}
$pass_meta = $pass_type === 'credits'
@@ -273,7 +273,7 @@ class PayChargeCards {
case 'vip':
$vip = isset($_REQUEST['vip_exchange']) && is_array($_REQUEST['vip_exchange']) ? wp_unslash($_REQUEST['vip_exchange']) : [];
if (empty($vip['time'])) {
$this->page_notice(__('错误!', 'aksrt'), __('请填写兑换会员参数', 'aksrt'), 'error');
$this->page_notice(__('错误!', 'jio'), __('请填写兑换会员参数', 'jio'), 'error');
return;
}
$pass_meta = $this->vip_meta($vip);
@@ -298,15 +298,15 @@ class PayChargeCards {
$remarks = isset($_REQUEST['auto_remarks']) ? sanitize_text_field((string) wp_unslash($_REQUEST['auto_remarks'])) : '';
$created = \AKSRT\Payment\ChargeCard::get_instance()->generate_cards($pass_type, $auto_num, $pass_meta, $rand_number, $rand_password, $remarks);
$created = \Jio\Payment\ChargeCard::get_instance()->generate_cards($pass_type, $auto_num, $pass_meta, $rand_number, $rand_password, $remarks);
$this->page_notice(__('完成!', 'aksrt'), sprintf(__('已自动生成%s个卡密', 'aksrt'), (string) $created));
$this->page_notice(__('完成!', 'jio'), sprintf(__('已自动生成%s个卡密', 'jio'), (string) $created));
}
private function handle_import(string $pass_type): void {
$import_data = isset($_REQUEST['import_data']) ? (string) wp_unslash($_REQUEST['import_data']) : '';
if (!$import_data) {
$this->page_notice(__('错误!', 'aksrt'), __('请粘贴您需要导入的数据', 'aksrt'), 'error');
$this->page_notice(__('错误!', 'jio'), __('请粘贴您需要导入的数据', 'jio'), 'error');
return;
}
@@ -318,7 +318,7 @@ class PayChargeCards {
$success_i = 0;
$error_i = 0;
$svc = \AKSRT\Payment\ChargeCard::get_instance();
$svc = \Jio\Payment\ChargeCard::get_instance();
foreach ($import_data_array as $line) {
$v = explode($import_division, $line);
@@ -395,11 +395,11 @@ class PayChargeCards {
if ($success_i) {
$this->page_notice(
__('导入完成', 'aksrt'),
sprintf(__('成功导入%s个卡密%s', 'aksrt'), (string) $success_i, $error_i ? sprintf(__(',%s个导入失败', 'aksrt'), (string) $error_i) : '')
__('导入完成', 'jio'),
sprintf(__('成功导入%s个卡密%s', 'jio'), (string) $success_i, $error_i ? sprintf(__(',%s个导入失败', 'jio'), (string) $error_i) : '')
);
} else {
$this->page_notice(__('导入失败', 'aksrt'), sprintf(__('%s个导入失败,数据格式错误', 'aksrt'), (string) $error_i), 'error');
$this->page_notice(__('导入失败', 'jio'), sprintf(__('%s个导入失败,数据格式错误', 'jio'), (string) $error_i), 'error');
}
}
@@ -409,13 +409,13 @@ class PayChargeCards {
$delete_ids = isset($_REQUEST['action_id']) ? array_map('absint', (array) wp_unslash($_REQUEST['action_id'])) : [];
$delete_ids = array_filter($delete_ids);
if (!$delete_ids) {
$this->page_notice(__('错误!', 'aksrt'), __('未选择需要删除的内容', 'aksrt'), 'error');
$this->page_notice(__('错误!', 'jio'), __('未选择需要删除的内容', 'jio'), 'error');
return;
}
$delete_i = \AKSRT\Payment\ChargeCard::get_instance()->delete_by_ids($delete_ids);
$delete_i = \Jio\Payment\ChargeCard::get_instance()->delete_by_ids($delete_ids);
$this->page_notice(__('删除完成', 'aksrt'), sprintf(__('已删除%s个卡密', 'aksrt'), (string) $delete_i));
$this->page_notice(__('删除完成', 'jio'), sprintf(__('已删除%s个卡密', 'jio'), (string) $delete_i));
}
/* ------------------------------------------------------------------
@@ -429,20 +429,20 @@ class PayChargeCards {
*/
private function type_labels(): array {
return [
'balance' => __('余额充值', 'aksrt'),
'vip' => __('会员兑换', 'aksrt'),
'credits' => __('积分兑换', 'aksrt'),
'custom' => __('自定义卡密', 'aksrt'),
'balance' => __('余额充值', 'jio'),
'vip' => __('会员兑换', 'jio'),
'credits' => __('积分兑换', 'jio'),
'custom' => __('自定义卡密', 'jio'),
];
}
private function vip_level_name(int $level): string {
$s = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_all() : [];
$s = class_exists('\Jio\Settings\Settings') ? \Jio\Settings\Settings::get_all() : [];
$name = trim((string) ($s['pay_user_vip_' . max(1, min(2, $level)) . '_name'] ?? ''));
if ($name !== '') {
return $name;
}
return $level === 2 ? __('二级会员', 'aksrt') : __('一级会员', 'aksrt');
return $level === 2 ? __('二级会员', 'jio') : __('一级会员', 'jio');
}
/**
@@ -454,13 +454,13 @@ class PayChargeCards {
$time = array_key_exists('time', $meta) ? (string) $meta['time'] : null;
if ($time !== null && $time !== '') {
if ($time === 'Permanent') {
return __('永久', 'aksrt');
return __('永久', 'jio');
}
$unit = (isset($meta['unit']) && $meta['unit'] === 'month') ? __('个月', 'aksrt') : __('天', 'aksrt');
$unit = (isset($meta['unit']) && $meta['unit'] === 'month') ? __('个月', 'jio') : __('天', 'jio');
return (string) ((int) $time) . $unit;
}
$days = (int) ($meta['days'] ?? 0);
return $days > 0 ? $days . __('天', 'aksrt') : __('永久', 'aksrt');
return $days > 0 ? $days . __('天', 'jio') : __('永久', 'jio');
}
/**
@@ -524,13 +524,13 @@ class PayChargeCards {
}
private function currency_unit(): string {
$s = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_all() : [];
return trim((string) ($s['pay_currency_unit'] ?? '')) !== '' ? trim((string) $s['pay_currency_unit']) : __('元', 'aksrt');
$s = class_exists('\Jio\Settings\Settings') ? \Jio\Settings\Settings::get_all() : [];
return trim((string) ($s['pay_currency_unit'] ?? '')) !== '' ? trim((string) $s['pay_currency_unit']) : __('元', 'jio');
}
private function ensure_cap(): void {
if (!current_user_can(self::CAPABILITY)) {
wp_die(esc_html__('您没有权限访问此页面。', 'aksrt'));
wp_die(esc_html__('您没有权限访问此页面。', 'jio'));
}
}
}
+16 -16
View File
@@ -1,6 +1,6 @@
<?php
/**
* @package AKSRT
* @package Jio
*
* PayChargeCards 页面专属内联样式与脚本:条件依赖显隐 + 点击复制。
* 这些是卡密管理页特有的表现(排序箭头基线锚定、按钮组、导入提示块、复制提示),
@@ -9,7 +9,7 @@
* 作为 flat sibling trait 被 PayChargeCards 使用(PSR-4:trait 必须与宿主类同目录)。
*/
namespace AKSRT\Admin\Native;
namespace Jio\Admin\Native;
defined('ABSPATH') || exit;
@@ -32,23 +32,23 @@ trait PayChargeCardsAssets {
.orderby-but > a + a { bottom: .075em; }
.orderby-but:hover a { opacity: .6; }
.orderby-but > a:hover { opacity: 1; }
.aksrt-cardpass-bulk { margin: 0 0 12px; }
.jio-cardpass-bulk { margin: 0 0 12px; }
/* 卡密表列多(≥10 列),窄视口下须保持最小宽度并由表格卡片横向滚动,
而非让各列挤扁。复刻原先列表表格的内联 style="min-width: 1000px;"。 */
.aksrt-cardpass-table { min-width: 1000px; }
.aksrt-btnset { display: flex; flex-wrap: wrap; gap: 6px; }
.aksrt-btnset-item { display: inline-block; cursor: pointer; }
.aksrt-btnset-item input { position: absolute; opacity: 0; pointer-events: none; }
.aksrt-btnset-item span { display: inline-block; padding: 3px 12px; border: 1px solid #c3c4c7; border-radius: 3px; background: #fff; }
.aksrt-btnset-item input:checked + span { background: #2271b1; border-color: #2271b1; color: #fff; }
.jio-cardpass-table { min-width: 1000px; }
.jio-btnset { display: flex; flex-wrap: wrap; gap: 6px; }
.jio-btnset-item { display: inline-block; cursor: pointer; }
.jio-btnset-item input { position: absolute; opacity: 0; pointer-events: none; }
.jio-btnset-item span { display: inline-block; padding: 3px 12px; border: 1px solid #c3c4c7; border-radius: 3px; background: #fff; }
.jio-btnset-item input:checked + span { background: #2271b1; border-color: #2271b1; color: #fff; }
/* 导入提示:复刻后台 notice-warning 的外观,但**不用** .notice 类 —— 见 submessage_row()
的说明,带 .notice 会被 wp-admin/js/common.js 搬到页面顶部。改这里请连带改那里。 */
.aksrt-submsg { margin: 6px 0; padding: 6px 12px; background: #fef8ee; border-left: 4px solid #f0b849; }
.jio-submsg { margin: 6px 0; padding: 6px 12px; background: #fef8ee; border-left: 4px solid #f0b849; }
/* 提示内的条目列表必须有 <ul> 承载:list-style-position 默认 outside,
圆点落在内容盒左侧的 padding 区里;若 <li> 没有列表容器提供缩进,
圆点就会溢出到提示块的文字边界之外。这里显式给出缩进,避免依赖 UA/后台的 ul 默认值。 */
.aksrt-submsg-list { margin: 6px 0; padding-left: 20px; list-style: disc; }
.aksrt-copy-tip { margin-left: 6px; color: #00a32a; font-size: 12px; white-space: nowrap; }
.jio-submsg-list { margin: 6px 0; padding-left: 20px; list-style: disc; }
.jio-copy-tip { margin-left: 6px; color: #00a32a; font-size: 12px; white-space: nowrap; }
</style>
<script>
(function () {
@@ -56,7 +56,7 @@ trait PayChargeCardsAssets {
// 含方括号的字段名(如 vip_exchange[time])会让选择器抛异常并被 catch 吞掉,
// 从而把依赖行永久留在初始状态。
function fieldsByName(key) {
var all = document.querySelectorAll('.aksrt-cardpass-form [name]');
var all = document.querySelectorAll('.jio-cardpass-form [name]');
var out = [];
for (var i = 0; i < all.length; i++) {
if (all[i].getAttribute('name') === key) out.push(all[i]);
@@ -77,7 +77,7 @@ trait PayChargeCardsAssets {
}
function evalDeps() {
// 全文档查找 data-dep 节点:行与提示块各自带状态、各自落 display。
// 刻意不做限域(不写 .aksrt-cardpass-form 前缀):提示块一旦被 HTML 解析
// 刻意不做限域(不写 .jio-cardpass-form 前缀):提示块一旦被 HTML 解析
// 移出表格甚至移出表单子树,限域查找会让它停在初始隐藏状态,表现为「提示不见了」。
document.querySelectorAll('[data-dep]').forEach(function (el) {
try {
@@ -115,9 +115,9 @@ trait PayChargeCardsAssets {
if (!text) return;
var done = function () {
var tip = document.createElement('span');
tip.className = 'aksrt-copy-tip';
tip.className = 'jio-copy-tip';
var tag = el.getAttribute('data-clipboard-tag') || '';
tip.textContent = (tag ? tag + ' ' : '') + '<?php echo esc_js(__('已复制', 'aksrt')); ?>';
tip.textContent = (tag ? tag + ' ' : '') + '<?php echo esc_js(__('已复制', 'jio')); ?>';
el.appendChild(tip);
setTimeout(function () { tip.remove(); }, 1200);
};
+91 -91
View File
@@ -1,6 +1,6 @@
<?php
/**
* @package AKSRT
* @package Jio
*
* PayChargeCards 的「添加卡密 / 导出卡密」表单渲染与依赖显隐求值。
* 表单结构、字段依赖(field_row / submessage_row / deps_visible)、按钮组与数字输入
@@ -9,7 +9,7 @@
* 作为 flat sibling trait 被 PayChargeCards 使用(PSR-4:trait 必须与宿主类同目录)。
*/
namespace AKSRT\Admin\Native;
namespace Jio\Admin\Native;
defined('ABSPATH') || exit;
@@ -18,55 +18,55 @@ trait PayChargeCardsForms {
private function render_add_form(): void {
$type_labels = $this->type_labels();
?>
<p><b><?php esc_html_e('在此添加卡密', 'aksrt'); ?></b></p>
<p><b><?php esc_html_e('在此添加卡密', 'jio'); ?></b></p>
<ul style="list-style:disc;margin:0 0 12px 18px;line-height:1.9">
<li><?php esc_html_e('如果您已经准备好了卡密资料,请选择导入的方式添加', 'aksrt'); ?></li>
<li><?php esc_html_e('您也可以采用系统生成的方式,自动批量添加卡密', 'aksrt'); ?></li>
<li><?php esc_html_e('如果您已经准备好了卡密资料,请选择导入的方式添加', 'jio'); ?></li>
<li><?php esc_html_e('您也可以采用系统生成的方式,自动批量添加卡密', 'jio'); ?></li>
</ul>
<form class="aksrt-cardpass-form" method="post">
<form class="jio-cardpass-form" method="post">
<?php wp_nonce_field(self::NONCE); ?>
<input type="hidden" name="action" value="add" />
<table class="form-table" role="presentation">
<?php
$this->field_row(
__('卡密类型', 'aksrt'),
__('卡密类型', 'jio'),
$this->button_set('type', $type_labels, 'balance'),
[]
);
$this->field_row(
__('添加方式', 'aksrt'),
__('添加方式', 'jio'),
$this->button_set('add_type', [
'auto' => __('系统自动生成', 'aksrt'),
'import' => __('导入卡密', 'aksrt'),
'auto' => __('系统自动生成', 'jio'),
'import' => __('导入卡密', 'jio'),
], 'auto'),
[]
);
// 自动生成
$this->field_row(
__('生成数量', 'aksrt'),
$this->spinner('auto_num', 20, __('张', 'aksrt')),
__('生成数量', 'jio'),
$this->spinner('auto_num', 20, __('张', 'jio')),
[['key' => 'add_type', 'op' => '==', 'val' => 'auto']],
__('需要生成多少张卡密(1 - 1000 的整数,单次生成数量太多可能会对服务器性能造成影响)', 'aksrt')
__('需要生成多少张卡密(1 - 1000 的整数,单次生成数量太多可能会对服务器性能造成影响)', 'jio')
);
$this->field_row(
__('标识', 'aksrt'),
__('标识', 'jio'),
'<input type="text" class="regular-text" name="auto_remarks" value="' . esc_attr('cardpass_' . current_time('YmdHis')) . '" />',
[['key' => 'add_type', 'op' => '==', 'val' => 'auto']],
__('对生成的卡密做标记标识,方便后期查找管理', 'aksrt') . '<br>' . sprintf(esc_html__('注意:标识中不能有%1$s字符,系统已占用', 'aksrt'), '<code>shipped</code>')
__('对生成的卡密做标记标识,方便后期查找管理', 'jio') . '<br>' . sprintf(esc_html__('注意:标识中不能有%1$s字符,系统已占用', 'jio'), '<code>shipped</code>')
);
$this->field_row(
__('面额', 'aksrt'),
__('面额', 'jio'),
'<input type="number" step="any" name="auto_price" value="0" />',
[
['key' => 'add_type', 'op' => '==', 'val' => 'auto'],
['key' => 'type', 'op' => 'any', 'val' => 'balance,credits'],
],
__('单张卡密的面额', 'aksrt')
__('单张卡密的面额', 'jio')
);
$this->field_row(
__('兑换会员', 'aksrt'),
__('会员等级', 'aksrt') . ' ' . $this->button_set('vip_exchange[level]', [
__('兑换会员', 'jio'),
__('会员等级', 'jio') . ' ' . $this->button_set('vip_exchange[level]', [
'1' => $this->vip_level_name(1),
'2' => $this->vip_level_name(2),
], '1'),
@@ -77,18 +77,18 @@ trait PayChargeCardsForms {
);
$this->field_row(
' ',
__('兑换会员时长', 'aksrt') . '<br><input type="text" name="vip_exchange[time]" value="" />',
__('兑换会员时长', 'jio') . '<br><input type="text" name="vip_exchange[time]" value="" />',
[
['key' => 'add_type', 'op' => '==', 'val' => 'auto'],
['key' => 'type', 'op' => '==', 'val' => 'vip'],
],
__('填', 'aksrt') . '<code>Permanent</code>' . __('为永久', 'aksrt')
__('填', 'jio') . '<code>Permanent</code>' . __('为永久', 'jio')
);
$this->field_row(
' ',
__('时长单位', 'aksrt') . ' ' . $this->button_set('vip_exchange[unit]', [
'day' => __('天', 'aksrt'),
'month' => __('个月', 'aksrt'),
__('时长单位', 'jio') . ' ' . $this->button_set('vip_exchange[unit]', [
'day' => __('天', 'jio'),
'month' => __('个月', 'jio'),
], 'month'),
[
['key' => 'add_type', 'op' => '==', 'val' => 'auto'],
@@ -97,41 +97,41 @@ trait PayChargeCardsForms {
]
);
$this->field_row(
__('自定义显示名称', 'aksrt'),
__('卡号显示名称', 'aksrt') . '<br><input type="text" class="regular-text" name="custom_name[card_name]" value="" /><br>' .
__('卡密显示名称', 'aksrt') . '<br><input type="text" class="regular-text" name="custom_name[password_name]" value="" />',
__('自定义显示名称', 'jio'),
__('卡号显示名称', 'jio') . '<br><input type="text" class="regular-text" name="custom_name[card_name]" value="" /><br>' .
__('卡密显示名称', 'jio') . '<br><input type="text" class="regular-text" name="custom_name[password_name]" value="" />',
[
['key' => 'add_type', 'op' => '==', 'val' => 'auto'],
['key' => 'type', 'op' => '==', 'val' => 'custom'],
]
);
$this->field_row(
__('高级选项', 'aksrt'),
__('高级选项', 'jio'),
'<input type="checkbox" name="auto_top_s" value="1" />',
[['key' => 'add_type', 'op' => '==', 'val' => 'auto']]
);
$this->field_row(
__('自定义卡号位数', 'aksrt'),
$this->spinner('auto_rand_number_limit', 20, __('位数', 'aksrt')),
__('自定义卡号位数', 'jio'),
$this->spinner('auto_rand_number_limit', 20, __('位数', 'jio')),
[
['key' => 'add_type', 'op' => '==', 'val' => 'auto'],
['key' => 'auto_top_s', 'op' => 'checked'],
]
);
$this->field_row(
__('自定义密码位数', 'aksrt'),
$this->spinner('auto_rand_password_limit', 35, __('位数', 'aksrt')),
__('自定义密码位数', 'jio'),
$this->spinner('auto_rand_password_limit', 35, __('位数', 'jio')),
[
['key' => 'add_type', 'op' => '==', 'val' => 'auto'],
['key' => 'auto_top_s', 'op' => 'checked'],
],
__('自定义自动生成的长度(不能太短,太短可能会出现重复)如果启用了单密码模式,可以将卡号位数设置为0', 'aksrt')
__('自定义自动生成的长度(不能太短,太短可能会出现重复)如果启用了单密码模式,可以将卡号位数设置为0', 'jio')
);
// 导入提示统一放在「卡密数据」正上方:切换卡密类型/添加方式时,
// 命中的那一条紧邻示例数据与输入框,便于对照填写。
$this->field_row(
__('单密码模式', 'aksrt'),
__('单密码模式', 'jio'),
'<input type="checkbox" name="only_pass_s" value="1" />',
[
['key' => 'add_type', 'op' => '==', 'val' => 'import'],
@@ -143,32 +143,32 @@ trait PayChargeCardsForms {
['key' => 'add_type', 'op' => '!=', 'val' => 'auto'],
['key' => 'type', 'op' => 'any', 'val' => 'balance,credits'],
],
'<p><b>' . esc_html__('导入卡密', 'aksrt') . '</b></p>
<ul class="aksrt-submsg-list">
<li>' . esc_html__('一行一个卡密,单行格式为:', 'aksrt') . '<code>' . esc_html__('卡号 密码 面额 标识', 'aksrt') . '</code></li>
<li>' . esc_html__('卡号、密码、面额、标识默认使用空格分割,您可以在下方自定义分割符号,与您的数据对应即可', 'aksrt') . '</li>
<li>' . esc_html__('单次导入数量太多可能会对服务器性能造成影响', 'aksrt') . '</li><li>' . esc_html__('如果使用的是单密码模式,卡号部分随便填什么都行,但不能留空', 'aksrt') . '</li>
'<p><b>' . esc_html__('导入卡密', 'jio') . '</b></p>
<ul class="jio-submsg-list">
<li>' . esc_html__('一行一个卡密,单行格式为:', 'jio') . '<code>' . esc_html__('卡号 密码 面额 标识', 'jio') . '</code></li>
<li>' . esc_html__('卡号、密码、面额、标识默认使用空格分割,您可以在下方自定义分割符号,与您的数据对应即可', 'jio') . '</li>
<li>' . esc_html__('单次导入数量太多可能会对服务器性能造成影响', 'jio') . '</li><li>' . esc_html__('如果使用的是单密码模式,卡号部分随便填什么都行,但不能留空', 'jio') . '</li>
</ul>
<p><b>' . esc_html__('数据示例', 'aksrt') . '</b></p>
<div>4849977843617 3w20V4Qe7dRhAIoPYum6KSA 50 ' . sprintf(esc_html__('%1$s余额', 'aksrt'), esc_html('50' . $this->currency_unit())) . '</div>
<div>6484027668705 SkdJZ5vWcpqPxrbs3pqPcpq 100 ' . sprintf(esc_html__('%1$s余额', 'aksrt'), esc_html('100' . $this->currency_unit())) . '</div>'
<p><b>' . esc_html__('数据示例', 'jio') . '</b></p>
<div>4849977843617 3w20V4Qe7dRhAIoPYum6KSA 50 ' . sprintf(esc_html__('%1$s余额', 'jio'), esc_html('50' . $this->currency_unit())) . '</div>
<div>6484027668705 SkdJZ5vWcpqPxrbs3pqPcpq 100 ' . sprintf(esc_html__('%1$s余额', 'jio'), esc_html('100' . $this->currency_unit())) . '</div>'
);
$this->submessage_row(
[
['key' => 'add_type', 'op' => '!=', 'val' => 'auto'],
['key' => 'type', 'op' => '==', 'val' => 'vip'],
],
'<p><b>' . esc_html__('导入会员兑换卡密', 'aksrt') . '</b></p>
<ul class="aksrt-submsg-list">
<li>' . esc_html__('一行一个卡密,单行格式为:', 'aksrt') . '<code>' . esc_html__('卡号 密码 会员参数 标识', 'aksrt') . '</code></li>
<li>' . esc_html__('卡号、密码、面额、标识默认使用空格分割,您可以在下方自定义分割符号,与您的数据对应即可', 'aksrt') . '</li>
<li>' . esc_html__('会员参数格式为:', 'aksrt') . '<code>level=1&time=5&unit=day</code></li>
<li>' . esc_html__('参数说明:level为等级,只能为1或2。time为时长,填Permanent或数字。unit为时长单位,只能填day(天)或者month(月)', 'aksrt') . '</li>
<li>' . esc_html__('单次导入数量太多可能会对服务器性能造成影响', 'aksrt') . '</li><li>' . esc_html__('如果使用的是单密码模式,卡号部分随便填什么都行,但不能留空', 'aksrt') . '</li>
'<p><b>' . esc_html__('导入会员兑换卡密', 'jio') . '</b></p>
<ul class="jio-submsg-list">
<li>' . esc_html__('一行一个卡密,单行格式为:', 'jio') . '<code>' . esc_html__('卡号 密码 会员参数 标识', 'jio') . '</code></li>
<li>' . esc_html__('卡号、密码、面额、标识默认使用空格分割,您可以在下方自定义分割符号,与您的数据对应即可', 'jio') . '</li>
<li>' . esc_html__('会员参数格式为:', 'jio') . '<code>level=1&time=5&unit=day</code></li>
<li>' . esc_html__('参数说明:level为等级,只能为1或2。time为时长,填Permanent或数字。unit为时长单位,只能填day(天)或者month(月)', 'jio') . '</li>
<li>' . esc_html__('单次导入数量太多可能会对服务器性能造成影响', 'jio') . '</li><li>' . esc_html__('如果使用的是单密码模式,卡号部分随便填什么都行,但不能留空', 'jio') . '</li>
</ul>
<p><b>' . esc_html__('数据示例', 'aksrt') . '</b></p>
<div>4849977843617 3w20V4Qe7dRhAIoPYum6KSA level=1&time=5&unit=day ' . esc_html__('5天一级会员', 'aksrt') . '</div>
<div>6484027668705 SkdJZ5vWcpqPxrbs3pqPcpq level=2&time=Permanent ' . esc_html__('永久2级会员', 'aksrt') . '</div>
<p><b>' . esc_html__('数据示例', 'jio') . '</b></p>
<div>4849977843617 3w20V4Qe7dRhAIoPYum6KSA level=1&time=5&unit=day ' . esc_html__('5天一级会员', 'jio') . '</div>
<div>6484027668705 SkdJZ5vWcpqPxrbs3pqPcpq level=2&time=Permanent ' . esc_html__('永久2级会员', 'jio') . '</div>
'
);
$this->submessage_row(
@@ -177,17 +177,17 @@ trait PayChargeCardsForms {
['key' => 'type', 'op' => '==', 'val' => 'custom'],
['key' => 'only_pass_s', 'op' => 'unchecked'],
],
'<p><b>' . esc_html__('自定义卡密导入', 'aksrt') . '</b></p>
<ul class="aksrt-submsg-list">
<li>' . esc_html__('一行一个卡密,单行格式为:', 'aksrt') . '<code>' . esc_html__('卡号 密码 标识 自定义参数', 'aksrt') . '</code></li>
<li>' . esc_html__('默认使用空格分割,您可以在下方自定义分割符号,与您的数据对应即可', 'aksrt') . '</li>
<li>' . esc_html__('自定义参数格式为:', 'aksrt') . '<code>card_name=授权号&password_name=授权密码</code>,' . esc_html__('card_name为卡号的显示名称,password_name为卡密的显示名称,可以留空', 'aksrt') . '</li>
<li>' . esc_html__('单次导入数量太多可能会对服务器性能造成影响', 'aksrt') . '</li>
'<p><b>' . esc_html__('自定义卡密导入', 'jio') . '</b></p>
<ul class="jio-submsg-list">
<li>' . esc_html__('一行一个卡密,单行格式为:', 'jio') . '<code>' . esc_html__('卡号 密码 标识 自定义参数', 'jio') . '</code></li>
<li>' . esc_html__('默认使用空格分割,您可以在下方自定义分割符号,与您的数据对应即可', 'jio') . '</li>
<li>' . esc_html__('自定义参数格式为:', 'jio') . '<code>card_name=授权号&password_name=授权密码</code>,' . esc_html__('card_name为卡号的显示名称,password_name为卡密的显示名称,可以留空', 'jio') . '</li>
<li>' . esc_html__('单次导入数量太多可能会对服务器性能造成影响', 'jio') . '</li>
</ul>
<p><b>' . esc_html__('数据示例', 'aksrt') . '</b></p>
<div>4849977843617 3w20V4Qe7dRhAIoPYum6KSA ' . esc_html__('平台卡密01', 'aksrt') . ' card_name=' . esc_html__('授权号', 'aksrt') . '&password_name=' . esc_html__('授权码', 'aksrt') . '</div>
<div>6484027668705 SkdJZ5vWcpqPxrbs3pqPcpq ' . esc_html__('平台卡密01', 'aksrt') . ' card_name=' . esc_html__('授权号', 'aksrt') . '&password_name=' . esc_html__('授权码', 'aksrt') . '</div>
<div>111111@qq.com 11assssddsds ' . esc_html__('某某账号', 'aksrt') . ' card_name=' . esc_html__('账号', 'aksrt') . '&password_name=' . esc_html__('密码', 'aksrt') . '</div>'
<p><b>' . esc_html__('数据示例', 'jio') . '</b></p>
<div>4849977843617 3w20V4Qe7dRhAIoPYum6KSA ' . esc_html__('平台卡密01', 'jio') . ' card_name=' . esc_html__('授权号', 'jio') . '&password_name=' . esc_html__('授权码', 'jio') . '</div>
<div>6484027668705 SkdJZ5vWcpqPxrbs3pqPcpq ' . esc_html__('平台卡密01', 'jio') . ' card_name=' . esc_html__('授权号', 'jio') . '&password_name=' . esc_html__('授权码', 'jio') . '</div>
<div>111111@qq.com 11assssddsds ' . esc_html__('某某账号', 'jio') . ' card_name=' . esc_html__('账号', 'jio') . '&password_name=' . esc_html__('密码', 'jio') . '</div>'
);
$this->submessage_row(
[
@@ -195,72 +195,72 @@ trait PayChargeCardsForms {
['key' => 'type', 'op' => '==', 'val' => 'custom'],
['key' => 'only_pass_s', 'op' => 'checked'],
],
'<p><b>' . esc_html__('自定义卡密导入(单密码模式)', 'aksrt') . '</b></p>
<ul class="aksrt-submsg-list">
<li>' . esc_html__('一行一个卡密,单行格式为:', 'aksrt') . '<code>' . esc_html__('密码 标识 自定义参数', 'aksrt') . '</code></li>
<li>' . esc_html__('默认使用空格分割,您可以在下方自定义分割符号,与您的数据对应即可', 'aksrt') . '</li>
<li>' . esc_html__('单次导入数量太多可能会对服务器性能造成影响', 'aksrt') . '</li>
<li>' . esc_html__('自定义参数格式为:', 'aksrt') . '<code>password_name=授权密码</code>,' . esc_html__('password_name为卡密的显示名称,可以留空', 'aksrt') . '</li>
'<p><b>' . esc_html__('自定义卡密导入(单密码模式)', 'jio') . '</b></p>
<ul class="jio-submsg-list">
<li>' . esc_html__('一行一个卡密,单行格式为:', 'jio') . '<code>' . esc_html__('密码 标识 自定义参数', 'jio') . '</code></li>
<li>' . esc_html__('默认使用空格分割,您可以在下方自定义分割符号,与您的数据对应即可', 'jio') . '</li>
<li>' . esc_html__('单次导入数量太多可能会对服务器性能造成影响', 'jio') . '</li>
<li>' . esc_html__('自定义参数格式为:', 'jio') . '<code>password_name=授权密码</code>,' . esc_html__('password_name为卡密的显示名称,可以留空', 'jio') . '</li>
</ul>
<p><b>' . esc_html__('数据示例', 'aksrt') . '</b></p>
<div>3w20V4Qe7dRhAIoPYum6KSA ' . esc_html__('平台卡密01', 'aksrt') . ' password_name=' . esc_html__('授权码', 'aksrt') . '</div>
<div>SkdJZ5vWcpqPxrbs3pqPcpq ' . esc_html__('平台卡密01', 'aksrt') . ' password_name=' . esc_html__('授权码', 'aksrt') . '</div>'
<p><b>' . esc_html__('数据示例', 'jio') . '</b></p>
<div>3w20V4Qe7dRhAIoPYum6KSA ' . esc_html__('平台卡密01', 'jio') . ' password_name=' . esc_html__('授权码', 'jio') . '</div>
<div>SkdJZ5vWcpqPxrbs3pqPcpq ' . esc_html__('平台卡密01', 'jio') . ' password_name=' . esc_html__('授权码', 'jio') . '</div>'
);
$this->field_row(
__('卡密数据', 'aksrt'),
__('卡密数据', 'jio'),
'<textarea name="import_data" rows="10" class="large-text code" style="resize: both;max-width: none;"></textarea>',
[['key' => 'add_type', 'op' => '==', 'val' => 'import']]
);
$this->field_row(
__('自定义分隔符号', 'aksrt'),
__('自定义分隔符号', 'jio'),
'<input type="text" class="mini-input" style="width:80px;" name="import_division" value=" " />',
[['key' => 'add_type', 'op' => '==', 'val' => 'import']],
__('卡号和密码之间分割符号(默认为空格分割)', 'aksrt')
__('卡号和密码之间分割符号(默认为空格分割)', 'jio')
);
?>
</table>
<?php submit_button(__('确认提交', 'aksrt'), 'primary', 'submit', true); ?>
<?php submit_button(__('确认提交', 'jio'), 'primary', 'submit', true); ?>
</form>
<?php
}
private function render_export_form(): void {
?>
<form class="aksrt-cardpass-form" method="post" action="<?php echo esc_url(admin_url('admin-ajax.php')); ?>">
<form class="jio-cardpass-form" method="post" action="<?php echo esc_url(admin_url('admin-ajax.php')); ?>">
<input type="hidden" name="action" value="<?php echo esc_attr(self::EXPORT_AJAX_ACTION); ?>" />
<table class="form-table" role="presentation">
<?php
$this->field_row(
__('卡密类型', 'aksrt'),
__('卡密类型', 'jio'),
$this->button_set('type', $this->type_labels(), 'balance'),
[]
);
$this->field_row(
__('选择状态', 'aksrt'),
__('选择状态', 'jio'),
$this->button_set('status', [
'all' => __('全部', 'aksrt'),
'0' => __('未使用', 'aksrt'),
'used' => __('已使用', 'aksrt'),
'all' => __('全部', 'jio'),
'0' => __('未使用', 'jio'),
'used' => __('已使用', 'jio'),
], 'all'),
[['key' => 'type', 'op' => '!=', 'val' => 'custom']]
);
$this->field_row(
__('导出格式', 'aksrt'),
__('导出格式', 'jio'),
$this->button_set('export_format', [
'text' => __('文本文档', 'aksrt'),
'xls' => __('Excel表格', 'aksrt'),
'text' => __('文本文档', 'jio'),
'xls' => __('Excel表格', 'jio'),
], 'xls'),
[]
);
$this->field_row(
' ',
__('分隔符号', 'aksrt') . '<br><input type="text" class="mini-input" style="width:80px;" name="text_division" value=" " />',
__('分隔符号', 'jio') . '<br><input type="text" class="mini-input" style="width:80px;" name="text_division" value=" " />',
[['key' => 'export_format', 'op' => '==', 'val' => 'text']],
__('卡号和密码之间分割符号(默认为空格分割)', 'aksrt')
__('卡号和密码之间分割符号(默认为空格分割)', 'jio')
);
?>
</table>
<?php submit_button(__('确认提交', 'aksrt'), 'primary', 'submit', true); ?>
<?php submit_button(__('确认提交', 'jio'), 'primary', 'submit', true); ?>
</form>
<?php
}
@@ -294,9 +294,9 @@ trait PayChargeCardsForms {
// 把页面上所有「非 inline」的 notice 整块搬到 .wp-header-end(不存在时取 .wrap 内首个
// h1/h2)之后。提示块一旦带上 notice,就会被搬到页面顶部、脱离「添加方式—卡密数据」
// 之间的字段区,而且搬迁发生在求值之后,行内的显隐逻辑无从纠正。
// 外观改由 .aksrt-submsg 自带(见 print_assets),与参考主题 CSF content 字段一致。
// 外观改由 .jio-submsg 自带(见 print_assets),与参考主题 CSF content 字段一致。
// data-dep 与初始隐藏状态同时挂在 <tr> 和内层提示块上:提示块若脱离所在行,仍能自行显隐。
echo '<tr' . $dep_attr . $hidden . '><td colspan="2"><div' . $dep_attr . ' class="aksrt-submsg"' . $hidden . '>' . $content . '</div></td></tr>'; // phpcs:ignore WordPress.Security.EscapeOutput -- 文案已转义。
echo '<tr' . $dep_attr . $hidden . '><td colspan="2"><div' . $dep_attr . ' class="jio-submsg"' . $hidden . '>' . $content . '</div></td></tr>'; // phpcs:ignore WordPress.Security.EscapeOutput -- 文案已转义。
}
/**
@@ -398,9 +398,9 @@ trait PayChargeCardsForms {
* @param array<string,string> $options
*/
private function button_set(string $name, array $options, string $current): string {
$html = '<div class="aksrt-btnset">';
$html = '<div class="jio-btnset">';
foreach ($options as $value => $label) {
$html .= '<label class="aksrt-btnset-item"><input type="radio" name="' . esc_attr($name) . '" value="' . esc_attr((string) $value) . '"' . checked((string) $current, (string) $value, false) . '><span>' . esc_html((string) $label) . '</span></label>';
$html .= '<label class="jio-btnset-item"><input type="radio" name="' . esc_attr($name) . '" value="' . esc_attr((string) $value) . '"' . checked((string) $current, (string) $value, false) . '><span>' . esc_html((string) $label) . '</span></label>';
}
return $html . '</div>';
}
+42 -42
View File
@@ -1,12 +1,12 @@
<?php
/**
* @package AKSRT
* @package Jio
*
* PayChargeCards 列表渲染(过滤 / 搜索 / 排序 / 批量删除 / 分页)与翻页参数拼装。
* 作为 flat sibling trait 被 PayChargeCards 使用(PSR-4:trait 必须与宿主类同目录)。
*/
namespace AKSRT\Admin\Native;
namespace Jio\Admin\Native;
defined('ABSPATH') || exit;
@@ -16,7 +16,7 @@ trait PayChargeCardsList {
* @param string $s 搜索词(由页面头部统一解析后传入,用于提示、过滤链接与查询)。
*/
private function render_list(string $s): void {
$svc = \AKSRT\Payment\ChargeCard::get_instance();
$svc = \Jio\Payment\ChargeCard::get_instance();
$status = isset($_GET['status']) ? sanitize_key((string) wp_unslash($_GET['status'])) : '';
$type = isset($_GET['type']) ? sanitize_key((string) wp_unslash($_GET['type'])) : '';
@@ -39,11 +39,11 @@ trait PayChargeCardsList {
// 过滤链接(标题右侧按钮与搜索提示由页面头部统一输出)。
$sub_but = [
['name' => __('全部', 'aksrt'), 'href' => $this->page_url],
['name' => __('全部', 'jio'), 'href' => $this->page_url],
];
if ($s === '') {
$sub_but[] = ['name' => __('未使用', 'aksrt'), 'href' => add_query_arg('status', '0', $this->page_url)];
$sub_but[] = ['name' => __('已使用', 'aksrt'), 'href' => add_query_arg('status', 'used', $this->page_url)];
$sub_but[] = ['name' => __('未使用', 'jio'), 'href' => add_query_arg('status', '0', $this->page_url)];
$sub_but[] = ['name' => __('已使用', 'jio'), 'href' => add_query_arg('status', 'used', $this->page_url)];
foreach ($this->type_labels() as $_k => $_v) {
$sub_but[] = ['name' => $_v, 'href' => add_query_arg('type', $_k, $this->page_url)];
}
@@ -56,8 +56,8 @@ trait PayChargeCardsList {
$search = '<form class="form-inline form-order" method="post">
<div class="form-group" style="float: right;">
<input type="text" class="form-control" name="s" placeholder="' . esc_attr__('搜索卡密', 'aksrt') . '">
<button type="submit" class="button">' . esc_html__('提交', 'aksrt') . '</button>
<input type="text" class="form-control" name="s" placeholder="' . esc_attr__('搜索卡密', 'jio') . '">
<button type="submit" class="button">' . esc_html__('提交', 'jio') . '</button>
</div>
</form>';
@@ -93,8 +93,8 @@ trait PayChargeCardsList {
if (!$items) {
NativeUi::table_card_open();
echo '<table class="widefat fixed striped posts table table-bordered aksrt-cardpass-table">';
echo '<tbody>' . NativeUi::empty_row(1, esc_html__('未找到对应内容,或暂无内容', 'aksrt')) . '</tbody>';
echo '<table class="widefat fixed striped posts table table-bordered jio-cardpass-table">';
echo '<tbody>' . NativeUi::empty_row(1, esc_html__('未找到对应内容,或暂无内容', 'jio')) . '</tbody>';
echo '</table>';
NativeUi::table_wrap_close();
NativeUi::card_close();
@@ -103,22 +103,22 @@ trait PayChargeCardsList {
// 表头(与参考主题同列、同排序链接)。
$theads = [
['width' => '6%', 'orderby' => 'card', 'name' => __('卡号', 'aksrt')],
['width' => '10%', 'orderby' => 'password', 'name' => __('密码', 'aksrt')],
['width' => '5%', 'orderby' => 'type', 'name' => __('类型', 'aksrt')],
['width' => '5%', 'orderby' => 'create_time', 'name' => __('创建时间', 'aksrt')],
['width' => '5%', 'orderby' => 'modified_time', 'name' => __('更新时间', 'aksrt')],
['width' => '5%', 'orderby' => 'status', 'name' => __('状态', 'aksrt')],
['width' => '8%', 'orderby' => 'other', 'name' => __('标识', 'aksrt')],
['width' => '6%', 'orderby' => 'card', 'name' => __('卡号', 'jio')],
['width' => '10%', 'orderby' => 'password', 'name' => __('密码', 'jio')],
['width' => '5%', 'orderby' => 'type', 'name' => __('类型', 'jio')],
['width' => '5%', 'orderby' => 'create_time', 'name' => __('创建时间', 'jio')],
['width' => '5%', 'orderby' => 'modified_time', 'name' => __('更新时间', 'jio')],
['width' => '5%', 'orderby' => 'status', 'name' => __('状态', 'jio')],
['width' => '8%', 'orderby' => 'other', 'name' => __('标识', 'jio')],
];
$thead_th = '<td id="cb" class="manage-column column-cb check-column" style="width: 2%;"><label class="screen-reader-text" for="cb-select-all-1">' . esc_html__('全选', 'aksrt') . '</label><input id="cb-select-all-1" type="checkbox"></td>';
$thead_th = '<td id="cb" class="manage-column column-cb check-column" style="width: 2%;"><label class="screen-reader-text" for="cb-select-all-1">' . esc_html__('全选', 'jio') . '</label><input id="cb-select-all-1" type="checkbox"></td>';
foreach ($theads as $thead) {
$orderby_links = '';
if ($thead['orderby']) {
$orderby_url = add_query_arg('orderby', $thead['orderby']);
$orderby_links .= '<a title="' . esc_attr__('降序', 'aksrt') . '" href="' . esc_url(add_query_arg('desc', 'ASC', $orderby_url)) . '"><span class="dashicons dashicons-arrow-up"></span></a>';
$orderby_links .= '<a title="' . esc_attr__('升序', 'aksrt') . '" href="' . esc_url(add_query_arg('desc', 'DESC', $orderby_url)) . '"><span class="dashicons dashicons-arrow-down"></span></a>';
$orderby_links .= '<a title="' . esc_attr__('降序', 'jio') . '" href="' . esc_url(add_query_arg('desc', 'ASC', $orderby_url)) . '"><span class="dashicons dashicons-arrow-up"></span></a>';
$orderby_links .= '<a title="' . esc_attr__('升序', 'jio') . '" href="' . esc_url(add_query_arg('desc', 'DESC', $orderby_url)) . '"><span class="dashicons dashicons-arrow-down"></span></a>';
$orderby_links = '<span class="orderby-but">' . $orderby_links . '</span>';
}
$thead_th .= '<th class="" width="' . esc_attr($thead['width']) . '">' . esc_html($thead['name']) . $orderby_links . '</th>';
@@ -135,17 +135,17 @@ trait PayChargeCardsList {
// 自定义卡密不展示状态;其余类型按状态着色(类型链接统一在下方链表中生成)。
$status_html = '';
if ($msg['type'] !== 'custom') {
$status_html = '<span style="color: #3d7ffd;">' . esc_html__('未使用', 'aksrt') . '</span>';
$status_html = '<span style="color: #3d7ffd;">' . esc_html__('未使用', 'jio') . '</span>';
if ($msg['status'] === 'used') {
$status_html = '<span style="color: #f93b3b;">' . esc_html__('已使用', 'aksrt') . '</span>';
$status_html = '<span style="color: #f93b3b;">' . esc_html__('已使用', 'jio') . '</span>';
if (!empty($msg['order_number'])) {
$order_link_url = admin_url('admin.php?page=aksrt_orders&s=' . urlencode((string) $msg['order_number']));
$status_html .= '<a style="color: #ff6215;" target="_blank" href="' . esc_url($order_link_url) . '"> [' . esc_html__('查看', 'aksrt') . ']</a>';
$order_link_url = admin_url('admin.php?page=jio_orders&s=' . urlencode((string) $msg['order_number']));
$status_html .= '<a style="color: #ff6215;" target="_blank" href="' . esc_url($order_link_url) . '"> [' . esc_html__('查看', 'jio') . ']</a>';
}
} elseif ($msg['status'] === 'expired') {
$status_html = '<span style="color: #8c8f94;">' . esc_html__('已过期', 'aksrt') . '</span>';
$status_html = '<span style="color: #8c8f94;">' . esc_html__('已过期', 'jio') . '</span>';
} elseif ($msg['status'] === 'disabled') {
$status_html = '<span style="color: #8c8f94;">' . esc_html__('已禁用', 'aksrt') . '</span>';
$status_html = '<span style="color: #8c8f94;">' . esc_html__('已禁用', 'jio') . '</span>';
}
}
@@ -155,28 +155,28 @@ trait PayChargeCardsList {
}
if ($msg['type'] === 'vip') {
$type_html = '<a style="color:#975106" href="' . esc_url(add_query_arg('type', 'vip', $this->page_url)) . '">' . esc_html__('会员兑换', 'aksrt') . '</a>';
$type_html = '<a style="color:#975106" href="' . esc_url(add_query_arg('type', 'vip', $this->page_url)) . '">' . esc_html__('会员兑换', 'jio') . '</a>';
$level = (int) ($meta['level'] ?? 1);
$type_html .= '<br>' . esc_html($this->vip_level_name($level)) . ':' . esc_html($this->vip_time_text($meta));
} elseif ($msg['type'] === 'credits') {
$type_html = '<a style="color:#289d0f" href="' . esc_url(add_query_arg('type', 'credits', $this->page_url)) . '">' . esc_html__('积分兑换', 'aksrt') . '</a>';
$type_html .= '<br>' . esc_html__('积分:', 'aksrt') . esc_html((string) (int) ($meta['points'] ?? 0));
$type_html = '<a style="color:#289d0f" href="' . esc_url(add_query_arg('type', 'credits', $this->page_url)) . '">' . esc_html__('积分兑换', 'jio') . '</a>';
$type_html .= '<br>' . esc_html__('积分:', 'jio') . esc_html((string) (int) ($meta['points'] ?? 0));
} elseif ($msg['type'] === 'balance') {
$type_html = '<a style="color:#0c76e6" href="' . esc_url(add_query_arg('type', 'balance', $this->page_url)) . '">' . esc_html__('余额充值', 'aksrt') . '</a>';
$type_html .= '<br>' . esc_html__('金额:', 'aksrt') . esc_html($this->balance_amount_text($meta));
$type_html = '<a style="color:#0c76e6" href="' . esc_url(add_query_arg('type', 'balance', $this->page_url)) . '">' . esc_html__('余额充值', 'jio') . '</a>';
$type_html .= '<br>' . esc_html__('金额:', 'jio') . esc_html($this->balance_amount_text($meta));
} else {
$type_html = '<a style="color:#a42ade" href="' . esc_url(add_query_arg('type', 'custom', $this->page_url)) . '">' . esc_html__('自定义卡密', 'aksrt') . '</a>';
$type_html = '<a style="color:#a42ade" href="' . esc_url(add_query_arg('type', 'custom', $this->page_url)) . '">' . esc_html__('自定义卡密', 'jio') . '</a>';
}
$card_name = (string) ($meta['custom_name']['card_name'] ?? '');
$password_name = (string) ($meta['custom_name']['password_name'] ?? '');
$tbody .= '<tr>';
$tbody .= '<th scope="row" class="check-column"><label class="screen-reader-text" for="cb-select-' . (int) $msg['id'] . '">' . esc_html__('选择', 'aksrt') . '</label>
$tbody .= '<th scope="row" class="check-column"><label class="screen-reader-text" for="cb-select-' . (int) $msg['id'] . '">' . esc_html__('选择', 'jio') . '</label>
<input id="cb-select-' . (int) $msg['id'] . '" type="checkbox" name="action_id[]" value="' . (int) $msg['id'] . '">
</th>';
$tbody .= "<td><span data-clipboard-text='" . esc_attr((string) $msg['card']) . "' data-clipboard-tag=\"" . esc_attr($card_name !== '' ? $card_name : __('卡号', 'aksrt')) . "\">" . esc_html($card_name) . ' ' . esc_html((string) $msg['card']) . '</span></td>';
$tbody .= "<td><span data-clipboard-text='" . esc_attr((string) $msg['password']) . "' data-clipboard-tag=\"" . esc_attr($password_name !== '' ? $password_name : __('密码', 'aksrt')) . "\">" . esc_html($password_name) . ' ' . esc_html((string) $msg['password']) . '</span></td>';
$tbody .= "<td><span data-clipboard-text='" . esc_attr((string) $msg['card']) . "' data-clipboard-tag=\"" . esc_attr($card_name !== '' ? $card_name : __('卡号', 'jio')) . "\">" . esc_html($card_name) . ' ' . esc_html((string) $msg['card']) . '</span></td>';
$tbody .= "<td><span data-clipboard-text='" . esc_attr((string) $msg['password']) . "' data-clipboard-tag=\"" . esc_attr($password_name !== '' ? $password_name : __('密码', 'jio')) . "\">" . esc_html($password_name) . ' ' . esc_html((string) $msg['password']) . '</span></td>';
$tbody .= "<td>$type_html</td>";
$tbody .= '<td>' . esc_html((string) $msg['created_at']) . '</td>';
$tbody .= '<td>' . esc_html((string) ($msg['used_at'] ?: $msg['created_at'])) . '</td>';
@@ -185,18 +185,18 @@ trait PayChargeCardsList {
$tbody .= '</tr>';
}
echo '<form class="aksrt-cardpass-list-form" method="post">';
echo '<form class="jio-cardpass-list-form" method="post">';
wp_nonce_field(self::NONCE);
echo '<div class="bulkactions aksrt-native-bulk aksrt-cardpass-bulk">
<label for="bulk-action-selector-top" class="screen-reader-text">' . esc_html__('选择批量操作', 'aksrt') . '</label><select name="action" id="bulk-action-selector-top">
<option value="-1">' . esc_html__('批量操作', 'aksrt') . '</option>
<option value="delete">' . esc_html__('删除', 'aksrt') . '</option>
echo '<div class="bulkactions jio-native-bulk jio-cardpass-bulk">
<label for="bulk-action-selector-top" class="screen-reader-text">' . esc_html__('选择批量操作', 'jio') . '</label><select name="action" id="bulk-action-selector-top">
<option value="-1">' . esc_html__('批量操作', 'jio') . '</option>
<option value="delete">' . esc_html__('删除', 'jio') . '</option>
</select>
<input type="submit" class="button action" value="' . esc_attr__('应用', 'aksrt') . '">
<input type="submit" class="button action" value="' . esc_attr__('应用', 'jio') . '">
</div>';
NativeUi::table_card_open();
echo '<table class="widefat fixed striped posts table table-bordered aksrt-cardpass-table">';
echo '<table class="widefat fixed striped posts table table-bordered jio-cardpass-table">';
echo '<thead><tr>' . $thead_th . '</tr></thead>'; // phpcs:ignore WordPress.Security.EscapeOutput -- 已转义。
echo '<tbody>' . $tbody . '</tbody>'; // phpcs:ignore WordPress.Security.EscapeOutput -- 已转义。
echo '</table>';
+94 -94
View File
@@ -1,9 +1,9 @@
<?php
/**
* @package AKSRT
* @package Jio
*/
namespace AKSRT\Admin\Native;
namespace Jio\Admin\Native;
defined('ABSPATH') || exit;
@@ -12,9 +12,9 @@ class PayCoupons {
use PayCouponsForms;
public const CAPABILITY = 'manage_options';
public const PAGE_SLUG = 'aksrt_coupons';
public const CREATE_DIALOG = 'aksrt-coupon-create';
public const BATCH_DIALOG = 'aksrt-coupon-batch';
public const PAGE_SLUG = 'jio_coupons';
public const CREATE_DIALOG = 'jio-coupon-create';
public const BATCH_DIALOG = 'jio-coupon-batch';
private static $instance = null;
@@ -30,11 +30,11 @@ class PayCoupons {
return;
}
add_action('admin_enqueue_scripts', [$this, 'enqueue_assets']);
add_action('admin_post_aksrt_pay_create_coupon', [$this, 'handle_create_coupon']);
add_action('admin_post_aksrt_pay_batch_coupons', [$this, 'handle_batch_coupons']);
add_action('admin_post_aksrt_pay_update_coupon', [$this, 'handle_update_coupon']);
add_action('admin_post_aksrt_pay_disable_coupon', [$this, 'handle_disable_coupon']);
add_action('admin_post_aksrt_pay_enable_coupon', [$this, 'handle_enable_coupon']);
add_action('admin_post_jio_pay_create_coupon', [$this, 'handle_create_coupon']);
add_action('admin_post_jio_pay_batch_coupons', [$this, 'handle_batch_coupons']);
add_action('admin_post_jio_pay_update_coupon', [$this, 'handle_update_coupon']);
add_action('admin_post_jio_pay_disable_coupon', [$this, 'handle_disable_coupon']);
add_action('admin_post_jio_pay_enable_coupon', [$this, 'handle_enable_coupon']);
}
public function enqueue_assets(): void {
@@ -44,56 +44,56 @@ class PayCoupons {
}
$css = '/assets/css/admin/coupons.css';
$js = '/assets/js/admin-coupons.js';
wp_enqueue_style('aksrt-admin-coupons', AKSRT_URI . $css, [NativeUi::STYLE_HANDLE], (string) filemtime(AKSRT_DIR . $css));
wp_enqueue_script('aksrt-admin-coupons', AKSRT_URI . $js, [], (string) filemtime(AKSRT_DIR . $js), true);
wp_enqueue_style('jio-admin-coupons', JIO_URI . $css, [NativeUi::STYLE_HANDLE], (string) filemtime(JIO_DIR . $css));
wp_enqueue_script('jio-admin-coupons', JIO_URI . $js, [], (string) filemtime(JIO_DIR . $js), true);
}
public function render_page(): void {
$this->ensure_cap();
$ready = class_exists('\AKSRT\Payment\Coupon');
NativeUi::page_open('aksrt-coupons-page');
$ready = class_exists('\Jio\Payment\Coupon');
NativeUi::page_open('jio-coupons-page');
NativeUi::header(
__('优惠码', 'aksrt'),
__('创建、批量生成与管理结算优惠码。配置项仍在 AKSRT 主题设置 SPA。', 'aksrt'),
__('优惠码', 'jio'),
__('创建、批量生成与管理结算优惠码。配置项仍在 Jio 主题设置 SPA。', 'jio'),
'dashicons-tickets-alt',
$ready ? [
['label' => __('新建优惠码', 'aksrt'), 'dialog' => self::CREATE_DIALOG, 'primary' => true, 'icon' => 'dashicons-plus-alt2'],
['label' => __('批量生成', 'aksrt'), 'dialog' => self::BATCH_DIALOG, 'icon' => 'dashicons-randomize'],
['label' => __('新建优惠码', 'jio'), 'dialog' => self::CREATE_DIALOG, 'primary' => true, 'icon' => 'dashicons-plus-alt2'],
['label' => __('批量生成', 'jio'), 'dialog' => self::BATCH_DIALOG, 'icon' => 'dashicons-randomize'],
] : []
);
$this->render_flash();
if (!$ready) {
echo '<div class="notice notice-error"><p>' . esc_html__('优惠码模块未初始化。', 'aksrt') . '</p></div>';
echo '<div class="notice notice-error"><p>' . esc_html__('优惠码模块未初始化。', 'jio') . '</p></div>';
NativeUi::page_close();
return;
}
$svc = \AKSRT\Payment\Coupon::get_instance();
$svc = \Jio\Payment\Coupon::get_instance();
$edit_id = isset($_GET['edit_id']) ? (int) $_GET['edit_id'] : 0;
$edit_row = $edit_id > 0 ? $svc->get($edit_id) : null;
if ($edit_id > 0 && $edit_row) {
$this->render_edit_form($edit_row);
} elseif ($edit_id > 0) {
echo '<div class="notice notice-error"><p>' . esc_html__('优惠码不存在。', 'aksrt') . '</p></div>';
echo '<div class="notice notice-error"><p>' . esc_html__('优惠码不存在。', 'jio') . '</p></div>';
}
$this->render_generated_codes();
$this->render_list($svc);
$this->render_create_dialog();
$this->render_batch_dialog();
echo '<div class="aksrt-coupon-toast" role="status" aria-live="polite" data-aksrt-coupon-toast'
. ' data-copied="' . esc_attr__('已复制到剪贴板', 'aksrt') . '"'
. ' data-copy-failed="' . esc_attr__('自动复制失败,请手动选中后复制。', 'aksrt') . '"></div>';
echo '<div class="jio-coupon-toast" role="status" aria-live="polite" data-jio-coupon-toast'
. ' data-copied="' . esc_attr__('已复制到剪贴板', 'jio') . '"'
. ' data-copy-failed="' . esc_attr__('自动复制失败,请手动选中后复制。', 'jio') . '"></div>';
NativeUi::page_close();
}
/** 批量生成后一次性展示本批代码(transient 读取即删),附「复制全部」。 */
private function render_generated_codes(): void {
$key = 'aksrt_pay_coupon_codes_' . get_current_user_id();
$key = 'jio_pay_coupon_codes_' . get_current_user_id();
$codes = get_transient($key);
if (!is_array($codes) || !$codes) {
return;
@@ -101,46 +101,46 @@ class PayCoupons {
delete_transient($key);
$text = implode("\n", array_map('strval', $codes));
$copy = '<button type="button" class="button button-small" data-clipboard-text="' . esc_attr($text) . '">'
. '<span class="dashicons dashicons-admin-page" aria-hidden="true"></span>' . esc_html__('复制全部', 'aksrt') . '</button>';
. '<span class="dashicons dashicons-admin-page" aria-hidden="true"></span>' . esc_html__('复制全部', 'jio') . '</button>';
NativeUi::card_open(
sprintf(
/* translators: %d count */
__('本次生成的 %d 个优惠码', 'aksrt'),
__('本次生成的 %d 个优惠码', 'jio'),
count($codes)
),
$copy,
false,
'aksrt-coupon-generated'
'jio-coupon-generated'
);
echo '<p class="aksrt-native-field__hint">' . esc_html__('离开或刷新本页后不再显示,请及时复制保存。', 'aksrt') . '</p>';
echo '<textarea class="large-text code" rows="' . esc_attr((string) min(12, max(3, count($codes)))) . '" readonly aria-label="' . esc_attr__('本次生成的优惠码', 'aksrt') . '">' . esc_textarea($text) . '</textarea>';
echo '<p class="jio-native-field__hint">' . esc_html__('离开或刷新本页后不再显示,请及时复制保存。', 'jio') . '</p>';
echo '<textarea class="large-text code" rows="' . esc_attr((string) min(12, max(3, count($codes)))) . '" readonly aria-label="' . esc_attr__('本次生成的优惠码', 'jio') . '">' . esc_textarea($text) . '</textarea>';
NativeUi::card_close();
}
private function render_list(\AKSRT\Payment\Coupon $svc): void {
private function render_list(\Jio\Payment\Coupon $svc): void {
$status = isset($_GET['status']) ? sanitize_key((string) wp_unslash($_GET['status'])) : '';
$search = isset($_GET['s']) ? sanitize_text_field((string) wp_unslash($_GET['s'])) : '';
$page_num = max(1, (int) ($_GET['paged'] ?? 1));
$per_page = AdminListPagination::DEFAULT_PER_PAGE;
$type_labels = [
'fixed' => __('固定金额', 'aksrt'),
'percent' => __('折扣百分比', 'aksrt'),
'fixed' => __('固定金额', 'jio'),
'percent' => __('折扣百分比', 'jio'),
];
$status_map = [
'active' => [__('可用', 'aksrt'), 'success'],
'disabled' => [__('已禁用', 'aksrt'), 'neutral'],
'active' => [__('可用', 'jio'), 'success'],
'disabled' => [__('已禁用', 'jio'), 'neutral'],
];
// 筛选栏(卡片头部的 .aksrt-native-filters 表单)。
$filters = '<form method="get" class="aksrt-native-filters">';
// 筛选栏(卡片头部的 .jio-native-filters 表单)。
$filters = '<form method="get" class="jio-native-filters">';
$filters .= '<input type="hidden" name="page" value="' . esc_attr(self::PAGE_SLUG) . '" />';
$filters .= '<select name="status">';
foreach (['' => __('全部状态', 'aksrt'), 'active' => __('可用', 'aksrt'), 'disabled' => __('已禁用', 'aksrt')] as $key => $label) {
foreach (['' => __('全部状态', 'jio'), 'active' => __('可用', 'jio'), 'disabled' => __('已禁用', 'jio')] as $key => $label) {
$filters .= sprintf('<option value="%s"%s>%s</option>', esc_attr($key), selected($status, $key, false), esc_html($label));
}
$filters .= '</select>';
$filters .= '<input type="search" name="s" value="' . esc_attr($search) . '" placeholder="' . esc_attr__('搜索代码', 'aksrt') . '" />';
$filters .= '<button type="submit" class="button">' . esc_html__('筛选', 'aksrt') . '</button>';
$filters .= '<input type="search" name="s" value="' . esc_attr($search) . '" placeholder="' . esc_attr__('搜索代码', 'jio') . '" />';
$filters .= '<button type="submit" class="button">' . esc_html__('筛选', 'jio') . '</button>';
$filters .= '</form>';
$result = $svc->get_list([
@@ -166,12 +166,12 @@ class PayCoupons {
$items = $result['items'] ?? [];
}
NativeUi::table_card_open(__('优惠码列表', 'aksrt'), $filters);
NativeUi::table_card_open(__('优惠码列表', 'jio'), $filters);
echo '<table class="wp-list-table widefat fixed striped"><thead><tr>';
echo '<th>ID</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><th>' . esc_html__('状态', 'aksrt') . '</th><th>' . esc_html__('过期', 'aksrt') . '</th><th>' . esc_html__('操作', 'aksrt') . '</th>';
echo '<th>ID</th><th>' . esc_html__('代码', 'jio') . '</th><th>' . esc_html__('类型', 'jio') . '</th><th>' . esc_html__('面值', 'jio') . '</th><th>' . esc_html__('门槛/封顶', 'jio') . '</th><th>' . esc_html__('范围', 'jio') . '</th><th>' . esc_html__('已用/上限/每人', 'jio') . '</th><th>' . esc_html__('状态', 'jio') . '</th><th>' . esc_html__('过期', 'jio') . '</th><th>' . esc_html__('操作', 'jio') . '</th>';
echo '</tr></thead><tbody>';
if (!$items) {
echo NativeUi::empty_row(10, __('暂无优惠码', 'aksrt'), __('点击右上角「新建优惠码」或「批量生成」添加。', 'aksrt')); // phpcs:ignore WordPress.Security.EscapeOutput -- 内部构造,已转义。
echo NativeUi::empty_row(10, __('暂无优惠码', 'jio'), __('点击右上角「新建优惠码」或「批量生成」添加。', 'jio')); // phpcs:ignore WordPress.Security.EscapeOutput -- 内部构造,已转义。
}
foreach ($items as $row) {
$id = (int) ($row['id'] ?? 0);
@@ -180,7 +180,7 @@ class PayCoupons {
echo '<tr>';
echo '<td>' . esc_html((string) $id) . '</td>';
$code = (string) ($row['code'] ?? '');
echo '<td><button type="button" class="aksrt-coupon-code aksrt-native-mono" data-clipboard-text="' . esc_attr($code) . '" aria-label="' . esc_attr(sprintf(__('复制优惠码 %s', 'aksrt'), $code)) . '">'
echo '<td><button type="button" class="jio-coupon-code jio-native-mono" data-clipboard-text="' . esc_attr($code) . '" aria-label="' . esc_attr(sprintf(__('复制优惠码 %s', 'jio'), $code)) . '">'
. esc_html($code) . '<span class="dashicons dashicons-admin-page" aria-hidden="true"></span></button></td>';
echo '<td>' . esc_html($type_labels[$row_type] ?? $row_type) . '</td>';
echo '<td>' . esc_html((string) ($row['value'] ?? '')) . ($row_type === 'percent' ? '%' : '') . '</td>';
@@ -189,21 +189,21 @@ class PayCoupons {
echo '<td>' . esc_html((string) ((int) ($row['used_count'] ?? 0)) . '/' . (string) ((int) ($row['usage_limit'] ?? 0)) . '/' . (string) ((int) ($row['per_user_limit'] ?? 0))) . '</td>';
echo '<td>' . NativeUi::status_badge($st, $status_map) . '</td>'; // phpcs:ignore WordPress.Security.EscapeOutput -- 内部构造,已转义。
echo '<td>' . esc_html((string) ($row['expires_at'] ?? '')) . '</td>';
echo '<td><div class="aksrt-native-inline">';
echo '<a class="button button-small" href="' . esc_url($this->page_url(['edit_id' => $id])) . '">' . esc_html__('编辑', 'aksrt') . '</a>';
echo '<td><div class="jio-native-inline">';
echo '<a class="button button-small" href="' . esc_url($this->page_url(['edit_id' => $id])) . '">' . esc_html__('编辑', 'jio') . '</a>';
if ($st === 'active') {
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" class="aksrt-native-inline">';
echo '<input type="hidden" name="action" value="aksrt_pay_disable_coupon" />';
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" class="jio-native-inline">';
echo '<input type="hidden" name="action" value="jio_pay_disable_coupon" />';
echo '<input type="hidden" name="coupon_id" value="' . esc_attr((string) $id) . '" />';
wp_nonce_field('aksrt_pay_disable_coupon_' . $id);
submit_button(__('禁用', 'aksrt'), 'small', '', false);
wp_nonce_field('jio_pay_disable_coupon_' . $id);
submit_button(__('禁用', 'jio'), 'small', '', false);
echo '</form>';
} elseif ($st === 'disabled') {
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" class="aksrt-native-inline">';
echo '<input type="hidden" name="action" value="aksrt_pay_enable_coupon" />';
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" class="jio-native-inline">';
echo '<input type="hidden" name="action" value="jio_pay_enable_coupon" />';
echo '<input type="hidden" name="coupon_id" value="' . esc_attr((string) $id) . '" />';
wp_nonce_field('aksrt_pay_enable_coupon_' . $id);
submit_button(__('启用', 'aksrt'), 'small', '', false);
wp_nonce_field('jio_pay_enable_coupon_' . $id);
submit_button(__('启用', 'jio'), 'small', '', false);
echo '</form>';
}
echo '</div></td></tr>';
@@ -224,13 +224,13 @@ class PayCoupons {
public function handle_create_coupon(): void {
if (!current_user_can(self::CAPABILITY)) {
wp_die(esc_html__('权限不足', 'aksrt'));
wp_die(esc_html__('权限不足', 'jio'));
}
check_admin_referer('aksrt_pay_create_coupon');
check_admin_referer('jio_pay_create_coupon');
$ok = false;
if (class_exists('\AKSRT\Payment\Coupon')) {
$id = \AKSRT\Payment\Coupon::get_instance()->create([
if (class_exists('\Jio\Payment\Coupon')) {
$id = \Jio\Payment\Coupon::get_instance()->create([
'code' => sanitize_text_field((string) wp_unslash($_POST['code'] ?? '')),
'type' => sanitize_key((string) wp_unslash($_POST['type'] ?? 'fixed')),
'value' => trim((string) wp_unslash($_POST['value'] ?? '0')),
@@ -243,19 +243,19 @@ class PayCoupons {
]);
$ok = $id > 0;
}
wp_safe_redirect($this->page_url(['aksrt_pay_msg' => $ok ? 'coupon_created' : 'coupon_create_failed']));
wp_safe_redirect($this->page_url(['jio_pay_msg' => $ok ? 'coupon_created' : 'coupon_create_failed']));
exit;
}
public function handle_batch_coupons(): void {
if (!current_user_can(self::CAPABILITY)) {
wp_die(esc_html__('权限不足', 'aksrt'));
wp_die(esc_html__('权限不足', 'jio'));
}
check_admin_referer('aksrt_pay_batch_coupons');
check_admin_referer('jio_pay_batch_coupons');
$ok = false;
if (class_exists('\AKSRT\Payment\Coupon')) {
$result = \AKSRT\Payment\Coupon::get_instance()->batch_create([
if (class_exists('\Jio\Payment\Coupon')) {
$result = \Jio\Payment\Coupon::get_instance()->batch_create([
'type' => sanitize_key((string) wp_unslash($_POST['type'] ?? 'fixed')),
'value' => trim((string) wp_unslash($_POST['value'] ?? '0')),
'min_amount' => trim((string) wp_unslash($_POST['min_amount'] ?? '0')),
@@ -268,24 +268,24 @@ class PayCoupons {
$codes = is_array($result['codes'] ?? null) ? $result['codes'] : [];
$ok = (int) ($result['created'] ?? 0) > 0;
if ($ok) {
set_transient('aksrt_pay_coupon_codes_' . get_current_user_id(), $codes, 10 * MINUTE_IN_SECONDS);
set_transient('jio_pay_coupon_codes_' . get_current_user_id(), $codes, 10 * MINUTE_IN_SECONDS);
}
}
wp_safe_redirect($this->page_url(['aksrt_pay_msg' => $ok ? 'coupon_batch_ok' : 'coupon_batch_failed']));
wp_safe_redirect($this->page_url(['jio_pay_msg' => $ok ? 'coupon_batch_ok' : 'coupon_batch_failed']));
exit;
}
public function handle_update_coupon(): void {
if (!current_user_can(self::CAPABILITY)) {
wp_die(esc_html__('权限不足', 'aksrt'));
wp_die(esc_html__('权限不足', 'jio'));
}
$id = isset($_POST['coupon_id']) ? (int) $_POST['coupon_id'] : 0;
check_admin_referer('aksrt_pay_update_coupon_' . $id);
check_admin_referer('jio_pay_update_coupon_' . $id);
$ok = false;
if ($id > 0 && class_exists('\AKSRT\Payment\Coupon')) {
$ok = \AKSRT\Payment\Coupon::get_instance()->update($id, [
if ($id > 0 && class_exists('\Jio\Payment\Coupon')) {
$ok = \Jio\Payment\Coupon::get_instance()->update($id, [
'type' => sanitize_key((string) wp_unslash($_POST['type'] ?? 'fixed')),
'value' => trim((string) wp_unslash($_POST['value'] ?? '0')),
'min_amount' => trim((string) wp_unslash($_POST['min_amount'] ?? '0')),
@@ -297,7 +297,7 @@ class PayCoupons {
]);
}
wp_safe_redirect($this->page_url([
'aksrt_pay_msg' => $ok ? 'coupon_updated' : 'coupon_update_failed',
'jio_pay_msg' => $ok ? 'coupon_updated' : 'coupon_update_failed',
'edit_id' => $id,
]));
exit;
@@ -305,23 +305,23 @@ class PayCoupons {
public function handle_disable_coupon(): void {
if (!current_user_can(self::CAPABILITY)) {
wp_die(esc_html__('权限不足', 'aksrt'));
wp_die(esc_html__('权限不足', 'jio'));
}
$id = isset($_POST['coupon_id']) ? (int) $_POST['coupon_id'] : 0;
check_admin_referer('aksrt_pay_disable_coupon_' . $id);
$ok = $id > 0 && class_exists('\AKSRT\Payment\Coupon') && \AKSRT\Payment\Coupon::get_instance()->disable($id);
wp_safe_redirect($this->page_url(['aksrt_pay_msg' => $ok ? 'coupon_disabled' : 'coupon_failed']));
check_admin_referer('jio_pay_disable_coupon_' . $id);
$ok = $id > 0 && class_exists('\Jio\Payment\Coupon') && \Jio\Payment\Coupon::get_instance()->disable($id);
wp_safe_redirect($this->page_url(['jio_pay_msg' => $ok ? 'coupon_disabled' : 'coupon_failed']));
exit;
}
public function handle_enable_coupon(): void {
if (!current_user_can(self::CAPABILITY)) {
wp_die(esc_html__('权限不足', 'aksrt'));
wp_die(esc_html__('权限不足', 'jio'));
}
$id = isset($_POST['coupon_id']) ? (int) $_POST['coupon_id'] : 0;
check_admin_referer('aksrt_pay_enable_coupon_' . $id);
$ok = $id > 0 && class_exists('\AKSRT\Payment\Coupon') && \AKSRT\Payment\Coupon::get_instance()->enable($id);
wp_safe_redirect($this->page_url(['aksrt_pay_msg' => $ok ? 'coupon_enabled' : 'coupon_failed']));
check_admin_referer('jio_pay_enable_coupon_' . $id);
$ok = $id > 0 && class_exists('\Jio\Payment\Coupon') && \Jio\Payment\Coupon::get_instance()->enable($id);
wp_safe_redirect($this->page_url(['jio_pay_msg' => $ok ? 'coupon_enabled' : 'coupon_failed']));
exit;
}
@@ -370,15 +370,15 @@ class PayCoupons {
$scope = is_array($decoded) ? $decoded : [];
}
if (!is_array($scope) || empty($scope)) {
return __('全部', 'aksrt');
return __('全部', 'jio');
}
$type = sanitize_key((string) ($scope['type'] ?? ''));
$ids = array_map('intval', (array) ($scope['ids'] ?? []));
$id_text = $ids ? implode(',', $ids) : '—';
$map = [
'include' => __('含商品', 'aksrt'),
'exclude' => __('排除商品', 'aksrt'),
'category' => __('分类', 'aksrt'),
'include' => __('含商品', 'jio'),
'exclude' => __('排除商品', 'jio'),
'category' => __('分类', 'jio'),
];
$label = $map[$type] ?? $type;
return $label . ':' . $id_text;
@@ -386,25 +386,25 @@ class PayCoupons {
private function ensure_cap(): void {
if (!current_user_can(self::CAPABILITY)) {
wp_die(esc_html__('您没有权限访问此页面。', 'aksrt'));
wp_die(esc_html__('您没有权限访问此页面。', 'jio'));
}
}
private function render_flash(): void {
if (empty($_GET['aksrt_pay_msg'])) {
if (empty($_GET['jio_pay_msg'])) {
return;
}
$code = sanitize_key((string) wp_unslash($_GET['aksrt_pay_msg']));
$code = sanitize_key((string) wp_unslash($_GET['jio_pay_msg']));
$map = [
'coupon_created' => __('优惠码已创建。', 'aksrt'),
'coupon_create_failed' => __('优惠码创建失败(可能重复)。', 'aksrt'),
'coupon_batch_ok' => __('优惠码批量生成完成。', 'aksrt'),
'coupon_batch_failed' => __('优惠码批量生成失败。', 'aksrt'),
'coupon_updated' => __('优惠码已更新。', 'aksrt'),
'coupon_update_failed' => __('优惠码更新失败。', 'aksrt'),
'coupon_disabled' => __('优惠码已禁用。', 'aksrt'),
'coupon_enabled' => __('优惠码已启用。', 'aksrt'),
'coupon_failed' => __('优惠码操作失败。', 'aksrt'),
'coupon_created' => __('优惠码已创建。', 'jio'),
'coupon_create_failed' => __('优惠码创建失败(可能重复)。', 'jio'),
'coupon_batch_ok' => __('优惠码批量生成完成。', 'jio'),
'coupon_batch_failed' => __('优惠码批量生成失败。', 'jio'),
'coupon_updated' => __('优惠码已更新。', 'jio'),
'coupon_update_failed' => __('优惠码更新失败。', 'jio'),
'coupon_disabled' => __('优惠码已禁用。', 'jio'),
'coupon_enabled' => __('优惠码已启用。', 'jio'),
'coupon_failed' => __('优惠码操作失败。', 'jio'),
];
if (!isset($map[$code])) {
return;
+63 -63
View File
@@ -3,13 +3,13 @@
* 优惠码:新建 / 批量生成弹窗与编辑表单。
*
* 作为 flat sibling trait 被 PayCoupons 使用(PSR-4:trait 必须与宿主类同目录)。
* 两个弹窗由页头按钮(data-aksrt-dialog-open)打开,交互在 admin-coupons.js;
* 两个弹窗由页头按钮(data-jio-dialog-open)打开,交互在 admin-coupons.js;
* 三张表单共用 render_rule_fields() 输出的优惠规则字段。
*
* @package AKSRT
* @package Jio
*/
namespace AKSRT\Admin\Native;
namespace Jio\Admin\Native;
defined('ABSPATH') || exit;
@@ -18,15 +18,15 @@ trait PayCouponsForms {
private function render_create_dialog(): void {
$this->dialog_open(
self::CREATE_DIALOG,
__('新建优惠码', 'aksrt'),
__('手动指定一个优惠码,用户结算时输入即可抵扣。', 'aksrt'),
'aksrt_pay_create_coupon'
__('新建优惠码', 'jio'),
__('手动指定一个优惠码,用户结算时输入即可抵扣。', 'jio'),
'jio_pay_create_coupon'
);
echo '<div class="aksrt-native-form-grid">';
echo '<div class="jio-native-form-grid">';
$this->field(
__('代码', 'aksrt'),
'<input type="text" name="code" maxlength="60" required autofocus autocomplete="off" spellcheck="false" class="aksrt-native-mono">',
__('需全站唯一,最多 60 个字符。', 'aksrt'),
__('代码', 'jio'),
'<input type="text" name="code" maxlength="60" required autofocus autocomplete="off" spellcheck="false" class="jio-native-mono">',
__('需全站唯一,最多 60 个字符。', 'jio'),
true
);
$this->render_rule_fields([
@@ -38,24 +38,24 @@ trait PayCouponsForms {
'per_user_limit' => '0',
]);
echo '</div>';
$this->dialog_close(__('创建优惠码', 'aksrt'));
$this->dialog_close(__('创建优惠码', 'jio'));
}
private function render_batch_dialog(): void {
$this->dialog_open(
self::BATCH_DIALOG,
__('批量生成优惠码', 'aksrt'),
__('按相同规则随机生成一批优惠码,生成后会在页面上列出供复制。', 'aksrt'),
'aksrt_pay_batch_coupons'
__('批量生成优惠码', 'jio'),
__('按相同规则随机生成一批优惠码,生成后会在页面上列出供复制。', 'jio'),
'jio_pay_batch_coupons'
);
echo '<h3 class="aksrt-coupon-section">' . esc_html__('生成规则', 'aksrt') . '</h3>';
echo '<div class="aksrt-native-form-grid aksrt-coupon-batch-grid">';
$this->field(__('数量', 'aksrt'), '<input type="number" name="count" min="1" max="100" step="1" value="10" required autofocus>', __('单次 1–100 个。', 'aksrt'));
$this->field(__('随机位数', 'aksrt'), '<input type="number" name="length" min="6" max="24" step="1" value="10" required>', __('6–24 位,不含前缀。', 'aksrt'));
$this->field(__('前缀', 'aksrt'), '<input type="text" name="prefix" maxlength="36" autocomplete="off" spellcheck="false" class="aksrt-native-mono">', __('选填,如 VIP2026-。', 'aksrt'));
echo '<h3 class="jio-coupon-section">' . esc_html__('生成规则', 'jio') . '</h3>';
echo '<div class="jio-native-form-grid jio-coupon-batch-grid">';
$this->field(__('数量', 'jio'), '<input type="number" name="count" min="1" max="100" step="1" value="10" required autofocus>', __('单次 1–100 个。', 'jio'));
$this->field(__('随机位数', 'jio'), '<input type="number" name="length" min="6" max="24" step="1" value="10" required>', __('6–24 位,不含前缀。', 'jio'));
$this->field(__('前缀', 'jio'), '<input type="text" name="prefix" maxlength="36" autocomplete="off" spellcheck="false" class="jio-native-mono">', __('选填,如 VIP2026-。', 'jio'));
echo '</div>';
echo '<h3 class="aksrt-coupon-section">' . esc_html__('优惠规则', 'aksrt') . '</h3>';
echo '<div class="aksrt-native-form-grid">';
echo '<h3 class="jio-coupon-section">' . esc_html__('优惠规则', 'jio') . '</h3>';
echo '<div class="jio-native-form-grid">';
$this->render_rule_fields([
'type' => 'fixed',
'value' => '10',
@@ -65,7 +65,7 @@ trait PayCouponsForms {
'per_user_limit' => '1',
]);
echo '</div>';
$this->dialog_close(__('批量生成', 'aksrt'));
$this->dialog_close(__('批量生成', 'jio'));
}
/**
@@ -78,18 +78,18 @@ trait PayCouponsForms {
$title = sprintf(
/* translators: %d coupon id */
__('编辑优惠码 #%d', 'aksrt'),
__('编辑优惠码 #%d', 'jio'),
$id
);
$back = NativeUi::button_link(__('返回列表', 'aksrt'), $this->page_url(), false, 'dashicons-arrow-left-alt');
$back = NativeUi::button_link(__('返回列表', 'jio'), $this->page_url(), false, 'dashicons-arrow-left-alt');
NativeUi::card_open($title, $back);
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" data-aksrt-coupon-form data-busy-label="' . esc_attr__('正在保存…', 'aksrt') . '">';
echo '<input type="hidden" name="action" value="aksrt_pay_update_coupon" />';
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" data-jio-coupon-form data-busy-label="' . esc_attr__('正在保存…', 'jio') . '">';
echo '<input type="hidden" name="action" value="jio_pay_update_coupon" />';
echo '<input type="hidden" name="coupon_id" value="' . esc_attr((string) $id) . '" />';
wp_nonce_field('aksrt_pay_update_coupon_' . $id);
echo '<div class="aksrt-native-form-grid">';
$this->field(__('代码', 'aksrt'), '<input type="text" value="' . esc_attr((string) ($row['code'] ?? '')) . '" readonly class="aksrt-native-mono" />', __('代码创建后不可修改。', 'aksrt'));
wp_nonce_field('jio_pay_update_coupon_' . $id);
echo '<div class="jio-native-form-grid">';
$this->field(__('代码', 'jio'), '<input type="text" value="' . esc_attr((string) ($row['code'] ?? '')) . '" readonly class="jio-native-mono" />', __('代码创建后不可修改。', 'jio'));
$this->render_rule_fields([
'type' => (string) ($row['type'] ?? 'fixed'),
'value' => (string) ($row['value'] ?? ''),
@@ -102,8 +102,8 @@ trait PayCouponsForms {
'expires_at' => $expires !== '' ? str_replace(' ', 'T', substr($expires, 0, 16)) : '',
]);
echo '</div>';
echo '<div class="aksrt-native-form-foot">';
echo '<button type="submit" class="button button-primary" data-aksrt-coupon-submit>' . esc_html__('保存修改', 'aksrt') . '</button>';
echo '<div class="jio-native-form-foot">';
echo '<button type="submit" class="button button-primary" data-jio-coupon-submit>' . esc_html__('保存修改', 'jio') . '</button>';
echo '</div>';
echo '</form>';
NativeUi::card_close();
@@ -119,39 +119,39 @@ trait PayCouponsForms {
$percent = $type === 'percent';
$currency = $this->currency_symbol();
$this->field(__('类型', 'aksrt'), $this->type_select($type));
$this->field(__('类型', 'jio'), $this->type_select($type));
$this->field(
__('面值', 'aksrt'),
'<span class="aksrt-coupon-inline"><input type="number" name="value" min="0" step="' . ($percent ? '1' : '0.01') . '"' . ($percent ? ' max="100"' : '')
__('面值', 'jio'),
'<span class="jio-coupon-inline"><input type="number" name="value" min="0" step="' . ($percent ? '1' : '0.01') . '"' . ($percent ? ' max="100"' : '')
. ' value="' . esc_attr($v['value'] ?? '') . '" required>'
. '<span class="aksrt-coupon-unit" data-aksrt-coupon-unit data-fixed="' . esc_attr($currency) . '" data-percent="%">' . esc_html($percent ? '%' : $currency) . '</span></span>',
__('固定金额直接抵扣;折扣百分比填 20 表示减免 20%(0–100 的整数)。', 'aksrt')
. '<span class="jio-coupon-unit" data-jio-coupon-unit data-fixed="' . esc_attr($currency) . '" data-percent="%">' . esc_html($percent ? '%' : $currency) . '</span></span>',
__('固定金额直接抵扣;折扣百分比填 20 表示减免 20%(0–100 的整数)。', 'jio')
);
$this->field(__('最低消费', 'aksrt'), $this->money_input('min_amount', $v['min_amount'] ?? '0'), __('订单金额达到该值才可使用,0 表示无门槛。', 'aksrt'));
$this->field(__('最高优惠', 'aksrt'), $this->money_input('max_discount', $v['max_discount'] ?? '0'), __('折扣百分比的封顶金额,0 表示不封顶。', 'aksrt'));
$this->field(__('总次数', 'aksrt'), '<input type="number" name="usage_limit" min="0" step="1" value="' . esc_attr($v['usage_limit'] ?? '0') . '">', __('所有用户合计可用次数,0 表示不限。', 'aksrt'));
$this->field(__('每人限次', 'aksrt'), '<input type="number" name="per_user_limit" min="0" step="1" value="' . esc_attr($v['per_user_limit'] ?? '0') . '">', __('单个用户可用次数,0 表示不限。', 'aksrt'));
$this->field(__('最低消费', 'jio'), $this->money_input('min_amount', $v['min_amount'] ?? '0'), __('订单金额达到该值才可使用,0 表示无门槛。', 'jio'));
$this->field(__('最高优惠', 'jio'), $this->money_input('max_discount', $v['max_discount'] ?? '0'), __('折扣百分比的封顶金额,0 表示不封顶。', 'jio'));
$this->field(__('总次数', 'jio'), '<input type="number" name="usage_limit" min="0" step="1" value="' . esc_attr($v['usage_limit'] ?? '0') . '">', __('所有用户合计可用次数,0 表示不限。', 'jio'));
$this->field(__('每人限次', 'jio'), '<input type="number" name="per_user_limit" min="0" step="1" value="' . esc_attr($v['per_user_limit'] ?? '0') . '">', __('单个用户可用次数,0 表示不限。', 'jio'));
$select = '<select name="scope_type">';
foreach ([
'' => __('全部商品', 'aksrt'),
'include' => __('仅指定商品 ID', 'aksrt'),
'exclude' => __('排除商品 ID', 'aksrt'),
'category' => __('指定分类 ID', 'aksrt'),
'' => __('全部商品', 'jio'),
'include' => __('仅指定商品 ID', 'jio'),
'exclude' => __('排除商品 ID', 'jio'),
'category' => __('指定分类 ID', 'jio'),
] as $k => $label) {
$select .= sprintf('<option value="%s"%s>%s</option>', esc_attr($k), selected($v['scope_type'] ?? '', $k, false), esc_html($label));
}
$select .= '</select>';
$this->field(__('适用范围', 'aksrt'), $select);
$this->field(__('适用范围', 'jio'), $select);
$this->field(
__('范围 ID', 'aksrt'),
'<input type="text" name="scope_ids" value="' . esc_attr($v['scope_ids'] ?? '') . '" placeholder="' . esc_attr__('如 12,34', 'aksrt') . '" autocomplete="off">',
__('逗号分隔;选择「全部商品」时无需填写。', 'aksrt')
__('范围 ID', 'jio'),
'<input type="text" name="scope_ids" value="' . esc_attr($v['scope_ids'] ?? '') . '" placeholder="' . esc_attr__('如 12,34', 'jio') . '" autocomplete="off">',
__('逗号分隔;选择「全部商品」时无需填写。', 'jio')
);
$this->field(
__('过期时间', 'aksrt'),
__('过期时间', 'jio'),
'<input type="datetime-local" name="expires_at" value="' . esc_attr($v['expires_at'] ?? '') . '">',
__('留空表示永不过期。', 'aksrt')
__('留空表示永不过期。', 'jio')
);
}
@@ -161,40 +161,40 @@ trait PayCouponsForms {
private function type_select(string $current): string {
$html = '<select name="type">';
foreach (['fixed' => __('固定金额', 'aksrt'), 'percent' => __('折扣百分比', 'aksrt')] as $k => $label) {
foreach (['fixed' => __('固定金额', 'jio'), 'percent' => __('折扣百分比', 'jio')] as $k => $label) {
$html .= sprintf('<option value="%s"%s>%s</option>', esc_attr($k), selected($current, $k, false), esc_html($label));
}
return $html . '</select>';
}
/** 单字段单元(.aksrt-native-field);$html 为调用方已转义的控件片段。 */
/** 单字段单元(.jio-native-field);$html 为调用方已转义的控件片段。 */
private function field(string $label, string $html, string $hint = '', bool $wide = false): void {
echo '<label class="aksrt-native-field' . ($wide ? ' is-wide' : '') . '">';
echo '<span class="aksrt-native-field__label">' . esc_html($label) . '</span>';
echo '<label class="jio-native-field' . ($wide ? ' is-wide' : '') . '">';
echo '<span class="jio-native-field__label">' . esc_html($label) . '</span>';
echo $html; // phpcs:ignore WordPress.Security.EscapeOutput -- 调用方已转义。
if ($hint !== '') {
echo '<small class="aksrt-native-field__hint">' . esc_html($hint) . '</small>';
echo '<small class="jio-native-field__hint">' . esc_html($hint) . '</small>';
}
echo '</label>';
}
private function dialog_open(string $id, string $title, string $description, string $action): void {
echo '<dialog class="aksrt-coupon-dialog" id="' . esc_attr($id) . '" aria-labelledby="' . esc_attr($id) . '-title" aria-describedby="' . esc_attr($id) . '-description">';
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" class="aksrt-coupon-dialog__frame" data-aksrt-coupon-form data-busy-label="' . esc_attr__('正在提交…', 'aksrt') . '">';
echo '<dialog class="jio-coupon-dialog" id="' . esc_attr($id) . '" aria-labelledby="' . esc_attr($id) . '-title" aria-describedby="' . esc_attr($id) . '-description">';
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" class="jio-coupon-dialog__frame" data-jio-coupon-form data-busy-label="' . esc_attr__('正在提交…', 'jio') . '">';
echo '<input type="hidden" name="action" value="' . esc_attr($action) . '">';
wp_nonce_field($action);
echo '<header class="aksrt-coupon-dialog__head"><div><h2 id="' . esc_attr($id) . '-title">' . esc_html($title) . '</h2><p id="' . esc_attr($id) . '-description">' . esc_html($description) . '</p></div>';
echo '<button type="button" class="aksrt-coupon-dialog__close" data-aksrt-dialog-close aria-label="' . esc_attr__('关闭弹窗', 'aksrt') . '"><span class="dashicons dashicons-no-alt" aria-hidden="true"></span></button></header>';
echo '<div class="aksrt-coupon-dialog__body">';
echo '<header class="jio-coupon-dialog__head"><div><h2 id="' . esc_attr($id) . '-title">' . esc_html($title) . '</h2><p id="' . esc_attr($id) . '-description">' . esc_html($description) . '</p></div>';
echo '<button type="button" class="jio-coupon-dialog__close" data-jio-dialog-close aria-label="' . esc_attr__('关闭弹窗', 'jio') . '"><span class="dashicons dashicons-no-alt" aria-hidden="true"></span></button></header>';
echo '<div class="jio-coupon-dialog__body">';
}
private function dialog_close(string $submit_label): void {
echo '</div><footer class="aksrt-coupon-dialog__foot"><button type="button" class="button" data-aksrt-dialog-close>' . esc_html__('取消', 'aksrt') . '</button>';
echo '<button type="submit" class="button button-primary" data-aksrt-coupon-submit>' . esc_html($submit_label) . '</button></footer></form></dialog>';
echo '</div><footer class="jio-coupon-dialog__foot"><button type="button" class="button" data-jio-dialog-close>' . esc_html__('取消', 'jio') . '</button>';
echo '<button type="submit" class="button button-primary" data-jio-coupon-submit>' . esc_html($submit_label) . '</button></footer></form></dialog>';
}
private function currency_symbol(): string {
$paid = \AKSRT\Settings\Settings::get_paid_content_settings();
$paid = \Jio\Settings\Settings::get_paid_content_settings();
return (string) ($paid['display']['currency_symbol'] ?? '¥');
}
}
+44 -44
View File
@@ -6,10 +6,10 @@
* admin-post 处理器(结算 / 删除 / 批量操作)与共用的展示助手。页面样式在 assets/css/admin/income.css,
* 批量操作、二次确认、全选与防重复提交在 assets/js/admin-income.js。
*
* @package AKSRT
* @package Jio
*/
namespace AKSRT\Admin\Native;
namespace Jio\Admin\Native;
defined('ABSPATH') || exit;
@@ -18,7 +18,7 @@ class PayIncome {
use PayIncomeList;
public const CAPABILITY = 'manage_options';
public const PAGE_SLUG = 'aksrt_income';
public const PAGE_SLUG = 'jio_income';
private static $instance = null;
@@ -34,9 +34,9 @@ class PayIncome {
return;
}
add_action('admin_enqueue_scripts', [$this, 'enqueue_assets']);
add_action('admin_post_aksrt_pay_settle_income', [$this, 'handle_settle_income']);
add_action('admin_post_aksrt_pay_delete_income', [$this, 'handle_delete_income']);
add_action('admin_post_aksrt_pay_bulk_income', [$this, 'handle_bulk_income']);
add_action('admin_post_jio_pay_settle_income', [$this, 'handle_settle_income']);
add_action('admin_post_jio_pay_delete_income', [$this, 'handle_delete_income']);
add_action('admin_post_jio_pay_bulk_income', [$this, 'handle_bulk_income']);
}
public function enqueue_assets(): void {
@@ -46,17 +46,17 @@ class PayIncome {
}
$css = '/assets/css/admin/income.css';
$js = '/assets/js/admin-income.js';
wp_enqueue_style('aksrt-admin-income', AKSRT_URI . $css, [NativeUi::STYLE_HANDLE], (string) filemtime(AKSRT_DIR . $css));
wp_enqueue_script('aksrt-admin-income', AKSRT_URI . $js, [], (string) filemtime(AKSRT_DIR . $js), true);
wp_enqueue_style('jio-admin-income', JIO_URI . $css, [NativeUi::STYLE_HANDLE], (string) filemtime(JIO_DIR . $css));
wp_enqueue_script('jio-admin-income', JIO_URI . $js, [], (string) filemtime(JIO_DIR . $js), true);
}
public function render_income_page(): void {
$this->ensure_cap();
NativeUi::page_open('aksrt-income-page');
NativeUi::page_open('jio-income-page');
if (!class_exists('\AKSRT\Payment\Revenue')) {
NativeUi::header(__('创作分成', 'aksrt'), '', 'dashicons-chart-pie');
echo '<div class="notice notice-error"><p>' . esc_html__('分成模块未初始化。', 'aksrt') . '</p></div>';
if (!class_exists('\Jio\Payment\Revenue')) {
NativeUi::header(__('创作分成', 'jio'), '', 'dashicons-chart-pie');
echo '<div class="notice notice-error"><p>' . esc_html__('分成模块未初始化。', 'jio') . '</p></div>';
NativeUi::page_close();
return;
}
@@ -72,28 +72,28 @@ class PayIncome {
public function handle_settle_income(): void {
$this->ensure_cap();
$id = isset($_POST['revenue_id']) ? (int) $_POST['revenue_id'] : 0;
check_admin_referer('aksrt_pay_settle_income_' . $id);
check_admin_referer('jio_pay_settle_income_' . $id);
$operation_key = $this->request_operation_key('admin-revenue-settle');
$ok = $id > 0 && $operation_key !== ''
&& \AKSRT\Payment\Revenue::get_instance()->settle_result($id, get_current_user_id(), $operation_key) instanceof \AKSRT\Ledger\LedgerResult;
$this->redirect_back(['aksrt_pay_msg' => $ok ? 'income_settled' : 'income_failed']);
&& \Jio\Payment\Revenue::get_instance()->settle_result($id, get_current_user_id(), $operation_key) instanceof \Jio\Ledger\LedgerResult;
$this->redirect_back(['jio_pay_msg' => $ok ? 'income_settled' : 'income_failed']);
}
/** 单条删除:只删待结算记录,已结算记录由 Revenue::delete_pending() 的状态条件挡住。 */
public function handle_delete_income(): void {
$this->ensure_cap();
$id = isset($_POST['revenue_id']) ? (int) $_POST['revenue_id'] : 0;
check_admin_referer('aksrt_pay_delete_income_' . $id);
check_admin_referer('jio_pay_delete_income_' . $id);
$deleted = $id > 0 ? \AKSRT\Payment\Revenue::get_instance()->delete_pending([$id]) : 0;
$this->redirect_back(['aksrt_pay_msg' => $deleted > 0 ? 'income_deleted' : 'income_delete_failed', 'done' => $deleted]);
$deleted = $id > 0 ? \Jio\Payment\Revenue::get_instance()->delete_pending([$id]) : 0;
$this->redirect_back(['jio_pay_msg' => $deleted > 0 ? 'income_deleted' : 'income_delete_failed', 'done' => $deleted]);
}
/** 批量操作:bulk_action = settle | delete,作用于勾选的记录。 */
public function handle_bulk_income(): void {
$this->ensure_cap();
check_admin_referer('aksrt_pay_bulk_income');
check_admin_referer('jio_pay_bulk_income');
$action = isset($_POST['bulk_action']) ? sanitize_key((string) wp_unslash($_POST['bulk_action'])) : '';
$raw_ids = isset($_POST['revenue_ids']) ? wp_unslash($_POST['revenue_ids']) : [];
@@ -101,17 +101,17 @@ class PayIncome {
? array_values(array_unique(array_filter(array_map('intval', $raw_ids), static fn (int $id): bool => $id > 0)))
: [];
if (!in_array($action, ['settle', 'delete'], true)) {
$this->redirect_back(['aksrt_pay_msg' => 'income_bulk_invalid']);
$this->redirect_back(['jio_pay_msg' => 'income_bulk_invalid']);
}
if ($ids === []) {
$this->redirect_back(['aksrt_pay_msg' => 'income_none_selected']);
$this->redirect_back(['jio_pay_msg' => 'income_none_selected']);
}
if ($action === 'delete') {
$deleted = \AKSRT\Payment\Revenue::get_instance()->delete_pending($ids);
$deleted = \Jio\Payment\Revenue::get_instance()->delete_pending($ids);
$failed = count($ids) - $deleted;
$this->redirect_back([
'aksrt_pay_msg' => $deleted === 0 ? 'income_delete_failed' : ($failed > 0 ? 'income_delete_partial' : 'income_deleted'),
'jio_pay_msg' => $deleted === 0 ? 'income_delete_failed' : ($failed > 0 ? 'income_delete_partial' : 'income_deleted'),
'done' => $deleted,
'failed' => $failed,
]);
@@ -119,13 +119,13 @@ class PayIncome {
$operation_key = $this->request_operation_key('admin-revenue-settle-batch');
$result = $operation_key !== ''
? \AKSRT\Payment\Revenue::get_instance()->settle_batch($ids, get_current_user_id(), $operation_key)
? \Jio\Payment\Revenue::get_instance()->settle_batch($ids, get_current_user_id(), $operation_key)
: ['settled' => [], 'failed' => $ids];
$settled = count($result['settled']);
$failed = count($result['failed']);
$this->redirect_back([
'aksrt_pay_msg' => $settled === 0 ? 'income_failed' : ($failed > 0 ? 'income_batch_partial' : 'income_batch_settled'),
'jio_pay_msg' => $settled === 0 ? 'income_failed' : ($failed > 0 ? 'income_batch_partial' : 'income_batch_settled'),
'done' => $settled,
'failed' => $failed,
]);
@@ -135,7 +135,7 @@ class PayIncome {
private function request_operation_key(string $scope): string {
$request_key = sanitize_text_field((string) wp_unslash($_POST['operation_key'] ?? ''));
return wp_is_uuid($request_key)
? \AKSRT\Ledger\OperationKey::deterministic($scope, [get_current_user_id(), strtolower($request_key)])
? \Jio\Ledger\OperationKey::deterministic($scope, [get_current_user_id(), strtolower($request_key)])
: '';
}
@@ -150,7 +150,7 @@ class PayIncome {
if (is_string($referer) && $referer !== '') {
parse_str((string) wp_parse_url($referer, PHP_URL_QUERY), $parsed);
if (($parsed['page'] ?? '') === self::PAGE_SLUG) {
unset($parsed['page'], $parsed['aksrt_pay_msg'], $parsed['done'], $parsed['failed']);
unset($parsed['page'], $parsed['jio_pay_msg'], $parsed['done'], $parsed['failed']);
$query = $parsed;
}
}
@@ -164,7 +164,7 @@ class PayIncome {
/** @return array{enabled:bool,cycle_days:int} */
private function revenue_policy(): array {
$settings = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_payment_revenue_settings() : [];
$settings = class_exists('\Jio\Settings\Settings') ? \Jio\Settings\Settings::get_payment_revenue_settings() : [];
return [
'enabled' => !empty($settings['enabled']),
'cycle_days' => max(1, (int) ($settings['cycle_days'] ?? 30)),
@@ -174,8 +174,8 @@ class PayIncome {
/** @return array<string, string> */
private function status_labels(): array {
return [
'pending' => __('待结算', 'aksrt'),
'settled' => __('已结算', 'aksrt'),
'pending' => __('待结算', 'jio'),
'settled' => __('已结算', 'jio'),
];
}
@@ -191,51 +191,51 @@ class PayIncome {
/** 分成金额:现金带两位小数与 ¥,积分取整加单位;现金与积分从不相加。 */
private function format_amount($value, bool $is_points): string {
if ($is_points) {
return number_format_i18n(max(0, (int) $value)) . ' ' . __('积分', 'aksrt');
return number_format_i18n(max(0, (int) $value)) . ' ' . __('积分', 'jio');
}
try {
return '¥' . \AKSRT\Ledger\AssetAmount::balance(is_string($value) ? $value : '0')->formatted();
return '¥' . \Jio\Ledger\AssetAmount::balance(is_string($value) ? $value : '0')->formatted();
} catch (\InvalidArgumentException $e) {
return '¥0.00';
}
}
private function render_flash(): void {
if (empty($_GET['aksrt_pay_msg'])) {
if (empty($_GET['jio_pay_msg'])) {
return;
}
$code = sanitize_key((string) wp_unslash($_GET['aksrt_pay_msg']));
$code = sanitize_key((string) wp_unslash($_GET['jio_pay_msg']));
$done = max(0, (int) ($_GET['done'] ?? 0));
$failed = max(0, (int) ($_GET['failed'] ?? 0));
// 码 => [文案, notice 类型]
$map = [
'income_settled' => [__('分成已结算,已转入作者钱包。', 'aksrt'), 'success'],
'income_settled' => [__('分成已结算,已转入作者钱包。', 'jio'), 'success'],
'income_batch_settled' => [sprintf(
/* translators: %d settled count */
__('已结算 %d 条分成记录。', 'aksrt'),
__('已结算 %d 条分成记录。', 'jio'),
$done
), 'success'],
'income_batch_partial' => [sprintf(
/* translators: 1 settled count 2 failed count */
__('已结算 %1$d 条,%2$d 条失败(记录状态可能已变化),可刷新后重试。', 'aksrt'),
__('已结算 %1$d 条,%2$d 条失败(记录状态可能已变化),可刷新后重试。', 'jio'),
$done,
$failed
), 'warning'],
'income_none_selected' => [__('请先勾选待结算记录。', 'aksrt'), 'warning'],
'income_bulk_invalid' => [__('请先选择批量操作。', 'aksrt'), 'warning'],
'income_failed' => [__('分成结算失败:记录可能已被结算,或分成功能未启用。', 'aksrt'), 'error'],
'income_none_selected' => [__('请先勾选待结算记录。', 'jio'), 'warning'],
'income_bulk_invalid' => [__('请先选择批量操作。', 'jio'), 'warning'],
'income_failed' => [__('分成结算失败:记录可能已被结算,或分成功能未启用。', 'jio'), 'error'],
'income_deleted' => [sprintf(
/* translators: %d deleted count */
__('已删除 %d 条待结算记录。', 'aksrt'),
__('已删除 %d 条待结算记录。', 'jio'),
$done
), 'success'],
'income_delete_partial' => [sprintf(
/* translators: 1 deleted count 2 skipped count */
__('已删除 %1$d 条,%2$d 条未删除(仅待结算记录可删除,可能已被结算)。', 'aksrt'),
__('已删除 %1$d 条,%2$d 条未删除(仅待结算记录可删除,可能已被结算)。', 'jio'),
$done,
$failed
), 'warning'],
'income_delete_failed' => [__('删除失败:仅待结算记录可删除,记录可能已被结算或不存在。', 'aksrt'), 'error'],
'income_delete_failed' => [__('删除失败:仅待结算记录可删除,记录可能已被结算或不存在。', 'jio'), 'error'],
];
if (!isset($map[$code])) {
return;
@@ -246,7 +246,7 @@ class PayIncome {
private function ensure_cap(): void {
if (!current_user_can(self::CAPABILITY)) {
wp_die(esc_html__('您没有权限访问此页面。', 'aksrt'));
wp_die(esc_html__('您没有权限访问此页面。', 'jio'));
}
}
+100 -100
View File
@@ -4,12 +4,12 @@
*
* 作为 flat sibling trait 被 PayIncome 使用(PSR-4:trait 必须与宿主类同目录)。
*
* @package AKSRT
* @package Jio
*/
namespace AKSRT\Admin\Native;
namespace Jio\Admin\Native;
use AKSRT\Payment\Revenue;
use Jio\Payment\Revenue;
defined('ABSPATH') || exit;
@@ -17,12 +17,12 @@ trait PayIncomeList {
private function render_income_list_page(): void {
NativeUi::header(
__('创作分成', 'aksrt'),
__('作者从付费内容订单获得的分成记录。待结算记录到期后自动结算,也可在这里提前手动结算。', 'aksrt'),
__('创作分成', 'jio'),
__('作者从付费内容订单获得的分成记录。待结算记录到期后自动结算,也可在这里提前手动结算。', 'jio'),
'dashicons-chart-pie',
[[
'label' => __('分成配置', 'aksrt'),
'url' => admin_url('admin.php?page=aksrt-settings#/payment/revenue'),
'label' => __('分成配置', 'jio'),
'url' => admin_url('admin.php?page=jio-settings#/payment/revenue'),
'icon' => 'dashicons-admin-generic',
]]
);
@@ -65,20 +65,20 @@ trait PayIncomeList {
$has_pending = in_array('pending', array_column($items, 'status'), true);
// 复选框列与批量栏始终渲染;不可用时禁用并用 title 说明原因,而不是整块隐藏。
NativeUi::table_card_open(__('分成记录', 'aksrt'), $this->bulk_form_html($can_settle, $has_pending));
echo '<table class="wp-list-table widefat striped aksrt-income-table">';
echo '<caption class="screen-reader-text">' . esc_html__('分成记录', 'aksrt') . '</caption><thead><tr>';
NativeUi::table_card_open(__('分成记录', 'jio'), $this->bulk_form_html($can_settle, $has_pending));
echo '<table class="wp-list-table widefat striped jio-income-table">';
echo '<caption class="screen-reader-text">' . esc_html__('分成记录', 'jio') . '</caption><thead><tr>';
echo '<td class="manage-column column-cb check-column">'
. '<input type="checkbox" data-aksrt-income-check-all aria-label="' . esc_attr__('全选本页待结算记录', 'aksrt') . '"'
. ($has_pending ? '' : ' disabled title="' . esc_attr__('本页没有待结算记录', 'aksrt') . '"') . ' /></td>';
. '<input type="checkbox" data-jio-income-check-all aria-label="' . esc_attr__('全选本页待结算记录', 'jio') . '"'
. ($has_pending ? '' : ' disabled title="' . esc_attr__('本页没有待结算记录', 'jio') . '"') . ' /></td>';
foreach ([
'record' => __('记录 / 产生时间', 'aksrt'),
'author' => __('作者', 'aksrt'),
'source' => __('来源文章 / 订单', 'aksrt'),
'base' => __('订单金额 × 比例', 'aksrt'),
'amount' => __('分成', 'aksrt'),
'status' => __('状态', 'aksrt'),
'actions' => __('操作', 'aksrt'),
'record' => __('记录 / 产生时间', 'jio'),
'author' => __('作者', 'jio'),
'source' => __('来源文章 / 订单', 'jio'),
'base' => __('订单金额 × 比例', 'jio'),
'amount' => __('分成', 'jio'),
'status' => __('状态', 'jio'),
'actions' => __('操作', 'jio'),
] as $key => $label) {
echo '<th scope="col" class="column-' . esc_attr($key) . '">' . esc_html($label) . '</th>';
}
@@ -86,9 +86,9 @@ trait PayIncomeList {
if (!$items) {
$filtered = array_diff_key($query_args, ['status' => true]) !== [];
echo NativeUi::empty_row(8, __('暂无分成记录', 'aksrt'), $filtered // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
? __('没有符合当前筛选条件的记录,请调整或重置筛选。', 'aksrt')
: __('付费内容订单支付成功后,作者分成会出现在这里。', 'aksrt'));
echo NativeUi::empty_row(8, __('暂无分成记录', 'jio'), $filtered // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
? __('没有符合当前筛选条件的记录,请调整或重置筛选。', 'jio')
: __('付费内容订单支付成功后,作者分成会出现在这里。', 'jio'));
}
foreach ($items as $row) {
$this->render_income_row($row, $can_settle, $policy['cycle_days']);
@@ -112,47 +112,47 @@ trait PayIncomeList {
echo '<tr>';
// 已结算记录已入账到钱包,不可结算也不可删除:复选框禁用占位,保持列对齐。
echo '<th scope="row" class="check-column">'
. '<input type="checkbox" form="aksrt-income-bulk" name="revenue_ids[]" value="' . esc_attr((string) $id) . '"'
. ($pending ? ' data-aksrt-income-check' : ' disabled title="' . esc_attr__('已结算记录不可操作', 'aksrt') . '"')
. ' aria-label="' . esc_attr(sprintf(__('选择分成记录 #%d', 'aksrt'), $id)) . '" /></th>';
. '<input type="checkbox" form="jio-income-bulk" name="revenue_ids[]" value="' . esc_attr((string) $id) . '"'
. ($pending ? ' data-jio-income-check' : ' disabled title="' . esc_attr__('已结算记录不可操作', 'jio') . '"')
. ' aria-label="' . esc_attr(sprintf(__('选择分成记录 #%d', 'jio'), $id)) . '" /></th>';
echo '<td class="column-record"><span class="aksrt-income-id">#' . esc_html((string) $id) . '</span>'
. '<div class="aksrt-income-sub">' . esc_html((string) ($row['created_at'] ?? '')) . '</div></td>';
echo '<td class="column-record"><span class="jio-income-id">#' . esc_html((string) $id) . '</span>'
. '<div class="jio-income-sub">' . esc_html((string) ($row['created_at'] ?? '')) . '</div></td>';
echo '<td class="column-author">' . NativeUi::user_cell((int) ($row['author_id'] ?? 0)) . '</td>';
$title = $post_id > 0 ? (string) get_the_title($post_id) : '';
$post_html = $post_id > 0
? '<a class="aksrt-income-post" href="' . esc_url((string) get_edit_post_link($post_id)) . '">' . esc_html($title !== '' ? $title : '#' . $post_id) . '</a>'
: '<span class="aksrt-native-muted">—</span>';
? '<a class="jio-income-post" href="' . esc_url((string) get_edit_post_link($post_id)) . '">' . esc_html($title !== '' ? $title : '#' . $post_id) . '</a>'
: '<span class="jio-native-muted">—</span>';
$buyer = $buyer_id > 0 ? get_userdata($buyer_id) : false;
$order_url = admin_url('admin.php?page=aksrt_orders&order=' . rawurlencode($order_number));
$order_url = admin_url('admin.php?page=jio_orders&order=' . rawurlencode($order_number));
echo '<td class="column-source">' . $post_html // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped above.
. '<div class="aksrt-income-sub"><a class="aksrt-income-order" href="' . esc_url($order_url) . '">' . esc_html($order_number) . '</a>'
. ($buyer ? ' · ' . esc_html(sprintf(__('买家 %s', 'aksrt'), $buyer->display_name)) : '')
. '<div class="jio-income-sub"><a class="jio-income-order" href="' . esc_url($order_url) . '">' . esc_html($order_number) . '</a>'
. ($buyer ? ' · ' . esc_html(sprintf(__('买家 %s', 'jio'), $buyer->display_name)) : '')
. '</div></td>';
echo '<td class="column-base">' . esc_html($this->format_amount($row['original_amount'] ?? 0, $is_points))
. '<div class="aksrt-income-sub">' . esc_html(sprintf(
. '<div class="jio-income-sub">' . esc_html(sprintf(
/* translators: 1 share ratio 2 author level label */
__('比例 %1$d%% · %2$s', 'aksrt'),
__('比例 %1$d%% · %2$s', 'jio'),
(int) ($row['share_ratio'] ?? 0),
$this->author_level_label((int) ($row['author_level'] ?? 0))
)) . '</div></td>';
echo '<td class="column-amount"><strong class="aksrt-native-amount' . ($pending ? '' : ' is-positive') . '">'
echo '<td class="column-amount"><strong class="jio-native-amount' . ($pending ? '' : ' is-positive') . '">'
. esc_html($this->format_amount($row['amount'] ?? 0, $is_points)) . '</strong>'
. '<div class="aksrt-income-sub">' . esc_html($is_points ? __('积分钱包', 'aksrt') : __('余额钱包', 'aksrt')) . '</div></td>';
. '<div class="jio-income-sub">' . esc_html($is_points ? __('积分钱包', 'jio') : __('余额钱包', 'jio')) . '</div></td>';
echo '<td class="column-status">' . NativeUi::status_badge($status, $this->status_badge_map())
. $this->status_hint($row, $cycle_days) . '</td>';
echo '<td class="column-actions">';
if ($pending) {
echo '<div class="aksrt-income-actions">'
echo '<div class="jio-income-actions">'
. ($can_settle ? $this->settle_form_html($id) : '')
. $this->delete_form_html($id)
. '</div>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
} else {
echo '<span class="aksrt-native-muted">—</span>';
echo '<span class="jio-native-muted">—</span>';
}
echo '</td></tr>';
}
@@ -161,7 +161,7 @@ trait PayIncomeList {
private function status_hint(array $row, int $cycle_days): string {
if (($row['status'] ?? '') === 'settled') {
$at = (string) ($row['settled_at'] ?? '');
return $at !== '' ? '<div class="aksrt-income-sub">' . esc_html(sprintf(__('%s 结算', 'aksrt'), $at)) . '</div>' : '';
return $at !== '' ? '<div class="jio-income-sub">' . esc_html(sprintf(__('%s 结算', 'jio'), $at)) . '</div>' : '';
}
$created = (string) ($row['created_at'] ?? '');
if ($created === '') {
@@ -173,29 +173,29 @@ trait PayIncomeList {
return '';
}
$text = $due <= new \DateTimeImmutable('now', wp_timezone())
? __('已到期,等待自动结算', 'aksrt')
? __('已到期,等待自动结算', 'jio')
/* translators: %s expected settle time */
: sprintf(__('预计 %s 自动结算', 'aksrt'), $due->format('m-d H:i'));
return '<div class="aksrt-income-sub">' . esc_html($text) . '</div>';
: sprintf(__('预计 %s 自动结算', 'jio'), $due->format('m-d H:i'));
return '<div class="jio-income-sub">' . esc_html($text) . '</div>';
}
private function author_level_label(int $level): string {
return $level > 0
/* translators: %d VIP level */
? sprintf(__('VIP%d 作者', 'aksrt'), $level)
: __('普通作者', 'aksrt');
? sprintf(__('VIP%d 作者', 'jio'), $level)
: __('普通作者', 'jio');
}
/** @param array{enabled:bool,cycle_days:int} $policy */
private function render_policy_callout(array $policy): void {
if (!$policy['enabled']) {
echo '<div class="aksrt-native-callout is-warning">' . esc_html__('创作分成功能未启用:不会产生新的分成记录,已有的待结算记录也无法结算。', 'aksrt')
. ' <a href="' . esc_url(admin_url('admin.php?page=aksrt-settings#/payment/revenue')) . '">' . esc_html__('前往启用', 'aksrt') . '</a></div>';
echo '<div class="jio-native-callout is-warning">' . esc_html__('创作分成功能未启用:不会产生新的分成记录,已有的待结算记录也无法结算。', 'jio')
. ' <a href="' . esc_url(admin_url('admin.php?page=jio-settings#/payment/revenue')) . '">' . esc_html__('前往启用', 'jio') . '</a></div>';
return;
}
echo '<div class="aksrt-native-callout">' . esc_html(sprintf(
echo '<div class="jio-native-callout">' . esc_html(sprintf(
/* translators: %d settlement cycle days */
__('结算周期 %d 天:分成产生满周期后由系统每小时自动结算到作者钱包。手动结算会立即入账,不可撤销。', 'aksrt'),
__('结算周期 %d 天:分成产生满周期后由系统每小时自动结算到作者钱包。手动结算会立即入账,不可撤销。', 'jio'),
$policy['cycle_days']
)) . '</div>';
}
@@ -206,22 +206,22 @@ trait PayIncomeList {
$points = (array) ($stats['points'] ?? []);
$scope = $user_id > 0
/* translators: %d user ID */
? sprintf(__('用户 #%d', 'aksrt'), $user_id)
: __('全站', 'aksrt');
? sprintf(__('用户 #%d', 'jio'), $user_id)
: __('全站', 'jio');
$count_hint = static function (array $bucket) use ($scope): string {
/* translators: 1 scope 2 record count */
return sprintf(__('%1$s · %2$s 条记录', 'aksrt'), $scope, number_format_i18n((int) ($bucket['count'] ?? 0)));
return sprintf(__('%1$s · %2$s 条记录', 'jio'), $scope, number_format_i18n((int) ($bucket['count'] ?? 0)));
};
$pending_url = $this->page_url(array_filter(['status' => 'pending', 'user_id' => $user_id ?: null]));
$settled_url = $this->page_url(array_filter(['status' => 'settled', 'user_id' => $user_id ?: null]));
NativeUi::stats([
['label' => __('现金待结算', 'aksrt'), 'value' => $this->format_amount($cash['pending'] ?? '0', false), 'url' => add_query_arg('currency', 'cny', $pending_url), 'icon' => 'dashicons-clock', 'tone' => 'warning'],
['label' => __('现金已结算', 'aksrt'), 'value' => $this->format_amount($cash['settled'] ?? '0', false), 'url' => add_query_arg('currency', 'cny', $settled_url), 'icon' => 'dashicons-yes-alt', 'tone' => 'success'],
['label' => __('现金分成合计', 'aksrt'), 'value' => $this->format_amount($cash['total'] ?? '0', false), 'hint' => $count_hint($cash), 'icon' => 'dashicons-chart-area', 'tone' => 'info'],
['label' => __('积分待结算', 'aksrt'), 'value' => $this->format_amount($points['pending'] ?? 0, true), 'url' => add_query_arg('currency', 'points', $pending_url), 'icon' => 'dashicons-clock', 'tone' => 'warning'],
['label' => __('积分已结算', 'aksrt'), 'value' => $this->format_amount($points['settled'] ?? 0, true), 'url' => add_query_arg('currency', 'points', $settled_url), 'icon' => 'dashicons-yes-alt', 'tone' => 'success'],
['label' => __('积分分成合计', 'aksrt'), 'value' => $this->format_amount($points['total'] ?? 0, true), 'hint' => $count_hint($points), 'icon' => 'dashicons-star-filled', 'tone' => 'info'],
['label' => __('现金待结算', 'jio'), 'value' => $this->format_amount($cash['pending'] ?? '0', false), 'url' => add_query_arg('currency', 'cny', $pending_url), 'icon' => 'dashicons-clock', 'tone' => 'warning'],
['label' => __('现金已结算', 'jio'), 'value' => $this->format_amount($cash['settled'] ?? '0', false), 'url' => add_query_arg('currency', 'cny', $settled_url), 'icon' => 'dashicons-yes-alt', 'tone' => 'success'],
['label' => __('现金分成合计', 'jio'), 'value' => $this->format_amount($cash['total'] ?? '0', false), 'hint' => $count_hint($cash), 'icon' => 'dashicons-chart-area', 'tone' => 'info'],
['label' => __('积分待结算', 'jio'), 'value' => $this->format_amount($points['pending'] ?? 0, true), 'url' => add_query_arg('currency', 'points', $pending_url), 'icon' => 'dashicons-clock', 'tone' => 'warning'],
['label' => __('积分已结算', 'jio'), 'value' => $this->format_amount($points['settled'] ?? 0, true), 'url' => add_query_arg('currency', 'points', $settled_url), 'icon' => 'dashicons-yes-alt', 'tone' => 'success'],
['label' => __('积分分成合计', 'jio'), 'value' => $this->format_amount($points['total'] ?? 0, true), 'hint' => $count_hint($points), 'icon' => 'dashicons-star-filled', 'tone' => 'info'],
]);
}
@@ -232,16 +232,16 @@ trait PayIncomeList {
*/
private function render_status_views(array $filters, array $query_args, array $counts): void {
$base = array_diff_key($query_args, ['status' => true]);
$views = ['' => [__('全部', 'aksrt'), array_sum($counts)]];
$views = ['' => [__('全部', 'jio'), array_sum($counts)]];
foreach ($this->status_labels() as $key => $label) {
$views[$key] = [$label, (int) ($counts[$key] ?? 0)];
}
echo '<nav class="aksrt-native-tabs aksrt-income-views" aria-label="' . esc_attr__('按结算状态查看', 'aksrt') . '">';
echo '<nav class="jio-native-tabs jio-income-views" aria-label="' . esc_attr__('按结算状态查看', 'jio') . '">';
foreach ($views as $key => [$label, $count]) {
$url = $this->page_url($base + ($key !== '' ? ['status' => $key] : []));
$active = $filters['status'] === $key;
echo '<a class="aksrt-native-tabs__link' . ($active ? ' is-active' : '') . '" href="' . esc_url($url) . '"' . ($active ? ' aria-current="page"' : '') . '>'
echo '<a class="jio-native-tabs__link' . ($active ? ' is-active' : '') . '" href="' . esc_url($url) . '"' . ($active ? ' aria-current="page"' : '') . '>'
. esc_html($label) . ' <span class="count">' . esc_html(number_format_i18n($count)) . '</span></a>';
}
echo '</nav>';
@@ -249,30 +249,30 @@ trait PayIncomeList {
/** @param array<string, mixed> $filters */
private function render_filter_card(array $filters): void {
NativeUi::card_open('', '', false, 'aksrt-income-filter-card');
echo '<form method="get" class="aksrt-income-filters" role="search">';
NativeUi::card_open('', '', false, 'jio-income-filter-card');
echo '<form method="get" class="jio-income-filters" role="search">';
echo '<input type="hidden" name="page" value="' . esc_attr(self::PAGE_SLUG) . '" />';
if ($filters['status'] !== '') {
echo '<input type="hidden" name="status" value="' . esc_attr($filters['status']) . '" />';
}
echo '<label class="aksrt-income-filters__search"><span class="screen-reader-text">' . esc_html__('订单号', 'aksrt') . '</span>'
. '<input type="search" name="order_number" value="' . esc_attr($filters['order_number']) . '" placeholder="' . esc_attr__('搜索订单号', 'aksrt') . '" /></label>';
echo $this->filter_select('currency', __('分成币种', 'aksrt'), $this->currency_options(), $filters['currency']); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo $this->filter_select('order_status', __('订单状态', 'aksrt'), $this->order_status_options(), $filters['order_status']); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo '<span class="aksrt-income-filters__range">'
. '<label><span class="screen-reader-text">' . esc_html__('开始日期', 'aksrt') . '</span><input type="date" name="date_from" value="' . esc_attr($filters['date_from']) . '" /></label>'
echo '<label class="jio-income-filters__search"><span class="screen-reader-text">' . esc_html__('订单号', 'jio') . '</span>'
. '<input type="search" name="order_number" value="' . esc_attr($filters['order_number']) . '" placeholder="' . esc_attr__('搜索订单号', 'jio') . '" /></label>';
echo $this->filter_select('currency', __('分成币种', 'jio'), $this->currency_options(), $filters['currency']); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo $this->filter_select('order_status', __('订单状态', 'jio'), $this->order_status_options(), $filters['order_status']); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo '<span class="jio-income-filters__range">'
. '<label><span class="screen-reader-text">' . esc_html__('开始日期', 'jio') . '</span><input type="date" name="date_from" value="' . esc_attr($filters['date_from']) . '" /></label>'
. '<span aria-hidden="true">–</span>'
. '<label><span class="screen-reader-text">' . esc_html__('结束日期', 'aksrt') . '</span><input type="date" name="date_to" value="' . esc_attr($filters['date_to']) . '" /></label>'
. '<label><span class="screen-reader-text">' . esc_html__('结束日期', 'jio') . '</span><input type="date" name="date_to" value="' . esc_attr($filters['date_to']) . '" /></label>'
. '</span>';
foreach (['user_id' => __('作者 ID', 'aksrt'), 'post_id' => __('文章 ID', 'aksrt')] as $name => $label) {
echo '<label><span class="screen-reader-text">' . esc_html($label) . '</span><input type="number" min="1" name="' . esc_attr($name) . '" class="aksrt-income-filters__id" value="'
foreach (['user_id' => __('作者 ID', 'jio'), 'post_id' => __('文章 ID', 'jio')] as $name => $label) {
echo '<label><span class="screen-reader-text">' . esc_html($label) . '</span><input type="number" min="1" name="' . esc_attr($name) . '" class="jio-income-filters__id" value="'
. esc_attr($filters[$name] > 0 ? (string) $filters[$name] : '') . '" placeholder="' . esc_attr($label) . '" /></label>';
}
echo '<span class="aksrt-income-filters__submit"><button type="submit" class="button button-primary">' . esc_html__('筛选', 'aksrt') . '</button>';
echo '<span class="jio-income-filters__submit"><button type="submit" class="button button-primary">' . esc_html__('筛选', 'jio') . '</button>';
if (array_diff_key($this->list_query_args($filters), ['status' => true]) !== []) {
$reset = $this->page_url($filters['status'] !== '' ? ['status' => $filters['status']] : []);
echo '<a class="button" href="' . esc_url($reset) . '">' . esc_html__('重置', 'aksrt') . '</a>';
echo '<a class="button" href="' . esc_url($reset) . '">' . esc_html__('重置', 'jio') . '</a>';
}
echo '</span></form>';
NativeUi::card_close();
@@ -290,60 +290,60 @@ trait PayIncomeList {
/** @return array<string, string> */
private function currency_options(): array {
return [
'' => __('全部币种', 'aksrt'),
'cny' => __('现金(CNY)', 'aksrt'),
'points' => __('积分', 'aksrt'),
'' => __('全部币种', 'jio'),
'cny' => __('现金(CNY)', 'jio'),
'points' => __('积分', 'jio'),
];
}
/** @return array<string, string> */
private function order_status_options(): array {
return ['' => __('全部订单状态', 'aksrt')] + \AKSRT\Core\DomainModel::order_status_labels();
return ['' => __('全部订单状态', 'jio')] + \Jio\Core\DomainModel::order_status_labels();
}
/**
* 批量操作栏:已选计数 + 「批量操作」下拉 + 应用。表格里的复选框通过 form="aksrt-income-bulk" 关联进来。
* 批量操作栏:已选计数 + 「批量操作」下拉 + 应用。表格里的复选框通过 form="jio-income-bulk" 关联进来。
* 各操作的确认文案挂在 option 的 data-confirm 上,由 admin-income.js 按所选操作取用。
*/
private function bulk_form_html(bool $can_settle, bool $has_pending): string {
$settle_attrs = $can_settle
? ' data-confirm="' . esc_attr__('确认立即结算选中的 {count} 条分成?金额会转入作者钱包,不可撤销。', 'aksrt') . '"'
? ' data-confirm="' . esc_attr__('确认立即结算选中的 {count} 条分成?金额会转入作者钱包,不可撤销。', 'jio') . '"'
: ' disabled';
$settle_label = $can_settle ? __('立即结算', 'aksrt') : __('立即结算(分成功能未启用)', 'aksrt');
$settle_label = $can_settle ? __('立即结算', 'jio') : __('立即结算(分成功能未启用)', 'jio');
return '<form id="aksrt-income-bulk" class="aksrt-native-bulk aksrt-income-bulk" method="post" action="' . esc_url(admin_url('admin-post.php')) . '">'
. '<input type="hidden" name="action" value="aksrt_pay_bulk_income" />'
return '<form id="jio-income-bulk" class="jio-native-bulk jio-income-bulk" method="post" action="' . esc_url(admin_url('admin-post.php')) . '">'
. '<input type="hidden" name="action" value="jio_pay_bulk_income" />'
. $this->operation_key_field()
. wp_nonce_field('aksrt_pay_bulk_income', '_wpnonce', true, false)
. '<span class="aksrt-income-bulk__count" data-aksrt-income-selected aria-live="polite">'
. esc_html($has_pending ? __('未选择记录', 'aksrt') : __('本页没有待结算记录', 'aksrt')) . '</span>'
. '<label class="screen-reader-text" for="aksrt-income-bulk-action">' . esc_html__('批量操作', 'aksrt') . '</label>'
. '<select name="bulk_action" id="aksrt-income-bulk-action" data-aksrt-income-bulk-action>'
. '<option value="">' . esc_html__('批量操作', 'aksrt') . '</option>'
. wp_nonce_field('jio_pay_bulk_income', '_wpnonce', true, false)
. '<span class="jio-income-bulk__count" data-jio-income-selected aria-live="polite">'
. esc_html($has_pending ? __('未选择记录', 'jio') : __('本页没有待结算记录', 'jio')) . '</span>'
. '<label class="screen-reader-text" for="jio-income-bulk-action">' . esc_html__('批量操作', 'jio') . '</label>'
. '<select name="bulk_action" id="jio-income-bulk-action" data-jio-income-bulk-action>'
. '<option value="">' . esc_html__('批量操作', 'jio') . '</option>'
. '<option value="settle"' . $settle_attrs . '>' . esc_html($settle_label) . '</option>'
. '<option value="delete" data-confirm="' . esc_attr__('确认删除选中的 {count} 条待结算分成?删除后作者不会获得这些分成,不可恢复。', 'aksrt') . '">' . esc_html__('删除', 'aksrt') . '</option>'
. '<option value="delete" data-confirm="' . esc_attr__('确认删除选中的 {count} 条待结算分成?删除后作者不会获得这些分成,不可恢复。', 'jio') . '">' . esc_html__('删除', 'jio') . '</option>'
. '</select>'
. '<button type="submit" class="button" disabled data-aksrt-income-bulk-submit>' . esc_html__('应用', 'aksrt') . '</button>'
. '<button type="submit" class="button" disabled data-jio-income-bulk-submit>' . esc_html__('应用', 'jio') . '</button>'
. '</form>';
}
private function delete_form_html(int $id): string {
return '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" class="aksrt-income-action"'
. ' data-aksrt-confirm="' . esc_attr(sprintf(__('确认删除待结算分成记录 #%d?删除后作者不会获得这笔分成,不可恢复。', 'aksrt'), $id)) . '">'
. '<input type="hidden" name="action" value="aksrt_pay_delete_income" />'
return '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" class="jio-income-action"'
. ' data-jio-confirm="' . esc_attr(sprintf(__('确认删除待结算分成记录 #%d?删除后作者不会获得这笔分成,不可恢复。', 'jio'), $id)) . '">'
. '<input type="hidden" name="action" value="jio_pay_delete_income" />'
. '<input type="hidden" name="revenue_id" value="' . esc_attr((string) $id) . '" />'
. wp_nonce_field('aksrt_pay_delete_income_' . $id, '_wpnonce', true, false)
. '<button type="submit" class="button button-small is-danger">' . esc_html__('删除', 'aksrt') . '</button></form>';
. wp_nonce_field('jio_pay_delete_income_' . $id, '_wpnonce', true, false)
. '<button type="submit" class="button button-small is-danger">' . esc_html__('删除', 'jio') . '</button></form>';
}
private function settle_form_html(int $id): string {
return '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" class="aksrt-income-action"'
. ' data-aksrt-confirm="' . esc_attr(sprintf(__('确认立即结算分成记录 #%d?金额会转入作者钱包,不可撤销。', 'aksrt'), $id)) . '">'
. '<input type="hidden" name="action" value="aksrt_pay_settle_income" />'
return '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" class="jio-income-action"'
. ' data-jio-confirm="' . esc_attr(sprintf(__('确认立即结算分成记录 #%d?金额会转入作者钱包,不可撤销。', 'jio'), $id)) . '">'
. '<input type="hidden" name="action" value="jio_pay_settle_income" />'
. '<input type="hidden" name="revenue_id" value="' . esc_attr((string) $id) . '" />'
. $this->operation_key_field()
. wp_nonce_field('aksrt_pay_settle_income_' . $id, '_wpnonce', true, false)
. '<button type="submit" class="button button-small">' . esc_html__('立即结算', 'aksrt') . '</button></form>';
. wp_nonce_field('jio_pay_settle_income_' . $id, '_wpnonce', true, false)
. '<button type="submit" class="button button-small">' . esc_html__('立即结算', 'jio') . '</button></form>';
}
/**
+43 -43
View File
@@ -1,16 +1,16 @@
<?php
/**
* @package AKSRT
* @package Jio
*/
namespace AKSRT\Admin\Native;
namespace Jio\Admin\Native;
defined('ABSPATH') || exit;
class PayMenus {
public const CAPABILITY = 'manage_options';
public const MENU_SLUG = 'aksrt_pay';
public const MENU_SLUG = 'jio_pay';
private static $instance = null;
@@ -42,85 +42,85 @@ class PayMenus {
'slug' => self::MENU_SLUG,
'icon' => 'dashicons-cart',
'desc' => '',
'title' => __('概览', 'aksrt'),
'title' => __('概览', 'jio'),
'callback' => [$this, 'render_overview_page'],
],
'orders' => [
'slug' => 'aksrt_orders',
'slug' => 'jio_orders',
'icon' => 'dashicons-list-view',
'desc' => __('收款确认、关闭与退款标记', 'aksrt'),
'title' => __('订单明细', 'aksrt'),
'desc' => __('收款确认、关闭与退款标记', 'jio'),
'title' => __('订单明细', 'jio'),
'callback' => static function (): void {
if (class_exists('AKSRT\Admin\Native\PayOrders')) {
if (class_exists('Jio\Admin\Native\PayOrders')) {
PayOrders::get_instance()->render_orders_page();
return;
}
wp_die(esc_html__('订单模块未初始化。', 'aksrt'));
wp_die(esc_html__('订单模块未初始化。', 'jio'));
},
],
'products' => [
'slug' => 'aksrt_pay_products',
'slug' => 'jio_pay_products',
'icon' => 'dashicons-media-document',
'desc' => __('付费文章定价与销量', 'aksrt'),
'title' => __('付费文章', 'aksrt'),
'desc' => __('付费文章定价与销量', 'jio'),
'title' => __('付费文章', 'jio'),
'callback' => static function (): void {
if (class_exists('AKSRT\\Admin\\Native\\PayProducts')) {
if (class_exists('Jio\\Admin\\Native\\PayProducts')) {
PayProducts::get_instance()->render_products_page();
return;
}
wp_die(esc_html__('付费文章模块未初始化。', 'aksrt'));
wp_die(esc_html__('付费文章模块未初始化。', 'jio'));
},
],
'coupons' => [
'slug' => 'aksrt_coupons',
'slug' => 'jio_coupons',
'icon' => 'dashicons-tickets-alt',
'desc' => __('创建与批量生成优惠码', 'aksrt'),
'title' => __('优惠码', 'aksrt'),
'desc' => __('创建与批量生成优惠码', 'jio'),
'title' => __('优惠码', 'jio'),
'callback' => static function (): void {
if (class_exists('AKSRT\\Admin\\Native\\PayCoupons')) {
if (class_exists('Jio\\Admin\\Native\\PayCoupons')) {
PayCoupons::get_instance()->render_page();
return;
}
wp_die(esc_html__('优惠码模块未初始化。', 'aksrt'));
wp_die(esc_html__('优惠码模块未初始化。', 'jio'));
},
],
'charge_cards' => [
'slug' => 'aksrt_charge_cards',
'slug' => 'jio_charge_cards',
'icon' => 'dashicons-id-alt',
'desc' => __('充值卡密生成与导出', 'aksrt'),
'title' => __('卡密管理', 'aksrt'),
'desc' => __('充值卡密生成与导出', 'jio'),
'title' => __('卡密管理', 'jio'),
'callback' => static function (): void {
if (class_exists('AKSRT\\Admin\\Native\\PayChargeCards')) {
if (class_exists('Jio\\Admin\\Native\\PayChargeCards')) {
PayChargeCards::get_instance()->render_charge_cards_page();
return;
}
wp_die(esc_html__('卡密模块未初始化。', 'aksrt'));
wp_die(esc_html__('卡密模块未初始化。', 'jio'));
},
],
'income' => [
'slug' => 'aksrt_income',
'slug' => 'jio_income',
'icon' => 'dashicons-chart-pie',
'desc' => __('创作者分成结算', 'aksrt'),
'title' => __('创作分成', 'aksrt'),
'desc' => __('创作者分成结算', 'jio'),
'title' => __('创作分成', 'jio'),
'callback' => static function (): void {
if (class_exists('AKSRT\\Admin\\Native\\PayIncome')) {
if (class_exists('Jio\\Admin\\Native\\PayIncome')) {
PayIncome::get_instance()->render_income_page();
return;
}
wp_die(esc_html__('分成模块未初始化。', 'aksrt'));
wp_die(esc_html__('分成模块未初始化。', 'jio'));
},
],
'withdraw' => [
'slug' => 'aksrt_withdraw',
'slug' => 'jio_withdraw',
'icon' => 'dashicons-money-alt',
'desc' => __('提现申请审核与打款', 'aksrt'),
'title' => __('提现审核', 'aksrt'),
'desc' => __('提现申请审核与打款', 'jio'),
'title' => __('提现审核', 'jio'),
'callback' => static function (): void {
if (class_exists('AKSRT\\Admin\\Native\\PayWithdrawals')) {
if (class_exists('Jio\\Admin\\Native\\PayWithdrawals')) {
PayWithdrawals::get_instance()->render_withdrawals_page();
return;
}
wp_die(esc_html__('提现模块未初始化。', 'aksrt'));
wp_die(esc_html__('提现模块未初始化。', 'jio'));
},
],
];
@@ -132,8 +132,8 @@ class PayMenus {
}
add_menu_page(
__('支付中心', 'aksrt'),
__('支付中心', 'aksrt'),
__('支付中心', 'jio'),
__('支付中心', 'jio'),
self::CAPABILITY,
self::MENU_SLUG,
[$this, 'render_overview_page'],
@@ -160,20 +160,20 @@ class PayMenus {
$page = isset($_GET['page']) ? sanitize_key((string) wp_unslash($_GET['page'])) : '';
if ($page !== 'aksrt_withdraw' && class_exists('\AKSRT\Payment\Withdrawal')) {
$pending = (int) \AKSRT\Payment\Withdrawal::get_instance()->get_count(['status' => \AKSRT\Payment\Withdrawal::STATUS_PENDING]);
$processing = (int) \AKSRT\Payment\Withdrawal::get_instance()->get_count(['status' => \AKSRT\Payment\Withdrawal::STATUS_PROCESSING]);
if ($page !== 'jio_withdraw' && class_exists('\Jio\Payment\Withdrawal')) {
$pending = (int) \Jio\Payment\Withdrawal::get_instance()->get_count(['status' => \Jio\Payment\Withdrawal::STATUS_PENDING]);
$processing = (int) \Jio\Payment\Withdrawal::get_instance()->get_count(['status' => \Jio\Payment\Withdrawal::STATUS_PROCESSING]);
$active = $pending + $processing;
if ($active > 0) {
$url = admin_url('admin.php?page=aksrt_withdraw&status=pending');
$url = admin_url('admin.php?page=jio_withdraw&status=pending');
echo '<div class="notice notice-info is-dismissible">';
echo '<h3>' . esc_html__('提现申请待处理', 'aksrt') . '</h3>';
echo '<h3>' . esc_html__('提现申请待处理', 'jio') . '</h3>';
echo '<p>' . esc_html(sprintf(
/* translators: %d: open withdrawal count */
__('您有 %d 个提现申请待处理', 'aksrt'),
__('您有 %d 个提现申请待处理', 'jio'),
$active
)) . '</p>';
echo '<p><a class="button button-primary" href="' . esc_url($url) . '">' . esc_html__('立即处理', 'aksrt') . '</a></p>';
echo '<p><a class="button button-primary" href="' . esc_url($url) . '">' . esc_html__('立即处理', 'jio') . '</a></p>';
echo '</div>';
}
}
+76 -76
View File
@@ -6,12 +6,12 @@
* PSR-4 要求与宿主类同目录);这里保留入口、admin-post 处理器与两个视图共用的展示助手。
* 页面样式在 assets/css/admin/orders.css,危险操作确认在 assets/js/admin-orders.js。
*
* @package AKSRT
* @package Jio
*/
namespace AKSRT\Admin\Native;
namespace Jio\Admin\Native;
use AKSRT\Core\DomainModel;
use Jio\Core\DomainModel;
defined('ABSPATH') || exit;
@@ -21,7 +21,7 @@ class PayOrders {
use PayOrdersDetail;
public const CAPABILITY = 'manage_options';
public const PAGE_SLUG = 'aksrt_orders';
public const PAGE_SLUG = 'jio_orders';
private static $instance = null;
@@ -40,11 +40,11 @@ class PayOrders {
return;
}
add_action('admin_enqueue_scripts', [$this, 'enqueue_assets']);
add_action('admin_post_aksrt_pay_update_order_status', [$this, 'handle_update_order_status']);
add_action('admin_post_aksrt_pay_update_order_note', [$this, 'handle_update_order_note']);
add_action('admin_post_aksrt_pay_clear_closed_orders', [$this, 'handle_clear_closed_orders']);
add_action('admin_post_aksrt_pay_delete_order', [$this, 'handle_delete_order']);
add_action('admin_post_aksrt_pay_refund_order', [$this, 'handle_refund_order']);
add_action('admin_post_jio_pay_update_order_status', [$this, 'handle_update_order_status']);
add_action('admin_post_jio_pay_update_order_note', [$this, 'handle_update_order_note']);
add_action('admin_post_jio_pay_clear_closed_orders', [$this, 'handle_clear_closed_orders']);
add_action('admin_post_jio_pay_delete_order', [$this, 'handle_delete_order']);
add_action('admin_post_jio_pay_refund_order', [$this, 'handle_refund_order']);
}
public function enqueue_assets(): void {
@@ -54,17 +54,17 @@ class PayOrders {
}
$css = '/assets/css/admin/orders.css';
$js = '/assets/js/admin-orders.js';
wp_enqueue_style('aksrt-admin-orders', AKSRT_URI . $css, [NativeUi::STYLE_HANDLE], (string) filemtime(AKSRT_DIR . $css));
wp_enqueue_script('aksrt-admin-orders', AKSRT_URI . $js, [], (string) filemtime(AKSRT_DIR . $js), true);
wp_enqueue_style('jio-admin-orders', JIO_URI . $css, [NativeUi::STYLE_HANDLE], (string) filemtime(JIO_DIR . $css));
wp_enqueue_script('jio-admin-orders', JIO_URI . $js, [], (string) filemtime(JIO_DIR . $js), true);
}
public function render_orders_page(): void {
$this->ensure_cap();
NativeUi::page_open('aksrt-orders-page');
NativeUi::page_open('jio-orders-page');
if (!class_exists('\AKSRT\Payment\OrderRepository')) {
NativeUi::header(__('订单明细', 'aksrt'), '', 'dashicons-list-view');
echo '<div class="notice notice-error"><p>' . esc_html__('订单服务不可用。', 'aksrt') . '</p></div>';
if (!class_exists('\Jio\Payment\OrderRepository')) {
NativeUi::header(__('订单明细', 'jio'), '', 'dashicons-list-view');
echo '<div class="notice notice-error"><p>' . esc_html__('订单服务不可用。', 'jio') . '</p></div>';
NativeUi::page_close();
return;
}
@@ -87,13 +87,13 @@ class PayOrders {
$this->ensure_cap();
$order_number = isset($_POST['order_number']) ? sanitize_text_field((string) wp_unslash($_POST['order_number'])) : '';
$new_status = isset($_POST['new_status']) ? sanitize_key((string) wp_unslash($_POST['new_status'])) : '';
check_admin_referer('aksrt_pay_update_order_status_' . $order_number);
check_admin_referer('jio_pay_update_order_status_' . $order_number);
$ok = false;
if ($order_number !== '' && $new_status !== '' && class_exists('\AKSRT\Payment\OrderRepository')) {
$ok = \AKSRT\Payment\OrderRepository::get_instance()->update_status($order_number, $new_status);
if ($order_number !== '' && $new_status !== '' && class_exists('\Jio\Payment\OrderRepository')) {
$ok = \Jio\Payment\OrderRepository::get_instance()->update_status($order_number, $new_status);
}
$this->redirect_back(['aksrt_pay_msg' => $ok ? 'order_updated' : 'order_failed']);
$this->redirect_back(['jio_pay_msg' => $ok ? 'order_updated' : 'order_failed']);
}
/**
@@ -105,11 +105,11 @@ class PayOrders {
public function handle_refund_order(): void {
$this->ensure_cap();
$order_number = isset($_POST['order_number']) ? sanitize_text_field((string) wp_unslash($_POST['order_number'])) : '';
check_admin_referer('aksrt_pay_refund_order_' . $order_number);
check_admin_referer('jio_pay_refund_order_' . $order_number);
$code = 'order_refund_failed';
if ($order_number !== '' && class_exists('\AKSRT\Payment\OrderRepository')) {
$repo = \AKSRT\Payment\OrderRepository::get_instance();
if ($order_number !== '' && class_exists('\Jio\Payment\OrderRepository')) {
$repo = \Jio\Payment\OrderRepository::get_instance();
$order = $repo->get($order_number);
if (!is_array($order)) {
$code = 'order_refund_missing';
@@ -118,37 +118,37 @@ class PayOrders {
} else {
$ok = $repo->update_status($order_number, DomainModel::ORDER_STATUS_REFUNDED, [
'allow_refund' => true,
'note' => __('管理员在订单明细中标记退款', 'aksrt'),
'note' => __('管理员在订单明细中标记退款', 'jio'),
]);
$code = $ok ? 'order_refunded' : 'order_refund_failed';
}
}
$this->redirect_back(['aksrt_pay_msg' => $code]);
$this->redirect_back(['jio_pay_msg' => $code]);
}
public function handle_update_order_note(): void {
$this->ensure_cap();
$order_number = isset($_POST['order_number']) ? sanitize_text_field((string) wp_unslash($_POST['order_number'])) : '';
$admin_note = isset($_POST['admin_note']) ? sanitize_textarea_field((string) wp_unslash($_POST['admin_note'])) : '';
check_admin_referer('aksrt_pay_update_order_note_' . $order_number);
check_admin_referer('jio_pay_update_order_note_' . $order_number);
$ok = false;
if ($order_number !== '' && class_exists('\AKSRT\Payment\OrderRepository')) {
$ok = \AKSRT\Payment\OrderRepository::get_instance()->update_admin_note($order_number, $admin_note);
if ($order_number !== '' && class_exists('\Jio\Payment\OrderRepository')) {
$ok = \Jio\Payment\OrderRepository::get_instance()->update_admin_note($order_number, $admin_note);
}
$this->redirect_back(['aksrt_pay_msg' => $ok ? 'order_note_updated' : 'order_note_failed']);
$this->redirect_back(['jio_pay_msg' => $ok ? 'order_note_updated' : 'order_note_failed']);
}
public function handle_clear_closed_orders(): void {
$this->ensure_cap();
check_admin_referer('aksrt_pay_clear_closed_orders');
check_admin_referer('jio_pay_clear_closed_orders');
$days = isset($_POST['days']) ? max(1, min(3650, (int) $_POST['days'])) : 14;
$deleted = 0;
if (class_exists('\AKSRT\Payment\OrderRepository')) {
$deleted = \AKSRT\Payment\OrderRepository::get_instance()->clear_closed_orders($days);
if (class_exists('\Jio\Payment\OrderRepository')) {
$deleted = \Jio\Payment\OrderRepository::get_instance()->clear_closed_orders($days);
}
$this->redirect_back([
'aksrt_pay_msg' => $deleted > 0 ? 'orders_cleared' : 'orders_clear_empty',
'jio_pay_msg' => $deleted > 0 ? 'orders_cleared' : 'orders_clear_empty',
'cleared' => $deleted,
], true);
}
@@ -156,10 +156,10 @@ class PayOrders {
public function handle_delete_order(): void {
$this->ensure_cap();
$order_number = isset($_POST['order_number']) ? sanitize_text_field((string) wp_unslash($_POST['order_number'])) : '';
check_admin_referer('aksrt_pay_delete_order_' . $order_number);
check_admin_referer('jio_pay_delete_order_' . $order_number);
$ok = false;
if ($order_number !== '' && class_exists('\AKSRT\Payment\OrderRepository')) {
$repo = \AKSRT\Payment\OrderRepository::get_instance();
if ($order_number !== '' && class_exists('\Jio\Payment\OrderRepository')) {
$repo = \Jio\Payment\OrderRepository::get_instance();
$order = $repo->get($order_number);
// 只允许硬删已关闭订单。
if (is_array($order) && (string) ($order['status'] ?? '') === DomainModel::ORDER_STATUS_CLOSED) {
@@ -167,7 +167,7 @@ class PayOrders {
}
}
// 删除成功后详情已不存在,回到列表;失败时留在原处。
$this->redirect_back(['aksrt_pay_msg' => $ok ? 'order_deleted' : 'order_delete_failed'], $ok);
$this->redirect_back(['jio_pay_msg' => $ok ? 'order_deleted' : 'order_delete_failed'], $ok);
}
/**
@@ -180,7 +180,7 @@ class PayOrders {
if (is_string($referer) && $referer !== '') {
parse_str((string) wp_parse_url($referer, PHP_URL_QUERY), $parsed);
if (($parsed['page'] ?? '') === self::PAGE_SLUG) {
unset($parsed['page'], $parsed['aksrt_pay_msg'], $parsed['cleared']);
unset($parsed['page'], $parsed['jio_pay_msg'], $parsed['cleared']);
if ($to_list) {
unset($parsed['order']);
}
@@ -198,12 +198,12 @@ class PayOrders {
/** 当前状态可执行的动作按钮;没有可执行动作时返回空串。 */
private function order_actions_html(string $order_number, string $status, bool $small = true): string {
$labels = [
DomainModel::ORDER_STATUS_PAID => __('确认收款', 'aksrt'),
DomainModel::ORDER_STATUS_CLOSED => __('关闭订单', 'aksrt'),
DomainModel::ORDER_STATUS_PAID => __('确认收款', 'jio'),
DomainModel::ORDER_STATUS_CLOSED => __('关闭订单', 'jio'),
];
$confirms = [
DomainModel::ORDER_STATUS_PAID => __('确认将该订单标记为已支付?系统会立即为用户发放对应权益。', 'aksrt'),
DomainModel::ORDER_STATUS_CLOSED => __('确认关闭该订单?关闭后用户无法继续支付。', 'aksrt'),
DomainModel::ORDER_STATUS_PAID => __('确认将该订单标记为已支付?系统会立即为用户发放对应权益。', 'jio'),
DomainModel::ORDER_STATUS_CLOSED => __('确认关闭该订单?关闭后用户无法继续支付。', 'jio'),
];
$html = '';
@@ -213,8 +213,8 @@ class PayOrders {
continue;
}
$html .= $this->action_form(
'aksrt_pay_update_order_status',
'aksrt_pay_update_order_status_' . $order_number,
'jio_pay_update_order_status',
'jio_pay_update_order_status_' . $order_number,
['order_number' => $order_number, 'new_status' => $target],
$labels[$target],
$confirms[$target],
@@ -225,22 +225,22 @@ class PayOrders {
// 已支付是已退款的唯一前置状态。
if ($status === DomainModel::ORDER_STATUS_PAID) {
$html .= $this->action_form(
'aksrt_pay_refund_order',
'aksrt_pay_refund_order_' . $order_number,
'jio_pay_refund_order',
'jio_pay_refund_order_' . $order_number,
['order_number' => $order_number],
__('标记退款', 'aksrt'),
__('只会把订单标记为已退款,不会自动原路退回款项。确认继续?此操作不可撤销。', 'aksrt'),
__('标记退款', 'jio'),
__('只会把订单标记为已退款,不会自动原路退回款项。确认继续?此操作不可撤销。', 'jio'),
true,
$small
);
}
if ($status === DomainModel::ORDER_STATUS_CLOSED) {
$html .= $this->action_form(
'aksrt_pay_delete_order',
'aksrt_pay_delete_order_' . $order_number,
'jio_pay_delete_order',
'jio_pay_delete_order_' . $order_number,
['order_number' => $order_number],
__('删除', 'aksrt'),
__('确认永久删除此订单?不可恢复。', 'aksrt'),
__('删除', 'jio'),
__('确认永久删除此订单?不可恢复。', 'jio'),
true,
$small
);
@@ -249,13 +249,13 @@ class PayOrders {
}
/**
* 单按钮 POST 表单。确认文案放在 data-aksrt-confirm,由 admin-orders.js 拦截提交。
* 单按钮 POST 表单。确认文案放在 data-jio-confirm,由 admin-orders.js 拦截提交。
*
* @param array<string, string> $fields
*/
private function action_form(string $action, string $nonce_action, array $fields, string $label, string $confirm = '', bool $danger = false, bool $small = true): string {
$html = '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" class="aksrt-orders-action"'
. ($confirm !== '' ? ' data-aksrt-confirm="' . esc_attr($confirm) . '"' : '') . '>';
$html = '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" class="jio-orders-action"'
. ($confirm !== '' ? ' data-jio-confirm="' . esc_attr($confirm) . '"' : '') . '>';
$html .= '<input type="hidden" name="action" value="' . esc_attr($action) . '" />';
foreach ($fields as $name => $value) {
$html .= '<input type="hidden" name="' . esc_attr($name) . '" value="' . esc_attr($value) . '" />';
@@ -312,11 +312,11 @@ class PayOrders {
private function payment_method_label(string $method): string {
if ($this->payment_labels === null) {
$this->payment_labels = [
'points' => __('积分', 'aksrt'),
'free' => __('免费', 'aksrt'),
'points' => __('积分', 'jio'),
'free' => __('免费', 'jio'),
];
if (class_exists('\AKSRT\Payment\Gateway')) {
foreach (\AKSRT\Payment\Gateway::get_instance()->get_all() as $id => $gateway) {
if (class_exists('\Jio\Payment\Gateway')) {
foreach (\Jio\Payment\Gateway::get_instance()->get_all() as $id => $gateway) {
$this->payment_labels[(string) $id] = $gateway->get_name();
}
}
@@ -327,7 +327,7 @@ class PayOrders {
/** @return string[] */
private function third_party_gateway_ids(): array {
return array_values(array_diff(
array_keys(\AKSRT\Payment\Gateway::get_instance()->get_all()),
array_keys(\Jio\Payment\Gateway::get_instance()->get_all()),
['balance', 'points']
));
}
@@ -340,7 +340,7 @@ class PayOrders {
/** @param array<string,mixed> $order */
private function order_amount(array $order): string {
if ($this->is_points_order($order)) {
return max(0, (int) ($order['amount'] ?? 0)) . ' ' . __('积分', 'aksrt');
return max(0, (int) ($order['amount'] ?? 0)) . ' ' . __('积分', 'jio');
}
return $this->cash_amount((string) ($order['amount'] ?? '0'), (string) ($order['currency'] ?? ''));
}
@@ -351,35 +351,35 @@ class PayOrders {
private function cash_decimal(string $value): string {
try {
return \AKSRT\Ledger\AssetAmount::balance($value)->decimal();
return \Jio\Ledger\AssetAmount::balance($value)->decimal();
} catch (\InvalidArgumentException $e) {
return '0.00';
}
}
private function render_flash(): void {
if (empty($_GET['aksrt_pay_msg'])) {
if (empty($_GET['jio_pay_msg'])) {
return;
}
$code = sanitize_key((string) wp_unslash($_GET['aksrt_pay_msg']));
$code = sanitize_key((string) wp_unslash($_GET['jio_pay_msg']));
// 码 => [文案, 是否失败]
$map = [
'order_updated' => [__('订单状态已更新。', 'aksrt'), false],
'order_failed' => [__('订单状态更新失败,订单可能已被其他操作改变,请刷新后重试。', 'aksrt'), true],
'order_refunded' => [__('订单已标记为已退款。款项请在支付渠道后台原路退回。', 'aksrt'), false],
'order_refund_failed' => [__('订单标记退款失败。', 'aksrt'), true],
'order_refund_missing' => [__('订单不存在。', 'aksrt'), true],
'order_refund_not_paid' => [__('仅已支付订单可以标记退款。', 'aksrt'), true],
'order_note_updated' => [__('运营备注已保存。', 'aksrt'), false],
'order_note_failed' => [__('运营备注保存失败。', 'aksrt'), true],
'order_updated' => [__('订单状态已更新。', 'jio'), false],
'order_failed' => [__('订单状态更新失败,订单可能已被其他操作改变,请刷新后重试。', 'jio'), true],
'order_refunded' => [__('订单已标记为已退款。款项请在支付渠道后台原路退回。', 'jio'), false],
'order_refund_failed' => [__('订单标记退款失败。', 'jio'), true],
'order_refund_missing' => [__('订单不存在。', 'jio'), true],
'order_refund_not_paid' => [__('仅已支付订单可以标记退款。', 'jio'), true],
'order_note_updated' => [__('运营备注已保存。', 'jio'), false],
'order_note_failed' => [__('运营备注保存失败。', 'jio'), true],
'orders_cleared' => [sprintf(
/* translators: %d deleted count */
__('已清理 %d 条关闭订单。', 'aksrt'),
__('已清理 %d 条关闭订单。', 'jio'),
max(0, (int) ($_GET['cleared'] ?? 0))
), false],
'orders_clear_empty' => [__('没有可清理的关闭订单。', 'aksrt'), false],
'order_deleted' => [__('订单已删除。', 'aksrt'), false],
'order_delete_failed' => [__('订单删除失败(仅允许删除已关闭订单)。', 'aksrt'), true],
'orders_clear_empty' => [__('没有可清理的关闭订单。', 'jio'), false],
'order_deleted' => [__('订单已删除。', 'jio'), false],
'order_delete_failed' => [__('订单删除失败(仅允许删除已关闭订单)。', 'jio'), true],
];
if (!isset($map[$code])) {
return;
@@ -390,7 +390,7 @@ class PayOrders {
private function ensure_cap(): void {
if (!current_user_can(self::CAPABILITY)) {
wp_die(esc_html__('您没有权限访问此页面。', 'aksrt'));
wp_die(esc_html__('您没有权限访问此页面。', 'jio'));
}
}
+59 -59
View File
@@ -4,52 +4,52 @@
*
* 作为 flat sibling trait 被 PayOrders 使用(PSR-4:trait 必须与宿主类同目录)。
*
* @package AKSRT
* @package Jio
*/
namespace AKSRT\Admin\Native;
namespace Jio\Admin\Native;
use AKSRT\Core\DomainModel;
use AKSRT\Payment\OrderRepository;
use Jio\Core\DomainModel;
use Jio\Payment\OrderRepository;
defined('ABSPATH') || exit;
trait PayOrdersDetail {
private function render_order_detail_page(string $order_number): void {
$back = NativeUi::button_link(__('返回订单列表', 'aksrt'), $this->list_back_url(), false, 'dashicons-arrow-left-alt');
$back = NativeUi::button_link(__('返回订单列表', 'jio'), $this->list_back_url(), false, 'dashicons-arrow-left-alt');
$repo = OrderRepository::get_instance();
$order = $repo->get($order_number);
if (!$order) {
echo '<div class="aksrt-native-header"><div class="aksrt-native-header__main"><h1 class="aksrt-native-header__title">'
. esc_html__('订单详情', 'aksrt') . '</h1></div><div class="aksrt-native-header__actions">' . $back . '</div></div>';
echo '<div class="jio-native-header"><div class="jio-native-header__main"><h1 class="jio-native-header__title">'
. esc_html__('订单详情', 'jio') . '</h1></div><div class="jio-native-header__actions">' . $back . '</div></div>';
echo '<hr class="wp-header-end">';
$this->render_flash();
echo NativeUi::empty_state(__('订单不存在', 'aksrt'), __('订单可能已被删除,或订单号有误。', 'aksrt'), 'dashicons-search'); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo NativeUi::empty_state(__('订单不存在', 'jio'), __('订单可能已被删除,或订单号有误。', 'jio'), 'dashicons-search'); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
return;
}
$status = (string) ($order['status'] ?? '');
$context = $repo->get_admin_order_context($order_number);
echo '<div class="aksrt-native-header aksrt-orders-detail__header"><div class="aksrt-native-header__main">';
echo '<h1 class="aksrt-native-header__title"><span class="aksrt-native-header__icon dashicons dashicons-media-spreadsheet" aria-hidden="true"></span>'
. esc_html__('订单详情', 'aksrt') . '</h1>';
echo '<p class="aksrt-native-header__desc"><span class="aksrt-orders-number">' . esc_html($order_number) . '</span> '
echo '<div class="jio-native-header jio-orders-detail__header"><div class="jio-native-header__main">';
echo '<h1 class="jio-native-header__title"><span class="jio-native-header__icon dashicons dashicons-media-spreadsheet" aria-hidden="true"></span>'
. esc_html__('订单详情', 'jio') . '</h1>';
echo '<p class="jio-native-header__desc"><span class="jio-orders-number">' . esc_html($order_number) . '</span> '
. NativeUi::status_badge($status, $this->status_badge_map()) . '</p>';
echo '</div><div class="aksrt-native-header__actions">' . $back . '</div></div>';
echo '</div><div class="jio-native-header__actions">' . $back . '</div></div>';
echo '<hr class="wp-header-end">';
$this->render_flash();
echo '<div class="aksrt-orders-detail">';
echo '<div class="aksrt-orders-detail__main">';
echo '<div class="jio-orders-detail">';
echo '<div class="jio-orders-detail__main">';
$this->render_detail_summary($order, $context);
$this->render_detail_timeline($order);
$this->render_detail_snapshot($order);
echo '</div>';
echo '<aside class="aksrt-orders-detail__side">';
echo '<aside class="jio-orders-detail__side">';
$this->render_detail_actions($order_number, $status);
$this->render_detail_buyer($order, $context);
$this->render_detail_note($order_number, $order, $context);
@@ -72,31 +72,31 @@ trait PayOrdersDetail {
}
}
NativeUi::card_open(__('订单概要', 'aksrt'));
echo '<dl class="aksrt-orders-dl">';
$this->dl_row(__('商品', 'aksrt'), $product);
$this->dl_row(__('商品类型', 'aksrt'), esc_html($this->product_type_label($type) . ($id > 0 ? ' · #' . $id : '')));
$this->dl_row(__('支付方式', 'aksrt'), esc_html($this->payment_method_label((string) ($order['payment_method'] ?? ''))));
NativeUi::card_open(__('订单概要', 'jio'));
echo '<dl class="jio-orders-dl">';
$this->dl_row(__('商品', 'jio'), $product);
$this->dl_row(__('商品类型', 'jio'), esc_html($this->product_type_label($type) . ($id > 0 ? ' · #' . $id : '')));
$this->dl_row(__('支付方式', 'jio'), esc_html($this->payment_method_label((string) ($order['payment_method'] ?? ''))));
$txn = (string) ($order['payment_transaction_id'] ?? '');
$this->dl_row(__('渠道交易号', 'aksrt'), $txn !== '' ? '<code class="aksrt-native-mono">' . esc_html($txn) . '</code>' : '<span class="aksrt-native-muted">—</span>');
$this->dl_row(__('渠道交易号', 'jio'), $txn !== '' ? '<code class="jio-native-mono">' . esc_html($txn) . '</code>' : '<span class="jio-native-muted">—</span>');
echo '</dl>';
// 金额拆分:只在有折扣 / 运费时展开,否则只显示实付。
$discount = (string) ($pricing['discount'] ?? '0');
$shipping = (string) ($pricing['shipping_fee'] ?? '0');
echo '<div class="aksrt-orders-amounts">';
echo '<div class="jio-orders-amounts">';
if ($this->has_amount($discount) || $this->has_amount($shipping)) {
$this->amount_row(__('商品金额', 'aksrt'), $this->format_amount($order, (string) ($pricing['subtotal'] ?? '0')));
$this->amount_row(__('商品金额', 'jio'), $this->format_amount($order, (string) ($pricing['subtotal'] ?? '0')));
if ($this->has_amount($shipping)) {
$this->amount_row(__('运费', 'aksrt'), $this->format_amount($order, $shipping));
$this->amount_row(__('运费', 'jio'), $this->format_amount($order, $shipping));
}
if ($this->has_amount($discount)) {
$code = (string) ($coupon['code'] ?? '');
$label = $code !== '' ? sprintf(__('优惠(%s)', 'aksrt'), $code) : __('优惠', 'aksrt');
$label = $code !== '' ? sprintf(__('优惠(%s)', 'jio'), $code) : __('优惠', 'jio');
$this->amount_row($label, '− ' . $this->format_amount($order, $discount));
}
}
echo '<div class="aksrt-orders-amounts__row is-total"><span>' . esc_html__('实付金额', 'aksrt') . '</span><strong class="aksrt-native-amount">'
echo '<div class="jio-orders-amounts__row is-total"><span>' . esc_html__('实付金额', 'jio') . '</span><strong class="jio-native-amount">'
. esc_html($this->order_amount($order)) . '</strong></div>';
echo '</div>';
NativeUi::card_close();
@@ -105,26 +105,26 @@ trait PayOrdersDetail {
private function render_detail_timeline(array $order): void {
// created_at / paid_at / refunded_at 为站点时间;expires_at 为 UTC。
$events = [
[__('创建订单', 'aksrt'), (string) ($order['created_at'] ?? ''), 'info'],
[__('完成支付', 'aksrt'), (string) ($order['paid_at'] ?? ''), 'success'],
[__('标记退款', 'aksrt'), (string) ($order['refunded_at'] ?? ''), 'info'],
[__('订单关闭', 'aksrt'), (string) ($order['cancelled_at'] ?? ''), 'neutral'],
[__('创建订单', 'jio'), (string) ($order['created_at'] ?? ''), 'info'],
[__('完成支付', 'jio'), (string) ($order['paid_at'] ?? ''), 'success'],
[__('标记退款', 'jio'), (string) ($order['refunded_at'] ?? ''), 'info'],
[__('订单关闭', 'jio'), (string) ($order['cancelled_at'] ?? ''), 'neutral'],
];
$expires = (string) ($order['expires_at'] ?? '');
if (($order['status'] ?? '') === DomainModel::ORDER_STATUS_PENDING && $expires !== '') {
$events[] = [__('超时自动关闭', 'aksrt'), get_date_from_gmt($expires, 'Y-m-d H:i:s'), 'warning', true];
$events[] = [__('超时自动关闭', 'jio'), get_date_from_gmt($expires, 'Y-m-d H:i:s'), 'warning', true];
}
NativeUi::card_open(__('订单进度', 'aksrt'));
echo '<ol class="aksrt-orders-timeline">';
NativeUi::card_open(__('订单进度', 'jio'));
echo '<ol class="jio-orders-timeline">';
foreach ($events as $event) {
[$label, $time, $tone] = $event;
if ($time === '' || str_starts_with($time, '0000-00-00')) {
continue;
}
$planned = !empty($event[3]);
echo '<li class="aksrt-orders-timeline__item is-' . esc_attr($tone) . ($planned ? ' is-planned' : '') . '">'
. '<span class="aksrt-orders-timeline__label">' . esc_html($label) . ($planned ? ' <span class="aksrt-native-muted">' . esc_html__('(预计)', 'aksrt') . '</span>' : '') . '</span>'
echo '<li class="jio-orders-timeline__item is-' . esc_attr($tone) . ($planned ? ' is-planned' : '') . '">'
. '<span class="jio-orders-timeline__label">' . esc_html($label) . ($planned ? ' <span class="jio-native-muted">' . esc_html__('(预计)', 'jio') . '</span>' : '') . '</span>'
. '<time>' . esc_html($time) . '</time></li>';
}
echo '</ol>';
@@ -138,22 +138,22 @@ trait PayOrdersDetail {
if ($meta === []) {
return;
}
echo '<details class="aksrt-native-card aksrt-native-collapse">';
echo '<summary><span class="dashicons dashicons-media-code" aria-hidden="true"></span>' . esc_html__('订单快照(原始数据)', 'aksrt') . '</summary>';
echo '<div class="aksrt-native-card__body"><pre class="aksrt-orders-snapshot">'
echo '<details class="jio-native-card jio-native-collapse">';
echo '<summary><span class="dashicons dashicons-media-code" aria-hidden="true"></span>' . esc_html__('订单快照(原始数据)', 'jio') . '</summary>';
echo '<div class="jio-native-card__body"><pre class="jio-orders-snapshot">'
. esc_html((string) wp_json_encode($meta, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT))
. '</pre></div></details>';
}
private function render_detail_actions(string $order_number, string $status): void {
$actions = $this->order_actions_html($order_number, $status, false);
NativeUi::card_open(__('处理订单', 'aksrt'));
NativeUi::card_open(__('处理订单', 'jio'));
if ($actions === '') {
echo '<p class="aksrt-native-muted">' . esc_html__('该订单已结束,没有可执行的操作。', 'aksrt') . '</p>';
echo '<p class="jio-native-muted">' . esc_html__('该订单已结束,没有可执行的操作。', 'jio') . '</p>';
} else {
echo '<div class="aksrt-orders-actions is-stacked">' . $actions . '</div>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo '<div class="jio-orders-actions is-stacked">' . $actions . '</div>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
if ($status === DomainModel::ORDER_STATUS_PAID) {
echo '<p class="aksrt-native-field__hint">' . esc_html__('「标记退款」只改订单状态;款项需在支付渠道后台原路退回。', 'aksrt') . '</p>';
echo '<p class="jio-native-field__hint">' . esc_html__('「标记退款」只改订单状态;款项需在支付渠道后台原路退回。', 'jio') . '</p>';
}
}
NativeUi::card_close();
@@ -163,15 +163,15 @@ trait PayOrdersDetail {
$user_id = (int) ($order['user_id'] ?? 0);
$buyer = is_array($context['buyer'] ?? null) ? $context['buyer'] : [];
NativeUi::card_open(__('买家', 'aksrt'));
echo '<div class="aksrt-orders-buyer">' . NativeUi::user_cell($user_id, __('游客', 'aksrt')) . '</div>';
NativeUi::card_open(__('买家', 'jio'));
echo '<div class="jio-orders-buyer">' . NativeUi::user_cell($user_id, __('游客', 'jio')) . '</div>';
if ($user_id > 0) {
echo '<dl class="aksrt-orders-dl is-compact">';
$this->dl_row(__('用户名', 'aksrt'), esc_html((string) ($buyer['user_login'] ?? '')) ?: '—');
$this->dl_row(__('邮箱', 'aksrt'), esc_html((string) ($buyer['user_email'] ?? '')) ?: '—');
echo '<dl class="jio-orders-dl is-compact">';
$this->dl_row(__('用户名', 'jio'), esc_html((string) ($buyer['user_login'] ?? '')) ?: '—');
$this->dl_row(__('邮箱', 'jio'), esc_html((string) ($buyer['user_email'] ?? '')) ?: '—');
echo '</dl>';
echo '<p><a class="button button-small" href="' . esc_url($this->page_url(self::PAGE_SLUG, ['user_id' => $user_id])) . '">'
. esc_html__('查看该用户全部订单', 'aksrt') . '</a></p>';
. esc_html__('查看该用户全部订单', 'jio') . '</a></p>';
}
NativeUi::card_close();
}
@@ -181,17 +181,17 @@ trait PayOrdersDetail {
$admin_note = (string) ($meta['admin_note'] ?? '');
$customer_note = (string) ($context['customer_note'] ?? '');
NativeUi::card_open(__('备注', 'aksrt'));
NativeUi::card_open(__('备注', 'jio'));
if ($customer_note !== '') {
echo '<div class="aksrt-native-callout"><strong>' . esc_html__('买家备注', 'aksrt') . '</strong><br>' . nl2br(esc_html($customer_note)) . '</div>';
echo '<div class="jio-native-callout"><strong>' . esc_html__('买家备注', 'jio') . '</strong><br>' . nl2br(esc_html($customer_note)) . '</div>';
}
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" class="aksrt-orders-note">';
echo '<input type="hidden" name="action" value="aksrt_pay_update_order_note" />';
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" class="jio-orders-note">';
echo '<input type="hidden" name="action" value="jio_pay_update_order_note" />';
echo '<input type="hidden" name="order_number" value="' . esc_attr($order_number) . '" />';
wp_nonce_field('aksrt_pay_update_order_note_' . $order_number);
echo '<label class="aksrt-native-field"><span class="aksrt-native-field__label">' . esc_html__('运营备注', 'aksrt') . '</span>'
. '<textarea name="admin_note" rows="4" placeholder="' . esc_attr__('仅管理员可见', 'aksrt') . '">' . esc_textarea($admin_note) . '</textarea></label>';
echo '<div class="aksrt-native-form-foot"><button type="submit" class="button">' . esc_html__('保存备注', 'aksrt') . '</button></div>';
wp_nonce_field('jio_pay_update_order_note_' . $order_number);
echo '<label class="jio-native-field"><span class="jio-native-field__label">' . esc_html__('运营备注', 'jio') . '</span>'
. '<textarea name="admin_note" rows="4" placeholder="' . esc_attr__('仅管理员可见', 'jio') . '">' . esc_textarea($admin_note) . '</textarea></label>';
echo '<div class="jio-native-form-foot"><button type="submit" class="button">' . esc_html__('保存备注', 'jio') . '</button></div>';
echo '</form>';
NativeUi::card_close();
}
@@ -209,7 +209,7 @@ trait PayOrdersDetail {
}
private function amount_row(string $label, string $value): void {
echo '<div class="aksrt-orders-amounts__row"><span>' . esc_html($label) . '</span><span>' . esc_html($value) . '</span></div>';
echo '<div class="jio-orders-amounts__row"><span>' . esc_html($label) . '</span><span>' . esc_html($value) . '</span></div>';
}
private function has_amount(string $value): bool {
@@ -218,7 +218,7 @@ trait PayOrdersDetail {
private function format_amount(array $order, string $value): string {
if ($this->is_points_order($order)) {
return max(0, (int) $value) . ' ' . __('积分', 'aksrt');
return max(0, (int) $value) . ' ' . __('积分', 'jio');
}
return $this->cash_amount($value, (string) ($order['currency'] ?? ''));
}
+66 -66
View File
@@ -4,13 +4,13 @@
*
* 作为 flat sibling trait 被 PayOrders 使用(PSR-4:trait 必须与宿主类同目录)。
*
* @package AKSRT
* @package Jio
*/
namespace AKSRT\Admin\Native;
namespace Jio\Admin\Native;
use AKSRT\Core\DomainModel;
use AKSRT\Payment\OrderRepository;
use Jio\Core\DomainModel;
use Jio\Payment\OrderRepository;
defined('ABSPATH') || exit;
@@ -18,8 +18,8 @@ trait PayOrdersList {
private function render_order_list_page(): void {
NativeUi::header(
__('订单明细', 'aksrt'),
__('查看全站付费订单,处理收款确认、关闭订单与退款标记。', 'aksrt'),
__('订单明细', 'jio'),
__('查看全站付费订单,处理收款确认、关闭订单与退款标记。', 'jio'),
'dashicons-list-view'
);
$this->render_flash();
@@ -55,17 +55,17 @@ trait PayOrdersList {
$this->render_filter_card($filters);
$row_args = $query_args + ($page_num > 1 ? ['paged' => $page_num] : []);
NativeUi::table_card_open(__('订单列表', 'aksrt'), $this->cleanup_form_html());
echo '<table class="wp-list-table widefat striped aksrt-orders-table">';
echo '<caption class="screen-reader-text">' . esc_html__('订单列表', 'aksrt') . '</caption><thead><tr>';
NativeUi::table_card_open(__('订单列表', 'jio'), $this->cleanup_form_html());
echo '<table class="wp-list-table widefat striped jio-orders-table">';
echo '<caption class="screen-reader-text">' . esc_html__('订单列表', 'jio') . '</caption><thead><tr>';
foreach ([
'order' => __('订单号 / 下单时间', 'aksrt'),
'user' => __('用户', 'aksrt'),
'product' => __('商品', 'aksrt'),
'amount' => __('金额 / 支付方式', 'aksrt'),
'status' => __('状态', 'aksrt'),
'paid' => __('支付时间', 'aksrt'),
'actions' => __('操作', 'aksrt'),
'order' => __('订单号 / 下单时间', 'jio'),
'user' => __('用户', 'jio'),
'product' => __('商品', 'jio'),
'amount' => __('金额 / 支付方式', 'jio'),
'status' => __('状态', 'jio'),
'paid' => __('支付时间', 'jio'),
'actions' => __('操作', 'jio'),
] as $key => $label) {
echo '<th scope="col" class="column-' . esc_attr($key) . '">' . esc_html($label) . '</th>';
}
@@ -73,9 +73,9 @@ trait PayOrdersList {
if (!$items) {
$filtered = array_diff_key($query_args, ['status' => true]) !== [];
echo NativeUi::empty_row(7, __('暂无订单', 'aksrt'), $filtered // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
? __('没有符合当前筛选条件的订单,请调整或重置筛选。', 'aksrt')
: __('用户下单后,订单会出现在这里。', 'aksrt'));
echo NativeUi::empty_row(7, __('暂无订单', 'jio'), $filtered // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
? __('没有符合当前筛选条件的订单,请调整或重置筛选。', 'jio')
: __('用户下单后,订单会出现在这里。', 'jio'));
}
foreach ($items as $row) {
$this->render_order_row($row, $row_args);
@@ -98,18 +98,18 @@ trait PayOrdersList {
$detail_url = $this->page_url(self::PAGE_SLUG, ['order' => $order_number] + $row_args);
echo '<tr>';
echo '<td class="column-order"><a class="aksrt-orders-number" href="' . esc_url($detail_url) . '">' . esc_html($order_number) . '</a>'
. '<div class="aksrt-orders-sub">' . esc_html((string) ($row['created_at'] ?? '')) . '</div></td>';
echo '<td class="column-user">' . NativeUi::user_cell((int) ($row['user_id'] ?? 0), __('游客', 'aksrt')) . '</td>';
echo '<td class="column-product"><span class="aksrt-orders-product">' . esc_html($title !== '' ? $title : $type_label) . '</span>'
. '<div class="aksrt-orders-sub">' . esc_html($type_label . ($product_id > 0 ? ' · #' . $product_id : '')) . '</div></td>';
echo '<td class="column-amount"><strong class="aksrt-native-amount">' . esc_html($this->order_amount($row)) . '</strong>'
. '<div class="aksrt-orders-sub">' . esc_html($this->payment_method_label((string) ($row['payment_method'] ?? ''))) . '</div></td>';
echo '<td class="column-order"><a class="jio-orders-number" href="' . esc_url($detail_url) . '">' . esc_html($order_number) . '</a>'
. '<div class="jio-orders-sub">' . esc_html((string) ($row['created_at'] ?? '')) . '</div></td>';
echo '<td class="column-user">' . NativeUi::user_cell((int) ($row['user_id'] ?? 0), __('游客', 'jio')) . '</td>';
echo '<td class="column-product"><span class="jio-orders-product">' . esc_html($title !== '' ? $title : $type_label) . '</span>'
. '<div class="jio-orders-sub">' . esc_html($type_label . ($product_id > 0 ? ' · #' . $product_id : '')) . '</div></td>';
echo '<td class="column-amount"><strong class="jio-native-amount">' . esc_html($this->order_amount($row)) . '</strong>'
. '<div class="jio-orders-sub">' . esc_html($this->payment_method_label((string) ($row['payment_method'] ?? ''))) . '</div></td>';
echo '<td class="column-status">' . NativeUi::status_badge($status, $this->status_badge_map()) . $this->expiry_hint($row) . '</td>';
$paid_at = (string) ($row['paid_at'] ?? '');
echo '<td class="column-paid">' . ($paid_at !== '' ? esc_html($paid_at) : '<span class="aksrt-native-muted">—</span>') . '</td>';
echo '<td class="column-actions"><div class="aksrt-orders-actions">'
. '<a class="button button-small" href="' . esc_url($detail_url) . '">' . esc_html__('详情', 'aksrt') . '</a>'
echo '<td class="column-paid">' . ($paid_at !== '' ? esc_html($paid_at) : '<span class="jio-native-muted">—</span>') . '</td>';
echo '<td class="column-actions"><div class="jio-orders-actions">'
. '<a class="button button-small" href="' . esc_url($detail_url) . '">' . esc_html__('详情', 'jio') . '</a>'
. $this->order_actions_html($order_number, $status)
. '</div></td>';
echo '</tr>';
@@ -121,9 +121,9 @@ trait PayOrdersList {
if (($order['status'] ?? '') !== DomainModel::ORDER_STATUS_PENDING || $expires === '') {
return '';
}
return '<div class="aksrt-orders-sub">' . esc_html(sprintf(
return '<div class="jio-orders-sub">' . esc_html(sprintf(
/* translators: %s local expiry time */
__('%s 前未支付将自动关闭', 'aksrt'),
__('%s 前未支付将自动关闭', 'jio'),
get_date_from_gmt($expires, 'm-d H:i')
)) . '</div>';
}
@@ -135,16 +135,16 @@ trait PayOrdersList {
*/
private function render_status_views(array $filters, array $query_args, array $counts): void {
$base = array_diff_key($query_args, ['status' => true]);
$views = ['' => [__('全部', 'aksrt'), array_sum($counts)]];
$views = ['' => [__('全部', 'jio'), array_sum($counts)]];
foreach ($this->status_labels() as $key => $label) {
$views[$key] = [$label, (int) ($counts[$key] ?? 0)];
}
echo '<nav class="aksrt-native-tabs aksrt-orders-views" aria-label="' . esc_attr__('按订单状态查看', 'aksrt') . '">';
echo '<nav class="jio-native-tabs jio-orders-views" aria-label="' . esc_attr__('按订单状态查看', 'jio') . '">';
foreach ($views as $key => [$label, $count]) {
$url = $this->page_url(self::PAGE_SLUG, $base + ($key !== '' ? ['status' => $key] : []));
$active = $filters['status'] === $key;
echo '<a class="aksrt-native-tabs__link' . ($active ? ' is-active' : '') . '" href="' . esc_url($url) . '"' . ($active ? ' aria-current="page"' : '') . '>'
echo '<a class="jio-native-tabs__link' . ($active ? ' is-active' : '') . '" href="' . esc_url($url) . '"' . ($active ? ' aria-current="page"' : '') . '>'
. esc_html($label) . ' <span class="count">' . esc_html(number_format_i18n($count)) . '</span></a>';
}
echo '</nav>';
@@ -152,8 +152,8 @@ trait PayOrdersList {
/** @param array<string, mixed> $filters */
private function render_filter_card(array $filters): void {
NativeUi::card_open('', '', false, 'aksrt-orders-filter-card');
echo '<form method="get" class="aksrt-orders-filters" role="search">';
NativeUi::card_open('', '', false, 'jio-orders-filter-card');
echo '<form method="get" class="jio-orders-filters" role="search">';
echo '<input type="hidden" name="page" value="' . esc_attr(self::PAGE_SLUG) . '" />';
if ($filters['status'] !== '') {
echo '<input type="hidden" name="status" value="' . esc_attr($filters['status']) . '" />';
@@ -162,21 +162,21 @@ trait PayOrdersList {
echo '<input type="hidden" name="product_id" value="' . esc_attr((string) $filters['product_id']) . '" />';
}
echo '<label class="aksrt-orders-filters__search"><span class="screen-reader-text">' . esc_html__('搜索订单', 'aksrt') . '</span>'
. '<input type="search" name="s" value="' . esc_attr($filters['s']) . '" placeholder="' . esc_attr__('订单号 / 用户名 / 邮箱 / 商品名', 'aksrt') . '" /></label>';
echo $this->filter_select('product_type', __('商品类型', 'aksrt'), $this->type_options(), $filters['product_type']); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo $this->filter_select('payment_method', __('支付方式', 'aksrt'), $this->method_options(), $filters['payment_method']); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo '<span class="aksrt-orders-filters__range">'
. '<label><span class="screen-reader-text">' . esc_html__('开始日期', 'aksrt') . '</span><input type="date" name="date_from" value="' . esc_attr($filters['date_from']) . '" /></label>'
echo '<label class="jio-orders-filters__search"><span class="screen-reader-text">' . esc_html__('搜索订单', 'jio') . '</span>'
. '<input type="search" name="s" value="' . esc_attr($filters['s']) . '" placeholder="' . esc_attr__('订单号 / 用户名 / 邮箱 / 商品名', 'jio') . '" /></label>';
echo $this->filter_select('product_type', __('商品类型', 'jio'), $this->type_options(), $filters['product_type']); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo $this->filter_select('payment_method', __('支付方式', 'jio'), $this->method_options(), $filters['payment_method']); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo '<span class="jio-orders-filters__range">'
. '<label><span class="screen-reader-text">' . esc_html__('开始日期', 'jio') . '</span><input type="date" name="date_from" value="' . esc_attr($filters['date_from']) . '" /></label>'
. '<span aria-hidden="true">–</span>'
. '<label><span class="screen-reader-text">' . esc_html__('结束日期', 'aksrt') . '</span><input type="date" name="date_to" value="' . esc_attr($filters['date_to']) . '" /></label>'
. '<label><span class="screen-reader-text">' . esc_html__('结束日期', 'jio') . '</span><input type="date" name="date_to" value="' . esc_attr($filters['date_to']) . '" /></label>'
. '</span>';
echo '<label><span class="screen-reader-text">' . esc_html__('用户 ID', 'aksrt') . '</span><input type="number" min="1" name="user_id" class="aksrt-orders-filters__user" value="'
. esc_attr($filters['user_id'] > 0 ? (string) $filters['user_id'] : '') . '" placeholder="' . esc_attr__('用户 ID', 'aksrt') . '" /></label>';
echo '<span class="aksrt-orders-filters__submit"><button type="submit" class="button button-primary">' . esc_html__('筛选', 'aksrt') . '</button>';
echo '<label><span class="screen-reader-text">' . esc_html__('用户 ID', 'jio') . '</span><input type="number" min="1" name="user_id" class="jio-orders-filters__user" value="'
. esc_attr($filters['user_id'] > 0 ? (string) $filters['user_id'] : '') . '" placeholder="' . esc_attr__('用户 ID', 'jio') . '" /></label>';
echo '<span class="jio-orders-filters__submit"><button type="submit" class="button button-primary">' . esc_html__('筛选', 'jio') . '</button>';
if (array_diff_key($this->list_query_args($filters), ['status' => true]) !== []) {
$reset = $this->page_url(self::PAGE_SLUG, $filters['status'] !== '' ? ['status' => $filters['status']] : []);
echo '<a class="button" href="' . esc_url($reset) . '">' . esc_html__('重置', 'aksrt') . '</a>';
echo '<a class="button" href="' . esc_url($reset) . '">' . esc_html__('重置', 'jio') . '</a>';
}
echo '</span></form>';
@@ -185,9 +185,9 @@ trait PayOrdersList {
if ($filters['product_id'] > 0) {
$remove = $this->page_url(self::PAGE_SLUG, array_diff_key($this->list_query_args($filters), ['product_id' => true]));
$title = (string) get_the_title($filters['product_id']);
echo '<p class="aksrt-orders-chips"><span class="aksrt-orders-chip">'
. esc_html(sprintf(__('商品:%s', 'aksrt'), $title !== '' ? $title . ' #' . $filters['product_id'] : '#' . $filters['product_id']))
. '<a href="' . esc_url($remove) . '" aria-label="' . esc_attr__('移除商品筛选', 'aksrt') . '"><span class="dashicons dashicons-no-alt" aria-hidden="true"></span></a></span></p>';
echo '<p class="jio-orders-chips"><span class="jio-orders-chip">'
. esc_html(sprintf(__('商品:%s', 'jio'), $title !== '' ? $title . ' #' . $filters['product_id'] : '#' . $filters['product_id']))
. '<a href="' . esc_url($remove) . '" aria-label="' . esc_attr__('移除商品筛选', 'jio') . '"><span class="dashicons dashicons-no-alt" aria-hidden="true"></span></a></span></p>';
}
NativeUi::card_close();
}
@@ -203,11 +203,11 @@ trait PayOrdersList {
private function cleanup_form_html(): string {
return $this->action_form(
'aksrt_pay_clear_closed_orders',
'aksrt_pay_clear_closed_orders',
'jio_pay_clear_closed_orders',
'jio_pay_clear_closed_orders',
['days' => '14'],
__('清理 14 天前关闭订单', 'aksrt'),
__('将永久删除 14 天前关闭的全部订单,不可恢复。确认清理?', 'aksrt'),
__('清理 14 天前关闭订单', 'jio'),
__('将永久删除 14 天前关闭的全部订单,不可恢复。确认清理?', 'jio'),
true
);
}
@@ -215,24 +215,24 @@ trait PayOrdersList {
/** @return array<string, string> */
private function type_options(): array {
return [
'' => __('全部类型', 'aksrt'),
DomainModel::ORDER_PRODUCT_READING => __('付费阅读', 'aksrt'),
DomainModel::ORDER_PRODUCT_DOWNLOAD => __('付费下载', 'aksrt'),
DomainModel::ORDER_PRODUCT_IMAGE => __('付费图片', 'aksrt'),
DomainModel::ORDER_PRODUCT_VIDEO => __('付费视频', 'aksrt'),
DomainModel::ORDER_PRODUCT_VIP => __('购买会员', 'aksrt'),
DomainModel::ORDER_PRODUCT_RECHARGE => __('余额充值', 'aksrt'),
DomainModel::ORDER_PRODUCT_POINTS => __('购买积分', 'aksrt'),
'' => __('全部类型', 'jio'),
DomainModel::ORDER_PRODUCT_READING => __('付费阅读', 'jio'),
DomainModel::ORDER_PRODUCT_DOWNLOAD => __('付费下载', 'jio'),
DomainModel::ORDER_PRODUCT_IMAGE => __('付费图片', 'jio'),
DomainModel::ORDER_PRODUCT_VIDEO => __('付费视频', 'jio'),
DomainModel::ORDER_PRODUCT_VIP => __('购买会员', 'jio'),
DomainModel::ORDER_PRODUCT_RECHARGE => __('余额充值', 'jio'),
DomainModel::ORDER_PRODUCT_POINTS => __('购买积分', 'jio'),
];
}
/** @return array<string, string> */
private function method_options(): array {
return [
'' => __('支付方式', 'aksrt'),
'balance' => __('余额', 'aksrt'),
'points' => __('积分', 'aksrt'),
'gateway' => __('第三方网关支付', 'aksrt'),
'' => __('支付方式', 'jio'),
'balance' => __('余额', 'jio'),
'points' => __('积分', 'jio'),
'gateway' => __('第三方网关支付', 'jio'),
];
}
+118 -118
View File
@@ -2,25 +2,25 @@
/**
* 支付中心「概览」:今日实时、区间指标卡、成交趋势、分类成交、支付方式、转化与退款、热销排行、待办与快捷入口。
*
* 统计口径见 \AKSRT\Payment\OrderAnalytics。首屏报表(近 30 天)随页面输出为 JSON,
* 之后切换区间 / 分类走 REST(aksrt/v1/admin/pay-overview,见 \AKSRT\Admin\PayOverviewController)。
* 统计口径见 \Jio\Payment\OrderAnalytics。首屏报表(近 30 天)随页面输出为 JSON,
* 之后切换区间 / 分类走 REST(jio/v1/admin/pay-overview,见 \Jio\Admin\PayOverviewController)。
* 图表用按需打包的 ECharts(assets/js/runtime/libs/echarts-pay.min.js,tools/build-pay-charts.mjs 生成),
* 绘制逻辑在 assets/js/admin-pay-overview.js,样式在 assets/css/admin/pay-overview.css。
*
* @package AKSRT
* @package Jio
*/
namespace AKSRT\Admin\Native;
namespace Jio\Admin\Native;
use AKSRT\Payment\OrderAnalytics;
use AKSRT\Payment\OrderAnalyticsDaily;
use Jio\Payment\OrderAnalytics;
use Jio\Payment\OrderAnalyticsDaily;
defined('ABSPATH') || exit;
final class PayOverview {
private const DEFAULT_DAYS = 30;
private const DATA_ID = 'aksrt-pay-overview-data';
private const DATA_ID = 'jio-pay-overview-data';
private static $instance = null;
@@ -43,14 +43,14 @@ final class PayOverview {
$lib = '/assets/js/runtime/libs/echarts-pay.min.js';
$css = '/assets/css/admin/pay-overview.css';
$js = '/assets/js/admin-pay-overview.js';
wp_enqueue_style('aksrt-admin-pay-overview', AKSRT_URI . $css, [NativeUi::STYLE_HANDLE], (string) filemtime(AKSRT_DIR . $css));
wp_enqueue_script('aksrt-echarts-pay', AKSRT_URI . $lib, [], (string) filemtime(AKSRT_DIR . $lib), true);
wp_enqueue_script('aksrt-admin-pay-overview', AKSRT_URI . $js, ['aksrt-echarts-pay'], (string) filemtime(AKSRT_DIR . $js), true);
wp_enqueue_style('jio-admin-pay-overview', JIO_URI . $css, [NativeUi::STYLE_HANDLE], (string) filemtime(JIO_DIR . $css));
wp_enqueue_script('jio-echarts-pay', JIO_URI . $lib, [], (string) filemtime(JIO_DIR . $lib), true);
wp_enqueue_script('jio-admin-pay-overview', JIO_URI . $js, ['jio-echarts-pay'], (string) filemtime(JIO_DIR . $js), true);
}
public function render_page(): void {
if (!current_user_can(PayMenus::CAPABILITY)) {
wp_die(esc_html__('您没有权限访问此页面。', 'aksrt'));
wp_die(esc_html__('您没有权限访问此页面。', 'jio'));
}
$analytics = OrderAnalytics::get_instance();
@@ -60,35 +60,35 @@ final class PayOverview {
$realtime = $analytics->today_vs_yesterday();
$symbol = $this->currency_symbol();
NativeUi::page_open('aksrt-po-page');
NativeUi::page_open('jio-po-page');
NativeUi::header(
__('支付中心', 'aksrt'),
__('成交走势、收入结构与待办一览。收款渠道、价格策略等配置在主题设置中修改。', 'aksrt'),
__('支付中心', 'jio'),
__('成交走势、收入结构与待办一览。收款渠道、价格策略等配置在主题设置中修改。', 'jio'),
'dashicons-cart',
[
['label' => __('订单明细', 'aksrt'), 'url' => $this->page_url('aksrt_orders'), 'icon' => 'dashicons-list-view'],
['label' => __('支付配置', 'aksrt'), 'url' => admin_url('admin.php?page=aksrt-settings#/payment/gateway'), 'icon' => 'dashicons-admin-generic', 'primary' => true],
['label' => __('订单明细', 'jio'), 'url' => $this->page_url('jio_orders'), 'icon' => 'dashicons-list-view'],
['label' => __('支付配置', 'jio'), 'url' => admin_url('admin.php?page=jio-settings#/payment/gateway'), 'icon' => 'dashicons-admin-generic', 'primary' => true],
]
);
$this->render_realtime($realtime, $symbol);
$this->render_toolbar($from, $today);
echo '<div class="aksrt-po-notice" data-aksrt-po-excluded hidden></div>';
echo '<div class="aksrt-po-error notice notice-error" data-aksrt-po-error hidden><p></p></div>';
echo '<div class="jio-po-notice" data-jio-po-excluded hidden></div>';
echo '<div class="jio-po-error notice notice-error" data-jio-po-error hidden><p></p></div>';
$this->render_kpis();
echo '<div class="aksrt-po-body" data-aksrt-po-body>';
echo '<div class="aksrt-po-grid aksrt-po-grid--main">';
$this->chart_card('trend', __('成交趋势', 'aksrt'), __('支付金额、净收入、退款金额曲线与支付订单数折线', 'aksrt'), 'lg');
echo '<div class="jio-po-body" data-jio-po-body>';
echo '<div class="jio-po-grid jio-po-grid--main">';
$this->chart_card('trend', __('成交趋势', 'jio'), __('支付金额、净收入、退款金额曲线与支付订单数折线', 'jio'), 'lg');
$this->render_funnel_card();
echo '</div>';
echo '<div class="aksrt-po-grid aksrt-po-grid--main">';
$this->chart_card('groups', __('分类成交', 'aksrt'), __('按商品分类的支付金额曲线', 'aksrt'), 'md');
$this->chart_card('methods', __('支付方式', 'aksrt'), __('按支付金额的支付方式占比', 'aksrt'), 'md');
echo '<div class="jio-po-grid jio-po-grid--main">';
$this->chart_card('groups', __('分类成交', 'jio'), __('按商品分类的支付金额曲线', 'jio'), 'md');
$this->chart_card('methods', __('支付方式', 'jio'), __('按支付金额的支付方式占比', 'jio'), 'md');
echo '</div>';
echo '<div class="aksrt-po-grid aksrt-po-grid--main">';
echo '<div class="jio-po-grid jio-po-grid--main">';
$this->render_top_card();
$this->render_todo_card();
echo '</div>';
@@ -100,23 +100,23 @@ final class PayOverview {
'currency' => $symbol,
'today' => $today,
'maxDays' => OrderAnalytics::MAX_RANGE_DAYS,
'rest' => ['url' => rest_url('aksrt/v1/admin/pay-overview'), 'nonce' => wp_create_nonce('wp_rest')],
'rest' => ['url' => rest_url('jio/v1/admin/pay-overview'), 'nonce' => wp_create_nonce('wp_rest')],
'initial' => ['from' => $from, 'to' => $today, 'group' => '', 'report' => $report],
'i18n' => [
'paid' => __('支付金额', 'aksrt'),
'net' => __('净收入', 'aksrt'),
'refund' => __('退款金额', 'aksrt'),
'count' => __('支付订单数', 'aksrt'),
'unit' => __('笔', 'aksrt'),
'people' => __('人', 'aksrt'),
'empty' => __('所选区间暂无成交', 'aksrt'),
'loadError' => __('统计数据加载失败,请稍后重试。', 'aksrt'),
'basis' => __('较上一周期', 'aksrt'),
'new' => __('新增', 'aksrt'),
'total' => __('合计', 'aksrt'),
'excluded' => __('以下外币订单未计入统计:', 'aksrt'),
'noTop' => __('所选区间暂无付费内容成交', 'aksrt'),
'badRange' => __('请选择有效的日期区间。', 'aksrt'),
'paid' => __('支付金额', 'jio'),
'net' => __('净收入', 'jio'),
'refund' => __('退款金额', 'jio'),
'count' => __('支付订单数', 'jio'),
'unit' => __('笔', 'jio'),
'people' => __('人', 'jio'),
'empty' => __('所选区间暂无成交', 'jio'),
'loadError' => __('统计数据加载失败,请稍后重试。', 'jio'),
'basis' => __('较上一周期', 'jio'),
'new' => __('新增', 'jio'),
'total' => __('合计', 'jio'),
'excluded' => __('以下外币订单未计入统计:', 'jio'),
'noTop' => __('所选区间暂无付费内容成交', 'jio'),
'badRange' => __('请选择有效的日期区间。', 'jio'),
],
];
echo '<script type="application/json" id="' . esc_attr(self::DATA_ID) . '">'
@@ -134,17 +134,17 @@ final class PayOverview {
private function render_realtime(array $data, string $symbol): void {
$amount = (float) $data['today']['amount'];
$prev = (float) $data['yesterday']['amount'];
echo '<section class="aksrt-po-live" aria-label="' . esc_attr__('今日实时', 'aksrt') . '">';
echo '<div class="aksrt-po-live__main"><span class="aksrt-po-live__dot" aria-hidden="true"></span>'
. '<span class="aksrt-po-live__label">' . esc_html__('今日支付金额', 'aksrt') . '</span>'
. '<strong class="aksrt-po-live__value">' . esc_html($symbol . number_format_i18n($amount, 2)) . '</strong>'
echo '<section class="jio-po-live" aria-label="' . esc_attr__('今日实时', 'jio') . '">';
echo '<div class="jio-po-live__main"><span class="jio-po-live__dot" aria-hidden="true"></span>'
. '<span class="jio-po-live__label">' . esc_html__('今日支付金额', 'jio') . '</span>'
. '<strong class="jio-po-live__value">' . esc_html($symbol . number_format_i18n($amount, 2)) . '</strong>'
. $this->delta_html($amount, $prev) . '</div>';
echo '<div class="aksrt-po-live__meta">';
echo '<div class="jio-po-live__meta">';
/* translators: %d order count */
echo '<span>' . esc_html(sprintf(__('今日 %d 笔', 'aksrt'), (int) $data['today']['count'])) . '</span>';
echo '<span>' . esc_html(sprintf(__('今日 %d 笔', 'jio'), (int) $data['today']['count'])) . '</span>';
echo '<span>' . esc_html(sprintf(
/* translators: 1 time 2 amount 3 order count */
__('昨日 00:00–%1$s:%2$s,%3$d 笔', 'aksrt'),
__('昨日 00:00–%1$s:%2$s,%3$d 笔', 'jio'),
$data['until'],
$symbol . number_format_i18n($prev, 2),
(int) $data['yesterday']['count']
@@ -154,16 +154,16 @@ final class PayOverview {
/** 同时段环比徽标(服务端渲染,仅今日实时使用;区间指标的环比由前端计算)。 */
private function delta_html(float $current, float $previous): string {
$basis = __('较昨日同时段', 'aksrt');
$basis = __('较昨日同时段', 'jio');
if ($previous <= 0) {
$text = $current > 0 ? __('新增', 'aksrt') : '—';
$text = $current > 0 ? __('新增', 'jio') : '—';
$class = $current > 0 ? 'is-up' : 'is-flat';
} else {
$delta = round(($current - $previous) / $previous * 100, 1);
$class = $delta > 0 ? 'is-up' : ($delta < 0 ? 'is-down' : 'is-flat');
$text = ($delta > 0 ? '↑' : ($delta < 0 ? '↓' : '')) . number_format_i18n(abs($delta), 1) . '%';
}
return '<span class="aksrt-po-delta ' . esc_attr($class) . '">' . esc_html($basis . ' ' . $text) . '</span>';
return '<span class="jio-po-delta ' . esc_attr($class) . '">' . esc_html($basis . ' ' . $text) . '</span>';
}
/* ------------------------------------------------------------------
@@ -172,54 +172,54 @@ final class PayOverview {
private function render_toolbar(string $from, string $to): void {
$presets = [
'd7' => __('近 7 天', 'aksrt'),
'd30' => __('近 30 天', 'aksrt'),
'd90' => __('近 90 天', 'aksrt'),
'this_month' => __('本月', 'aksrt'),
'last_month' => __('上月', 'aksrt'),
'd365' => __('近一年', 'aksrt'),
'd7' => __('近 7 天', 'jio'),
'd30' => __('近 30 天', 'jio'),
'd90' => __('近 90 天', 'jio'),
'this_month' => __('本月', 'jio'),
'last_month' => __('上月', 'jio'),
'd365' => __('近一年', 'jio'),
];
echo '<div class="aksrt-po-toolbar">';
echo '<div class="aksrt-po-seg" role="group" data-aksrt-po-presets aria-label="' . esc_attr__('统计区间', 'aksrt') . '">';
echo '<div class="jio-po-toolbar">';
echo '<div class="jio-po-seg" role="group" data-jio-po-presets aria-label="' . esc_attr__('统计区间', 'jio') . '">';
foreach ($presets as $key => $label) {
echo '<button type="button" data-preset="' . esc_attr($key) . '" aria-pressed="' . ($key === 'd30' ? 'true' : 'false') . '">' . esc_html($label) . '</button>';
}
echo '</div>';
echo '<form class="aksrt-po-range" data-aksrt-po-range>';
echo '<label><span class="screen-reader-text">' . esc_html__('开始日期', 'aksrt') . '</span>'
echo '<form class="jio-po-range" data-jio-po-range>';
echo '<label><span class="screen-reader-text">' . esc_html__('开始日期', 'jio') . '</span>'
. '<input type="date" name="from" value="' . esc_attr($from) . '" max="' . esc_attr($to) . '" required></label>';
echo '<span aria-hidden="true">–</span>';
echo '<label><span class="screen-reader-text">' . esc_html__('结束日期', 'aksrt') . '</span>'
echo '<label><span class="screen-reader-text">' . esc_html__('结束日期', 'jio') . '</span>'
. '<input type="date" name="to" value="' . esc_attr($to) . '" max="' . esc_attr($to) . '" required></label>';
echo '<label><span class="screen-reader-text">' . esc_html__('商品分类', 'aksrt') . '</span><select name="group">'
. '<option value="">' . esc_html__('全部分类', 'aksrt') . '</option>';
echo '<label><span class="screen-reader-text">' . esc_html__('商品分类', 'jio') . '</span><select name="group">'
. '<option value="">' . esc_html__('全部分类', 'jio') . '</option>';
foreach (OrderAnalytics::group_labels() as $key => $label) {
echo '<option value="' . esc_attr($key) . '">' . esc_html($label) . '</option>';
}
echo '</select></label>';
echo '<button type="submit" class="button">' . esc_html__('查询', 'aksrt') . '</button>';
echo '<button type="submit" class="button">' . esc_html__('查询', 'jio') . '</button>';
echo '</form>';
echo '<span class="aksrt-po-range-label" data-aksrt-po-range-label aria-live="polite"></span>';
echo '<span class="jio-po-range-label" data-jio-po-range-label aria-live="polite"></span>';
echo '</div>';
}
private function render_kpis(): void {
$cards = [
['paid_amount', __('支付金额', 'aksrt'), 'success'],
['net_amount', __('净收入', 'aksrt'), 'info'],
['paid_count', __('支付订单数', 'aksrt'), 'primary'],
['buyers', __('支付人数', 'aksrt'), 'primary'],
['avg_amount', __('客单价', 'aksrt'), 'warning'],
['refund_amount', __('退款金额', 'aksrt'), 'danger'],
['paid_amount', __('支付金额', 'jio'), 'success'],
['net_amount', __('净收入', 'jio'), 'info'],
['paid_count', __('支付订单数', 'jio'), 'primary'],
['buyers', __('支付人数', 'jio'), 'primary'],
['avg_amount', __('客单价', 'jio'), 'warning'],
['refund_amount', __('退款金额', 'jio'), 'danger'],
];
echo '<div class="aksrt-po-kpis" data-aksrt-po-kpis>';
echo '<div class="jio-po-kpis" data-jio-po-kpis>';
foreach ($cards as [$key, $label, $tone]) {
echo '<div class="aksrt-po-kpi aksrt-po-kpi--' . esc_attr($tone) . '" data-kpi="' . esc_attr($key) . '">'
. '<span class="aksrt-po-kpi__label">' . esc_html($label) . '</span>'
. '<span class="aksrt-po-kpi__value" data-kpi-value>—</span>'
. '<span class="aksrt-po-kpi__foot"><span class="aksrt-po-delta is-flat" data-kpi-delta>—</span>'
. '<span class="aksrt-po-kpi__meta" data-kpi-meta></span></span>'
echo '<div class="jio-po-kpi jio-po-kpi--' . esc_attr($tone) . '" data-kpi="' . esc_attr($key) . '">'
. '<span class="jio-po-kpi__label">' . esc_html($label) . '</span>'
. '<span class="jio-po-kpi__value" data-kpi-value>—</span>'
. '<span class="jio-po-kpi__foot"><span class="jio-po-delta is-flat" data-kpi-delta>—</span>'
. '<span class="jio-po-kpi__meta" data-kpi-meta></span></span>'
. '</div>';
}
echo '</div>';
@@ -230,65 +230,65 @@ final class PayOverview {
* ----------------------------------------------------------------*/
private function chart_card(string $key, string $title, string $label, string $size): void {
NativeUi::card_open($title, '', false, 'aksrt-po-card');
echo '<div class="aksrt-po-chart aksrt-po-chart--' . esc_attr($size) . '" data-aksrt-po-chart="' . esc_attr($key) . '" role="img" aria-label="' . esc_attr($label) . '"></div>';
NativeUi::card_open($title, '', false, 'jio-po-card');
echo '<div class="jio-po-chart jio-po-chart--' . esc_attr($size) . '" data-jio-po-chart="' . esc_attr($key) . '" role="img" aria-label="' . esc_attr($label) . '"></div>';
NativeUi::card_close();
}
private function render_funnel_card(): void {
$rows = [
['created_count', __('下单数', 'aksrt'), __('区间内创建的订单', 'aksrt')],
['conversion', __('支付转化率', 'aksrt'), __('下单后完成支付的比例', 'aksrt')],
['close_rate', __('超时关闭率', 'aksrt'), __('未支付而关闭的比例', 'aksrt')],
['refund_rate', __('退款率', 'aksrt'), __('退款金额 ÷ 支付金额', 'aksrt')],
['created_count', __('下单数', 'jio'), __('区间内创建的订单', 'jio')],
['conversion', __('支付转化率', 'jio'), __('下单后完成支付的比例', 'jio')],
['close_rate', __('超时关闭率', 'jio'), __('未支付而关闭的比例', 'jio')],
['refund_rate', __('退款率', 'jio'), __('退款金额 ÷ 支付金额', 'jio')],
];
NativeUi::card_open(__('转化与退款', 'aksrt'), '', false, 'aksrt-po-card');
echo '<ul class="aksrt-po-funnel" data-aksrt-po-funnel>';
NativeUi::card_open(__('转化与退款', 'jio'), '', false, 'jio-po-card');
echo '<ul class="jio-po-funnel" data-jio-po-funnel>';
foreach ($rows as [$key, $label, $hint]) {
echo '<li data-metric="' . esc_attr($key) . '"><div class="aksrt-po-funnel__head">'
. '<span class="aksrt-po-funnel__label">' . esc_html($label) . '</span>'
. '<strong class="aksrt-po-funnel__value" data-value>—</strong></div>'
. '<span class="aksrt-po-funnel__bar" aria-hidden="true"><i data-bar></i></span>'
. '<span class="aksrt-po-funnel__hint">' . esc_html($hint) . ' <span data-delta></span></span></li>';
echo '<li data-metric="' . esc_attr($key) . '"><div class="jio-po-funnel__head">'
. '<span class="jio-po-funnel__label">' . esc_html($label) . '</span>'
. '<strong class="jio-po-funnel__value" data-value>—</strong></div>'
. '<span class="jio-po-funnel__bar" aria-hidden="true"><i data-bar></i></span>'
. '<span class="jio-po-funnel__hint">' . esc_html($hint) . ' <span data-delta></span></span></li>';
}
echo '</ul>';
NativeUi::card_close();
}
private function render_top_card(): void {
NativeUi::card_open(__('付费内容热销 TOP 10', 'aksrt'), '', true, 'aksrt-po-card');
echo '<div class="aksrt-native-table-wrap"><table class="widefat aksrt-po-top">'
. '<caption class="screen-reader-text">' . esc_html__('付费内容热销排行', 'aksrt') . '</caption>'
. '<thead><tr><th scope="col" class="column-rank">#</th><th scope="col">' . esc_html__('文章', 'aksrt') . '</th>'
. '<th scope="col" class="column-num">' . esc_html__('销量', 'aksrt') . '</th>'
. '<th scope="col" class="column-num">' . esc_html__('支付金额', 'aksrt') . '</th>'
. '<th scope="col" class="column-share">' . esc_html__('占比', 'aksrt') . '</th></tr></thead>'
. '<tbody data-aksrt-po-top></tbody></table></div>';
NativeUi::card_open(__('付费内容热销 TOP 10', 'jio'), '', true, 'jio-po-card');
echo '<div class="jio-native-table-wrap"><table class="widefat jio-po-top">'
. '<caption class="screen-reader-text">' . esc_html__('付费内容热销排行', 'jio') . '</caption>'
. '<thead><tr><th scope="col" class="column-rank">#</th><th scope="col">' . esc_html__('文章', 'jio') . '</th>'
. '<th scope="col" class="column-num">' . esc_html__('销量', 'jio') . '</th>'
. '<th scope="col" class="column-num">' . esc_html__('支付金额', 'jio') . '</th>'
. '<th scope="col" class="column-share">' . esc_html__('占比', 'jio') . '</th></tr></thead>'
. '<tbody data-jio-po-top></tbody></table></div>';
NativeUi::card_close();
}
private function render_todo_card(): void {
$orders = class_exists('\AKSRT\Payment\OrderRepository') ? \AKSRT\Payment\OrderRepository::get_instance()->get_orders_status_counts() : [];
$withdraw = class_exists('\AKSRT\Payment\Withdrawal') ? \AKSRT\Payment\Withdrawal::get_instance()->get_stats() : [];
$income = class_exists('\AKSRT\Payment\Revenue') ? \AKSRT\Payment\Revenue::get_instance()->get_stats() : [];
$cards = class_exists('\AKSRT\Payment\ChargeCard') ? \AKSRT\Payment\ChargeCard::get_instance()->get_stats() : [];
$orders = class_exists('\Jio\Payment\OrderRepository') ? \Jio\Payment\OrderRepository::get_instance()->get_orders_status_counts() : [];
$withdraw = class_exists('\Jio\Payment\Withdrawal') ? \Jio\Payment\Withdrawal::get_instance()->get_stats() : [];
$income = class_exists('\Jio\Payment\Revenue') ? \Jio\Payment\Revenue::get_instance()->get_stats() : [];
$cards = class_exists('\Jio\Payment\ChargeCard') ? \Jio\Payment\ChargeCard::get_instance()->get_stats() : [];
$items = [
[__('待审提现', 'aksrt'), (int) ($withdraw['pending'] ?? 0), $this->page_url('aksrt_withdraw', ['status' => 'pending']), 'dashicons-money-alt', 'danger'],
[__('打款中提现', 'aksrt'), (int) ($withdraw['processing'] ?? 0), $this->page_url('aksrt_withdraw', ['status' => 'processing']), 'dashicons-update', 'warning'],
[__('待结算分成', 'aksrt'), (string) ($income['cash']['pending'] ?? '0.00'), $this->page_url('aksrt_income', ['status' => 'pending']), 'dashicons-chart-pie', 'warning'],
[__('待支付订单', 'aksrt'), (int) ($orders['pending'] ?? 0), $this->page_url('aksrt_orders', ['status' => 'pending']), 'dashicons-clock', 'info'],
[__('可用卡密', 'aksrt'), (int) ($cards['active'] ?? 0), $this->page_url('aksrt_charge_cards', ['status' => 'active']), 'dashicons-id-alt', 'neutral'],
[__('待审提现', 'jio'), (int) ($withdraw['pending'] ?? 0), $this->page_url('jio_withdraw', ['status' => 'pending']), 'dashicons-money-alt', 'danger'],
[__('打款中提现', 'jio'), (int) ($withdraw['processing'] ?? 0), $this->page_url('jio_withdraw', ['status' => 'processing']), 'dashicons-update', 'warning'],
[__('待结算分成', 'jio'), (string) ($income['cash']['pending'] ?? '0.00'), $this->page_url('jio_income', ['status' => 'pending']), 'dashicons-chart-pie', 'warning'],
[__('待支付订单', 'jio'), (int) ($orders['pending'] ?? 0), $this->page_url('jio_orders', ['status' => 'pending']), 'dashicons-clock', 'info'],
[__('可用卡密', 'jio'), (int) ($cards['active'] ?? 0), $this->page_url('jio_charge_cards', ['status' => 'active']), 'dashicons-id-alt', 'neutral'],
];
NativeUi::card_open(__('待办事项', 'aksrt'), '', false, 'aksrt-po-card');
echo '<ul class="aksrt-po-todo">';
NativeUi::card_open(__('待办事项', 'jio'), '', false, 'jio-po-card');
echo '<ul class="jio-po-todo">';
foreach ($items as [$label, $value, $url, $icon, $tone]) {
$idle = (string) $value === '0' || (string) $value === '0.00';
echo '<li><a class="aksrt-po-todo__item aksrt-po-todo__item--' . esc_attr($idle ? 'neutral' : $tone) . '" href="' . esc_url($url) . '">'
. '<span class="aksrt-po-todo__icon dashicons ' . esc_attr($icon) . '" aria-hidden="true"></span>'
. '<span class="aksrt-po-todo__label">' . esc_html($label) . '</span>'
. '<span class="aksrt-po-todo__value">' . esc_html(is_int($value) ? number_format_i18n($value) : $this->currency_symbol() . $value) . '</span>'
echo '<li><a class="jio-po-todo__item jio-po-todo__item--' . esc_attr($idle ? 'neutral' : $tone) . '" href="' . esc_url($url) . '">'
. '<span class="jio-po-todo__icon dashicons ' . esc_attr($icon) . '" aria-hidden="true"></span>'
. '<span class="jio-po-todo__label">' . esc_html($label) . '</span>'
. '<span class="jio-po-todo__value">' . esc_html(is_int($value) ? number_format_i18n($value) : $this->currency_symbol() . $value) . '</span>'
. '</a></li>';
}
echo '</ul>';
@@ -296,12 +296,12 @@ final class PayOverview {
}
private function render_shortcuts_card(): void {
NativeUi::card_open(__('快捷入口', 'aksrt'), '', false, 'aksrt-po-card');
echo '<ul class="aksrt-po-links">';
NativeUi::card_open(__('快捷入口', 'jio'), '', false, 'jio-po-card');
echo '<ul class="jio-po-links">';
foreach (PayMenus::get_instance()->shortcuts() as $item) {
echo '<li><a href="' . esc_url($this->page_url($item['slug'])) . '">'
. '<span class="aksrt-po-links__icon dashicons ' . esc_attr($item['icon']) . '" aria-hidden="true"></span>'
. '<span class="aksrt-po-links__text"><strong>' . esc_html($item['title']) . '</strong>'
. '<span class="jio-po-links__icon dashicons ' . esc_attr($item['icon']) . '" aria-hidden="true"></span>'
. '<span class="jio-po-links__text"><strong>' . esc_html($item['title']) . '</strong>'
. ($item['desc'] !== '' ? '<span>' . esc_html($item['desc']) . '</span>' : '')
. '</span></a></li>';
}
@@ -314,7 +314,7 @@ final class PayOverview {
* ----------------------------------------------------------------*/
private function currency_symbol(): string {
$paid = \AKSRT\Settings\Settings::get_paid_content_settings();
$paid = \Jio\Settings\Settings::get_paid_content_settings();
return (string) ($paid['display']['currency_symbol'] ?? '¥');
}
+24 -24
View File
@@ -10,12 +10,12 @@
* - PayProductsList 列表查询与表格渲染
* - PayProductsEditor 快速设置弹窗与保存
*
* @package AKSRT
* @package Jio
*/
namespace AKSRT\Admin\Native;
namespace Jio\Admin\Native;
use AKSRT\Core\DomainModel;
use Jio\Core\DomainModel;
defined('ABSPATH') || exit;
@@ -25,8 +25,8 @@ class PayProducts {
use PayProductsEditor;
public const CAPABILITY = 'manage_options';
public const PAGE_SLUG = 'aksrt_pay_products';
public const SAVE_ACTION = 'aksrt_pay_save_product';
public const PAGE_SLUG = 'jio_pay_products';
public const SAVE_ACTION = 'jio_pay_save_product';
/** 列表纳入的文章状态。 */
private const LIST_STATUSES = ['publish', 'future', 'draft', 'pending', 'private'];
@@ -74,21 +74,21 @@ class PayProducts {
}
$css = '/assets/css/admin/pay-products.css';
$js = '/assets/js/admin-pay-products.js';
wp_enqueue_style('aksrt-admin-pay-products', AKSRT_URI . $css, [NativeUi::STYLE_HANDLE], (string) filemtime(AKSRT_DIR . $css));
wp_enqueue_script('aksrt-admin-pay-products', AKSRT_URI . $js, [], (string) filemtime(AKSRT_DIR . $js), true);
wp_enqueue_style('jio-admin-pay-products', JIO_URI . $css, [NativeUi::STYLE_HANDLE], (string) filemtime(JIO_DIR . $css));
wp_enqueue_script('jio-admin-pay-products', JIO_URI . $js, [], (string) filemtime(JIO_DIR . $js), true);
}
public function render_products_page(): void {
$this->ensure_cap();
NativeUi::page_open('aksrt-products-page');
NativeUi::page_open('jio-products-page');
NativeUi::header(
__('付费文章', 'aksrt'),
__('管理付费内容的售卖配置。点击「快速设置」即可在本页修改价格、权限与销量,无需进入文章编辑页。', 'aksrt'),
__('付费文章', 'jio'),
__('管理付费内容的售卖配置。点击「快速设置」即可在本页修改价格、权限与销量,无需进入文章编辑页。', 'jio'),
'dashicons-media-document'
);
if (!class_exists('\AKSRT\Payment\OrderRepository')) {
echo '<div class="notice notice-error"><p>' . esc_html__('订单服务不可用。', 'aksrt') . '</p></div>';
if (!class_exists('\Jio\Payment\OrderRepository')) {
echo '<div class="notice notice-error"><p>' . esc_html__('订单服务不可用。', 'jio') . '</p></div>';
NativeUi::page_close();
return;
}
@@ -109,19 +109,19 @@ class PayProducts {
/** @return array<int, string> */
private function pay_limit_labels(): array {
return [
0 => __('所有人可购买', 'aksrt'),
1 => __('VIP1 及以上可购买', 'aksrt'),
2 => __('仅 VIP2 可购买', 'aksrt'),
0 => __('所有人可购买', 'jio'),
1 => __('VIP1 及以上可购买', 'jio'),
2 => __('仅 VIP2 可购买', 'jio'),
];
}
private function currency_symbol(): string {
$paid = \AKSRT\Settings\Settings::get_paid_content_settings();
$paid = \Jio\Settings\Settings::get_paid_content_settings();
return (string) ($paid['display']['currency_symbol'] ?? '¥');
}
private function render_flash(): void {
$code = isset($_GET['aksrt_pay_msg']) ? sanitize_key((string) wp_unslash($_GET['aksrt_pay_msg'])) : '';
$code = isset($_GET['jio_pay_msg']) ? sanitize_key((string) wp_unslash($_GET['jio_pay_msg'])) : '';
if ($code === '') {
return;
}
@@ -129,11 +129,11 @@ class PayProducts {
$title = $post instanceof \WP_Post ? get_the_title($post) : '';
// 码 => [文案, 是否失败]
$map = [
'product_saved' => [sprintf(__('已保存「%s」的商品配置。', 'aksrt'), $title), false],
'product_disabled' => [sprintf(__('已关闭「%s」的付费功能,文章已移出本列表。', 'aksrt'), $title), false],
'product_invalid_amount' => [__('保存失败:金额最多两位小数,积分须为非负整数。', 'aksrt'), true],
'product_vip_price' => [__('保存失败:会员价格不能高于执行价(积分商品不能高于积分售价)。', 'aksrt'), true],
'product_missing' => [__('保存失败:文章不存在或您无权编辑。', 'aksrt'), true],
'product_saved' => [sprintf(__('已保存「%s」的商品配置。', 'jio'), $title), false],
'product_disabled' => [sprintf(__('已关闭「%s」的付费功能,文章已移出本列表。', 'jio'), $title), false],
'product_invalid_amount' => [__('保存失败:金额最多两位小数,积分须为非负整数。', 'jio'), true],
'product_vip_price' => [__('保存失败:会员价格不能高于执行价(积分商品不能高于积分售价)。', 'jio'), true],
'product_missing' => [__('保存失败:文章不存在或您无权编辑。', 'jio'), true],
];
if (!isset($map[$code])) {
return;
@@ -144,13 +144,13 @@ class PayProducts {
private function ensure_cap(): void {
if (!current_user_can(self::CAPABILITY)) {
wp_die(esc_html__('您没有权限访问此页面。', 'aksrt'));
wp_die(esc_html__('您没有权限访问此页面。', 'jio'));
}
}
private function cash_decimal(string $value): string {
try {
return \AKSRT\Ledger\AssetAmount::balance($value)->decimal();
return \Jio\Ledger\AssetAmount::balance($value)->decimal();
} catch (\InvalidArgumentException $e) {
return '0.00';
}
+76 -76
View File
@@ -3,15 +3,15 @@
* 付费文章:快速设置弹窗与保存。
*
* 作为 flat sibling trait 被 PayProducts 使用(PSR-4:trait 必须与宿主类同目录)。
* 弹窗是单个 <dialog>,由 admin-pay-products.js 按行内 data-aksrt-product-edit 的 JSON 填充。
* 弹窗是单个 <dialog>,由 admin-pay-products.js 按行内 data-jio-product-edit 的 JSON 填充。
*
* @package AKSRT
* @package Jio
*/
namespace AKSRT\Admin\Native;
namespace Jio\Admin\Native;
use AKSRT\Core\DomainModel;
use AKSRT\Ledger\AssetAmount;
use Jio\Core\DomainModel;
use Jio\Ledger\AssetAmount;
defined('ABSPATH') || exit;
@@ -32,7 +32,7 @@ trait PayProductsEditor {
$title = html_entity_decode(get_the_title($post), ENT_QUOTES, 'UTF-8');
return [
'id' => (int) $post->ID,
'title' => $title !== '' ? $title : sprintf(__('(无标题 #%d)', 'aksrt'), (int) $post->ID),
'title' => $title !== '' ? $title : sprintf(__('(无标题 #%d)', 'jio'), (int) $post->ID),
'editUrl' => $edit_url,
'fields' => $fields,
];
@@ -41,12 +41,12 @@ trait PayProductsEditor {
/** @param array<string, string|int> $context 列表筛选与页码,保存后据此回到原位置。 */
private function render_product_editor(array $context): void {
$currency = $this->currency_symbol();
echo '<dialog class="aksrt-product-dialog" id="aksrt-product-editor" aria-labelledby="aksrt-product-editor-title" aria-describedby="aksrt-product-editor-subject">';
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" class="aksrt-product-dialog__frame" data-aksrt-product-form'
. ' data-discard="' . esc_attr__('有未保存的修改,确定放弃?', 'aksrt') . '"'
. ' data-vip-error="' . esc_attr__('会员价格不能高于执行价。', 'aksrt') . '"'
. ' data-vip-points-error="' . esc_attr__('会员积分价不能高于积分售价。', 'aksrt') . '"'
. ' data-busy-label="' . esc_attr__('正在保存…', 'aksrt') . '">';
echo '<dialog class="jio-product-dialog" id="jio-product-editor" aria-labelledby="jio-product-editor-title" aria-describedby="jio-product-editor-subject">';
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" class="jio-product-dialog__frame" data-jio-product-form'
. ' data-discard="' . esc_attr__('有未保存的修改,确定放弃?', 'jio') . '"'
. ' data-vip-error="' . esc_attr__('会员价格不能高于执行价。', 'jio') . '"'
. ' data-vip-points-error="' . esc_attr__('会员积分价不能高于积分售价。', 'jio') . '"'
. ' data-busy-label="' . esc_attr__('正在保存…', 'jio') . '">';
echo '<input type="hidden" name="action" value="' . esc_attr(self::SAVE_ACTION) . '">';
echo '<input type="hidden" name="post_id" value="">';
wp_nonce_field(self::SAVE_ACTION, '_wpnonce', false);
@@ -54,83 +54,83 @@ trait PayProductsEditor {
echo '<input type="hidden" name="ctx[' . esc_attr((string) $key) . ']" value="' . esc_attr((string) $value) . '">';
}
echo '<header class="aksrt-product-dialog__head"><div>';
echo '<h2 id="aksrt-product-editor-title">' . esc_html__('快速设置', 'aksrt') . '</h2>';
echo '<p id="aksrt-product-editor-subject" data-aksrt-product-subject></p></div>';
echo '<button type="button" class="aksrt-product-dialog__close" data-aksrt-dialog-close aria-label="' . esc_attr__('关闭弹窗', 'aksrt') . '"><span class="dashicons dashicons-no-alt" aria-hidden="true"></span></button>';
echo '</header><div class="aksrt-product-dialog__body">';
echo '<header class="jio-product-dialog__head"><div>';
echo '<h2 id="jio-product-editor-title">' . esc_html__('快速设置', 'jio') . '</h2>';
echo '<p id="jio-product-editor-subject" data-jio-product-subject></p></div>';
echo '<button type="button" class="jio-product-dialog__close" data-jio-dialog-close aria-label="' . esc_attr__('关闭弹窗', 'jio') . '"><span class="dashicons dashicons-no-alt" aria-hidden="true"></span></button>';
echo '</header><div class="jio-product-dialog__body">';
$this->editor_section_open(__('售卖方式', 'aksrt'));
echo '<div class="aksrt-native-form-grid">';
echo '<label class="aksrt-native-field"><span class="aksrt-native-field__label">' . esc_html__('付费类型', 'aksrt') . '</span><select name="aksrt_pay[pay_type]">';
$this->editor_section_open(__('售卖方式', 'jio'));
echo '<div class="jio-native-form-grid">';
echo '<label class="jio-native-field"><span class="jio-native-field__label">' . esc_html__('付费类型', 'jio') . '</span><select name="jio_pay[pay_type]">';
foreach ($this->paid_type_labels() as $key => $label) {
echo '<option value="' . esc_attr($key) . '">' . esc_html($label) . '</option>';
}
echo '<option value="' . esc_attr(DomainModel::PAY_TYPE_NO) . '">' . esc_html__('关闭付费', 'aksrt') . '</option></select></label>';
$this->editor_radios('pay_modo', __('支付方式', 'aksrt'), ['0' => __('现金', 'aksrt'), 'points' => __('积分', 'aksrt')], 'enabled');
echo '<option value="' . esc_attr(DomainModel::PAY_TYPE_NO) . '">' . esc_html__('关闭付费', 'jio') . '</option></select></label>';
$this->editor_radios('pay_modo', __('支付方式', 'jio'), ['0' => __('现金', 'jio'), 'points' => __('积分', 'jio')], 'enabled');
echo '</div>';
echo '<p class="aksrt-native-callout is-warning" data-aksrt-product-pane="disabled" hidden>'
. esc_html__('关闭后文章恢复免费阅读并移出本列表,价格等配置会保留,重新开启即可恢复。', 'aksrt') . '</p>';
echo '<div data-aksrt-product-pane="enabled">';
$this->editor_radios('pay_limit', __('购买权限', 'aksrt'), array_map('strval', $this->pay_limit_labels()));
echo '<p class="jio-native-callout is-warning" data-jio-product-pane="disabled" hidden>'
. esc_html__('关闭后文章恢复免费阅读并移出本列表,价格等配置会保留,重新开启即可恢复。', 'jio') . '</p>';
echo '<div data-jio-product-pane="enabled">';
$this->editor_radios('pay_limit', __('购买权限', 'jio'), array_map('strval', $this->pay_limit_labels()));
echo '</div>';
$this->editor_section_close();
echo '<div data-aksrt-product-pane="enabled">';
$this->editor_section_open(__('价格', 'aksrt'), __('会员价填 0 表示该等级免费,且不能高于售价。', 'aksrt'));
echo '<fieldset class="aksrt-native-form-grid aksrt-product-pane" data-aksrt-product-pane="cash"><legend class="screen-reader-text">' . esc_html__('现金价格', 'aksrt') . '</legend>';
$this->editor_number('price', __('执行价', 'aksrt'), $currency, '0.01', 'data-aksrt-product-base="cash"');
$this->editor_number('original_price', __('原价', 'aksrt'), $currency, '0.01', '', __('高于执行价时划线显示。', 'aksrt'));
$this->editor_number('vip1_price', __('VIP1 价格', 'aksrt'), $currency, '0.01', 'data-aksrt-product-vip="cash"');
$this->editor_number('vip2_price', __('VIP2 价格', 'aksrt'), $currency, '0.01', 'data-aksrt-product-vip="cash"');
echo '<label class="aksrt-native-field is-wide"><span class="aksrt-native-field__label">' . esc_html__('促销标签', 'aksrt') . '</span>'
. '<input type="text" name="aksrt_pay[promotion_tag]"><small class="aksrt-native-field__hint">' . esc_html__('显示在原价上方,支持安全 HTML,例如图标。', 'aksrt') . '</small></label>';
echo '<div data-jio-product-pane="enabled">';
$this->editor_section_open(__('价格', 'jio'), __('会员价填 0 表示该等级免费,且不能高于售价。', 'jio'));
echo '<fieldset class="jio-native-form-grid jio-product-pane" data-jio-product-pane="cash"><legend class="screen-reader-text">' . esc_html__('现金价格', 'jio') . '</legend>';
$this->editor_number('price', __('执行价', 'jio'), $currency, '0.01', 'data-jio-product-base="cash"');
$this->editor_number('original_price', __('原价', 'jio'), $currency, '0.01', '', __('高于执行价时划线显示。', 'jio'));
$this->editor_number('vip1_price', __('VIP1 价格', 'jio'), $currency, '0.01', 'data-jio-product-vip="cash"');
$this->editor_number('vip2_price', __('VIP2 价格', 'jio'), $currency, '0.01', 'data-jio-product-vip="cash"');
echo '<label class="jio-native-field is-wide"><span class="jio-native-field__label">' . esc_html__('促销标签', 'jio') . '</span>'
. '<input type="text" name="jio_pay[promotion_tag]"><small class="jio-native-field__hint">' . esc_html__('显示在原价上方,支持安全 HTML,例如图标。', 'jio') . '</small></label>';
echo '</fieldset>';
echo '<fieldset class="aksrt-native-form-grid aksrt-product-pane" data-aksrt-product-pane="points" hidden disabled><legend class="screen-reader-text">' . esc_html__('积分价格', 'aksrt') . '</legend>';
$this->editor_number('points_price', __('积分售价', 'aksrt'), __('积分', 'aksrt'), '1', 'data-aksrt-product-base="points"');
$this->editor_number('vip1_points', __('VIP1 积分价', 'aksrt'), __('积分', 'aksrt'), '1', 'data-aksrt-product-vip="points"');
$this->editor_number('vip2_points', __('VIP2 积分价', 'aksrt'), __('积分', 'aksrt'), '1', 'data-aksrt-product-vip="points"');
echo '<fieldset class="jio-native-form-grid jio-product-pane" data-jio-product-pane="points" hidden disabled><legend class="screen-reader-text">' . esc_html__('积分价格', 'jio') . '</legend>';
$this->editor_number('points_price', __('积分售价', 'jio'), __('积分', 'jio'), '1', 'data-jio-product-base="points"');
$this->editor_number('vip1_points', __('VIP1 积分价', 'jio'), __('积分', 'jio'), '1', 'data-jio-product-vip="points"');
$this->editor_number('vip2_points', __('VIP2 积分价', 'jio'), __('积分', 'jio'), '1', 'data-jio-product-vip="points"');
echo '</fieldset>';
$this->editor_section_close();
$this->editor_section_open(__('销售', 'aksrt'));
echo '<div class="aksrt-native-form-grid">';
$this->editor_number('pay_count', __('销量浮动', 'aksrt'), '', '1', '', __('在真实销量上增加或减少的数量,可填负数。', 'aksrt'), false);
echo '<div class="aksrt-product-switches" role="group" aria-label="' . esc_attr__('销售选项', 'aksrt') . '">';
$this->editor_checkbox('show_sales', __('前台显示销量', 'aksrt'));
$this->editor_checkbox('coupon_s', __('允许使用优惠码', 'aksrt'));
$this->editor_checkbox('order_expired_s', __('启用订单有效期', 'aksrt'));
$this->editor_section_open(__('销售', 'jio'));
echo '<div class="jio-native-form-grid">';
$this->editor_number('pay_count', __('销量浮动', 'jio'), '', '1', '', __('在真实销量上增加或减少的数量,可填负数。', 'jio'), false);
echo '<div class="jio-product-switches" role="group" aria-label="' . esc_attr__('销售选项', 'jio') . '">';
$this->editor_checkbox('show_sales', __('前台显示销量', 'jio'));
$this->editor_checkbox('coupon_s', __('允许使用优惠码', 'jio'));
$this->editor_checkbox('order_expired_s', __('启用订单有效期', 'jio'));
echo '</div></div>';
echo '<fieldset class="aksrt-product-pane aksrt-product-expire" data-aksrt-product-pane="expire" hidden disabled><legend class="screen-reader-text">' . esc_html__('订单有效期', 'aksrt') . '</legend>';
echo '<label class="aksrt-native-field"><span class="aksrt-native-field__label">' . esc_html__('有效时长', 'aksrt') . '</span><span class="aksrt-product-inline">'
. '<input type="number" name="aksrt_pay[order_expired_time]" min="1" step="1" required>'
. '<select name="aksrt_pay[order_expired_unit]" aria-label="' . esc_attr__('时长单位', 'aksrt') . '">'
. '<option value="day">' . esc_html__('天', 'aksrt') . '</option><option value="hour">' . esc_html__('小时', 'aksrt') . '</option><option value="minute">' . esc_html__('分钟', 'aksrt') . '</option>'
. '</select></span><small class="aksrt-native-field__hint">' . esc_html__('购买后仅在有效期内可查看,过期需再次购买。', 'aksrt') . '</small></label></fieldset>';
echo '<fieldset class="jio-product-pane jio-product-expire" data-jio-product-pane="expire" hidden disabled><legend class="screen-reader-text">' . esc_html__('订单有效期', 'jio') . '</legend>';
echo '<label class="jio-native-field"><span class="jio-native-field__label">' . esc_html__('有效时长', 'jio') . '</span><span class="jio-product-inline">'
. '<input type="number" name="jio_pay[order_expired_time]" min="1" step="1" required>'
. '<select name="jio_pay[order_expired_unit]" aria-label="' . esc_attr__('时长单位', 'jio') . '">'
. '<option value="day">' . esc_html__('天', 'jio') . '</option><option value="hour">' . esc_html__('小时', 'jio') . '</option><option value="minute">' . esc_html__('分钟', 'jio') . '</option>'
. '</select></span><small class="jio-native-field__hint">' . esc_html__('购买后仅在有效期内可查看,过期需再次购买。', 'jio') . '</small></label></fieldset>';
$this->editor_section_close();
$this->editor_section_open(__('商品文案', 'aksrt'));
echo '<div class="aksrt-native-form-grid">';
echo '<label class="aksrt-native-field is-wide"><span class="aksrt-native-field__label">' . esc_html__('商品标题', 'aksrt') . '</span><input type="text" name="aksrt_pay[pay_title]" maxlength="200">'
. '<small class="aksrt-native-field__hint">' . esc_html__('留空则使用文章标题。', 'aksrt') . '</small></label>';
echo '<label class="aksrt-native-field is-wide"><span class="aksrt-native-field__label">' . esc_html__('商品简介', 'aksrt') . '</span><textarea name="aksrt_pay[pay_doc]" rows="2"></textarea></label>';
$this->editor_section_open(__('商品文案', 'jio'));
echo '<div class="jio-native-form-grid">';
echo '<label class="jio-native-field is-wide"><span class="jio-native-field__label">' . esc_html__('商品标题', 'jio') . '</span><input type="text" name="jio_pay[pay_title]" maxlength="200">'
. '<small class="jio-native-field__hint">' . esc_html__('留空则使用文章标题。', 'jio') . '</small></label>';
echo '<label class="jio-native-field is-wide"><span class="jio-native-field__label">' . esc_html__('商品简介', 'jio') . '</span><textarea name="jio_pay[pay_doc]" rows="2"></textarea></label>';
echo '</div>';
$this->editor_section_close();
echo '</div>';
echo '<p class="aksrt-native-callout aksrt-product-note">' . esc_html__('下载资源、付费图片、视频剧集和隐藏内容请在文章编辑页的「付费功能」面板维护。', 'aksrt')
. ' <a href="#" target="_blank" rel="noopener noreferrer" data-aksrt-product-edit-link>' . esc_html__('打开文章编辑页', 'aksrt') . '</a></p>';
echo '<p class="jio-native-callout jio-product-note">' . esc_html__('下载资源、付费图片、视频剧集和隐藏内容请在文章编辑页的「付费功能」面板维护。', 'jio')
. ' <a href="#" target="_blank" rel="noopener noreferrer" data-jio-product-edit-link>' . esc_html__('打开文章编辑页', 'jio') . '</a></p>';
echo '</div><footer class="aksrt-product-dialog__foot">';
echo '<button type="button" class="button" data-aksrt-dialog-close>' . esc_html__('取消', 'aksrt') . '</button>';
echo '<button type="submit" class="button button-primary" data-aksrt-product-submit>' . esc_html__('保存设置', 'aksrt') . '</button>';
echo '</div><footer class="jio-product-dialog__foot">';
echo '<button type="button" class="button" data-jio-dialog-close>' . esc_html__('取消', 'jio') . '</button>';
echo '<button type="submit" class="button button-primary" data-jio-product-submit>' . esc_html__('保存设置', 'jio') . '</button>';
echo '</footer></form></dialog>';
}
private function editor_section_open(string $title, string $hint = ''): void {
echo '<section class="aksrt-product-section"><h3>' . esc_html($title) . '</h3>';
echo '<section class="jio-product-section"><h3>' . esc_html($title) . '</h3>';
if ($hint !== '') {
echo '<p class="aksrt-product-section__hint">' . esc_html($hint) . '</p>';
echo '<p class="jio-product-section__hint">' . esc_html($hint) . '</p>';
}
}
@@ -140,29 +140,29 @@ trait PayProductsEditor {
/** @param array<string, string> $options */
private function editor_radios(string $key, string $title, array $options, string $pane = ''): void {
echo '<fieldset class="aksrt-native-field aksrt-product-radios"' . ($pane !== '' ? ' data-aksrt-product-pane="' . esc_attr($pane) . '"' : '') . '>';
echo '<legend class="aksrt-native-field__label">' . esc_html($title) . '</legend><div>';
echo '<fieldset class="jio-native-field jio-product-radios"' . ($pane !== '' ? ' data-jio-product-pane="' . esc_attr($pane) . '"' : '') . '>';
echo '<legend class="jio-native-field__label">' . esc_html($title) . '</legend><div>';
foreach ($options as $value => $label) {
echo '<label><input type="radio" name="aksrt_pay[' . esc_attr($key) . ']" value="' . esc_attr((string) $value) . '"><span>' . esc_html($label) . '</span></label>';
echo '<label><input type="radio" name="jio_pay[' . esc_attr($key) . ']" value="' . esc_attr((string) $value) . '"><span>' . esc_html($label) . '</span></label>';
}
echo '</div></fieldset>';
}
private function editor_number(string $key, string $title, string $unit, string $step, string $attributes = '', string $hint = '', bool $non_negative = true): void {
echo '<label class="aksrt-native-field"><span class="aksrt-native-field__label">' . esc_html($title) . '</span><span class="aksrt-product-inline">';
echo '<input type="number" name="aksrt_pay[' . esc_attr($key) . ']" step="' . esc_attr($step) . '"' . ($non_negative ? ' min="0"' : '') . ' required ' . $attributes . '>';
echo '<label class="jio-native-field"><span class="jio-native-field__label">' . esc_html($title) . '</span><span class="jio-product-inline">';
echo '<input type="number" name="jio_pay[' . esc_attr($key) . ']" step="' . esc_attr($step) . '"' . ($non_negative ? ' min="0"' : '') . ' required ' . $attributes . '>';
if ($unit !== '') {
echo '<span class="aksrt-product-unit">' . esc_html($unit) . '</span>';
echo '<span class="jio-product-unit">' . esc_html($unit) . '</span>';
}
echo '</span>';
if ($hint !== '') {
echo '<small class="aksrt-native-field__hint">' . esc_html($hint) . '</small>';
echo '<small class="jio-native-field__hint">' . esc_html($hint) . '</small>';
}
echo '</label>';
}
private function editor_checkbox(string $key, string $label): void {
echo '<label class="aksrt-product-check"><input type="checkbox" name="aksrt_pay[' . esc_attr($key) . ']" value="1">' . esc_html($label) . '</label>';
echo '<label class="jio-product-check"><input type="checkbox" name="jio_pay[' . esc_attr($key) . ']" value="1">' . esc_html($label) . '</label>';
}
/* ------------------------------------------------------------------
@@ -171,7 +171,7 @@ trait PayProductsEditor {
public function handle_save(): void {
if (!current_user_can(self::CAPABILITY)) {
wp_die(esc_html__('您没有权限执行此操作。', 'aksrt'), '', ['response' => 403]);
wp_die(esc_html__('您没有权限执行此操作。', 'jio'), '', ['response' => 403]);
}
check_admin_referer(self::SAVE_ACTION);
@@ -182,8 +182,8 @@ trait PayProductsEditor {
$this->redirect_after_save($context, 'product_missing', 0);
}
$raw = isset($_POST['aksrt_pay']) && is_array($_POST['aksrt_pay']) ? wp_unslash($_POST['aksrt_pay']) : [];
$meta = aksrt_get_pay_meta($post);
$raw = isset($_POST['jio_pay']) && is_array($_POST['jio_pay']) ? wp_unslash($_POST['jio_pay']) : [];
$meta = jio_get_pay_meta($post);
$pay_type = DomainModel::normalize_pay_type((string) ($raw['pay_type'] ?? ''));
if ($pay_type === DomainModel::PAY_TYPE_NO) {
@@ -282,7 +282,7 @@ trait PayProductsEditor {
/** @param array<string, string|int> $context */
private function redirect_after_save(array $context, string $code, int $post_id): void {
$args = $context + ['aksrt_pay_msg' => $code];
$args = $context + ['jio_pay_msg' => $code];
if ($post_id > 0) {
$args['product'] = $post_id;
}
+50 -50
View File
@@ -4,14 +4,14 @@
*
* 作为 flat sibling trait 被 PayProducts 使用(PSR-4:trait 必须与宿主类同目录)。
*
* @package AKSRT
* @package Jio
*/
namespace AKSRT\Admin\Native;
namespace Jio\Admin\Native;
use AKSRT\Core\DomainModel;
use AKSRT\Ledger\AssetAmount;
use AKSRT\Payment\OrderRepository;
use Jio\Core\DomainModel;
use Jio\Ledger\AssetAmount;
use Jio\Payment\OrderRepository;
defined('ABSPATH') || exit;
@@ -54,25 +54,25 @@ trait PayProductsList {
// 翻页链接显式带上当前筛选条件(不含 paged)。
$query_args = array_filter(['pay_type' => $pay_type, 's' => $search], static fn(string $value): bool => $value !== '');
NativeUi::card_open('', '', true, 'aksrt-products-card');
NativeUi::card_open('', '', true, 'jio-products-card');
$this->render_product_toolbar($pay_type, $search, $type_labels);
echo '<div class="aksrt-native-table-wrap">';
echo '<table class="wp-list-table widefat fixed striped aksrt-products-table"><thead><tr>';
echo '<th scope="col" class="column-product">' . esc_html__('商品', 'aksrt') . '</th>';
echo '<th scope="col" class="column-type">' . esc_html__('付费类型', 'aksrt') . '</th>';
echo '<th scope="col" class="column-price">' . esc_html__('价格', 'aksrt') . '</th>';
echo '<th scope="col" class="column-sales">' . esc_html__('销量', 'aksrt') . '</th>';
echo '<th scope="col" class="column-revenue">' . esc_html__('销售额', 'aksrt') . '</th>';
echo '<th scope="col" class="column-actions">' . esc_html__('操作', 'aksrt') . '</th>';
echo '<div class="jio-native-table-wrap">';
echo '<table class="wp-list-table widefat fixed striped jio-products-table"><thead><tr>';
echo '<th scope="col" class="column-product">' . esc_html__('商品', 'jio') . '</th>';
echo '<th scope="col" class="column-type">' . esc_html__('付费类型', 'jio') . '</th>';
echo '<th scope="col" class="column-price">' . esc_html__('价格', 'jio') . '</th>';
echo '<th scope="col" class="column-sales">' . esc_html__('销量', 'jio') . '</th>';
echo '<th scope="col" class="column-revenue">' . esc_html__('销售额', 'jio') . '</th>';
echo '<th scope="col" class="column-actions">' . esc_html__('操作', 'jio') . '</th>';
echo '</tr></thead><tbody>';
if (!$posts) {
echo NativeUi::empty_row(
6,
__('暂无付费文章', 'aksrt'),
__('暂无付费文章', 'jio'),
$query_args !== []
? __('调整类型或搜索条件后重试。', 'aksrt')
: __('在文章编辑页的「付费功能」面板开启付费后,文章会出现在这里。', 'aksrt')
? __('调整类型或搜索条件后重试。', 'jio')
: __('在文章编辑页的「付费功能」面板开启付费后,文章会出现在这里。', 'jio')
);
}
@@ -90,7 +90,7 @@ trait PayProductsList {
}
/**
* 付费文章 = `_aksrt_pay_type` 为任一付费类型;所有写入口都同步这个派生键,直接按它查询分页。
* 付费文章 = `_jio_pay_type` 为任一付费类型;所有写入口都同步这个派生键,直接按它查询分页。
*
* @return array{posts: \WP_Post[], total: int}
*/
@@ -152,9 +152,9 @@ trait PayProductsList {
$counts = $this->paid_type_counts();
$keep = $search !== '' ? ['s' => $search] : [];
echo '<div class="aksrt-products-toolbar">';
echo '<ul class="subsubsub" aria-label="' . esc_attr__('付费类型筛选', 'aksrt') . '">';
foreach (['' => __('全部', 'aksrt')] + $type_labels as $key => $label) {
echo '<div class="jio-products-toolbar">';
echo '<ul class="subsubsub" aria-label="' . esc_attr__('付费类型筛选', 'jio') . '">';
foreach (['' => __('全部', 'jio')] + $type_labels as $key => $label) {
$key = (string) $key;
$count = $key === '' ? array_sum($counts) : (int) ($counts[$key] ?? 0);
$url = $this->page_url(self::PAGE_SLUG, $key === '' ? $keep : $keep + ['pay_type' => $key]);
@@ -163,17 +163,17 @@ trait PayProductsList {
}
echo '</ul>';
echo '<form method="get" action="' . esc_url(admin_url('admin.php')) . '" class="aksrt-native-filters" role="search">';
echo '<form method="get" action="' . esc_url(admin_url('admin.php')) . '" class="jio-native-filters" role="search">';
echo '<input type="hidden" name="page" value="' . esc_attr(self::PAGE_SLUG) . '">';
if ($pay_type !== '') {
echo '<input type="hidden" name="pay_type" value="' . esc_attr($pay_type) . '">';
}
echo '<label class="screen-reader-text" for="aksrt-products-search">' . esc_html__('搜索标题或文章 ID', 'aksrt') . '</label>';
echo '<input type="search" id="aksrt-products-search" name="s" value="' . esc_attr($search) . '" placeholder="' . esc_attr__('搜索标题或文章 ID', 'aksrt') . '">';
echo '<button type="submit" class="button">' . esc_html__('搜索', 'aksrt') . '</button>';
echo '<label class="screen-reader-text" for="jio-products-search">' . esc_html__('搜索标题或文章 ID', 'jio') . '</label>';
echo '<input type="search" id="jio-products-search" name="s" value="' . esc_attr($search) . '" placeholder="' . esc_attr__('搜索标题或文章 ID', 'jio') . '">';
echo '<button type="submit" class="button">' . esc_html__('搜索', 'jio') . '</button>';
if ($search !== '') {
$clear = $this->page_url(self::PAGE_SLUG, $pay_type !== '' ? ['pay_type' => $pay_type] : []);
echo '<a class="button-link" href="' . esc_url($clear) . '">' . esc_html__('清除搜索', 'aksrt') . '</a>';
echo '<a class="button-link" href="' . esc_url($clear) . '">' . esc_html__('清除搜索', 'jio') . '</a>';
}
echo '</form></div>';
}
@@ -184,11 +184,11 @@ trait PayProductsList {
*/
private function render_product_row(\WP_Post $post, array $sales, array $type_labels, string $currency): void {
$post_id = (int) $post->ID;
$meta = aksrt_get_pay_meta($post);
$meta = jio_get_pay_meta($post);
$pay_type = DomainModel::normalize_pay_type((string) $meta['pay_type']);
$title = get_the_title($post);
if ($title === '') {
$title = sprintf(__('(无标题 #%d)', 'aksrt'), $post_id);
$title = sprintf(__('(无标题 #%d)', 'jio'), $post_id);
}
$edit_url = (string) get_edit_post_link($post_id, 'raw');
$permalink = (string) get_permalink($post_id);
@@ -201,60 +201,60 @@ trait PayProductsList {
echo $edit_url !== ''
? '<a class="row-title" href="' . esc_url($edit_url) . '">' . esc_html($title) . '</a>'
: esc_html($title);
echo '</strong><div class="aksrt-products-sub">#' . esc_html((string) $post_id);
echo '</strong><div class="jio-products-sub">#' . esc_html((string) $post_id);
if ($post->post_status !== 'publish') {
$status = get_post_status_object($post->post_status);
echo ' · ' . esc_html($status ? (string) $status->label : $post->post_status);
}
if ((string) $meta['pay_title'] !== '') {
echo ' · ' . esc_html(sprintf(__('商品标题:%s', 'aksrt'), (string) $meta['pay_title']));
echo ' · ' . esc_html(sprintf(__('商品标题:%s', 'jio'), (string) $meta['pay_title']));
}
echo '</div></td>';
echo '<td class="column-type">' . NativeUi::badge($type_labels[$pay_type] ?? $pay_type, self::TYPE_TONES[$pay_type] ?? 'neutral');
$limit = (int) $meta['pay_limit'];
if ($limit > 0) {
echo '<div class="aksrt-products-sub">' . esc_html($this->pay_limit_labels()[$limit] ?? '') . '</div>';
echo '<div class="jio-products-sub">' . esc_html($this->pay_limit_labels()[$limit] ?? '') . '</div>';
}
echo '</td>';
echo '<td class="column-price">' . $this->price_cell_html($meta, $currency) . '</td>';
echo '<td class="column-sales"><span class="aksrt-native-amount">' . esc_html(sprintf(__('%d 单', 'aksrt'), $orders)) . '</span>';
echo '<div class="aksrt-products-sub">' . esc_html(!empty($meta['show_sales'])
? sprintf(__('前台显示 %d', 'aksrt'), aksrt_get_content_sales_count($post, $meta))
: __('前台不显示销量', 'aksrt')) . '</div></td>';
echo '<td class="column-sales"><span class="jio-native-amount">' . esc_html(sprintf(__('%d 单', 'jio'), $orders)) . '</span>';
echo '<div class="jio-products-sub">' . esc_html(!empty($meta['show_sales'])
? sprintf(__('前台显示 %d', 'jio'), jio_get_content_sales_count($post, $meta))
: __('前台不显示销量', 'jio')) . '</div></td>';
echo '<td class="column-revenue"><span class="aksrt-native-amount">' . esc_html($this->sales_amount($sales['revenue'] ?? '0.00', $currency)) . '</span>';
echo '<td class="column-revenue"><span class="jio-native-amount">' . esc_html($this->sales_amount($sales['revenue'] ?? '0.00', $currency)) . '</span>';
if ($orders > 0) {
echo '<div class="aksrt-products-sub">' . esc_html(sprintf(__('均价 %s', 'aksrt'), $this->sales_amount($sales['avg_amount'] ?? '0.00', $currency))) . '</div>';
echo '<div class="jio-products-sub">' . esc_html(sprintf(__('均价 %s', 'jio'), $this->sales_amount($sales['avg_amount'] ?? '0.00', $currency))) . '</div>';
}
echo '</td>';
$payload = $this->product_payload($post, $meta, $edit_url);
echo '<td class="column-actions">';
echo '<button type="button" class="button button-small button-primary" data-aksrt-product-edit="' . esc_attr((string) wp_json_encode($payload)) . '" aria-haspopup="dialog" aria-controls="aksrt-product-editor">'
. esc_html__('快速设置', 'aksrt') . '</button>';
echo '<div class="aksrt-products-links">';
echo '<button type="button" class="button button-small button-primary" data-jio-product-edit="' . esc_attr((string) wp_json_encode($payload)) . '" aria-haspopup="dialog" aria-controls="jio-product-editor">'
. esc_html__('快速设置', 'jio') . '</button>';
echo '<div class="jio-products-links">';
if ($edit_url !== '') {
echo '<a href="' . esc_url($edit_url) . '">' . esc_html__('编辑文章', 'aksrt') . '</a>';
echo '<a href="' . esc_url($edit_url) . '">' . esc_html__('编辑文章', 'jio') . '</a>';
}
$orders_url = $this->page_url('aksrt_orders', [
$orders_url = $this->page_url('jio_orders', [
'product_type' => DomainModel::ORDER_PRODUCT_RESOURCE,
'product_id' => $post_id,
]);
echo '<a href="' . esc_url($orders_url) . '">' . esc_html__('订单', 'aksrt') . '</a>';
echo '<a href="' . esc_url($orders_url) . '">' . esc_html__('订单', 'jio') . '</a>';
if ($permalink !== '') {
echo '<a href="' . esc_url($permalink) . '" target="_blank" rel="noopener noreferrer">' . esc_html__('查看', 'aksrt') . '</a>';
echo '<a href="' . esc_url($permalink) . '" target="_blank" rel="noopener noreferrer">' . esc_html__('查看', 'jio') . '</a>';
}
echo '</div></td></tr>';
}
/** @param array<string, mixed> $meta */
private function price_cell_html(array $meta, string $currency): string {
$free = __('免费', 'aksrt');
$free = __('免费', 'jio');
if ($meta['pay_modo'] === 'points') {
$fmt = static fn(int $points): string => $points > 0 ? sprintf(__('%d 积分', 'aksrt'), $points) : $free;
$fmt = static fn(int $points): string => $points > 0 ? sprintf(__('%d 积分', 'jio'), $points) : $free;
$main = $fmt((int) $meta['points_price']);
$original = '';
$vip = [$fmt((int) $meta['vip1_points']), $fmt((int) $meta['vip2_points'])];
@@ -267,15 +267,15 @@ trait PayProductsList {
$vip = [$fmt((string) $meta['vip1_price']), $fmt((string) $meta['vip2_price'])];
}
return '<span class="aksrt-native-amount">' . esc_html($main) . '</span>'
. ($original !== '' ? ' <del class="aksrt-native-muted">' . esc_html($original) . '</del>' : '')
. '<div class="aksrt-products-sub">' . esc_html(sprintf(__('VIP1 %1$s · VIP2 %2$s', 'aksrt'), $vip[0], $vip[1])) . '</div>';
return '<span class="jio-native-amount">' . esc_html($main) . '</span>'
. ($original !== '' ? ' <del class="jio-native-muted">' . esc_html($original) . '</del>' : '')
. '<div class="jio-products-sub">' . esc_html(sprintf(__('VIP1 %1$s · VIP2 %2$s', 'jio'), $vip[0], $vip[1])) . '</div>';
}
/** 订单统计里积分订单为 int、现金订单为两位小数字符串。 */
private function sales_amount($amount, string $currency): string {
return is_int($amount)
? sprintf(__('%d 积分', 'aksrt'), $amount)
? sprintf(__('%d 积分', 'jio'), $amount)
: $currency . $this->cash_decimal((string) $amount);
}
}
+43 -43
View File
@@ -6,12 +6,12 @@
* admin-post 处理器与共用的展示助手。页面样式在 assets/css/admin/withdrawals.css,
* 处理弹窗、复制收款账户与防重复提交在 assets/js/admin-withdrawals.js。
*
* @package AKSRT
* @package Jio
*/
namespace AKSRT\Admin\Native;
namespace Jio\Admin\Native;
use AKSRT\Payment\Withdrawal;
use Jio\Payment\Withdrawal;
defined('ABSPATH') || exit;
@@ -20,7 +20,7 @@ class PayWithdrawals {
use PayWithdrawalsList;
public const CAPABILITY = 'manage_options';
public const PAGE_SLUG = 'aksrt_withdraw';
public const PAGE_SLUG = 'jio_withdraw';
/** 后台可发起的目标状态。 */
private const TARGET_STATUSES = [Withdrawal::STATUS_PROCESSING, Withdrawal::STATUS_COMPLETED, Withdrawal::STATUS_REJECTED];
@@ -39,7 +39,7 @@ class PayWithdrawals {
return;
}
add_action('admin_enqueue_scripts', [$this, 'enqueue_assets']);
add_action('admin_post_aksrt_pay_update_withdrawal', [$this, 'handle_update_withdrawal']);
add_action('admin_post_jio_pay_update_withdrawal', [$this, 'handle_update_withdrawal']);
}
public function enqueue_assets(): void {
@@ -49,17 +49,17 @@ class PayWithdrawals {
}
$css = '/assets/css/admin/withdrawals.css';
$js = '/assets/js/admin-withdrawals.js';
wp_enqueue_style('aksrt-admin-withdrawals', AKSRT_URI . $css, [NativeUi::STYLE_HANDLE], (string) filemtime(AKSRT_DIR . $css));
wp_enqueue_script('aksrt-admin-withdrawals', AKSRT_URI . $js, [], (string) filemtime(AKSRT_DIR . $js), true);
wp_enqueue_style('jio-admin-withdrawals', JIO_URI . $css, [NativeUi::STYLE_HANDLE], (string) filemtime(JIO_DIR . $css));
wp_enqueue_script('jio-admin-withdrawals', JIO_URI . $js, [], (string) filemtime(JIO_DIR . $js), true);
}
public function render_withdrawals_page(): void {
$this->ensure_cap();
NativeUi::page_open('aksrt-wd-page');
NativeUi::page_open('jio-wd-page');
if (!class_exists('\AKSRT\Payment\Withdrawal')) {
NativeUi::header(__('提现审核', 'aksrt'), '', 'dashicons-money-alt');
echo '<div class="notice notice-error"><p>' . esc_html__('提现模块未初始化。', 'aksrt') . '</p></div>';
if (!class_exists('\Jio\Payment\Withdrawal')) {
NativeUi::header(__('提现审核', 'jio'), '', 'dashicons-money-alt');
echo '<div class="notice notice-error"><p>' . esc_html__('提现模块未初始化。', 'jio') . '</p></div>';
NativeUi::page_close();
return;
}
@@ -75,39 +75,39 @@ class PayWithdrawals {
public function handle_update_withdrawal(): void {
$this->ensure_cap();
$id = isset($_POST['withdrawal_id']) ? (int) $_POST['withdrawal_id'] : 0;
check_admin_referer('aksrt_pay_update_withdrawal_' . $id);
check_admin_referer('jio_pay_update_withdrawal_' . $id);
$new_status = isset($_POST['new_status']) ? sanitize_key((string) wp_unslash($_POST['new_status'])) : '';
$note = isset($_POST['note']) ? trim(sanitize_textarea_field((string) wp_unslash($_POST['note']))) : '';
$request_key = isset($_POST['idempotency_key']) ? sanitize_text_field((string) wp_unslash($_POST['idempotency_key'])) : '';
if ($id <= 0 || !in_array($new_status, self::TARGET_STATUSES, true) || !wp_is_uuid($request_key)) {
$this->redirect_back(['aksrt_pay_msg' => 'withdraw_failed']);
$this->redirect_back(['jio_pay_msg' => 'withdraw_failed']);
}
// 拒绝原因会随站内信通知用户,必须填写。
if ($new_status === Withdrawal::STATUS_REJECTED && $note === '') {
$this->redirect_back(['aksrt_pay_msg' => 'withdraw_note_required']);
$this->redirect_back(['jio_pay_msg' => 'withdraw_note_required']);
}
$operation_key = \AKSRT\Ledger\OperationKey::deterministic('admin-withdraw-transition', [get_current_user_id(), strtolower($request_key)]);
$operation_key = \Jio\Ledger\OperationKey::deterministic('admin-withdraw-transition', [get_current_user_id(), strtolower($request_key)]);
$result = Withdrawal::get_instance()->process_status_change($id, $new_status, $note, $operation_key);
if (is_array($result)) {
$this->redirect_back([
'aksrt_pay_msg' => 'withdraw_' . $new_status,
'jio_pay_msg' => 'withdraw_' . $new_status,
'number' => (string) ($result['withdrawal_number'] ?? ''),
]);
}
$code = is_wp_error($result) ? $result->get_error_code() : '';
$msg = [
'aksrt_withdraw_transition' => 'withdraw_stale',
'aksrt_withdraw_idempotency_conflict' => 'withdraw_stale',
'aksrt_balance_insufficient' => 'withdraw_refund_failed',
'jio_withdraw_transition' => 'withdraw_stale',
'jio_withdraw_idempotency_conflict' => 'withdraw_stale',
'jio_balance_insufficient' => 'withdraw_refund_failed',
][$code] ?? 'withdraw_failed';
// 可重试的失败带回同一个幂等键:重提同一操作不会重复入账。
$args = ['aksrt_pay_msg' => $msg];
$args = ['jio_pay_msg' => $msg];
if ($msg !== 'withdraw_stale') {
$args['retry_id'] = $id;
$args['retry_key'] = $request_key;
@@ -122,7 +122,7 @@ class PayWithdrawals {
if (is_string($referer) && $referer !== '') {
parse_str((string) wp_parse_url($referer, PHP_URL_QUERY), $parsed);
if (($parsed['page'] ?? '') === self::PAGE_SLUG) {
unset($parsed['page'], $parsed['aksrt_pay_msg'], $parsed['number'], $parsed['retry_id'], $parsed['retry_key']);
unset($parsed['page'], $parsed['jio_pay_msg'], $parsed['number'], $parsed['retry_id'], $parsed['retry_key']);
$query = $parsed;
}
}
@@ -137,10 +137,10 @@ class PayWithdrawals {
/** @return array<string, string> */
private function status_labels(): array {
return [
Withdrawal::STATUS_PENDING => __('待处理', 'aksrt'),
Withdrawal::STATUS_PROCESSING => __('处理中', 'aksrt'),
Withdrawal::STATUS_COMPLETED => __('已完成', 'aksrt'),
Withdrawal::STATUS_REJECTED => __('已拒绝', 'aksrt'),
Withdrawal::STATUS_PENDING => __('待处理', 'jio'),
Withdrawal::STATUS_PROCESSING => __('处理中', 'jio'),
Withdrawal::STATUS_COMPLETED => __('已完成', 'jio'),
Withdrawal::STATUS_REJECTED => __('已拒绝', 'jio'),
];
}
@@ -171,22 +171,22 @@ class PayWithdrawals {
/** @return array<string, string> */
private function source_labels(): array {
return [
Withdrawal::SOURCE_BALANCE => __('余额钱包', 'aksrt'),
Withdrawal::SOURCE_COMMISSION => __('推广佣金', 'aksrt'),
Withdrawal::SOURCE_BALANCE => __('余额钱包', 'jio'),
Withdrawal::SOURCE_COMMISSION => __('推广佣金', 'jio'),
];
}
/** @return array<string, string> */
private function method_labels(): array {
return [
'alipay' => __('支付宝', 'aksrt'),
'wechat' => __('微信', 'aksrt'),
'alipay' => __('支付宝', 'jio'),
'wechat' => __('微信', 'jio'),
];
}
private function format_money(string $decimal): string {
try {
return '¥' . \AKSRT\Ledger\AssetAmount::balance($decimal)->formatted();
return '¥' . \Jio\Ledger\AssetAmount::balance($decimal)->formatted();
} catch (\InvalidArgumentException $e) {
return '¥0.00';
}
@@ -195,11 +195,11 @@ class PayWithdrawals {
/** 手续费 = 申请毛额 − 到账净额,按分计算避免浮点误差。 */
private function fee_decimal(string $gross, string $net): string {
try {
$fee = \AKSRT\Ledger\AssetAmount::balance($gross)->minor() - \AKSRT\Ledger\AssetAmount::balance($net)->minor();
$fee = \Jio\Ledger\AssetAmount::balance($gross)->minor() - \Jio\Ledger\AssetAmount::balance($net)->minor();
} catch (\InvalidArgumentException $e) {
return '0.00';
}
return \AKSRT\Ledger\AssetAmount::decimal_from_minor(max(0, $fee));
return \Jio\Ledger\AssetAmount::decimal_from_minor(max(0, $fee));
}
/** 行内操作的幂等键:上次失败的那一行沿用带回的键,其余行每次渲染新生成。 */
@@ -208,21 +208,21 @@ class PayWithdrawals {
}
private function render_flash(): void {
if (empty($_GET['aksrt_pay_msg'])) {
if (empty($_GET['jio_pay_msg'])) {
return;
}
$code = sanitize_key((string) wp_unslash($_GET['aksrt_pay_msg']));
$code = sanitize_key((string) wp_unslash($_GET['jio_pay_msg']));
$number = isset($_GET['number']) ? sanitize_text_field((string) wp_unslash($_GET['number'])) : '';
$suffix = $number !== '' ? sprintf(__('(订单号 %s)', 'aksrt'), $number) : '';
$suffix = $number !== '' ? sprintf(__('(订单号 %s)', 'jio'), $number) : '';
// 码 => [文案, notice 类型]
$map = [
'withdraw_processing' => [__('已标记为处理中,请尽快完成打款。', 'aksrt') . $suffix, 'success'],
'withdraw_completed' => [__('已确认打款完成,用户已收到通知。', 'aksrt') . $suffix, 'success'],
'withdraw_rejected' => [__('已拒绝提现,冻结金额已退回用户,拒绝原因已通知用户。', 'aksrt') . $suffix, 'success'],
'withdraw_note_required' => [__('拒绝提现必须填写拒绝原因。', 'aksrt'), 'warning'],
'withdraw_stale' => [__('操作未执行:该申请的状态已被更新,请查看最新状态后再处理。', 'aksrt'), 'warning'],
'withdraw_refund_failed' => [__('状态未更新:冻结金额退回失败,请检查用户钱包后重试。', 'aksrt'), 'error'],
'withdraw_failed' => [__('提现状态更新失败,请重试。重试不会重复入账。', 'aksrt'), 'error'],
'withdraw_processing' => [__('已标记为处理中,请尽快完成打款。', 'jio') . $suffix, 'success'],
'withdraw_completed' => [__('已确认打款完成,用户已收到通知。', 'jio') . $suffix, 'success'],
'withdraw_rejected' => [__('已拒绝提现,冻结金额已退回用户,拒绝原因已通知用户。', 'jio') . $suffix, 'success'],
'withdraw_note_required' => [__('拒绝提现必须填写拒绝原因。', 'jio'), 'warning'],
'withdraw_stale' => [__('操作未执行:该申请的状态已被更新,请查看最新状态后再处理。', 'jio'), 'warning'],
'withdraw_refund_failed' => [__('状态未更新:冻结金额退回失败,请检查用户钱包后重试。', 'jio'), 'error'],
'withdraw_failed' => [__('提现状态更新失败,请重试。重试不会重复入账。', 'jio'), 'error'],
];
if (!isset($map[$code])) {
return;
@@ -233,7 +233,7 @@ class PayWithdrawals {
private function ensure_cap(): void {
if (!current_user_can(self::CAPABILITY)) {
wp_die(esc_html__('您没有权限访问此页面。', 'aksrt'));
wp_die(esc_html__('您没有权限访问此页面。', 'jio'));
}
}
+98 -98
View File
@@ -4,12 +4,12 @@
*
* 作为 flat sibling trait 被 PayWithdrawals 使用(PSR-4:trait 必须与宿主类同目录)。
*
* @package AKSRT
* @package Jio
*/
namespace AKSRT\Admin\Native;
namespace Jio\Admin\Native;
use AKSRT\Payment\Withdrawal;
use Jio\Payment\Withdrawal;
defined('ABSPATH') || exit;
@@ -17,12 +17,12 @@ trait PayWithdrawalsList {
private function render_withdrawals_list_page(): void {
NativeUi::header(
__('提现审核', 'aksrt'),
__('用户发起的余额与推广佣金提现申请。按收款信息线下打款后,在这里确认完成;拒绝会把冻结金额退回原资金池。', 'aksrt'),
__('提现审核', 'jio'),
__('用户发起的余额与推广佣金提现申请。按收款信息线下打款后,在这里确认完成;拒绝会把冻结金额退回原资金池。', 'jio'),
'dashicons-money-alt',
[[
'label' => __('提现策略', 'aksrt'),
'url' => admin_url('admin.php?page=aksrt-settings#/payment/withdrawal'),
'label' => __('提现策略', 'jio'),
'url' => admin_url('admin.php?page=jio-settings#/payment/withdrawal'),
'icon' => 'dashicons-admin-generic',
]]
);
@@ -78,17 +78,17 @@ trait PayWithdrawalsList {
$this->render_status_views($filters['status'], $query_args, $counts);
$this->render_filter_card($filters, $query_args);
NativeUi::table_card_open(__('提现申请', 'aksrt'));
echo '<table class="wp-list-table widefat striped aksrt-wd-table">';
echo '<caption class="screen-reader-text">' . esc_html__('提现申请', 'aksrt') . '</caption><thead><tr>';
NativeUi::table_card_open(__('提现申请', 'jio'));
echo '<table class="wp-list-table widefat striped jio-wd-table">';
echo '<caption class="screen-reader-text">' . esc_html__('提现申请', 'jio') . '</caption><thead><tr>';
foreach ([
'number' => esc_html__('订单号', 'aksrt'),
'user' => esc_html__('用户', 'aksrt'),
'amount' => esc_html__('金额', 'aksrt'),
'payee' => esc_html__('收款信息', 'aksrt'),
'status' => esc_html__('状态', 'aksrt'),
'note' => esc_html__('备注', 'aksrt'),
'actions' => esc_html__('操作', 'aksrt'),
'number' => esc_html__('订单号', 'jio'),
'user' => esc_html__('用户', 'jio'),
'amount' => esc_html__('金额', 'jio'),
'payee' => esc_html__('收款信息', 'jio'),
'status' => esc_html__('状态', 'jio'),
'note' => esc_html__('备注', 'jio'),
'actions' => esc_html__('操作', 'jio'),
] as $key => $label) {
echo '<th scope="col" class="column-' . esc_attr($key) . '">' . $label . '</th>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped above.
}
@@ -96,9 +96,9 @@ trait PayWithdrawalsList {
if (!$items) {
$filtered = array_diff_key($query_args, ['status' => true]) !== [];
echo NativeUi::empty_row(7, __('暂无提现记录', 'aksrt'), $filtered // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
? __('没有符合当前筛选条件的申请,请调整或重置筛选。', 'aksrt')
: __('用户在用户中心发起余额或佣金提现后,申请会出现在这里。', 'aksrt'));
echo NativeUi::empty_row(7, __('暂无提现记录', 'jio'), $filtered // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
? __('没有符合当前筛选条件的申请,请调整或重置筛选。', 'jio')
: __('用户在用户中心发起余额或佣金提现后,申请会出现在这里。', 'jio'));
}
foreach ($items as $row) {
$this->render_withdrawal_row($row, $retry_id, $retry_key);
@@ -110,9 +110,9 @@ trait PayWithdrawalsList {
NativeUi::card_close();
$this->render_process_dialog();
echo '<div class="aksrt-wd-toast" role="status" aria-live="polite" data-aksrt-wd-toast'
. ' data-copied="' . esc_attr__('收款信息已复制', 'aksrt') . '"'
. ' data-copy-failed="' . esc_attr__('自动复制失败,请手动选中后复制。', 'aksrt') . '"></div>';
echo '<div class="jio-wd-toast" role="status" aria-live="polite" data-jio-wd-toast'
. ' data-copied="' . esc_attr__('收款信息已复制', 'jio') . '"'
. ' data-copy-failed="' . esc_attr__('自动复制失败,请手动选中后复制。', 'jio') . '"></div>';
}
private function render_withdrawal_row(array $row, int $retry_id, string $retry_key): void {
@@ -131,44 +131,44 @@ trait PayWithdrawalsList {
$user = $user_id > 0 ? get_userdata($user_id) : false;
echo '<tr' . ($retry_id === $id ? ' class="is-retry"' : '') . '>';
echo '<td class="column-number"><code class="aksrt-native-mono">' . esc_html($number !== '' ? $number : '#' . $id) . '</code>'
. '<div class="aksrt-wd-sub">' . esc_html(sprintf(__('%s 申请', 'aksrt'), (string) ($row['created_at'] ?? ''))) . '</div></td>';
echo '<td class="column-number"><code class="jio-native-mono">' . esc_html($number !== '' ? $number : '#' . $id) . '</code>'
. '<div class="jio-wd-sub">' . esc_html(sprintf(__('%s 申请', 'jio'), (string) ($row['created_at'] ?? ''))) . '</div></td>';
echo '<td class="column-user">' . NativeUi::user_cell($user_id) // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
. '<div class="aksrt-wd-sub">' . esc_html(sprintf(__('来源:%s', 'aksrt'), $source_label)) . '</div></td>';
. '<div class="jio-wd-sub">' . esc_html(sprintf(__('来源:%s', 'jio'), $source_label)) . '</div></td>';
echo '<td class="column-amount"><strong class="aksrt-native-amount">' . esc_html($this->format_money($net)) . '</strong>'
. '<div class="aksrt-wd-sub">' . esc_html(sprintf(
echo '<td class="column-amount"><strong class="jio-native-amount">' . esc_html($this->format_money($net)) . '</strong>'
. '<div class="jio-wd-sub">' . esc_html(sprintf(
/* translators: 1 gross 2 fee 3 fee percent */
__('申请 %1$s · 手续费 %2$s(%3$d%%)', 'aksrt'),
__('申请 %1$s · 手续费 %2$s(%3$d%%)', 'jio'),
$this->format_money($gross),
$this->format_money($this->fee_decimal($gross, $net)),
(int) ($row['fee_rate'] ?? 0)
)) . '</div></td>';
echo '<td class="column-payee"><span class="aksrt-wd-method aksrt-wd-method--' . esc_attr(sanitize_html_class($method)) . '">' . esc_html($method_label) . '</span>';
echo '<td class="column-payee"><span class="jio-wd-method jio-wd-method--' . esc_attr(sanitize_html_class($method)) . '">' . esc_html($method_label) . '</span>';
if ($account !== '') {
echo '<button type="button" class="aksrt-wd-account" data-clipboard-text="' . esc_attr($account) . '" aria-label="' . esc_attr(sprintf(__('复制收款信息:%s', 'aksrt'), $account)) . '">'
echo '<button type="button" class="jio-wd-account" data-clipboard-text="' . esc_attr($account) . '" aria-label="' . esc_attr(sprintf(__('复制收款信息:%s', 'jio'), $account)) . '">'
. '<span>' . esc_html($account) . '</span><span class="dashicons dashicons-admin-page" aria-hidden="true"></span></button>';
}
echo '</td>';
$processed = (string) ($row['processed_at'] ?? '');
echo '<td class="column-status">' . NativeUi::status_badge($status, $this->status_badge_map()) // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
. ($processed !== '' ? '<div class="aksrt-wd-sub">' . esc_html(sprintf(__('%s 处理', 'aksrt'), $processed)) . '</div>' : '') . '</td>';
. ($processed !== '' ? '<div class="jio-wd-sub">' . esc_html(sprintf(__('%s 处理', 'jio'), $processed)) . '</div>' : '') . '</td>';
echo '<td class="column-note">' . ($note !== ''
? '<div class="aksrt-wd-note">' . nl2br(esc_html($note)) . '</div>'
: '<span class="aksrt-native-muted">—</span>') . '</td>';
? '<div class="jio-wd-note">' . nl2br(esc_html($note)) . '</div>'
: '<span class="jio-native-muted">—</span>') . '</td>';
echo '<td class="column-actions">';
$next = $this->next_statuses($status);
if ($next === []) {
echo '<span class="aksrt-native-muted">—</span></td></tr>';
echo '<span class="jio-native-muted">—</span></td></tr>';
return;
}
$data = [
'id' => (string) $id,
'nonce' => wp_create_nonce('aksrt_pay_update_withdrawal_' . $id),
'nonce' => wp_create_nonce('jio_pay_update_withdrawal_' . $id),
'key' => $this->request_key_for($id, $retry_id, $retry_key),
'number' => $number !== '' ? $number : '#' . $id,
'user' => $user ? $user->display_name . ' #' . $user_id : '#' . $user_id,
@@ -182,38 +182,38 @@ trait PayWithdrawalsList {
$attrs .= ' data-' . $key . '="' . esc_attr($value) . '"';
}
$buttons = [
Withdrawal::STATUS_PROCESSING => [__('开始处理', 'aksrt'), 'button-primary'],
Withdrawal::STATUS_COMPLETED => [__('确认打款', 'aksrt'), 'button-primary'],
Withdrawal::STATUS_REJECTED => [__('拒绝', 'aksrt'), 'is-danger'],
Withdrawal::STATUS_PROCESSING => [__('开始处理', 'jio'), 'button-primary'],
Withdrawal::STATUS_COMPLETED => [__('确认打款', 'jio'), 'button-primary'],
Withdrawal::STATUS_REJECTED => [__('拒绝', 'jio'), 'is-danger'],
];
echo '<div class="aksrt-wd-actions">';
echo '<div class="jio-wd-actions">';
foreach ($next as $target) {
[$label, $class] = $buttons[$target];
echo '<button type="button" class="button button-small ' . esc_attr($class) . '" data-aksrt-wd-open="' . esc_attr($target) . '"' . $attrs // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped above.
. ' aria-haspopup="dialog" aria-controls="aksrt-wd-dialog">' . esc_html($label) . '</button>';
echo '<button type="button" class="button button-small ' . esc_attr($class) . '" data-jio-wd-open="' . esc_attr($target) . '"' . $attrs // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped above.
. ' aria-haspopup="dialog" aria-controls="jio-wd-dialog">' . esc_html($label) . '</button>';
}
echo '</div></td></tr>';
}
/** 处理流程 + 当前提现策略;有待办时用警示色提醒冻结金额。 */
private function render_process_callout(array $stats): void {
$settings = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_withdrawal_settings() : [];
$settings = class_exists('\Jio\Settings\Settings') ? \Jio\Settings\Settings::get_withdrawal_settings() : [];
$open = (int) ($stats['pending'] ?? 0) + (int) ($stats['processing'] ?? 0);
$policy = sprintf(
/* translators: 1 min amount 2 fee percent 3 daily count 4 daily limit */
__('当前策略:最低 %1$s,手续费 %2$d%%,每日 %3$d 次 / %4$s。', 'aksrt'),
__('当前策略:最低 %1$s,手续费 %2$d%%,每日 %3$d 次 / %4$s。', 'jio'),
$this->format_money((string) ($settings['min_amount'] ?? '0.00')),
(int) ($settings['service_charge_rate'] ?? 0),
(int) ($settings['daily_count'] ?? 0),
$this->format_money((string) ($settings['daily_limit'] ?? '0.00'))
);
$flow = __('处理流程:开始处理 → 按收款信息线下打款 → 确认打款。确认打款与拒绝都不可撤销。', 'aksrt');
$flow = __('处理流程:开始处理 → 按收款信息线下打款 → 确认打款。确认打款与拒绝都不可撤销。', 'jio');
echo '<div class="aksrt-native-callout' . ($open > 0 ? ' is-warning' : '') . '">';
echo '<div class="jio-native-callout' . ($open > 0 ? ' is-warning' : '') . '">';
if ($open > 0) {
echo '<strong>' . esc_html(sprintf(
/* translators: 1 open count 2 frozen amount */
__('%1$d 笔申请待办,冻结中 %2$s。', 'aksrt'),
__('%1$d 笔申请待办,冻结中 %2$s。', 'jio'),
$open,
$this->format_money((string) ($stats['open_gross'] ?? '0.00'))
)) . '</strong> ';
@@ -223,32 +223,32 @@ trait PayWithdrawalsList {
private function render_withdrawal_stats(array $stats): void {
NativeUi::stats([
['label' => __('待处理', 'aksrt'), 'value' => number_format_i18n((int) ($stats['pending'] ?? 0)), 'url' => $this->page_url(['status' => Withdrawal::STATUS_PENDING]), 'icon' => 'dashicons-clock', 'tone' => 'warning'],
['label' => __('处理中', 'aksrt'), 'value' => number_format_i18n((int) ($stats['processing'] ?? 0)), 'url' => $this->page_url(['status' => Withdrawal::STATUS_PROCESSING]), 'icon' => 'dashicons-update', 'tone' => 'info'],
['label' => __('冻结中金额', 'aksrt'), 'value' => $this->format_money((string) ($stats['open_gross'] ?? '0.00')), 'hint' => __('待处理 + 处理中的申请毛额', 'aksrt'), 'icon' => 'dashicons-lock', 'tone' => 'warning'],
['label' => __('已打款', 'aksrt'), 'value' => $this->format_money((string) ($stats['paid_net'] ?? '0.00')), 'url' => $this->page_url(['status' => Withdrawal::STATUS_COMPLETED]), 'hint' => sprintf(
['label' => __('待处理', 'jio'), 'value' => number_format_i18n((int) ($stats['pending'] ?? 0)), 'url' => $this->page_url(['status' => Withdrawal::STATUS_PENDING]), 'icon' => 'dashicons-clock', 'tone' => 'warning'],
['label' => __('处理中', 'jio'), 'value' => number_format_i18n((int) ($stats['processing'] ?? 0)), 'url' => $this->page_url(['status' => Withdrawal::STATUS_PROCESSING]), 'icon' => 'dashicons-update', 'tone' => 'info'],
['label' => __('冻结中金额', 'jio'), 'value' => $this->format_money((string) ($stats['open_gross'] ?? '0.00')), 'hint' => __('待处理 + 处理中的申请毛额', 'jio'), 'icon' => 'dashicons-lock', 'tone' => 'warning'],
['label' => __('已打款', 'jio'), 'value' => $this->format_money((string) ($stats['paid_net'] ?? '0.00')), 'url' => $this->page_url(['status' => Withdrawal::STATUS_COMPLETED]), 'hint' => sprintf(
/* translators: 1 completed count 2 fee total */
__('%1$s 笔 · 手续费 %2$s', 'aksrt'),
__('%1$s 笔 · 手续费 %2$s', 'jio'),
number_format_i18n((int) ($stats['completed'] ?? 0)),
$this->format_money((string) ($stats['paid_fee'] ?? '0.00'))
), 'icon' => 'dashicons-yes-alt', 'tone' => 'success'],
['label' => __('已拒绝', 'aksrt'), 'value' => number_format_i18n((int) ($stats['rejected'] ?? 0)), 'url' => $this->page_url(['status' => Withdrawal::STATUS_REJECTED]), 'icon' => 'dashicons-no-alt', 'tone' => 'danger'],
['label' => __('已拒绝', 'jio'), 'value' => number_format_i18n((int) ($stats['rejected'] ?? 0)), 'url' => $this->page_url(['status' => Withdrawal::STATUS_REJECTED]), 'icon' => 'dashicons-no-alt', 'tone' => 'danger'],
]);
}
/** @param array<string, int> $counts */
private function render_status_views(string $current, array $query_args, array $counts): void {
$base = array_diff_key($query_args, ['status' => true]);
$views = ['' => [__('全部', 'aksrt'), array_sum($counts)]];
$views = ['' => [__('全部', 'jio'), array_sum($counts)]];
foreach ($this->status_labels() as $key => $label) {
$views[$key] = [$label, (int) ($counts[$key] ?? 0)];
}
echo '<nav class="aksrt-native-tabs aksrt-wd-views" aria-label="' . esc_attr__('按处理状态查看', 'aksrt') . '">';
echo '<nav class="jio-native-tabs jio-wd-views" aria-label="' . esc_attr__('按处理状态查看', 'jio') . '">';
foreach ($views as $key => [$label, $count]) {
$url = $this->page_url($base + ($key !== '' ? ['status' => $key] : []));
$active = $current === $key;
echo '<a class="aksrt-native-tabs__link' . ($active ? ' is-active' : '') . '" href="' . esc_url($url) . '"' . ($active ? ' aria-current="page"' : '') . '>'
echo '<a class="jio-native-tabs__link' . ($active ? ' is-active' : '') . '" href="' . esc_url($url) . '"' . ($active ? ' aria-current="page"' : '') . '>'
. esc_html($label) . ' <span class="count">' . esc_html(number_format_i18n($count)) . '</span></a>';
}
echo '</nav>';
@@ -259,28 +259,28 @@ trait PayWithdrawalsList {
* @param array<string, string|int> $query_args
*/
private function render_filter_card(array $filters, array $query_args): void {
NativeUi::card_open('', '', false, 'aksrt-wd-filter-card');
echo '<form method="get" class="aksrt-wd-filters" role="search">';
NativeUi::card_open('', '', false, 'jio-wd-filter-card');
echo '<form method="get" class="jio-wd-filters" role="search">';
echo '<input type="hidden" name="page" value="' . esc_attr(self::PAGE_SLUG) . '" />';
if ($filters['status'] !== '') {
echo '<input type="hidden" name="status" value="' . esc_attr($filters['status']) . '" />';
}
echo '<label class="aksrt-wd-filters__search"><span class="screen-reader-text">' . esc_html__('订单号 / 用户 ID', 'aksrt') . '</span>'
. '<input type="search" name="s" value="' . esc_attr($filters['keyword']) . '" placeholder="' . esc_attr__('搜索订单号 / 用户 ID', 'aksrt') . '" /></label>';
echo $this->filter_select('source', __('提现来源', 'aksrt'), ['' => __('全部来源', 'aksrt')] + $this->source_labels(), $filters['source']); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo $this->filter_select('method', __('收款方式', 'aksrt'), ['' => __('全部收款方式', 'aksrt')] + $this->method_labels(), $filters['method']); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo '<span class="aksrt-wd-filters__range">'
. '<label><span class="screen-reader-text">' . esc_html__('开始日期', 'aksrt') . '</span><input type="date" name="date_from" value="' . esc_attr($filters['date_from']) . '" /></label>'
echo '<label class="jio-wd-filters__search"><span class="screen-reader-text">' . esc_html__('订单号 / 用户 ID', 'jio') . '</span>'
. '<input type="search" name="s" value="' . esc_attr($filters['keyword']) . '" placeholder="' . esc_attr__('搜索订单号 / 用户 ID', 'jio') . '" /></label>';
echo $this->filter_select('source', __('提现来源', 'jio'), ['' => __('全部来源', 'jio')] + $this->source_labels(), $filters['source']); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo $this->filter_select('method', __('收款方式', 'jio'), ['' => __('全部收款方式', 'jio')] + $this->method_labels(), $filters['method']); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo '<span class="jio-wd-filters__range">'
. '<label><span class="screen-reader-text">' . esc_html__('开始日期', 'jio') . '</span><input type="date" name="date_from" value="' . esc_attr($filters['date_from']) . '" /></label>'
. '<span aria-hidden="true">–</span>'
. '<label><span class="screen-reader-text">' . esc_html__('结束日期', 'aksrt') . '</span><input type="date" name="date_to" value="' . esc_attr($filters['date_to']) . '" /></label>'
. '<label><span class="screen-reader-text">' . esc_html__('结束日期', 'jio') . '</span><input type="date" name="date_to" value="' . esc_attr($filters['date_to']) . '" /></label>'
. '</span>';
echo '<label><span class="screen-reader-text">' . esc_html__('用户 ID', 'aksrt') . '</span><input type="number" min="1" name="user_id" class="aksrt-wd-filters__id" value="'
. esc_attr($filters['user_id'] > 0 ? (string) $filters['user_id'] : '') . '" placeholder="' . esc_attr__('用户 ID', 'aksrt') . '" /></label>';
echo '<span class="aksrt-wd-filters__submit"><button type="submit" class="button button-primary">' . esc_html__('筛选', 'aksrt') . '</button>';
echo '<label><span class="screen-reader-text">' . esc_html__('用户 ID', 'jio') . '</span><input type="number" min="1" name="user_id" class="jio-wd-filters__id" value="'
. esc_attr($filters['user_id'] > 0 ? (string) $filters['user_id'] : '') . '" placeholder="' . esc_attr__('用户 ID', 'jio') . '" /></label>';
echo '<span class="jio-wd-filters__submit"><button type="submit" class="button button-primary">' . esc_html__('筛选', 'jio') . '</button>';
if (array_diff_key($query_args, ['status' => true]) !== []) {
$reset = $this->page_url($filters['status'] !== '' ? ['status' => $filters['status']] : []);
echo '<a class="button" href="' . esc_url($reset) . '">' . esc_html__('重置', 'aksrt') . '</a>';
echo '<a class="button" href="' . esc_url($reset) . '">' . esc_html__('重置', 'jio') . '</a>';
}
echo '</span></form>';
NativeUi::card_close();
@@ -300,9 +300,9 @@ trait PayWithdrawalsList {
* 三种目标状态各自的说明与备注要求用 data-mode 区分。
*/
private function render_process_dialog(): void {
echo '<dialog class="aksrt-wd-dialog" id="aksrt-wd-dialog" aria-labelledby="aksrt-wd-dialog-title">';
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" class="aksrt-wd-dialog__frame" data-aksrt-wd-form data-busy-label="' . esc_attr__('正在提交…', 'aksrt') . '">';
echo '<input type="hidden" name="action" value="aksrt_pay_update_withdrawal" />';
echo '<dialog class="jio-wd-dialog" id="jio-wd-dialog" aria-labelledby="jio-wd-dialog-title">';
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" class="jio-wd-dialog__frame" data-jio-wd-form data-busy-label="' . esc_attr__('正在提交…', 'jio') . '">';
echo '<input type="hidden" name="action" value="jio_pay_update_withdrawal" />';
echo '<input type="hidden" name="withdrawal_id" value="" data-field="id" />';
echo '<input type="hidden" name="new_status" value="" data-field="status" />';
echo '<input type="hidden" name="idempotency_key" value="" data-field="key" />';
@@ -310,52 +310,52 @@ trait PayWithdrawalsList {
wp_referer_field();
$titles = [
Withdrawal::STATUS_PROCESSING => __('开始处理提现', 'aksrt'),
Withdrawal::STATUS_COMPLETED => __('确认已打款', 'aksrt'),
Withdrawal::STATUS_REJECTED => __('拒绝提现', 'aksrt'),
Withdrawal::STATUS_PROCESSING => __('开始处理提现', 'jio'),
Withdrawal::STATUS_COMPLETED => __('确认已打款', 'jio'),
Withdrawal::STATUS_REJECTED => __('拒绝提现', 'jio'),
];
echo '<header class="aksrt-wd-dialog__head"><h2 id="aksrt-wd-dialog-title">';
echo '<header class="jio-wd-dialog__head"><h2 id="jio-wd-dialog-title">';
foreach ($titles as $mode => $title) {
echo '<span data-mode="' . esc_attr($mode) . '" hidden>' . esc_html($title) . '</span>';
}
echo '</h2><button type="button" class="aksrt-wd-dialog__close" data-aksrt-wd-close aria-label="' . esc_attr__('关闭弹窗', 'aksrt') . '"><span class="dashicons dashicons-no-alt" aria-hidden="true"></span></button></header>';
echo '</h2><button type="button" class="jio-wd-dialog__close" data-jio-wd-close aria-label="' . esc_attr__('关闭弹窗', 'jio') . '"><span class="dashicons dashicons-no-alt" aria-hidden="true"></span></button></header>';
echo '<div class="aksrt-wd-dialog__body"><dl class="aksrt-wd-summary">';
echo '<div class="jio-wd-dialog__body"><dl class="jio-wd-summary">';
foreach ([
'number' => __('订单号', 'aksrt'),
'user' => __('用户', 'aksrt'),
'net' => __('应打款', 'aksrt'),
'gross' => __('申请金额', 'aksrt'),
'source' => __('来源', 'aksrt'),
'payee' => __('收款信息', 'aksrt'),
'number' => __('订单号', 'jio'),
'user' => __('用户', 'jio'),
'net' => __('应打款', 'jio'),
'gross' => __('申请金额', 'jio'),
'source' => __('来源', 'jio'),
'payee' => __('收款信息', 'jio'),
] as $field => $label) {
echo '<div class="aksrt-wd-summary__item aksrt-wd-summary__item--' . esc_attr($field) . '"><dt>' . esc_html($label) . '</dt><dd data-summary="' . esc_attr($field) . '"></dd></div>';
echo '<div class="jio-wd-summary__item jio-wd-summary__item--' . esc_attr($field) . '"><dt>' . esc_html($label) . '</dt><dd data-summary="' . esc_attr($field) . '"></dd></div>';
}
echo '</dl>';
$hints = [
Withdrawal::STATUS_PROCESSING => [__('标记为处理中后,请按上方收款信息线下打款,完成后回到这里确认打款。', 'aksrt'), ''],
Withdrawal::STATUS_COMPLETED => [__('请确认已向用户实际打出「应打款」金额。确认后状态不可更改,用户会收到完成通知。', 'aksrt'), 'is-warning'],
Withdrawal::STATUS_REJECTED => [__('拒绝后,冻结的申请金额会退回用户的原资金池,拒绝原因会通知用户。此操作不可撤销。', 'aksrt'), 'is-danger'],
Withdrawal::STATUS_PROCESSING => [__('标记为处理中后,请按上方收款信息线下打款,完成后回到这里确认打款。', 'jio'), ''],
Withdrawal::STATUS_COMPLETED => [__('请确认已向用户实际打出「应打款」金额。确认后状态不可更改,用户会收到完成通知。', 'jio'), 'is-warning'],
Withdrawal::STATUS_REJECTED => [__('拒绝后,冻结的申请金额会退回用户的原资金池,拒绝原因会通知用户。此操作不可撤销。', 'jio'), 'is-danger'],
];
foreach ($hints as $mode => [$hint, $tone]) {
echo '<p class="aksrt-native-callout ' . esc_attr($tone) . '" data-mode="' . esc_attr($mode) . '" hidden>' . esc_html($hint) . '</p>';
echo '<p class="jio-native-callout ' . esc_attr($tone) . '" data-mode="' . esc_attr($mode) . '" hidden>' . esc_html($hint) . '</p>';
}
echo '<label class="aksrt-native-field"><span class="aksrt-native-field__label">'
. '<span data-mode="' . esc_attr(Withdrawal::STATUS_REJECTED) . '" hidden>' . esc_html__('拒绝原因(必填)', 'aksrt') . '</span>'
. '<span data-mode="' . esc_attr(Withdrawal::STATUS_PROCESSING) . ' ' . esc_attr(Withdrawal::STATUS_COMPLETED) . '" hidden>' . esc_html__('备注(选填)', 'aksrt') . '</span>'
. '</span><textarea name="note" rows="3" maxlength="1000" data-aksrt-wd-note placeholder="' . esc_attr__('如打款流水号、拒绝原因等,追加记录在申请上', 'aksrt') . '"></textarea></label>';
echo '<label class="jio-native-field"><span class="jio-native-field__label">'
. '<span data-mode="' . esc_attr(Withdrawal::STATUS_REJECTED) . '" hidden>' . esc_html__('拒绝原因(必填)', 'jio') . '</span>'
. '<span data-mode="' . esc_attr(Withdrawal::STATUS_PROCESSING) . ' ' . esc_attr(Withdrawal::STATUS_COMPLETED) . '" hidden>' . esc_html__('备注(选填)', 'jio') . '</span>'
. '</span><textarea name="note" rows="3" maxlength="1000" data-jio-wd-note placeholder="' . esc_attr__('如打款流水号、拒绝原因等,追加记录在申请上', 'jio') . '"></textarea></label>';
echo '</div>';
$submits = [
Withdrawal::STATUS_PROCESSING => [__('开始处理', 'aksrt'), 'button-primary'],
Withdrawal::STATUS_COMPLETED => [__('确认已打款', 'aksrt'), 'button-primary'],
Withdrawal::STATUS_REJECTED => [__('确认拒绝', 'aksrt'), 'button-primary is-danger'],
Withdrawal::STATUS_PROCESSING => [__('开始处理', 'jio'), 'button-primary'],
Withdrawal::STATUS_COMPLETED => [__('确认已打款', 'jio'), 'button-primary'],
Withdrawal::STATUS_REJECTED => [__('确认拒绝', 'jio'), 'button-primary is-danger'],
];
echo '<footer class="aksrt-wd-dialog__foot"><button type="button" class="button" data-aksrt-wd-close>' . esc_html__('取消', 'aksrt') . '</button>';
echo '<footer class="jio-wd-dialog__foot"><button type="button" class="button" data-jio-wd-close>' . esc_html__('取消', 'jio') . '</button>';
foreach ($submits as $mode => [$label, $class]) {
echo '<button type="submit" class="button ' . esc_attr($class) . '" data-mode="' . esc_attr($mode) . '" data-aksrt-wd-submit hidden>' . esc_html($label) . '</button>';
echo '<button type="submit" class="button ' . esc_attr($class) . '" data-mode="' . esc_attr($mode) . '" data-jio-wd-submit hidden>' . esc_html($label) . '</button>';
}
echo '</footer></form></dialog>';
}
+48 -48
View File
@@ -8,10 +8,10 @@
* DomainModel::default_post_meta() or the custom-filter settings — the
* post-list-quick-edit contract test enforces that pairing.
*
* @package AKSRT
* @package Jio
*/
namespace AKSRT\Admin\Native;
namespace Jio\Admin\Native;
defined('ABSPATH') || exit;
@@ -27,40 +27,40 @@ class PostListEditFields {
[
'id' => 'views',
'type' => 'number',
'title' => __('阅读量', 'aksrt'),
'title' => __('阅读量', 'jio'),
],
[
'id' => 'like',
'type' => 'number',
'title' => __('点赞数', 'aksrt'),
'title' => __('点赞数', 'jio'),
],
[
'id' => 'show_layout',
'type' => 'radio',
'title' => __('显示布局', 'aksrt'),
'title' => __('显示布局', 'jio'),
'default' => 'false',
'options' => [
'false' => __('跟随主题', 'aksrt'),
'no_sidebar' => __('无侧边栏', 'aksrt'),
'sidebar_left' => __('侧边栏靠左', 'aksrt'),
'sidebar_right' => __('侧边栏靠右', 'aksrt'),
'false' => __('跟随主题', 'jio'),
'no_sidebar' => __('无侧边栏', 'jio'),
'sidebar_left' => __('侧边栏靠左', 'jio'),
'sidebar_right' => __('侧边栏靠右', 'jio'),
],
],
[
'id' => 'no_article-navs',
'type' => 'checkbox',
'label' => __('不显示目录树', 'aksrt'),
'label' => __('不显示目录树', 'jio'),
],
[
'id' => 'article_maxheight_xz',
'type' => 'checkbox',
'label' => __('限制内容最大高度', 'aksrt'),
'label' => __('限制内容最大高度', 'jio'),
],
];
}
/**
* Pay parameters (saved into the _aksrt_pay meta bag).
* Pay parameters (saved into the _jio_pay meta bag).
*
* @return array<int,array<string,mixed>>
*/
@@ -69,132 +69,132 @@ class PostListEditFields {
[
'id' => 'pay_type',
'type' => 'radio',
'title' => __('付费模式', 'aksrt'),
'options' => \AKSRT\Core\DomainModel::pay_type_labels(),
'title' => __('付费模式', 'jio'),
'options' => \Jio\Core\DomainModel::pay_type_labels(),
],
[
'id' => 'pay_limit',
'type' => 'radio',
'title' => __('购买权限', 'aksrt'),
'title' => __('购买权限', 'jio'),
'options' => [
'0' => __('所有人可购买', 'aksrt'),
'1' => __('VIP1 及以上会员可购买', 'aksrt'),
'2' => __('仅 VIP2 可购买', 'aksrt'),
'0' => __('所有人可购买', 'jio'),
'1' => __('VIP1 及以上会员可购买', 'jio'),
'2' => __('仅 VIP2 可购买', 'jio'),
],
],
[
'id' => 'pay_modo',
'type' => 'radio',
'title' => __('支付类型', 'aksrt'),
'title' => __('支付类型', 'jio'),
'options' => [
'0' => __('普通商品(金钱购买)', 'aksrt'),
'points' => __('积分商品(积分购买,依赖于用户积分功能)', 'aksrt'),
'0' => __('普通商品(金钱购买)', 'jio'),
'points' => __('积分商品(积分购买,依赖于用户积分功能)', 'jio'),
],
],
[
'dependency' => [['pay_type', '!=', 'no'], ['pay_modo', '==', 'points']],
'id' => 'points_price',
'title' => __('积分售价', 'aksrt'),
'title' => __('积分售价', 'jio'),
'type' => 'number',
'unit' => __('积分', 'aksrt'),
'unit' => __('积分', 'jio'),
],
[
'dependency' => [['pay_type', '!=', 'no'], ['pay_modo', '==', 'points']],
'title' => __('VIP1 积分售价', 'aksrt'),
'title' => __('VIP1 积分售价', 'jio'),
'id' => 'vip1_points',
'class' => 'compact',
'subtitle' => __('填 0 则为 VIP1 免费', 'aksrt'),
'subtitle' => __('填 0 则为 VIP1 免费', 'jio'),
'type' => 'number',
'unit' => __('积分', 'aksrt'),
'unit' => __('积分', 'jio'),
],
[
'dependency' => [['pay_type', '!=', 'no'], ['pay_modo', '==', 'points']],
'title' => __('VIP2 积分售价', 'aksrt'),
'title' => __('VIP2 积分售价', 'jio'),
'id' => 'vip2_points',
'class' => 'compact',
'subtitle' => __('填 0 则为 VIP2 免费', 'aksrt'),
'desc' => __('会员价格不能高于售价', 'aksrt'),
'subtitle' => __('填 0 则为 VIP2 免费', 'jio'),
'desc' => __('会员价格不能高于售价', 'jio'),
'type' => 'number',
'unit' => __('积分', 'aksrt'),
'unit' => __('积分', 'jio'),
],
[
'dependency' => [['pay_type', '!=', 'no'], ['pay_modo', '!=', 'points']],
'id' => 'price',
'title' => __('执行价', 'aksrt'),
'title' => __('执行价', 'jio'),
'type' => 'number',
'unit' => $currency,
],
[
'dependency' => [['pay_type', '!=', 'no'], ['pay_modo', '!=', 'points']],
'id' => 'original_price',
'title' => __('原价', 'aksrt'),
'title' => __('原价', 'jio'),
'class' => 'compact',
'subtitle' => __('显示在执行价格前面,并划掉', 'aksrt'),
'subtitle' => __('显示在执行价格前面,并划掉', 'jio'),
'type' => 'number',
'unit' => $currency,
],
[
'dependency' => [['pay_type', '!=', 'no'], ['pay_modo', '!=', 'points']],
'title' => __('VIP1 价格', 'aksrt'),
'title' => __('VIP1 价格', 'jio'),
'id' => 'vip1_price',
'class' => 'compact',
'subtitle' => __('填 0 则为 VIP1 免费', 'aksrt'),
'subtitle' => __('填 0 则为 VIP1 免费', 'jio'),
'type' => 'number',
'unit' => $currency,
],
[
'dependency' => [['pay_type', '!=', 'no'], ['pay_modo', '!=', 'points']],
'title' => __('VIP2 价格', 'aksrt'),
'title' => __('VIP2 价格', 'jio'),
'id' => 'vip2_price',
'class' => 'compact',
'subtitle' => __('填 0 则为 VIP2 免费', 'aksrt'),
'desc' => __('会员价格不能高于执行价', 'aksrt'),
'subtitle' => __('填 0 则为 VIP2 免费', 'jio'),
'desc' => __('会员价格不能高于执行价', 'jio'),
'type' => 'number',
'unit' => $currency,
],
[
'dependency' => [['pay_type', '!=', 'no']],
'id' => 'pay_count',
'title' => __('销量浮动', 'aksrt'),
'subtitle' => __('为真实销量增加或减少的数量', 'aksrt'),
'title' => __('销量浮动', 'jio'),
'subtitle' => __('为真实销量增加或减少的数量', 'jio'),
'type' => 'number',
],
[
'dependency' => [['pay_type', '!=', 'no']],
'id' => 'show_sales',
'title' => __('显示销量', 'aksrt'),
'title' => __('显示销量', 'jio'),
'default' => true,
'type' => 'switcher',
],
[
'dependency' => [['pay_type', '!=', 'no']],
'id' => 'coupon_s',
'title' => __('允许使用优惠码', 'aksrt'),
'title' => __('允许使用优惠码', 'jio'),
'default' => false,
'type' => 'switcher',
],
[
'dependency' => [['pay_type', '!=', 'no']],
'id' => 'order_expired_s',
'title' => __('订单时效', 'aksrt'),
'title' => __('订单时效', 'jio'),
'default' => false,
'type' => 'switcher',
],
[
'id' => 'order_expired_time',
'title' => __('订单时效有效期数值', 'aksrt'),
'title' => __('订单时效有效期数值', 'jio'),
'default' => 0,
'type' => 'number',
],
[
'title' => __('订单时效有效期单位', 'aksrt'),
'title' => __('订单时效有效期单位', 'jio'),
'type' => 'select',
'class' => 'compact',
'id' => 'order_expired_unit',
'options' => [
'day' => __('天', 'aksrt'),
'hour' => __('小时', 'aksrt'),
'minute' => __('分钟', 'aksrt'),
'day' => __('天', 'jio'),
'hour' => __('小时', 'jio'),
'minute' => __('分钟', 'jio'),
],
'default' => 'day',
],
+93 -93
View File
@@ -9,17 +9,17 @@
* those boxes onto its topics taxonomy column; this project has no topics
* taxonomy, so they hang off the interaction-data column instead.
*
* @package AKSRT
* @package Jio
*/
namespace AKSRT\Admin\Native;
namespace Jio\Admin\Native;
defined('ABSPATH') || exit;
class PostListEnhance {
public const COLUMN_STATS = 'aksrt_stats';
public const COLUMN_SALES = 'aksrt_sales';
public const COLUMN_STATS = 'jio_stats';
public const COLUMN_SALES = 'jio_sales';
private static $instance = null;
@@ -61,17 +61,17 @@ class PostListEnhance {
return;
}
if (function_exists('aksrt_enqueue_fontawesome')) {
aksrt_enqueue_fontawesome();
if (function_exists('jio_enqueue_fontawesome')) {
jio_enqueue_fontawesome();
}
$css = AKSRT_DIR . '/assets/css/admin-main.css';
$css = JIO_DIR . '/assets/css/admin-main.css';
if (is_file($css)) {
wp_enqueue_style('aksrt-admin-main', AKSRT_URI . '/assets/css/admin-main.css', ['aksrt-font-awesome'], (string) filemtime($css));
wp_enqueue_style('jio-admin-main', JIO_URI . '/assets/css/admin-main.css', ['jio-font-awesome'], (string) filemtime($css));
}
$js = AKSRT_DIR . '/assets/js/admin-post-list.js';
$js = JIO_DIR . '/assets/js/admin-post-list.js';
if (is_file($js)) {
wp_enqueue_script('aksrt-post-list', AKSRT_URI . '/assets/js/admin-post-list.js', [], (string) filemtime($js), true);
wp_enqueue_script('jio-post-list', JIO_URI . '/assets/js/admin-post-list.js', [], (string) filemtime($js), true);
}
}
@@ -102,8 +102,8 @@ class PostListEnhance {
if ($key !== 'title') {
continue;
}
$new_columns[self::COLUMN_STATS] = __('互动数据', 'aksrt');
$new_columns[self::COLUMN_SALES] = '<a href="' . esc_url(add_query_arg(['orderby' => 'sales_volume', 'order' => $order])) . '"><span>' . esc_html__('销售数据', 'aksrt') . ($order_by === 'sales_volume' ? $o_icon : '') . '</span></a>';
$new_columns[self::COLUMN_STATS] = __('互动数据', 'jio');
$new_columns[self::COLUMN_SALES] = '<a href="' . esc_url(add_query_arg(['orderby' => 'sales_volume', 'order' => $order])) . '"><span>' . esc_html__('销售数据', 'jio') . ($order_by === 'sales_volume' ? $o_icon : '') . '</span></a>';
}
return $new_columns;
@@ -124,13 +124,13 @@ class PostListEnhance {
}
private function render_stats_column(int $post_id): void {
$views = function_exists('aksrt_get_post_views') ? (int) aksrt_get_post_views($post_id) : (int) get_post_meta($post_id, \AKSRT\Core\DomainModel::POST_VIEW_COUNT_KEY, true);
$likes = function_exists('aksrt_get_post_likes') ? (int) aksrt_get_post_likes($post_id) : (int) get_post_meta($post_id, '_aksrt_like_count', true);
$favorites = function_exists('aksrt_get_post_favorites') ? (int) aksrt_get_post_favorites($post_id) : (int) get_post_meta($post_id, '_aksrt_favorite_count', true);
$views = function_exists('jio_get_post_views') ? (int) jio_get_post_views($post_id) : (int) get_post_meta($post_id, \Jio\Core\DomainModel::POST_VIEW_COUNT_KEY, true);
$likes = function_exists('jio_get_post_likes') ? (int) jio_get_post_likes($post_id) : (int) get_post_meta($post_id, '_jio_like_count', true);
$favorites = function_exists('jio_get_post_favorites') ? (int) jio_get_post_favorites($post_id) : (int) get_post_meta($post_id, '_jio_favorite_count', true);
$format = static function (int $n): string {
if (function_exists('aksrt_format_number')) {
return (string) aksrt_format_number($n);
if (function_exists('jio_format_number')) {
return (string) jio_format_number($n);
}
if ($n >= 10000) {
return round($n / 10000, 1) . 'W+';
@@ -142,45 +142,45 @@ class PostListEnhance {
};
$parts = [];
$parts[] = '<span title="' . esc_attr__('浏览', 'aksrt') . '" style="margin-right:4px;"><i class="fa fas fa-eye" style="color:#8a909c;font-size:12px;"></i> ' . esc_html($format($views)) . '</span>';
$parts[] = '<span title="' . esc_attr__('点赞', 'aksrt') . '" style="margin-right:4px;"><i class="fa fas fa-thumbs-up" style="color:#1890ff;font-size:12px;"></i> ' . esc_html($format($likes)) . '</span>';
$parts[] = '<span title="' . esc_attr__('收藏', 'aksrt') . '" style="margin-right:4px;"><i class="fa fas fa-star" style="color:#f39c12;font-size:12px;"></i> ' . esc_html($format($favorites)) . '</span>';
$parts[] = '<span title="' . esc_attr__('浏览', 'jio') . '" style="margin-right:4px;"><i class="fa fas fa-eye" style="color:#8a909c;font-size:12px;"></i> ' . esc_html($format($views)) . '</span>';
$parts[] = '<span title="' . esc_attr__('点赞', 'jio') . '" style="margin-right:4px;"><i class="fa fas fa-thumbs-up" style="color:#1890ff;font-size:12px;"></i> ' . esc_html($format($likes)) . '</span>';
$parts[] = '<span title="' . esc_attr__('收藏', 'jio') . '" style="margin-right:4px;"><i class="fa fas fa-star" style="color:#f39c12;font-size:12px;"></i> ' . esc_html($format($favorites)) . '</span>';
echo implode(' &middot; ', $parts);
}
private function render_sales_column(int $post_id): void {
$pay_meta = function_exists('aksrt_get_pay_meta') ? aksrt_get_pay_meta($post_id) : [];
$pay_meta = function_exists('jio_get_pay_meta') ? jio_get_pay_meta($post_id) : [];
$pay_type = (string) ($pay_meta['pay_type'] ?? '');
if ($pay_type === '' || $pay_type === \AKSRT\Core\DomainModel::PAY_TYPE_NO) {
echo '<div style="font-size: 12px;">' . esc_html__('未开启付费', 'aksrt') . '</div>';
if ($pay_type === '' || $pay_type === \Jio\Core\DomainModel::PAY_TYPE_NO) {
echo '<div style="font-size: 12px;">' . esc_html__('未开启付费', 'jio') . '</div>';
return;
}
$pay_modo = (string) ($pay_meta['pay_modo'] ?? '0');
$type_labels = \AKSRT\Core\DomainModel::pay_type_labels();
$type_labels = \Jio\Core\DomainModel::pay_type_labels();
$type_label = $type_labels[$pay_type] ?? $pay_type;
$type_color = $this->pay_type_color($pay_type);
if ($pay_modo === 'points') {
$price_text = sprintf(__('积分:%s', 'aksrt'), (string) max(0, (int) ($pay_meta['points_price'] ?? 0)));
$price_text = sprintf(__('积分:%s', 'jio'), (string) max(0, (int) ($pay_meta['points_price'] ?? 0)));
} else {
$price_text = $this->currency_symbol() . ':' . (string) ($pay_meta['price'] ?? '0.00');
}
// 列表列与前台同一口径:真实订单 + 后台「销量浮动」(pay_count)。
// 真实销量取订单表统计,_aksrt_sales_volume 只是订单流水累加的副本。
// 真实销量取订单表统计,_jio_sales_volume 只是订单流水累加的副本。
$real_sales = $this->real_sales_count($post_id);
$meta_sales = max(0, (int) get_post_meta($post_id, \AKSRT\Core\DomainModel::RESOURCE_SALES_VOLUME_KEY, true));
$meta_sales = max(0, (int) get_post_meta($post_id, \Jio\Core\DomainModel::RESOURCE_SALES_VOLUME_KEY, true));
$sales_volume = max($real_sales, $meta_sales) + max(0, (int) ($pay_meta['pay_count'] ?? 0));
$real_html = $real_sales > 0
? ' <a title="' . esc_attr__('点击查看销售明细', 'aksrt') . '" href="' . esc_url($this->orders_url($post_id)) . '">[' . esc_html(sprintf(__('真实销量%s', 'aksrt'), $real_sales)) . ']</a>'
? ' <a title="' . esc_attr__('点击查看销售明细', 'jio') . '" href="' . esc_url($this->orders_url($post_id)) . '">[' . esc_html(sprintf(__('真实销量%s', 'jio'), $real_sales)) . ']</a>'
: '';
$link = add_query_arg(['aksrt_pay_type' => $pay_type]);
$link = add_query_arg(['jio_pay_type' => $pay_type]);
echo '<div>';
echo '<a style="color: ' . esc_attr($type_color) . ';" href="' . esc_url($link) . '">' . esc_html($type_label) . '</a>';
echo '<div style="font-size: 12px;">' . esc_html($price_text) . ' · ' . esc_html__('销量', 'aksrt') . esc_html((string) $sales_volume) . wp_kses_post($real_html) . '</div>';
echo '<div style="font-size: 12px;">' . esc_html($price_text) . ' · ' . esc_html__('销量', 'jio') . esc_html((string) $sales_volume) . wp_kses_post($real_html) . '</div>';
echo '</div>';
}
@@ -189,11 +189,11 @@ class PostListEnhance {
*/
private function pay_type_color(string $pay_type): string {
$palette = [
\AKSRT\Core\DomainModel::PAY_TYPE_READING => '#ff4747',
\AKSRT\Core\DomainModel::PAY_TYPE_DOWNLOAD => '#ee5307',
\AKSRT\Core\DomainModel::PAY_TYPE_GALLERY => '#1e8608',
\AKSRT\Core\DomainModel::PAY_TYPE_VIDEO => '#086ae8',
\AKSRT\Core\DomainModel::PAY_TYPE_VIP => '#3353fd',
\Jio\Core\DomainModel::PAY_TYPE_READING => '#ff4747',
\Jio\Core\DomainModel::PAY_TYPE_DOWNLOAD => '#ee5307',
\Jio\Core\DomainModel::PAY_TYPE_GALLERY => '#1e8608',
\Jio\Core\DomainModel::PAY_TYPE_VIDEO => '#086ae8',
\Jio\Core\DomainModel::PAY_TYPE_VIP => '#3353fd',
];
return $palette[$pay_type] ?? '#464646';
}
@@ -207,12 +207,12 @@ class PostListEnhance {
return;
}
$current = isset($_GET['aksrt_pay_type']) ? sanitize_key((string) wp_unslash($_GET['aksrt_pay_type'])) : '';
$labels = \AKSRT\Core\DomainModel::pay_type_labels();
unset($labels[\AKSRT\Core\DomainModel::PAY_TYPE_NO]);
$current = isset($_GET['jio_pay_type']) ? sanitize_key((string) wp_unslash($_GET['jio_pay_type'])) : '';
$labels = \Jio\Core\DomainModel::pay_type_labels();
unset($labels[\Jio\Core\DomainModel::PAY_TYPE_NO]);
echo '<select name="aksrt_pay_type">';
echo '<option value="">' . esc_html__('商品类型', 'aksrt') . '</option>';
echo '<select name="jio_pay_type">';
echo '<option value="">' . esc_html__('商品类型', 'jio') . '</option>';
foreach ($labels as $key => $label) {
printf('<option %s value="%s">%s</option>', selected($current, $key, false), esc_attr($key), esc_html($label));
}
@@ -232,25 +232,25 @@ class PostListEnhance {
$orderby = isset($_GET['orderby']) ? sanitize_key((string) wp_unslash($_GET['orderby'])) : '';
if ($orderby === 'sales_volume') {
$query->set('meta_key', \AKSRT\Core\DomainModel::RESOURCE_SALES_VOLUME_KEY);
$query->set('meta_key', \Jio\Core\DomainModel::RESOURCE_SALES_VOLUME_KEY);
$query->set('orderby', 'meta_value_num');
} elseif ($orderby === 'views' || $orderby === 'like' || $orderby === 'favorite') {
$query->set('meta_key', $this->stats_meta_key($orderby));
$query->set('orderby', 'meta_value_num');
}
$pay_type = isset($_GET['aksrt_pay_type']) ? sanitize_key((string) wp_unslash($_GET['aksrt_pay_type'])) : '';
if ($pay_type !== '' && isset(\AKSRT\Core\DomainModel::pay_type_labels()[$pay_type])) {
$pay_type = isset($_GET['jio_pay_type']) ? sanitize_key((string) wp_unslash($_GET['jio_pay_type'])) : '';
if ($pay_type !== '' && isset(\Jio\Core\DomainModel::pay_type_labels()[$pay_type])) {
$meta_query = $query->get('meta_query');
$meta_query = is_array($meta_query) ? $meta_query : [];
$meta_query[] = [
'relation' => 'OR',
[
'key' => \AKSRT\Core\DomainModel::PAY_TYPE_KEY,
'key' => \Jio\Core\DomainModel::PAY_TYPE_KEY,
'value' => $pay_type,
],
[
'key' => \AKSRT\Core\DomainModel::PAY_META_KEY,
'key' => \Jio\Core\DomainModel::PAY_META_KEY,
'value' => '"pay_type";s:' . strlen($pay_type) . ':"' . $pay_type . '"',
'compare' => 'LIKE',
],
@@ -264,11 +264,11 @@ class PostListEnhance {
*/
private function stats_meta_key(string $orderby): string {
$map = [
'views' => \AKSRT\Core\DomainModel::POST_VIEW_COUNT_KEY,
'like' => '_aksrt_like_count',
'favorite' => '_aksrt_favorite_count',
'views' => \Jio\Core\DomainModel::POST_VIEW_COUNT_KEY,
'like' => '_jio_like_count',
'favorite' => '_jio_favorite_count',
];
return $map[$orderby] ?? \AKSRT\Core\DomainModel::POST_VIEW_COUNT_KEY;
return $map[$orderby] ?? \Jio\Core\DomainModel::POST_VIEW_COUNT_KEY;
}
// ============================================================
@@ -291,7 +291,7 @@ class PostListEnhance {
private function edit_boxes_html(): string {
$html = $this->fields_html(PostListEditFields::extended(), 'extended');
$html .= $this->fields_html(PostListEditFields::pay($this->currency_symbol()), 'pay', __('付费参数', 'aksrt'), '<div class="c-yellow">' . esc_html__('批量设置付费参数,请仔细检查配置内容,更新后无法撤回。请注意配置逻辑,例如会员价不能高于普通价,执行价不能高于原价等', 'aksrt') . '</div>');
$html .= $this->fields_html(PostListEditFields::pay($this->currency_symbol()), 'pay', __('付费参数', 'jio'), '<div class="c-yellow">' . esc_html__('批量设置付费参数,请仔细检查配置内容,更新后无法撤回。请注意配置逻辑,例如会员价不能高于普通价,执行价不能高于原价等', 'jio') . '</div>');
$html .= $this->filter_fields_html();
return $html;
}
@@ -302,8 +302,8 @@ class PostListEnhance {
*/
private function filter_fields_html(): string {
$dimensions = [];
if (class_exists('\AKSRT\Settings\Settings')) {
$settings = \AKSRT\Settings\Settings::get_post_filter_settings();
if (class_exists('\Jio\Settings\Settings')) {
$settings = \Jio\Settings\Settings::get_post_filter_settings();
$dimensions = is_array($settings['custom_filter'] ?? null) ? $settings['custom_filter'] : [];
}
if (!$dimensions) {
@@ -332,7 +332,7 @@ class PostListEnhance {
'title' => (string) ($dimension['name'] ?? $key),
'type' => 'checkbox',
'options' => $options,
'placeholder' => sprintf(__('请选择%s', 'aksrt'), (string) ($dimension['name'] ?? $key)),
'placeholder' => sprintf(__('请选择%s', 'jio'), (string) ($dimension['name'] ?? $key)),
];
}
@@ -340,7 +340,7 @@ class PostListEnhance {
return '';
}
return $this->fields_html($fields, 'filter', __('高级筛选', 'aksrt'), '<div class="px12 aksrt-bulk-edit-desc">' . esc_html__('让文章实现更加精细化的分类,同时方便用户进行文章筛选', 'aksrt') . '</div>');
return $this->fields_html($fields, 'filter', __('高级筛选', 'jio'), '<div class="px12 jio-bulk-edit-desc">' . esc_html__('让文章实现更加精细化的分类,同时方便用户进行文章筛选', 'jio') . '</div>');
}
/**
@@ -357,15 +357,15 @@ class PostListEnhance {
$options_html = ' ';
switch ($field['type']) {
case 'number':
$name = 'aksrt_bulk_edit[' . $id . '][' . $field['id'] . ']';
$name = 'jio_bulk_edit[' . $id . '][' . $field['id'] . ']';
$options_html = '<div class="flex ac hh">'
. '<select name="' . esc_attr($name) . '[operation]">'
. '<option value="ignore" selected="selected">' . esc_html__('不更改', 'aksrt') . '</option>'
. '<option value="set">' . esc_html__('设置为', 'aksrt') . '</option>'
. '<option value="plus">' . esc_html__('加', 'aksrt') . '</option>'
. '<option value="subtract">' . esc_html__('减', 'aksrt') . '</option>'
. '<option value="multiply">' . esc_html__('乘', 'aksrt') . '</option>'
. '<option value="division">' . esc_html__('除', 'aksrt') . '</option>'
. '<option value="ignore" selected="selected">' . esc_html__('不更改', 'jio') . '</option>'
. '<option value="set">' . esc_html__('设置为', 'jio') . '</option>'
. '<option value="plus">' . esc_html__('加', 'jio') . '</option>'
. '<option value="subtract">' . esc_html__('减', 'jio') . '</option>'
. '<option value="multiply">' . esc_html__('乘', 'jio') . '</option>'
. '<option value="division">' . esc_html__('除', 'jio') . '</option>'
. '</select>'
. '<input type="text" name="' . esc_attr($name) . '[val]" value="">'
. '</div>';
@@ -374,32 +374,32 @@ class PostListEnhance {
case 'checkbox':
$select_options = !empty($field['options']) ? $field['options'] : [];
if ($select_options) {
$name = 'aksrt_bulk_edit[' . $id . '][' . $field['id'] . ']';
$name = 'jio_bulk_edit[' . $id . '][' . $field['id'] . ']';
$options_html = $this->checkbox_options_html($name, $select_options);
break;
}
$field['options'] = [
'ignore' => __('不更改', 'aksrt'),
'1' => __('开启', 'aksrt'),
'0' => __('关闭', 'aksrt'),
'ignore' => __('不更改', 'jio'),
'1' => __('开启', 'jio'),
'0' => __('关闭', 'jio'),
];
// Falls through to the switcher renderer below.
case 'switcher':
$field['options'] = [
'ignore' => __('不更改', 'aksrt'),
'1' => __('开启', 'aksrt'),
'0' => __('关闭', 'aksrt'),
'ignore' => __('不更改', 'jio'),
'1' => __('开启', 'jio'),
'0' => __('关闭', 'jio'),
];
// Falls through to the select renderer below.
case 'radio':
case 'select':
$options = isset($field['options']) && is_array($field['options']) ? $field['options'] : [];
$name = 'aksrt_bulk_edit[' . $id . '][' . $field['id'] . ']';
$name = 'jio_bulk_edit[' . $id . '][' . $field['id'] . ']';
$options_html = '<div class="flex ac hh">'
. '<select name="' . esc_attr($name) . '">'
. '<option selected="selected" value="ignore">--' . esc_html__('不更改', 'aksrt') . '--</option>';
. '<option selected="selected" value="ignore">--' . esc_html__('不更改', 'jio') . '--</option>';
foreach ($options as $key => $value) {
$options_html .= '<option value="' . esc_attr((string) $key) . '">' . esc_html((string) $value) . '</option>';
}
@@ -412,11 +412,11 @@ class PostListEnhance {
}
$title_html = !empty($field['title']) ? (string) $field['title'] : (!empty($field['label']) ? (string) $field['label'] : '');
$desc_html = !empty($field['desc']) ? '<div class="px12 aksrt-bulk-edit-desc">' . esc_html((string) $field['desc']) . '</div>' : '';
$subtitle = !empty($field['subtitle']) ? '<div class="px12 aksrt-bulk-edit-desc">' . esc_html((string) $field['subtitle']) . '</div>' : '';
$desc_html = !empty($field['desc']) ? '<div class="px12 jio-bulk-edit-desc">' . esc_html((string) $field['desc']) . '</div>' : '';
$subtitle = !empty($field['subtitle']) ? '<div class="px12 jio-bulk-edit-desc">' . esc_html((string) $field['subtitle']) . '</div>' : '';
$dependency = !empty($field['dependency']) ? ' data-dependency="' . esc_attr(wp_json_encode($field['dependency'])) . '"' : '';
$item_html .= '<div class="aksrt-bulk-edit-box"' . $dependency . '>'
$item_html .= '<div class="jio-bulk-edit-box"' . $dependency . '>'
. '<div>' . esc_html($title_html) . '</div>'
. $options_html . $subtitle . $desc_html
. '</div>';
@@ -438,8 +438,8 @@ class PostListEnhance {
*/
private function checkbox_options_html(string $name, array $options): string {
$html = '<select class="mr10" name="' . esc_attr($name) . '[operation]">'
. '<option value="ignore" selected="selected">' . esc_html__('不更改', 'aksrt') . '</option>'
. '<option value="set">' . esc_html__('设置为', 'aksrt') . '</option>'
. '<option value="ignore" selected="selected">' . esc_html__('不更改', 'jio') . '</option>'
. '<option value="set">' . esc_html__('设置为', 'jio') . '</option>'
. '</select>';
foreach ($options as $key => $value) {
@@ -476,7 +476,7 @@ class PostListEnhance {
if ($screen !== 'edit-post') {
return;
}
$submitted = isset($_POST['aksrt_bulk_edit']) && is_array($_POST['aksrt_bulk_edit']) ? wp_unslash($_POST['aksrt_bulk_edit']) : [];
$submitted = isset($_POST['jio_bulk_edit']) && is_array($_POST['jio_bulk_edit']) ? wp_unslash($_POST['jio_bulk_edit']) : [];
if (!$submitted) {
return;
}
@@ -538,9 +538,9 @@ class PostListEnhance {
* @param array<string,mixed> $fields
*/
private function save_pay(int $post_id, array $fields): void {
$pay_meta = function_exists('aksrt_get_pay_meta') ? aksrt_get_pay_meta($post_id) : [];
$pay_meta = function_exists('jio_get_pay_meta') ? jio_get_pay_meta($post_id) : [];
if (!is_array($pay_meta) || $pay_meta === []) {
$pay_meta = \AKSRT\Core\DomainModel::default_pay_meta();
$pay_meta = \Jio\Core\DomainModel::default_pay_meta();
}
foreach ($fields as $field_id => $field_value) {
@@ -600,19 +600,19 @@ class PostListEnhance {
}
$current = (string) ($pay_meta[$field_id] ?? '0.00');
try {
$current_minor = (float) \AKSRT\Ledger\AssetAmount::balance_minor($current);
$current_minor = (float) \Jio\Ledger\AssetAmount::balance_minor($current);
} catch (\InvalidArgumentException $e) {
$current_minor = 0.0;
}
$new = $this->apply_operation($current_minor, (float) $field_value['val'], $operation);
if ($new !== null) {
$pay_meta[$field_id] = \AKSRT\Ledger\AssetAmount::decimal_from_minor(max(0, (int) round($new)));
$pay_meta[$field_id] = \Jio\Ledger\AssetAmount::decimal_from_minor(max(0, (int) round($new)));
}
break;
}
}
\AKSRT\Core\DomainModel::persist_pay_meta($post_id, $pay_meta);
\Jio\Core\DomainModel::persist_pay_meta($post_id, $pay_meta);
}
/**
@@ -620,8 +620,8 @@ class PostListEnhance {
*/
private function save_filters(int $post_id, array $fields): void {
$dimensions = [];
if (class_exists('\AKSRT\Settings\Settings')) {
$settings = \AKSRT\Settings\Settings::get_post_filter_settings();
if (class_exists('\Jio\Settings\Settings')) {
$settings = \Jio\Settings\Settings::get_post_filter_settings();
$dimensions = is_array($settings['custom_filter'] ?? null) ? $settings['custom_filter'] : [];
}
if (!$dimensions) {
@@ -652,9 +652,9 @@ class PostListEnhance {
: [];
$values = array_values(array_intersect($allowed_dimensions[$field_id], $values));
if ($values) {
update_post_meta($post_id, '_aksrt_cf_' . $field_id, $values);
update_post_meta($post_id, '_jio_cf_' . $field_id, $values);
} else {
delete_post_meta($post_id, '_aksrt_cf_' . $field_id);
delete_post_meta($post_id, '_jio_cf_' . $field_id);
}
}
}
@@ -664,7 +664,7 @@ class PostListEnhance {
* meta box.
*/
private function update_post_meta_bag(int $post_id, string $key, string $value): void {
$bag_key = \AKSRT\Core\PostMetaBox::META_KEY;
$bag_key = \Jio\Core\PostMetaBox::META_KEY;
$bag = get_post_meta($post_id, $bag_key, true);
$bag = is_array($bag) ? $bag : [];
$bag[$key] = $value;
@@ -698,8 +698,8 @@ class PostListEnhance {
* Configured cash symbol, shared by the sales column and the pay fields.
*/
private function currency_symbol(): string {
if (class_exists('\AKSRT\Settings\Settings')) {
$paid = \AKSRT\Settings\Settings::get_paid_content_settings();
if (class_exists('\Jio\Settings\Settings')) {
$paid = \Jio\Settings\Settings::get_paid_content_settings();
return (string) ($paid['display']['currency_symbol'] ?? '¥');
}
return '¥';
@@ -732,7 +732,7 @@ class PostListEnhance {
if (!$wp_query instanceof \WP_Query || empty($wp_query->posts)) {
return [];
}
if (!class_exists('\AKSRT\Payment\OrderRepository')) {
if (!class_exists('\Jio\Payment\OrderRepository')) {
return [];
}
@@ -746,9 +746,9 @@ class PostListEnhance {
return [];
}
$map = \AKSRT\Payment\OrderRepository::get_instance()->get_content_sales_map(
$map = \Jio\Payment\OrderRepository::get_instance()->get_content_sales_map(
$ids,
\AKSRT\Core\DomainModel::ORDER_PRODUCT_RESOURCE
\Jio\Core\DomainModel::ORDER_PRODUCT_RESOURCE
);
$out = [];
foreach ($map as $pid => $row) {
@@ -760,8 +760,8 @@ class PostListEnhance {
private function orders_url(int $post_id): string {
return add_query_arg(
[
'page' => 'aksrt_orders',
'product_type' => \AKSRT\Core\DomainModel::ORDER_PRODUCT_RESOURCE,
'page' => 'jio_orders',
'product_type' => \Jio\Core\DomainModel::ORDER_PRODUCT_RESOURCE,
'product_id' => $post_id,
],
admin_url('admin.php')
+74 -74
View File
@@ -1,9 +1,9 @@
<?php
/**
* @package AKSRT
* @package Jio
*/
namespace AKSRT\Admin\Native;
namespace Jio\Admin\Native;
defined('ABSPATH') || exit;
@@ -35,13 +35,13 @@ class UsersMenus {
NativeUi::boot();
add_action('admin_menu', [$this, 'register_menus']);
add_action('admin_notices', [$this, 'pending_notices']);
add_action('admin_post_aksrt_users_send_message', [$this, 'handle_send_message']);
add_action('admin_post_aksrt_users_bulk_messages', [$this, 'handle_bulk_messages']);
add_action('admin_post_aksrt_users_review_verify', [$this, 'handle_review_verify']);
add_action('admin_post_aksrt_users_delete_verify', [$this, 'handle_delete_verify']);
add_action('admin_post_aksrt_users_update_report', [$this, 'handle_update_report']);
add_action('admin_post_aksrt_users_process_appeal', [$this, 'handle_process_appeal']);
add_action('admin_post_aksrt_users_mutate_vip', [$this, 'handle_mutate_vip']);
add_action('admin_post_jio_users_send_message', [$this, 'handle_send_message']);
add_action('admin_post_jio_users_bulk_messages', [$this, 'handle_bulk_messages']);
add_action('admin_post_jio_users_review_verify', [$this, 'handle_review_verify']);
add_action('admin_post_jio_users_delete_verify', [$this, 'handle_delete_verify']);
add_action('admin_post_jio_users_update_report', [$this, 'handle_update_report']);
add_action('admin_post_jio_users_process_appeal', [$this, 'handle_process_appeal']);
add_action('admin_post_jio_users_mutate_vip', [$this, 'handle_mutate_vip']);
add_action('admin_enqueue_scripts', [$this, 'enqueue_ticket_assets']);
add_action('admin_enqueue_scripts', [$this, 'enqueue_message_assets']);
add_action('admin_enqueue_scripts', [$this, 'enqueue_invite_assets']);
@@ -53,10 +53,10 @@ class UsersMenus {
public function register_menus(): void {
add_submenu_page(
'users.php',
__('邀请码管理', 'aksrt'),
__('邀请码管理', 'aksrt'),
__('邀请码管理', 'jio'),
__('邀请码管理', 'jio'),
'edit_users',
'aksrt_invite_codes',
'jio_invite_codes',
[$this, 'render_invite_codes_page']
);
@@ -66,42 +66,42 @@ class UsersMenus {
add_submenu_page(
'users.php',
__('消息管理', 'aksrt'),
__('消息管理', 'aksrt'),
__('消息管理', 'jio'),
__('消息管理', 'jio'),
self::CAPABILITY,
'aksrt_messages',
'jio_messages',
[$this, 'render_messages_page']
);
add_submenu_page(
'users.php',
__('用户会员', 'aksrt'),
__('用户会员', 'aksrt'),
__('用户会员', 'jio'),
__('用户会员', 'jio'),
self::CAPABILITY,
'aksrt_user_vip',
'jio_user_vip',
[$this, 'render_vip_page']
);
add_submenu_page(
'users.php',
__('身份认证', 'aksrt'),
__('身份认证', 'aksrt'),
__('身份认证', 'jio'),
__('身份认证', 'jio'),
self::CAPABILITY,
'aksrt_user_auth',
'jio_user_auth',
[$this, 'render_verify_page']
);
add_submenu_page(
'users.php',
__('处理举报及禁封申诉', 'aksrt'),
__('举报&禁封申诉', 'aksrt'),
__('处理举报及禁封申诉', 'jio'),
__('举报&禁封申诉', 'jio'),
self::CAPABILITY,
'aksrt_user_ban',
'jio_user_ban',
[$this, 'render_ban_page']
);
add_submenu_page(
'users.php',
__('工单管理', 'aksrt'),
__('工单管理', 'aksrt'),
__('工单管理', 'jio'),
__('工单管理', 'jio'),
self::CAPABILITY,
'aksrt_tickets',
'jio_tickets',
[$this, 'render_tickets_page']
);
}
@@ -112,115 +112,115 @@ class UsersMenus {
}
$page = isset($_GET['page']) ? sanitize_key((string) wp_unslash($_GET['page'])) : '';
if ($page !== 'aksrt_user_auth') {
if ($page !== 'jio_user_auth') {
$pending_verify = $this->count_verify_status('pending');
if ($pending_verify > 0) {
$url = admin_url('users.php?page=aksrt_user_auth&status=pending');
echo '<div class="notice notice-info is-dismissible"><h3>' . esc_html__('用户身份认证申请待处理', 'aksrt') . '</h3>';
$url = admin_url('users.php?page=jio_user_auth&status=pending');
echo '<div class="notice notice-info is-dismissible"><h3>' . esc_html__('用户身份认证申请待处理', 'jio') . '</h3>';
echo '<p>' . esc_html(sprintf(
/* translators: %d count */
__('您有 %d 个用户身份认证申请待处理', 'aksrt'),
__('您有 %d 个用户身份认证申请待处理', 'jio'),
$pending_verify
)) . '</p>';
echo '<p><a class="button button-primary" href="' . esc_url($url) . '">' . esc_html__('立即处理', 'aksrt') . '</a></p></div>';
echo '<p><a class="button button-primary" href="' . esc_url($url) . '">' . esc_html__('立即处理', 'jio') . '</a></p></div>';
}
}
if ($page !== 'aksrt_user_ban' && class_exists('\AKSRT\Messages\ReportCenter')) {
$pending_reports = (int) \AKSRT\Messages\ReportCenter::get_instance()->get_pending_count();
if ($page !== 'jio_user_ban' && class_exists('\Jio\Messages\ReportCenter')) {
$pending_reports = (int) \Jio\Messages\ReportCenter::get_instance()->get_pending_count();
if ($pending_reports > 0) {
$url = admin_url('users.php?page=aksrt_user_ban&status=pending');
echo '<div class="notice notice-info is-dismissible"><h3>' . esc_html__('举报待处理', 'aksrt') . '</h3>';
$url = admin_url('users.php?page=jio_user_ban&status=pending');
echo '<div class="notice notice-info is-dismissible"><h3>' . esc_html__('举报待处理', 'jio') . '</h3>';
echo '<p>' . esc_html(sprintf(
/* translators: %d count */
__('您有 %d 条举报待处理', 'aksrt'),
__('您有 %d 条举报待处理', 'jio'),
$pending_reports
)) . '</p>';
echo '<p><a class="button button-primary" href="' . esc_url($url) . '">' . esc_html__('立即处理', 'aksrt') . '</a></p></div>';
echo '<p><a class="button button-primary" href="' . esc_url($url) . '">' . esc_html__('立即处理', 'jio') . '</a></p></div>';
}
}
if ($page !== 'aksrt_user_ban' && class_exists('\AKSRT\User\BanAppeal')) {
if ($page !== 'jio_user_ban' && class_exists('\Jio\User\BanAppeal')) {
$pending_appeals = count(get_users([
'meta_key' => \AKSRT\User\BanAppeal::META_STATUS,
'meta_key' => \Jio\User\BanAppeal::META_STATUS,
'meta_value' => 'pending',
'number' => 50,
'fields' => 'ID',
]));
if ($pending_appeals > 0) {
$url = admin_url('users.php?page=aksrt_user_ban&type=appeal&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>';
$url = admin_url('users.php?page=jio_user_ban&type=appeal&status=pending');
echo '<div class="notice notice-warning is-dismissible"><h3>' . esc_html__('禁封申诉待处理', 'jio') . '</h3>';
echo '<p>' . esc_html(sprintf(__('您有 %d 条禁封申诉待处理', 'jio'), $pending_appeals)) . '</p>';
echo '<p><a class="button button-primary" href="' . esc_url($url) . '">' . esc_html__('立即处理', 'jio') . '</a></p></div>';
}
}
if ($page !== 'aksrt_tickets' && class_exists('\AKSRT\Ticket\TicketSettings') && \AKSRT\Ticket\TicketSettings::enabled()) {
if ($page !== 'jio_tickets' && class_exists('\Jio\Ticket\TicketSettings') && \Jio\Ticket\TicketSettings::enabled()) {
$pending_tickets = $this->count_pending_tickets();
if ($pending_tickets > 0) {
$url = admin_url('users.php?page=aksrt_tickets&status=' . \AKSRT\Ticket\TicketStatusService::KEY_PENDING);
echo '<div class="notice notice-info is-dismissible"><h3>' . esc_html__('工单待接单', 'aksrt') . '</h3>';
$url = admin_url('users.php?page=jio_tickets&status=' . \Jio\Ticket\TicketStatusService::KEY_PENDING);
echo '<div class="notice notice-info is-dismissible"><h3>' . esc_html__('工单待接单', 'jio') . '</h3>';
echo '<p>' . esc_html(sprintf(
/* translators: %d count */
__('您有 %d 个工单待接单', 'aksrt'),
__('您有 %d 个工单待接单', 'jio'),
$pending_tickets
)) . '</p>';
echo '<p><a class="button button-primary" href="' . esc_url($url) . '">' . esc_html__('立即处理', 'aksrt') . '</a></p></div>';
echo '<p><a class="button button-primary" href="' . esc_url($url) . '">' . esc_html__('立即处理', 'jio') . '</a></p></div>';
}
}
}
private function ensure_cap(): void {
if (!current_user_can(self::CAPABILITY)) {
wp_die(esc_html__('您没有权限访问此页面。', 'aksrt'));
wp_die(esc_html__('您没有权限访问此页面。', 'jio'));
}
}
private function flash(): void {
if (empty($_GET['aksrt_users_msg'])) {
if (empty($_GET['jio_users_msg'])) {
return;
}
$code = sanitize_key((string) wp_unslash($_GET['aksrt_users_msg']));
$code = sanitize_key((string) wp_unslash($_GET['jio_users_msg']));
$map = [
'verify_updated' => __('认证状态已更新。', 'aksrt'),
'verify_deleted' => __('认证记录已删除。', 'aksrt'),
'verify_failed' => __('认证审核失败。', 'aksrt'),
'report_updated' => __('举报状态已更新。', 'aksrt'),
'report_failed' => __('举报更新失败。', 'aksrt'),
'appeal_approved' => __('申诉已通过,限制已解除。', 'aksrt'),
'appeal_rejected' => __('申诉已驳回。', 'aksrt'),
'appeal_failed' => __('申诉处理失败。', 'aksrt'),
'vip_updated' => __('会员状态已更新。', 'aksrt'),
'vip_partial' => __('部分用户的会员状态已更新。', 'aksrt'),
'vip_failed' => __('会员操作失败。', 'aksrt'),
'verify_updated' => __('认证状态已更新。', 'jio'),
'verify_deleted' => __('认证记录已删除。', 'jio'),
'verify_failed' => __('认证审核失败。', 'jio'),
'report_updated' => __('举报状态已更新。', 'jio'),
'report_failed' => __('举报更新失败。', 'jio'),
'appeal_approved' => __('申诉已通过,限制已解除。', 'jio'),
'appeal_rejected' => __('申诉已驳回。', 'jio'),
'appeal_failed' => __('申诉处理失败。', 'jio'),
'vip_updated' => __('会员状态已更新。', 'jio'),
'vip_partial' => __('部分用户的会员状态已更新。', 'jio'),
'vip_failed' => __('会员操作失败。', 'jio'),
];
if (!isset($map[$code])) {
return;
}
$class = str_contains($code, 'failed') ? 'notice-error' : 'notice-success';
$message = $map[$code];
if ($code === 'verify_failed' && isset($_GET['aksrt_verify_error'])) {
$message = sanitize_text_field((string) wp_unslash($_GET['aksrt_verify_error']));
if ($code === 'verify_failed' && isset($_GET['jio_verify_error'])) {
$message = sanitize_text_field((string) wp_unslash($_GET['jio_verify_error']));
}
if (in_array($code, ['report_failed', 'appeal_failed'], true) && isset($_GET['aksrt_ban_error'])) {
$message = sanitize_text_field((string) wp_unslash($_GET['aksrt_ban_error']));
if (in_array($code, ['report_failed', 'appeal_failed'], true) && isset($_GET['jio_ban_error'])) {
$message = sanitize_text_field((string) wp_unslash($_GET['jio_ban_error']));
}
if ($code === 'vip_failed' && isset($_GET['aksrt_vip_error'])) {
$message = sanitize_text_field((string) wp_unslash($_GET['aksrt_vip_error']));
if ($code === 'vip_failed' && isset($_GET['jio_vip_error'])) {
$message = sanitize_text_field((string) wp_unslash($_GET['jio_vip_error']));
}
if (in_array($code, ['vip_updated', 'vip_partial'], true)) {
$message .= ' ' . sprintf(
__('成功:%1$d;失败:%2$d;跳过:%3$d。', 'aksrt'),
absint($_GET['aksrt_vip_ok'] ?? 0),
absint($_GET['aksrt_vip_failed_count'] ?? 0),
absint($_GET['aksrt_vip_skipped'] ?? 0)
__('成功:%1$d;失败:%2$d;跳过:%3$d。', 'jio'),
absint($_GET['jio_vip_ok'] ?? 0),
absint($_GET['jio_vip_failed_count'] ?? 0),
absint($_GET['jio_vip_skipped'] ?? 0)
);
}
echo '<div class="notice ' . esc_attr($class) . ' is-dismissible"><p>' . esc_html($message) . '</p></div>';
}
private function redirect(string $page, string $msg, array $extra = []): void {
wp_safe_redirect(add_query_arg(array_merge(['page' => $page, 'aksrt_users_msg' => $msg], $extra), admin_url('users.php')));
wp_safe_redirect(add_query_arg(array_merge(['page' => $page, 'jio_users_msg' => $msg], $extra), admin_url('users.php')));
exit;
}
+51 -51
View File
@@ -1,9 +1,9 @@
<?php
/** Native report and ban appeal queue. @package AKSRT */
/** Native report and ban appeal queue. @package Jio */
namespace AKSRT\Admin\Native;
namespace Jio\Admin\Native;
use AKSRT\User\Restriction;
use Jio\User\Restriction;
defined('ABSPATH') || exit;
@@ -15,11 +15,11 @@ trait UsersMenusBan {
private array $ban_restrictions = [];
public function enqueue_ban_assets(string $hook): void {
if ($hook !== 'users_page_aksrt_user_ban') return;
if ($hook !== 'users_page_jio_user_ban') return;
$css = '/assets/css/admin/user-ban.css';
$js = '/assets/js/admin-user-ban.js';
wp_enqueue_style('aksrt-admin-user-ban', AKSRT_URI . $css, [NativeUi::STYLE_HANDLE], (string) filemtime(AKSRT_DIR . $css));
wp_enqueue_script('aksrt-admin-user-ban', AKSRT_URI . $js, [], (string) filemtime(AKSRT_DIR . $js), true);
wp_enqueue_style('jio-admin-user-ban', JIO_URI . $css, [NativeUi::STYLE_HANDLE], (string) filemtime(JIO_DIR . $css));
wp_enqueue_script('jio-admin-user-ban', JIO_URI . $js, [], (string) filemtime(JIO_DIR . $js), true);
}
public function render_ban_page(): void {
@@ -28,71 +28,71 @@ trait UsersMenusBan {
$data = $this->query_ban_requests($filters, max(1, (int) ($_GET['paged'] ?? 1)));
$this->cache_ban_targets($data['items']);
$this->load_ban_restrictions($data['items']);
NativeUi::page_open('aksrt-ban-page');
NativeUi::page_open('jio-ban-page');
NativeUi::header(
__('举报与禁封申诉', 'aksrt'),
__('核对举报内容与申诉资料,在审核弹窗中记录处理结果。用户禁封操作仍在前台用户主页完成。', 'aksrt'),
__('举报与禁封申诉', 'jio'),
__('核对举报内容与申诉资料,在审核弹窗中记录处理结果。用户禁封操作仍在前台用户主页完成。', 'jio'),
'dashicons-shield'
);
$this->flash();
NativeUi::card_open('', '', true, 'aksrt-ban-list-card');
NativeUi::card_open('', '', true, 'jio-ban-list-card');
$this->render_ban_filters($filters, $data['counts']);
echo '<div class="aksrt-native-table-wrap">';
echo '<div class="jio-native-table-wrap">';
$this->render_ban_table($data['items'], $filters, $data['page']);
NativeUi::table_wrap_close();
AdminListPagination::render('aksrt_user_ban', $data['total'], $data['page'], self::BAN_PER_PAGE, $filters, 'users.php');
AdminListPagination::render('jio_user_ban', $data['total'], $data['page'], self::BAN_PER_PAGE, $filters, 'users.php');
NativeUi::card_close();
echo '<dialog id="aksrt-ban-dialog" class="aksrt-ban-dialog" aria-labelledby="aksrt-ban-dialog-title" aria-describedby="aksrt-ban-dialog-description"></dialog>';
echo '<noscript><p class="aksrt-native-callout">' . esc_html__('请启用 JavaScript 后查看详情和处理审核。', 'aksrt') . '</p></noscript>';
echo '<dialog id="jio-ban-dialog" class="jio-ban-dialog" aria-labelledby="jio-ban-dialog-title" aria-describedby="jio-ban-dialog-description"></dialog>';
echo '<noscript><p class="jio-native-callout">' . esc_html__('请启用 JavaScript 后查看详情和处理审核。', 'jio') . '</p></noscript>';
NativeUi::page_close();
}
private function ban_page_url(array $args = []): string {
return add_query_arg(array_merge(['page' => 'aksrt_user_ban'], $args), admin_url('users.php'));
return add_query_arg(array_merge(['page' => 'jio_user_ban'], $args), admin_url('users.php'));
}
private function render_ban_filters(array $filters, array $counts): void {
echo '<div class="aksrt-ban-toolbar"><div class="aksrt-ban-toolbar__top"><h2 class="aksrt-native-card__title">' . esc_html__('处理记录', 'aksrt') . '</h2>';
echo '<nav class="aksrt-ban-types" aria-label="' . esc_attr__('记录类型', 'aksrt') . '">';
foreach (['' => __('全部类型', 'aksrt'), 'report' => __('用户举报', 'aksrt'), 'appeal' => __('禁封申诉', 'aksrt')] as $key => $label) {
echo '<div class="jio-ban-toolbar"><div class="jio-ban-toolbar__top"><h2 class="jio-native-card__title">' . esc_html__('处理记录', 'jio') . '</h2>';
echo '<nav class="jio-ban-types" aria-label="' . esc_attr__('记录类型', 'jio') . '">';
foreach (['' => __('全部类型', 'jio'), 'report' => __('用户举报', 'jio'), 'appeal' => __('禁封申诉', 'jio')] as $key => $label) {
echo '<a href="' . esc_url($this->ban_page_url(array_merge($filters, ['type' => $key]))) . '"' . ($key === $filters['type'] ? ' aria-current="page"' : '') . '>' . esc_html($label) . '</a>';
}
echo '</nav></div><div class="aksrt-ban-toolbar__bottom"><ul class="subsubsub" aria-label="' . esc_attr__('处理状态筛选', 'aksrt') . '">';
foreach (['pending' => __('待处理', 'aksrt'), 'resolved' => __('已处理 / 已通过', 'aksrt'), 'dismissed' => __('已驳回', 'aksrt'), '' => __('全部状态', 'aksrt')] as $key => $label) {
if ($filters['type'] === 'report' && $key === 'resolved') $label = __('已处理', 'aksrt');
if ($filters['type'] === 'appeal' && $key === 'resolved') $label = __('已通过', 'aksrt');
echo '</nav></div><div class="jio-ban-toolbar__bottom"><ul class="subsubsub" aria-label="' . esc_attr__('处理状态筛选', 'jio') . '">';
foreach (['pending' => __('待处理', 'jio'), 'resolved' => __('已处理 / 已通过', 'jio'), 'dismissed' => __('已驳回', 'jio'), '' => __('全部状态', 'jio')] as $key => $label) {
if ($filters['type'] === 'report' && $key === 'resolved') $label = __('已处理', 'jio');
if ($filters['type'] === 'appeal' && $key === 'resolved') $label = __('已通过', 'jio');
echo '<li><a href="' . esc_url($this->ban_page_url(array_merge($filters, ['status' => $key]))) . '"' . ($key === $filters['status'] ? ' class="current" aria-current="page"' : '') . '>' . esc_html($label)
. ' <span class="count">' . esc_html(number_format_i18n($counts[$key === '' ? 'all' : $key])) . '</span></a></li>';
}
echo '</ul><form method="get" action="' . esc_url(admin_url('users.php')) . '" class="aksrt-native-filters">';
echo '<input type="hidden" name="page" value="aksrt_user_ban">';
echo '</ul><form method="get" action="' . esc_url(admin_url('users.php')) . '" class="jio-native-filters">';
echo '<input type="hidden" name="page" value="jio_user_ban">';
foreach (['type', 'status', 'send_user', 'orderby', 'order'] as $key) {
echo '<input type="hidden" name="' . esc_attr($key) . '" value="' . esc_attr((string) $filters[$key]) . '">';
}
echo '<label class="screen-reader-text" for="aksrt-ban-search">' . esc_html__('搜索用户或记录内容', 'aksrt') . '</label>';
echo '<input type="search" id="aksrt-ban-search" name="s" value="' . esc_attr($filters['s']) . '" placeholder="' . esc_attr__('用户 / 目标 ID / 原因 / 申诉资料', 'aksrt') . '">';
echo '<button type="submit" class="button">' . esc_html__('搜索', 'aksrt') . '</button>';
echo '<label class="screen-reader-text" for="jio-ban-search">' . esc_html__('搜索用户或记录内容', 'jio') . '</label>';
echo '<input type="search" id="jio-ban-search" name="s" value="' . esc_attr($filters['s']) . '" placeholder="' . esc_attr__('用户 / 目标 ID / 原因 / 申诉资料', 'jio') . '">';
echo '<button type="submit" class="button">' . esc_html__('搜索', 'jio') . '</button>';
if ($filters['s'] !== '' || $filters['send_user'] > 0) {
echo '<a class="button-link" href="' . esc_url($this->ban_page_url(array_merge($filters, ['s' => '', 'send_user' => 0]))) . '">' . esc_html__('清除筛选', 'aksrt') . '</a>';
echo '<a class="button-link" href="' . esc_url($this->ban_page_url(array_merge($filters, ['s' => '', 'send_user' => 0]))) . '">' . esc_html__('清除筛选', 'jio') . '</a>';
}
echo '</form></div>';
if ($filters['send_user'] > 0) {
$user = get_userdata($filters['send_user']);
echo '<p class="aksrt-ban-filter-hint">' . esc_html(sprintf(__('当前仅显示 %s 提交的记录。', 'aksrt'), $user ? $user->display_name : '#' . $filters['send_user'])) . '</p>';
echo '<p class="jio-ban-filter-hint">' . esc_html(sprintf(__('当前仅显示 %s 提交的记录。', 'jio'), $user ? $user->display_name : '#' . $filters['send_user'])) . '</p>';
}
echo '</div>';
}
private function render_ban_table(array $items, array $filters, int $page): void {
echo '<table class="widefat fixed striped aksrt-ban-table"><caption class="screen-reader-text">' . esc_html__('举报与禁封申诉处理记录', 'aksrt') . '</caption><thead><tr>';
foreach (['type' => __('类型 / 编号', 'aksrt'), 'user' => __('提交用户', 'aksrt'), 'summary' => __('事项摘要', 'aksrt'), 'status' => __('状态', 'aksrt'), 'time' => __('提交时间', 'aksrt'), 'updated' => __('更新时间', 'aksrt'), 'actions' => __('操作', 'aksrt')] as $key => $label) {
echo '<table class="widefat fixed striped jio-ban-table"><caption class="screen-reader-text">' . esc_html__('举报与禁封申诉处理记录', 'jio') . '</caption><thead><tr>';
foreach (['type' => __('类型 / 编号', 'jio'), 'user' => __('提交用户', 'jio'), 'summary' => __('事项摘要', 'jio'), 'status' => __('状态', 'jio'), 'time' => __('提交时间', 'jio'), 'updated' => __('更新时间', 'jio'), 'actions' => __('操作', 'jio')] as $key => $label) {
$sortable = in_array($key, ['type', 'status', 'time', 'updated'], true);
$active = $sortable && $filters['orderby'] === $key;
echo '<th scope="col" class="column-' . esc_attr($key) . '"' . ($active ? ' aria-sort="' . ($filters['order'] === 'ASC' ? 'ascending' : 'descending') . '"' : '') . '>';
if ($sortable) {
$order = $active && $filters['order'] === 'DESC' ? 'ASC' : 'DESC';
$title = sprintf($order === 'ASC' ? __('按%s升序排列', 'aksrt') : __('按%s降序排列', 'aksrt'), $label);
echo '<a class="aksrt-ban-sort" href="' . esc_url($this->ban_page_url(array_merge($filters, ['orderby' => $key, 'order' => $order]))) . '" aria-label="' . esc_attr($title) . '">' . esc_html($label) . '<span aria-hidden="true">' . ($active ? ($filters['order'] === 'ASC' ? '↑' : '↓') : '↕') . '</span></a>';
$title = sprintf($order === 'ASC' ? __('按%s升序排列', 'jio') : __('按%s降序排列', 'jio'), $label);
echo '<a class="jio-ban-sort" href="' . esc_url($this->ban_page_url(array_merge($filters, ['orderby' => $key, 'order' => $order]))) . '" aria-label="' . esc_attr($title) . '">' . esc_html($label) . '<span aria-hidden="true">' . ($active ? ($filters['order'] === 'ASC' ? '↑' : '↓') : '↕') . '</span></a>';
} else {
echo esc_html($label);
}
@@ -102,8 +102,8 @@ trait UsersMenusBan {
if ($items === []) {
$filtered = $filters['s'] !== '' || $filters['send_user'] > 0;
echo NativeUi::empty_row(7,
!$filtered && $filters['status'] === 'pending' ? __('暂无待处理记录', 'aksrt') : __('没有符合条件的记录', 'aksrt'),
$filtered ? __('请调整关键词、清除用户筛选或切换类型和状态。', 'aksrt') : __('可切换状态查看处理记录,新举报和申诉将在这里显示。', 'aksrt')
!$filtered && $filters['status'] === 'pending' ? __('暂无待处理记录', 'jio') : __('没有符合条件的记录', 'jio'),
$filtered ? __('请调整关键词、清除用户筛选或切换类型和状态。', 'jio') : __('可切换状态查看处理记录,新举报和申诉将在这里显示。', 'jio')
);
}
foreach ($items as $row) $this->render_ban_row($row, $filters, $page);
@@ -115,31 +115,31 @@ trait UsersMenusBan {
$payload = $row['payload'];
$key = $row['kind'] . '-' . $row['id'];
$user_id = (int) $row['user_id'];
echo '<tr><td class="column-type">' . NativeUi::badge($report ? __('用户举报', 'aksrt') : __('禁封申诉', 'aksrt'), $report ? 'info' : 'neutral', false);
echo '<div class="aksrt-ban-sub">#' . (int) $row['id'] . '</div></td><td class="column-user">' . NativeUi::user_cell($user_id, __('用户已删除', 'aksrt'));
echo '<tr><td class="column-type">' . NativeUi::badge($report ? __('用户举报', 'jio') : __('禁封申诉', 'jio'), $report ? 'info' : 'neutral', false);
echo '<div class="jio-ban-sub">#' . (int) $row['id'] . '</div></td><td class="column-user">' . NativeUi::user_cell($user_id, __('用户已删除', 'jio'));
if ($user_id > 0) {
echo '<a class="aksrt-ban-user-filter" href="' . esc_url($this->ban_page_url(array_merge($filters, ['send_user' => $user_id]))) . '">' . esc_html__('筛选此用户', 'aksrt') . '</a>';
echo '<a class="jio-ban-user-filter" href="' . esc_url($this->ban_page_url(array_merge($filters, ['send_user' => $user_id]))) . '">' . esc_html__('筛选此用户', 'jio') . '</a>';
}
echo '</td><td class="column-summary">';
if ($report) {
echo '<strong>' . $this->ban_target_link((string) ($payload['target_type'] ?? ''), (int) ($payload['target_id'] ?? 0)) . '</strong>';
echo '<div class="aksrt-ban-summary">' . esc_html((string) ($payload['category'] ?? '')) . '</div>';
echo '<div class="jio-ban-summary">' . esc_html((string) ($payload['category'] ?? '')) . '</div>';
} else {
echo '<strong>' . esc_html__('申请解除禁封', 'aksrt') . '</strong>';
echo '<strong>' . esc_html__('申请解除禁封', 'jio') . '</strong>';
}
if ((string) ($payload['reason'] ?? '') !== '') echo '<div class="aksrt-ban-summary">' . esc_html((string) $payload['reason']) . '</div>';
if ((string) ($payload['reason'] ?? '') !== '') echo '<div class="jio-ban-summary">' . esc_html((string) $payload['reason']) . '</div>';
$this->render_ban_detail_template($row, $filters, $page);
echo '</td><td class="column-status">' . $this->ban_status_badge($row) . '</td>';
echo '<td class="column-time">' . esc_html($this->ban_time((string) $row['time'])) . '</td>';
echo '<td class="column-updated">' . esc_html($this->ban_time((string) $row['updated'])) . '</td>';
echo '<td class="column-actions"><button type="button" class="button' . ($row['status'] === 'pending' ? ' button-primary' : '') . '" data-aksrt-ban-view="' . esc_attr($key) . '" aria-haspopup="dialog" aria-controls="aksrt-ban-dialog">'
. esc_html($row['status'] === 'pending' ? __('审核', 'aksrt') : __('查看详情', 'aksrt')) . '</button></td></tr>';
echo '<td class="column-actions"><button type="button" class="button' . ($row['status'] === 'pending' ? ' button-primary' : '') . '" data-jio-ban-view="' . esc_attr($key) . '" aria-haspopup="dialog" aria-controls="jio-ban-dialog">'
. esc_html($row['status'] === 'pending' ? __('审核', 'jio') : __('查看详情', 'jio')) . '</button></td></tr>';
}
private function ban_status_badge(array $row): string {
if ($row['status'] === 'pending') return NativeUi::badge($row['raw_status'] === 'reviewing' ? __('审核中', 'aksrt') : __('待处理', 'aksrt'), 'warning');
if ($row['status'] === 'resolved') return NativeUi::badge($row['kind'] === 'report' ? __('已处理', 'aksrt') : __('已通过', 'aksrt'), 'success');
return NativeUi::badge(__('已驳回', 'aksrt'), 'danger');
if ($row['status'] === 'pending') return NativeUi::badge($row['raw_status'] === 'reviewing' ? __('审核中', 'jio') : __('待处理', 'jio'), 'warning');
if ($row['status'] === 'resolved') return NativeUi::badge($row['kind'] === 'report' ? __('已处理', 'jio') : __('已通过', 'jio'), 'success');
return NativeUi::badge(__('已驳回', 'jio'), 'danger');
}
private function ban_time(string $time): string {
@@ -147,12 +147,12 @@ trait UsersMenusBan {
}
private function ban_user_home(int $user_id): string {
return function_exists('aksrt_get_user_home_url') ? aksrt_get_user_home_url($user_id) : get_author_posts_url($user_id);
return function_exists('jio_get_user_home_url') ? jio_get_user_home_url($user_id) : get_author_posts_url($user_id);
}
private function ban_target_link(string $type, int $id): string {
$labels = ['post' => __('文章', 'aksrt'), 'comment' => __('评论', 'aksrt'), 'user' => __('用户', 'aksrt')];
$label = ($labels[$type] ?? __('目标', 'aksrt')) . ' #' . $id;
$labels = ['post' => __('文章', 'jio'), 'comment' => __('评论', 'jio'), 'user' => __('用户', 'jio')];
$label = ($labels[$type] ?? __('目标', 'jio')) . ' #' . $id;
$url = '';
if ($type === 'post' && get_post($id)) {
$label = get_the_title($id) ?: $label;
@@ -163,7 +163,7 @@ trait UsersMenusBan {
$label = $user->display_name . ' #' . $id;
$url = $this->ban_user_home($id);
}
return $url !== '' ? '<a href="' . esc_url($url) . '" target="_blank" rel="noopener noreferrer">' . esc_html($label) . '</a>' : esc_html($label . ' ' . __('(目标已不可用)', 'aksrt'));
return $url !== '' ? '<a href="' . esc_url($url) . '" target="_blank" rel="noopener noreferrer">' . esc_html($label) . '</a>' : esc_html($label . ' ' . __('(目标已不可用)', 'jio'));
}
private function cache_ban_targets(array $items): void {
@@ -194,7 +194,7 @@ trait UsersMenusBan {
global $wpdb;
$placeholders = implode(',', array_fill(0, count($ids), '%d'));
$rows = $wpdb->get_results($wpdb->prepare(
"SELECT user_id, type, scope, reason, created_at, expires_at FROM {$wpdb->prefix}aksrt_user_restriction WHERE user_id IN ({$placeholders}) AND (expires_at IS NULL OR expires_at > %s) ORDER BY created_at DESC",
"SELECT user_id, type, scope, reason, created_at, expires_at FROM {$wpdb->prefix}jio_user_restriction WHERE user_id IN ({$placeholders}) AND (expires_at IS NULL OR expires_at > %s) ORDER BY created_at DESC",
array_merge($ids, [current_time('mysql', true)])
), ARRAY_A);
foreach ((array) $rows as $row) $this->ban_restrictions[(int) $row['user_id']][] = $row;
+49 -49
View File
@@ -1,9 +1,9 @@
<?php
/**
* @package AKSRT
* @package Jio
*/
namespace AKSRT\Admin\Native;
namespace Jio\Admin\Native;
defined('ABSPATH') || exit;
@@ -11,87 +11,87 @@ trait UsersMenusBanActions {
public function handle_update_report(): void {
if (!current_user_can(self::CAPABILITY)) {
wp_die(esc_html__('权限不足', 'aksrt'));
wp_die(esc_html__('权限不足', 'jio'));
}
$report_id = absint($this->ban_post_value('report_id'));
$new_status = sanitize_key($this->ban_post_value('new_status'));
$note = trim(sanitize_textarea_field($this->ban_post_value('note')));
check_admin_referer('aksrt_users_update_report_' . $report_id);
check_admin_referer('jio_users_update_report_' . $report_id);
if ($report_id <= 0 || !in_array($new_status, ['resolved', 'dismissed'], true)) {
$this->redirect_ban_action('report_failed', __('举报处理请求无效。', 'aksrt'));
$this->redirect_ban_action('report_failed', __('举报处理请求无效。', 'jio'));
}
if ($new_status === 'dismissed' && $note === '') {
$this->redirect_ban_action('report_failed', __('驳回举报时必须填写处理说明。', 'aksrt'));
$this->redirect_ban_action('report_failed', __('驳回举报时必须填写处理说明。', 'jio'));
}
$row = $this->find_ban_request('report', $report_id);
if (!$this->ban_request_is_current($row, 'pending')) {
$this->redirect_ban_action('report_failed', __('举报记录已变化或已处理,请刷新后重试。', 'aksrt'));
$this->redirect_ban_action('report_failed', __('举报记录已变化或已处理,请刷新后重试。', 'jio'));
}
if (!class_exists('\\AKSRT\\Messages\\ReportCenter')) {
$this->redirect_ban_action('report_failed', __('举报处理服务不可用。', 'aksrt'));
if (!class_exists('\\Jio\\Messages\\ReportCenter')) {
$this->redirect_ban_action('report_failed', __('举报处理服务不可用。', 'jio'));
}
$ok = \AKSRT\Messages\ReportCenter::get_instance()->update_status($report_id, $new_status, $note);
$ok = \Jio\Messages\ReportCenter::get_instance()->update_status($report_id, $new_status, $note);
$this->redirect_ban_action(
$ok ? 'report_updated' : 'report_failed',
$ok ? '' : __('举报状态更新失败,请稍后重试。', 'aksrt')
$ok ? '' : __('举报状态更新失败,请稍后重试。', 'jio')
);
}
public function handle_process_appeal(): void {
if (!current_user_can(self::CAPABILITY)) {
wp_die(esc_html__('权限不足', 'aksrt'));
wp_die(esc_html__('权限不足', 'jio'));
}
$user_id = absint($this->ban_post_value('user_id'));
$decision = sanitize_key($this->ban_post_value('decision'));
$note = trim(sanitize_textarea_field($this->ban_post_value('note')));
check_admin_referer('aksrt_users_process_appeal_' . $user_id);
check_admin_referer('jio_users_process_appeal_' . $user_id);
if ($user_id <= 0 || !in_array($decision, ['approve', 'reject'], true)) {
$this->redirect_ban_action('appeal_failed', __('申诉处理请求无效。', 'aksrt'));
$this->redirect_ban_action('appeal_failed', __('申诉处理请求无效。', 'jio'));
}
if ($decision === 'reject' && $note === '') {
$this->redirect_ban_action('appeal_failed', __('驳回申诉时必须填写拒绝原因。', 'aksrt'));
$this->redirect_ban_action('appeal_failed', __('驳回申诉时必须填写拒绝原因。', 'jio'));
}
$row = $this->find_ban_request('appeal', $user_id);
if (!$this->ban_request_is_current($row, 'pending')) {
$this->redirect_ban_action('appeal_failed', __('申诉记录已变化或已处理,请刷新后重试。', 'aksrt'));
$this->redirect_ban_action('appeal_failed', __('申诉记录已变化或已处理,请刷新后重试。', 'jio'));
}
$user = get_userdata($user_id);
if (!($user instanceof \WP_User)) {
$this->redirect_ban_action('appeal_failed', __('申诉用户不存在,无法处理。', 'aksrt'));
$this->redirect_ban_action('appeal_failed', __('申诉用户不存在,无法处理。', 'jio'));
}
if ($decision === 'approve') {
if (!class_exists('\\AKSRT\\User\\Restriction')) {
$this->redirect_ban_action('appeal_failed', __('用户限制服务不可用,无法通过申诉。', 'aksrt'));
if (!class_exists('\\Jio\\User\\Restriction')) {
$this->redirect_ban_action('appeal_failed', __('用户限制服务不可用,无法通过申诉。', 'jio'));
}
global $wpdb;
$restriction_service = \AKSRT\User\Restriction::get_instance();
$restriction_service = \Jio\User\Restriction::get_instance();
$restriction_service->lift_user_restriction($user_id);
$lift_error = (string) $wpdb->last_error;
if ($lift_error !== '') {
$this->redirect_ban_action('appeal_failed', __('解除限制时发生数据库错误,申诉未通过。', 'aksrt'));
$this->redirect_ban_action('appeal_failed', __('解除限制时发生数据库错误,申诉未通过。', 'jio'));
}
$active_restrictions = $restriction_service->get_user_restrictions($user_id, true);
$check_error = (string) $wpdb->last_error;
if ($check_error !== '') {
$this->redirect_ban_action('appeal_failed', __('无法核实限制是否解除,限制可能已解除,请检查用户当前状态。', 'aksrt'));
$this->redirect_ban_action('appeal_failed', __('无法核实限制是否解除,限制可能已解除,请检查用户当前状态。', 'jio'));
}
foreach ($active_restrictions as $restriction) {
if (in_array($restriction['type'] ?? '', [
\AKSRT\User\Restriction::TYPE_BAN,
\AKSRT\User\Restriction::TYPE_SUSPEND,
\AKSRT\User\Restriction::TYPE_MUTE,
\Jio\User\Restriction::TYPE_BAN,
\Jio\User\Restriction::TYPE_SUSPEND,
\Jio\User\Restriction::TYPE_MUTE,
], true)) {
$this->redirect_ban_action('appeal_failed', __('仍有生效限制,申诉未通过。', 'aksrt'));
$this->redirect_ban_action('appeal_failed', __('仍有生效限制,申诉未通过。', 'jio'));
}
}
}
@@ -103,58 +103,58 @@ trait UsersMenusBanActions {
$payload['handled_by'] = get_current_user_id();
$payload['decision'] = $raw_status;
$history = get_user_meta($user_id, \AKSRT\User\BanAppeal::META_HISTORY, true);
$history = get_user_meta($user_id, \Jio\User\BanAppeal::META_HISTORY, true);
$history = is_array($history) ? $history : [];
$history[] = $payload;
$history = array_slice($history, -30);
$old_payload = $row['payload'];
$old_history = get_user_meta($user_id, \AKSRT\User\BanAppeal::META_HISTORY, true);
$payload_updated = update_user_meta($user_id, \AKSRT\User\BanAppeal::META_PAYLOAD, $payload, $old_payload);
$old_history = get_user_meta($user_id, \Jio\User\BanAppeal::META_HISTORY, true);
$payload_updated = update_user_meta($user_id, \Jio\User\BanAppeal::META_PAYLOAD, $payload, $old_payload);
if ($payload_updated === false) {
$this->redirect_ban_action('appeal_failed', $this->appeal_metadata_error(__('申诉处理记录保存失败。', 'aksrt'), $decision));
$this->redirect_ban_action('appeal_failed', $this->appeal_metadata_error(__('申诉处理记录保存失败。', 'jio'), $decision));
}
$status_updated = update_user_meta($user_id, \AKSRT\User\BanAppeal::META_STATUS, $raw_status, 'pending');
$status_updated = update_user_meta($user_id, \Jio\User\BanAppeal::META_STATUS, $raw_status, 'pending');
if ($status_updated === false) {
update_user_meta($user_id, \AKSRT\User\BanAppeal::META_PAYLOAD, $old_payload);
$this->redirect_ban_action('appeal_failed', $this->appeal_metadata_error(__('申诉状态保存失败。', 'aksrt'), $decision));
update_user_meta($user_id, \Jio\User\BanAppeal::META_PAYLOAD, $old_payload);
$this->redirect_ban_action('appeal_failed', $this->appeal_metadata_error(__('申诉状态保存失败。', 'jio'), $decision));
}
$history_updated = update_user_meta($user_id, \AKSRT\User\BanAppeal::META_HISTORY, $history);
$history_updated = update_user_meta($user_id, \Jio\User\BanAppeal::META_HISTORY, $history);
if ($history_updated === false) {
update_user_meta($user_id, \AKSRT\User\BanAppeal::META_STATUS, 'pending');
update_user_meta($user_id, \AKSRT\User\BanAppeal::META_PAYLOAD, $old_payload);
update_user_meta($user_id, \Jio\User\BanAppeal::META_STATUS, 'pending');
update_user_meta($user_id, \Jio\User\BanAppeal::META_PAYLOAD, $old_payload);
if ($old_history === '' || $old_history === null) {
delete_user_meta($user_id, \AKSRT\User\BanAppeal::META_HISTORY);
delete_user_meta($user_id, \Jio\User\BanAppeal::META_HISTORY);
} else {
update_user_meta($user_id, \AKSRT\User\BanAppeal::META_HISTORY, $old_history);
update_user_meta($user_id, \Jio\User\BanAppeal::META_HISTORY, $old_history);
}
$this->redirect_ban_action('appeal_failed', $this->appeal_metadata_error(__('申诉历史保存失败。', 'aksrt'), $decision));
$this->redirect_ban_action('appeal_failed', $this->appeal_metadata_error(__('申诉历史保存失败。', 'jio'), $decision));
}
if (class_exists('\\AKSRT\\Messages\\Message')) {
if (class_exists('\\Jio\\Messages\\Message')) {
$message = $decision === 'approve'
? __('你的禁封申诉已通过,限制已解除。', 'aksrt')
: __('你的禁封申诉未通过,可修改后重新提交。', 'aksrt');
? __('你的禁封申诉已通过,限制已解除。', 'jio')
: __('你的禁封申诉未通过,可修改后重新提交。', 'jio');
if ($note !== '') {
$message .= ' ' . sprintf(__('备注:%s', 'aksrt'), $note);
$message .= ' ' . sprintf(__('备注:%s', 'jio'), $note);
}
\AKSRT\Messages\Message::get_instance()->send(
\Jio\Messages\Message::get_instance()->send(
$user_id,
'ban_appeal',
$message,
['title' => $decision === 'approve' ? __('申诉通过', 'aksrt') : __('申诉驳回', 'aksrt')]
['title' => $decision === 'approve' ? __('申诉通过', 'jio') : __('申诉驳回', 'jio')]
);
}
do_action('aksrt_ban_appeal_processed', $user_id, $decision, $note);
do_action('jio_ban_appeal_processed', $user_id, $decision, $note);
$this->redirect_ban_action($decision === 'approve' ? 'appeal_approved' : 'appeal_rejected');
}
private function appeal_metadata_error(string $message, string $decision): string {
if ($decision === 'approve') {
return $message . ' ' . __('限制可能已解除,请核对用户当前状态。', 'aksrt');
return $message . ' ' . __('限制可能已解除,请核对用户当前状态。', 'jio');
}
return $message;
}
@@ -193,8 +193,8 @@ trait UsersMenusBanActions {
'paged' => max(1, absint($this->ban_post_value('paged', '1'))),
];
if ($error !== '') {
$extra['aksrt_ban_error'] = $error;
$extra['jio_ban_error'] = $error;
}
$this->redirect('aksrt_user_ban', $message, $extra);
$this->redirect('jio_user_ban', $message, $extra);
}
}
+55 -55
View File
@@ -1,9 +1,9 @@
<?php
/** Report and ban appeal detail/review dialog templates. @package AKSRT */
/** Report and ban appeal detail/review dialog templates. @package Jio */
namespace AKSRT\Admin\Native;
namespace Jio\Admin\Native;
use AKSRT\User\Restriction;
use Jio\User\Restriction;
defined('ABSPATH') || exit;
@@ -13,24 +13,24 @@ trait UsersMenusBanDialogs {
$pending = $row['status'] === 'pending';
$report = $row['kind'] === 'report';
$key = $row['kind'] . '-' . $row['id'];
echo '<template id="aksrt-ban-detail-' . esc_attr($key) . '">';
echo '<template id="jio-ban-detail-' . esc_attr($key) . '">';
if ($pending) {
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" class="aksrt-ban-dialog__frame" data-aksrt-ban-review>';
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" class="jio-ban-dialog__frame" data-jio-ban-review>';
$this->render_ban_context($row, $filters, $page);
} else {
echo '<div class="aksrt-ban-dialog__frame">';
echo '<div class="jio-ban-dialog__frame">';
}
$title = $report
? ($pending ? __('审核用户举报', 'aksrt') : __('用户举报详情', 'aksrt'))
: ($pending ? __('审核禁封申诉', 'aksrt') : __('禁封申诉详情', 'aksrt'));
echo '<div class="aksrt-ban-dialog__head"><div><h2 id="aksrt-ban-dialog-title" tabindex="-1" autofocus>' . esc_html($title) . '</h2>';
echo '<p id="aksrt-ban-dialog-description">' . esc_html($pending ? __('先核对原始资料,再选择处理结果;提交后将通知用户。', 'aksrt') : __('该记录已处理,以下资料和结果仅供查看。', 'aksrt')) . '</p></div>';
echo '<button type="button" class="aksrt-ban-dialog__close" data-aksrt-dialog-close aria-label="' . esc_attr__('关闭弹窗', 'aksrt') . '"><span class="dashicons dashicons-no-alt" aria-hidden="true"></span></button></div>';
echo '<div class="aksrt-ban-dialog__body"><div class="aksrt-ban-applicant">' . NativeUi::user_cell((int) $row['user_id'], __('用户已删除', 'aksrt')) . $this->ban_status_badge($row) . '</div>';
echo '<dl class="aksrt-ban-meta">';
$this->render_ban_meta_item(__('记录编号', 'aksrt'), ($report ? __('举报', 'aksrt') : __('申诉', 'aksrt')) . ' #' . $row['id']);
$this->render_ban_meta_item(__('提交时间', 'aksrt'), $this->ban_time((string) $row['time']));
$this->render_ban_meta_item(__('更新时间', 'aksrt'), $this->ban_time((string) $row['updated']));
? ($pending ? __('审核用户举报', 'jio') : __('用户举报详情', 'jio'))
: ($pending ? __('审核禁封申诉', 'jio') : __('禁封申诉详情', 'jio'));
echo '<div class="jio-ban-dialog__head"><div><h2 id="jio-ban-dialog-title" tabindex="-1" autofocus>' . esc_html($title) . '</h2>';
echo '<p id="jio-ban-dialog-description">' . esc_html($pending ? __('先核对原始资料,再选择处理结果;提交后将通知用户。', 'jio') : __('该记录已处理,以下资料和结果仅供查看。', 'jio')) . '</p></div>';
echo '<button type="button" class="jio-ban-dialog__close" data-jio-dialog-close aria-label="' . esc_attr__('关闭弹窗', 'jio') . '"><span class="dashicons dashicons-no-alt" aria-hidden="true"></span></button></div>';
echo '<div class="jio-ban-dialog__body"><div class="jio-ban-applicant">' . NativeUi::user_cell((int) $row['user_id'], __('用户已删除', 'jio')) . $this->ban_status_badge($row) . '</div>';
echo '<dl class="jio-ban-meta">';
$this->render_ban_meta_item(__('记录编号', 'jio'), ($report ? __('举报', 'jio') : __('申诉', 'jio')) . ' #' . $row['id']);
$this->render_ban_meta_item(__('提交时间', 'jio'), $this->ban_time((string) $row['time']));
$this->render_ban_meta_item(__('更新时间', 'jio'), $this->ban_time((string) $row['updated']));
echo '</dl>';
if ($report) {
$this->render_ban_report_details($row);
@@ -39,20 +39,20 @@ trait UsersMenusBanDialogs {
}
$this->render_ban_review_history($row);
if ($pending) $this->render_ban_review_fields($report);
echo '</div><div class="aksrt-ban-dialog__foot"><button type="button" class="button" data-aksrt-dialog-close>' . esc_html($pending ? __('取消', 'aksrt') : __('关闭', 'aksrt')) . '</button>';
echo '</div><div class="jio-ban-dialog__foot"><button type="button" class="button" data-jio-dialog-close>' . esc_html($pending ? __('取消', 'jio') : __('关闭', 'jio')) . '</button>';
if ($pending) {
echo '<button type="submit" class="button button-primary" data-aksrt-ban-submit disabled'
. ' data-idle-label="' . esc_attr__('请选择处理结果', 'aksrt') . '"'
. ' data-approve-label="' . esc_attr($report ? __('确认标记已处理', 'aksrt') : __('确认通过并解除限制', 'aksrt')) . '"'
. ' data-reject-label="' . esc_attr__('确认驳回', 'aksrt') . '"'
. ' data-busy-label="' . esc_attr__('正在提交…', 'aksrt') . '">' . esc_html__('请选择处理结果', 'aksrt') . '</button>';
echo '<button type="submit" class="button button-primary" data-jio-ban-submit disabled'
. ' data-idle-label="' . esc_attr__('请选择处理结果', 'jio') . '"'
. ' data-approve-label="' . esc_attr($report ? __('确认标记已处理', 'jio') : __('确认通过并解除限制', 'jio')) . '"'
. ' data-reject-label="' . esc_attr__('确认驳回', 'jio') . '"'
. ' data-busy-label="' . esc_attr__('正在提交…', 'jio') . '">' . esc_html__('请选择处理结果', 'jio') . '</button>';
}
echo '</div>' . ($pending ? '</form>' : '</div>') . '</template>';
}
private function render_ban_context(array $row, array $filters, int $page): void {
$report = $row['kind'] === 'report';
$action = $report ? 'aksrt_users_update_report' : 'aksrt_users_process_appeal';
$action = $report ? 'jio_users_update_report' : 'jio_users_process_appeal';
$fields = [
'action' => $action,
$report ? 'report_id' : 'user_id' => (int) $row['id'],
@@ -76,7 +76,7 @@ trait UsersMenusBanDialogs {
}
private function render_ban_text_section(string $title, string $text): void {
echo '<section class="aksrt-ban-section"><h3>' . esc_html($title) . '</h3><div class="aksrt-ban-copy">';
echo '<section class="jio-ban-section"><h3>' . esc_html($title) . '</h3><div class="jio-ban-copy">';
// Evidence URLs remain links: opening a case must not load reporter-supplied remote images.
$parts = preg_split('~(https?://[^\s<>"\x27]+)~u', $text, -1, PREG_SPLIT_DELIM_CAPTURE);
foreach ($parts ?: [$text] as $index => $part) {
@@ -84,7 +84,7 @@ trait UsersMenusBanDialogs {
? '<a href="' . esc_url($part, ['http', 'https']) . '" target="_blank" rel="noopener noreferrer">' . esc_html($part) . '</a>'
: esc_html($part !== '' ? $part : '');
}
if ($text === '') echo esc_html__('未填写', 'aksrt');
if ($text === '') echo esc_html__('未填写', 'jio');
echo '</div></section>';
}
@@ -92,59 +92,59 @@ trait UsersMenusBanDialogs {
$payload = $row['payload'];
$type = (string) ($payload['target_type'] ?? '');
$id = (int) ($payload['target_id'] ?? 0);
echo '<section class="aksrt-ban-section"><h3>' . esc_html__('举报对象', 'aksrt') . '</h3><div class="aksrt-ban-target">' . $this->ban_target_link($type, $id) . '</div></section>';
if ((string) ($payload['category'] ?? '') !== '') $this->render_ban_text_section(__('举报分类', 'aksrt'), (string) $payload['category']);
$this->render_ban_text_section(__('举报内容与举证', 'aksrt'), (string) ($payload['reason'] ?? ''));
echo '<section class="jio-ban-section"><h3>' . esc_html__('举报对象', 'jio') . '</h3><div class="jio-ban-target">' . $this->ban_target_link($type, $id) . '</div></section>';
if ((string) ($payload['category'] ?? '') !== '') $this->render_ban_text_section(__('举报分类', 'jio'), (string) $payload['category']);
$this->render_ban_text_section(__('举报内容与举证', 'jio'), (string) ($payload['reason'] ?? ''));
if ($type === 'user') {
$this->render_ban_restriction_section($id);
if (get_userdata($id)) {
echo '<p class="aksrt-ban-profile-link"><a href="' . esc_url($this->ban_user_home($id)) . '" target="_blank" rel="noopener noreferrer">' . esc_html__('查看用户主页并管理禁封(新窗口)', 'aksrt') . '</a></p>';
echo '<p class="jio-ban-profile-link"><a href="' . esc_url($this->ban_user_home($id)) . '" target="_blank" rel="noopener noreferrer">' . esc_html__('查看用户主页并管理禁封(新窗口)', 'jio') . '</a></p>';
}
}
if ($row['status'] === 'pending') {
echo '<p class="aksrt-native-callout">' . esc_html__('本次审核仅记录举报处理结果,不会自动禁封用户或删除内容。请先核实举证并完成必要处置。', 'aksrt') . '</p>';
echo '<p class="jio-native-callout">' . esc_html__('本次审核仅记录举报处理结果,不会自动禁封用户或删除内容。请先核实举证并完成必要处置。', 'jio') . '</p>';
}
}
private function render_ban_appeal_details(array $row): void {
$payload = $row['payload'];
$this->render_ban_restriction_section((int) $row['user_id']);
if (!empty($payload['reason_key'])) $this->render_ban_text_section(__('申诉类型', 'aksrt'), (string) $payload['reason_key']);
if (!empty($payload['reason_key'])) $this->render_ban_text_section(__('申诉类型', 'jio'), (string) $payload['reason_key']);
$data = isset($payload['data']) && is_array($payload['data']) ? $payload['data'] : [];
if ($data !== []) {
echo '<section class="aksrt-ban-section"><h3>' . esc_html__('申诉资料', 'aksrt') . '</h3><dl class="aksrt-ban-materials">';
echo '<section class="jio-ban-section"><h3>' . esc_html__('申诉资料', 'jio') . '</h3><dl class="jio-ban-materials">';
foreach ($data as $label => $value) {
if (is_scalar($value)) $this->render_ban_meta_item((string) $label, (string) $value);
}
echo '</dl></section>';
}
$reason = (string) ($payload['reason'] ?? '');
if ($data === [] || !in_array($reason, $data, true)) $this->render_ban_text_section(__('申诉说明', 'aksrt'), $reason);
if ($data === [] || !in_array($reason, $data, true)) $this->render_ban_text_section(__('申诉说明', 'jio'), $reason);
if (get_userdata((int) $row['user_id'])) {
echo '<p class="aksrt-ban-profile-link"><a href="' . esc_url($this->ban_user_home((int) $row['user_id'])) . '" target="_blank" rel="noopener noreferrer">' . esc_html__('查看申诉用户主页(新窗口)', 'aksrt') . '</a></p>';
echo '<p class="jio-ban-profile-link"><a href="' . esc_url($this->ban_user_home((int) $row['user_id'])) . '" target="_blank" rel="noopener noreferrer">' . esc_html__('查看申诉用户主页(新窗口)', 'jio') . '</a></p>';
}
}
private function render_ban_restriction_section(int $user_id): void {
echo '<section class="aksrt-ban-section"><h3>' . esc_html__('当前限制', 'aksrt') . '</h3>';
echo '<section class="jio-ban-section"><h3>' . esc_html__('当前限制', 'jio') . '</h3>';
if (!class_exists(Restriction::class)) {
echo '<p class="aksrt-native-callout is-warning">' . esc_html__('限制服务不可用,暂时无法核对当前状态。', 'aksrt') . '</p></section>';
echo '<p class="jio-native-callout is-warning">' . esc_html__('限制服务不可用,暂时无法核对当前状态。', 'jio') . '</p></section>';
return;
}
$rows = $this->ban_restrictions[$user_id] ?? [];
if ($rows === []) {
echo '<p class="aksrt-ban-normal">' . esc_html__('当前无生效限制。此处仅展示当前状态,不会自动更改处理结果。', 'aksrt') . '</p></section>';
echo '<p class="jio-ban-normal">' . esc_html__('当前无生效限制。此处仅展示当前状态,不会自动更改处理结果。', 'jio') . '</p></section>';
return;
}
$labels = [Restriction::TYPE_WARN => __('警告', 'aksrt'), Restriction::TYPE_MUTE => __('禁言', 'aksrt'), Restriction::TYPE_SUSPEND => __('停权', 'aksrt'), Restriction::TYPE_BAN => __('封禁', 'aksrt')];
echo '<ul class="aksrt-ban-restrictions">';
$labels = [Restriction::TYPE_WARN => __('警告', 'jio'), Restriction::TYPE_MUTE => __('禁言', 'jio'), Restriction::TYPE_SUSPEND => __('停权', 'jio'), Restriction::TYPE_BAN => __('封禁', 'jio')];
echo '<ul class="jio-ban-restrictions">';
foreach ($rows as $restriction) {
echo '<li><div class="aksrt-ban-restriction__title">' . NativeUi::badge($labels[$restriction['type']] ?? (string) $restriction['type'], 'warning', false)
. '<span>' . esc_html(($restriction['scope'] ?? '') === Restriction::SCOPE_COMMENT ? __('评论范围', 'aksrt') : __('全站范围', 'aksrt')) . '</span></div>';
echo '<div class="aksrt-ban-copy">' . esc_html((string) ($restriction['reason'] ?? '')) . '</div>';
echo '<li><div class="jio-ban-restriction__title">' . NativeUi::badge($labels[$restriction['type']] ?? (string) $restriction['type'], 'warning', false)
. '<span>' . esc_html(($restriction['scope'] ?? '') === Restriction::SCOPE_COMMENT ? __('评论范围', 'jio') : __('全站范围', 'jio')) . '</span></div>';
echo '<div class="jio-ban-copy">' . esc_html((string) ($restriction['reason'] ?? '')) . '</div>';
$start = !empty($restriction['created_at']) ? get_date_from_gmt((string) $restriction['created_at'], 'Y-m-d H:i') : '—';
$end = !empty($restriction['expires_at']) ? get_date_from_gmt((string) $restriction['expires_at'], 'Y-m-d H:i') : __('永久', 'aksrt');
echo '<div class="aksrt-ban-sub">' . esc_html(sprintf(__('开始:%1$s;结束:%2$s', 'aksrt'), $start, $end)) . '</div></li>';
$end = !empty($restriction['expires_at']) ? get_date_from_gmt((string) $restriction['expires_at'], 'Y-m-d H:i') : __('永久', 'jio');
echo '<div class="jio-ban-sub">' . esc_html(sprintf(__('开始:%1$s;结束:%2$s', 'jio'), $start, $end)) . '</div></li>';
}
echo '</ul></section>';
}
@@ -156,30 +156,30 @@ trait UsersMenusBanDialogs {
$notes[] = ['note' => $payload['admin_note'] ?? '', 'operator' => $payload['handled_by'] ?? 0, 'timestamp' => $payload['handled_at'], 'status' => $payload['decision'] ?? $row['raw_status']];
}
if ($notes === []) return;
$labels = ['pending' => __('待处理', 'aksrt'), 'reviewing' => __('审核中', 'aksrt'), 'resolved' => __('已处理', 'aksrt'), 'dismissed' => __('已驳回', 'aksrt'), 'approved' => __('已通过', 'aksrt'), 'rejected' => __('已驳回', 'aksrt')];
echo '<section class="aksrt-ban-section"><h3>' . esc_html__('处理记录', 'aksrt') . '</h3><ol class="aksrt-ban-history">';
$labels = ['pending' => __('待处理', 'jio'), 'reviewing' => __('审核中', 'jio'), 'resolved' => __('已处理', 'jio'), 'dismissed' => __('已驳回', 'jio'), 'approved' => __('已通过', 'jio'), 'rejected' => __('已驳回', 'jio')];
echo '<section class="jio-ban-section"><h3>' . esc_html__('处理记录', 'jio') . '</h3><ol class="jio-ban-history">';
foreach ($notes as $note) {
if (!is_array($note)) continue;
$operator = get_userdata((int) ($note['operator'] ?? 0));
echo '<li><div class="aksrt-ban-history__meta"><strong>' . esc_html($labels[$note['status'] ?? ''] ?? (string) ($note['status'] ?? '')) . '</strong><span>'
. esc_html($operator ? $operator->display_name : __('管理员', 'aksrt')) . '</span><time>' . esc_html((string) ($note['timestamp'] ?? '')) . '</time></div>';
echo '<div class="aksrt-ban-copy">' . esc_html((string) ($note['note'] ?? '') !== '' ? (string) $note['note'] : __('未填写处理留言', 'aksrt')) . '</div></li>';
echo '<li><div class="jio-ban-history__meta"><strong>' . esc_html($labels[$note['status'] ?? ''] ?? (string) ($note['status'] ?? '')) . '</strong><span>'
. esc_html($operator ? $operator->display_name : __('管理员', 'jio')) . '</span><time>' . esc_html((string) ($note['timestamp'] ?? '')) . '</time></div>';
echo '<div class="jio-ban-copy">' . esc_html((string) ($note['note'] ?? '') !== '' ? (string) $note['note'] : __('未填写处理留言', 'jio')) . '</div></li>';
}
echo '</ol></section>';
}
private function render_ban_review_fields(bool $report): void {
echo '<section class="aksrt-ban-review"><h3>' . esc_html__('审核处理', 'aksrt') . '</h3><fieldset class="aksrt-ban-decisions"><legend>' . esc_html__('处理结果(必选)', 'aksrt') . '</legend>';
$choices = $report ? ['resolved' => __('标记已处理', 'aksrt'), 'dismissed' => __('驳回举报', 'aksrt')] : ['approve' => __('通过申诉', 'aksrt'), 'reject' => __('驳回申诉', 'aksrt')];
echo '<section class="jio-ban-review"><h3>' . esc_html__('审核处理', 'jio') . '</h3><fieldset class="jio-ban-decisions"><legend>' . esc_html__('处理结果(必选)', 'jio') . '</legend>';
$choices = $report ? ['resolved' => __('标记已处理', 'jio'), 'dismissed' => __('驳回举报', 'jio')] : ['approve' => __('通过申诉', 'jio'), 'reject' => __('驳回申诉', 'jio')];
foreach ($choices as $value => $label) {
echo '<label><input type="radio" name="' . ($report ? 'new_status' : 'decision') . '" value="' . esc_attr($value) . '" required><span>' . esc_html($label) . '</span></label>';
}
echo '</fieldset>';
if (!$report) {
echo '<p class="aksrt-native-callout is-warning">' . esc_html__('通过申诉将解除该用户当前生效的封禁、停权和禁言,保留警告记录;驳回不会改变限制。', 'aksrt') . '</p>';
echo '<p class="jio-native-callout is-warning">' . esc_html__('通过申诉将解除该用户当前生效的封禁、停权和禁言,保留警告记录;驳回不会改变限制。', 'jio') . '</p>';
}
echo '<label class="aksrt-native-field"><span class="aksrt-native-field__label" data-aksrt-ban-note-label data-optional="' . esc_attr__('处理说明(选填)', 'aksrt') . '" data-required="' . esc_attr__('驳回原因(必填)', 'aksrt') . '">' . esc_html__('处理说明(选填)', 'aksrt') . '</span>';
echo '<textarea name="note" rows="3" data-required-error="' . esc_attr__('请填写驳回原因,不能只包含空格', 'aksrt') . '" aria-describedby="aksrt-ban-note-hint"></textarea></label>';
echo '<p id="aksrt-ban-note-hint" class="aksrt-native-field__hint">' . esc_html($report ? __('处理结果及说明会反馈给举报人。驳回时请填写具体原因。', 'aksrt') : __('处理结果及说明会反馈给申诉用户。驳回时请说明原因,便于用户补充资料。', 'aksrt')) . '</p></section>';
echo '<label class="jio-native-field"><span class="jio-native-field__label" data-jio-ban-note-label data-optional="' . esc_attr__('处理说明(选填)', 'jio') . '" data-required="' . esc_attr__('驳回原因(必填)', 'jio') . '">' . esc_html__('处理说明(选填)', 'jio') . '</span>';
echo '<textarea name="note" rows="3" data-required-error="' . esc_attr__('请填写驳回原因,不能只包含空格', 'jio') . '" aria-describedby="jio-ban-note-hint"></textarea></label>';
echo '<p id="jio-ban-note-hint" class="jio-native-field__hint">' . esc_html($report ? __('处理结果及说明会反馈给举报人。驳回时请填写具体原因。', 'jio') : __('处理结果及说明会反馈给申诉用户。驳回时请说明原因,便于用户补充资料。', 'jio')) . '</p></section>';
}
}
+13 -13
View File
@@ -1,9 +1,9 @@
<?php
/**
* @package AKSRT
* @package Jio
*/
namespace AKSRT\Admin\Native;
namespace Jio\Admin\Native;
defined('ABSPATH') || exit;
@@ -105,10 +105,10 @@ trait UsersMenusBanQuery {
private function query_ban_report_rows(array $filters): array {
global $wpdb;
if (!class_exists('\\AKSRT\\Core\\DBMigrator')) {
if (!class_exists('\\Jio\\Core\\DBMigrator')) {
return [];
}
$table = \AKSRT\Core\DBMigrator::table('report');
$table = \Jio\Core\DBMigrator::table('report');
if ($table === '') {
return [];
}
@@ -147,13 +147,13 @@ trait UsersMenusBanQuery {
/** @return array<int,array<string,mixed>> */
private function query_ban_appeal_rows(array $filters): array {
if (!class_exists('\\AKSRT\\User\\BanAppeal')) {
if (!class_exists('\\Jio\\User\\BanAppeal')) {
return [];
}
$args = [
'meta_query' => [[
'key' => \AKSRT\User\BanAppeal::META_STATUS,
'key' => \Jio\User\BanAppeal::META_STATUS,
'value' => ['pending', 'approved', 'rejected'],
'compare' => 'IN',
]],
@@ -216,12 +216,12 @@ trait UsersMenusBanQuery {
}
}
$report_ids = array_values(array_unique($report_ids));
if ($report_ids === [] || !class_exists('\\AKSRT\\Core\\DBMigrator')) {
if ($report_ids === [] || !class_exists('\\Jio\\Core\\DBMigrator')) {
return;
}
global $wpdb;
$table = \AKSRT\Core\DBMigrator::table('report');
$table = \Jio\Core\DBMigrator::table('report');
if ($table === '') {
return;
}
@@ -281,12 +281,12 @@ trait UsersMenusBanQuery {
if ($kind === 'appeal') {
return $this->get_ban_appeal_row($id);
}
if ($kind !== 'report' || !class_exists('\\AKSRT\\Core\\DBMigrator')) {
if ($kind !== 'report' || !class_exists('\\Jio\\Core\\DBMigrator')) {
return null;
}
global $wpdb;
$table = \AKSRT\Core\DBMigrator::table('report');
$table = \Jio\Core\DBMigrator::table('report');
if ($table === '') {
return null;
}
@@ -333,17 +333,17 @@ trait UsersMenusBanQuery {
}
private function get_ban_appeal_row(int $user_id): ?array {
if ($user_id <= 0 || !class_exists('\\AKSRT\\User\\BanAppeal')) {
if ($user_id <= 0 || !class_exists('\\Jio\\User\\BanAppeal')) {
return null;
}
$raw_status = \AKSRT\User\BanAppeal::get_status($user_id);
$raw_status = \Jio\User\BanAppeal::get_status($user_id);
$status = $raw_status === 'approved' ? 'resolved' : ($raw_status === 'rejected' ? 'dismissed' : $raw_status);
if (!in_array($status, ['pending', 'resolved', 'dismissed'], true)) {
return null;
}
$payload = \AKSRT\User\BanAppeal::get_payload($user_id);
$payload = \Jio\User\BanAppeal::get_payload($user_id);
return [
'kind' => 'appeal',
'id' => $user_id,
+59 -59
View File
@@ -2,10 +2,10 @@
/**
* Native invite-code list and page assets.
*
* @package AKSRT
* @package Jio
*/
namespace AKSRT\Admin\Native;
namespace Jio\Admin\Native;
defined('ABSPATH') || exit;
@@ -13,44 +13,44 @@ trait UsersMenusInviteCode {
use UsersMenusInviteForms;
public function enqueue_invite_assets(string $hook): void {
if ($hook !== 'users_page_aksrt_invite_codes') {
if ($hook !== 'users_page_jio_invite_codes') {
return;
}
$css = '/assets/css/admin/invite-codes.css';
$js = '/assets/js/admin-invite-codes.js';
wp_enqueue_style('aksrt-admin-invite-codes', AKSRT_URI . $css, [NativeUi::STYLE_HANDLE], (string) filemtime(AKSRT_DIR . $css));
wp_enqueue_script('aksrt-admin-invite-codes', AKSRT_URI . $js, [], (string) filemtime(AKSRT_DIR . $js), true);
wp_enqueue_style('jio-admin-invite-codes', JIO_URI . $css, [NativeUi::STYLE_HANDLE], (string) filemtime(JIO_DIR . $css));
wp_enqueue_script('jio-admin-invite-codes', JIO_URI . $js, [], (string) filemtime(JIO_DIR . $js), true);
}
public function render_invite_codes_page(): void {
if (!current_user_can('edit_users')) {
wp_die(esc_html__('您没有权限访问此页面。', 'aksrt'));
wp_die(esc_html__('您没有权限访问此页面。', 'jio'));
}
$service = \AKSRT\Payment\InviteCode::get_instance();
$this_url = admin_url('users.php?page=aksrt_invite_codes');
$nonce = wp_create_nonce('aksrt_invite_admin');
NativeUi::page_open('aksrt-inv-page');
$service = \Jio\Payment\InviteCode::get_instance();
$this_url = admin_url('users.php?page=jio_invite_codes');
$nonce = wp_create_nonce('jio_invite_admin');
NativeUi::page_open('jio-inv-page');
NativeUi::header(
__('邀请码管理', 'aksrt'),
__('管理注册邀请码与使用奖励,点击邀请码即可复制。', 'aksrt'),
__('邀请码管理', 'jio'),
__('管理注册邀请码与使用奖励,点击邀请码即可复制。', 'jio'),
'dashicons-tickets-alt',
[
['label' => __('添加邀请码', 'aksrt'), 'dialog' => 'aksrt-inv-add', 'primary' => true, 'icon' => 'dashicons-plus-alt2'],
['label' => __('导出邀请码', 'aksrt'), 'dialog' => 'aksrt-inv-export', 'icon' => 'dashicons-download'],
['label' => __('添加邀请码', 'jio'), 'dialog' => 'jio-inv-add', 'primary' => true, 'icon' => 'dashicons-plus-alt2'],
['label' => __('导出邀请码', 'jio'), 'dialog' => 'jio-inv-export', 'icon' => 'dashicons-download'],
]
);
$settings = $service->registration_settings();
if (empty($settings['enabled'])) {
$settings_url = admin_url('admin.php?page=aksrt-settings#/users/auth');
echo '<div class="notice notice-warning"><p>' . esc_html__('邀请码注册尚未启用,已添加的邀请码暂不能用于注册。', 'aksrt')
. ' <a href="' . esc_url($settings_url) . '">' . esc_html__('前往登录注册设置', 'aksrt') . '</a></p></div>';
$settings_url = admin_url('admin.php?page=jio-settings#/users/auth');
echo '<div class="notice notice-warning"><p>' . esc_html__('邀请码注册尚未启用,已添加的邀请码暂不能用于注册。', 'jio')
. ' <a href="' . esc_url($settings_url) . '">' . esc_html__('前往登录注册设置', 'jio') . '</a></p></div>';
}
$title = isset($_GET['aksrt_inv_title']) ? sanitize_text_field(rawurldecode((string) wp_unslash($_GET['aksrt_inv_title']))) : '';
$message = isset($_GET['aksrt_inv_msg']) ? sanitize_text_field(rawurldecode((string) wp_unslash($_GET['aksrt_inv_msg']))) : '';
$type = ($_GET['aksrt_inv_type'] ?? '') === 'error' ? 'error' : 'success';
$title = isset($_GET['jio_inv_title']) ? sanitize_text_field(rawurldecode((string) wp_unslash($_GET['jio_inv_title']))) : '';
$message = isset($_GET['jio_inv_msg']) ? sanitize_text_field(rawurldecode((string) wp_unslash($_GET['jio_inv_msg']))) : '';
$type = ($_GET['jio_inv_type'] ?? '') === 'error' ? 'error' : 'success';
if ($title !== '') {
echo '<div class="notice notice-' . esc_attr($type) . ' is-dismissible"><p><strong>' . esc_html($title) . '</strong>'
. ($message !== '' ? ' ' . esc_html($message) : '') . '</p></div>';
@@ -59,14 +59,14 @@ trait UsersMenusInviteCode {
$this->render_invite_codes_list($service, $this_url, $nonce);
$this->render_invite_codes_add($nonce);
$this->render_invite_codes_export($nonce);
echo '<div class="aksrt-inv-toast" role="status" aria-live="polite" data-aksrt-inv-toast'
. ' data-copied="' . esc_attr__('邀请码已复制', 'aksrt') . '"'
. ' data-copy-failed="' . esc_attr__('自动复制失败,请选中邀请码后手动复制。', 'aksrt') . '"'
. ' data-exported="' . esc_attr__('下载请求已提交,请查看浏览器下载列表。', 'aksrt') . '"></div>';
echo '<div class="jio-inv-toast" role="status" aria-live="polite" data-jio-inv-toast'
. ' data-copied="' . esc_attr__('邀请码已复制', 'jio') . '"'
. ' data-copy-failed="' . esc_attr__('自动复制失败,请选中邀请码后手动复制。', 'jio') . '"'
. ' data-exported="' . esc_attr__('下载请求已提交,请查看浏览器下载列表。', 'jio') . '"></div>';
NativeUi::page_close();
}
private function render_invite_codes_list(\AKSRT\Payment\InviteCode $service, string $this_url, string $nonce): void {
private function render_invite_codes_list(\Jio\Payment\InviteCode $service, string $this_url, string $nonce): void {
$status = in_array(($_GET['status'] ?? 'all'), ['all', 'unused', 'used'], true) ? (string) $_GET['status'] : 'all';
$s = isset($_GET['s']) ? sanitize_text_field((string) wp_unslash($_GET['s'])) : '';
$remark = isset($_GET['remark']) ? sanitize_text_field((string) wp_unslash($_GET['remark'])) : '';
@@ -81,44 +81,44 @@ trait UsersMenusInviteCode {
$filters = ['status' => $status, 's' => $s, 'remark' => $remark, 'orderby' => $data['orderby'], 'desc' => $data['desc']];
$filter_url = add_query_arg($filters, $this_url);
NativeUi::card_open('', '', true, 'aksrt-inv-list-card');
echo '<div class="aksrt-inv-toolbar"><div class="aksrt-native-toolbar"><div class="aksrt-inv-toolbar__views">';
echo '<h2 class="aksrt-native-card__title">' . esc_html__('邀请码列表', 'aksrt') . '</h2>';
echo '<ul class="subsubsub" aria-label="' . esc_attr__('使用状态筛选', 'aksrt') . '">';
foreach (['all' => __('全部', 'aksrt'), 'unused' => __('未使用', 'aksrt'), 'used' => __('已使用', 'aksrt')] as $key => $label) {
NativeUi::card_open('', '', true, 'jio-inv-list-card');
echo '<div class="jio-inv-toolbar"><div class="jio-native-toolbar"><div class="jio-inv-toolbar__views">';
echo '<h2 class="jio-native-card__title">' . esc_html__('邀请码列表', 'jio') . '</h2>';
echo '<ul class="subsubsub" aria-label="' . esc_attr__('使用状态筛选', 'jio') . '">';
foreach (['all' => __('全部', 'jio'), 'unused' => __('未使用', 'jio'), 'used' => __('已使用', 'jio')] as $key => $label) {
$url = add_query_arg('status', $key, $filter_url);
echo '<li><a href="' . esc_url($url) . '"' . ($key === $status ? ' class="current" aria-current="page"' : '') . '>' . esc_html($label) . '</a></li>';
}
echo '</ul></div><form method="get" action="' . esc_url(admin_url('users.php')) . '" class="aksrt-native-filters">';
echo '<input type="hidden" name="page" value="aksrt_invite_codes">';
echo '</ul></div><form method="get" action="' . esc_url(admin_url('users.php')) . '" class="jio-native-filters">';
echo '<input type="hidden" name="page" value="jio_invite_codes">';
foreach (array_diff_key($filters, ['s' => '']) as $key => $value) {
echo '<input type="hidden" name="' . esc_attr($key) . '" value="' . esc_attr($value) . '">';
}
echo '<label class="screen-reader-text" for="aksrt-inv-search">' . esc_html__('搜索邀请码或标识', 'aksrt') . '</label>';
echo '<input type="search" id="aksrt-inv-search" name="s" value="' . esc_attr($s) . '" placeholder="' . esc_attr__('搜索邀请码或标识', 'aksrt') . '">';
echo '<button type="submit" class="button">' . esc_html__('搜索', 'aksrt') . '</button>';
echo '<label class="screen-reader-text" for="jio-inv-search">' . esc_html__('搜索邀请码或标识', 'jio') . '</label>';
echo '<input type="search" id="jio-inv-search" name="s" value="' . esc_attr($s) . '" placeholder="' . esc_attr__('搜索邀请码或标识', 'jio') . '">';
echo '<button type="submit" class="button">' . esc_html__('搜索', 'jio') . '</button>';
if ($remark !== '') {
echo '<span class="aksrt-inv-filter-remark">' . esc_html(sprintf(__('标识:%s', 'aksrt'), $remark)) . '</span>';
echo '<span class="jio-inv-filter-remark">' . esc_html(sprintf(__('标识:%s', 'jio'), $remark)) . '</span>';
}
if ($s !== '' || $remark !== '' || $status !== 'all') {
echo '<a class="button-link" href="' . esc_url($this_url) . '">' . esc_html__('清除筛选', 'aksrt') . '</a>';
echo '<a class="button-link" href="' . esc_url($this_url) . '">' . esc_html__('清除筛选', 'jio') . '</a>';
}
echo '</form></div></div>';
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" data-aksrt-inv-list'
. ' data-confirm="' . esc_attr__('确定删除选中的 %d 个邀请码及其使用记录?此操作不可撤销。', 'aksrt') . '">';
echo '<input type="hidden" name="action" value="aksrt_invite_delete"><input type="hidden" name="aksrt_nonce" value="' . esc_attr($nonce) . '">';
echo '<div class="aksrt-native-bulk aksrt-inv-bulk"><span class="aksrt-inv-selection" data-aksrt-inv-selection data-label="' . esc_attr__('已选 %d 项', 'aksrt') . '" aria-live="polite"></span>'
. '<label class="screen-reader-text" for="bulk-action-selector-top">' . esc_html__('批量操作', 'aksrt') . '</label>'
. '<select name="bulk_action" id="bulk-action-selector-top"><option value="-1">' . esc_html__('批量操作', 'aksrt') . '</option><option value="delete">' . esc_html__('删除', 'aksrt') . '</option></select>'
. '<button type="submit" class="button" data-aksrt-inv-bulk disabled>' . esc_html__('应用', 'aksrt') . '</button></div>';
echo '<div class="aksrt-native-table-wrap">';
echo '<table class="widefat fixed striped aksrt-inv-table"><thead><tr><td class="manage-column column-cb check-column">';
echo '<input type="checkbox" data-aksrt-inv-check-all aria-label="' . esc_attr__('全选本页邀请码', 'aksrt') . '"></td>';
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" data-jio-inv-list'
. ' data-confirm="' . esc_attr__('确定删除选中的 %d 个邀请码及其使用记录?此操作不可撤销。', 'jio') . '">';
echo '<input type="hidden" name="action" value="jio_invite_delete"><input type="hidden" name="jio_nonce" value="' . esc_attr($nonce) . '">';
echo '<div class="jio-native-bulk jio-inv-bulk"><span class="jio-inv-selection" data-jio-inv-selection data-label="' . esc_attr__('已选 %d 项', 'jio') . '" aria-live="polite"></span>'
. '<label class="screen-reader-text" for="bulk-action-selector-top">' . esc_html__('批量操作', 'jio') . '</label>'
. '<select name="bulk_action" id="bulk-action-selector-top"><option value="-1">' . esc_html__('批量操作', 'jio') . '</option><option value="delete">' . esc_html__('删除', 'jio') . '</option></select>'
. '<button type="submit" class="button" data-jio-inv-bulk disabled>' . esc_html__('应用', 'jio') . '</button></div>';
echo '<div class="jio-native-table-wrap">';
echo '<table class="widefat fixed striped jio-inv-table"><thead><tr><td class="manage-column column-cb check-column">';
echo '<input type="checkbox" data-jio-inv-check-all aria-label="' . esc_attr__('全选本页邀请码', 'jio') . '"></td>';
$columns = [
'code' => __('邀请码', 'aksrt'), 'rewards' => __('使用奖励', 'aksrt'),
'created_at' => __('创建时间', 'aksrt'), 'updated_at' => __('更新时间', 'aksrt'),
'used_count' => __('使用状态', 'aksrt'), 'remark' => __('标识', 'aksrt'),
'code' => __('邀请码', 'jio'), 'rewards' => __('使用奖励', 'jio'),
'created_at' => __('创建时间', 'jio'), 'updated_at' => __('更新时间', 'jio'),
'used_count' => __('使用状态', 'jio'), 'remark' => __('标识', 'jio'),
];
foreach ($columns as $key => $label) {
$active = $key === $data['orderby'];
@@ -130,38 +130,38 @@ trait UsersMenusInviteCode {
$next = $active && $data['desc'] === 'ASC' ? 'DESC' : 'ASC';
$url = add_query_arg(['orderby' => $key, 'desc' => $next], $filter_url);
$icon = $active && $data['desc'] === 'ASC' ? 'dashicons-arrow-up-alt2' : 'dashicons-arrow-down-alt2';
echo '<a class="orderby-but' . ($active ? ' is-active' : '') . '" href="' . esc_url($url) . '" title="' . esc_attr($next === 'ASC' ? __('按此列升序排列', 'aksrt') : __('按此列降序排列', 'aksrt')) . '">'
echo '<a class="orderby-but' . ($active ? ' is-active' : '') . '" href="' . esc_url($url) . '" title="' . esc_attr($next === 'ASC' ? __('按此列升序排列', 'jio') : __('按此列降序排列', 'jio')) . '">'
. esc_html($label) . '<span class="dashicons ' . esc_attr($icon) . '" aria-hidden="true"></span></a>';
}
echo '</th>';
}
echo '</tr></thead><tbody>';
if ($data['items'] === []) {
echo NativeUi::empty_row(7, __('暂无符合条件的邀请码', 'aksrt'), __('调整筛选条件,或点击“添加邀请码”生成、导入新的邀请码。', 'aksrt'));
echo NativeUi::empty_row(7, __('暂无符合条件的邀请码', 'jio'), __('调整筛选条件,或点击“添加邀请码”生成、导入新的邀请码。', 'jio'));
}
foreach ($data['items'] as $item) {
$used = (int) $item['used_count'];
$max_uses = (int) $item['max_uses'];
echo '<tr><th scope="row" class="check-column"><input type="checkbox" name="action_id[]" value="' . esc_attr((string) $item['id']) . '" aria-label="' . esc_attr(sprintf(__('选择邀请码 %s', 'aksrt'), $item['code'])) . '"></th>';
echo '<td><button type="button" class="aksrt-inv-code aksrt-native-mono" data-clipboard-text="' . esc_attr($item['code']) . '" aria-label="' . esc_attr(sprintf(__('复制邀请码 %s', 'aksrt'), $item['code'])) . '">' . esc_html($item['code']) . '<span class="dashicons dashicons-admin-page" aria-hidden="true"></span></button></td>';
echo '<tr><th scope="row" class="check-column"><input type="checkbox" name="action_id[]" value="' . esc_attr((string) $item['id']) . '" aria-label="' . esc_attr(sprintf(__('选择邀请码 %s', 'jio'), $item['code'])) . '"></th>';
echo '<td><button type="button" class="jio-inv-code jio-native-mono" data-clipboard-text="' . esc_attr($item['code']) . '" aria-label="' . esc_attr(sprintf(__('复制邀请码 %s', 'jio'), $item['code'])) . '">' . esc_html($item['code']) . '<span class="dashicons dashicons-admin-page" aria-hidden="true"></span></button></td>';
echo '<td>' . wp_kses((string) $item['reward_text'], ['br' => []]) . '</td>';
echo '<td class="aksrt-inv-date">' . esc_html($item['created_at']) . '</td><td class="aksrt-inv-date">' . esc_html($item['updated_at']) . '</td><td>';
$label = $used > 0 && $max_uses > 0 && $used < $max_uses ? sprintf(__('已使用 %1$d/%2$d', 'aksrt'), $used, $max_uses) : ($used > 0 ? __('已使用', 'aksrt') : __('未使用', 'aksrt'));
echo '<td class="jio-inv-date">' . esc_html($item['created_at']) . '</td><td class="jio-inv-date">' . esc_html($item['updated_at']) . '</td><td>';
$label = $used > 0 && $max_uses > 0 && $used < $max_uses ? sprintf(__('已使用 %1$d/%2$d', 'jio'), $used, $max_uses) : ($used > 0 ? __('已使用', 'jio') : __('未使用', 'jio'));
echo NativeUi::badge($label, $used > 0 ? 'neutral' : 'success');
foreach ((array) $item['recent_users'] as $user) {
echo '<div class="aksrt-inv-user"><a href="' . esc_url(get_edit_user_link((int) $user['user_id'])) . '">' . esc_html($user['name']) . '</a></div>';
echo '<div class="jio-inv-user"><a href="' . esc_url(get_edit_user_link((int) $user['user_id'])) . '">' . esc_html($user['name']) . '</a></div>';
}
echo '</td><td>';
if ($item['remark'] !== '') {
echo '<a href="' . esc_url(add_query_arg('remark', $item['remark'], $this_url)) . '">' . esc_html($item['remark']) . '</a>';
} else {
echo '<span class="aksrt-native-muted">—</span>';
echo '<span class="jio-native-muted">—</span>';
}
echo '</td></tr>';
}
echo '</tbody></table>';
NativeUi::table_wrap_close();
echo '<div class="tablenav bottom"><div class="tablenav-pages"><span class="displaying-num">' . esc_html(sprintf(__('共 %s 项', 'aksrt'), number_format_i18n((int) $data['total']))) . '</span>';
echo '<div class="tablenav bottom"><div class="tablenav-pages"><span class="displaying-num">' . esc_html(sprintf(__('共 %s 项', 'jio'), number_format_i18n((int) $data['total']))) . '</span>';
if ((int) $data['max_pages'] > 1) {
echo paginate_links([
'base' => add_query_arg('paged', '%#%', $filter_url), 'format' => '',
+49 -49
View File
@@ -2,91 +2,91 @@
/**
* Invite-code generation, import and export dialogs.
*
* @package AKSRT
* @package Jio
*/
namespace AKSRT\Admin\Native;
namespace Jio\Admin\Native;
defined('ABSPATH') || exit;
trait UsersMenusInviteForms {
private function render_invite_codes_add(string $nonce): void {
$this->invite_dialog_open('aksrt-inv-add', __('添加邀请码', 'aksrt'), __('批量生成新邀请码,或导入已有的邀请码数据。', 'aksrt'), 'post', 'aksrt_invite_add', $nonce);
echo '<fieldset class="aksrt-inv-modes"><legend class="screen-reader-text">' . esc_html__('添加方式', 'aksrt') . '</legend>';
foreach (['auto' => __('系统自动生成', 'aksrt'), 'import' => __('导入邀请码', 'aksrt')] as $value => $label) {
$this->invite_dialog_open('jio-inv-add', __('添加邀请码', 'jio'), __('批量生成新邀请码,或导入已有的邀请码数据。', 'jio'), 'post', 'jio_invite_add', $nonce);
echo '<fieldset class="jio-inv-modes"><legend class="screen-reader-text">' . esc_html__('添加方式', 'jio') . '</legend>';
foreach (['auto' => __('系统自动生成', 'jio'), 'import' => __('导入邀请码', 'jio')] as $value => $label) {
echo '<label><input type="radio" name="add_type" value="' . esc_attr($value) . '"' . checked($value, 'auto', false) . '><span>' . esc_html($label) . '</span></label>';
}
echo '</fieldset><fieldset class="aksrt-inv-pane" data-aksrt-inv-dep="add_type=auto"><legend class="screen-reader-text">' . esc_html__('自动生成设置', 'aksrt') . '</legend>';
echo '<div class="aksrt-native-form-grid">';
echo '<label class="aksrt-native-field"><span class="aksrt-native-field__label">' . esc_html__('生成数量', 'aksrt') . '</span>'
echo '</fieldset><fieldset class="jio-inv-pane" data-jio-inv-dep="add_type=auto"><legend class="screen-reader-text">' . esc_html__('自动生成设置', 'jio') . '</legend>';
echo '<div class="jio-native-form-grid">';
echo '<label class="jio-native-field"><span class="jio-native-field__label">' . esc_html__('生成数量', 'jio') . '</span>'
. '<input type="number" name="auto_num" value="20" min="1" max="1000" step="1" required autofocus>'
. '<small class="aksrt-native-field__hint">' . esc_html__('单次可生成 1–1000 个。', 'aksrt') . '</small></label>';
echo '<label class="aksrt-native-field"><span class="aksrt-native-field__label">' . esc_html__('标识', 'aksrt') . '</span>'
. '<small class="jio-native-field__hint">' . esc_html__('单次可生成 1–1000 个。', 'jio') . '</small></label>';
echo '<label class="jio-native-field"><span class="jio-native-field__label">' . esc_html__('标识', 'jio') . '</span>'
. '<input type="text" name="auto_remarks" value="' . esc_attr('invita_' . current_time('YmdHis')) . '" maxlength="191">'
. '<small class="aksrt-native-field__hint">' . esc_html__('为本批邀请码命名,方便查找与管理。', 'aksrt') . '</small></label></div>';
. '<small class="jio-native-field__hint">' . esc_html__('为本批邀请码命名,方便查找与管理。', 'jio') . '</small></label></div>';
$this->render_invite_reward_fields();
echo '<label class="aksrt-inv-advanced"><input type="checkbox" name="auto_top_s" value="1">' . esc_html__('自定义邀请码位数', 'aksrt') . '</label>';
echo '<fieldset class="aksrt-inv-pane aksrt-inv-length" data-aksrt-inv-dep="auto_top_s=1" hidden disabled><legend class="screen-reader-text">' . esc_html__('自定义位数', 'aksrt') . '</legend>';
echo '<label class="aksrt-native-field"><span class="aksrt-native-field__label">' . esc_html__('邀请码位数', 'aksrt') . '</span>'
echo '<label class="jio-inv-advanced"><input type="checkbox" name="auto_top_s" value="1">' . esc_html__('自定义邀请码位数', 'jio') . '</label>';
echo '<fieldset class="jio-inv-pane jio-inv-length" data-jio-inv-dep="auto_top_s=1" hidden disabled><legend class="screen-reader-text">' . esc_html__('自定义位数', 'jio') . '</legend>';
echo '<label class="jio-native-field"><span class="jio-native-field__label">' . esc_html__('邀请码位数', 'jio') . '</span>'
. '<input type="number" name="auto_rand_password_limit" value="10" min="4" max="50" step="1" required>'
. '<small class="aksrt-native-field__hint">' . esc_html__('4–50 位;默认 10 位。位数过短容易重复。', 'aksrt') . '</small></label></fieldset></fieldset>';
. '<small class="jio-native-field__hint">' . esc_html__('4–50 位;默认 10 位。位数过短容易重复。', 'jio') . '</small></label></fieldset></fieldset>';
echo '<fieldset class="aksrt-inv-pane" data-aksrt-inv-dep="add_type=import" hidden disabled><legend class="screen-reader-text">' . esc_html__('导入设置', 'aksrt') . '</legend>';
echo '<label class="aksrt-native-field"><span class="aksrt-native-field__label">' . esc_html__('邀请码数据', 'aksrt') . '</span>'
. '<textarea name="import_data" rows="7" class="code" required data-limit-error="' . esc_attr__('单次最多导入 1000 行,请拆分后重试。', 'aksrt') . '" placeholder="93FW0XAY 无 signup&#10;P86NPWKI points=10 signup" aria-describedby="aksrt-inv-import-hint"></textarea>'
. '<small class="aksrt-native-field__hint" id="aksrt-inv-import-hint">' . esc_html__('每行一个,格式为“邀请码 奖励 标识”,无奖励填“无”。单次最多 1000 条,空行会忽略,重复的邀请码不会导入。', 'aksrt') . '</small></label>';
echo '<label class="aksrt-native-field aksrt-inv-separator"><span class="aksrt-native-field__label">' . esc_html__('分隔符', 'aksrt') . '</span>'
echo '<fieldset class="jio-inv-pane" data-jio-inv-dep="add_type=import" hidden disabled><legend class="screen-reader-text">' . esc_html__('导入设置', 'jio') . '</legend>';
echo '<label class="jio-native-field"><span class="jio-native-field__label">' . esc_html__('邀请码数据', 'jio') . '</span>'
. '<textarea name="import_data" rows="7" class="code" required data-limit-error="' . esc_attr__('单次最多导入 1000 行,请拆分后重试。', 'jio') . '" placeholder="93FW0XAY 无 signup&#10;P86NPWKI points=10 signup" aria-describedby="jio-inv-import-hint"></textarea>'
. '<small class="jio-native-field__hint" id="jio-inv-import-hint">' . esc_html__('每行一个,格式为“邀请码 奖励 标识”,无奖励填“无”。单次最多 1000 条,空行会忽略,重复的邀请码不会导入。', 'jio') . '</small></label>';
echo '<label class="jio-native-field jio-inv-separator"><span class="jio-native-field__label">' . esc_html__('分隔符', 'jio') . '</span>'
. '<input type="text" name="import_division" value=" " maxlength="4">'
. '<small class="aksrt-native-field__hint">' . esc_html__('默认为空格,也可填写其他分隔符,例如 |。', 'aksrt') . '</small></label>';
echo '<details class="aksrt-inv-help"><summary>' . esc_html__('查看奖励格式与示例', 'aksrt') . '</summary><div>'
. '<p>' . esc_html__('奖励参数用 & 连接:level_integral 为经验值,points 为积分,balance 为余额(元),vip 为会员等级(1 或 2),vip_time 为会员天数。', 'aksrt') . '</p>'
. '<small class="jio-native-field__hint">' . esc_html__('默认为空格,也可填写其他分隔符,例如 |。', 'jio') . '</small></label>';
echo '<details class="jio-inv-help"><summary>' . esc_html__('查看奖励格式与示例', 'jio') . '</summary><div>'
. '<p>' . esc_html__('奖励参数用 & 连接:level_integral 为经验值,points 为积分,balance 为余额(元),vip 为会员等级(1 或 2),vip_time 为会员天数。', 'jio') . '</p>'
. '<pre>93FW0XAY 无 signup&#10;P86NPWKI points=10 signup&#10;AUF9D2B4 balance=30&amp;vip=2&amp;vip_time=5 signup</pre>'
. '<p>' . esc_html__('第三行示例:奖励 30 元余额和 5 天二级会员。标识可省略;导出的文本文件也可按相同分隔符重新导入。', 'aksrt') . '</p></div></details></fieldset>';
$this->invite_dialog_close(__('生成邀请码', 'aksrt'), ' data-aksrt-inv-add-submit data-auto-label="' . esc_attr__('生成邀请码', 'aksrt') . '" data-import-label="' . esc_attr__('导入邀请码', 'aksrt') . '" data-busy-label="' . esc_attr__('正在添加…', 'aksrt') . '"');
. '<p>' . esc_html__('第三行示例:奖励 30 元余额和 5 天二级会员。标识可省略;导出的文本文件也可按相同分隔符重新导入。', 'jio') . '</p></div></details></fieldset>';
$this->invite_dialog_close(__('生成邀请码', 'jio'), ' data-jio-inv-add-submit data-auto-label="' . esc_attr__('生成邀请码', 'jio') . '" data-import-label="' . esc_attr__('导入邀请码', 'jio') . '" data-busy-label="' . esc_attr__('正在添加…', 'jio') . '"');
}
private function render_invite_reward_fields(): void {
echo '<fieldset class="aksrt-inv-rewards"><legend>' . esc_html__('使用奖励', 'aksrt') . '</legend><p class="aksrt-inv-hint">' . esc_html__('用户使用邀请码注册后获得以下奖励;数值为 0 表示不赠送。', 'aksrt') . '</p>';
echo '<div class="aksrt-native-form-grid aksrt-inv-reward-grid">';
foreach (['level_integral' => __('经验值', 'aksrt'), 'points' => __('积分', 'aksrt'), 'balance' => __('余额(元)', 'aksrt')] as $name => $label) {
echo '<label class="aksrt-native-field"><span class="aksrt-native-field__label">' . esc_html($label) . '</span><input type="number" name="' . esc_attr($name) . '" value="0" min="0" step="' . ($name === 'balance' ? '0.01' : '1') . '"></label>';
echo '<fieldset class="jio-inv-rewards"><legend>' . esc_html__('使用奖励', 'jio') . '</legend><p class="jio-inv-hint">' . esc_html__('用户使用邀请码注册后获得以下奖励;数值为 0 表示不赠送。', 'jio') . '</p>';
echo '<div class="jio-native-form-grid jio-inv-reward-grid">';
foreach (['level_integral' => __('经验值', 'jio'), 'points' => __('积分', 'jio'), 'balance' => __('余额(元)', 'jio')] as $name => $label) {
echo '<label class="jio-native-field"><span class="jio-native-field__label">' . esc_html($label) . '</span><input type="number" name="' . esc_attr($name) . '" value="0" min="0" step="' . ($name === 'balance' ? '0.01' : '1') . '"></label>';
}
echo '</div><fieldset class="aksrt-inv-vip"><legend>' . esc_html__('VIP 会员', 'aksrt') . '</legend><div class="aksrt-inv-radios">';
foreach (['0' => __('不赠送', 'aksrt'), '1' => __('一级会员', 'aksrt'), '2' => __('二级会员', 'aksrt')] as $value => $label) {
echo '</div><fieldset class="jio-inv-vip"><legend>' . esc_html__('VIP 会员', 'jio') . '</legend><div class="jio-inv-radios">';
foreach (['0' => __('不赠送', 'jio'), '1' => __('一级会员', 'jio'), '2' => __('二级会员', 'jio')] as $value => $label) {
echo '<label><input type="radio" name="vip" value="' . esc_attr($value) . '"' . checked((string) $value, '0', false) . '>' . esc_html($label) . '</label>';
}
echo '</div></fieldset><fieldset class="aksrt-inv-pane aksrt-inv-vip-time" data-aksrt-inv-dep="vip=1|2" hidden disabled><legend class="screen-reader-text">' . esc_html__('会员时长', 'aksrt') . '</legend>';
echo '<label class="aksrt-native-field"><span class="aksrt-native-field__label">' . esc_html__('会员赠送时长(天)', 'aksrt') . '</span><input type="number" name="vip_time" value="30" min="1" step="1" required></label></fieldset></fieldset>';
echo '</div></fieldset><fieldset class="jio-inv-pane jio-inv-vip-time" data-jio-inv-dep="vip=1|2" hidden disabled><legend class="screen-reader-text">' . esc_html__('会员时长', 'jio') . '</legend>';
echo '<label class="jio-native-field"><span class="jio-native-field__label">' . esc_html__('会员赠送时长(天)', 'jio') . '</span><input type="number" name="vip_time" value="30" min="1" step="1" required></label></fieldset></fieldset>';
}
private function render_invite_codes_export(string $nonce): void {
$status = in_array(($_GET['status'] ?? 'all'), ['all', 'unused', 'used'], true) ? (string) $_GET['status'] : 'all';
$this->invite_dialog_open('aksrt-inv-export', __('导出邀请码', 'aksrt'), __('选择使用状态与文件格式,导出后可在本地保存或分发。', 'aksrt'), 'get', 'aksrt_invite_export', $nonce);
echo '<fieldset class="aksrt-inv-options"><legend>' . esc_html__('使用状态', 'aksrt') . '</legend><div class="aksrt-inv-radios">';
foreach (['all' => __('全部', 'aksrt'), 'unused' => __('未使用', 'aksrt'), 'used' => __('已使用', 'aksrt')] as $value => $label) {
$this->invite_dialog_open('jio-inv-export', __('导出邀请码', 'jio'), __('选择使用状态与文件格式,导出后可在本地保存或分发。', 'jio'), 'get', 'jio_invite_export', $nonce);
echo '<fieldset class="jio-inv-options"><legend>' . esc_html__('使用状态', 'jio') . '</legend><div class="jio-inv-radios">';
foreach (['all' => __('全部', 'jio'), 'unused' => __('未使用', 'jio'), 'used' => __('已使用', 'jio')] as $value => $label) {
echo '<label><input type="radio" name="status" value="' . esc_attr($value) . '"' . checked($value, $status, false) . '>' . esc_html($label) . '</label>';
}
echo '</div></fieldset><fieldset class="aksrt-inv-options"><legend>' . esc_html__('导出格式', 'aksrt') . '</legend><div class="aksrt-inv-formats">';
foreach (['csv' => [__('Excel 表格(.csv)', 'aksrt'), __('包含奖励、时间、使用状态与标识,适合整理统计。', 'aksrt')], 'text' => [__('文本文档(.txt)', 'aksrt'), __('每行一个邀请码,包含奖励与标识,可再次导入。', 'aksrt')]] as $value => [$label, $hint]) {
echo '</div></fieldset><fieldset class="jio-inv-options"><legend>' . esc_html__('导出格式', 'jio') . '</legend><div class="jio-inv-formats">';
foreach (['csv' => [__('Excel 表格(.csv)', 'jio'), __('包含奖励、时间、使用状态与标识,适合整理统计。', 'jio')], 'text' => [__('文本文档(.txt)', 'jio'), __('每行一个邀请码,包含奖励与标识,可再次导入。', 'jio')]] as $value => [$label, $hint]) {
echo '<label><input type="radio" name="export_format" value="' . esc_attr($value) . '"' . checked($value, 'csv', false) . '><span><strong>' . esc_html($label) . '</strong><small>' . esc_html($hint) . '</small></span></label>';
}
echo '</div></fieldset><fieldset class="aksrt-inv-pane" data-aksrt-inv-dep="export_format=text" hidden disabled><legend class="screen-reader-text">' . esc_html__('文本设置', 'aksrt') . '</legend>';
echo '<label class="aksrt-native-field aksrt-inv-separator"><span class="aksrt-native-field__label">' . esc_html__('分隔符', 'aksrt') . '</span><input type="text" name="text_division" value=" " maxlength="4">'
. '<small class="aksrt-native-field__hint">' . esc_html__('默认为空格;重新导入时请使用相同分隔符。', 'aksrt') . '</small></label></fieldset>';
echo '<p class="aksrt-native-callout aksrt-inv-export-note">' . esc_html__('每次最多导出最新的 10,000 条记录。导出仅按上方使用状态筛选,不受列表关键词、标识或勾选项影响。', 'aksrt') . '</p>';
$this->invite_dialog_close(__('导出并下载', 'aksrt'));
echo '</div></fieldset><fieldset class="jio-inv-pane" data-jio-inv-dep="export_format=text" hidden disabled><legend class="screen-reader-text">' . esc_html__('文本设置', 'jio') . '</legend>';
echo '<label class="jio-native-field jio-inv-separator"><span class="jio-native-field__label">' . esc_html__('分隔符', 'jio') . '</span><input type="text" name="text_division" value=" " maxlength="4">'
. '<small class="jio-native-field__hint">' . esc_html__('默认为空格;重新导入时请使用相同分隔符。', 'jio') . '</small></label></fieldset>';
echo '<p class="jio-native-callout jio-inv-export-note">' . esc_html__('每次最多导出最新的 10,000 条记录。导出仅按上方使用状态筛选,不受列表关键词、标识或勾选项影响。', 'jio') . '</p>';
$this->invite_dialog_close(__('导出并下载', 'jio'));
}
private function invite_dialog_open(string $id, string $title, string $description, string $method, string $action, string $nonce): void {
echo '<dialog class="aksrt-inv-dialog" id="' . esc_attr($id) . '" aria-labelledby="' . esc_attr($id) . '-title" aria-describedby="' . esc_attr($id) . '-description">';
echo '<form method="' . esc_attr($method) . '" action="' . esc_url(admin_url('admin-post.php')) . '" class="aksrt-inv-dialog__frame">';
echo '<input type="hidden" name="action" value="' . esc_attr($action) . '"><input type="hidden" name="aksrt_nonce" value="' . esc_attr($nonce) . '">';
echo '<header class="aksrt-inv-dialog__head"><div><h2 id="' . esc_attr($id) . '-title">' . esc_html($title) . '</h2><p id="' . esc_attr($id) . '-description">' . esc_html($description) . '</p></div>';
echo '<button type="button" class="aksrt-inv-dialog__close" data-aksrt-dialog-close aria-label="' . esc_attr__('关闭弹窗', 'aksrt') . '"><span class="dashicons dashicons-no-alt" aria-hidden="true"></span></button></header><div class="aksrt-inv-dialog__body">';
echo '<dialog class="jio-inv-dialog" id="' . esc_attr($id) . '" aria-labelledby="' . esc_attr($id) . '-title" aria-describedby="' . esc_attr($id) . '-description">';
echo '<form method="' . esc_attr($method) . '" action="' . esc_url(admin_url('admin-post.php')) . '" class="jio-inv-dialog__frame">';
echo '<input type="hidden" name="action" value="' . esc_attr($action) . '"><input type="hidden" name="jio_nonce" value="' . esc_attr($nonce) . '">';
echo '<header class="jio-inv-dialog__head"><div><h2 id="' . esc_attr($id) . '-title">' . esc_html($title) . '</h2><p id="' . esc_attr($id) . '-description">' . esc_html($description) . '</p></div>';
echo '<button type="button" class="jio-inv-dialog__close" data-jio-dialog-close aria-label="' . esc_attr__('关闭弹窗', 'jio') . '"><span class="dashicons dashicons-no-alt" aria-hidden="true"></span></button></header><div class="jio-inv-dialog__body">';
}
private function invite_dialog_close(string $label, string $attributes = ''): void {
echo '</div><footer class="aksrt-inv-dialog__foot"><button type="button" class="button" data-aksrt-dialog-close>' . esc_html__('取消', 'aksrt') . '</button>';
echo '</div><footer class="jio-inv-dialog__foot"><button type="button" class="button" data-jio-dialog-close>' . esc_html__('取消', 'jio') . '</button>';
echo '<button type="submit" class="button button-primary"' . $attributes . '>' . esc_html($label) . '</button></footer></form></dialog>';
}
}
+47 -47
View File
@@ -3,13 +3,13 @@
* 消息管理后台页:页面编排、带权限 / nonce 的写操作、筛选与反馈。
* 展示由同目录的 MessagesList / MessagesCompose traits 承载;查询与投递下沉消息服务。
*
* @package AKSRT
* @package Jio
*/
namespace AKSRT\Admin\Native;
namespace Jio\Admin\Native;
use AKSRT\Messages\AdminBroadcast;
use AKSRT\Messages\NotificationAdminQuery;
use Jio\Messages\AdminBroadcast;
use Jio\Messages\NotificationAdminQuery;
defined('ABSPATH') || exit;
@@ -18,8 +18,8 @@ trait UsersMenusMessages {
use UsersMenusMessagesList;
use UsersMenusMessagesCompose;
private const MESSAGE_PAGE_SLUG = 'aksrt_messages';
private const MESSAGE_COMPOSE_ID = 'aksrt-msg-compose';
private const MESSAGE_PAGE_SLUG = 'jio_messages';
private const MESSAGE_COMPOSE_ID = 'jio-msg-compose';
public function enqueue_message_assets(string $hook): void {
if ($hook !== 'users_page_' . self::MESSAGE_PAGE_SLUG) {
@@ -27,11 +27,11 @@ trait UsersMenusMessages {
}
$css = '/assets/css/admin/messages.css';
$js = '/assets/js/admin-messages.js';
if (is_file(AKSRT_DIR . $css)) {
wp_enqueue_style('aksrt-admin-messages', AKSRT_URI . $css, [NativeUi::STYLE_HANDLE], (string) filemtime(AKSRT_DIR . $css));
if (is_file(JIO_DIR . $css)) {
wp_enqueue_style('jio-admin-messages', JIO_URI . $css, [NativeUi::STYLE_HANDLE], (string) filemtime(JIO_DIR . $css));
}
if (is_file(AKSRT_DIR . $js)) {
wp_enqueue_script('aksrt-admin-messages', AKSRT_URI . $js, [], (string) filemtime(AKSRT_DIR . $js), true);
if (is_file(JIO_DIR . $js)) {
wp_enqueue_script('jio-admin-messages', JIO_URI . $js, [], (string) filemtime(JIO_DIR . $js), true);
}
}
@@ -40,21 +40,21 @@ trait UsersMenusMessages {
$filters = $this->message_filters();
$paged = max(1, absint($_GET['paged'] ?? 1));
NativeUi::page_open('aksrt-messages-admin');
NativeUi::page_open('jio-messages-admin');
NativeUi::header(
__('消息管理', 'aksrt'),
__('消息管理', 'jio'),
'',
'dashicons-email-alt',
[
[
'label' => __('发送系统消息', 'aksrt'),
'label' => __('发送系统消息', 'jio'),
'dialog' => self::MESSAGE_COMPOSE_ID,
'icon' => 'dashicons-megaphone',
'primary' => true,
],
[
'label' => __('通知配置', 'aksrt'),
'url' => admin_url('admin.php?page=aksrt-settings#/users/notifications'),
'label' => __('通知配置', 'jio'),
'url' => admin_url('admin.php?page=jio-settings#/users/notifications'),
'icon' => 'dashicons-admin-generic',
],
]
@@ -62,7 +62,7 @@ trait UsersMenusMessages {
$this->render_message_notices();
if (NotificationAdminQuery::table() === '') {
echo '<div class="notice notice-warning"><p>' . esc_html__('通知表不可用。', 'aksrt') . '</p></div>';
echo '<div class="notice notice-warning"><p>' . esc_html__('通知表不可用。', 'jio') . '</p></div>';
NativeUi::page_close();
return;
}
@@ -71,8 +71,8 @@ trait UsersMenusMessages {
$result = NotificationAdminQuery::paginate($query, $paged);
$this->render_message_overview();
NativeUi::card_open(__('站内通知', 'aksrt'), '', true, 'aksrt-messages-admin__card');
echo '<div class="aksrt-messages-admin__toolbar">';
NativeUi::card_open(__('站内通知', 'jio'), '', true, 'jio-messages-admin__card');
echo '<div class="jio-messages-admin__toolbar">';
echo $this->message_views_html($filters, NotificationAdminQuery::status_counts($query)); // phpcs:ignore WordPress.Security.EscapeOutput -- builder escapes.
$this->render_message_filter_form($filters);
echo '</div>';
@@ -94,7 +94,7 @@ trait UsersMenusMessages {
public function handle_send_message(): void {
$this->ensure_cap();
check_admin_referer('aksrt_users_send_message');
check_admin_referer('jio_users_send_message');
$mode = sanitize_key((string) wp_unslash($_POST['recipient_mode'] ?? 'users'));
$title = trim(sanitize_text_field((string) wp_unslash($_POST['title'] ?? '')));
@@ -102,16 +102,16 @@ trait UsersMenusMessages {
$target_url = esc_url_raw(trim((string) wp_unslash($_POST['target_url'] ?? '')));
if (!in_array($mode, ['users', 'role'], true)) {
$this->message_fail(__('请选择有效的收件人模式。', 'aksrt'));
$this->message_fail(__('请选择有效的收件人模式。', 'jio'));
}
if ($content === '') {
$this->message_fail(__('请填写消息内容。', 'aksrt'));
$this->message_fail(__('请填写消息内容。', 'jio'));
}
if (mb_strlen($content) > AdminBroadcast::CONTENT_MAX) {
$this->message_fail(sprintf(__('消息内容不能超过 %d 字。', 'aksrt'), AdminBroadcast::CONTENT_MAX));
$this->message_fail(sprintf(__('消息内容不能超过 %d 字。', 'jio'), AdminBroadcast::CONTENT_MAX));
}
if (mb_strlen($title) > AdminBroadcast::TITLE_MAX) {
$this->message_fail(sprintf(__('消息标题不能超过 %d 字。', 'aksrt'), AdminBroadcast::TITLE_MAX));
$this->message_fail(sprintf(__('消息标题不能超过 %d 字。', 'jio'), AdminBroadcast::TITLE_MAX));
}
$title = mb_substr($title !== '' ? $title : $content, 0, AdminBroadcast::TITLE_MAX);
@@ -128,28 +128,28 @@ trait UsersMenusMessages {
$this->message_fail($ids->get_error_message());
}
if ($ids === []) {
$this->message_fail(__('没有匹配到任何有效收件人。', 'aksrt'));
$this->message_fail(__('没有匹配到任何有效收件人。', 'jio'));
}
// 确认绑定角色与人数;页面加载后人数变化时拒绝投递,避免按过期预览群发。
if ($mode === 'role' && (
sanitize_key((string) wp_unslash($_POST['confirmed_role'] ?? '')) !== $role
|| absint($_POST['confirmed_count'] ?? 0) !== count($ids)
)) {
$this->message_fail(__('收件人范围尚未确认或人数已变化,请重新确认后发送。', 'aksrt'));
$this->message_fail(__('收件人范围尚未确认或人数已变化,请重新确认后发送。', 'jio'));
}
$sent = AdminBroadcast::send($ids, $title, $content, $target_url);
$this->message_redirect([
'aksrt_msg' => $sent['sent'] > 0 ? 'sent' : 'send_failed',
'aksrt_msg_n' => $sent['sent'],
'aksrt_msg_f' => $sent['failed'],
'aksrt_msg_u' => $unknown,
'jio_msg' => $sent['sent'] > 0 ? 'sent' : 'send_failed',
'jio_msg_n' => $sent['sent'],
'jio_msg_f' => $sent['failed'],
'jio_msg_u' => $unknown,
]);
}
public function handle_bulk_messages(): void {
$this->ensure_cap();
check_admin_referer('aksrt_users_bulk_messages');
check_admin_referer('jio_users_bulk_messages');
$op = sanitize_key((string) wp_unslash($_POST['bulk_action'] ?? ''));
$single_id = absint($_POST['message_id'] ?? 0);
@@ -161,46 +161,46 @@ trait UsersMenusMessages {
}
if (!in_array($op, ['delete', 'mark_read'], true) || array_filter($ids) === []) {
$this->message_redirect(array_merge($back, ['aksrt_msg' => 'bulk_none']));
$this->message_redirect(array_merge($back, ['jio_msg' => 'bulk_none']));
}
$count = $op === 'delete' ? NotificationAdminQuery::delete($ids) : NotificationAdminQuery::mark_read($ids);
$this->message_redirect(array_merge($back, [
'aksrt_msg' => $op === 'delete' ? 'deleted' : 'marked',
'aksrt_msg_n' => $count,
'jio_msg' => $op === 'delete' ? 'deleted' : 'marked',
'jio_msg_n' => $count,
]));
}
private function render_message_notices(): void {
$code = sanitize_key((string) wp_unslash($_GET['aksrt_msg'] ?? ''));
$n = absint($_GET['aksrt_msg_n'] ?? 0);
$code = sanitize_key((string) wp_unslash($_GET['jio_msg'] ?? ''));
$n = absint($_GET['jio_msg_n'] ?? 0);
$error = false;
switch ($code) {
case 'sent':
$text = sprintf(__('已发送 %d 条消息。', 'aksrt'), $n);
$failed = absint($_GET['aksrt_msg_f'] ?? 0);
$unknown = absint($_GET['aksrt_msg_u'] ?? 0);
$text = sprintf(__('已发送 %d 条消息。', 'jio'), $n);
$failed = absint($_GET['jio_msg_f'] ?? 0);
$unknown = absint($_GET['jio_msg_u'] ?? 0);
if ($failed > 0) {
$text .= ' ' . sprintf(__('投递失败 %d 条。', 'aksrt'), $failed);
$text .= ' ' . sprintf(__('投递失败 %d 条。', 'jio'), $failed);
}
if ($unknown > 0) {
$text .= ' ' . sprintf(__('%d 个收件人未找到,已跳过。', 'aksrt'), $unknown);
$text .= ' ' . sprintf(__('%d 个收件人未找到,已跳过。', 'jio'), $unknown);
}
break;
case 'send_failed':
$error = true;
$text = isset($_GET['aksrt_msg_error'])
? sanitize_text_field((string) wp_unslash($_GET['aksrt_msg_error']))
: __('消息发送失败。', 'aksrt');
$text = isset($_GET['jio_msg_error'])
? sanitize_text_field((string) wp_unslash($_GET['jio_msg_error']))
: __('消息发送失败。', 'jio');
break;
case 'deleted':
$text = sprintf(__('已删除 %d 条消息。', 'aksrt'), $n);
$text = sprintf(__('已删除 %d 条消息。', 'jio'), $n);
break;
case 'marked':
$text = sprintf(__('已将 %d 条消息标记为已读。', 'aksrt'), $n);
$text = sprintf(__('已将 %d 条消息标记为已读。', 'jio'), $n);
break;
case 'bulk_none':
$error = true;
$text = __('请先勾选消息并选择操作。', 'aksrt');
$text = __('请先勾选消息并选择操作。', 'jio');
break;
default:
return;
@@ -258,7 +258,7 @@ trait UsersMenusMessages {
}
private function message_fail(string $error): void {
$this->message_redirect(['aksrt_msg' => 'send_failed', 'aksrt_msg_error' => $error]);
$this->message_redirect(['jio_msg' => 'send_failed', 'jio_msg_error' => $error]);
}
private function message_redirect(array $args): void {
+54 -54
View File
@@ -2,108 +2,108 @@
/**
* 消息管理弹窗:发送表单、只读详情、删除与角色群发共用的二次确认。
*
* @package AKSRT
* @package Jio
*/
namespace AKSRT\Admin\Native;
namespace Jio\Admin\Native;
use AKSRT\Messages\AdminBroadcast;
use Jio\Messages\AdminBroadcast;
defined('ABSPATH') || exit;
trait UsersMenusMessagesCompose {
private function render_message_compose(): void {
$open = sanitize_key((string) wp_unslash($_GET['aksrt_msg'] ?? '')) === 'send_failed';
$open = sanitize_key((string) wp_unslash($_GET['jio_msg'] ?? '')) === 'send_failed';
printf(
'<dialog class="aksrt-msg-dialog" id="%s" aria-labelledby="%s-title"%s>',
esc_attr(self::MESSAGE_COMPOSE_ID), esc_attr(self::MESSAGE_COMPOSE_ID), $open ? ' data-aksrt-dialog-autoopen' : ''
'<dialog class="jio-msg-dialog" id="%s" aria-labelledby="%s-title"%s>',
esc_attr(self::MESSAGE_COMPOSE_ID), esc_attr(self::MESSAGE_COMPOSE_ID), $open ? ' data-jio-dialog-autoopen' : ''
);
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" class="aksrt-msg-dialog__frame aksrt-messages-admin__compose" data-aksrt-msg-compose';
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" class="jio-msg-dialog__frame jio-messages-admin__compose" data-jio-msg-compose';
$strings = [
'count-error' => __('最多输入 {count} 字。', 'aksrt'),
'role-hint' => __('将发送给 {count} 位用户;提交前需再次确认。', 'aksrt'),
'role-title' => __('确认按角色群发', 'aksrt'),
'role-confirm' => __('将向「{role}」的 {count} 位用户发送系统消息,并按通知配置触发提醒。请确认收件人范围与消息内容。', 'aksrt'),
'role-send' => __('确认发送', 'aksrt'),
'count-error' => __('最多输入 {count} 字。', 'jio'),
'role-hint' => __('将发送给 {count} 位用户;提交前需再次确认。', 'jio'),
'role-title' => __('确认按角色群发', 'jio'),
'role-confirm' => __('将向「{role}」的 {count} 位用户发送系统消息,并按通知配置触发提醒。请确认收件人范围与消息内容。', 'jio'),
'role-send' => __('确认发送', 'jio'),
];
foreach ($strings as $key => $text) {
echo ' data-i18n-' . esc_attr($key) . '="' . esc_attr($text) . '"';
}
echo '>';
echo '<input type="hidden" name="action" value="aksrt_users_send_message" />';
echo '<input type="hidden" name="action" value="jio_users_send_message" />';
echo '<input type="hidden" name="confirmed_role" value="" /><input type="hidden" name="confirmed_count" value="" />';
wp_nonce_field('aksrt_users_send_message');
$this->render_message_dialog_head(self::MESSAGE_COMPOSE_ID, __('发送系统消息', 'aksrt'), 'dashicons-megaphone');
echo '<div class="aksrt-msg-dialog__body">';
wp_nonce_field('jio_users_send_message');
$this->render_message_dialog_head(self::MESSAGE_COMPOSE_ID, __('发送系统消息', 'jio'), 'dashicons-megaphone');
echo '<div class="jio-msg-dialog__body">';
if ($open) {
$error = sanitize_text_field((string) wp_unslash($_GET['aksrt_msg_error'] ?? __('消息发送失败,请检查后重试。', 'aksrt')));
echo '<p class="aksrt-native-callout is-danger">' . esc_html($error) . '</p>';
$error = sanitize_text_field((string) wp_unslash($_GET['jio_msg_error'] ?? __('消息发送失败,请检查后重试。', 'jio')));
echo '<p class="jio-native-callout is-danger">' . esc_html($error) . '</p>';
}
echo '<fieldset class="aksrt-msg-modes"><legend class="aksrt-native-field__label">' . esc_html__('收件人', 'aksrt') . '</legend>';
foreach (['users' => [__('指定用户', 'aksrt'), 'dashicons-admin-users'], 'role' => [__('按角色群发', 'aksrt'), 'dashicons-groups']] as $value => [$label, $icon]) {
echo '<label class="aksrt-msg-mode"><input type="radio" name="recipient_mode" value="' . esc_attr($value) . '"' . checked($value, 'users', false) . ' />';
echo '<fieldset class="jio-msg-modes"><legend class="jio-native-field__label">' . esc_html__('收件人', 'jio') . '</legend>';
foreach (['users' => [__('指定用户', 'jio'), 'dashicons-admin-users'], 'role' => [__('按角色群发', 'jio'), 'dashicons-groups']] as $value => [$label, $icon]) {
echo '<label class="jio-msg-mode"><input type="radio" name="recipient_mode" value="' . esc_attr($value) . '"' . checked($value, 'users', false) . ' />';
echo '<span><span class="dashicons ' . esc_attr($icon) . '" aria-hidden="true"></span>' . esc_html($label) . '</span></label>';
}
echo '</fieldset><div class="aksrt-native-form-grid">';
echo '</fieldset><div class="jio-native-form-grid">';
printf(
'<label class="aksrt-native-field is-wide" data-aksrt-msg-pane="users"><span class="aksrt-native-field__label">%s</span><textarea name="users" rows="2" required placeholder="%s"></textarea><small class="aksrt-native-field__hint">%s</small></label>',
esc_html__('用户', 'aksrt'), esc_attr__('用户 ID / 用户名 / 邮箱,逗号或换行分隔', 'aksrt'), esc_html(sprintf(__('单次最多 %d 位。', 'aksrt'), AdminBroadcast::MAX_USERS))
'<label class="jio-native-field is-wide" data-jio-msg-pane="users"><span class="jio-native-field__label">%s</span><textarea name="users" rows="2" required placeholder="%s"></textarea><small class="jio-native-field__hint">%s</small></label>',
esc_html__('用户', 'jio'), esc_attr__('用户 ID / 用户名 / 邮箱,逗号或换行分隔', 'jio'), esc_html(sprintf(__('单次最多 %d 位。', 'jio'), AdminBroadcast::MAX_USERS))
);
echo '<label class="aksrt-native-field is-wide" data-aksrt-msg-pane="role" hidden><span class="aksrt-native-field__label">' . esc_html__('角色', 'aksrt') . '</span>';
echo '<select name="role" required disabled><option value="" selected disabled>' . esc_html__('请选择角色', 'aksrt') . '</option>';
echo '<label class="jio-native-field is-wide" data-jio-msg-pane="role" hidden><span class="jio-native-field__label">' . esc_html__('角色', 'jio') . '</span>';
echo '<select name="role" required disabled><option value="" selected disabled>' . esc_html__('请选择角色', 'jio') . '</option>';
$counts = count_users('time');
foreach (wp_roles()->get_names() as $role => $name) {
$count = (int) ($counts['avail_roles'][$role] ?? 0);
$label = translate_user_role($name);
$disabled = $count === 0 || $count > AdminBroadcast::MAX_ROLE_USERS;
$suffix = $count > AdminBroadcast::MAX_ROLE_USERS ? __('(超过发送上限)', 'aksrt') : ($count === 0 ? __('(暂无用户)', 'aksrt') : '');
$suffix = $count > AdminBroadcast::MAX_ROLE_USERS ? __('(超过发送上限)', 'jio') : ($count === 0 ? __('(暂无用户)', 'jio') : '');
printf(
'<option value="%s" data-count="%d" data-label="%s"%s>%s</option>',
esc_attr($role), $count, esc_attr($label), $disabled ? ' disabled' : '',
esc_html(sprintf(__('%1$s · %2$s 人', 'aksrt'), $label, number_format_i18n($count)) . $suffix)
esc_html(sprintf(__('%1$s · %2$s 人', 'jio'), $label, number_format_i18n($count)) . $suffix)
);
}
echo '</select><small class="aksrt-native-field__hint">' . esc_html(sprintf(__('单个角色最多 %d 人;空角色和超限角色不可选。', 'aksrt'), AdminBroadcast::MAX_ROLE_USERS)) . '</small>';
echo '<small class="aksrt-native-field__hint" data-aksrt-msg-role-hint aria-live="polite"></small></label>';
echo '</select><small class="jio-native-field__hint">' . esc_html(sprintf(__('单个角色最多 %d 人;空角色和超限角色不可选。', 'jio'), AdminBroadcast::MAX_ROLE_USERS)) . '</small>';
echo '<small class="jio-native-field__hint" data-jio-msg-role-hint aria-live="polite"></small></label>';
printf(
'<label class="aksrt-native-field"><span class="aksrt-native-field__label">%s</span><input type="text" name="title" data-count-limit="%d" aria-describedby="aksrt-msg-title-count" placeholder="%s" /><small class="aksrt-msg-counter" id="aksrt-msg-title-count" data-count-for="title"></small></label>',
esc_html__('标题', 'aksrt'), AdminBroadcast::TITLE_MAX, esc_attr__('留空则取内容开头', 'aksrt')
'<label class="jio-native-field"><span class="jio-native-field__label">%s</span><input type="text" name="title" data-count-limit="%d" aria-describedby="jio-msg-title-count" placeholder="%s" /><small class="jio-msg-counter" id="jio-msg-title-count" data-count-for="title"></small></label>',
esc_html__('标题', 'jio'), AdminBroadcast::TITLE_MAX, esc_attr__('留空则取内容开头', 'jio')
);
echo '<label class="aksrt-native-field"><span class="aksrt-native-field__label">' . esc_html__('跳转链接(可选)', 'aksrt') . '</span><input type="url" name="target_url" placeholder="https://" /></label>';
echo '<label class="jio-native-field"><span class="jio-native-field__label">' . esc_html__('跳转链接(可选)', 'jio') . '</span><input type="url" name="target_url" placeholder="https://" /></label>';
printf(
'<label class="aksrt-native-field is-wide"><span class="aksrt-native-field__label">%s</span><textarea name="content" rows="5" data-count-limit="%d" required aria-describedby="aksrt-msg-content-count"></textarea><small class="aksrt-msg-counter" id="aksrt-msg-content-count" data-count-for="content"></small></label>',
esc_html__('内容', 'aksrt'), AdminBroadcast::CONTENT_MAX
'<label class="jio-native-field is-wide"><span class="jio-native-field__label">%s</span><textarea name="content" rows="5" data-count-limit="%d" required aria-describedby="jio-msg-content-count"></textarea><small class="jio-msg-counter" id="jio-msg-content-count" data-count-for="content"></small></label>',
esc_html__('内容', 'jio'), AdminBroadcast::CONTENT_MAX
);
echo '</div><p class="aksrt-native-muted aksrt-msg-dialog__note">' . esc_html__('以「系统通知」类型逐人投递;邮件 / 公众号提醒按通知配置同步触发。', 'aksrt') . '</p></div>';
echo '<footer class="aksrt-msg-dialog__foot"><button type="button" class="button" data-aksrt-dialog-close>' . esc_html__('取消', 'aksrt') . '</button>';
echo '<button type="submit" class="button button-primary" data-aksrt-msg-send>' . esc_html__('发送', 'aksrt') . '</button></footer></form></dialog>';
echo '</div><p class="jio-native-muted jio-msg-dialog__note">' . esc_html__('以「系统通知」类型逐人投递;邮件 / 公众号提醒按通知配置同步触发。', 'jio') . '</p></div>';
echo '<footer class="jio-msg-dialog__foot"><button type="button" class="button" data-jio-dialog-close>' . esc_html__('取消', 'jio') . '</button>';
echo '<button type="submit" class="button button-primary" data-jio-msg-send>' . esc_html__('发送', 'jio') . '</button></footer></form></dialog>';
}
private function render_message_dialogs(): void {
echo '<dialog class="aksrt-msg-dialog" id="aksrt-msg-detail" aria-labelledby="aksrt-msg-detail-title"><div class="aksrt-msg-dialog__frame">';
$this->render_message_dialog_head('aksrt-msg-detail', __('消息详情', 'aksrt'), 'dashicons-email-alt');
echo '<div class="aksrt-msg-dialog__body" data-aksrt-msg-detail-body></div>';
echo '<footer class="aksrt-msg-dialog__foot"><button type="button" class="button" data-aksrt-dialog-close>' . esc_html__('关闭', 'aksrt') . '</button></footer></div></dialog>';
echo '<dialog class="jio-msg-dialog" id="jio-msg-detail" aria-labelledby="jio-msg-detail-title"><div class="jio-msg-dialog__frame">';
$this->render_message_dialog_head('jio-msg-detail', __('消息详情', 'jio'), 'dashicons-email-alt');
echo '<div class="jio-msg-dialog__body" data-jio-msg-detail-body></div>';
echo '<footer class="jio-msg-dialog__foot"><button type="button" class="button" data-jio-dialog-close>' . esc_html__('关闭', 'jio') . '</button></footer></div></dialog>';
echo '<dialog class="aksrt-msg-dialog aksrt-msg-dialog--confirm" id="aksrt-msg-confirm" aria-labelledby="aksrt-msg-confirm-title" aria-describedby="aksrt-msg-confirm-text"';
echo '<dialog class="jio-msg-dialog jio-msg-dialog--confirm" id="jio-msg-confirm" aria-labelledby="jio-msg-confirm-title" aria-describedby="jio-msg-confirm-text"';
foreach ([
'delete-title' => __('删除消息', 'aksrt'),
'delete-text' => __('确定删除 {count} 条消息?删除后用户收件箱中也将不再显示,此操作不可撤销。', 'aksrt'),
'delete-send' => __('确认删除', 'aksrt'),
'delete-title' => __('删除消息', 'jio'),
'delete-text' => __('确定删除 {count} 条消息?删除后用户收件箱中也将不再显示,此操作不可撤销。', 'jio'),
'delete-send' => __('确认删除', 'jio'),
] as $key => $text) {
echo ' data-i18n-' . esc_attr($key) . '="' . esc_attr($text) . '"';
}
echo '><div class="aksrt-msg-dialog__frame">';
$this->render_message_dialog_head('aksrt-msg-confirm', __('确认操作', 'aksrt'), 'dashicons-warning');
echo '<div class="aksrt-msg-dialog__body"><p id="aksrt-msg-confirm-text"></p></div>';
echo '<footer class="aksrt-msg-dialog__foot"><button type="button" class="button" data-aksrt-dialog-close autofocus>' . esc_html__('取消', 'aksrt') . '</button>';
echo '<button type="button" class="button button-primary" data-aksrt-msg-confirm>' . esc_html__('确认', 'aksrt') . '</button></footer></div></dialog>';
echo '><div class="jio-msg-dialog__frame">';
$this->render_message_dialog_head('jio-msg-confirm', __('确认操作', 'jio'), 'dashicons-warning');
echo '<div class="jio-msg-dialog__body"><p id="jio-msg-confirm-text"></p></div>';
echo '<footer class="jio-msg-dialog__foot"><button type="button" class="button" data-jio-dialog-close autofocus>' . esc_html__('取消', 'jio') . '</button>';
echo '<button type="button" class="button button-primary" data-jio-msg-confirm>' . esc_html__('确认', 'jio') . '</button></footer></div></dialog>';
}
private function render_message_dialog_head(string $id, string $title, string $icon): void {
echo '<header class="aksrt-msg-dialog__head"><h2 id="' . esc_attr($id) . '-title" class="aksrt-msg-dialog__title">';
echo '<span class="dashicons ' . esc_attr($icon) . '" aria-hidden="true"></span><span data-aksrt-dialog-title>' . esc_html($title) . '</span></h2>';
echo '<button type="button" class="aksrt-msg-dialog__close" data-aksrt-dialog-close aria-label="' . esc_attr__('关闭', 'aksrt') . '"><span class="dashicons dashicons-no-alt" aria-hidden="true"></span></button></header>';
echo '<header class="jio-msg-dialog__head"><h2 id="' . esc_attr($id) . '-title" class="jio-msg-dialog__title">';
echo '<span class="dashicons ' . esc_attr($icon) . '" aria-hidden="true"></span><span data-jio-dialog-title>' . esc_html($title) . '</span></h2>';
echo '<button type="button" class="jio-msg-dialog__close" data-jio-dialog-close aria-label="' . esc_attr__('关闭', 'jio') . '"><span class="dashicons dashicons-no-alt" aria-hidden="true"></span></button></header>';
}
}
+67 -67
View File
@@ -2,13 +2,13 @@
/**
* 消息管理展示:概览、筛选、列表与按行预渲染的只读详情。
*
* @package AKSRT
* @package Jio
*/
namespace AKSRT\Admin\Native;
namespace Jio\Admin\Native;
use AKSRT\Messages\MessageTypes;
use AKSRT\Messages\NotificationAdminQuery;
use Jio\Messages\MessageTypes;
use Jio\Messages\NotificationAdminQuery;
defined('ABSPATH') || exit;
@@ -17,22 +17,22 @@ trait UsersMenusMessagesList {
private function render_message_overview(): void {
$stats = NotificationAdminQuery::overview();
NativeUi::stats([
['label' => __('消息总数', 'aksrt'), 'value' => number_format_i18n($stats['total']), 'icon' => 'dashicons-email-alt', 'tone' => 'info', 'url' => $this->message_page_url()],
['label' => __('未读', 'aksrt'), 'value' => number_format_i18n($stats['unread']), 'icon' => 'dashicons-marker', 'tone' => 'danger', 'url' => $this->message_page_url(['status' => 'unread'])],
['label' => __('今日新增', 'aksrt'), 'value' => number_format_i18n($stats['today']), 'icon' => 'dashicons-calendar-alt', 'tone' => 'success', 'url' => $this->message_page_url(['period' => 'today'])],
['label' => __('涉及用户', 'aksrt'), 'value' => number_format_i18n($stats['recipients']), 'icon' => 'dashicons-groups', 'tone' => 'primary', 'url' => admin_url('users.php'), 'hint' => __('前往用户管理', 'aksrt')],
['label' => __('消息总数', 'jio'), 'value' => number_format_i18n($stats['total']), 'icon' => 'dashicons-email-alt', 'tone' => 'info', 'url' => $this->message_page_url()],
['label' => __('未读', 'jio'), 'value' => number_format_i18n($stats['unread']), 'icon' => 'dashicons-marker', 'tone' => 'danger', 'url' => $this->message_page_url(['status' => 'unread'])],
['label' => __('今日新增', 'jio'), 'value' => number_format_i18n($stats['today']), 'icon' => 'dashicons-calendar-alt', 'tone' => 'success', 'url' => $this->message_page_url(['period' => 'today'])],
['label' => __('涉及用户', 'jio'), 'value' => number_format_i18n($stats['recipients']), 'icon' => 'dashicons-groups', 'tone' => 'primary', 'url' => admin_url('users.php'), 'hint' => __('前往用户管理', 'jio')],
]);
}
private function message_views_html(array $filters, array $counts): string {
$views = [
'' => [__('全部', 'aksrt'), $counts['all']],
'unread' => [__('未读', 'aksrt'), $counts['unread']],
'read' => [__('已读', 'aksrt'), $counts['read']],
'' => [__('全部', 'jio'), $counts['all']],
'unread' => [__('未读', 'jio'), $counts['unread']],
'read' => [__('已读', 'jio'), $counts['read']],
];
$args = $this->message_filter_args($filters);
unset($args['status']);
$html = '<ul class="subsubsub" aria-label="' . esc_attr__('消息状态', 'aksrt') . '">';
$html = '<ul class="subsubsub" aria-label="' . esc_attr__('消息状态', 'jio') . '">';
foreach ($views as $key => [$label, $count]) {
$html .= sprintf(
'<li><a href="%s"%s>%s <span class="count">(%s)</span></a></li>',
@@ -46,33 +46,33 @@ trait UsersMenusMessagesList {
}
private function render_message_filter_form(array $filters): void {
echo '<form method="get" action="' . esc_url(admin_url('users.php')) . '" class="aksrt-native-filters">';
echo '<form method="get" action="' . esc_url(admin_url('users.php')) . '" class="jio-native-filters">';
echo '<input type="hidden" name="page" value="' . esc_attr(self::MESSAGE_PAGE_SLUG) . '" />';
if ($filters['status'] !== '') {
echo '<input type="hidden" name="status" value="' . esc_attr($filters['status']) . '" />';
}
echo '<label class="screen-reader-text" for="aksrt-msg-filter-type">' . esc_html__('按类型筛选', 'aksrt') . '</label>';
echo '<select name="type" id="aksrt-msg-filter-type"><option value="">' . esc_html__('全部类型', 'aksrt') . '</option>';
echo '<label class="screen-reader-text" for="jio-msg-filter-type">' . esc_html__('按类型筛选', 'jio') . '</label>';
echo '<select name="type" id="jio-msg-filter-type"><option value="">' . esc_html__('全部类型', 'jio') . '</option>';
foreach (NotificationAdminQuery::types() as $type) {
printf('<option value="%s"%s>%s</option>', esc_attr($type), selected($filters['type'], $type, false), esc_html(MessageTypes::label($type)));
}
echo '</select>';
echo '<label class="screen-reader-text" for="aksrt-msg-filter-period">' . esc_html__('按发送时间筛选', 'aksrt') . '</label>';
echo '<select name="period" id="aksrt-msg-filter-period"><option value="">' . esc_html__('全部时间', 'aksrt') . '</option>';
echo '<option value="today"' . selected($filters['period'], 'today', false) . '>' . esc_html__('今日新增', 'aksrt') . '</option></select>';
echo '<label class="screen-reader-text" for="jio-msg-filter-period">' . esc_html__('按发送时间筛选', 'jio') . '</label>';
echo '<select name="period" id="jio-msg-filter-period"><option value="">' . esc_html__('全部时间', 'jio') . '</option>';
echo '<option value="today"' . selected($filters['period'], 'today', false) . '>' . esc_html__('今日新增', 'jio') . '</option></select>';
printf(
'<label class="screen-reader-text" for="aksrt-msg-filter-user">%s</label><input type="text" id="aksrt-msg-filter-user" name="user" value="%s" placeholder="%s" />',
esc_html__('按收件人筛选', 'aksrt'), esc_attr($filters['user']), esc_attr__('收件人 ID / 用户名 / 邮箱', 'aksrt')
'<label class="screen-reader-text" for="jio-msg-filter-user">%s</label><input type="text" id="jio-msg-filter-user" name="user" value="%s" placeholder="%s" />',
esc_html__('按收件人筛选', 'jio'), esc_attr($filters['user']), esc_attr__('收件人 ID / 用户名 / 邮箱', 'jio')
);
printf(
'<label class="screen-reader-text" for="aksrt-msg-filter-s">%s</label><input type="search" id="aksrt-msg-filter-s" name="s" value="%s" placeholder="%s" />',
esc_html__('搜索消息', 'aksrt'), esc_attr($filters['s']), esc_attr__('标题 / 内容', 'aksrt')
'<label class="screen-reader-text" for="jio-msg-filter-s">%s</label><input type="search" id="jio-msg-filter-s" name="s" value="%s" placeholder="%s" />',
esc_html__('搜索消息', 'jio'), esc_attr($filters['s']), esc_attr__('标题 / 内容', 'jio')
);
submit_button(__('筛选', 'aksrt'), '', '', false);
submit_button(__('筛选', 'jio'), '', '', false);
$other = $filters;
unset($other['status']);
if ($this->message_filter_args($other) !== []) {
echo '<a class="button-link" href="' . esc_url($this->message_page_url($filters['status'] === '' ? [] : ['status' => $filters['status']])) . '">' . esc_html__('清除筛选', 'aksrt') . '</a>';
echo '<a class="button-link" href="' . esc_url($this->message_page_url($filters['status'] === '' ? [] : ['status' => $filters['status']])) . '">' . esc_html__('清除筛选', 'jio') . '</a>';
}
echo '</form>';
}
@@ -82,29 +82,29 @@ trait UsersMenusMessagesList {
if ($user_ids !== []) {
cache_users($user_ids);
}
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" class="aksrt-messages-admin__list" data-aksrt-msg-list>';
echo '<input type="hidden" name="action" value="aksrt_users_bulk_messages" />';
wp_nonce_field('aksrt_users_bulk_messages');
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" class="jio-messages-admin__list" data-jio-msg-list>';
echo '<input type="hidden" name="action" value="jio_users_bulk_messages" />';
wp_nonce_field('jio_users_bulk_messages');
foreach ($this->message_filter_args($filters) as $key => $value) {
printf('<input type="hidden" name="%s" value="%s" />', esc_attr($key), esc_attr((string) $value));
}
echo '<input type="hidden" name="paged" value="' . esc_attr((string) $page) . '" />';
echo '<input type="hidden" name="bulk_action" value="" /><input type="hidden" name="message_id" value="" />';
echo '<div class="aksrt-messages-admin__bulk">';
echo '<span class="aksrt-messages-admin__selection" data-aksrt-msg-selection role="status" aria-live="polite" data-label="' . esc_attr__('已选 %d 条', 'aksrt') . '"></span>';
echo '<button type="button" class="button" data-aksrt-msg-action="mark_read" disabled>' . esc_html__('标记已读', 'aksrt') . '</button>';
echo '<button type="button" class="button aksrt-messages-admin__danger" data-aksrt-msg-action="delete" disabled>' . esc_html__('删除', 'aksrt') . '</button>';
echo '</div><div class="aksrt-native-table-wrap">';
echo '<table class="wp-list-table widefat fixed striped aksrt-messages-admin__table"><thead><tr>';
echo '<td class="manage-column column-cb check-column"><label class="screen-reader-text" for="aksrt-msg-check-all">' . esc_html__('全选本页消息', 'aksrt') . '</label><input id="aksrt-msg-check-all" type="checkbox" data-aksrt-msg-check-all /></td>';
foreach (['title' => __('消息', 'aksrt'), 'type' => __('类型', 'aksrt'), 'user' => __('收件人', 'aksrt'), 'read' => __('状态', 'aksrt'), 'time' => __('发送时间', 'aksrt'), 'actions' => __('操作', 'aksrt')] as $column => $label) {
echo '<div class="jio-messages-admin__bulk">';
echo '<span class="jio-messages-admin__selection" data-jio-msg-selection role="status" aria-live="polite" data-label="' . esc_attr__('已选 %d 条', 'jio') . '"></span>';
echo '<button type="button" class="button" data-jio-msg-action="mark_read" disabled>' . esc_html__('标记已读', 'jio') . '</button>';
echo '<button type="button" class="button jio-messages-admin__danger" data-jio-msg-action="delete" disabled>' . esc_html__('删除', 'jio') . '</button>';
echo '</div><div class="jio-native-table-wrap">';
echo '<table class="wp-list-table widefat fixed striped jio-messages-admin__table"><thead><tr>';
echo '<td class="manage-column column-cb check-column"><label class="screen-reader-text" for="jio-msg-check-all">' . esc_html__('全选本页消息', 'jio') . '</label><input id="jio-msg-check-all" type="checkbox" data-jio-msg-check-all /></td>';
foreach (['title' => __('消息', 'jio'), 'type' => __('类型', 'jio'), 'user' => __('收件人', 'jio'), 'read' => __('状态', 'jio'), 'time' => __('发送时间', 'jio'), 'actions' => __('操作', 'jio')] as $column => $label) {
echo '<th scope="col" class="column-' . esc_attr($column) . '">' . esc_html($label) . '</th>';
}
echo '</tr></thead><tbody>';
if ($items === []) {
$hint = $user_missing ? __('没有找到该收件人。', 'aksrt')
: ($this->message_filter_args($filters) === [] ? __('站内还没有任何通知。', 'aksrt') : __('当前筛选条件下没有消息。', 'aksrt'));
echo NativeUi::empty_row(7, __('暂无消息', 'aksrt'), $hint); // phpcs:ignore WordPress.Security.EscapeOutput -- builder escapes.
$hint = $user_missing ? __('没有找到该收件人。', 'jio')
: ($this->message_filter_args($filters) === [] ? __('站内还没有任何通知。', 'jio') : __('当前筛选条件下没有消息。', 'jio'));
echo NativeUi::empty_row(7, __('暂无消息', 'jio'), $hint); // phpcs:ignore WordPress.Security.EscapeOutput -- builder escapes.
}
foreach ($items as $row) {
$this->render_message_row($row, $filters);
@@ -120,77 +120,77 @@ trait UsersMenusMessagesList {
$unread = empty($row['is_read']);
echo '<tr class="' . ($unread ? 'is-unread' : 'is-read') . '">';
printf(
'<th scope="row" class="check-column"><label class="screen-reader-text" for="aksrt-msg-%1$d">%2$s</label><input id="aksrt-msg-%1$d" type="checkbox" name="ids[]" value="%1$d" data-aksrt-msg-check /></th>',
$id, esc_html(sprintf(__('选择消息 #%d', 'aksrt'), $id))
'<th scope="row" class="check-column"><label class="screen-reader-text" for="jio-msg-%1$d">%2$s</label><input id="jio-msg-%1$d" type="checkbox" name="ids[]" value="%1$d" data-jio-msg-check /></th>',
$id, esc_html(sprintf(__('选择消息 #%d', 'jio'), $id))
);
echo '<td class="column-title"><strong class="row-title">' . esc_html($title) . '</strong>';
if ($desc !== '' && $desc !== $title) {
echo '<div class="aksrt-messages-admin__desc">' . esc_html(mb_strimwidth($desc, 0, 160, '…')) . '</div>';
echo '<div class="jio-messages-admin__desc">' . esc_html(mb_strimwidth($desc, 0, 160, '…')) . '</div>';
}
echo '<div class="aksrt-messages-admin__sub"><code>#' . esc_html((string) $id) . '</code></div>';
echo '<div class="jio-messages-admin__sub"><code>#' . esc_html((string) $id) . '</code></div>';
$this->render_message_detail_template($row);
echo '</td>';
printf(
'<td class="column-type"><a class="aksrt-msg-badge is-%s" href="%s">%s</a></td>',
'<td class="column-type"><a class="jio-msg-badge is-%s" href="%s">%s</a></td>',
esc_attr(MessageTypes::tone($type)), esc_url($this->message_page_url(array_merge($this->message_filter_args($filters), ['type' => $type]))), esc_html(MessageTypes::label($type))
);
echo '<td class="column-user">' . $this->message_user_cell((int) $row['user_id'], $filters) . '</td>'; // phpcs:ignore WordPress.Security.EscapeOutput -- builder escapes.
$read_at = (string) ($row['read_at'] ?? '');
printf(
'<td class="column-read"><span class="aksrt-msg-state %s"%s>%s</span></td>',
'<td class="column-read"><span class="jio-msg-state %s"%s>%s</span></td>',
$unread ? 'is-unread' : 'is-read',
!$unread && $read_at !== '' ? ' title="' . esc_attr(sprintf(__('阅读于 %s', 'aksrt'), $this->message_time($read_at))) . '"' : '',
$unread ? esc_html__('未读', 'aksrt') : esc_html__('已读', 'aksrt')
!$unread && $read_at !== '' ? ' title="' . esc_attr(sprintf(__('阅读于 %s', 'jio'), $this->message_time($read_at))) . '"' : '',
$unread ? esc_html__('未读', 'jio') : esc_html__('已读', 'jio')
);
$created = (string) $row['created_at'];
$ts = $created !== '' ? strtotime($created . ' UTC') : false;
printf(
'<td class="column-time"><time datetime="%s">%s</time>%s</td>',
esc_attr($ts ? gmdate('c', $ts) : ''), esc_html($this->message_time($created)),
$ts ? '<div class="aksrt-messages-admin__sub">' . esc_html(sprintf(__('%s前', 'aksrt'), human_time_diff($ts))) . '</div>' : ''
$ts ? '<div class="jio-messages-admin__sub">' . esc_html(sprintf(__('%s前', 'jio'), human_time_diff($ts))) . '</div>' : ''
);
echo '<td class="column-actions"><div class="aksrt-msg-row-actions">';
printf('<button type="button" class="button-link" data-aksrt-msg-view="%d" aria-haspopup="dialog" aria-controls="aksrt-msg-detail">%s</button>', $id, esc_html__('查看', 'aksrt'));
echo '<td class="column-actions"><div class="jio-msg-row-actions">';
printf('<button type="button" class="button-link" data-jio-msg-view="%d" aria-haspopup="dialog" aria-controls="jio-msg-detail">%s</button>', $id, esc_html__('查看', 'jio'));
if ($unread) {
printf('<button type="button" class="button-link" data-aksrt-msg-action="mark_read" data-id="%d">%s</button>', $id, esc_html__('标记已读', 'aksrt'));
printf('<button type="button" class="button-link" data-jio-msg-action="mark_read" data-id="%d">%s</button>', $id, esc_html__('标记已读', 'jio'));
}
printf('<button type="button" class="button-link button-link-delete" data-aksrt-msg-action="delete" data-id="%d">%s</button>', $id, esc_html__('删除', 'aksrt'));
printf('<button type="button" class="button-link button-link-delete" data-jio-msg-action="delete" data-id="%d">%s</button>', $id, esc_html__('删除', 'jio'));
echo '</div></td></tr>';
}
private function render_message_detail_template(array $row): void {
$user = get_userdata((int) $row['user_id']);
$recipient = $user ? $user->display_name : __('已删除用户', 'aksrt');
echo '<template id="aksrt-msg-detail-' . esc_attr((string) $row['id']) . '">';
echo '<h3 class="aksrt-msg-detail__title">' . esc_html((string) $row['title']) . '</h3><dl class="aksrt-msg-detail__meta">';
$recipient = $user ? $user->display_name : __('已删除用户', 'jio');
echo '<template id="jio-msg-detail-' . esc_attr((string) $row['id']) . '">';
echo '<h3 class="jio-msg-detail__title">' . esc_html((string) $row['title']) . '</h3><dl class="jio-msg-detail__meta">';
$meta = [
__('消息编号', 'aksrt') => '#' . $row['id'],
__('收件人', 'aksrt') => $recipient . ' · #' . $row['user_id'],
__('消息类型', 'aksrt') => MessageTypes::label((string) $row['type']),
__('发送时间', 'aksrt') => $this->message_time((string) $row['created_at']),
__('阅读状态', 'aksrt') => empty($row['is_read']) ? __('未读', 'aksrt') : __('已读', 'aksrt'),
__('阅读时间', 'aksrt') => empty($row['is_read']) || empty($row['read_at']) ? '—' : $this->message_time((string) $row['read_at']),
__('消息编号', 'jio') => '#' . $row['id'],
__('收件人', 'jio') => $recipient . ' · #' . $row['user_id'],
__('消息类型', 'jio') => MessageTypes::label((string) $row['type']),
__('发送时间', 'jio') => $this->message_time((string) $row['created_at']),
__('阅读状态', 'jio') => empty($row['is_read']) ? __('未读', 'jio') : __('已读', 'jio'),
__('阅读时间', 'jio') => empty($row['is_read']) || empty($row['read_at']) ? '—' : $this->message_time((string) $row['read_at']),
];
foreach ($meta as $label => $value) {
echo '<div><dt>' . esc_html($label) . '</dt><dd>' . esc_html($value) . '</dd></div>';
}
echo '</dl><h4>' . esc_html__('完整内容', 'aksrt') . '</h4><div class="aksrt-msg-detail__content">' . esc_html((string) $row['description']) . '</div>';
echo '</dl><h4>' . esc_html__('完整内容', 'jio') . '</h4><div class="jio-msg-detail__content">' . esc_html((string) $row['description']) . '</div>';
if (!empty($row['target_url'])) {
printf('<a class="aksrt-msg-detail__link" href="%s" target="_blank" rel="noopener noreferrer">%s<span class="screen-reader-text">%s</span></a>', esc_url((string) $row['target_url']), esc_html((string) $row['target_url']), esc_html__('(在新窗口打开)', 'aksrt'));
printf('<a class="jio-msg-detail__link" href="%s" target="_blank" rel="noopener noreferrer">%s<span class="screen-reader-text">%s</span></a>', esc_url((string) $row['target_url']), esc_html((string) $row['target_url']), esc_html__('(在新窗口打开)', 'jio'));
}
echo '<p class="aksrt-native-field__hint">' . esc_html__('查看详情不会改变收件人的已读状态。', 'aksrt') . '</p></template>';
echo '<p class="jio-native-field__hint">' . esc_html__('查看详情不会改变收件人的已读状态。', 'jio') . '</p></template>';
}
private function message_user_cell(int $user_id, array $filters): string {
$user = $user_id > 0 ? get_userdata($user_id) : false;
if (!$user) {
return '<span class="aksrt-messages-admin__muted">' . esc_html(sprintf(__('已删除用户 #%d', 'aksrt'), $user_id)) . '</span>';
return '<span class="jio-messages-admin__muted">' . esc_html(sprintf(__('已删除用户 #%d', 'jio'), $user_id)) . '</span>';
}
$avatar = function_exists('aksrt_get_avatar_url') ? aksrt_get_avatar_url($user_id, 48) : get_avatar_url($user_id, ['size' => 48]);
$avatar = function_exists('jio_get_avatar_url') ? jio_get_avatar_url($user_id, 48) : get_avatar_url($user_id, ['size' => 48]);
$filter_url = $this->message_page_url(array_merge($this->message_filter_args($filters), ['user' => (string) $user_id]));
return '<a class="aksrt-msg-user" href="' . esc_url($filter_url) . '" title="' . esc_attr__('只看该用户', 'aksrt') . '">'
return '<a class="jio-msg-user" href="' . esc_url($filter_url) . '" title="' . esc_attr__('只看该用户', 'jio') . '">'
. '<img src="' . esc_url($avatar) . '" width="24" height="24" alt="" />'
. '<span>' . esc_html($user->display_name) . '</span></a>'
. '<div class="aksrt-messages-admin__sub">#' . esc_html((string) $user_id) . ' · <a href="' . esc_url(get_edit_user_link($user_id)) . '">' . esc_html__('编辑', 'aksrt') . '</a></div>';
. '<div class="jio-messages-admin__sub">#' . esc_html((string) $user_id) . ' · <a href="' . esc_url(get_edit_user_link($user_id)) . '">' . esc_html__('编辑', 'jio') . '</a></div>';
}
}
+27 -27
View File
@@ -3,13 +3,13 @@
* 工单管理:列表(含代客提交弹窗)与处理工作区(?id=)两个页内视图。
* 写操作统一使用 TicketRestAdmin;此处只负责服务端渲染。
*
* @package AKSRT
* @package Jio
*/
namespace AKSRT\Admin\Native;
namespace Jio\Admin\Native;
use AKSRT\Ticket\TicketRepository;
use AKSRT\Ticket\TicketStatusService;
use Jio\Ticket\TicketRepository;
use Jio\Ticket\TicketStatusService;
defined('ABSPATH') || exit;
@@ -19,7 +19,7 @@ trait UsersMenusTicketsCenter {
use UsersMenusTicketsDetail;
use UsersMenusTicketsCreate;
private const TICKET_PAGE_SLUG = 'aksrt_tickets';
private const TICKET_PAGE_SLUG = 'jio_tickets';
private const TICKET_PER_PAGE = 20;
private function count_pending_tickets(): int {
@@ -35,18 +35,18 @@ trait UsersMenusTicketsCenter {
}
$css = '/assets/css/admin/tickets.css';
$js = '/assets/js/admin-tickets.js';
wp_enqueue_style('aksrt-admin-tickets', AKSRT_URI . $css, [NativeUi::STYLE_HANDLE], (string) filemtime(AKSRT_DIR . $css));
wp_enqueue_script('aksrt-admin-tickets', AKSRT_URI . $js, [], (string) filemtime(AKSRT_DIR . $js), true);
wp_localize_script('aksrt-admin-tickets', 'AKSRTAdminTickets', [
wp_enqueue_style('jio-admin-tickets', JIO_URI . $css, [NativeUi::STYLE_HANDLE], (string) filemtime(JIO_DIR . $css));
wp_enqueue_script('jio-admin-tickets', JIO_URI . $js, [], (string) filemtime(JIO_DIR . $js), true);
wp_localize_script('jio-admin-tickets', 'JioAdminTickets', [
'restNonce' => wp_create_nonce('wp_rest'),
'currentUser' => get_current_user_id(),
'strings' => [
'saving' => __('正在保存…', 'aksrt'),
'saveFailed' => __('保存失败,请检查填写内容后重试。', 'aksrt'),
'uncertain' => __('连接中断,提交结果尚未确认。请先重新加载核对,避免重复提交。', 'aksrt'),
'required' => __('请填写内容,不能只包含空格。', 'aksrt'),
'tooLong' => __('插入后将超过字数限制,请先精简回复内容。', 'aksrt'),
'discard' => __('有尚未保存的内容。关闭后将丢弃,是否继续?', 'aksrt'),
'saving' => __('正在保存…', 'jio'),
'saveFailed' => __('保存失败,请检查填写内容后重试。', 'jio'),
'uncertain' => __('连接中断,提交结果尚未确认。请先重新加载核对,避免重复提交。', 'jio'),
'required' => __('请填写内容,不能只包含空格。', 'jio'),
'tooLong' => __('插入后将超过字数限制,请先精简回复内容。', 'jio'),
'discard' => __('有尚未保存的内容。关闭后将丢弃,是否继续?', 'jio'),
],
]);
}
@@ -56,7 +56,7 @@ trait UsersMenusTicketsCenter {
$filters = $this->ticket_filters($_GET);
$paged = max(1, absint($_GET['paged'] ?? 1));
$id = absint($_GET['id'] ?? 0);
NativeUi::page_open('aksrt-tickets-admin');
NativeUi::page_open('jio-tickets-admin');
if ($id > 0) {
$this->render_ticket_detail_page($id, $this->ticket_context($filters, $paged));
} else {
@@ -85,12 +85,12 @@ trait UsersMenusTicketsCenter {
/** 写操作成功后脚本跳转回工作区并带上结果码,这里转成原生通知。 */
private function render_ticket_notice(): void {
$code = isset($_GET['aksrt_ticket_msg']) && is_scalar($_GET['aksrt_ticket_msg']) ? sanitize_key((string) wp_unslash($_GET['aksrt_ticket_msg'])) : '';
$code = isset($_GET['jio_ticket_msg']) && is_scalar($_GET['jio_ticket_msg']) ? sanitize_key((string) wp_unslash($_GET['jio_ticket_msg'])) : '';
$map = [
'created' => __('工单已创建,用户将收到通知。', 'aksrt'),
'reply' => __('回复已发送,用户将收到通知。', 'aksrt'),
'status' => __('工单信息已保存。', 'aksrt'),
'note' => __('内部备注已添加,仅管理员可见。', 'aksrt'),
'created' => __('工单已创建,用户将收到通知。', 'jio'),
'reply' => __('回复已发送,用户将收到通知。', 'jio'),
'status' => __('工单信息已保存。', 'jio'),
'note' => __('内部备注已添加,仅管理员可见。', 'jio'),
];
if (isset($map[$code])) {
echo '<div class="notice notice-success is-dismissible"><p>' . esc_html($map[$code]) . '</p></div>';
@@ -99,21 +99,21 @@ trait UsersMenusTicketsCenter {
/** 提交中与失败提示;失败且结果不确定时显示「重新加载」。 */
private function ticket_feedback(): string {
return '<div class="aksrt-ticket-feedback" hidden data-aksrt-ticket-feedback><p role="status" aria-live="polite" aria-atomic="true"></p>'
. '<button type="button" class="button" data-aksrt-ticket-reload hidden>' . esc_html__('重新加载', 'aksrt') . '</button></div>';
return '<div class="jio-ticket-feedback" hidden data-jio-ticket-feedback><p role="status" aria-live="polite" aria-atomic="true"></p>'
. '<button type="button" class="button" data-jio-ticket-reload hidden>' . esc_html__('重新加载', 'jio') . '</button></div>';
}
/** 代客提交与内部备注弹窗共用的头部:标题、说明与关闭按钮。 */
private function ticket_dialog_head(string $key, string $title, string $description): string {
return '<header class="aksrt-ticket-dialog__head"><div><h2 id="aksrt-ticket-' . esc_attr($key) . '-title" tabindex="-1">' . esc_html($title) . '</h2>'
. '<p id="aksrt-ticket-' . esc_attr($key) . '-description">' . esc_html($description) . '</p></div>'
. '<button type="button" class="aksrt-ticket-dialog__close" data-aksrt-dialog-close aria-label="' . esc_attr__('关闭弹窗', 'aksrt') . '"><span class="dashicons dashicons-no-alt" aria-hidden="true"></span></button></header>';
return '<header class="jio-ticket-dialog__head"><div><h2 id="jio-ticket-' . esc_attr($key) . '-title" tabindex="-1">' . esc_html($title) . '</h2>'
. '<p id="jio-ticket-' . esc_attr($key) . '-description">' . esc_html($description) . '</p></div>'
. '<button type="button" class="jio-ticket-dialog__close" data-jio-dialog-close aria-label="' . esc_attr__('关闭弹窗', 'jio') . '"><span class="dashicons dashicons-no-alt" aria-hidden="true"></span></button></header>';
}
/** 所有表单只提交到现有 REST 写路由,不保留 admin_post 分支。 */
private function ticket_form_open(string $action, int $id, string $class = ''): string {
$path = $action === 'create' ? '' : '/' . $id . '/' . ($action === 'note' ? 'notes' : $action);
return '<form method="post" action="' . esc_url(rest_url('aksrt/v1/admin/tickets' . $path)) . '"'
. ' class="' . esc_attr($class) . '" data-aksrt-ticket-form="' . esc_attr($action) . '">';
return '<form method="post" action="' . esc_url(rest_url('jio/v1/admin/tickets' . $path)) . '"'
. ' class="' . esc_attr($class) . '" data-jio-ticket-form="' . esc_attr($action) . '">';
}
}
+22 -22
View File
@@ -1,48 +1,48 @@
<?php
/** 代客提交工单:列表页上的原生 <dialog>,创建成功后跳转到新工单的工作区。@package AKSRT */
/** 代客提交工单:列表页上的原生 <dialog>,创建成功后跳转到新工单的工作区。@package Jio */
namespace AKSRT\Admin\Native;
namespace Jio\Admin\Native;
use AKSRT\Ticket\TicketSettings;
use AKSRT\Ticket\TicketTypeService;
use Jio\Ticket\TicketSettings;
use Jio\Ticket\TicketTypeService;
defined('ABSPATH') || exit;
trait UsersMenusTicketsCreate {
private const TICKET_CREATE_DIALOG = 'aksrt-ticket-create-dialog';
private const TICKET_CREATE_DIALOG = 'jio-ticket-create-dialog';
private function render_ticket_create_dialog(): void {
$types = TicketTypeService::all();
echo '<dialog id="' . esc_attr(self::TICKET_CREATE_DIALOG) . '" class="aksrt-ticket-dialog" aria-labelledby="aksrt-ticket-create-title" aria-describedby="aksrt-ticket-create-description">';
echo $types === [] ? '<div class="aksrt-ticket-dialog__frame">' : $this->ticket_form_open('create', 0, 'aksrt-ticket-dialog__frame'); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo $this->ticket_dialog_head('create', __('代客提交工单', 'aksrt'), __('为指定用户创建工单,创建后由你跟进,用户会收到通知。', 'aksrt')); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo '<div class="aksrt-ticket-dialog__body">' . $this->ticket_feedback(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo '<dialog id="' . esc_attr(self::TICKET_CREATE_DIALOG) . '" class="jio-ticket-dialog" aria-labelledby="jio-ticket-create-title" aria-describedby="jio-ticket-create-description">';
echo $types === [] ? '<div class="jio-ticket-dialog__frame">' : $this->ticket_form_open('create', 0, 'jio-ticket-dialog__frame'); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo $this->ticket_dialog_head('create', __('代客提交工单', 'jio'), __('为指定用户创建工单,创建后由你跟进,用户会收到通知。', 'jio')); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo '<div class="jio-ticket-dialog__body">' . $this->ticket_feedback(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
if ($types === []) {
echo NativeUi::empty_state(__('尚未配置工单部门', 'aksrt'), __('请先添加部门,再为用户创建工单。', 'aksrt')); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo '<p><a class="button" href="' . esc_url(admin_url('admin.php?page=aksrt-settings#/users/ticket')) . '">' . esc_html__('前往工单配置', 'aksrt') . '</a></p>';
echo NativeUi::empty_state(__('尚未配置工单部门', 'jio'), __('请先添加部门,再为用户创建工单。', 'jio')); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo '<p><a class="button" href="' . esc_url(admin_url('admin.php?page=jio-settings#/users/ticket')) . '">' . esc_html__('前往工单配置', 'jio') . '</a></p>';
} else {
echo '<div class="aksrt-native-form-grid">';
echo '<label class="aksrt-native-field"><span class="aksrt-native-field__label">' . esc_html__('用户 ID', 'aksrt') . '</span><input type="number" name="user_id" min="1" step="1" required placeholder="' . esc_attr__('填写工单归属用户的 ID', 'aksrt') . '"></label>';
echo '<label class="aksrt-native-field"><span class="aksrt-native-field__label">' . esc_html__('工单部门', 'aksrt') . '</span><select name="ticket_type_id" required><option value="">' . esc_html__('请选择部门', 'aksrt') . '</option>';
echo '<div class="jio-native-form-grid">';
echo '<label class="jio-native-field"><span class="jio-native-field__label">' . esc_html__('用户 ID', 'jio') . '</span><input type="number" name="user_id" min="1" step="1" required placeholder="' . esc_attr__('填写工单归属用户的 ID', 'jio') . '"></label>';
echo '<label class="jio-native-field"><span class="jio-native-field__label">' . esc_html__('工单部门', 'jio') . '</span><select name="ticket_type_id" required><option value="">' . esc_html__('请选择部门', 'jio') . '</option>';
foreach ($types as $type) {
echo '<option value="' . (int) $type['id'] . '">' . esc_html((string) $type['name']) . '</option>';
}
echo '</select></label>';
echo '<label class="aksrt-native-field is-wide"><span class="aksrt-native-field__label">' . esc_html__('工单标题', 'aksrt') . '</span><input type="text" name="title" maxlength="' . TicketSettings::title_max() . '" required placeholder="' . esc_attr__('简要说明用户遇到的问题', 'aksrt') . '"></label>';
echo '<label class="jio-native-field is-wide"><span class="jio-native-field__label">' . esc_html__('工单标题', 'jio') . '</span><input type="text" name="title" maxlength="' . TicketSettings::title_max() . '" required placeholder="' . esc_attr__('简要说明用户遇到的问题', 'jio') . '"></label>';
if (TicketSettings::order_link_enabled()) {
echo '<label class="aksrt-native-field is-wide"><span class="aksrt-native-field__label">' . esc_html__('关联订单(可选)', 'aksrt') . '</span><input type="text" name="order_number" placeholder="' . esc_attr__('填写订单号', 'aksrt') . '"></label>';
echo '<label class="jio-native-field is-wide"><span class="jio-native-field__label">' . esc_html__('关联订单(可选)', 'jio') . '</span><input type="text" name="order_number" placeholder="' . esc_attr__('填写订单号', 'jio') . '"></label>';
}
echo '<label class="aksrt-native-field is-wide"><span class="aksrt-native-field__label">' . esc_html__('问题描述', 'aksrt') . '</span><textarea id="aksrt-ticket-create-content" name="content" rows="7" maxlength="' . TicketSettings::content_max() . '" required data-aksrt-ticket-editor aria-describedby="aksrt-ticket-create-count" placeholder="' . esc_attr__('描述问题、发生时间和已尝试的处理方式', 'aksrt') . '"></textarea>';
echo '<span id="aksrt-ticket-create-count" class="aksrt-ticket-counter" data-aksrt-ticket-counter="aksrt-ticket-create-content">0 / ' . TicketSettings::content_max() . '</span></label></div>';
echo '<label class="jio-native-field is-wide"><span class="jio-native-field__label">' . esc_html__('问题描述', 'jio') . '</span><textarea id="jio-ticket-create-content" name="content" rows="7" maxlength="' . TicketSettings::content_max() . '" required data-jio-ticket-editor aria-describedby="jio-ticket-create-count" placeholder="' . esc_attr__('描述问题、发生时间和已尝试的处理方式', 'jio') . '"></textarea>';
echo '<span id="jio-ticket-create-count" class="jio-ticket-counter" data-jio-ticket-counter="jio-ticket-create-content">0 / ' . TicketSettings::content_max() . '</span></label></div>';
}
echo '</div><footer class="aksrt-ticket-dialog__foot">';
echo '</div><footer class="jio-ticket-dialog__foot">';
if ($types !== []) {
echo '<span class="aksrt-ticket-hint">' . esc_html__('新工单状态为「已回复」', 'aksrt') . '</span>';
echo '<span class="jio-ticket-hint">' . esc_html__('新工单状态为「已回复」', 'jio') . '</span>';
}
echo '<button type="button" class="button" data-aksrt-dialog-close>' . esc_html__('取消', 'aksrt') . '</button>';
echo '<button type="button" class="button" data-jio-dialog-close>' . esc_html__('取消', 'jio') . '</button>';
if ($types !== []) {
echo '<button type="submit" class="button button-primary">' . esc_html__('创建工单并通知用户', 'aksrt') . '</button>';
echo '<button type="submit" class="button button-primary">' . esc_html__('创建工单并通知用户', 'jio') . '</button>';
}
echo '</footer>' . ($types === [] ? '</div>' : '</form>') . '</dialog>';
}
+83 -83
View File
@@ -1,34 +1,34 @@
<?php
/** 工单处理工作区(?id=):左侧会话面板(含底部回复框),右侧处理信息与内部备注(弹窗添加)。@package AKSRT */
/** 工单处理工作区(?id=):左侧会话面板(含底部回复框),右侧处理信息与内部备注(弹窗添加)。@package Jio */
namespace AKSRT\Admin\Native;
namespace Jio\Admin\Native;
use AKSRT\Ticket\TicketNoteService;
use AKSRT\Ticket\TicketPrereplyService;
use AKSRT\Ticket\TicketRepository;
use AKSRT\Ticket\TicketSettings;
use AKSRT\Ticket\TicketStatusService;
use AKSRT\Ticket\TicketTypeService;
use Jio\Ticket\TicketNoteService;
use Jio\Ticket\TicketPrereplyService;
use Jio\Ticket\TicketRepository;
use Jio\Ticket\TicketSettings;
use Jio\Ticket\TicketStatusService;
use Jio\Ticket\TicketTypeService;
defined('ABSPATH') || exit;
trait UsersMenusTicketsDetail {
private const TICKET_NOTE_DIALOG = 'aksrt-ticket-note-dialog';
private const TICKET_NOTE_DIALOG = 'jio-ticket-note-dialog';
/** 详情按 ID 查询,不受列表筛选影响;$context 只用于「返回工单列表」。 */
private function render_ticket_detail_page(int $id, array $context): void {
$back = NativeUi::button_link(__('返回工单列表', 'aksrt'), $this->ticket_page_url($context), false, 'dashicons-arrow-left-alt');
$back = NativeUi::button_link(__('返回工单列表', 'jio'), $this->ticket_page_url($context), false, 'dashicons-arrow-left-alt');
$ticket = TicketRepository::get_instance()->find($id);
echo '<div class="aksrt-native-header aksrt-tickets-admin__detail-header"><div class="aksrt-native-header__main"><h1 class="aksrt-native-header__title">';
echo '<div class="jio-native-header jio-tickets-admin__detail-header"><div class="jio-native-header__main"><h1 class="jio-native-header__title">';
if ($ticket === null) {
echo esc_html__('工单详情', 'aksrt') . '</h1></div><div class="aksrt-native-header__actions">' . $back . '</div></div><hr class="wp-header-end">'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo NativeUi::empty_state(__('工单不存在', 'aksrt'), __('工单可能已被删除,或链接有误。', 'aksrt'), 'dashicons-search'); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo esc_html__('工单详情', 'jio') . '</h1></div><div class="jio-native-header__actions">' . $back . '</div></div><hr class="wp-header-end">'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo NativeUi::empty_state(__('工单不存在', 'jio'), __('工单可能已被删除,或链接有误。', 'jio'), 'dashicons-search'); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
return;
}
echo '<span class="aksrt-native-header__icon dashicons dashicons-tickets-alt" aria-hidden="true"></span>' . esc_html((string) $ticket['title']) . '</h1>';
echo '<p class="aksrt-native-header__desc"><span>' . esc_html((string) $ticket['ticket_num']) . '</span> ' . $this->ticket_status_badge($ticket) . $this->ticket_urge_badge($ticket) . '</p>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo '</div><div class="aksrt-native-header__actions">' . $back . '</div></div><hr class="wp-header-end">'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo '<span class="jio-native-header__icon dashicons dashicons-tickets-alt" aria-hidden="true"></span>' . esc_html((string) $ticket['title']) . '</h1>';
echo '<p class="jio-native-header__desc"><span>' . esc_html((string) $ticket['ticket_num']) . '</span> ' . $this->ticket_status_badge($ticket) . $this->ticket_urge_badge($ticket) . '</p>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo '</div><div class="jio-native-header__actions">' . $back . '</div></div><hr class="wp-header-end">'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
$this->render_ticket_notice();
echo $this->ticket_feedback(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo $this->render_ticket_detail_panel($ticket); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
@@ -38,11 +38,11 @@ trait UsersMenusTicketsDetail {
private function render_ticket_detail_panel(array $ticket): string {
$id = (int) $ticket['id'];
$footer = !empty($ticket['is_closed'])
? '<p class="aksrt-ticket-detail__closed"><span class="dashicons dashicons-lock" aria-hidden="true"></span>' . esc_html__('工单已关闭,不能继续回复或变更处理信息;仍可添加内部备注。', 'aksrt') . '</p>'
? '<p class="jio-ticket-detail__closed"><span class="dashicons dashicons-lock" aria-hidden="true"></span>' . esc_html__('工单已关闭,不能继续回复或变更处理信息;仍可添加内部备注。', 'jio') . '</p>'
: $this->ticket_reply_form($id);
$html = '<div class="aksrt-ticket-detail" data-aksrt-ticket-detail="' . $id . '"><div class="aksrt-ticket-detail__main">';
$html = '<div class="jio-ticket-detail" data-jio-ticket-detail="' . $id . '"><div class="jio-ticket-detail__main">';
$html .= $this->ticket_conversation($ticket, TicketRepository::get_instance()->replies($id), $footer);
$html .= '</div><aside class="aksrt-ticket-detail__side" aria-label="' . esc_attr__('工单信息与内部备注', 'aksrt') . '">';
$html .= '</div><aside class="jio-ticket-detail__side" aria-label="' . esc_attr__('工单信息与内部备注', 'jio') . '">';
return $html . $this->ticket_info_card($ticket) . $this->ticket_notes_card($id) . '</aside></div>';
}
@@ -52,48 +52,48 @@ trait UsersMenusTicketsDetail {
* 原始问题与回复同属一条时间线:用户在左、客服在右、系统事件居中。
*/
private function ticket_conversation(array $ticket, array $replies, string $footer): string {
$html = '<section class="aksrt-ticket-conversation" aria-label="' . esc_attr__('问题与沟通记录', 'aksrt') . '">';
$html .= '<div class="aksrt-ticket-section-head"><h3>' . esc_html__('沟通记录', 'aksrt') . ' <span class="aksrt-ticket-pill">' . esc_html(sprintf(__('%d 条记录', 'aksrt'), count($replies))) . '</span></h3>';
$html = '<section class="jio-ticket-conversation" aria-label="' . esc_attr__('问题与沟通记录', 'jio') . '">';
$html .= '<div class="jio-ticket-section-head"><h3>' . esc_html__('沟通记录', 'jio') . ' <span class="jio-ticket-pill">' . esc_html(sprintf(__('%d 条记录', 'jio'), count($replies))) . '</span></h3>';
if ($replies !== []) {
$html .= '<button type="button" class="button-link" data-aksrt-ticket-latest hidden>' . esc_html__('查看最新记录', 'aksrt') . '</button>';
$html .= '<button type="button" class="button-link" data-jio-ticket-latest hidden>' . esc_html__('查看最新记录', 'jio') . '</button>';
}
$html .= '</div><div class="aksrt-ticket-conversation__scroll" tabindex="0" role="region" aria-label="' . esc_attr__('可滚动的沟通记录', 'aksrt') . '"><ol class="aksrt-ticket-detail__thread">';
$html .= $this->ticket_message('origin', (int) $ticket['user_id'], (string) $ticket['user_name'], !empty($ticket['post_admin_id']) ? __('代客提交', 'aksrt') : __('提交了工单', 'aksrt'), (string) $ticket['post_time'], (string) $ticket['content'], (array) $ticket['attachment']);
$html .= '</div><div class="jio-ticket-conversation__scroll" tabindex="0" role="region" aria-label="' . esc_attr__('可滚动的沟通记录', 'jio') . '"><ol class="jio-ticket-detail__thread">';
$html .= $this->ticket_message('origin', (int) $ticket['user_id'], (string) $ticket['user_name'], !empty($ticket['post_admin_id']) ? __('代客提交', 'jio') : __('提交了工单', 'jio'), (string) $ticket['post_time'], (string) $ticket['content'], (array) $ticket['attachment']);
if ($replies === []) {
$html .= '<li class="aksrt-ticket-detail__empty">' . esc_html__('暂无回复,请核对问题后联系用户。', 'aksrt') . '</li>';
$html .= '<li class="jio-ticket-detail__empty">' . esc_html__('暂无回复,请核对问题后联系用户。', 'jio') . '</li>';
}
foreach ($replies as $reply) {
if ((string) $reply['author_type'] === 'system') {
$html .= '<li class="aksrt-ticket-detail__event"><span>' . esc_html((string) $reply['content']) . '</span><time>' . esc_html((string) $reply['created_at']) . '</time></li>';
$html .= '<li class="jio-ticket-detail__event"><span>' . esc_html((string) $reply['content']) . '</span><time>' . esc_html((string) $reply['created_at']) . '</time></li>';
continue;
}
$admin = (string) $reply['author_type'] === 'admin';
$quote_html = '';
if (is_array($reply['quote'] ?? null)) {
$quote = $reply['quote'];
$quote_html = '<blockquote class="aksrt-ticket-detail__quote">' . esc_html(!empty($quote['is_deleted']) ? __('原回复已删除', 'aksrt') : (string) $quote['content']) . '</blockquote>';
$quote_html = '<blockquote class="jio-ticket-detail__quote">' . esc_html(!empty($quote['is_deleted']) ? __('原回复已删除', 'jio') : (string) $quote['content']) . '</blockquote>';
}
$html .= $this->ticket_message($admin ? 'admin' : 'user', (int) $reply['author_id'], (string) $reply['author_name'], $admin ? __('客服', 'aksrt') : __('用户', 'aksrt'), (string) $reply['created_at'], (string) $reply['content'], (array) $reply['attachment'], $quote_html);
$html .= $this->ticket_message($admin ? 'admin' : 'user', (int) $reply['author_id'], (string) $reply['author_name'], $admin ? __('客服', 'jio') : __('用户', 'jio'), (string) $reply['created_at'], (string) $reply['content'], (array) $reply['attachment'], $quote_html);
}
return $html . '</ol></div>' . $footer . '</section>';
}
private function ticket_message(string $variant, int $user_id, string $name, string $role, string $time, string $content, array $attachments, string $quote_html = ''): string {
$avatar = function_exists('aksrt_get_avatar_url') ? aksrt_get_avatar_url($user_id, 64) : '';
return '<li class="aksrt-ticket-detail__message is-' . esc_attr($variant) . '">'
. ($avatar !== '' ? '<img class="aksrt-ticket-detail__avatar" src="' . esc_url($avatar) . '" alt="" width="32" height="32">' : '<span class="aksrt-ticket-detail__avatar" aria-hidden="true"></span>')
. '<div class="aksrt-ticket-detail__bubble"><header class="aksrt-ticket-detail__meta"><strong>' . esc_html($name !== '' ? $name : '#' . $user_id) . '</strong>'
. '<span class="aksrt-ticket-detail__role">' . esc_html($role) . '</span><time>' . esc_html($time) . '</time></header>'
. $quote_html . '<div class="aksrt-ticket-detail__body">' . wpautop(esc_html($content)) . '</div>'
$avatar = function_exists('jio_get_avatar_url') ? jio_get_avatar_url($user_id, 64) : '';
return '<li class="jio-ticket-detail__message is-' . esc_attr($variant) . '">'
. ($avatar !== '' ? '<img class="jio-ticket-detail__avatar" src="' . esc_url($avatar) . '" alt="" width="32" height="32">' : '<span class="jio-ticket-detail__avatar" aria-hidden="true"></span>')
. '<div class="jio-ticket-detail__bubble"><header class="jio-ticket-detail__meta"><strong>' . esc_html($name !== '' ? $name : '#' . $user_id) . '</strong>'
. '<span class="jio-ticket-detail__role">' . esc_html($role) . '</span><time>' . esc_html($time) . '</time></header>'
. $quote_html . '<div class="jio-ticket-detail__body">' . wpautop(esc_html($content)) . '</div>'
. $this->ticket_attachments_html($attachments) . '</div></li>';
}
private function ticket_reply_form(int $id): string {
$field = 'aksrt-ticket-reply-' . $id;
$field = 'jio-ticket-reply-' . $id;
$tools = '';
$prereplies = TicketPrereplyService::all();
if ($prereplies !== []) {
$tools = '<label class="screen-reader-text" for="' . $field . '-pre">' . esc_html__('插入预设回复', 'aksrt') . '</label><select id="' . $field . '-pre" data-aksrt-ticket-prereply="' . $field . '"><option value="">' . esc_html__('插入预设回复…', 'aksrt') . '</option>';
$tools = '<label class="screen-reader-text" for="' . $field . '-pre">' . esc_html__('插入预设回复', 'jio') . '</label><select id="' . $field . '-pre" data-jio-ticket-prereply="' . $field . '"><option value="">' . esc_html__('插入预设回复…', 'jio') . '</option>';
foreach ($prereplies as $row) {
$tools .= '<option value="' . esc_attr((string) $row['content']) . '">' . esc_html(mb_strimwidth((string) $row['content'], 0, 40, '…')) . '</option>';
}
@@ -101,13 +101,13 @@ trait UsersMenusTicketsDetail {
}
$max = TicketSettings::reply_max();
// 输入框与工具栏合为一个整体:预设回复、字数与发送按钮同排;Ctrl/⌘ + Enter 发送。
return $this->ticket_form_open('reply', $id, 'aksrt-ticket-composer')
. '<label class="screen-reader-text" for="' . esc_attr($field) . '">' . esc_html__('回复用户', 'aksrt') . '</label>'
. '<div class="aksrt-ticket-composer__box"><textarea id="' . esc_attr($field) . '" name="content" rows="3" maxlength="' . $max . '" required data-aksrt-ticket-editor data-aksrt-ticket-send-shortcut aria-describedby="' . esc_attr($field) . '-count ' . esc_attr($field) . '-hint" placeholder="' . esc_attr__('回复用户,发送后用户将收到通知…', 'aksrt') . '"></textarea>'
. '<div class="aksrt-ticket-composer__bar">' . $tools
. '<span id="' . esc_attr($field) . '-hint" class="aksrt-ticket-hint aksrt-ticket-composer__shortcut">' . esc_html__('Ctrl + Enter 发送', 'aksrt') . '</span>'
. '<span id="' . esc_attr($field) . '-count" class="aksrt-ticket-counter" data-aksrt-ticket-counter="' . esc_attr($field) . '">0 / ' . $max . '</span>'
. '<button type="submit" class="button button-primary"><span class="dashicons dashicons-email-alt" aria-hidden="true"></span>' . esc_html__('发送回复', 'aksrt') . '</button></div></div></form>';
return $this->ticket_form_open('reply', $id, 'jio-ticket-composer')
. '<label class="screen-reader-text" for="' . esc_attr($field) . '">' . esc_html__('回复用户', 'jio') . '</label>'
. '<div class="jio-ticket-composer__box"><textarea id="' . esc_attr($field) . '" name="content" rows="3" maxlength="' . $max . '" required data-jio-ticket-editor data-jio-ticket-send-shortcut aria-describedby="' . esc_attr($field) . '-count ' . esc_attr($field) . '-hint" placeholder="' . esc_attr__('回复用户,发送后用户将收到通知…', 'jio') . '"></textarea>'
. '<div class="jio-ticket-composer__bar">' . $tools
. '<span id="' . esc_attr($field) . '-hint" class="jio-ticket-hint jio-ticket-composer__shortcut">' . esc_html__('Ctrl + Enter 发送', 'jio') . '</span>'
. '<span id="' . esc_attr($field) . '-count" class="jio-ticket-counter" data-jio-ticket-counter="' . esc_attr($field) . '">0 / ' . $max . '</span>'
. '<button type="submit" class="button button-primary"><span class="dashicons dashicons-email-alt" aria-hidden="true"></span>' . esc_html__('发送回复', 'jio') . '</button></div></div></form>';
}
private function ticket_info_card(array $ticket): string {
@@ -115,40 +115,40 @@ trait UsersMenusTicketsDetail {
$editable = empty($ticket['is_closed']);
$controls = $editable ? $this->ticket_info_controls($ticket) : [];
$rows = [
[__('工单号', 'aksrt'), esc_html((string) $ticket['ticket_num'])],
[__('状态', 'aksrt'), $controls['status'] ?? $this->ticket_status_badge($ticket), 'status'],
[__('用户', 'aksrt'), $this->ticket_user_chip((int) $ticket['user_id'], (string) $ticket['user_name'])],
[__('部门', 'aksrt'), $controls['ticket_type_id'] ?? esc_html((string) $ticket['ticket_type_name'] !== '' ? (string) $ticket['ticket_type_name'] : '—'), 'ticket_type_id'],
[__('跟进人', 'aksrt'), $controls['admin_id'] ?? esc_html((int) $ticket['admin_id'] > 0 ? (string) $ticket['admin_name'] : __('未指派', 'aksrt')), 'admin_id'],
[__('工单号', 'jio'), esc_html((string) $ticket['ticket_num'])],
[__('状态', 'jio'), $controls['status'] ?? $this->ticket_status_badge($ticket), 'status'],
[__('用户', 'jio'), $this->ticket_user_chip((int) $ticket['user_id'], (string) $ticket['user_name'])],
[__('部门', 'jio'), $controls['ticket_type_id'] ?? esc_html((string) $ticket['ticket_type_name'] !== '' ? (string) $ticket['ticket_type_name'] : '—'), 'ticket_type_id'],
[__('跟进人', 'jio'), $controls['admin_id'] ?? esc_html((int) $ticket['admin_id'] > 0 ? (string) $ticket['admin_name'] : __('未指派', 'jio')), 'admin_id'],
];
if ((string) $ticket['order_number'] !== '') {
$rows[] = [__('关联订单', 'aksrt'), esc_html((string) $ticket['order_number'])];
$rows[] = [__('关联订单', 'jio'), esc_html((string) $ticket['order_number'])];
}
$rows[] = [__('提交时间', 'aksrt'), esc_html((string) $ticket['post_time'])];
$rows[] = [__('提交时间', 'jio'), esc_html((string) $ticket['post_time'])];
if ((int) $ticket['urge_count'] > 0) {
$rows[] = [__('催单', 'aksrt'), '<span class="aksrt-ticket-urge">' . esc_html(sprintf(__('%1$d 次,最近 %2$s', 'aksrt'), (int) $ticket['urge_count'], (string) $ticket['last_urge_time'])) . '</span>'];
$rows[] = [__('催单', 'jio'), '<span class="jio-ticket-urge">' . esc_html(sprintf(__('%1$d 次,最近 %2$s', 'jio'), (int) $ticket['urge_count'], (string) $ticket['last_urge_time'])) . '</span>'];
}
$html = '<section class="aksrt-ticket-detail__card"><div class="aksrt-ticket-section-head"><h3>' . esc_html__('工单信息', 'aksrt') . '</h3>';
$html = '<section class="jio-ticket-detail__card"><div class="jio-ticket-section-head"><h3>' . esc_html__('工单信息', 'jio') . '</h3>';
if ($editable) {
$html .= '<button type="button" class="button-link" data-aksrt-ticket-assign>' . esc_html__('由我跟进', 'aksrt') . '</button>';
$html .= '<button type="button" class="button-link" data-jio-ticket-assign>' . esc_html__('由我跟进', 'jio') . '</button>';
}
$html .= '</div>';
if ($editable) {
$html .= $this->ticket_form_open('status', $id, 'aksrt-ticket-detail__status-form');
$html .= $this->ticket_form_open('status', $id, 'jio-ticket-detail__status-form');
}
$html .= '<dl class="aksrt-ticket-detail__info">';
$html .= '<dl class="jio-ticket-detail__info">';
foreach ($rows as $row) {
[$label, $value] = $row;
$label_html = esc_html($label);
if ($editable && isset($row[2])) {
$label_html = '<label for="aksrt-ticket-' . esc_attr($row[2]) . '-' . $id . '">' . $label_html . '</label>';
$label_html = '<label for="jio-ticket-' . esc_attr($row[2]) . '-' . $id . '">' . $label_html . '</label>';
}
$html .= '<dt>' . $label_html . '</dt><dd>' . $value . '</dd>';
}
$html .= '</dl>';
if ($editable) {
$html .= '<div class="aksrt-ticket-close-warning" data-aksrt-ticket-close-warning hidden><label><input type="checkbox" data-aksrt-ticket-close-confirm> <span>' . esc_html__('确认关闭工单:关闭后无法再次回复,尚未发送的回复不会保存。', 'aksrt') . '</span></label></div>';
$html .= '<button type="submit" class="button">' . esc_html__('保存工单信息', 'aksrt') . '</button></form>';
$html .= '<div class="jio-ticket-close-warning" data-jio-ticket-close-warning hidden><label><input type="checkbox" data-jio-ticket-close-confirm> <span>' . esc_html__('确认关闭工单:关闭后无法再次回复,尚未发送的回复不会保存。', 'jio') . '</span></label></div>';
$html .= '<button type="submit" class="button">' . esc_html__('保存工单信息', 'jio') . '</button></form>';
}
return $html . '</section>';
}
@@ -157,7 +157,7 @@ trait UsersMenusTicketsDetail {
private function ticket_info_controls(array $ticket): array {
$id = (int) $ticket['id'];
$from = (string) $ticket['status'];
$controls = ['status' => '<select id="aksrt-ticket-status-' . $id . '" name="status" data-aksrt-ticket-status>'];
$controls = ['status' => '<select id="jio-ticket-status-' . $id . '" name="status" data-jio-ticket-status>'];
foreach (TicketStatusService::map() as $row) {
$key = (string) $row['status_key'];
if (TicketStatusService::can_transition($from, $key)) {
@@ -165,19 +165,19 @@ trait UsersMenusTicketsDetail {
}
}
$controls['status'] .= '</select>';
$controls['ticket_type_id'] = '<select id="aksrt-ticket-ticket_type_id-' . $id . '" name="ticket_type_id" required>';
$controls['ticket_type_id'] = '<select id="jio-ticket-ticket_type_id-' . $id . '" name="ticket_type_id" required>';
foreach (TicketTypeService::all() as $row) {
$controls['ticket_type_id'] .= '<option value="' . (int) $row['id'] . '"' . selected((int) $ticket['ticket_type_id'], (int) $row['id'], false) . '>' . esc_html((string) $row['name']) . '</option>';
}
$controls['ticket_type_id'] .= '</select>';
$controls['admin_id'] = '<select id="aksrt-ticket-admin_id-' . $id . '" name="admin_id"><option value="0">' . esc_html__('未指派', 'aksrt') . '</option>';
$controls['admin_id'] = '<select id="jio-ticket-admin_id-' . $id . '" name="admin_id"><option value="0">' . esc_html__('未指派', 'jio') . '</option>';
$current = (int) $ticket['admin_id'];
$admins = $this->ticket_admin_options();
if ($current > 0 && !isset($admins[$current])) {
$admins[$current] = (string) $ticket['admin_name'];
}
foreach ($admins as $admin_id => $name) {
$controls['admin_id'] .= '<option value="' . $admin_id . '"' . selected($current, $admin_id, false) . '>' . esc_html($admin_id === get_current_user_id() ? sprintf(__('%s(我)', 'aksrt'), $name) : $name) . '</option>';
$controls['admin_id'] .= '<option value="' . $admin_id . '"' . selected($current, $admin_id, false) . '>' . esc_html($admin_id === get_current_user_id() ? sprintf(__('%s(我)', 'jio'), $name) : $name) . '</option>';
}
$controls['admin_id'] .= '</select>';
return $controls;
@@ -203,42 +203,42 @@ trait UsersMenusTicketsDetail {
private function ticket_notes_card(int $id): string {
$notes = TicketNoteService::for_ticket($id);
$visible = 3;
$html = '<section class="aksrt-ticket-detail__card aksrt-ticket-detail__private"><div class="aksrt-ticket-section-head"><h3>' . esc_html__('内部备注', 'aksrt');
$html = '<section class="jio-ticket-detail__card jio-ticket-detail__private"><div class="jio-ticket-section-head"><h3>' . esc_html__('内部备注', 'jio');
if ($notes !== []) {
$html .= ' <span class="aksrt-ticket-pill">' . count($notes) . '</span>';
$html .= ' <span class="jio-ticket-pill">' . count($notes) . '</span>';
}
$html .= '</h3><button type="button" class="button button-small" data-aksrt-dialog-open="' . esc_attr(self::TICKET_NOTE_DIALOG) . '" aria-haspopup="dialog" aria-controls="' . esc_attr(self::TICKET_NOTE_DIALOG) . '">'
. '<span class="dashicons dashicons-plus-alt2" aria-hidden="true"></span>' . esc_html__('添加备注', 'aksrt') . '</button></div>';
$html .= '<p class="aksrt-ticket-hint">' . esc_html__('仅管理员可见,不会发送给用户。', 'aksrt') . '</p>';
$html .= '</h3><button type="button" class="button button-small" data-jio-dialog-open="' . esc_attr(self::TICKET_NOTE_DIALOG) . '" aria-haspopup="dialog" aria-controls="' . esc_attr(self::TICKET_NOTE_DIALOG) . '">'
. '<span class="dashicons dashicons-plus-alt2" aria-hidden="true"></span>' . esc_html__('添加备注', 'jio') . '</button></div>';
$html .= '<p class="jio-ticket-hint">' . esc_html__('仅管理员可见,不会发送给用户。', 'jio') . '</p>';
if ($notes === []) {
return $html . '<p class="aksrt-ticket-detail__notes-empty">' . esc_html__('暂无备注', 'aksrt') . '</p></section>';
return $html . '<p class="jio-ticket-detail__notes-empty">' . esc_html__('暂无备注', 'jio') . '</p></section>';
}
$older = array_slice($notes, 0, -$visible);
$html .= '<div class="aksrt-ticket-detail__notes">';
$html .= '<div class="jio-ticket-detail__notes">';
if ($older !== []) {
$html .= '<details class="aksrt-ticket-detail__notes-older"><summary>' . esc_html(sprintf(__('更早的 %d 条备注', 'aksrt'), count($older))) . '</summary>' . $this->ticket_notes_list($older) . '</details>';
$html .= '<details class="jio-ticket-detail__notes-older"><summary>' . esc_html(sprintf(__('更早的 %d 条备注', 'jio'), count($older))) . '</summary>' . $this->ticket_notes_list($older) . '</details>';
}
return $html . $this->ticket_notes_list(array_slice($notes, -$visible)) . '</div></section>';
}
private function ticket_note_dialog(int $id): string {
$field = 'aksrt-ticket-note-' . $id;
$field = 'jio-ticket-note-' . $id;
$max = TicketSettings::reply_max();
return '<dialog id="' . esc_attr(self::TICKET_NOTE_DIALOG) . '" class="aksrt-ticket-dialog aksrt-ticket-dialog--note" aria-labelledby="aksrt-ticket-note-title" aria-describedby="aksrt-ticket-note-description">'
. $this->ticket_form_open('note', $id, 'aksrt-ticket-dialog__frame')
. $this->ticket_dialog_head('note', __('添加内部备注', 'aksrt'), __('仅管理员可见,不会发送给用户。', 'aksrt'))
. '<div class="aksrt-ticket-dialog__body">' . $this->ticket_feedback()
. '<label class="screen-reader-text" for="' . esc_attr($field) . '">' . esc_html__('备注内容', 'aksrt') . '</label>'
. '<textarea id="' . esc_attr($field) . '" name="content" rows="6" maxlength="' . $max . '" required autofocus data-aksrt-ticket-editor aria-describedby="' . esc_attr($field) . '-count" placeholder="' . esc_attr__('记录处理进展、排查结论…', 'aksrt') . '"></textarea>'
. '<span id="' . esc_attr($field) . '-count" class="aksrt-ticket-counter aksrt-ticket-dialog__counter" data-aksrt-ticket-counter="' . esc_attr($field) . '">0 / ' . $max . '</span></div>'
. '<footer class="aksrt-ticket-dialog__foot"><button type="button" class="button" data-aksrt-dialog-close>' . esc_html__('取消', 'aksrt') . '</button>'
. '<button type="submit" class="button button-primary">' . esc_html__('保存备注', 'aksrt') . '</button></footer></form></dialog>';
return '<dialog id="' . esc_attr(self::TICKET_NOTE_DIALOG) . '" class="jio-ticket-dialog jio-ticket-dialog--note" aria-labelledby="jio-ticket-note-title" aria-describedby="jio-ticket-note-description">'
. $this->ticket_form_open('note', $id, 'jio-ticket-dialog__frame')
. $this->ticket_dialog_head('note', __('添加内部备注', 'jio'), __('仅管理员可见,不会发送给用户。', 'jio'))
. '<div class="jio-ticket-dialog__body">' . $this->ticket_feedback()
. '<label class="screen-reader-text" for="' . esc_attr($field) . '">' . esc_html__('备注内容', 'jio') . '</label>'
. '<textarea id="' . esc_attr($field) . '" name="content" rows="6" maxlength="' . $max . '" required autofocus data-jio-ticket-editor aria-describedby="' . esc_attr($field) . '-count" placeholder="' . esc_attr__('记录处理进展、排查结论…', 'jio') . '"></textarea>'
. '<span id="' . esc_attr($field) . '-count" class="jio-ticket-counter jio-ticket-dialog__counter" data-jio-ticket-counter="' . esc_attr($field) . '">0 / ' . $max . '</span></div>'
. '<footer class="jio-ticket-dialog__foot"><button type="button" class="button" data-jio-dialog-close>' . esc_html__('取消', 'jio') . '</button>'
. '<button type="submit" class="button button-primary">' . esc_html__('保存备注', 'jio') . '</button></footer></form></dialog>';
}
private function ticket_notes_list(array $notes): string {
$html = '<ol>';
foreach ($notes as $note) {
$html .= '<li><div class="aksrt-ticket-detail__note-head"><strong>' . esc_html((string) $note['admin_name']) . '</strong><time>' . esc_html((string) $note['created_at']) . '</time></div><div>' . nl2br(esc_html((string) $note['content'])) . '</div></li>';
$html .= '<li><div class="jio-ticket-detail__note-head"><strong>' . esc_html((string) $note['admin_name']) . '</strong><time>' . esc_html((string) $note['created_at']) . '</time></div><div>' . nl2br(esc_html((string) $note['content'])) . '</div></li>';
}
return $html . '</ol>';
}
@@ -247,9 +247,9 @@ trait UsersMenusTicketsDetail {
if ($urls === []) {
return '';
}
$html = '<ul class="aksrt-ticket-detail__attachments">';
$html = '<ul class="jio-ticket-detail__attachments">';
foreach ($urls as $index => $url) {
$html .= '<li><a href="' . esc_url((string) $url) . '" target="_blank" rel="noopener noreferrer"><img src="' . esc_url((string) $url) . '" alt="' . esc_attr(sprintf(__('查看附件 %d(新窗口)', 'aksrt'), $index + 1)) . '"></a></li>';
$html .= '<li><a href="' . esc_url((string) $url) . '" target="_blank" rel="noopener noreferrer"><img src="' . esc_url((string) $url) . '" alt="' . esc_attr(sprintf(__('查看附件 %d(新窗口)', 'jio'), $index + 1)) . '"></a></li>';
}
return $html . '</ul>';
}
+43 -43
View File
@@ -1,11 +1,11 @@
<?php
/** 工单筛选与列表;标题与操作直接链接到处理工作区(?id=)。@package AKSRT */
/** 工单筛选与列表;标题与操作直接链接到处理工作区(?id=)。@package Jio */
namespace AKSRT\Admin\Native;
namespace Jio\Admin\Native;
use AKSRT\Ticket\TicketRepository;
use AKSRT\Ticket\TicketStatusService;
use AKSRT\Ticket\TicketTypeService;
use Jio\Ticket\TicketRepository;
use Jio\Ticket\TicketStatusService;
use Jio\Ticket\TicketTypeService;
defined('ABSPATH') || exit;
@@ -15,19 +15,19 @@ trait UsersMenusTicketsList {
// 先查询并夹取页码,创建成功后跳转的工作区链接才能带上实际页码。
[$result, $paged] = $this->ticket_list_result($filters, $paged);
NativeUi::header(
__('工单管理', 'aksrt'),
__('查看用户问题、跟进沟通并记录处理结果。', 'aksrt'),
__('工单管理', 'jio'),
__('查看用户问题、跟进沟通并记录处理结果。', 'jio'),
'dashicons-tickets-alt',
[
[
'label' => __('代客提交工单', 'aksrt'),
'label' => __('代客提交工单', 'jio'),
'dialog' => self::TICKET_CREATE_DIALOG,
'primary' => true,
'icon' => 'dashicons-plus-alt2',
],
[
'label' => __('工单配置', 'aksrt'),
'url' => admin_url('admin.php?page=aksrt-settings#/users/ticket'),
'label' => __('工单配置', 'jio'),
'url' => admin_url('admin.php?page=jio-settings#/users/ticket'),
'icon' => 'dashicons-admin-generic',
],
]
@@ -57,27 +57,27 @@ trait UsersMenusTicketsList {
}
private function render_ticket_list(array $filters, int $paged, array $result): void {
NativeUi::card_open('', '', true, 'aksrt-tickets-admin__list-card');
NativeUi::card_open('', '', true, 'jio-tickets-admin__list-card');
$this->render_ticket_toolbar($filters);
echo '<div class="aksrt-native-table-wrap"><table class="widefat striped aksrt-tickets-admin__table">';
echo '<caption class="screen-reader-text">' . esc_html__('工单处理列表', 'aksrt') . '</caption><thead><tr>';
echo '<div class="jio-native-table-wrap"><table class="widefat striped jio-tickets-admin__table">';
echo '<caption class="screen-reader-text">' . esc_html__('工单处理列表', 'jio') . '</caption><thead><tr>';
foreach ([
'title' => __('工单 / 问题', 'aksrt'),
'status' => __('状态', 'aksrt'),
'type' => __('部门', 'aksrt'),
'user' => __('用户', 'aksrt'),
'admin' => __('跟进人', 'aksrt'),
'time' => __('最后活动', 'aksrt'),
'actions' => __('操作', 'aksrt'),
'title' => __('工单 / 问题', 'jio'),
'status' => __('状态', 'jio'),
'type' => __('部门', 'jio'),
'user' => __('用户', 'jio'),
'admin' => __('跟进人', 'jio'),
'time' => __('最后活动', 'jio'),
'actions' => __('操作', 'jio'),
] as $key => $label) {
echo '<th scope="col" class="column-' . esc_attr($key) . '">' . esc_html($label) . '</th>';
}
echo '</tr></thead><tbody>';
if ($result['items'] === []) {
$filtered = $filters['status'] !== '' || $filters['ticket_type_id'] > 0 || $filters['s'] !== '';
echo NativeUi::empty_row(7, __('暂无工单', 'aksrt'), $filtered // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
? __('请调整筛选条件,或使用标题右侧的「代客提交工单」。', 'aksrt')
: __('暂无未关闭工单。可切换状态查看历史记录,或代用户创建工单。', 'aksrt'));
echo NativeUi::empty_row(7, __('暂无工单', 'jio'), $filtered // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
? __('请调整筛选条件,或使用标题右侧的「代客提交工单」。', 'jio')
: __('暂无未关闭工单。可切换状态查看历史记录,或代用户创建工单。', 'jio'));
}
$context = $this->ticket_context($filters, $paged);
foreach ($result['items'] as $ticket) {
@@ -106,13 +106,13 @@ trait UsersMenusTicketsList {
}
$views[] = [$key, (string) $row['name'], $count, (string) $row['color']];
}
array_unshift($views, ['', __('未关闭', 'aksrt'), $open_total, '']);
echo '<div class="aksrt-tickets-admin__toolbar"><h2 class="aksrt-native-card__title">' . esc_html__('工单列表', 'aksrt') . '</h2>';
echo '<nav aria-label="' . esc_attr__('按工单状态筛选', 'aksrt') . '"><ul class="subsubsub">';
array_unshift($views, ['', __('未关闭', 'jio'), $open_total, '']);
echo '<div class="jio-tickets-admin__toolbar"><h2 class="jio-native-card__title">' . esc_html__('工单列表', 'jio') . '</h2>';
echo '<nav aria-label="' . esc_attr__('按工单状态筛选', 'jio') . '"><ul class="subsubsub">';
foreach ($views as [$key, $label, $count, $color]) {
echo '<li><a href="' . esc_url($this->ticket_page_url(array_merge($filters, ['status' => $key]))) . '"' . ($filters['status'] === $key ? ' class="current" aria-current="page"' : '') . '>';
if ($color !== '') {
echo '<span class="aksrt-ticket-dot" aria-hidden="true" style="--aksrt-ticket-color:' . esc_attr($color) . '"></span>';
echo '<span class="jio-ticket-dot" aria-hidden="true" style="--jio-ticket-color:' . esc_attr($color) . '"></span>';
}
echo esc_html($label) . ' <span class="count">' . esc_html(number_format_i18n($count)) . '</span></a></li>';
}
@@ -122,17 +122,17 @@ trait UsersMenusTicketsList {
}
private function render_ticket_filter_form(array $filters): void {
echo '<form method="get" action="' . esc_url(admin_url('users.php')) . '" class="aksrt-native-filters">';
echo '<form method="get" action="' . esc_url(admin_url('users.php')) . '" class="jio-native-filters">';
echo '<input type="hidden" name="page" value="' . esc_attr(self::TICKET_PAGE_SLUG) . '"><input type="hidden" name="status" value="' . esc_attr($filters['status']) . '">';
echo '<label class="screen-reader-text" for="aksrt-ticket-filter-type">' . esc_html__('按部门筛选', 'aksrt') . '</label><select name="ticket_type_id" id="aksrt-ticket-filter-type"><option value="">' . esc_html__('全部部门', 'aksrt') . '</option>';
echo '<label class="screen-reader-text" for="jio-ticket-filter-type">' . esc_html__('按部门筛选', 'jio') . '</label><select name="ticket_type_id" id="jio-ticket-filter-type"><option value="">' . esc_html__('全部部门', 'jio') . '</option>';
foreach (TicketTypeService::all() as $row) {
echo '<option value="' . (int) $row['id'] . '"' . selected($filters['ticket_type_id'], (int) $row['id'], false) . '>' . esc_html((string) $row['name']) . '</option>';
}
echo '</select><label class="screen-reader-text" for="aksrt-ticket-filter-s">' . esc_html__('搜索工单', 'aksrt') . '</label>';
echo '<input type="search" id="aksrt-ticket-filter-s" name="s" value="' . esc_attr($filters['s']) . '" placeholder="' . esc_attr__('工单号 / 标题', 'aksrt') . '">';
echo '<button type="submit" class="button">' . esc_html__('筛选', 'aksrt') . '</button>';
echo '</select><label class="screen-reader-text" for="jio-ticket-filter-s">' . esc_html__('搜索工单', 'jio') . '</label>';
echo '<input type="search" id="jio-ticket-filter-s" name="s" value="' . esc_attr($filters['s']) . '" placeholder="' . esc_attr__('工单号 / 标题', 'jio') . '">';
echo '<button type="submit" class="button">' . esc_html__('筛选', 'jio') . '</button>';
if ($filters['ticket_type_id'] > 0 || $filters['s'] !== '') {
echo '<a class="button-link" href="' . esc_url($this->ticket_page_url(['status' => $filters['status']])) . '">' . esc_html__('清除筛选', 'aksrt') . '</a>';
echo '<a class="button-link" href="' . esc_url($this->ticket_page_url(['status' => $filters['status']])) . '">' . esc_html__('清除筛选', 'jio') . '</a>';
}
echo '</form>';
}
@@ -142,22 +142,22 @@ trait UsersMenusTicketsList {
$url = esc_url($this->ticket_page_url(['id' => $id] + $context));
$last = (string) $ticket['last_reply_time'] !== '' ? (string) $ticket['last_reply_time'] : (string) $ticket['post_time'];
$awaiting = in_array((string) $ticket['status'], [TicketStatusService::KEY_PENDING, TicketStatusService::KEY_USER_REPLIED], true);
echo '<tr class="aksrt-tickets-admin__row' . ($awaiting ? ' is-awaiting' : '') . '">';
echo '<tr class="jio-tickets-admin__row' . ($awaiting ? ' is-awaiting' : '') . '">';
echo '<td class="column-title"><a class="row-title" href="' . $url . '">' . esc_html((string) $ticket['title']) . '</a>' . $this->ticket_urge_badge($ticket); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo '<div class="aksrt-tickets-admin__sub">' . esc_html((string) $ticket['ticket_num']) . '</div>';
echo '<div class="jio-tickets-admin__sub">' . esc_html((string) $ticket['ticket_num']) . '</div>';
if ((string) $ticket['order_number'] !== '') {
echo '<div class="aksrt-tickets-admin__sub">' . esc_html__('订单:', 'aksrt') . esc_html((string) $ticket['order_number']) . '</div>';
echo '<div class="jio-tickets-admin__sub">' . esc_html__('订单:', 'jio') . esc_html((string) $ticket['order_number']) . '</div>';
}
echo '</td><td class="column-status">' . $this->ticket_status_badge($ticket) . '</td>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo '<td class="column-type">' . esc_html((string) $ticket['ticket_type_name'] !== '' ? (string) $ticket['ticket_type_name'] : '—') . '</td>';
echo '<td class="column-user">' . $this->ticket_user_chip((int) $ticket['user_id'], (string) $ticket['user_name']) . '</td>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo '<td class="column-admin">' . ((int) $ticket['admin_id'] > 0 ? esc_html((string) $ticket['admin_name']) : '<span class="aksrt-ticket-hint">' . esc_html__('未指派', 'aksrt') . '</span>') . '</td>';
echo '<td class="column-time"><time>' . esc_html($last) . '</time><div class="aksrt-tickets-admin__sub">' . esc_html(sprintf(__('提交于 %s', 'aksrt'), (string) $ticket['post_time'])) . '</div></td>';
echo '<td class="column-actions"><a class="button' . ($awaiting ? ' button-primary' : '') . '" href="' . $url . '">' . esc_html(!empty($ticket['is_closed']) ? __('查看记录', 'aksrt') : __('处理工单', 'aksrt')) . '</a></td></tr>';
echo '<td class="column-admin">' . ((int) $ticket['admin_id'] > 0 ? esc_html((string) $ticket['admin_name']) : '<span class="jio-ticket-hint">' . esc_html__('未指派', 'jio') . '</span>') . '</td>';
echo '<td class="column-time"><time>' . esc_html($last) . '</time><div class="jio-tickets-admin__sub">' . esc_html(sprintf(__('提交于 %s', 'jio'), (string) $ticket['post_time'])) . '</div></td>';
echo '<td class="column-actions"><a class="button' . ($awaiting ? ' button-primary' : '') . '" href="' . $url . '">' . esc_html(!empty($ticket['is_closed']) ? __('查看记录', 'jio') : __('处理工单', 'jio')) . '</a></td></tr>';
}
private function ticket_status_badge(array $ticket): string {
return '<span class="aksrt-ticket-badge" style="--aksrt-ticket-color:' . esc_attr((string) $ticket['status_color']) . '">' . esc_html((string) $ticket['status_label']) . '</span>';
return '<span class="jio-ticket-badge" style="--jio-ticket-color:' . esc_attr((string) $ticket['status_color']) . '">' . esc_html((string) $ticket['status_label']) . '</span>';
}
private function ticket_urge_badge(array $ticket): string {
@@ -165,13 +165,13 @@ trait UsersMenusTicketsList {
if ($count <= 0 || !empty($ticket['is_closed'])) {
return '';
}
return ' <span class="aksrt-ticket-urge" title="' . esc_attr(sprintf(__('最近催单:%s', 'aksrt'), (string) $ticket['last_urge_time'])) . '">' . esc_html(sprintf(__('催单 ×%d', 'aksrt'), $count)) . '</span>';
return ' <span class="jio-ticket-urge" title="' . esc_attr(sprintf(__('最近催单:%s', 'jio'), (string) $ticket['last_urge_time'])) . '">' . esc_html(sprintf(__('催单 ×%d', 'jio'), $count)) . '</span>';
}
private function ticket_user_chip(int $user_id, string $name): string {
$avatar = function_exists('aksrt_get_avatar_url') ? aksrt_get_avatar_url($user_id, 48) : '';
$avatar = function_exists('jio_get_avatar_url') ? jio_get_avatar_url($user_id, 48) : '';
$label = $name !== '' ? $name : '#' . $user_id;
return '<a class="aksrt-ticket-user" href="' . esc_url(get_edit_user_link($user_id)) . '" target="_blank" rel="noopener noreferrer" aria-label="' . esc_attr(sprintf(__('%s 的用户资料(新窗口)', 'aksrt'), $label)) . '">'
return '<a class="jio-ticket-user" href="' . esc_url(get_edit_user_link($user_id)) . '" target="_blank" rel="noopener noreferrer" aria-label="' . esc_attr(sprintf(__('%s 的用户资料(新窗口)', 'jio'), $label)) . '">'
. ($avatar !== '' ? '<img src="' . esc_url($avatar) . '" alt="" width="24" height="24">' : '')
. '<span>' . esc_html($label) . '</span></a>';
}
+54 -54
View File
@@ -1,12 +1,12 @@
<?php
/** Native WordPress identity application review. @package AKSRT */
/** Native WordPress identity application review. @package Jio */
namespace AKSRT\Admin\Native;
namespace Jio\Admin\Native;
use AKSRT\Identity\ApplicationRepository;
use AKSRT\Identity\EvidenceService;
use AKSRT\Identity\EventRepository;
use AKSRT\User\AuthApply;
use Jio\Identity\ApplicationRepository;
use Jio\Identity\EvidenceService;
use Jio\Identity\EventRepository;
use Jio\User\AuthApply;
defined('ABSPATH') || exit;
@@ -17,16 +17,16 @@ trait UsersMenusVerify {
private const VERIFY_STATUSES = ['pending', 'approved', 'rejected', ''];
public function enqueue_verify_assets(string $hook): void {
if ($hook !== 'users_page_aksrt_user_auth') return;
if ($hook !== 'users_page_jio_user_auth') return;
$css = '/assets/css/admin/user-auth.css';
$js = '/assets/js/admin-user-auth.js';
wp_enqueue_style('aksrt-admin-user-auth', AKSRT_URI . $css, [NativeUi::STYLE_HANDLE], (string) filemtime(AKSRT_DIR . $css));
wp_enqueue_script('aksrt-admin-user-auth', AKSRT_URI . $js, [], (string) filemtime(AKSRT_DIR . $js), true);
wp_enqueue_style('jio-admin-user-auth', JIO_URI . $css, [NativeUi::STYLE_HANDLE], (string) filemtime(JIO_DIR . $css));
wp_enqueue_script('jio-admin-user-auth', JIO_URI . $js, [], (string) filemtime(JIO_DIR . $js), true);
}
private function count_verify_status(string $status): int {
global $wpdb;
$table = \AKSRT\Core\DBMigrator::table('identity_application');
$table = \Jio\Core\DBMigrator::table('identity_application');
return (int) $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*) FROM {$table} WHERE type = %s AND channel = %s AND status = %s",
'general', 'manual', $status === 'pending' ? 'manual_pending' : $status
@@ -38,23 +38,23 @@ trait UsersMenusVerify {
$filters = $this->get_verify_filters($_GET);
$data = $this->query_verify_requests($filters, max(1, (int) ($_GET['paged'] ?? 1)));
$counts = $this->get_verify_status_counts($filters);
NativeUi::page_open('aksrt-verify-page');
NativeUi::page_open('jio-verify-page');
NativeUi::header(
__('身份认证', 'aksrt'),
__('查看申请资料与证明材料,在审核弹窗中确认处理结果。', 'aksrt'),
__('身份认证', 'jio'),
__('查看申请资料与证明材料,在审核弹窗中确认处理结果。', 'jio'),
'dashicons-id',
[['label' => __('认证设置', 'aksrt'), 'url' => admin_url('admin.php?page=aksrt-settings#/users/verify'), 'icon' => 'dashicons-admin-settings']]
[['label' => __('认证设置', 'jio'), 'url' => admin_url('admin.php?page=jio-settings#/users/verify'), 'icon' => 'dashicons-admin-settings']]
);
$this->flash();
NativeUi::card_open('', '', true, 'aksrt-verify-list-card');
NativeUi::card_open('', '', true, 'jio-verify-list-card');
$this->render_verify_filters($filters, $counts);
echo '<div class="aksrt-native-table-wrap">';
echo '<div class="jio-native-table-wrap">';
$this->render_verify_table($data['items'], $filters, $data['page']);
NativeUi::table_wrap_close();
AdminListPagination::render('aksrt_user_auth', $data['total'], $data['page'], self::VERIFY_PER_PAGE, $filters, 'users.php');
AdminListPagination::render('jio_user_auth', $data['total'], $data['page'], self::VERIFY_PER_PAGE, $filters, 'users.php');
NativeUi::card_close();
echo '<dialog id="aksrt-verify-dialog" class="aksrt-verify-dialog" aria-labelledby="aksrt-verify-dialog-title" aria-describedby="aksrt-verify-dialog-description"></dialog>';
echo '<noscript><p class="aksrt-native-callout">' . esc_html__('请启用 JavaScript 后查看申请资料和处理审核。', 'aksrt') . '</p></noscript>';
echo '<dialog id="jio-verify-dialog" class="jio-verify-dialog" aria-labelledby="jio-verify-dialog-title" aria-describedby="jio-verify-dialog-description"></dialog>';
echo '<noscript><p class="jio-native-callout">' . esc_html__('请启用 JavaScript 后查看申请资料和处理审核。', 'jio') . '</p></noscript>';
NativeUi::page_close();
}
@@ -70,7 +70,7 @@ trait UsersMenusVerify {
/** @return array{0:string,1:array} */
private function verify_query_base(array $filters): array {
global $wpdb;
$table = \AKSRT\Core\DBMigrator::table('identity_application');
$table = \Jio\Core\DBMigrator::table('identity_application');
$where = ['a.type = %s', 'a.channel = %s'];
$params = ['general', 'manual'];
if ($filters['status'] !== '') {
@@ -128,38 +128,38 @@ trait UsersMenusVerify {
}
private function verify_page_url(array $args = []): string {
return add_query_arg(array_merge(['page' => 'aksrt_user_auth'], $args), admin_url('users.php'));
return add_query_arg(array_merge(['page' => 'jio_user_auth'], $args), admin_url('users.php'));
}
private function render_verify_filters(array $filters, array $counts): void {
echo '<div class="aksrt-verify-toolbar"><div class="aksrt-verify-toolbar__views">';
echo '<h2 class="aksrt-native-card__title">' . esc_html__('认证申请', 'aksrt') . '</h2>';
echo '<ul class="subsubsub" aria-label="' . esc_attr__('认证状态筛选', 'aksrt') . '">';
foreach (['pending' => __('待审核', 'aksrt'), 'approved' => __('已通过', 'aksrt'), 'rejected' => __('已拒绝', 'aksrt'), '' => __('全部', 'aksrt')] as $key => $label) {
echo '<div class="jio-verify-toolbar"><div class="jio-verify-toolbar__views">';
echo '<h2 class="jio-native-card__title">' . esc_html__('认证申请', 'jio') . '</h2>';
echo '<ul class="subsubsub" aria-label="' . esc_attr__('认证状态筛选', 'jio') . '">';
foreach (['pending' => __('待审核', 'jio'), 'approved' => __('已通过', 'jio'), 'rejected' => __('已拒绝', 'jio'), '' => __('全部', 'jio')] as $key => $label) {
$url = $this->verify_page_url(['status' => $key, 'keyword' => $filters['keyword']]);
echo '<li><a href="' . esc_url($url) . '"' . ($key === $filters['status'] ? ' class="current" aria-current="page"' : '') . '>' . esc_html($label)
. ' <span class="count">' . esc_html(number_format_i18n($counts[$key === '' ? 'all' : $key])) . '</span></a></li>';
}
echo '</ul></div><form method="get" action="' . esc_url(admin_url('users.php')) . '" class="aksrt-native-filters">';
echo '<input type="hidden" name="page" value="aksrt_user_auth"><input type="hidden" name="status" value="' . esc_attr($filters['status']) . '">';
echo '<label class="screen-reader-text" for="aksrt-verify-search">' . esc_html__('搜索用户或认证资料', 'aksrt') . '</label>';
echo '<input type="search" id="aksrt-verify-search" name="keyword" value="' . esc_attr($filters['keyword']) . '" placeholder="' . esc_attr__('用户 ID / 昵称 / 邮箱 / 认证名称', 'aksrt') . '">';
echo '<button type="submit" class="button">' . esc_html__('搜索', 'aksrt') . '</button>';
echo '</ul></div><form method="get" action="' . esc_url(admin_url('users.php')) . '" class="jio-native-filters">';
echo '<input type="hidden" name="page" value="jio_user_auth"><input type="hidden" name="status" value="' . esc_attr($filters['status']) . '">';
echo '<label class="screen-reader-text" for="jio-verify-search">' . esc_html__('搜索用户或认证资料', 'jio') . '</label>';
echo '<input type="search" id="jio-verify-search" name="keyword" value="' . esc_attr($filters['keyword']) . '" placeholder="' . esc_attr__('用户 ID / 昵称 / 邮箱 / 认证名称', 'jio') . '">';
echo '<button type="submit" class="button">' . esc_html__('搜索', 'jio') . '</button>';
if ($filters['keyword'] !== '') {
echo '<a class="button-link" href="' . esc_url($this->verify_page_url(['status' => $filters['status']])) . '">' . esc_html__('清除搜索', 'aksrt') . '</a>';
echo '<a class="button-link" href="' . esc_url($this->verify_page_url(['status' => $filters['status']])) . '">' . esc_html__('清除搜索', 'jio') . '</a>';
}
echo '</form></div>';
}
private function render_verify_table(array $items, array $filters, int $page): void {
echo '<table class="widefat fixed striped aksrt-verify-table"><thead><tr>';
foreach (['user' => __('申请用户', 'aksrt'), 'profile' => __('认证资料', 'aksrt'), 'evidence' => __('证明材料', 'aksrt'), 'status' => __('状态', 'aksrt'), 'time' => __('申请时间', 'aksrt'), 'actions' => __('操作', 'aksrt')] as $key => $label) {
echo '<table class="widefat fixed striped jio-verify-table"><thead><tr>';
foreach (['user' => __('申请用户', 'jio'), 'profile' => __('认证资料', 'jio'), 'evidence' => __('证明材料', 'jio'), 'status' => __('状态', 'jio'), 'time' => __('申请时间', 'jio'), 'actions' => __('操作', 'jio')] as $key => $label) {
echo '<th scope="col" class="column-' . esc_attr($key) . '">' . esc_html($label) . '</th>';
}
echo '</tr></thead><tbody>';
if ($items === []) {
$title = $filters['status'] === 'pending' && $filters['keyword'] === '' ? __('暂无待审核申请', 'aksrt') : __('没有符合条件的认证申请', 'aksrt');
$hint = $filters['keyword'] !== '' ? __('请调整搜索关键词或切换状态。', 'aksrt') : __('可切换状态查看其他申请,新申请将在这里显示。', 'aksrt');
$title = $filters['status'] === 'pending' && $filters['keyword'] === '' ? __('暂无待审核申请', 'jio') : __('没有符合条件的认证申请', 'jio');
$hint = $filters['keyword'] !== '' ? __('请调整搜索关键词或切换状态。', 'jio') : __('可切换状态查看其他申请,新申请将在这里显示。', 'jio');
echo NativeUi::empty_row(6, $title, $hint); // phpcs:ignore WordPress.Security.EscapeOutput -- builder escapes.
}
foreach ($items as $row) $this->render_verify_row($row, $filters, $page);
@@ -171,23 +171,23 @@ trait UsersMenusVerify {
$pending = $row['status'] === 'manual_pending';
$evidence = (new EvidenceService())->items($row, true);
echo '<tr><td class="column-user">' . NativeUi::user_cell((int) $row['user_id'], (string) $row['display_name']); // phpcs:ignore WordPress.Security.EscapeOutput -- builder escapes.
echo '<div class="aksrt-verify-sub">' . esc_html((string) $row['user_email']) . '</div></td>';
echo '<td class="column-profile"><strong>' . esc_html((string) $row['public_name']) . '</strong><div class="aksrt-verify-summary">' . esc_html((string) $row['description']) . '</div>';
echo '<div class="jio-verify-sub">' . esc_html((string) $row['user_email']) . '</div></td>';
echo '<td class="column-profile"><strong>' . esc_html((string) $row['public_name']) . '</strong><div class="jio-verify-summary">' . esc_html((string) $row['description']) . '</div>';
$this->render_verify_detail_template($row, $evidence, $filters, $page);
$this->render_verify_delete_template($row, $filters, $page);
echo '</td><td class="column-evidence">' . esc_html($evidence === [] ? __('未提供材料', 'aksrt') : sprintf(__('%d 份材料', 'aksrt'), count($evidence))) . '</td>';
echo '</td><td class="column-evidence">' . esc_html($evidence === [] ? __('未提供材料', 'jio') : sprintf(__('%d 份材料', 'jio'), count($evidence))) . '</td>';
echo '<td class="column-status">' . $this->verify_status_badge((string) $row['status']) . '</td>'; // phpcs:ignore WordPress.Security.EscapeOutput -- builder escapes.
echo '<td class="column-time">' . esc_html($this->verify_time((string) $row['created_at'])) . '</td>';
echo '<td class="column-actions"><div class="aksrt-verify-row-actions">';
echo '<button type="button" class="button' . ($pending ? ' button-primary' : '') . '" data-aksrt-verify-view="' . $id . '" aria-haspopup="dialog" aria-controls="aksrt-verify-dialog">' . esc_html($pending ? __('审核', 'aksrt') : __('查看详情', 'aksrt')) . '</button>';
echo '<button type="button" class="button-link button-link-delete" data-aksrt-verify-delete="' . $id . '" aria-haspopup="dialog" aria-controls="aksrt-verify-dialog">' . esc_html__('删除记录', 'aksrt') . '</button></div></td></tr>';
echo '<td class="column-actions"><div class="jio-verify-row-actions">';
echo '<button type="button" class="button' . ($pending ? ' button-primary' : '') . '" data-jio-verify-view="' . $id . '" aria-haspopup="dialog" aria-controls="jio-verify-dialog">' . esc_html($pending ? __('审核', 'jio') : __('查看详情', 'jio')) . '</button>';
echo '<button type="button" class="button-link button-link-delete" data-jio-verify-delete="' . $id . '" aria-haspopup="dialog" aria-controls="jio-verify-dialog">' . esc_html__('删除记录', 'jio') . '</button></div></td></tr>';
}
private function verify_status_badge(string $status): string {
return NativeUi::status_badge($status, [
'manual_pending' => [__('待审核', 'aksrt'), 'warning'],
'verified' => [__('已通过', 'aksrt'), 'success'],
'rejected' => [__('已拒绝', 'aksrt'), 'danger'],
'manual_pending' => [__('待审核', 'jio'), 'warning'],
'verified' => [__('已通过', 'jio'), 'success'],
'rejected' => [__('已拒绝', 'jio'), 'danger'],
]);
}
@@ -203,9 +203,9 @@ trait UsersMenusVerify {
}
public function handle_review_verify(): void {
if (!current_user_can('aksrt_review_identity') && !current_user_can(self::CAPABILITY)) wp_die(esc_html__('权限不足', 'aksrt'));
if (!current_user_can('jio_review_identity') && !current_user_can(self::CAPABILITY)) wp_die(esc_html__('权限不足', 'jio'));
$uuid = sanitize_text_field((string) wp_unslash($_POST['application_uuid'] ?? ''));
check_admin_referer('aksrt_users_review_verify_' . $uuid);
check_admin_referer('jio_users_review_verify_' . $uuid);
$user_id = absint($_POST['user_id'] ?? 0);
$status = sanitize_key((string) wp_unslash($_POST['status'] ?? ''));
$note = sanitize_textarea_field((string) wp_unslash($_POST['note'] ?? ''));
@@ -215,27 +215,27 @@ trait UsersMenusVerify {
];
$result = AuthApply::review($user_id, $uuid, $status, $note, get_current_user_id(), $fields);
$extra = $this->verify_return_args();
if (is_wp_error($result)) $extra['aksrt_verify_error'] = $result->get_error_message();
$this->redirect('aksrt_user_auth', is_wp_error($result) ? 'verify_failed' : 'verify_updated', $extra);
if (is_wp_error($result)) $extra['jio_verify_error'] = $result->get_error_message();
$this->redirect('jio_user_auth', is_wp_error($result) ? 'verify_failed' : 'verify_updated', $extra);
}
public function handle_delete_verify(): void {
if (!current_user_can('aksrt_review_identity') && !current_user_can(self::CAPABILITY)) wp_die(esc_html__('权限不足', 'aksrt'));
if (!current_user_can('jio_review_identity') && !current_user_can(self::CAPABILITY)) wp_die(esc_html__('权限不足', 'jio'));
$uuid = sanitize_text_field((string) wp_unslash($_POST['application_uuid'] ?? ''));
check_admin_referer('aksrt_users_delete_verify_' . $uuid);
check_admin_referer('jio_users_delete_verify_' . $uuid);
$extra = $this->verify_return_args();
$repo = new ApplicationRepository();
$application = $repo->find_by_uuid($uuid);
if (!$application) {
$this->redirect('aksrt_user_auth', 'verify_failed', array_merge($extra, ['aksrt_verify_error' => __('认证记录不存在', 'aksrt')]));
$this->redirect('jio_user_auth', 'verify_failed', array_merge($extra, ['jio_verify_error' => __('认证记录不存在', 'jio')]));
}
if (!$repo->delete((int) $application['id'])) {
$this->redirect('aksrt_user_auth', 'verify_failed', array_merge($extra, ['aksrt_verify_error' => __('认证记录删除失败', 'aksrt')]));
$this->redirect('jio_user_auth', 'verify_failed', array_merge($extra, ['jio_verify_error' => __('认证记录删除失败', 'jio')]));
}
(new EvidenceService())->delete((array) $application['evidence_ids']);
if (!(new EventRepository())->delete_for_application((int) $application['id'])) {
$this->redirect('aksrt_user_auth', 'verify_failed', array_merge($extra, ['aksrt_verify_error' => __('认证申请记录已删除,但审核事件清理失败。', 'aksrt')]));
$this->redirect('jio_user_auth', 'verify_failed', array_merge($extra, ['jio_verify_error' => __('认证申请记录已删除,但审核事件清理失败。', 'jio')]));
}
$this->redirect('aksrt_user_auth', 'verify_deleted', $extra);
$this->redirect('jio_user_auth', 'verify_deleted', $extra);
}
}
+55 -55
View File
@@ -1,16 +1,16 @@
<?php
/** Identity application detail, review and deletion dialogs. @package AKSRT */
/** Identity application detail, review and deletion dialogs. @package Jio */
namespace AKSRT\Admin\Native;
namespace Jio\Admin\Native;
defined('ABSPATH') || exit;
trait UsersMenusVerifyDialogs {
private function render_verify_dialog_head(string $title, string $description): void {
echo '<div class="aksrt-verify-dialog__head"><div><h2 id="aksrt-verify-dialog-title">' . esc_html($title) . '</h2>';
echo '<p id="aksrt-verify-dialog-description">' . esc_html($description) . '</p></div>';
echo '<button type="button" class="aksrt-verify-dialog__close" data-aksrt-dialog-close aria-label="' . esc_attr__('关闭弹窗', 'aksrt') . '"><span class="dashicons dashicons-no-alt" aria-hidden="true"></span></button></div>';
echo '<div class="jio-verify-dialog__head"><div><h2 id="jio-verify-dialog-title">' . esc_html($title) . '</h2>';
echo '<p id="jio-verify-dialog-description">' . esc_html($description) . '</p></div>';
echo '<button type="button" class="jio-verify-dialog__close" data-jio-dialog-close aria-label="' . esc_attr__('关闭弹窗', 'jio') . '"><span class="dashicons dashicons-no-alt" aria-hidden="true"></span></button></div>';
}
private function render_verify_context(array $row, array $filters, int $page, string $action): void {
@@ -24,70 +24,70 @@ trait UsersMenusVerifyDialogs {
private function render_verify_detail_template(array $row, array $evidence, array $filters, int $page): void {
$pending = $row['status'] === 'manual_pending';
echo '<template id="aksrt-verify-detail-' . (int) $row['id'] . '">';
echo '<template id="jio-verify-detail-' . (int) $row['id'] . '">';
if ($pending) {
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" class="aksrt-verify-dialog__frame" data-aksrt-verify-review>';
$this->render_verify_context($row, $filters, $page, 'aksrt_users_review_verify');
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" class="jio-verify-dialog__frame" data-jio-verify-review>';
$this->render_verify_context($row, $filters, $page, 'jio_users_review_verify');
echo '<input type="hidden" name="user_id" value="' . (int) $row['user_id'] . '">';
} else {
echo '<div class="aksrt-verify-dialog__frame">';
echo '<div class="jio-verify-dialog__frame">';
}
$this->render_verify_dialog_head(
$pending ? __('审核认证申请', 'aksrt') : __('认证申请详情', 'aksrt'),
$pending ? __('先核对申请资料与证明材料,再选择审核结果。', 'aksrt') : __('该申请已处理,以下为申请资料与审核结果。', 'aksrt')
$pending ? __('审核认证申请', 'jio') : __('认证申请详情', 'jio'),
$pending ? __('先核对申请资料与证明材料,再选择审核结果。', 'jio') : __('该申请已处理,以下为申请资料与审核结果。', 'jio')
);
echo '<div class="aksrt-verify-dialog__body">';
echo '<div class="aksrt-verify-applicant">' . NativeUi::user_cell((int) $row['user_id'], (string) $row['display_name']); // phpcs:ignore WordPress.Security.EscapeOutput -- builder escapes.
echo '<div class="jio-verify-dialog__body">';
echo '<div class="jio-verify-applicant">' . NativeUi::user_cell((int) $row['user_id'], (string) $row['display_name']); // phpcs:ignore WordPress.Security.EscapeOutput -- builder escapes.
echo $this->verify_status_badge((string) $row['status']) . '</div>'; // phpcs:ignore WordPress.Security.EscapeOutput -- builder escapes.
$meta = [
__('用户账号', 'aksrt') => (string) $row['user_login'],
__('邮箱', 'aksrt') => (string) $row['user_email'],
__('申请时间', 'aksrt') => $this->verify_time((string) $row['created_at']),
__('用户账号', 'jio') => (string) $row['user_login'],
__('邮箱', 'jio') => (string) $row['user_email'],
__('申请时间', 'jio') => $this->verify_time((string) $row['created_at']),
];
if (!$pending) {
$reviewer = get_userdata((int) $row['reviewer_id']);
$meta[__('审核人', 'aksrt')] = $reviewer ? (string) $reviewer->display_name : '—';
$meta[__('审核时间', 'aksrt')] = $this->verify_time((string) ($row['completed_at'] ?? ''));
$meta[__('审核人', 'jio')] = $reviewer ? (string) $reviewer->display_name : '—';
$meta[__('审核时间', 'jio')] = $this->verify_time((string) ($row['completed_at'] ?? ''));
}
echo '<dl class="aksrt-verify-meta">';
echo '<dl class="jio-verify-meta">';
foreach ($meta as $label => $value) {
echo '<div><dt>' . esc_html($label) . '</dt><dd>' . esc_html($value !== '' ? $value : '—') . '</dd></div>';
}
echo '</dl>';
$this->render_verify_text_section(__('认证名称', 'aksrt'), (string) $row['public_name']);
$this->render_verify_text_section(__('认证简介', 'aksrt'), (string) $row['description']);
$this->render_verify_text_section(__('申请说明', 'aksrt'), (string) $row['manual_reason']);
$this->render_verify_text_section(__('认证名称', 'jio'), (string) $row['public_name']);
$this->render_verify_text_section(__('认证简介', 'jio'), (string) $row['description']);
$this->render_verify_text_section(__('申请说明', 'jio'), (string) $row['manual_reason']);
$this->render_verify_evidence($evidence);
if ($pending) {
$this->render_verify_review_fields($row);
} else {
$this->render_verify_text_section($row['status'] === 'rejected' ? __('拒绝原因', 'aksrt') : __('处理留言', 'aksrt'), (string) $row['review_note']);
$this->render_verify_text_section($row['status'] === 'rejected' ? __('拒绝原因', 'jio') : __('处理留言', 'jio'), (string) $row['review_note']);
}
echo '</div><div class="aksrt-verify-dialog__foot">';
echo '<button type="button" class="button" data-aksrt-dialog-close>' . esc_html($pending ? __('取消', 'aksrt') : __('关闭', 'aksrt')) . '</button>';
echo '</div><div class="jio-verify-dialog__foot">';
echo '<button type="button" class="button" data-jio-dialog-close>' . esc_html($pending ? __('取消', 'jio') : __('关闭', 'jio')) . '</button>';
if ($pending) {
echo '<button type="submit" class="button button-primary" data-aksrt-verify-submit disabled'
. ' data-idle-label="' . esc_attr__('请选择审核结果', 'aksrt') . '"'
. ' data-approve-label="' . esc_attr__('确认通过', 'aksrt') . '"'
. ' data-reject-label="' . esc_attr__('确认拒绝', 'aksrt') . '"'
. ' data-busy-label="' . esc_attr__('正在提交…', 'aksrt') . '">' . esc_html__('请选择审核结果', 'aksrt') . '</button>';
echo '<button type="submit" class="button button-primary" data-jio-verify-submit disabled'
. ' data-idle-label="' . esc_attr__('请选择审核结果', 'jio') . '"'
. ' data-approve-label="' . esc_attr__('确认通过', 'jio') . '"'
. ' data-reject-label="' . esc_attr__('确认拒绝', 'jio') . '"'
. ' data-busy-label="' . esc_attr__('正在提交…', 'jio') . '">' . esc_html__('请选择审核结果', 'jio') . '</button>';
}
echo '</div>' . ($pending ? '</form>' : '</div>') . '</template>';
}
private function render_verify_text_section(string $title, string $text): void {
echo '<section class="aksrt-verify-section"><h3>' . esc_html($title) . '</h3><div class="aksrt-verify-copy">' . esc_html($text !== '' ? $text : __('未填写', 'aksrt')) . '</div></section>';
echo '<section class="jio-verify-section"><h3>' . esc_html($title) . '</h3><div class="jio-verify-copy">' . esc_html($text !== '' ? $text : __('未填写', 'jio')) . '</div></section>';
}
private function render_verify_evidence(array $evidence): void {
echo '<section class="aksrt-verify-section"><h3>' . esc_html__('证明材料', 'aksrt') . '</h3>';
echo '<section class="jio-verify-section"><h3>' . esc_html__('证明材料', 'jio') . '</h3>';
if ($evidence === []) {
echo '<p class="aksrt-verify-evidence-empty">' . esc_html__('未提供可查看的证明材料,请结合申请说明判断。', 'aksrt') . '</p>';
echo '<p class="jio-verify-evidence-empty">' . esc_html__('未提供可查看的证明材料,请结合申请说明判断。', 'jio') . '</p>';
} else {
echo '<ul class="aksrt-verify-evidence">';
echo '<ul class="jio-verify-evidence">';
foreach ($evidence as $index => $item) {
$label = sprintf(__('材料 %d(新窗口查看原图)', 'aksrt'), $index + 1);
echo '<li><a href="' . esc_url($item['url']) . '" target="_blank" rel="noopener noreferrer"><img src="' . esc_url($item['thumbnail_url']) . '" alt="' . esc_attr(sprintf(__('证明材料 %d', 'aksrt'), $index + 1)) . '"><span>' . esc_html($label) . '</span></a></li>';
$label = sprintf(__('材料 %d(新窗口查看原图)', 'jio'), $index + 1);
echo '<li><a href="' . esc_url($item['url']) . '" target="_blank" rel="noopener noreferrer"><img src="' . esc_url($item['thumbnail_url']) . '" alt="' . esc_attr(sprintf(__('证明材料 %d', 'jio'), $index + 1)) . '"><span>' . esc_html($label) . '</span></a></li>';
}
echo '</ul>';
}
@@ -95,30 +95,30 @@ trait UsersMenusVerifyDialogs {
}
private function render_verify_review_fields(array $row): void {
echo '<section class="aksrt-verify-review"><h3>' . esc_html__('审核处理', 'aksrt') . '</h3>';
echo '<fieldset class="aksrt-verify-decisions"><legend>' . esc_html__('审核结果(必选)', 'aksrt') . '</legend>';
foreach (['verified' => __('通过申请', 'aksrt'), 'rejected' => __('拒绝申请', 'aksrt')] as $status => $label) {
echo '<section class="jio-verify-review"><h3>' . esc_html__('审核处理', 'jio') . '</h3>';
echo '<fieldset class="jio-verify-decisions"><legend>' . esc_html__('审核结果(必选)', 'jio') . '</legend>';
foreach (['verified' => __('通过申请', 'jio'), 'rejected' => __('拒绝申请', 'jio')] as $status => $label) {
echo '<label><input type="radio" name="status" value="' . esc_attr($status) . '" required><span>' . esc_html($label) . '</span></label>';
}
echo '</fieldset><fieldset class="aksrt-verify-profile" data-aksrt-verify-profile hidden disabled><legend>' . esc_html__('通过后公开展示的认证资料', 'aksrt') . '</legend>';
echo '<label class="aksrt-native-field"><span class="aksrt-native-field__label">' . esc_html__('认证名称(必填)', 'aksrt') . '</span>';
echo '<input type="text" name="name" maxlength="64" value="' . esc_attr((string) $row['public_name']) . '" required data-required-error="' . esc_attr__('请填写认证名称', 'aksrt') . '"></label>';
echo '<label class="aksrt-native-field"><span class="aksrt-native-field__label">' . esc_html__('认证简介', 'aksrt') . '</span><textarea name="description" rows="3">' . esc_textarea((string) $row['description']) . '</textarea></label>';
echo '<p class="aksrt-native-field__hint">' . esc_html__('通过审核前可修订名称与简介,原申请说明和证明材料不会被修改。', 'aksrt') . '</p></fieldset>';
echo '<label class="aksrt-native-field"><span class="aksrt-native-field__label" data-aksrt-verify-note-label data-optional="' . esc_attr__('处理留言(选填)', 'aksrt') . '" data-required="' . esc_attr__('拒绝原因(必填)', 'aksrt') . '">' . esc_html__('处理留言(选填)', 'aksrt') . '</span>';
echo '<textarea name="note" rows="3" data-required-error="' . esc_attr__('请填写拒绝原因', 'aksrt') . '" aria-describedby="aksrt-verify-note-hint"></textarea></label>';
echo '<p id="aksrt-verify-note-hint" class="aksrt-native-field__hint">' . esc_html__('审核结果会通知申请用户;拒绝时请说明原因,便于用户修改后重新申请。', 'aksrt') . '</p></section>';
echo '</fieldset><fieldset class="jio-verify-profile" data-jio-verify-profile hidden disabled><legend>' . esc_html__('通过后公开展示的认证资料', 'jio') . '</legend>';
echo '<label class="jio-native-field"><span class="jio-native-field__label">' . esc_html__('认证名称(必填)', 'jio') . '</span>';
echo '<input type="text" name="name" maxlength="64" value="' . esc_attr((string) $row['public_name']) . '" required data-required-error="' . esc_attr__('请填写认证名称', 'jio') . '"></label>';
echo '<label class="jio-native-field"><span class="jio-native-field__label">' . esc_html__('认证简介', 'jio') . '</span><textarea name="description" rows="3">' . esc_textarea((string) $row['description']) . '</textarea></label>';
echo '<p class="jio-native-field__hint">' . esc_html__('通过审核前可修订名称与简介,原申请说明和证明材料不会被修改。', 'jio') . '</p></fieldset>';
echo '<label class="jio-native-field"><span class="jio-native-field__label" data-jio-verify-note-label data-optional="' . esc_attr__('处理留言(选填)', 'jio') . '" data-required="' . esc_attr__('拒绝原因(必填)', 'jio') . '">' . esc_html__('处理留言(选填)', 'jio') . '</span>';
echo '<textarea name="note" rows="3" data-required-error="' . esc_attr__('请填写拒绝原因', 'jio') . '" aria-describedby="jio-verify-note-hint"></textarea></label>';
echo '<p id="jio-verify-note-hint" class="jio-native-field__hint">' . esc_html__('审核结果会通知申请用户;拒绝时请说明原因,便于用户修改后重新申请。', 'jio') . '</p></section>';
}
private function render_verify_delete_template(array $row, array $filters, int $page): void {
echo '<template id="aksrt-verify-delete-' . (int) $row['id'] . '"><form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" class="aksrt-verify-dialog__frame" data-aksrt-verify-delete-form>';
$this->render_verify_context($row, $filters, $page, 'aksrt_users_delete_verify');
$this->render_verify_dialog_head(__('删除认证申请记录', 'aksrt'), __('请核对申请信息。此操作不可撤销。', 'aksrt'));
echo '<div class="aksrt-verify-dialog__body"><div class="aksrt-verify-applicant">' . NativeUi::user_cell((int) $row['user_id'], (string) $row['display_name']); // phpcs:ignore WordPress.Security.EscapeOutput -- builder escapes.
echo '<template id="jio-verify-delete-' . (int) $row['id'] . '"><form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" class="jio-verify-dialog__frame" data-jio-verify-delete-form>';
$this->render_verify_context($row, $filters, $page, 'jio_users_delete_verify');
$this->render_verify_dialog_head(__('删除认证申请记录', 'jio'), __('请核对申请信息。此操作不可撤销。', 'jio'));
echo '<div class="jio-verify-dialog__body"><div class="jio-verify-applicant">' . NativeUi::user_cell((int) $row['user_id'], (string) $row['display_name']); // phpcs:ignore WordPress.Security.EscapeOutput -- builder escapes.
echo $this->verify_status_badge((string) $row['status']) . '</div>'; // phpcs:ignore WordPress.Security.EscapeOutput -- builder escapes.
$this->render_verify_text_section(__('认证名称', 'aksrt'), (string) $row['public_name']);
echo '<p class="aksrt-native-callout is-danger">' . esc_html__('将永久删除该申请记录、证明材料及审核事件。删除记录不会撤销用户已获得的认证。', 'aksrt') . '</p>';
echo '</div><div class="aksrt-verify-dialog__foot"><button type="button" class="button" data-aksrt-dialog-close autofocus>' . esc_html__('取消', 'aksrt') . '</button>';
echo '<button type="submit" class="button is-danger" data-aksrt-verify-submit data-idle-label="' . esc_attr__('确认删除', 'aksrt') . '" data-busy-label="' . esc_attr__('正在提交…', 'aksrt') . '">' . esc_html__('确认删除', 'aksrt') . '</button></div></form></template>';
$this->render_verify_text_section(__('认证名称', 'jio'), (string) $row['public_name']);
echo '<p class="jio-native-callout is-danger">' . esc_html__('将永久删除该申请记录、证明材料及审核事件。删除记录不会撤销用户已获得的认证。', 'jio') . '</p>';
echo '</div><div class="jio-verify-dialog__foot"><button type="button" class="button" data-jio-dialog-close autofocus>' . esc_html__('取消', 'jio') . '</button>';
echo '<button type="submit" class="button is-danger" data-jio-verify-submit data-idle-label="' . esc_attr__('确认删除', 'jio') . '" data-busy-label="' . esc_attr__('正在提交…', 'jio') . '">' . esc_html__('确认删除', 'jio') . '</button></div></form></template>';
}
}
+35 -35
View File
@@ -1,15 +1,15 @@
<?php
/**
* 用户会员后台页(users.php → aksrt_user_vip):页面编排、写操作与查询。
* 用户会员后台页(users.php → jio_user_vip):页面编排、写操作与查询。
*
* 列表展示由 UsersMenusVipView 输出;所有会员变更都通过同一个弹窗表单提交到
* admin_post_aksrt_users_mutate_vip(单人调整与批量调整共用),交互脚本见
* admin_post_jio_users_mutate_vip(单人调整与批量调整共用),交互脚本见
* assets/js/admin-vip.js。
*
* @package AKSRT
* @package Jio
*/
namespace AKSRT\Admin\Native;
namespace Jio\Admin\Native;
defined('ABSPATH') || exit;
@@ -17,7 +17,7 @@ trait UsersMenusVip {
use UsersMenusVipView;
private const VIP_PAGE_SLUG = 'aksrt_user_vip';
private const VIP_PAGE_SLUG = 'jio_user_vip';
private const VIP_PER_PAGE = 20;
private const VIP_FILTER_KEYS = ['user_id', 'level', 'state', 'date_from', 'date_to', 'orderby', 'order'];
@@ -27,11 +27,11 @@ trait UsersMenusVip {
}
$css = '/assets/css/admin/vip.css';
$js = '/assets/js/admin-vip.js';
if (is_file(AKSRT_DIR . $css)) {
wp_enqueue_style('aksrt-admin-vip', AKSRT_URI . $css, [NativeUi::STYLE_HANDLE], (string) filemtime(AKSRT_DIR . $css));
if (is_file(JIO_DIR . $css)) {
wp_enqueue_style('jio-admin-vip', JIO_URI . $css, [NativeUi::STYLE_HANDLE], (string) filemtime(JIO_DIR . $css));
}
if (is_file(AKSRT_DIR . $js)) {
wp_enqueue_script('aksrt-admin-vip', AKSRT_URI . $js, [], (string) filemtime(AKSRT_DIR . $js), true);
if (is_file(JIO_DIR . $js)) {
wp_enqueue_script('jio-admin-vip', JIO_URI . $js, [], (string) filemtime(JIO_DIR . $js), true);
}
}
@@ -39,14 +39,14 @@ trait UsersMenusVip {
$this->ensure_cap();
$tab = $this->vip_tab();
$page = max(1, absint($_GET['paged'] ?? 1));
NativeUi::page_open('aksrt-vip');
NativeUi::page_open('jio-vip');
NativeUi::header(
__('用户会员', 'aksrt'),
__('查看会员档位与有效期,单人或批量调整会员;每次变更都会记录原因与结果。', 'aksrt'),
__('用户会员', 'jio'),
__('查看会员档位与有效期,单人或批量调整会员;每次变更都会记录原因与结果。', 'jio'),
'dashicons-awards',
[[
'label' => __('会员配置', 'aksrt'),
'url' => admin_url('admin.php?page=aksrt-settings#/payment/vip'),
'label' => __('会员配置', 'jio'),
'url' => admin_url('admin.php?page=jio-settings#/payment/vip'),
'icon' => 'dashicons-admin-generic',
]]
);
@@ -67,18 +67,18 @@ trait UsersMenusVip {
public function handle_mutate_vip(): void {
$this->ensure_cap();
check_admin_referer('aksrt_users_mutate_vip');
check_admin_referer('jio_users_mutate_vip');
$ids = isset($_POST['user_ids']) ? wp_unslash($_POST['user_ids']) : [];
$ids = is_array($ids) ? array_values(array_unique(array_filter(array_map('absint', $ids)))) : [];
if ($ids === [] || count($ids) > 100) {
$this->redirect_vip_result('vip_failed', ['aksrt_vip_error' => __('请选择 1 至 100 名用户。', 'aksrt')]);
$this->redirect_vip_result('vip_failed', ['jio_vip_error' => __('请选择 1 至 100 名用户。', 'jio')]);
}
$operation = sanitize_key((string) wp_unslash($_POST['vip_operation'] ?? ''));
$reason = sanitize_text_field((string) wp_unslash($_POST['reason'] ?? ''));
$payload = $this->vip_mutation_payload($operation);
if (is_wp_error($payload)) {
$this->redirect_vip_result('vip_failed', ['aksrt_vip_error' => $payload->get_error_message()]);
$this->redirect_vip_result('vip_failed', ['jio_vip_error' => $payload->get_error_message()]);
}
$batch = count($ids) > 1 ? wp_generate_uuid4() : '';
@@ -89,14 +89,14 @@ trait UsersMenusVip {
$skipped++;
continue;
}
$result = class_exists('\AKSRT\Content\VipService')
? \AKSRT\Content\VipService::get_instance()->mutate_membership($user_id, $operation, $payload, [
$result = class_exists('\Jio\Content\VipService')
? \Jio\Content\VipService::get_instance()->mutate_membership($user_id, $operation, $payload, [
'actor_user_id' => get_current_user_id(),
'source' => 'admin',
'reason' => $reason,
'batch_uuid' => $batch,
])
: new \WP_Error('aksrt_vip_unavailable', __('会员服务不可用。', 'aksrt'));
: new \WP_Error('jio_vip_unavailable', __('会员服务不可用。', 'jio'));
if (is_wp_error($result)) {
$failed++;
$last_error = $result->get_error_message();
@@ -105,12 +105,12 @@ trait UsersMenusVip {
}
}
if ($ok === 0) {
$this->redirect_vip_result('vip_failed', $last_error !== '' ? ['aksrt_vip_error' => $last_error] : []);
$this->redirect_vip_result('vip_failed', $last_error !== '' ? ['jio_vip_error' => $last_error] : []);
}
$this->redirect_vip_result($failed > 0 || $skipped > 0 ? 'vip_partial' : 'vip_updated', [
'aksrt_vip_ok' => $ok,
'aksrt_vip_failed_count' => $failed,
'aksrt_vip_skipped' => $skipped,
'jio_vip_ok' => $ok,
'jio_vip_failed_count' => $failed,
'jio_vip_skipped' => $skipped,
]);
}
@@ -164,13 +164,13 @@ trait UsersMenusVip {
private function vip_sql_parts(): array {
global $wpdb;
$meta = "(SELECT user_id,
MAX(CASE WHEN meta_key = 'aksrt_vip_level' THEN meta_value END) level,
MAX(CASE WHEN meta_key = 'aksrt_vip_expires' THEN meta_value END) expires,
MAX(CASE WHEN meta_key = 'aksrt_vip_base_level' THEN meta_value END) base_level,
MAX(CASE WHEN meta_key = 'aksrt_vip_base_expires' THEN meta_value END) base_expires,
MAX(CASE WHEN meta_key = 'aksrt_vip_level_expired' THEN meta_value END) expired_level
MAX(CASE WHEN meta_key = 'jio_vip_level' THEN meta_value END) level,
MAX(CASE WHEN meta_key = 'jio_vip_expires' THEN meta_value END) expires,
MAX(CASE WHEN meta_key = 'jio_vip_base_level' THEN meta_value END) base_level,
MAX(CASE WHEN meta_key = 'jio_vip_base_expires' THEN meta_value END) base_expires,
MAX(CASE WHEN meta_key = 'jio_vip_level_expired' THEN meta_value END) expired_level
FROM {$wpdb->usermeta}
WHERE meta_key IN ('aksrt_vip_level', 'aksrt_vip_expires', 'aksrt_vip_base_level', 'aksrt_vip_base_expires', 'aksrt_vip_level_expired')
WHERE meta_key IN ('jio_vip_level', 'jio_vip_expires', 'jio_vip_base_level', 'jio_vip_base_expires', 'jio_vip_level_expired')
GROUP BY user_id) m";
$raw_level = 'CAST(COALESCE(m.level, 0) AS UNSIGNED)';
$raw_expires = 'CAST(COALESCE(m.expires, 0) AS UNSIGNED)';
@@ -274,7 +274,7 @@ trait UsersMenusVip {
/** @return array<string,mixed>|\WP_Error */
private function vip_mutation_payload(string $operation): array|\WP_Error {
if (!in_array($operation, ['set', 'extend', 'disable', 'restore'], true)) {
return new \WP_Error('aksrt_vip_operation_invalid', __('会员操作无效。', 'aksrt'));
return new \WP_Error('jio_vip_operation_invalid', __('会员操作无效。', 'jio'));
}
if ($operation === 'disable' || $operation === 'restore') {
return [];
@@ -290,7 +290,7 @@ trait UsersMenusVip {
}
$date = $this->vip_date($_POST['expires_date'] ?? '');
if ($date === '') {
return new \WP_Error('aksrt_vip_expiry_required', __('请设置永久会员或有效的到期日期。', 'aksrt'));
return new \WP_Error('jio_vip_expiry_required', __('请设置永久会员或有效的到期日期。', 'jio'));
}
$expires = (new \DateTimeImmutable($date . ' 23:59:59', wp_timezone()))->getTimestamp();
return ['level' => absint($_POST['level'] ?? 0), 'expires' => $expires];
@@ -298,10 +298,10 @@ trait UsersMenusVip {
/** @return array<int,string> */
private function vip_level_names(): array {
$levels = class_exists('\AKSRT\Settings\Settings') ? (array) (\AKSRT\Settings\Settings::get_payment_vip_settings()['levels'] ?? []) : [];
$levels = class_exists('\Jio\Settings\Settings') ? (array) (\Jio\Settings\Settings::get_payment_vip_settings()['levels'] ?? []) : [];
return [
1 => (string) ($levels[1]['name'] ?? __('一级会员', 'aksrt')),
2 => (string) ($levels[2]['name'] ?? __('二级会员', 'aksrt')),
1 => (string) ($levels[1]['name'] ?? __('一级会员', 'jio')),
2 => (string) ($levels[2]['name'] ?? __('二级会员', 'jio')),
];
}
+113 -113
View File
@@ -5,10 +5,10 @@
* 弹窗是页面上唯一的写表单:行内「调整」与工具栏「批量调整」都只负责把目标用户
* 填进弹窗(assets/js/admin-vip.js),提交由 UsersMenusVip::handle_mutate_vip() 处理。
*
* @package AKSRT
* @package Jio
*/
namespace AKSRT\Admin\Native;
namespace Jio\Admin\Native;
defined('ABSPATH') || exit;
@@ -21,29 +21,29 @@ trait UsersMenusVipView {
$base = admin_url('users.php?page=' . self::VIP_PAGE_SLUG);
NativeUi::stats([
[
'label' => __('限时会员', 'aksrt'),
'label' => __('限时会员', 'jio'),
'value' => $stats['active'],
'url' => add_query_arg('state', 'active', $base),
'icon' => 'dashicons-clock',
'hint' => __('有效期内', 'aksrt'),
'hint' => __('有效期内', 'jio'),
],
[
'label' => __('永久会员', 'aksrt'),
'label' => __('永久会员', 'jio'),
'value' => $stats['permanent'],
'url' => add_query_arg('state', 'permanent', $base),
'tone' => 'primary',
'icon' => 'dashicons-awards',
],
[
'label' => __('7 天内到期', 'aksrt'),
'label' => __('7 天内到期', 'jio'),
'value' => $stats['expiring'],
'url' => add_query_arg(['state' => 'active', 'date_from' => $window['from'], 'date_to' => $window['to'], 'orderby' => 'expires', 'order' => 'ASC'], $base),
'tone' => $stats['expiring'] > 0 ? 'warning' : 'neutral',
'icon' => 'dashicons-warning',
'hint' => __('建议提醒续费', 'aksrt'),
'hint' => __('建议提醒续费', 'jio'),
],
[
'label' => __('已过期', 'aksrt'),
'label' => __('已过期', 'jio'),
'value' => $stats['expired'],
'url' => add_query_arg('state', 'expired', $base),
'tone' => 'neutral',
@@ -54,14 +54,14 @@ trait UsersMenusVipView {
'value' => $stats['level_1'],
'tone' => 'info',
'icon' => 'dashicons-star-half',
'hint' => __('当前有效人数', 'aksrt'),
'hint' => __('当前有效人数', 'jio'),
],
[
'label' => $names[2],
'value' => $stats['level_2'],
'tone' => 'info',
'icon' => 'dashicons-star-filled',
'hint' => __('当前有效人数', 'aksrt'),
'hint' => __('当前有效人数', 'jio'),
],
]);
}
@@ -69,13 +69,13 @@ trait UsersMenusVipView {
private function render_vip_tabs(string $tab): void {
$base = admin_url('users.php?page=' . self::VIP_PAGE_SLUG);
$tabs = [
'members' => [__('会员列表', 'aksrt'), $base, 'dashicons-groups'],
'history' => [__('变更记录', 'aksrt'), add_query_arg('tab', 'history', $base), 'dashicons-backup'],
'members' => [__('会员列表', 'jio'), $base, 'dashicons-groups'],
'history' => [__('变更记录', 'jio'), add_query_arg('tab', 'history', $base), 'dashicons-backup'],
];
echo '<nav class="aksrt-native-tabs" aria-label="' . esc_attr__('会员视图', 'aksrt') . '">';
echo '<nav class="jio-native-tabs" aria-label="' . esc_attr__('会员视图', 'jio') . '">';
foreach ($tabs as $key => [$label, $url, $icon]) {
$current = $key === $tab;
echo '<a class="aksrt-native-tabs__link' . ($current ? ' is-active' : '') . '" href="' . esc_url($url) . '"' . ($current ? ' aria-current="page"' : '') . '>'
echo '<a class="jio-native-tabs__link' . ($current ? ' is-active' : '') . '" href="' . esc_url($url) . '"' . ($current ? ' aria-current="page"' : '') . '>'
. '<span class="dashicons ' . esc_attr($icon) . '" aria-hidden="true"></span>' . esc_html($label) . '</a>';
}
echo '</nav>';
@@ -83,25 +83,25 @@ trait UsersMenusVipView {
private function render_vip_filters(array $filters): void {
$names = $this->vip_level_names();
NativeUi::card_open('', '', false, 'aksrt-native-toolbar aksrt-vip-filters');
echo '<form method="get" class="aksrt-native-filters">';
NativeUi::card_open('', '', false, 'jio-native-toolbar jio-vip-filters');
echo '<form method="get" class="jio-native-filters">';
echo '<input type="hidden" name="page" value="' . esc_attr(self::VIP_PAGE_SLUG) . '">';
echo '<input type="hidden" name="orderby" value="' . esc_attr($filters['orderby']) . '"><input type="hidden" name="order" value="' . esc_attr($filters['order']) . '">';
echo '<label>UID<input type="number" min="1" name="user_id" class="small-text" value="' . esc_attr($filters['user_id'] > 0 ? (string) $filters['user_id'] : '') . '" placeholder="' . esc_attr__('全部', 'aksrt') . '"></label>';
echo '<label>' . esc_html__('档位', 'aksrt') . '<select name="level">';
foreach (['all' => __('全部', 'aksrt'), '0' => __('普通用户', 'aksrt'), '1' => $names[1], '2' => $names[2]] as $key => $label) {
echo '<label>UID<input type="number" min="1" name="user_id" class="small-text" value="' . esc_attr($filters['user_id'] > 0 ? (string) $filters['user_id'] : '') . '" placeholder="' . esc_attr__('全部', 'jio') . '"></label>';
echo '<label>' . esc_html__('档位', 'jio') . '<select name="level">';
foreach (['all' => __('全部', 'jio'), '0' => __('普通用户', 'jio'), '1' => $names[1], '2' => $names[2]] as $key => $label) {
printf('<option value="%s"%s>%s</option>', esc_attr((string) $key), selected($filters['level'], (string) $key, false), esc_html($label));
}
echo '</select></label><label>' . esc_html__('状态', 'aksrt') . '<select name="state">';
echo '</select></label><label>' . esc_html__('状态', 'jio') . '<select name="state">';
foreach ($this->vip_state_labels() as $key => $label) {
printf('<option value="%s"%s>%s</option>', esc_attr($key), selected($filters['state'], $key, false), esc_html($label));
}
echo '</select></label>';
echo '<label>' . esc_html__('到期日', 'aksrt') . '<input type="date" name="date_from" value="' . esc_attr($filters['date_from']) . '" aria-label="' . esc_attr__('到期日起', 'aksrt') . '">'
. '<span aria-hidden="true">—</span><input type="date" name="date_to" value="' . esc_attr($filters['date_to']) . '" aria-label="' . esc_attr__('到期日止', 'aksrt') . '"></label>';
echo '<button type="submit" class="button button-primary">' . esc_html__('筛选', 'aksrt') . '</button>';
echo '<label>' . esc_html__('到期日', 'jio') . '<input type="date" name="date_from" value="' . esc_attr($filters['date_from']) . '" aria-label="' . esc_attr__('到期日起', 'jio') . '">'
. '<span aria-hidden="true">—</span><input type="date" name="date_to" value="' . esc_attr($filters['date_to']) . '" aria-label="' . esc_attr__('到期日止', 'jio') . '"></label>';
echo '<button type="submit" class="button button-primary">' . esc_html__('筛选', 'jio') . '</button>';
if ($this->vip_filters_active($filters)) {
echo '<a class="button" href="' . esc_url(admin_url('users.php?page=' . self::VIP_PAGE_SLUG)) . '">' . esc_html__('重置', 'aksrt') . '</a>';
echo '<a class="button" href="' . esc_url(admin_url('users.php?page=' . self::VIP_PAGE_SLUG)) . '">' . esc_html__('重置', 'jio') . '</a>';
}
echo '</form>';
NativeUi::card_close();
@@ -110,28 +110,28 @@ trait UsersMenusVipView {
/** @return array<string,string> */
private function vip_state_labels(): array {
return [
'all' => __('全部', 'aksrt'),
'active' => __('限时有效', 'aksrt'),
'permanent' => __('永久', 'aksrt'),
'expired' => __('已过期', 'aksrt'),
'none' => __('无会员', 'aksrt'),
'all' => __('全部', 'jio'),
'active' => __('限时有效', 'jio'),
'permanent' => __('永久', 'jio'),
'expired' => __('已过期', 'jio'),
'none' => __('无会员', 'jio'),
];
}
private function render_vip_table(array $items, array $filters, int $pages, int $page): void {
$actions = '<span class="aksrt-vip-selection" data-aksrt-vip-selection hidden></span>'
. '<button type="button" class="button button-primary" data-aksrt-vip-bulk disabled>'
. '<span class="dashicons dashicons-edit" aria-hidden="true"></span>' . esc_html__('批量调整', 'aksrt') . '</button>';
NativeUi::table_card_open(__('会员列表', 'aksrt'), $actions);
echo '<table class="wp-list-table widefat fixed striped aksrt-vip-table"><thead><tr>';
echo '<td class="manage-column column-cb check-column"><input type="checkbox" data-aksrt-vip-check-all aria-label="' . esc_attr__('选择本页全部用户', 'aksrt') . '"></td>';
echo '<th class="column-user">' . esc_html__('用户', 'aksrt') . '</th>';
echo '<th>' . esc_html__('当前会员', 'aksrt') . '</th>';
$actions = '<span class="jio-vip-selection" data-jio-vip-selection hidden></span>'
. '<button type="button" class="button button-primary" data-jio-vip-bulk disabled>'
. '<span class="dashicons dashicons-edit" aria-hidden="true"></span>' . esc_html__('批量调整', 'jio') . '</button>';
NativeUi::table_card_open(__('会员列表', 'jio'), $actions);
echo '<table class="wp-list-table widefat fixed striped jio-vip-table"><thead><tr>';
echo '<td class="manage-column column-cb check-column"><input type="checkbox" data-jio-vip-check-all aria-label="' . esc_attr__('选择本页全部用户', 'jio') . '"></td>';
echo '<th class="column-user">' . esc_html__('用户', 'jio') . '</th>';
echo '<th>' . esc_html__('当前会员', 'jio') . '</th>';
echo '<th>' . $this->vip_sort_link($filters) . '</th>'; // phpcs:ignore WordPress.Security.EscapeOutput -- builder escapes.
echo '<th class="column-actions">' . esc_html__('操作', 'aksrt') . '</th></tr></thead><tbody>';
echo '<th class="column-actions">' . esc_html__('操作', 'jio') . '</th></tr></thead><tbody>';
if ($items === []) {
$hint = $this->vip_filters_active($filters) ? __('试试调整或重置筛选条件。', 'aksrt') : '';
echo NativeUi::empty_row(5, __('没有符合条件的用户', 'aksrt'), $hint); // phpcs:ignore WordPress.Security.EscapeOutput -- builder escapes.
$hint = $this->vip_filters_active($filters) ? __('试试调整或重置筛选条件。', 'jio') : '';
echo NativeUi::empty_row(5, __('没有符合条件的用户', 'jio'), $hint); // phpcs:ignore WordPress.Security.EscapeOutput -- builder escapes.
}
foreach ($items as $row) {
$this->render_vip_row($row);
@@ -146,8 +146,8 @@ trait UsersMenusVipView {
$active = $filters['orderby'] === 'expires';
$next = $active && $filters['order'] === 'ASC' ? 'DESC' : 'ASC';
$state = $active ? ($filters['order'] === 'ASC' ? ' is-asc' : ' is-desc') : '';
return '<a class="aksrt-vip-sort' . $state . '" href="' . esc_url($this->vip_list_url($filters, ['orderby' => 'expires', 'order' => $next])) . '">'
. esc_html__('到期时间', 'aksrt') . '<span class="aksrt-vip-sort__icon" aria-hidden="true"><i></i><i></i></span></a>';
return '<a class="jio-vip-sort' . $state . '" href="' . esc_url($this->vip_list_url($filters, ['orderby' => 'expires', 'order' => $next])) . '">'
. esc_html__('到期时间', 'jio') . '<span class="jio-vip-sort__icon" aria-hidden="true"><i></i><i></i></span></a>';
}
private function render_vip_row(array $row): void {
@@ -160,27 +160,27 @@ trait UsersMenusVipView {
$fell_back = $level > 0 && ((int) $row['raw_level'] !== $level || (int) $row['raw_expires'] !== $expires);
$base_level = (int) $row['base_level'];
$base_expires = (int) $row['base_expires'];
$label = $level > 0 ? ($names[$level] ?? ('VIP' . $level)) : __('普通用户', 'aksrt');
$label = $level > 0 ? ($names[$level] ?? ('VIP' . $level)) : __('普通用户', 'jio');
if ($expired) {
$lapsed = max((int) $row['raw_level'], (int) $row['expired_level']);
$badge = NativeUi::badge(sprintf(__('%s · 已过期', 'aksrt'), $names[$lapsed] ?? __('会员', 'aksrt')), 'neutral');
$badge = NativeUi::badge(sprintf(__('%s · 已过期', 'jio'), $names[$lapsed] ?? __('会员', 'jio')), 'neutral');
} else {
$badge = NativeUi::badge($label, $level > 0 ? ($fell_back ? 'warning' : ($expires === 0 ? 'primary' : 'success')) : 'neutral', $level > 0);
}
$note = '';
if ($fell_back) {
$note = __('限时档位已到期,按基础档位计', 'aksrt');
$note = __('限时档位已到期,按基础档位计', 'jio');
} elseif ($level > 0 && $base_level > 0) {
$note = sprintf(__('到期后回退:%s', 'aksrt'), $names[$base_level] ?? ('VIP' . $base_level));
$note = sprintf(__('到期后回退:%s', 'jio'), $names[$base_level] ?? ('VIP' . $base_level));
}
$expiry_label = $this->vip_expiry_label($level, $expires, $expired);
$expiry_html = esc_html($expiry_label);
if ($level > 0 && $expires > 0 && !$expired) {
$days = (int) ceil(($expires - $now) / DAY_IN_SECONDS);
$expiry_html .= '<span class="aksrt-vip-remaining ' . ($days <= 7 ? 'aksrt-native-text--warning' : 'aksrt-native-muted') . '">'
. esc_html(sprintf(__('剩余 %s', 'aksrt'), human_time_diff($now, $expires))) . '</span>';
$expiry_html .= '<span class="jio-vip-remaining ' . ($days <= 7 ? 'jio-native-text--warning' : 'jio-native-muted') . '">'
. esc_html(sprintf(__('剩余 %s', 'jio'), human_time_diff($now, $expires))) . '</span>';
}
$can_extend = $level > 0 && $expires > 0 && !$expired;
@@ -189,7 +189,7 @@ trait UsersMenusVipView {
$data = [
'user-id' => (string) $id,
'user-name' => $user_name,
'current' => $expired ? __('已过期', 'aksrt') : $label . ($level > 0 ? ' · ' . $expiry_label : ''),
'current' => $expired ? __('已过期', 'jio') : $label . ($level > 0 ? ' · ' . $expiry_label : ''),
'level' => (string) max(1, $level),
'can-extend' => $can_extend ? '1' : '0',
'can-restore' => $can_restore ? '1' : '0',
@@ -200,130 +200,130 @@ trait UsersMenusVipView {
}
echo '<tr>';
echo '<th scope="row" class="check-column"><input type="checkbox" class="aksrt-vip-check" value="' . esc_attr((string) $id) . '"' . $attrs . ' aria-label="' . esc_attr(sprintf(__('选择 %s', 'aksrt'), $user_name)) . '"></th>'; // phpcs:ignore WordPress.Security.EscapeOutput -- attrs escaped above.
echo '<td class="column-user">' . NativeUi::user_cell($id, $user_name) . '<span class="aksrt-native-muted aksrt-vip-login">@' . esc_html((string) $row['user_login']) . '</span></td>'; // phpcs:ignore WordPress.Security.EscapeOutput -- builder escapes.
echo '<td>' . $badge . ($note !== '' ? '<span class="aksrt-vip-note aksrt-native-muted">' . esc_html($note) . '</span>' : '') . '</td>'; // phpcs:ignore WordPress.Security.EscapeOutput -- builder escapes.
echo '<td class="aksrt-vip-expiry">' . $expiry_html . '</td>'; // phpcs:ignore WordPress.Security.EscapeOutput -- escaped above.
echo '<td class="column-actions"><button type="button" class="button button-small" data-aksrt-vip-edit' . $attrs . '>' // phpcs:ignore WordPress.Security.EscapeOutput -- attrs escaped above.
. '<span class="dashicons dashicons-edit" aria-hidden="true"></span>' . esc_html__('调整', 'aksrt') . '</button></td>';
echo '<th scope="row" class="check-column"><input type="checkbox" class="jio-vip-check" value="' . esc_attr((string) $id) . '"' . $attrs . ' aria-label="' . esc_attr(sprintf(__('选择 %s', 'jio'), $user_name)) . '"></th>'; // phpcs:ignore WordPress.Security.EscapeOutput -- attrs escaped above.
echo '<td class="column-user">' . NativeUi::user_cell($id, $user_name) . '<span class="jio-native-muted jio-vip-login">@' . esc_html((string) $row['user_login']) . '</span></td>'; // phpcs:ignore WordPress.Security.EscapeOutput -- builder escapes.
echo '<td>' . $badge . ($note !== '' ? '<span class="jio-vip-note jio-native-muted">' . esc_html($note) . '</span>' : '') . '</td>'; // phpcs:ignore WordPress.Security.EscapeOutput -- builder escapes.
echo '<td class="jio-vip-expiry">' . $expiry_html . '</td>'; // phpcs:ignore WordPress.Security.EscapeOutput -- escaped above.
echo '<td class="column-actions"><button type="button" class="button button-small" data-jio-vip-edit' . $attrs . '>' // phpcs:ignore WordPress.Security.EscapeOutput -- attrs escaped above.
. '<span class="dashicons dashicons-edit" aria-hidden="true"></span>' . esc_html__('调整', 'jio') . '</button></td>';
echo '</tr>';
}
private function vip_expiry_label(int $level, int $expires, bool $expired): string {
if ($expired) {
return __('已过期', 'aksrt');
return __('已过期', 'jio');
}
if ($level <= 0) {
return '—';
}
return $expires === 0 ? __('永久有效', 'aksrt') : wp_date('Y-m-d H:i', $expires, wp_timezone());
return $expires === 0 ? __('永久有效', 'jio') : wp_date('Y-m-d H:i', $expires, wp_timezone());
}
private function render_vip_dialog(array $filters): void {
$names = $this->vip_level_names();
$today = (new \DateTimeImmutable('today', wp_timezone()))->format('Y-m-d');
$strings = [
'single' => __('调整会员:%s', 'aksrt'),
'bulk' => __('批量调整会员(%d 人)', 'aksrt'),
'selected' => __('已选 %d 人', 'aksrt'),
'bulk-note' => __('不满足条件的用户会被跳过并计入失败,结果会在提交后汇总显示。', 'aksrt'),
'extend-off' => __('该用户不是限时有效会员,无法延长。', 'aksrt'),
'restore-off' => __('该用户没有可恢复的回退会员。', 'aksrt'),
'single' => __('调整会员:%s', 'jio'),
'bulk' => __('批量调整会员(%d 人)', 'jio'),
'selected' => __('已选 %d 人', 'jio'),
'bulk-note' => __('不满足条件的用户会被跳过并计入失败,结果会在提交后汇总显示。', 'jio'),
'extend-off' => __('该用户不是限时有效会员,无法延长。', 'jio'),
'restore-off' => __('该用户没有可恢复的回退会员。', 'jio'),
];
$attrs = '';
foreach ($strings as $key => $value) {
$attrs .= ' data-i18n-' . $key . '="' . esc_attr($value) . '"';
}
echo '<dialog class="aksrt-vip-dialog" id="aksrt-vip-dialog" aria-labelledby="aksrt-vip-dialog-title"' . $attrs . '>'; // phpcs:ignore WordPress.Security.EscapeOutput -- attrs escaped above.
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" class="aksrt-vip-dialog__form">';
echo '<input type="hidden" name="action" value="aksrt_users_mutate_vip">';
wp_nonce_field('aksrt_users_mutate_vip');
echo '<dialog class="jio-vip-dialog" id="jio-vip-dialog" aria-labelledby="jio-vip-dialog-title"' . $attrs . '>'; // phpcs:ignore WordPress.Security.EscapeOutput -- attrs escaped above.
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" class="jio-vip-dialog__form">';
echo '<input type="hidden" name="action" value="jio_users_mutate_vip">';
wp_nonce_field('jio_users_mutate_vip');
$this->render_vip_return_fields($filters);
echo '<div data-aksrt-vip-ids></div>';
echo '<div data-jio-vip-ids></div>';
echo '<header class="aksrt-vip-dialog__head"><h2 id="aksrt-vip-dialog-title" class="aksrt-vip-dialog__title" data-aksrt-vip-title></h2>'
. '<button type="button" class="aksrt-vip-dialog__close" data-aksrt-vip-close aria-label="' . esc_attr__('关闭', 'aksrt') . '"><span class="dashicons dashicons-no-alt" aria-hidden="true"></span></button></header>';
echo '<div class="aksrt-vip-dialog__body">';
echo '<p class="aksrt-vip-dialog__target" data-aksrt-vip-target></p>';
echo '<header class="jio-vip-dialog__head"><h2 id="jio-vip-dialog-title" class="jio-vip-dialog__title" data-jio-vip-title></h2>'
. '<button type="button" class="jio-vip-dialog__close" data-jio-vip-close aria-label="' . esc_attr__('关闭', 'jio') . '"><span class="dashicons dashicons-no-alt" aria-hidden="true"></span></button></header>';
echo '<div class="jio-vip-dialog__body">';
echo '<p class="jio-vip-dialog__target" data-jio-vip-target></p>';
echo '<fieldset class="aksrt-vip-ops"><legend class="aksrt-native-field__label">' . esc_html__('操作', 'aksrt') . '</legend>';
echo '<fieldset class="jio-vip-ops"><legend class="jio-native-field__label">' . esc_html__('操作', 'jio') . '</legend>';
$ops = [
'set' => [__('设置会员', 'aksrt'), 'dashicons-awards'],
'extend' => [__('延长有效期', 'aksrt'), 'dashicons-clock'],
'restore' => [__('恢复回退会员', 'aksrt'), 'dashicons-undo'],
'disable' => [__('停用会员', 'aksrt'), 'dashicons-dismiss'],
'set' => [__('设置会员', 'jio'), 'dashicons-awards'],
'extend' => [__('延长有效期', 'jio'), 'dashicons-clock'],
'restore' => [__('恢复回退会员', 'jio'), 'dashicons-undo'],
'disable' => [__('停用会员', 'jio'), 'dashicons-dismiss'],
];
foreach ($ops as $key => [$label, $icon]) {
echo '<label class="aksrt-vip-op' . ($key === 'disable' ? ' is-danger' : '') . '"><input type="radio" name="vip_operation" value="' . esc_attr($key) . '"' . checked($key, 'set', false) . '>'
echo '<label class="jio-vip-op' . ($key === 'disable' ? ' is-danger' : '') . '"><input type="radio" name="vip_operation" value="' . esc_attr($key) . '"' . checked($key, 'set', false) . '>'
. '<span><span class="dashicons ' . esc_attr($icon) . '" aria-hidden="true"></span>' . esc_html($label) . '</span></label>';
}
echo '</fieldset>';
echo '<p class="aksrt-vip-op-hint aksrt-native-muted" data-aksrt-vip-op-hint hidden></p>';
echo '<p class="jio-vip-op-hint jio-native-muted" data-jio-vip-op-hint hidden></p>';
echo '<div class="aksrt-native-form-grid aksrt-vip-pane" data-aksrt-vip-pane="set">';
echo '<label class="aksrt-native-field"><span class="aksrt-native-field__label">' . esc_html__('会员档位', 'aksrt') . '</span><select name="level" data-aksrt-vip-level>';
echo '<div class="jio-native-form-grid jio-vip-pane" data-jio-vip-pane="set">';
echo '<label class="jio-native-field"><span class="jio-native-field__label">' . esc_html__('会员档位', 'jio') . '</span><select name="level" data-jio-vip-level>';
foreach ($names as $value => $label) {
echo '<option value="' . esc_attr((string) $value) . '">' . esc_html($label) . '</option>';
}
echo '</select></label>';
echo '<label class="aksrt-native-field"><span class="aksrt-native-field__label">' . esc_html__('到期日', 'aksrt') . '</span><input type="date" name="expires_date" min="' . esc_attr($today) . '" required data-aksrt-vip-date>'
. '<span class="aksrt-native-field__hint">' . esc_html__('当天 23:59:59 到期', 'aksrt') . '</span></label>';
echo '<label class="aksrt-vip-switch is-wide"><input type="checkbox" name="permanent" value="1" data-aksrt-vip-permanent>' . esc_html__('永久会员(不设到期日)', 'aksrt') . '</label>';
echo '<p class="aksrt-native-field__hint is-wide">' . esc_html__('设置会员会直接覆盖当前档位与到期时间,并清除回退会员。', 'aksrt') . '</p>';
echo '<label class="jio-native-field"><span class="jio-native-field__label">' . esc_html__('到期日', 'jio') . '</span><input type="date" name="expires_date" min="' . esc_attr($today) . '" required data-jio-vip-date>'
. '<span class="jio-native-field__hint">' . esc_html__('当天 23:59:59 到期', 'jio') . '</span></label>';
echo '<label class="jio-vip-switch is-wide"><input type="checkbox" name="permanent" value="1" data-jio-vip-permanent>' . esc_html__('永久会员(不设到期日)', 'jio') . '</label>';
echo '<p class="jio-native-field__hint is-wide">' . esc_html__('设置会员会直接覆盖当前档位与到期时间,并清除回退会员。', 'jio') . '</p>';
echo '</div>';
echo '<div class="aksrt-native-form-grid aksrt-vip-pane" data-aksrt-vip-pane="extend" hidden>';
echo '<div class="aksrt-native-field is-wide"><span class="aksrt-native-field__label">' . esc_html__('延长时长', 'aksrt') . '</span><div class="aksrt-vip-duration">'
. '<input type="number" name="amount" min="1" max="3650" value="30" required aria-label="' . esc_attr__('延长数量', 'aksrt') . '">'
. '<select name="unit" aria-label="' . esc_attr__('延长单位', 'aksrt') . '"><option value="day">' . esc_html__('天', 'aksrt') . '</option><option value="month">' . esc_html__('月', 'aksrt') . '</option></select></div>'
. '<span class="aksrt-native-field__hint">' . esc_html__('从当前到期时间起顺延;仅对限时有效会员生效。', 'aksrt') . '</span></div>';
echo '<div class="jio-native-form-grid jio-vip-pane" data-jio-vip-pane="extend" hidden>';
echo '<div class="jio-native-field is-wide"><span class="jio-native-field__label">' . esc_html__('延长时长', 'jio') . '</span><div class="jio-vip-duration">'
. '<input type="number" name="amount" min="1" max="3650" value="30" required aria-label="' . esc_attr__('延长数量', 'jio') . '">'
. '<select name="unit" aria-label="' . esc_attr__('延长单位', 'jio') . '"><option value="day">' . esc_html__('天', 'jio') . '</option><option value="month">' . esc_html__('月', 'jio') . '</option></select></div>'
. '<span class="jio-native-field__hint">' . esc_html__('从当前到期时间起顺延;仅对限时有效会员生效。', 'jio') . '</span></div>';
echo '</div>';
echo '<div class="aksrt-native-callout is-warning aksrt-vip-pane" data-aksrt-vip-pane="restore" hidden>' . esc_html__('将取消当前限时升级,恢复为回退会员(档位与到期时间按升级前保存的记录)。', 'aksrt') . '</div>';
echo '<div class="aksrt-native-callout is-danger aksrt-vip-pane" data-aksrt-vip-pane="disable" hidden>' . esc_html__('停用后立即撤销前端会员权益,并清除回退会员记录。此操作无法撤销,只能重新设置会员。', 'aksrt') . '</div>';
echo '<div class="jio-native-callout is-warning jio-vip-pane" data-jio-vip-pane="restore" hidden>' . esc_html__('将取消当前限时升级,恢复为回退会员(档位与到期时间按升级前保存的记录)。', 'jio') . '</div>';
echo '<div class="jio-native-callout is-danger jio-vip-pane" data-jio-vip-pane="disable" hidden>' . esc_html__('停用后立即撤销前端会员权益,并清除回退会员记录。此操作无法撤销,只能重新设置会员。', 'jio') . '</div>';
echo '<label class="aksrt-native-field aksrt-vip-reason"><span class="aksrt-native-field__label">' . esc_html__('变更原因', 'aksrt') . ' <span class="aksrt-native-text--danger" aria-hidden="true">*</span></span>'
. '<input type="text" name="reason" required maxlength="255" placeholder="' . esc_attr__('例如:活动赠送、客服补偿、退款撤销', 'aksrt') . '">'
. '<span class="aksrt-native-field__hint">' . esc_html__('写入变更记录,便于后续追溯。', 'aksrt') . '</span></label>';
echo '<p class="aksrt-native-muted aksrt-vip-bulk-note" data-aksrt-vip-bulk-note hidden></p>';
echo '<label class="jio-native-field jio-vip-reason"><span class="jio-native-field__label">' . esc_html__('变更原因', 'jio') . ' <span class="jio-native-text--danger" aria-hidden="true">*</span></span>'
. '<input type="text" name="reason" required maxlength="255" placeholder="' . esc_attr__('例如:活动赠送、客服补偿、退款撤销', 'jio') . '">'
. '<span class="jio-native-field__hint">' . esc_html__('写入变更记录,便于后续追溯。', 'jio') . '</span></label>';
echo '<p class="jio-native-muted jio-vip-bulk-note" data-jio-vip-bulk-note hidden></p>';
echo '</div>';
echo '<footer class="aksrt-vip-dialog__foot"><button type="button" class="button" data-aksrt-vip-close>' . esc_html__('取消', 'aksrt') . '</button>'
. '<button type="submit" class="button button-primary" data-aksrt-vip-submit>' . esc_html__('确认变更', 'aksrt') . '</button></footer>';
echo '<footer class="jio-vip-dialog__foot"><button type="button" class="button" data-jio-vip-close>' . esc_html__('取消', 'jio') . '</button>'
. '<button type="submit" class="button button-primary" data-jio-vip-submit>' . esc_html__('确认变更', 'jio') . '</button></footer>';
echo '</form></dialog>';
}
private function render_vip_history(int $page): void {
$result = (new \AKSRT\Content\VipMembershipEventRepository())->query($page, self::VIP_PER_PAGE);
$result = (new \Jio\Content\VipMembershipEventRepository())->query($page, self::VIP_PER_PAGE);
$ops = [
'set' => [__('设置会员', 'aksrt'), 'success'],
'extend' => [__('延长有效期', 'aksrt'), 'info'],
'restore' => [__('恢复回退会员', 'aksrt'), 'warning'],
'disable' => [__('停用会员', 'aksrt'), 'danger'],
'expire_recover' => [__('到期自动回退', 'aksrt'), 'neutral'],
'set' => [__('设置会员', 'jio'), 'success'],
'extend' => [__('延长有效期', 'jio'), 'info'],
'restore' => [__('恢复回退会员', 'jio'), 'warning'],
'disable' => [__('停用会员', 'jio'), 'danger'],
'expire_recover' => [__('到期自动回退', 'jio'), 'neutral'],
];
$statuses = [
'succeeded' => [__('成功', 'aksrt'), 'success'],
'failed' => [__('失败', 'aksrt'), 'danger'],
'pending' => [__('处理中', 'aksrt'), 'warning'],
'succeeded' => [__('成功', 'jio'), 'success'],
'failed' => [__('失败', 'jio'), 'danger'],
'pending' => [__('处理中', 'jio'), 'warning'],
];
NativeUi::table_card_open(__('变更记录', 'aksrt'), '<span class="aksrt-native-muted">' . esc_html(sprintf(__('共 %d 条', 'aksrt'), (int) $result['total'])) . '</span>');
echo '<table class="wp-list-table widefat fixed striped aksrt-vip-history"><thead><tr><th class="column-time">' . esc_html__('时间', 'aksrt') . '</th><th class="column-user">' . esc_html__('用户', 'aksrt') . '</th><th>' . esc_html__('操作', 'aksrt') . '</th><th>' . esc_html__('操作人', 'aksrt') . '</th><th class="column-reason">' . esc_html__('原因', 'aksrt') . '</th><th>' . esc_html__('结果', 'aksrt') . '</th></tr></thead><tbody>';
NativeUi::table_card_open(__('变更记录', 'jio'), '<span class="jio-native-muted">' . esc_html(sprintf(__('共 %d 条', 'jio'), (int) $result['total'])) . '</span>');
echo '<table class="wp-list-table widefat fixed striped jio-vip-history"><thead><tr><th class="column-time">' . esc_html__('时间', 'jio') . '</th><th class="column-user">' . esc_html__('用户', 'jio') . '</th><th>' . esc_html__('操作', 'jio') . '</th><th>' . esc_html__('操作人', 'jio') . '</th><th class="column-reason">' . esc_html__('原因', 'jio') . '</th><th>' . esc_html__('结果', 'jio') . '</th></tr></thead><tbody>';
if ($result['items'] === []) {
echo NativeUi::empty_row(6, __('暂无变更记录', 'aksrt'), __('在会员列表中调整会员后会出现在这里。', 'aksrt')); // phpcs:ignore WordPress.Security.EscapeOutput -- builder escapes.
echo NativeUi::empty_row(6, __('暂无变更记录', 'jio'), __('在会员列表中调整会员后会出现在这里。', 'jio')); // phpcs:ignore WordPress.Security.EscapeOutput -- builder escapes.
}
foreach ($result['items'] as $event) {
$created = (string) $event['created_at'];
$time = $created !== '' ? get_date_from_gmt($created, 'Y-m-d H:i') : '—';
$actor = (string) ($event['actor_name'] ?? '');
if ($actor === '') {
$actor = ((string) ($event['source'] ?? '') === 'admin') ? '—' : __('系统', 'aksrt');
$actor = ((string) ($event['source'] ?? '') === 'admin') ? '—' : __('系统', 'jio');
}
$result_html = NativeUi::status_badge((string) $event['status'], $statuses);
if ((string) $event['status'] === 'failed' && (string) ($event['error_message'] ?? '') !== '') {
$result_html .= '<span class="aksrt-vip-note aksrt-native-text--danger">' . esc_html((string) $event['error_message']) . '</span>';
$result_html .= '<span class="jio-vip-note jio-native-text--danger">' . esc_html((string) $event['error_message']) . '</span>';
}
echo '<tr><td class="column-time">' . esc_html($time) . (!empty($event['batch_uuid']) ? '<span class="aksrt-vip-note aksrt-native-muted">' . esc_html__('批量', 'aksrt') . '</span>' : '') . '</td>'
echo '<tr><td class="column-time">' . esc_html($time) . (!empty($event['batch_uuid']) ? '<span class="jio-vip-note jio-native-muted">' . esc_html__('批量', 'jio') . '</span>' : '') . '</td>'
. '<td class="column-user">' . NativeUi::user_cell((int) $event['user_id'], (string) ($event['display_name'] ?: $event['user_login'] ?: '')) . '</td>' // phpcs:ignore WordPress.Security.EscapeOutput -- builder escapes.
. '<td>' . NativeUi::status_badge((string) $event['operation'], $ops) . '</td>' // phpcs:ignore WordPress.Security.EscapeOutput -- builder escapes.
. '<td>' . esc_html($actor) . '</td>'