feat(admin): add user info and keyword search to admin order list

Backend:
- Add Keyword field to OrderListParams
- AdminListOrders supports keyword search on out_trade_no, user_email, user_name
- PaymentOrder already has user_email/user_name/user_notes fields (no join needed)

Frontend:
- Replace inline status badge with OrderStatusBadge component
- Replace user_id column with user_email (shows email, fallback to username, with notes)
- Keyword search matches order number and user info

chore: bump version to 0.1.108.143
This commit is contained in:
erio
2026-04-09 23:41:59 +08:00
parent e9aa1b1330
commit 753a87dee3
5 changed files with 20 additions and 15 deletions
+1 -1
View File
@@ -1 +1 @@
0.1.108.142
0.1.108.143
@@ -60,6 +60,7 @@ func (h *PaymentHandler) ListOrders(c *gin.Context) {
Status: c.Query("status"),
OrderType: c.Query("order_type"),
PaymentType: c.Query("payment_type"),
Keyword: c.Query("keyword"),
})
if err != nil {
response.ErrorFrom(c, err)
@@ -354,6 +354,13 @@ func (s *PaymentService) AdminListOrders(ctx context.Context, userID int64, p Or
if p.PaymentType != "" {
q = q.Where(paymentorder.PaymentTypeEQ(p.PaymentType))
}
if p.Keyword != "" {
q = q.Where(paymentorder.Or(
paymentorder.OutTradeNoContainsFold(p.Keyword),
paymentorder.UserEmailContainsFold(p.Keyword),
paymentorder.UserNameContainsFold(p.Keyword),
))
}
total, err := q.Clone().Count(ctx)
if err != nil {
return nil, 0, fmt.Errorf("count admin orders: %w", err)
@@ -95,6 +95,7 @@ type OrderListParams struct {
Status string
OrderType string
PaymentType string
Keyword string
}
type RefundPlan struct {
@@ -24,7 +24,12 @@
<template #cell-out_trade_no="{ value }">
<span class="text-sm text-gray-900 dark:text-white">{{ value }}</span>
</template>
<template #cell-user_id="{ value }"><span class="text-sm text-gray-600 dark:text-gray-400">#{{ value }}</span></template>
<template #cell-user_email="{ value, row }">
<div class="text-sm">
<span class="text-gray-900 dark:text-white">{{ value || row.user_name || '#' + row.user_id }}</span>
<span v-if="row.user_notes" class="ml-1 text-xs text-gray-400">({{ row.user_notes }})</span>
</div>
</template>
<template #cell-amount="{ value, row }">
<div class="text-sm">
<span class="font-medium text-gray-900 dark:text-white">${{ value.toFixed(2) }}</span>
@@ -32,7 +37,7 @@
</div>
</template>
<template #cell-status="{ value }">
<span :class="['badge', statusBadgeClass(value)]">{{ t('payment.status.' + value.toLowerCase(), value) }}</span>
<OrderStatusBadge :status="value" />
</template>
<template #cell-payment_type="{ value }">
<span class="text-sm text-gray-700 dark:text-gray-300">{{ t('payment.methods.' + value, value) }}</span>
@@ -81,7 +86,7 @@
<div class="grid grid-cols-2 gap-4">
<div><p class="text-xs text-gray-500 dark:text-gray-400">{{ t('payment.orders.orderId') }}</p><p class="font-mono text-sm font-medium text-gray-900 dark:text-white">#{{ selectedOrder.id }}</p></div>
<div><p class="text-xs text-gray-500 dark:text-gray-400">{{ t('payment.orders.orderNo') }}</p><p class="text-sm font-medium text-gray-900 dark:text-white">{{ selectedOrder.out_trade_no }}</p></div>
<div><p class="text-xs text-gray-500 dark:text-gray-400">{{ t('payment.orders.status') }}</p><span :class="['badge', statusBadgeClass(selectedOrder.status)]">{{ t('payment.status.' + selectedOrder.status.toLowerCase(), selectedOrder.status) }}</span></div>
<div><p class="text-xs text-gray-500 dark:text-gray-400">{{ t('payment.orders.status') }}</p><OrderStatusBadge :status="selectedOrder.status" /></div>
<div><p class="text-xs text-gray-500 dark:text-gray-400">{{ t('payment.orders.amount') }}</p><p class="text-sm font-medium text-gray-900 dark:text-white">${{ selectedOrder.amount.toFixed(2) }}</p></div>
<div><p class="text-xs text-gray-500 dark:text-gray-400">{{ t('payment.orders.payAmount') }}</p><p class="text-sm font-medium text-gray-900 dark:text-white">${{ selectedOrder.pay_amount.toFixed(2) }}</p></div>
<div><p class="text-xs text-gray-500 dark:text-gray-400">{{ t('payment.orders.paymentMethod') }}</p><p class="text-sm text-gray-700 dark:text-gray-300">{{ t('payment.methods.' + selectedOrder.payment_type, selectedOrder.payment_type) }}</p></div>
@@ -146,6 +151,7 @@ import BaseDialog from '@/components/common/BaseDialog.vue'
import Select from '@/components/common/Select.vue'
import Icon from '@/components/icons/Icon.vue'
import AdminRefundDialog from '@/components/admin/payment/AdminRefundDialog.vue'
import OrderStatusBadge from '@/components/payment/OrderStatusBadge.vue'
interface AuditLog {
id: number
@@ -220,21 +226,11 @@ const orderTypeFilterOptions = computed(() => [
])
const orderColumns = computed((): Column[] => [
{ key: 'id', label: 'ID' }, { key: 'out_trade_no', label: t('payment.orders.orderNo') }, { key: 'user_id', label: t('payment.admin.colUser') }, { key: 'amount', label: t('payment.orders.amount') },
{ key: 'id', label: 'ID' }, { key: 'out_trade_no', label: t('payment.orders.orderNo') }, { key: 'user_email', label: t('payment.admin.colUser') }, { key: 'amount', label: t('payment.orders.amount') },
{ key: 'payment_type', label: t('payment.orders.paymentMethod') }, { key: 'status', label: t('payment.orders.status') },
{ key: 'created_at', label: t('payment.orders.createdAt') }, { key: 'actions', label: t('common.actions') },
])
function statusBadgeClass(status: string): string {
const m: Record<string, string> = {
PENDING: 'badge-warning', PAID: 'badge-info', RECHARGING: 'badge-info',
COMPLETED: 'badge-success', EXPIRED: 'badge-secondary', CANCELLED: 'badge-secondary',
FAILED: 'badge-danger', REFUND_REQUESTED: 'badge-warning', REFUNDING: 'badge-warning',
PARTIALLY_REFUNDED: 'badge-warning', REFUNDED: 'badge-info', REFUND_FAILED: 'badge-danger',
}
return m[status] || 'badge-secondary'
}
async function showOrderDetail(order: PaymentOrder) {
selectedOrder.value = order
orderAuditLogs.value = []