fix(user-center): 优化提现记录弹窗分页

This commit is contained in:
2026-09-16 12:53:38 +08:00
parent 27f54c8e35
commit 6bbb79213b
5 changed files with 290 additions and 62 deletions
+93 -1
View File
@@ -886,10 +886,102 @@ body.uc-modal-open {
border: 0;
cursor: pointer;
}
.uc-withdraw-history-modal__dialog {
max-width: min(860px, calc(100vw - 32px));
overflow: hidden;
}
.uc-withdraw-history {
width: 100%;
}
.uc-withdraw-history__item {
display: flex;
align-items: center;
gap: 12px;
min-width: 0;
padding: 10px 0;
border-bottom: 1px solid var(--muted-border-color, rgba(0,0,0,.08));
white-space: nowrap;
}
.uc-withdraw-history__item:last-child {
border-bottom: 0 !important;
border-bottom: 0;
}
.uc-withdraw-history__status,
.uc-withdraw-history__amount {
flex: 0 0 auto;
}
.uc-withdraw-history__amount {
min-width: 84px;
text-align: right;
font-weight: 700;
}
.uc-withdraw-history__number,
.uc-withdraw-history__meta,
.uc-withdraw-history__note {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
}
.uc-withdraw-history__number {
flex: 1 1 190px;
}
.uc-withdraw-history__meta {
flex: 0 1 190px;
}
.uc-withdraw-history__note {
flex: 0 1 160px;
}
.uc-withdraw-history__pagination {
display: flex;
justify-content: center;
align-items: center;
flex-wrap: wrap;
gap: 6px;
margin-top: 14px;
}
.uc-withdraw-history__page {
min-width: 34px;
height: 32px;
padding: 0 8px;
border: 1px solid var(--muted-border-color, rgba(0,0,0,.12));
border-radius: 6px;
background: transparent;
color: inherit;
cursor: pointer;
}
.uc-withdraw-history__page:hover:not(:disabled),
.uc-withdraw-history__page.is-current {
border-color: var(--focus-color, #3b82f6);
background: var(--focus-color, #3b82f6);
color: #fff;
}
.uc-withdraw-history__page:disabled {
cursor: not-allowed;
opacity: .45;
}
@media (max-width: 640px) {
.uc-withdraw-history-modal__dialog {
max-width: calc(100vw - 20px);
padding-left: 12px;
padding-right: 12px;
}
.uc-withdraw-history__item {
gap: 8px;
font-size: 12px;
}
.uc-withdraw-history__amount {
min-width: 66px;
}
.uc-withdraw-history__number {
flex-basis: 120px;
}
.uc-withdraw-history__meta {
flex-basis: 120px;
}
.uc-withdraw-history__note {
display: none;
}
}
/* Points tasks tab */
.uc-points-tasks .border-bottom,
+137 -13
View File
@@ -804,19 +804,143 @@
}
function initWithdrawHistory() {
var modal = document.querySelector('[data-aksrt-withdraw-history-modal]');
if (!modal || modal.dataset.aksrtWithdrawHistoryInitialized === '1') return;
modal.dataset.aksrtWithdrawHistoryInitialized = '1';
function open() { modal.hidden = false; document.body.classList.add('uc-modal-open'); }
function close() { modal.hidden = true; document.body.classList.remove('uc-modal-open'); }
document.querySelectorAll('[data-aksrt-withdraw-history-open]').forEach(function (btn) {
btn.addEventListener('click', function (e) { e.preventDefault(); open(); });
});
modal.querySelectorAll('[data-aksrt-withdraw-history-close]').forEach(function (btn) {
btn.addEventListener('click', function (e) { e.preventDefault(); close(); });
});
}
function initWithdrawHistory() {
var modal = document.querySelector('[data-aksrt-withdraw-history-modal]');
if (!modal || modal.dataset.aksrtWithdrawHistoryInitialized === '1') return;
modal.dataset.aksrtWithdrawHistoryInitialized = '1';
var list = modal.querySelector('[data-aksrt-withdraw-history-list]');
var pagination = modal.querySelector('[data-aksrt-withdraw-history-pagination]');
var statusLabels = {
pending: '待处理',
processing: '处理中',
completed: '已完成',
rejected: '已拒绝'
};
var currentPage = 1;
var loading = false;
function text(value) {
return value === null || value === undefined ? '' : String(value);
}
function appendText(parent, value, className) {
var node = document.createElement('span');
if (className) node.className = className;
node.textContent = text(value);
parent.appendChild(node);
return node;
}
function renderItems(items) {
if (!list) return;
list.textContent = '';
if (!Array.isArray(items) || !items.length) {
var empty = document.createElement('div');
empty.className = 'uc-transactions__empty';
var emptyText = document.createElement('p');
emptyText.textContent = '暂无提现记录';
empty.appendChild(emptyText);
list.appendChild(empty);
return;
}
items.forEach(function (item) {
var status = text(item && item.status).toLowerCase();
var row = document.createElement('div');
row.className = 'uc-withdraw-history__item';
var badge = appendText(row, statusLabels[status] || status, 'badg badg-sm uc-withdraw-history__status');
badge.classList.add(status === 'rejected' ? 'c-red' : (status === 'completed' ? 'c-green' : 'c-blue'));
appendText(row, item && (item.gross_amount || item.amount), 'uc-withdraw-history__amount em12');
var number = document.createElement('span');
number.className = 'uc-withdraw-history__number muted-2-color em09';
var numberLabel = document.createElement('span');
numberLabel.textContent = '提现单号:';
number.appendChild(numberLabel);
var code = document.createElement('code');
code.textContent = text(item && item.withdrawal_number);
number.appendChild(code);
row.appendChild(number);
var meta = appendText(row, text(item && item.created_at), 'uc-withdraw-history__meta muted-2-color em09');
var gross = text(item && (item.gross_amount || item.amount));
var net = text(item && item.amount);
if (gross !== net && net !== '') meta.appendChild(document.createTextNode(' · 到账 ' + net));
if (item && item.note) appendText(row, item.note, 'uc-withdraw-history__note muted-2-color em09');
list.appendChild(row);
});
}
function renderPagination(page, maxPages) {
if (!pagination) return;
pagination.textContent = '';
if (maxPages <= 1) return;
function addButton(label, target, disabled, current) {
var button = document.createElement('button');
button.type = 'button';
button.className = 'uc-withdraw-history__page' + (current ? ' is-current' : '');
button.textContent = label;
button.disabled = !!disabled;
if (current) button.setAttribute('aria-current', 'page');
button.dataset.aksrtWithdrawHistoryPage = String(target);
pagination.appendChild(button);
}
addButton('上一页', page - 1, page <= 1, false);
for (var index = 1; index <= maxPages; index += 1) addButton(String(index), index, false, index === page);
addButton('下一页', page + 1, page >= maxPages, false);
}
function loadPage(page) {
if (loading || !list) return;
loading = true;
list.textContent = '加载中…';
var rest = (window.aksrtData && (window.aksrtData.restUrl || window.aksrtData.apiUrl)) || '';
if (!rest) {
list.textContent = '记录加载失败,请重试';
loading = false;
return;
}
var url = rest.replace(/\/$/, '') + '/earnings/withdrawals?page=' + encodeURIComponent(Math.max(1, page)) + '&per_page=10';
fetch(url, {
credentials: 'same-origin',
headers: { 'X-WP-Nonce': (window.aksrtData && (window.aksrtData.restNonce || window.aksrtData.nonce)) || '' }
}).then(function (response) {
return response.json().then(function (payload) {
if (!response.ok || !payload || !Array.isArray(payload.items)) throw new Error('withdraw-history-load-failed');
currentPage = Math.max(1, parseInt(payload.page, 10) || 1);
var maxPages = Math.max(1, parseInt(payload.max_pages, 10) || 1);
renderItems(payload.items);
renderPagination(currentPage, maxPages);
});
}).catch(function () {
list.textContent = '记录加载失败,请重试';
if (pagination) pagination.textContent = '';
}).finally(function () {
loading = false;
});
}
function open() {
modal.hidden = false;
document.body.classList.add('uc-modal-open');
loadPage(1);
}
function close() {
modal.hidden = true;
document.body.classList.remove('uc-modal-open');
}
document.querySelectorAll('[data-aksrt-withdraw-history-open]').forEach(function (btn) {
btn.addEventListener('click', function (e) { e.preventDefault(); open(); });
});
modal.querySelectorAll('[data-aksrt-withdraw-history-close]').forEach(function (btn) {
btn.addEventListener('click', function (e) { e.preventDefault(); close(); });
});
if (pagination) {
pagination.addEventListener('click', function (e) {
var button = e.target.closest('[data-aksrt-withdraw-history-page]');
if (!button || button.disabled) return;
e.preventDefault();
loadPage(parseInt(button.dataset.aksrtWithdrawHistoryPage || '1', 10) || 1);
});
}
}
window.AKSRTWithdrawInit = initWithdraw;
window.AKSRTAssetBalances = {
@@ -40,14 +40,11 @@ $withdraw_settings = class_exists('\\AKSRT\\Settings\\Settings') ? \AKSRT\Settin
$withdraw_min = $normalize_balance($withdraw_settings['min_amount'] ?? '50')->decimal();
$withdraw_usage = ['remaining_count' => 0, 'remaining_amount' => '0.00'];
$withdraw_pending_hold = '0.00';
$withdraw_history = [];
if ($withdraw_on && class_exists('\AKSRT\Payment\Withdrawal')) {
$wd_svc = \AKSRT\Payment\Withdrawal::get_instance();
$withdraw_pending_hold = $wd_svc->sum_pending_hold((int) ($user_id ?? 0));
$withdraw_history = $wd_svc->get_user_withdrawals((int) ($user_id ?? 0), ['limit' => 30]);
$withdraw_usage = $wd_svc->daily_usage((int) ($user_id ?? 0), $withdraw_settings);
}
$withdraw_processing = $normalize_balance($withdraw_pending_hold)->minor() > 0;
$card_pass_balance = ['enabled' => false, 'desc' => '', 'only_password' => true];
if (class_exists('\AKSRT\Payment\ChargeCard')) {
$opts = \AKSRT\Payment\ChargeCard::get_instance()->get_public_options();
@@ -187,50 +184,16 @@ $mark = (string) ($charge_view['currency_mark'] ?? '元');
endif; ?>
<?php if ($withdraw_on) :
$status_labels = [
'pending' => __('待处理', 'aksrt'),
'processing' => __('处理中', 'aksrt'),
'completed' => __('已完成', 'aksrt'),
'rejected' => __('已拒绝', 'aksrt'),
];
?>
<div class="uc-modal" id="aksrt-withdraw-history-modal" hidden data-aksrt-withdraw-history-modal>
<div class="uc-modal__overlay" data-aksrt-withdraw-history-close></div>
<div class="uc-modal__dialog" role="dialog" aria-modal="true">
<button type="button" class="uc-modal__close" data-aksrt-withdraw-history-close aria-label="<?php esc_attr_e('关闭', 'aksrt'); ?>"><i class="fa fas fa-times" aria-hidden="true"></i></button>
<h3 class="uc-modal__title"><i class="fa fas fa-file-alt mr6" aria-hidden="true"></i><?php esc_html_e('提现记录', 'aksrt'); ?></h3>
<div class="uc-withdraw-history">
<?php if (!empty($withdraw_history)) : ?>
<?php foreach ($withdraw_history as $wd) :
$st = sanitize_key((string) ($wd['status'] ?? ''));
$gross = $normalize_balance($wd['gross_amount'] ?? $wd['amount'] ?? '0');
$net = $normalize_balance($wd['amount'] ?? '0');
$st_class = $st === 'rejected' ? 'c-red' : ($st === 'completed' ? 'c-green' : 'c-blue');
?>
<div class="uc-withdraw-history__item border-bottom padding-h10">
<div class="flex jsb ac">
<span class="badg badg-sm <?php echo esc_attr($st_class); ?>"><?php echo esc_html($status_labels[$st] ?? $st); ?></span>
<b class="em12"><?php echo esc_html($gross->formatted()); ?></b>
</div>
<div class="muted-2-color em09 mt6"><?php esc_html_e('提现单号:', 'aksrt'); ?><code><?php echo esc_html((string) ($wd['withdrawal_number'] ?? '')); ?></code></div>
<div class="muted-2-color em09 mt6">
<?php echo esc_html((string) ($wd['created_at'] ?? '')); ?>
<?php if ($gross->minor() !== $net->minor()) : ?>
· <?php printf(esc_html__('到账 %s', 'aksrt'), esc_html($net->formatted())); ?>
<?php endif; ?>
</div>
<?php if (!empty($wd['note'])) : ?>
<div class="muted-2-color em09 mt3"><?php echo esc_html((string) $wd['note']); ?></div>
<?php endif; ?>
</div>
<?php endforeach; ?>
<?php else : ?>
<div class="uc-transactions__empty"><p><?php esc_html_e('暂无提现记录', 'aksrt'); ?></p></div>
<?php endif; ?>
<?php if ($withdraw_on) : ?>
<div class="uc-modal" id="aksrt-withdraw-history-modal" hidden data-aksrt-withdraw-history-modal>
<div class="uc-modal__overlay" data-aksrt-withdraw-history-close></div>
<div class="uc-modal__dialog uc-withdraw-history-modal__dialog" role="dialog" aria-modal="true" aria-labelledby="aksrt-withdraw-history-title">
<button type="button" class="uc-modal__close" data-aksrt-withdraw-history-close aria-label="<?php esc_attr_e('关闭', 'aksrt'); ?>"><i class="fa fas fa-times" aria-hidden="true"></i></button>
<h3 id="aksrt-withdraw-history-title" class="uc-modal__title"><i class="fa fas fa-file-alt mr6" aria-hidden="true"></i><?php esc_html_e('提现记录', 'aksrt'); ?></h3>
<div class="uc-withdraw-history" data-aksrt-withdraw-history-list aria-live="polite"></div>
<nav class="uc-withdraw-history__pagination" data-aksrt-withdraw-history-pagination aria-label="<?php esc_attr_e('提现记录分页', 'aksrt'); ?>"></nav>
</div>
</div>
</div>
<?php endif; ?>
<?php if (empty($skip_wallet_ledger)) : ?>
+30 -3
View File
@@ -258,19 +258,23 @@ final class CommissionLedger {
'permission_callback' => [$this, 'check_user_rest'],
'args' => $list_args,
]);
$withdrawal_args = $list_args;
$withdrawal_args['per_page']['default'] = 10;
$withdrawal_args['per_page']['maximum'] = 10;
register_rest_route('aksrt/v1', '/earnings/withdrawals', [
[
'methods' => \WP_REST_Server::READABLE,
'callback' => fn(\WP_REST_Request $request) => rest_ensure_response($this->withdrawals(get_current_user_id(), $request->get_params())),
'permission_callback' => [$this, 'check_user_rest'],
'args' => $list_args,
'args' => $withdrawal_args,
],
[
'methods' => \WP_REST_Server::CREATABLE,
'callback' => [$this, 'create_withdrawal'],
'permission_callback' => [$this, 'check_user_write_rest'],
'args' => [
'amount' => ['type' => 'string', 'required' => true, 'pattern' => '^(0|[1-9]\\d*)(?:\\.\\d{1,2})?$'],
'amount' => ['type' => 'string', 'required' => true, 'pattern' => '^(0|[1-9]\d*)(?:\.\d{1,2})?$'],
'method' => ['type' => 'string', 'required' => true, 'sanitize_callback' => 'sanitize_key'],
'account' => ['type' => 'string', 'required' => true, 'minLength' => 1, 'maxLength' => 500, 'sanitize_callback' => 'sanitize_text_field'],
],
@@ -384,7 +388,30 @@ final class CommissionLedger {
}
public function admin_overview(): array { global $wpdb; $table=$this->table('commission_ledger'); $row=$wpdb->get_row("SELECT COUNT(*) total, SUM(status='pending') pending, SUM(status='settled') settled, COALESCE(SUM(original_amount+adjustment_total),0) amount FROM {$table}",ARRAY_A) ?: []; return ['total'=>(int)($row['total']??0),'pending'=>(int)($row['pending']??0),'settled'=>(int)($row['settled']??0),'amount'=>$this->balance_decimal((string)($row['amount']??'0'))]; }
public function admin_ledger(array $args): array { global $wpdb; $table=$this->table('commission_ledger'); $limit=max(1,min(100,(int)($args['per_page']??20))); $page=max(1,(int)($args['page']??1)); $offset=($page-1)*$limit; $status=sanitize_key((string)($args['status']??'')); $where='1=1'; $params=[]; if(in_array($status,['pending','settled','cancelled'],true)){ $where.=' AND status=%s'; $params[]=$status; } $total=(int)($wpdb->get_var($params?$wpdb->prepare("SELECT COUNT(*) FROM {$table} WHERE {$where}",$params):"SELECT COUNT(*) FROM {$table}")); $rows=$wpdb->get_results($wpdb->prepare("SELECT * FROM {$table} WHERE {$where} ORDER BY id DESC LIMIT %d OFFSET %d",array_merge($params,[$limit,$offset])),ARRAY_A); return ['items'=>$rows?:[],'total'=>$total,'page'=>$page,'per_page'=>$limit,'max_pages'=>max(1,(int)ceil($total/$limit))]; }
public function withdrawals(int $user_id, array $args): array { return \AKSRT\Payment\Withdrawal::get_instance()->get_user_withdrawals($user_id, ['status'=>sanitize_key((string)($args['status']??'')),'limit'=>max(1,min(100,(int)($args['per_page']??20))),'offset'=>max(0,((int)($args['page']??1)-1)*max(1,(int)($args['per_page']??20)))]); }
public function withdrawals(int $user_id, array $args): array {
$service = \AKSRT\Payment\Withdrawal::get_instance();
$page = max(1, (int) ($args['page'] ?? 1));
$per_page = 10;
$status = sanitize_key((string) ($args['status'] ?? ''));
$filters = ['user_id' => $user_id];
if ($status !== '') {
$filters['status'] = $status;
}
$total = $service->get_count($filters);
$max_pages = max(1, (int) ceil($total / $per_page));
$page = min($page, $max_pages);
return [
'items' => $service->get_user_withdrawals($user_id, [
'status' => $status,
'limit' => $per_page,
'offset' => ($page - 1) * $per_page,
]),
'total' => $total,
'page' => $page,
'per_page' => $per_page,
'max_pages' => $max_pages,
];
}
public function create_withdrawal(\WP_REST_Request $request): \WP_REST_Response|\WP_Error {
$user_id=get_current_user_id(); $method=sanitize_key((string)$request->get_param('method')); $account=sanitize_text_field((string)$request->get_param('account')); $settings=\AKSRT\Settings\Settings::get_withdrawal_settings(); $affiliate=\AKSRT\Settings\Settings::get_affiliate_settings();
$raw_amount=$request->get_param('amount');
@@ -53,3 +53,25 @@ test('balance history renders withdrawal numbers from the committed flow', () =>
assert.match(source, /\$row\['withdrawal_number'\]/)
}
})
test('user withdrawal history uses one paginated ten-item endpoint', () => {
const controller = read('src/Payment/CommissionLedger.php')
const renderer = read('inc/helpers/user-center-renderers/credits-wallet.php')
const script = read('assets/js/credits.js')
const styles = read('assets/css/routes/user-center/credits.css')
assert.match(controller, /\$withdrawal_args\['per_page'\]\['default'\]\s*=\s*10/)
assert.match(controller, /\$withdrawal_args\['per_page'\]\['maximum'\]\s*=\s*10/)
assert.match(controller, /'total'\s*=>\s*\$total/)
assert.match(controller, /'max_pages'\s*=>\s*\$max_pages/)
assert.match(controller, /'user_id'\s*=>\s*\$user_id/)
assert.doesNotMatch(renderer, /get_user_withdrawals\(/)
assert.doesNotMatch(renderer, /withdraw_history/)
assert.match(renderer, /data-aksrt-withdraw-history-list/)
assert.match(script, /\/earnings\/withdrawals\?page=/)
assert.match(script, /&per_page=10/)
assert.match(script, /data-aksrt-withdraw-history-page/)
assert.match(styles, /\.uc-withdraw-history-modal__dialog\s*\{[\s\S]*?max-width: min\(860px/)
assert.match(styles, /\.uc-withdraw-history__item\s*\{[\s\S]*?display: flex/)
assert.match(styles, /\.uc-withdraw-history-modal__dialog\s*\{[\s\S]*?overflow: hidden/)
})