feat(admin): 售后列表改为手风琴展开详情并内联审核表单

This commit is contained in:
2026-10-07 23:38:30 +08:00
parent d2034f5031
commit 80373746f8
3 changed files with 221 additions and 61 deletions
+97 -34
View File
@@ -403,6 +403,20 @@ class PayOrders {
echo '</select> <label>' . esc_html__('开始日期', 'aksrt') . ' <input type="date" name="date_from" value="' . esc_attr($date_from) . '" /></label> <label>' . esc_html__('结束日期', 'aksrt') . ' <input type="date" name="date_to" value="' . esc_attr($date_to) . '" /></label> ';
submit_button(__('筛选', 'aksrt'), 'secondary', '', false);
echo '</form>';
echo '<style>'
. '.aksrt-as-detail-row>td{padding:0 0 6px;}'
. '.aksrt-as-detail-row[hidden]{display:none;}'
. '.aksrt-as-detail{margin:0 12px;padding:14px 18px;border:1px solid #dcdcde;background:#fff;box-shadow:0 1px 1px rgba(0,0,0,.04);}'
. '.aksrt-as-detail-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(230px,1fr));gap:10px 28px;}'
. '.aksrt-as-field .aksrt-as-label{display:block;color:#646970;font-size:12px;margin-bottom:2px;}'
. '.aksrt-as-field .aksrt-as-value{display:block;word-break:break-word;}'
. '.aksrt-as-thumb{width:48px;height:48px;object-fit:cover;border-radius:4px;margin:0 4px 4px 0;vertical-align:middle;}'
. '.aksrt-as-detail .aksrt-as-thumb{width:80px;height:80px;}'
. '.aksrt-as-review{margin-top:12px;padding-top:10px;border-top:1px solid #dcdcde;max-width:720px;}'
. '.aksrt-as-review textarea{width:100%;box-sizing:border-box;margin:4px 0 8px;}'
. '.aksrt-as-review-actions{display:flex;gap:8px;flex-wrap:wrap;}'
. '.aksrt-qa-danger{color:#b32d2e;border-color:#b32d2e;}'
. '</style>';
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" id="aksrt-after-sale-bulk-form">';
echo '<input type="hidden" name="action" value="aksrt_after_sale_bulk" />'; wp_nonce_field('aksrt_after_sale_bulk');
echo '<input type="hidden" name="bulk_note" id="aksrt-bulk-note" value="" />';
@@ -411,13 +425,14 @@ class PayOrders {
echo '<button type="submit" class="button action">' . esc_html__('应用', 'aksrt') . '</button></div><div class="tablenav-spacer" style="clear:both;"></div></div>';
echo '<table class="widefat striped"><thead><tr><th class="check-column"><input type="checkbox" class="aksrt-select-all" /></th><th>ID</th><th>订单号</th><th>用户</th><th>原因</th><th>申请资料</th><th>申请说明</th><th>举证图片</th><th>退款金额</th><th>状态</th><th>操作 / 处理记录</th></tr></thead><tbody>';
if (!$items) echo '<tr><td colspan="11">' . esc_html__('暂无售后申请', 'aksrt') . '</td></tr>';
$status_labels = [\AKSRT\Payment\AfterSaleService::STATUS_PENDING => '待审核', \AKSRT\Payment\AfterSaleService::STATUS_PROCESSING => '处理中', \AKSRT\Payment\AfterSaleService::STATUS_APPROVED => '已通过', \AKSRT\Payment\AfterSaleService::STATUS_REJECTED => '已拒绝', \AKSRT\Payment\AfterSaleService::STATUS_CANCELLED => '已取消', \AKSRT\Payment\AfterSaleService::STATUS_COMPLETED => '已完成'];
foreach ($items as $item) {
$id = (int) $item['id']; $number = (string) $item['order_number']; $uid = (int) $item['user_id'];
$user = $uid > 0 ? get_userdata($uid) : null;
$order = \AKSRT\Payment\OrderRepository::get_instance()->get($number);
$type = $order ? \AKSRT\Core\DomainModel::resolve_order_product_type((string) ($order['product_type'] ?? ''), (array) ($order['meta_payload'] ?? [])) : '';
$type_label = \AKSRT\Core\DomainModel::order_product_type_labels()[$type] ?? $type;
$status_labels = [\AKSRT\Payment\AfterSaleService::STATUS_PENDING => '待审核', \AKSRT\Payment\AfterSaleService::STATUS_PROCESSING => '处理中', \AKSRT\Payment\AfterSaleService::STATUS_APPROVED => '已通过', \AKSRT\Payment\AfterSaleService::STATUS_REJECTED => '已拒绝', \AKSRT\Payment\AfterSaleService::STATUS_CANCELLED => '已取消', \AKSRT\Payment\AfterSaleService::STATUS_COMPLETED => '已完成'];
$product_name = $order ? (string) (\AKSRT\Payment\UserOrders::get_instance()->decorate_order_view($order, false)['product_name'] ?? '') : '';
$refund_amount = (string) ($item['refund_amount'] ?? '0');
if ($order && (string) ($order['currency'] ?? '') === 'points') $refund_amount .= ' ' . __('积分', 'aksrt'); else $refund_amount = '¥' . $refund_amount;
$apply_form = is_array($item['apply_form'] ?? null) ? $item['apply_form'] : null;
@@ -441,11 +456,17 @@ class PayOrders {
$image_urls = array_values(array_filter((array) ($item['images'] ?? []), 'is_string'));
$thumbs = [];
foreach ($image_urls as $url) {
$thumbs[] = '<a href="' . esc_url($url) . '" target="_blank" rel="noopener"><img src="' . esc_url($url) . '" alt="" style="width:48px;height:48px;object-fit:cover;border-radius:4px;margin:0 4px 4px 0;vertical-align:middle;" loading="lazy" /></a>';
$thumbs[] = '<a href="' . esc_url($url) . '" target="_blank" rel="noopener"><img src="' . esc_url($url) . '" alt="" class="aksrt-as-thumb" loading="lazy" /></a>';
}
$images_cell = $thumbs ? implode('', $thumbs) : '—';
echo '<tr><td class="check-column"><input type="checkbox" class="aksrt-row-check" name="after_sale_ids[]" value="' . esc_attr((string) $id) . '" /></td><td>' . esc_html((string) $id) . '<br><small>' . esc_html((string) ($item['created_at'] ?? '')) . '</small></td><td><a href="' . esc_url($this->page_url(self::PAGE_SLUG, ['order' => $number])) . '"><code>' . esc_html($number) . '</code></a><br><span class="description">' . esc_html($type_label) . ($order ? ' · ' . esc_html(\AKSRT\Payment\UserOrders::get_instance()->decorate_order_view($order, false)['product_name'] ?? '') : '') . '</span></td><td>' . esc_html($user ? $user->display_name . ' (#' . $uid . ')' : '#' . $uid) . '</td><td>' . $reason_cell . '</td><td>' . $materials_cell . '</td><td>' . $desc_cell . '</td><td>' . $images_cell . '</td><td>' . esc_html($refund_amount) . '</td><td>' . esc_html($status_labels[(string) $item['status']] ?? (string) $item['status']) . '</td><td>';
// 快捷操作:点击「操作」展开面板,点击链接经 JS 以 POST 提交
$status_label = $status_labels[(string) $item['status']] ?? (string) $item['status'];
$admin_note = (string) ($item['admin_note'] ?? '');
$gateway_text = '';
if (!empty($item['gateway_result'])) {
$result_data = json_decode((string) $item['gateway_result'], true);
$gateway_text = is_array($result_data) ? implode(' · ', array_filter([(string) ($result_data['status'] ?? ''), (string) ($result_data['message'] ?? ''), !empty($result_data['refund_id']) ? '退款单号 ' . (string) $result_data['refund_id'] : ''])) : (string) $item['gateway_result'];
}
// 按状态决定该记录可执行的审核动作(经 JS 以 POST 提交)
$quick_actions = [];
if ((string) $item['status'] === 'pending') {
$quick_actions = ['approve' => __('通过', 'aksrt'), 'reject' => __('拒绝', 'aksrt')];
@@ -456,62 +477,104 @@ class PayOrders {
? ['complete' => __('确认线下退款完成', 'aksrt'), 'reject' => __('确认未退款并拒绝', 'aksrt')]
: ['approve' => __('同步退款状态', 'aksrt')];
}
// 摘要行 + 手风琴详情行:点击「操作」展开整条记录的完整信息与审核表单
echo '<tr class="aksrt-as-row">';
echo '<td class="check-column"><input type="checkbox" class="aksrt-row-check" name="after_sale_ids[]" value="' . esc_attr((string) $id) . '" /></td>';
echo '<td>' . esc_html((string) $id) . '<br><small>' . esc_html((string) ($item['created_at'] ?? '')) . '</small></td>';
echo '<td><a href="' . esc_url($this->page_url(self::PAGE_SLUG, ['order' => $number])) . '"><code>' . esc_html($number) . '</code></a><br><span class="description">' . esc_html($type_label) . ($product_name !== '' ? ' · ' . esc_html($product_name) : '') . '</span></td>';
echo '<td>' . esc_html($user ? $user->display_name . ' (#' . $uid . ')' : '#' . $uid) . '</td>';
echo '<td>' . $reason_cell . '</td>';
echo '<td>' . $materials_cell . '</td>';
echo '<td>' . $desc_cell . '</td>';
echo '<td>' . $images_cell . '</td>';
echo '<td>' . esc_html($refund_amount) . '</td>';
echo '<td>' . esc_html($status_label) . '</td>';
echo '<td><a href="#" class="aksrt-toggle-row-actions" data-target="aksrt-as-detail-' . esc_attr((string) $id) . '" aria-expanded="false" aria-controls="aksrt-as-detail-' . esc_attr((string) $id) . '">' . esc_html__('操作', 'aksrt') . '</a></td>';
echo '</tr>';
echo '<tr class="aksrt-as-detail-row" id="aksrt-as-detail-' . esc_attr((string) $id) . '" hidden><td colspan="11">';
$detail_fields = [
__('订单号', 'aksrt') => '<a href="' . esc_url($this->page_url(self::PAGE_SLUG, ['order' => $number])) . '"><code>' . esc_html($number) . '</code></a>',
__('商品', 'aksrt') => esc_html($type_label . ($product_name !== '' ? ' · ' . $product_name : '')),
__('用户', 'aksrt') => esc_html($user ? $user->display_name . ' (#' . $uid . ')' : '#' . $uid),
__('申请时间', 'aksrt') => esc_html((string) ($item['created_at'] ?? '')),
__('退款金额', 'aksrt') => esc_html($refund_amount),
__('当前状态', 'aksrt') => esc_html($status_label),
__('售后原因', 'aksrt') => $reason_cell,
__('申请资料', 'aksrt') => $materials_cell,
__('申请说明', 'aksrt') => $desc_cell,
__('举证图片', 'aksrt') => $images_cell,
__('处理备注', 'aksrt') => $admin_note !== '' ? esc_html($admin_note) : '—',
__('渠道结果', 'aksrt') => $gateway_text !== '' ? '<code>' . esc_html($gateway_text) . '</code>' : '—',
__('审核时间', 'aksrt') => !empty($item['reviewed_at']) ? esc_html((string) $item['reviewed_at']) : '—',
];
echo '<div class="aksrt-as-detail"><div class="aksrt-as-detail-grid">';
foreach ($detail_fields as $field_label => $field_value) {
echo '<span class="aksrt-as-field"><span class="aksrt-as-label">' . esc_html($field_label) . '</span><span class="aksrt-as-value">' . $field_value . '</span></span>';
}
echo '</div>';
if ($quick_actions) {
$links = [];
echo '<div class="aksrt-as-review">';
echo '<p style="margin:0;"><strong>' . esc_html__('审核操作', 'aksrt') . '</strong> <span class="description">' . esc_html__('处理备注可留空,将记为管理员备注。', 'aksrt') . '</span></p>';
echo '<textarea class="aksrt-review-note" rows="2" placeholder="' . esc_attr__('填写处理备注(可留空)', 'aksrt') . '"></textarea>';
echo '<div class="aksrt-as-review-actions">';
foreach ($quick_actions as $decision => $label) {
$links[] = '<span class="' . esc_attr($decision) . '"><a href="#" class="aksrt-after-sale-quick-action"'
echo '<button type="button" class="button button-small aksrt-after-sale-quick-action' . ($decision === 'reject' ? ' aksrt-qa-danger' : ' button-primary') . '"'
. ' data-id="' . esc_attr((string) $id) . '"'
. ' data-decision="' . esc_attr($decision) . '"'
. ' data-label="' . esc_attr($label) . '"'
. ' data-nonce="' . esc_attr(wp_create_nonce('aksrt_after_sale_review_' . $id)) . '"'
. '>' . esc_html($label) . '</a></span>';
. ' data-nonce="' . esc_attr(wp_create_nonce('aksrt_after_sale_review_' . $id)) . '">'
. esc_html($label) . '</button>';
}
echo '<a href="#" class="aksrt-toggle-row-actions" data-target="aksrt-ra-' . esc_attr((string) $id) . '">' . esc_html__('操作', 'aksrt') . '</a>';
echo '<div class="aksrt-row-actions-panel" id="aksrt-ra-' . esc_attr((string) $id) . '" hidden style="margin-top:6px;"><div>' . implode(' | ', $links) . '</div></div>';
echo '</div></div>';
} else {
echo '<p class="description" style="margin:12px 0 0;">' . esc_html__('该售后申请已处理完毕,无需再审核。', 'aksrt') . '</p>';
}
if (!empty($item['admin_note'])) echo '<small>' . esc_html__('备注:', 'aksrt') . esc_html((string) $item['admin_note']) . '</small><br>';
if (!empty($item['gateway_result'])) {
$result_data = json_decode((string) $item['gateway_result'], true);
$result_text = is_array($result_data) ? implode(' · ', array_filter([(string) ($result_data['status'] ?? ''), (string) ($result_data['message'] ?? ''), !empty($result_data['refund_id']) ? '退款单号 ' . (string) $result_data['refund_id'] : ''])) : (string) $item['gateway_result'];
echo '<small>' . esc_html__('渠道结果:', 'aksrt') . ' ' . esc_html($result_text) . '</small>';
}
echo '</td></tr>';
echo '</div></td></tr>';
}
echo '</tbody></table>';
echo '</form>';
// 事件委托:点击「操作」切换手风琴详情;点击审核按钮经 JS 以 POST 提交(详情行在批量表单内,禁止嵌套 form)
echo '<script>(function(){'
. 'var postUrl=' . wp_json_encode(esc_url_raw(admin_url('admin-post.php'))) . ';'
. 'var tReject=' . wp_json_encode(__('请填写原因(将记为管理员备注,可留空):', 'aksrt')) . ';'
. 'var tConfirmA=' . wp_json_encode(__('确认执行', 'aksrt')) . ';'
. 'var tConfirmB=' . wp_json_encode(__('吗?', 'aksrt')) . ';'
. 'document.querySelectorAll(".aksrt-toggle-row-actions").forEach(function(t){'
. 't.addEventListener("click",function(e){e.preventDefault();'
. 'var p=document.getElementById(t.getAttribute("data-target"));var willOpen=p.hasAttribute("hidden");'
. 'document.querySelectorAll(".aksrt-row-actions-panel").forEach(function(x){x.setAttribute("hidden","");});'
. 'if(willOpen)p.removeAttribute("hidden");'
. '});});'
. 'document.querySelectorAll(".aksrt-after-sale-quick-action").forEach(function(a){'
. 'a.addEventListener("click",function(e){e.preventDefault();'
. 'var decision=a.getAttribute("data-decision"),label=a.getAttribute("data-label"),note="";'
. 'if(decision==="reject"){note=prompt(tReject,"");if(note===null)return;}'
. 'else if(!confirm(tConfirmA+"「"+label+"」"+tConfirmB))return;'
. 'function closeAllDetails(){'
. 'document.querySelectorAll(".aksrt-as-detail-row").forEach(function(x){x.setAttribute("hidden","");});'
. 'document.querySelectorAll(".aksrt-toggle-row-actions").forEach(function(t){t.setAttribute("aria-expanded","false");});'
. '}'
. 'document.addEventListener("click",function(e){'
. 'var el=e.target,t=el&&el.closest?el.closest(".aksrt-toggle-row-actions"):null;'
. 'if(t){e.preventDefault();'
. 'var p=document.getElementById(t.getAttribute("data-target"));if(!p)return;'
. 'var willOpen=p.hasAttribute("hidden");closeAllDetails();'
. 'if(willOpen){p.removeAttribute("hidden");t.setAttribute("aria-expanded","true");'
. 'var n=p.querySelector(".aksrt-review-note");if(n)n.focus();}'
. 'return;}'
. 'var a=el&&el.closest?el.closest(".aksrt-after-sale-quick-action"):null;'
. 'if(!a)return;e.preventDefault();'
. 'var panel=a.closest(".aksrt-as-detail-row");'
. 'var noteEl=panel?panel.querySelector(".aksrt-review-note"):null;'
. 'var note=noteEl?noteEl.value:"";'
. 'if(!confirm(tConfirmA+"「"+a.getAttribute("data-label")+"」"+tConfirmB))return;'
. 'var f=document.createElement("form");f.method="post";f.action=postUrl;'
. 'var fields={action:"aksrt_after_sale_review",after_sale_id:a.getAttribute("data-id"),decision:decision,_wpnonce:a.getAttribute("data-nonce"),admin_note:note||""};'
. 'var fields={action:"aksrt_after_sale_review",after_sale_id:a.getAttribute("data-id"),decision:a.getAttribute("data-decision"),_wpnonce:a.getAttribute("data-nonce"),admin_note:note};'
. 'Object.keys(fields).forEach(function(k){var i=document.createElement("input");i.type="hidden";i.name=k;i.value=fields[k];f.appendChild(i);});'
. 'document.body.appendChild(f);f.submit();'
. '});});'
. '});'
. 'var selAll=document.querySelector(".aksrt-select-all");'
. 'if(selAll){selAll.addEventListener("change",function(){'
. 'document.querySelectorAll(".aksrt-row-check").forEach(function(c){c.checked=selAll.checked;});});}'
. 'var bulkForm=document.getElementById("aksrt-after-sale-bulk-form");'
. 'if(bulkForm){bulkForm.addEventListener("submit",function(e){'
. 'document.addEventListener("submit",function(e){'
. 'if(!e.target||e.target.id!=="aksrt-after-sale-bulk-form")return;'
. 'var checked=document.querySelectorAll(".aksrt-row-check:checked");'
. 'var decision=document.getElementById("aksrt-bulk-decision").value;'
. 'var decisionEl=document.getElementById("aksrt-bulk-decision");'
. 'var decision=decisionEl?decisionEl.value:"";'
. 'if(!checked.length){e.preventDefault();alert(' . wp_json_encode(__('请先勾选要操作的售后记录。', 'aksrt')) . ');return;}'
. 'if(!decision){e.preventDefault();alert(' . wp_json_encode(__('请选择批量操作。', 'aksrt')) . ');return;}'
. 'if(decision==="reject"){var note=prompt(tReject,"");if(note===null){e.preventDefault();return;}document.getElementById("aksrt-bulk-note").value=note;}'
. 'else if(!confirm(' . wp_json_encode(__('确认批量通过选中的记录吗?', 'aksrt')) . ')){e.preventDefault();return;}'
. '});}'
. '},true);'
. '})();</script>';
}
+97
View File
@@ -0,0 +1,97 @@
<?php
// Dev harness: render the REAL PayOrders::render_after_sales_page() with WP stubs,
// capturing the generated HTML so the inline JS and markup can be verified offline.
namespace {
define('ABSPATH', __DIR__ . '/');
// ---------- WP function stubs ----------
function esc_html($s) { return htmlspecialchars((string) $s, ENT_QUOTES); }
function esc_attr($s) { return htmlspecialchars((string) $s, ENT_QUOTES); }
function esc_url($s) { return (string) $s; }
function esc_url_raw($s) { return (string) $s; }
function esc_js($s) { return addslashes((string) $s); }
function esc_textarea($s) { return htmlspecialchars((string) $s, ENT_QUOTES); }
function esc_html__($s, $d = null) { return esc_html($s); }
function esc_attr__($s, $d = null) { return esc_attr($s); }
function esc_html_e($s, $d = null) { echo esc_html($s); }
function __($s, $d = null) { return $s; }
function wp_unslash($v) { return $v; }
function sanitize_key($s) { return preg_replace('/[^a-z0-9_\-]/', '', strtolower((string) $s)); }
function sanitize_text_field($s) { return trim(strip_tags((string) $s)); }
function sanitize_textarea_field($s) { return trim(strip_tags((string) $s)); }
function selected($a, $b, $echo = true) { $r = ((string) $a === (string) $b) ? " selected='selected'" : ''; if ($echo) echo $r; return $r; }
function submit_button($text = null, $type = 'primary', $name = 'submit', $wrap = true, $other = []) { echo '<button type="submit" class="' . esc_attr($type) . '">' . esc_html($text ?: 'Submit') . '</button>'; }
function wp_nonce_field($a = -1, $n = '_wpnonce', $r = true, $e = true) { if ($e) echo '<input type="hidden" name="' . esc_attr($n) . '" value="stubnonce" />'; }
function wp_create_nonce($a = -1) { return 'stubnonce' . md5((string) $a); }
function admin_url($p = '') { return 'https://example.test/wp-admin/' . $p; }
function add_query_arg($k, $v = null, $u = null) { if (is_array($k)) { $u = $v ?: ''; $q = http_build_query($k); return $u . (str_contains($u, '?') ? '&' : '?') . $q; } return ($u ?: '') . (str_contains(($u ?: ''), '?') ? '&' : '?') . $k . '=' . $v; }
function wp_json_encode($v, $f = 0) { return json_encode($v, $f); }
function current_user_can($c) { return true; }
function wp_die($m) { throw new Exception('wp_die: ' . $m); }
function get_userdata($id) { return $id > 0 ? (object) ['display_name' => 'User' . $id] : null; }
function get_the_title($id) { return 'Product' . $id; }
function is_admin() { return true; }
}
namespace AKSRT\Payment {
class AfterSaleService {
public const STATUS_PENDING = 'pending';
public const STATUS_PROCESSING = 'processing';
public const STATUS_APPROVED = 'approved';
public const STATUS_REJECTED = 'rejected';
public const STATUS_CANCELLED = 'cancelled';
public const STATUS_COMPLETED = 'completed';
private static $i;
public static function get_instance(): self { return self::$i ?: self::$i = new self(); }
public function admin_list(array $args = []): array {
return [
['id' => 5, 'order_number' => 'ORD100', 'user_id' => 3, 'reason' => 'legacy-reason',
'apply_form' => ['reason_label' => '商品与描述不符', 'materials' => [['label' => '联系方式', 'value' => 'wx-abc']], 'description' => '说明文字'],
'images' => [], 'refund_amount' => '12.50', 'status' => 'pending', 'admin_note' => '', 'gateway_result' => '', 'created_at' => '2026-10-01 10:00:00'],
['id' => 6, 'order_number' => 'ORD101', 'user_id' => 4, 'reason' => 'r2',
'apply_form' => null, 'images' => [], 'refund_amount' => '8.00', 'status' => 'processing',
'admin_note' => '已有备注', 'gateway_result' => json_encode(['status' => 'manual', 'message' => '线下退款']), 'created_at' => '2026-10-02 11:00:00'],
];
}
}
class OrderRepository {
private static $i;
public static function get_instance(): self { return self::$i ?: self::$i = new self(); }
public function get($n): array { return ['order_number' => $n, 'user_id' => 3, 'product_id' => 9, 'product_type' => 'reading', 'meta_payload' => [], 'currency' => 'CNY', 'status' => 'paid']; }
}
class UserOrders {
private static $i;
public static function get_instance(): self { return self::$i ?: self::$i = new self(); }
public function decorate_order_view(array $o, bool $x): array { return ['product_name' => '测试商品']; }
}
}
namespace AKSRT\Core {
class DomainModel {
public static function resolve_order_product_type(string $t, array $m): string { return $t; }
public static function order_product_type_labels(): array { return ['reading' => '付费阅读']; }
}
}
namespace AKSRT\Settings {
class PaymentRuntime {
public static function get_after_sale_settings(): array {
return ['enabled' => true, 'time_limit' => 7, 'reasons' => [['name' => '原因A']], 'materials' => ['截图']];
}
}
}
namespace {
require 'C:/Users/Xing/Desktop/AKSRT/AKSRT-theme/src/Admin/Native/PayOrders.php';
$_GET = ['page' => 'aksrt_after_sales'];
$pay = AKSRT\Admin\Native\PayOrders::get_instance();
$ref = new ReflectionMethod($pay, 'render_after_sales_page');
$ref->setAccessible(true);
ob_start();
$ref->invoke($pay);
$html = ob_get_clean();
file_put_contents(__DIR__ . '/after-sales-rendered.html', $html);
echo 'bytes=' . strlen($html) . "\n";
}
@@ -145,14 +145,14 @@ test('order detail page drops tabs; after-sales becomes a standalone submenu', (
assert.ok(page.includes('<th>申请说明</th>'), 'description column')
assert.ok(page.includes('<th>举证图片</th>'), 'evidence images column')
assert.ok(page.includes('$images_cell'), 'evidence images cell rendered from item images')
// 操作列仿 WP 文章列表 row-actions:悬停显示的快捷操作链接,经 JS 以 POST 提交
assert.ok(page.includes('row-actions'), 'row-actions container')
assert.ok(page.includes('aksrt-after-sale-quick-action'), 'quick action links')
assert.ok(!page.includes('aksrt_after_sale_review" /><input type="hidden" name="after_sale_id"'), 'no inline review forms in after-sale list')
assert.ok(page.includes('data-nonce'), 'per-item nonce for quick actions')
// 操作列改为点击「操作」展开快捷面板;顶部有批量操作
assert.ok(page.includes('aksrt-toggle-row-actions'), 'toggle to expand quick actions')
assert.ok(page.includes('aksrt-row-actions-panel'), 'expandable quick action panel')
// 操作列:点击「操作」以手风琴展开整条记录详情(含完整字段与内联审核表单),经 JS 以 POST 提交
assert.ok(page.includes('aksrt-toggle-row-actions'), 'toggle to expand record accordion')
assert.ok(page.includes('aksrt-as-detail-row'), 'accordion detail row per after-sale record')
assert.ok(page.includes('aksrt-as-detail-grid'), 'accordion panel renders full record fields')
assert.ok(page.includes('aksrt-review-note'), 'review note textarea inside expanded panel')
assert.ok(page.includes('aksrt-as-review-actions'), 'inline approve/reject buttons inside expanded panel')
assert.ok(page.includes('.aksrt-as-detail-row[hidden]'), 'hidden attribute guard keeps accordion closed')
assert.ok(page.includes('document.addEventListener("click"'), 'delegated click handler survives row re-render')
assert.ok(!page.includes('class="row-actions"'), 'no hover-only row-actions')
assert.ok(page.includes('aksrt_after_sale_bulk'), 'bulk review handler registered')
assert.ok(page.includes('批量操作'), 'bulk actions dropdown')