feat(admin): 售后列表改为手风琴展开详情并内联审核表单
This commit is contained in:
+116
-53
@@ -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> ';
|
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);
|
submit_button(__('筛选', 'aksrt'), 'secondary', '', false);
|
||||||
echo '</form>';
|
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 '<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="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="" />';
|
echo '<input type="hidden" name="bulk_note" id="aksrt-bulk-note" value="" />';
|
||||||
@@ -411,41 +425,48 @@ class PayOrders {
|
|||||||
echo '<button type="submit" class="button action">' . esc_html__('应用', 'aksrt') . '</button></div><div class="tablenav-spacer" style="clear:both;"></div></div>';
|
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>';
|
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>';
|
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) {
|
foreach ($items as $item) {
|
||||||
$id = (int) $item['id']; $number = (string) $item['order_number']; $uid = (int) $item['user_id'];
|
$id = (int) $item['id']; $number = (string) $item['order_number']; $uid = (int) $item['user_id'];
|
||||||
$user = $uid > 0 ? get_userdata($uid) : null;
|
$user = $uid > 0 ? get_userdata($uid) : null;
|
||||||
$order = \AKSRT\Payment\OrderRepository::get_instance()->get($number);
|
$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 = $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;
|
$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');
|
$refund_amount = (string) ($item['refund_amount'] ?? '0');
|
||||||
if ($order && (string) ($order['currency'] ?? '') === 'points') $refund_amount .= ' ' . __('积分', 'aksrt'); else $refund_amount = '¥' . $refund_amount;
|
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;
|
$apply_form = is_array($item['apply_form'] ?? null) ? $item['apply_form'] : null;
|
||||||
if ($apply_form) {
|
if ($apply_form) {
|
||||||
$reason_cell = esc_html((string) ($apply_form['reason_label'] ?? ''));
|
$reason_cell = esc_html((string) ($apply_form['reason_label'] ?? ''));
|
||||||
$material_lines = [];
|
$material_lines = [];
|
||||||
foreach ((array) ($apply_form['materials'] ?? []) as $m) {
|
foreach ((array) ($apply_form['materials'] ?? []) as $m) {
|
||||||
$ml = trim((string) ($m['label'] ?? '')); $mv = trim((string) ($m['value'] ?? ''));
|
$ml = trim((string) ($m['label'] ?? '')); $mv = trim((string) ($m['value'] ?? ''));
|
||||||
if ($ml === '' && $mv === '') continue;
|
if ($ml === '' && $mv === '') continue;
|
||||||
$material_lines[] = esc_html($ml !== '' ? $ml . ':' . $mv : $mv);
|
$material_lines[] = esc_html($ml !== '' ? $ml . ':' . $mv : $mv);
|
||||||
|
}
|
||||||
|
$materials_cell = $material_lines ? implode('<br>', $material_lines) : '—';
|
||||||
|
$desc_text = trim((string) ($apply_form['description'] ?? ''));
|
||||||
|
$desc_cell = $desc_text !== '' ? nl2br(esc_html($desc_text)) : '—';
|
||||||
|
} else {
|
||||||
|
// 老数据:融合串原样展示在原因列
|
||||||
|
$reason_cell = nl2br(esc_html((string) $item['reason']));
|
||||||
|
$materials_cell = '—';
|
||||||
|
$desc_cell = '—';
|
||||||
}
|
}
|
||||||
$materials_cell = $material_lines ? implode('<br>', $material_lines) : '—';
|
|
||||||
$desc_text = trim((string) ($apply_form['description'] ?? ''));
|
|
||||||
$desc_cell = $desc_text !== '' ? nl2br(esc_html($desc_text)) : '—';
|
|
||||||
} else {
|
|
||||||
// 老数据:融合串原样展示在原因列
|
|
||||||
$reason_cell = nl2br(esc_html((string) $item['reason']));
|
|
||||||
$materials_cell = '—';
|
|
||||||
$desc_cell = '—';
|
|
||||||
}
|
|
||||||
$image_urls = array_values(array_filter((array) ($item['images'] ?? []), 'is_string'));
|
$image_urls = array_values(array_filter((array) ($item['images'] ?? []), 'is_string'));
|
||||||
$thumbs = [];
|
$thumbs = [];
|
||||||
foreach ($image_urls as $url) {
|
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) : '—';
|
$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>';
|
$status_label = $status_labels[(string) $item['status']] ?? (string) $item['status'];
|
||||||
// 快捷操作:点击「操作」展开面板,点击链接经 JS 以 POST 提交
|
$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 = [];
|
$quick_actions = [];
|
||||||
if ((string) $item['status'] === 'pending') {
|
if ((string) $item['status'] === 'pending') {
|
||||||
$quick_actions = ['approve' => __('通过', 'aksrt'), 'reject' => __('拒绝', 'aksrt')];
|
$quick_actions = ['approve' => __('通过', 'aksrt'), 'reject' => __('拒绝', 'aksrt')];
|
||||||
@@ -456,62 +477,104 @@ class PayOrders {
|
|||||||
? ['complete' => __('确认线下退款完成', 'aksrt'), 'reject' => __('确认未退款并拒绝', 'aksrt')]
|
? ['complete' => __('确认线下退款完成', 'aksrt'), 'reject' => __('确认未退款并拒绝', 'aksrt')]
|
||||||
: ['approve' => __('同步退款状态', '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) {
|
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) {
|
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-id="' . esc_attr((string) $id) . '"'
|
||||||
. ' data-decision="' . esc_attr($decision) . '"'
|
. ' data-decision="' . esc_attr($decision) . '"'
|
||||||
. ' data-label="' . esc_attr($label) . '"'
|
. ' data-label="' . esc_attr($label) . '"'
|
||||||
. ' data-nonce="' . esc_attr(wp_create_nonce('aksrt_after_sale_review_' . $id)) . '"'
|
. ' data-nonce="' . esc_attr(wp_create_nonce('aksrt_after_sale_review_' . $id)) . '">'
|
||||||
. '>' . esc_html($label) . '</a></span>';
|
. 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></div>';
|
||||||
echo '<div class="aksrt-row-actions-panel" id="aksrt-ra-' . esc_attr((string) $id) . '" hidden style="margin-top:6px;"><div>' . implode(' | ', $links) . '</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>';
|
echo '</div></td></tr>';
|
||||||
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 '</tbody></table>';
|
echo '</tbody></table>';
|
||||||
echo '</form>';
|
echo '</form>';
|
||||||
|
// 事件委托:点击「操作」切换手风琴详情;点击审核按钮经 JS 以 POST 提交(详情行在批量表单内,禁止嵌套 form)
|
||||||
echo '<script>(function(){'
|
echo '<script>(function(){'
|
||||||
. 'var postUrl=' . wp_json_encode(esc_url_raw(admin_url('admin-post.php'))) . ';'
|
. 'var postUrl=' . wp_json_encode(esc_url_raw(admin_url('admin-post.php'))) . ';'
|
||||||
. 'var tReject=' . wp_json_encode(__('请填写原因(将记为管理员备注,可留空):', 'aksrt')) . ';'
|
. 'var tReject=' . wp_json_encode(__('请填写原因(将记为管理员备注,可留空):', 'aksrt')) . ';'
|
||||||
. 'var tConfirmA=' . wp_json_encode(__('确认执行', 'aksrt')) . ';'
|
. 'var tConfirmA=' . wp_json_encode(__('确认执行', 'aksrt')) . ';'
|
||||||
. 'var tConfirmB=' . wp_json_encode(__('吗?', 'aksrt')) . ';'
|
. 'var tConfirmB=' . wp_json_encode(__('吗?', 'aksrt')) . ';'
|
||||||
. 'document.querySelectorAll(".aksrt-toggle-row-actions").forEach(function(t){'
|
. 'function closeAllDetails(){'
|
||||||
. 't.addEventListener("click",function(e){e.preventDefault();'
|
. 'document.querySelectorAll(".aksrt-as-detail-row").forEach(function(x){x.setAttribute("hidden","");});'
|
||||||
. 'var p=document.getElementById(t.getAttribute("data-target"));var willOpen=p.hasAttribute("hidden");'
|
. 'document.querySelectorAll(".aksrt-toggle-row-actions").forEach(function(t){t.setAttribute("aria-expanded","false");});'
|
||||||
. 'document.querySelectorAll(".aksrt-row-actions-panel").forEach(function(x){x.setAttribute("hidden","");});'
|
. '}'
|
||||||
. 'if(willOpen)p.removeAttribute("hidden");'
|
. 'document.addEventListener("click",function(e){'
|
||||||
. '});});'
|
. 'var el=e.target,t=el&&el.closest?el.closest(".aksrt-toggle-row-actions"):null;'
|
||||||
. 'document.querySelectorAll(".aksrt-after-sale-quick-action").forEach(function(a){'
|
. 'if(t){e.preventDefault();'
|
||||||
. 'a.addEventListener("click",function(e){e.preventDefault();'
|
. 'var p=document.getElementById(t.getAttribute("data-target"));if(!p)return;'
|
||||||
. 'var decision=a.getAttribute("data-decision"),label=a.getAttribute("data-label"),note="";'
|
. 'var willOpen=p.hasAttribute("hidden");closeAllDetails();'
|
||||||
. 'if(decision==="reject"){note=prompt(tReject,"");if(note===null)return;}'
|
. 'if(willOpen){p.removeAttribute("hidden");t.setAttribute("aria-expanded","true");'
|
||||||
. 'else if(!confirm(tConfirmA+"「"+label+"」"+tConfirmB))return;'
|
. '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 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);});'
|
. '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();'
|
. 'document.body.appendChild(f);f.submit();'
|
||||||
. '});});'
|
. '});'
|
||||||
. 'var selAll=document.querySelector(".aksrt-select-all");'
|
. 'var selAll=document.querySelector(".aksrt-select-all");'
|
||||||
. 'if(selAll){selAll.addEventListener("change",function(){'
|
. 'if(selAll){selAll.addEventListener("change",function(){'
|
||||||
. 'document.querySelectorAll(".aksrt-row-check").forEach(function(c){c.checked=selAll.checked;});});}'
|
. 'document.querySelectorAll(".aksrt-row-check").forEach(function(c){c.checked=selAll.checked;});});}'
|
||||||
. 'var bulkForm=document.getElementById("aksrt-after-sale-bulk-form");'
|
. 'document.addEventListener("submit",function(e){'
|
||||||
. 'if(bulkForm){bulkForm.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 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(!checked.length){e.preventDefault();alert(' . wp_json_encode(__('请先勾选要操作的售后记录。', 'aksrt')) . ');return;}'
|
||||||
. 'if(!decision){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;}'
|
. '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;}'
|
. 'else if(!confirm(' . wp_json_encode(__('确认批量通过选中的记录吗?', 'aksrt')) . ')){e.preventDefault();return;}'
|
||||||
. '});}'
|
. '},true);'
|
||||||
. '})();</script>';
|
. '})();</script>';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>'), 'description column')
|
||||||
assert.ok(page.includes('<th>举证图片</th>'), 'evidence images column')
|
assert.ok(page.includes('<th>举证图片</th>'), 'evidence images column')
|
||||||
assert.ok(page.includes('$images_cell'), 'evidence images cell rendered from item images')
|
assert.ok(page.includes('$images_cell'), 'evidence images cell rendered from item images')
|
||||||
// 操作列仿 WP 文章列表 row-actions:悬停显示的快捷操作链接,经 JS 以 POST 提交
|
// 操作列:点击「操作」以手风琴展开整条记录详情(含完整字段与内联审核表单),经 JS 以 POST 提交
|
||||||
assert.ok(page.includes('row-actions'), 'row-actions container')
|
assert.ok(page.includes('aksrt-toggle-row-actions'), 'toggle to expand record accordion')
|
||||||
assert.ok(page.includes('aksrt-after-sale-quick-action'), 'quick action links')
|
assert.ok(page.includes('aksrt-as-detail-row'), 'accordion detail row per after-sale record')
|
||||||
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('aksrt-as-detail-grid'), 'accordion panel renders full record fields')
|
||||||
assert.ok(page.includes('data-nonce'), 'per-item nonce for quick actions')
|
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-toggle-row-actions'), 'toggle to expand quick actions')
|
assert.ok(page.includes('.aksrt-as-detail-row[hidden]'), 'hidden attribute guard keeps accordion closed')
|
||||||
assert.ok(page.includes('aksrt-row-actions-panel'), 'expandable quick action panel')
|
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('class="row-actions"'), 'no hover-only row-actions')
|
||||||
assert.ok(page.includes('aksrt_after_sale_bulk'), 'bulk review handler registered')
|
assert.ok(page.includes('aksrt_after_sale_bulk'), 'bulk review handler registered')
|
||||||
assert.ok(page.includes('批量操作'), 'bulk actions dropdown')
|
assert.ok(page.includes('批量操作'), 'bulk actions dropdown')
|
||||||
|
|||||||
Reference in New Issue
Block a user