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
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')