refactor(frontend): extract shared OrderTable component

- Create OrderTable.vue with shared cell rendering (ID, order number,
  amount, payment method, status badge, created time)
- Accepts showUser prop to conditionally show user_email column
- Actions column uses scoped slot for view-specific buttons
- UserOrdersView and AdminOrdersView both use OrderTable
- Removes duplicated DataTable cell templates from both views

chore: bump version to 0.1.108.144
This commit is contained in:
erio
2026-04-09 23:49:47 +08:00
parent 753a87dee3
commit 62e9453ddc
4 changed files with 80 additions and 76 deletions
+1 -1
View File
@@ -1 +1 @@
0.1.108.143
0.1.108.144
@@ -0,0 +1,71 @@
<template>
<DataTable :columns="columns" :data="orders" :loading="loading">
<template #cell-id="{ value }">
<span class="font-mono text-sm">#{{ value }}</span>
</template>
<template #cell-out_trade_no="{ value }">
<span class="text-sm text-gray-900 dark:text-white">{{ value }}</span>
</template>
<template v-if="showUser" #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>
<span v-if="row.pay_amount !== value" class="ml-1 text-xs text-gray-500">(${{ row.pay_amount.toFixed(2) }})</span>
</div>
</template>
<template #cell-payment_type="{ value }">
<span class="text-sm text-gray-700 dark:text-gray-300">{{ t('payment.methods.' + value, value) }}</span>
</template>
<template #cell-status="{ value }">
<OrderStatusBadge :status="value" />
</template>
<template #cell-created_at="{ value }">
<span class="text-xs text-gray-500 dark:text-gray-400">{{ formatDate(value) }}</span>
</template>
<template #cell-actions="{ row }">
<slot name="actions" :row="row" />
</template>
</DataTable>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import type { PaymentOrder } from '@/types/payment'
import type { Column } from '@/components/common/types'
import DataTable from '@/components/common/DataTable.vue'
import OrderStatusBadge from '@/components/payment/OrderStatusBadge.vue'
const { t } = useI18n()
const props = defineProps<{
orders: PaymentOrder[]
loading: boolean
showUser?: boolean
}>()
function formatDate(dateStr: string) { return new Date(dateStr).toLocaleString() }
const columns = computed((): Column[] => {
const cols: Column[] = [
{ key: 'id', label: t('payment.orders.orderId') },
{ key: 'out_trade_no', label: t('payment.orders.orderNo') },
]
if (props.showUser) {
cols.push({ key: 'user_email', label: t('payment.admin.colUser') })
}
cols.push(
{ 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') },
)
return cols
})
</script>
@@ -19,33 +19,8 @@
</div>
<!-- Table -->
<DataTable :columns="orderColumns" :data="orders" :loading="ordersLoading">
<template #cell-id="{ value }"><span class="font-mono text-sm">#{{ value }}</span></template>
<template #cell-out_trade_no="{ value }">
<span class="text-sm text-gray-900 dark:text-white">{{ 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>
<span v-if="row.pay_amount !== value" class="ml-1 text-xs text-gray-500">({{ t('payment.orders.payAmount') }}: ${{ row.pay_amount.toFixed(2) }})</span>
</div>
</template>
<template #cell-status="{ value }">
<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>
</template>
<template #cell-created_at="{ value }">
<span class="text-xs text-gray-500 dark:text-gray-400">{{ formatDateTime(value) }}</span>
</template>
<template #cell-actions="{ row }">
<OrderTable :orders="orders" :loading="ordersLoading" show-user>
<template #actions="{ row }">
<div class="flex items-center gap-1">
<button @click="showOrderDetail(row)" class="inline-flex items-center gap-1 rounded-md px-2 py-1 text-xs font-medium text-gray-600 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-dark-600">
<Icon name="eye" size="sm" />
@@ -76,7 +51,7 @@
</button>
</div>
</template>
</DataTable>
</OrderTable>
<Pagination v-if="orderPagination.total > 0" :page="orderPagination.page" :total="orderPagination.total" :page-size="orderPagination.page_size" @update:page="handleOrderPageChange" @update:pageSize="handleOrderPageSizeChange" />
</div>
@@ -143,15 +118,14 @@ import { useAppStore } from '@/stores/app'
import { adminPaymentAPI } from '@/api/admin/payment'
import { extractApiErrorMessage } from '@/utils/apiError'
import type { PaymentOrder } from '@/types/payment'
import type { Column } from '@/components/common/types'
import AppLayout from '@/components/layout/AppLayout.vue'
import DataTable from '@/components/common/DataTable.vue'
import Pagination from '@/components/common/Pagination.vue'
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'
import OrderTable from '@/components/payment/OrderTable.vue'
interface AuditLog {
id: number
@@ -225,12 +199,6 @@ const orderTypeFilterOptions = computed(() => [
{ value: 'subscription', label: t('payment.admin.subscriptionOrder') },
])
const orderColumns = computed((): Column[] => [
{ 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') },
])
async function showOrderDetail(order: PaymentOrder) {
selectedOrder.value = order
orderAuditLogs.value = []
+4 -39
View File
@@ -15,29 +15,8 @@
</div>
<!-- Table -->
<DataTable :columns="columns" :data="orders" :loading="loading">
<template #cell-id="{ value }">
<span class="font-mono text-sm">#{{ value }}</span>
</template>
<template #cell-out_trade_no="{ value }">
<span class="text-sm text-gray-900 dark:text-white">{{ value }}</span>
</template>
<template #cell-amount="{ value, row }">
<div class="text-sm">
<span class="font-medium text-gray-900 dark:text-white">${{ value.toFixed(2) }}</span>
<span v-if="row.pay_amount !== value" class="ml-1 text-xs text-gray-500">(${{ row.pay_amount.toFixed(2) }})</span>
</div>
</template>
<template #cell-payment_type="{ value }">
<span class="text-sm text-gray-700 dark:text-gray-300">{{ t('payment.methods.' + value, value) }}</span>
</template>
<template #cell-status="{ value }">
<OrderStatusBadge :status="value" />
</template>
<template #cell-created_at="{ value }">
<span class="text-xs text-gray-500 dark:text-gray-400">{{ formatDate(value) }}</span>
</template>
<template #cell-actions="{ row }">
<OrderTable :orders="orders" :loading="loading">
<template #actions="{ row }">
<div class="flex items-center gap-2">
<button v-if="row.status === 'PENDING'" @click="handleCancel(row.id)" class="inline-flex items-center gap-1 rounded-md px-2 py-1 text-xs font-medium text-yellow-600 hover:bg-yellow-50 dark:text-yellow-400 dark:hover:bg-yellow-900/20">
<Icon name="x" size="sm" />
@@ -49,7 +28,7 @@
</button>
</div>
</template>
</DataTable>
</OrderTable>
<!-- Pagination -->
<Pagination
@@ -109,14 +88,12 @@ import { useAppStore } from '@/stores'
import { paymentAPI } from '@/api/payment'
import { extractApiErrorMessage } from '@/utils/apiError'
import type { PaymentOrder } from '@/types/payment'
import type { Column } from '@/components/common/types'
import AppLayout from '@/components/layout/AppLayout.vue'
import DataTable from '@/components/common/DataTable.vue'
import Pagination from '@/components/common/Pagination.vue'
import BaseDialog from '@/components/common/BaseDialog.vue'
import Select from '@/components/common/Select.vue'
import Icon from '@/components/icons/Icon.vue'
import OrderStatusBadge from '@/components/payment/OrderStatusBadge.vue'
import OrderTable from '@/components/payment/OrderTable.vue'
const { t } = useI18n()
const router = useRouter()
@@ -139,18 +116,6 @@ const statusFilters = computed(() => [
{ value: 'REFUNDED', label: t('payment.status.refunded') },
])
const columns = computed((): Column[] => [
{ key: 'id', label: t('payment.orders.orderId') },
{ key: 'out_trade_no', label: t('payment.orders.orderNo') },
{ 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 formatDate(dateStr: string) { return new Date(dateStr).toLocaleString() }
async function fetchOrders() {
loading.value = true
try {