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> ';
|
||||
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,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 '<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 => '已完成'];
|
||||
$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;
|
||||
if ($apply_form) {
|
||||
$reason_cell = esc_html((string) ($apply_form['reason_label'] ?? ''));
|
||||
$material_lines = [];
|
||||
foreach ((array) ($apply_form['materials'] ?? []) as $m) {
|
||||
$ml = trim((string) ($m['label'] ?? '')); $mv = trim((string) ($m['value'] ?? ''));
|
||||
if ($ml === '' && $mv === '') continue;
|
||||
$material_lines[] = esc_html($ml !== '' ? $ml . ':' . $mv : $mv);
|
||||
$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;
|
||||
if ($apply_form) {
|
||||
$reason_cell = esc_html((string) ($apply_form['reason_label'] ?? ''));
|
||||
$material_lines = [];
|
||||
foreach ((array) ($apply_form['materials'] ?? []) as $m) {
|
||||
$ml = trim((string) ($m['label'] ?? '')); $mv = trim((string) ($m['value'] ?? ''));
|
||||
if ($ml === '' && $mv === '') continue;
|
||||
$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'));
|
||||
$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>';
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user