mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-31 16:26:30 +00:00
fix(admin): 订单金额币种符号读取 currency 字段
This commit is contained in:
@@ -19,19 +19,19 @@
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">{{ t('payment.orders.baseAmount') }}</p>
|
||||
<p class="text-sm font-medium text-gray-900 dark:text-white">¥{{ baseAmount.toFixed(2) }}</p>
|
||||
<p class="text-sm font-medium text-gray-900 dark:text-white">{{ paymentAmountSymbol }}{{ baseAmount.toFixed(2) }}</p>
|
||||
</div>
|
||||
<div v-if="order.fee_rate > 0">
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">{{ t('payment.orders.fee') }} ({{ order.fee_rate }}%)</p>
|
||||
<p class="text-sm font-medium text-gray-900 dark:text-white">¥{{ feeAmount.toFixed(2) }}</p>
|
||||
<p class="text-sm font-medium text-gray-900 dark:text-white">{{ paymentAmountSymbol }}{{ feeAmount.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">¥{{ order.pay_amount.toFixed(2) }}</p>
|
||||
<p class="text-sm font-medium text-gray-900 dark:text-white">{{ paymentAmountSymbol }}{{ order.pay_amount.toFixed(2) }}</p>
|
||||
</div>
|
||||
<div v-if="order.amount !== order.pay_amount">
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">{{ t('payment.orders.creditedAmount') }}</p>
|
||||
<p class="text-sm font-medium text-gray-900 dark:text-white">{{ order.order_type === 'balance' ? '$' : '¥' }}{{ order.amount.toFixed(2) }}</p>
|
||||
<p class="text-sm font-medium text-gray-900 dark:text-white">{{ creditedAmountSymbol }}{{ order.amount.toFixed(2) }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">{{ t('payment.orders.paymentMethod') }}</p>
|
||||
@@ -77,7 +77,7 @@
|
||||
<div class="grid grid-cols-2 gap-2 text-sm">
|
||||
<div>
|
||||
<span class="text-red-600 dark:text-red-400">{{ t('payment.admin.refundAmount') }}:</span>
|
||||
<span class="ml-1 font-medium text-red-700 dark:text-red-300">{{ order.order_type === 'balance' ? '$' : '¥' }}{{ order.refund_amount.toFixed(2) }}</span>
|
||||
<span class="ml-1 font-medium text-red-700 dark:text-red-300">{{ creditedAmountSymbol }}{{ order.refund_amount.toFixed(2) }}</span>
|
||||
</div>
|
||||
<div v-if="order.refund_reason" class="col-span-2">
|
||||
<span class="text-red-600 dark:text-red-400">{{ t('payment.admin.refundReason') }}:</span>
|
||||
@@ -119,6 +119,7 @@ import { useI18n } from 'vue-i18n'
|
||||
import BaseDialog from '@/components/common/BaseDialog.vue'
|
||||
import type { PaymentOrder } from '@/types/payment'
|
||||
import { statusBadgeClass, canRefund as canRefundStatus, formatOrderDateTime } from '@/components/payment/orderUtils'
|
||||
import { currencySymbol } from '@/components/payment/currency'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
@@ -127,6 +128,10 @@ const props = defineProps<{
|
||||
order: PaymentOrder | null
|
||||
}>()
|
||||
|
||||
const creditedAmountSymbol = currencySymbol('USD')
|
||||
|
||||
const paymentAmountSymbol = computed(() => currencySymbol(props.order?.currency))
|
||||
|
||||
/** 充值金额 (base amount before fee) = pay_amount - fee = pay_amount / (1 + fee_rate/100) */
|
||||
const baseAmount = computed(() => {
|
||||
if (!props.order) return 0
|
||||
|
||||
@@ -53,12 +53,12 @@
|
||||
|
||||
<template #cell-pay_amount="{ value, row }">
|
||||
<div class="text-sm">
|
||||
<span class="font-medium text-gray-900 dark:text-white">¥{{ value.toFixed(2) }}</span>
|
||||
<span class="font-medium text-gray-900 dark:text-white">{{ paymentAmountSymbol(row) }}{{ value.toFixed(2) }}</span>
|
||||
<span v-if="row.fee_rate > 0" class="ml-1 text-xs text-gray-400" :title="t('payment.orders.fee') + ': ' + row.fee_rate + '%'">
|
||||
({{ row.fee_rate }}%)
|
||||
</span>
|
||||
<div v-if="row.amount !== row.pay_amount" class="text-xs text-gray-500">
|
||||
{{ t('payment.orders.creditedAmount') }}: {{ row.order_type === 'balance' ? '$' : '¥' }}{{ row.amount.toFixed(2) }}
|
||||
{{ t('payment.orders.creditedAmount') }}: {{ creditedAmountSymbol }}{{ row.amount.toFixed(2) }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -143,6 +143,7 @@ import Pagination from '@/components/common/Pagination.vue'
|
||||
import Select from '@/components/common/Select.vue'
|
||||
import Icon from '@/components/icons/Icon.vue'
|
||||
import { statusBadgeClass, canRefund, formatOrderDateTime } from '@/components/payment/orderUtils'
|
||||
import { currencySymbol } from '@/components/payment/currency'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
@@ -167,6 +168,11 @@ const emit = defineEmits<{
|
||||
|
||||
const searchQuery = ref('')
|
||||
const filters = reactive({ status: '', payment_type: '', order_type: '' })
|
||||
const creditedAmountSymbol = currencySymbol('USD')
|
||||
|
||||
function paymentAmountSymbol(order: PaymentOrder): string {
|
||||
return currencySymbol(order.currency)
|
||||
}
|
||||
|
||||
let debounceTimer: ReturnType<typeof setTimeout> | null = null
|
||||
function handleSearch() {
|
||||
|
||||
@@ -35,15 +35,15 @@
|
||||
</div>
|
||||
<div class="mt-1 flex justify-between text-sm">
|
||||
<span class="text-gray-500 dark:text-gray-400">{{ t('payment.orders.creditedAmount') }}</span>
|
||||
<span class="font-medium text-gray-900 dark:text-white">{{ order?.order_type === 'balance' ? '$' : '¥' }}{{ order?.amount?.toFixed(2) }}</span>
|
||||
<span class="font-medium text-gray-900 dark:text-white">{{ creditedAmountSymbol }}{{ order?.amount?.toFixed(2) }}</span>
|
||||
</div>
|
||||
<div class="mt-1 flex justify-between text-sm">
|
||||
<span class="text-gray-500 dark:text-gray-400">{{ t('payment.orders.payAmount') }}</span>
|
||||
<span class="font-medium text-gray-900 dark:text-white">¥{{ order?.pay_amount?.toFixed(2) }}</span>
|
||||
<span class="font-medium text-gray-900 dark:text-white">{{ paymentAmountSymbol }}{{ order?.pay_amount?.toFixed(2) }}</span>
|
||||
</div>
|
||||
<div v-if="actuallyRefunded > 0" class="mt-1 flex justify-between text-sm">
|
||||
<span class="text-gray-500 dark:text-gray-400">{{ t('payment.admin.alreadyRefunded') }}</span>
|
||||
<span class="font-medium text-red-600 dark:text-red-400">{{ order?.order_type === 'balance' ? '$' : '¥' }}{{ actuallyRefunded.toFixed(2) }}</span>
|
||||
<span class="font-medium text-red-600 dark:text-red-400">{{ creditedAmountSymbol }}{{ actuallyRefunded.toFixed(2) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -66,11 +66,11 @@
|
||||
<div v-if="form.deduct_balance && userBalance != null" class="mt-3 grid grid-cols-2 gap-3">
|
||||
<div class="rounded-lg bg-gray-50 p-3 text-sm dark:bg-dark-700">
|
||||
<div class="text-gray-500 dark:text-gray-400">{{ t('payment.admin.userBalance') }}</div>
|
||||
<div class="mt-1 font-semibold text-gray-900 dark:text-white">${{ userBalance.toFixed(2) }}</div>
|
||||
<div class="mt-1 font-semibold text-gray-900 dark:text-white">{{ creditedAmountSymbol }}{{ userBalance.toFixed(2) }}</div>
|
||||
</div>
|
||||
<div class="rounded-lg bg-gray-50 p-3 text-sm dark:bg-dark-700">
|
||||
<div class="text-gray-500 dark:text-gray-400">{{ t('payment.admin.orderAmount') }}</div>
|
||||
<div class="mt-1 font-semibold text-gray-900 dark:text-white">{{ order?.order_type === 'balance' ? '$' : '¥' }}{{ order?.amount?.toFixed(2) }}</div>
|
||||
<div class="mt-1 font-semibold text-gray-900 dark:text-white">{{ creditedAmountSymbol }}{{ order?.amount?.toFixed(2) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -95,7 +95,7 @@
|
||||
<div>
|
||||
<label class="input-label">{{ t('payment.admin.refundAmount') }}</label>
|
||||
<div class="relative">
|
||||
<span class="absolute left-3 top-1/2 -translate-y-1/2 text-gray-500">{{ order?.order_type === 'balance' ? '$' : '¥' }}</span>
|
||||
<span class="absolute left-3 top-1/2 -translate-y-1/2 text-gray-500">{{ creditedAmountSymbol }}</span>
|
||||
<input
|
||||
v-model.number="form.amount"
|
||||
type="number"
|
||||
@@ -107,7 +107,7 @@
|
||||
/>
|
||||
</div>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('payment.admin.maxRefundable') }}: {{ order?.order_type === 'balance' ? '$' : '¥' }}{{ maxRefundable.toFixed(2) }}
|
||||
{{ t('payment.admin.maxRefundable') }}: {{ creditedAmountSymbol }}{{ maxRefundable.toFixed(2) }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -169,6 +169,7 @@ import { useI18n } from 'vue-i18n'
|
||||
import BaseDialog from '@/components/common/BaseDialog.vue'
|
||||
import type { PaymentOrder } from '@/types/payment'
|
||||
import { formatOrderDateTime } from '@/components/payment/orderUtils'
|
||||
import { currencySymbol } from '@/components/payment/currency'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
@@ -186,6 +187,10 @@ const emit = defineEmits<{
|
||||
(e: 'cancel'): void
|
||||
}>()
|
||||
|
||||
const creditedAmountSymbol = currencySymbol('USD')
|
||||
|
||||
const paymentAmountSymbol = computed(() => currencySymbol(props.order?.currency))
|
||||
|
||||
const form = reactive({
|
||||
amount: 0,
|
||||
reason: '',
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import type { PaymentOrder } from '@/types/payment'
|
||||
import AdminOrderDetail from '../AdminOrderDetail.vue'
|
||||
import AdminOrderTable from '../AdminOrderTable.vue'
|
||||
import AdminRefundDialog from '../AdminRefundDialog.vue'
|
||||
import OrderTable from '@/components/payment/OrderTable.vue'
|
||||
|
||||
vi.mock('vue-i18n', async () => {
|
||||
const actual = await vi.importActual<typeof import('vue-i18n')>('vue-i18n')
|
||||
return {
|
||||
...actual,
|
||||
useI18n: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}
|
||||
})
|
||||
|
||||
const BaseDialogStub = {
|
||||
props: ['show'],
|
||||
template: '<div v-if="show"><slot /><slot name="footer" /></div>',
|
||||
}
|
||||
|
||||
const DataTableStub = {
|
||||
props: ['data'],
|
||||
template: `
|
||||
<div>
|
||||
<div v-for="row in data" :key="row.id">
|
||||
<slot name="cell-pay_amount" :value="row.pay_amount" :row="row" />
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
}
|
||||
|
||||
function orderFactory(overrides: Partial<PaymentOrder> = {}): PaymentOrder {
|
||||
return {
|
||||
id: 1,
|
||||
user_id: 10,
|
||||
amount: 100,
|
||||
pay_amount: 108,
|
||||
currency: 'USD',
|
||||
fee_rate: 8,
|
||||
payment_type: 'stripe',
|
||||
out_trade_no: 'sub2_202606250001',
|
||||
status: 'COMPLETED',
|
||||
order_type: 'subscription',
|
||||
created_at: '2026-06-25T10:00:00Z',
|
||||
expires_at: '2026-06-25T10:30:00Z',
|
||||
refund_amount: 25,
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
describe('admin order currency display', () => {
|
||||
it('uses order currency for paid/base/fee amounts and USD for credited/refund amounts', () => {
|
||||
const wrapper = mount(AdminOrderDetail, {
|
||||
props: {
|
||||
show: true,
|
||||
order: orderFactory({ currency: 'CNY' }),
|
||||
},
|
||||
global: {
|
||||
stubs: {
|
||||
BaseDialog: BaseDialogStub,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const text = wrapper.text()
|
||||
expect(text).toContain('¥100.00')
|
||||
expect(text).toContain('¥8.00')
|
||||
expect(text).toContain('¥108.00')
|
||||
expect(text).toContain('$100.00')
|
||||
expect(text).toContain('$25.00')
|
||||
})
|
||||
|
||||
it('uses order currency for pay_amount and USD for refundable balance amounts', () => {
|
||||
const wrapper = mount(AdminRefundDialog, {
|
||||
props: {
|
||||
show: true,
|
||||
order: orderFactory({
|
||||
currency: 'USD',
|
||||
status: 'PARTIALLY_REFUNDED',
|
||||
refund_amount: 20,
|
||||
}),
|
||||
userBalance: 200,
|
||||
},
|
||||
global: {
|
||||
stubs: {
|
||||
BaseDialog: BaseDialogStub,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const text = wrapper.text()
|
||||
expect(text).toContain('$108.00')
|
||||
expect(text).toContain('$100.00')
|
||||
expect(text).toContain('$20.00')
|
||||
expect(text).toContain('$80.00')
|
||||
expect(text).toContain('$200.00')
|
||||
})
|
||||
|
||||
it('renders payment currency consistently in the shared order table', () => {
|
||||
const wrapper = mount(OrderTable, {
|
||||
props: {
|
||||
orders: [
|
||||
orderFactory({ id: 1, currency: 'USD', amount: 100, pay_amount: 108 }),
|
||||
orderFactory({ id: 2, currency: 'CNY', amount: 100, pay_amount: 108 }),
|
||||
],
|
||||
loading: false,
|
||||
showUser: true,
|
||||
},
|
||||
global: {
|
||||
stubs: {
|
||||
DataTable: DataTableStub,
|
||||
OrderStatusBadge: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const text = wrapper.text()
|
||||
expect(text).toContain('$108.00')
|
||||
expect(text).toContain('¥108.00')
|
||||
expect(text).toContain('$100.00')
|
||||
})
|
||||
|
||||
it('renders payment currency consistently in the admin order table', () => {
|
||||
const wrapper = mount(AdminOrderTable, {
|
||||
props: {
|
||||
orders: [
|
||||
orderFactory({ id: 1, currency: 'USD', amount: 100, pay_amount: 108 }),
|
||||
orderFactory({ id: 2, currency: 'CNY', amount: 100, pay_amount: 108 }),
|
||||
],
|
||||
loading: false,
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: 2,
|
||||
},
|
||||
global: {
|
||||
stubs: {
|
||||
DataTable: DataTableStub,
|
||||
Icon: true,
|
||||
Pagination: true,
|
||||
Select: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const text = wrapper.text()
|
||||
expect(text).toContain('$108.00')
|
||||
expect(text).toContain('¥108.00')
|
||||
expect(text).toContain('$100.00')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user