add refund items page
This commit is contained in:
@@ -0,0 +1,536 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
||||||
|
import { setActivePinia, createPinia } from 'pinia'
|
||||||
|
import { mount, flushPromises } from '@vue/test-utils'
|
||||||
|
import { nextTick } from 'vue'
|
||||||
|
import { useRefundItemStore } from '@/stores/refund-item'
|
||||||
|
import type { RefundItemRecord } from '@/stores/refund-item'
|
||||||
|
|
||||||
|
// jsdom 不支持 matchMedia,Ant Design Vue 响应式布局需要
|
||||||
|
Object.defineProperty(window, 'matchMedia', {
|
||||||
|
writable: true,
|
||||||
|
value: (query: string) => ({
|
||||||
|
matches: false,
|
||||||
|
media: query,
|
||||||
|
onchange: null,
|
||||||
|
addListener: () => {},
|
||||||
|
removeListener: () => {},
|
||||||
|
addEventListener: () => {},
|
||||||
|
removeEventListener: () => {},
|
||||||
|
dispatchEvent: () => false,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
vi.mock('@/utils/request', () => ({
|
||||||
|
api: {
|
||||||
|
get: vi.fn(),
|
||||||
|
post: vi.fn(),
|
||||||
|
put: vi.fn(),
|
||||||
|
patch: vi.fn(),
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
const mockRefundItems: RefundItemRecord[] = [
|
||||||
|
{
|
||||||
|
id: 301,
|
||||||
|
company_id: 1,
|
||||||
|
platform_id: 1,
|
||||||
|
store_id: 1,
|
||||||
|
refund_id: 201,
|
||||||
|
platform_refund_id: 'RF-20260101-001',
|
||||||
|
platform_order_id: 'ORD-20260101-001',
|
||||||
|
platform_sub_order_id: 'SUB-001',
|
||||||
|
platform_product_id: 'PROD-001',
|
||||||
|
refund_status_id: 1,
|
||||||
|
refund_type_id: 1,
|
||||||
|
quantity: 2,
|
||||||
|
refund_amount: '99.99',
|
||||||
|
currency: 'CNY',
|
||||||
|
created_date: '2026-01-01T10:00:00Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 302,
|
||||||
|
company_id: 2,
|
||||||
|
platform_id: 1,
|
||||||
|
store_id: 2,
|
||||||
|
refund_id: 202,
|
||||||
|
platform_refund_id: 'RF-20260201-002',
|
||||||
|
platform_order_id: 'ORD-20260201-002',
|
||||||
|
platform_sub_order_id: null,
|
||||||
|
platform_product_id: 'PROD-002',
|
||||||
|
refund_status_id: 5,
|
||||||
|
refund_type_id: 2,
|
||||||
|
quantity: 1,
|
||||||
|
refund_amount: '59.00',
|
||||||
|
currency: 'USD',
|
||||||
|
created_date: '2026-02-01T14:00:00Z',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const mockPaginatedResponse = {
|
||||||
|
items: mockRefundItems,
|
||||||
|
total: 2,
|
||||||
|
page: 1,
|
||||||
|
per_page: 15,
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockRefundItemDetail = {
|
||||||
|
...mockRefundItems[0],
|
||||||
|
platform_parent_refund_id: 'RF-PARENT-001',
|
||||||
|
reason: '商品质量问题',
|
||||||
|
buyer_user_id: 'buyer-001',
|
||||||
|
order_created_date: '2025-12-30T08:00:00Z',
|
||||||
|
order_paid_date: '2025-12-30T08:05:00Z',
|
||||||
|
updated_date: '2026-01-02T10:00:00Z',
|
||||||
|
completed_date: '2026-01-03T12:00:00Z',
|
||||||
|
ext: null,
|
||||||
|
created_at: '2026-01-01T10:00:00Z',
|
||||||
|
updated_at: '2026-01-02T10:00:00Z',
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockLookupCompanies = [
|
||||||
|
{ id: 1, name: 'Company A', label: '公司A' },
|
||||||
|
{ id: 2, name: 'Company B', label: '公司B' },
|
||||||
|
]
|
||||||
|
const mockLookupPlatforms = [{ id: 1, developer_id: 1 }]
|
||||||
|
const mockLookupStores = [
|
||||||
|
{ id: 1, company_id: 1, platform_id: 1, name: 'Store 1', label: '店铺1' },
|
||||||
|
{ id: 2, company_id: 2, platform_id: 1, name: 'Store 2', label: '店铺2' },
|
||||||
|
]
|
||||||
|
|
||||||
|
// ─── Store Tests ───────────────────────────────────────
|
||||||
|
|
||||||
|
describe('useRefundItemStore', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
setActivePinia(createPinia())
|
||||||
|
vi.restoreAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('initial state', () => {
|
||||||
|
it('starts with empty refundItems and default pagination', () => {
|
||||||
|
const store = useRefundItemStore()
|
||||||
|
expect(store.refundItems).toEqual([])
|
||||||
|
expect(store.loading).toBe(false)
|
||||||
|
expect(store.pagination.page).toBe(1)
|
||||||
|
expect(store.pagination.per_page).toBe(15)
|
||||||
|
expect(store.pagination.total).toBe(0)
|
||||||
|
expect(store.filters.refund_id).toBeUndefined()
|
||||||
|
expect(store.filters.refund_status_id).toBeUndefined()
|
||||||
|
expect(store.filters.refund_type_id).toBeUndefined()
|
||||||
|
expect(store.filters.platform_refund_id).toBe('')
|
||||||
|
expect(store.filters.platform_order_id).toBe('')
|
||||||
|
expect(store.filters.platform_product_id).toBe('')
|
||||||
|
expect(store.filters.created_date_range).toBeNull()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('fetchRefundItems', () => {
|
||||||
|
it('loads data and updates state', async () => {
|
||||||
|
const { api } = await import('@/utils/request')
|
||||||
|
vi.mocked(api.get).mockResolvedValueOnce(mockPaginatedResponse)
|
||||||
|
|
||||||
|
const store = useRefundItemStore()
|
||||||
|
await store.fetchRefundItems()
|
||||||
|
|
||||||
|
expect(api.get).toHaveBeenCalledWith(
|
||||||
|
'/api/v1/refund-items',
|
||||||
|
expect.objectContaining({
|
||||||
|
page: 1,
|
||||||
|
per_page: 15,
|
||||||
|
refund_id: undefined,
|
||||||
|
refund_status_id: undefined,
|
||||||
|
refund_type_id: undefined,
|
||||||
|
platform_refund_id: undefined,
|
||||||
|
platform_order_id: undefined,
|
||||||
|
platform_product_id: undefined,
|
||||||
|
created_date_from: undefined,
|
||||||
|
created_date_to: undefined,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
expect(store.refundItems).toEqual(mockRefundItems)
|
||||||
|
expect(store.pagination.total).toBe(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('passes filter params correctly', async () => {
|
||||||
|
const { api } = await import('@/utils/request')
|
||||||
|
vi.mocked(api.get).mockResolvedValueOnce(mockPaginatedResponse)
|
||||||
|
|
||||||
|
const store = useRefundItemStore()
|
||||||
|
store.filters.refund_id = 201
|
||||||
|
store.filters.refund_status_id = 1
|
||||||
|
store.filters.refund_type_id = 2
|
||||||
|
store.filters.platform_refund_id = 'RF-001'
|
||||||
|
store.filters.platform_order_id = 'ORD-001'
|
||||||
|
store.filters.platform_product_id = 'PROD-001'
|
||||||
|
store.filters.created_date_range = ['2026-01-01', '2026-01-31']
|
||||||
|
store.cascadeValue.company_id = 1
|
||||||
|
store.cascadeValue.platform_id = 2
|
||||||
|
store.cascadeValue.store_id = 3
|
||||||
|
|
||||||
|
await store.fetchRefundItems()
|
||||||
|
|
||||||
|
expect(api.get).toHaveBeenCalledWith(
|
||||||
|
'/api/v1/refund-items',
|
||||||
|
expect.objectContaining({
|
||||||
|
refund_id: 201,
|
||||||
|
refund_status_id: 1,
|
||||||
|
refund_type_id: 2,
|
||||||
|
platform_refund_id: 'RF-001',
|
||||||
|
platform_order_id: 'ORD-001',
|
||||||
|
platform_product_id: 'PROD-001',
|
||||||
|
created_date_from: '2026-01-01',
|
||||||
|
created_date_to: '2026-01-31',
|
||||||
|
company_id: 1,
|
||||||
|
platform_id: 2,
|
||||||
|
store_id: 3,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows error message on failure', async () => {
|
||||||
|
const { api } = await import('@/utils/request')
|
||||||
|
const { message } = await import('ant-design-vue')
|
||||||
|
const errorSpy = vi.spyOn(message, 'error')
|
||||||
|
vi.mocked(api.get).mockRejectedValueOnce(new Error('Network error'))
|
||||||
|
|
||||||
|
const store = useRefundItemStore()
|
||||||
|
await store.fetchRefundItems()
|
||||||
|
|
||||||
|
expect(errorSpy).toHaveBeenCalledWith('Network error')
|
||||||
|
expect(store.refundItems).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('clears refundItems on fetch failure', async () => {
|
||||||
|
const { api } = await import('@/utils/request')
|
||||||
|
const { message } = await import('ant-design-vue')
|
||||||
|
vi.spyOn(message, 'error')
|
||||||
|
|
||||||
|
const store = useRefundItemStore()
|
||||||
|
vi.mocked(api.get).mockResolvedValueOnce(mockPaginatedResponse)
|
||||||
|
await store.fetchRefundItems()
|
||||||
|
expect(store.refundItems.length).toBe(2)
|
||||||
|
|
||||||
|
vi.mocked(api.get).mockRejectedValueOnce(new Error('Server error'))
|
||||||
|
await store.fetchRefundItems()
|
||||||
|
|
||||||
|
expect(store.refundItems).toEqual([])
|
||||||
|
expect(store.pagination.total).toBe(0)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('resetFilters', () => {
|
||||||
|
it('clears all filters and resets page to 1', () => {
|
||||||
|
const store = useRefundItemStore()
|
||||||
|
store.filters.refund_id = 201
|
||||||
|
store.filters.refund_status_id = 1
|
||||||
|
store.filters.refund_type_id = 2
|
||||||
|
store.filters.platform_refund_id = 'RF-001'
|
||||||
|
store.filters.platform_order_id = 'ORD-001'
|
||||||
|
store.filters.platform_product_id = 'PROD-001'
|
||||||
|
store.filters.created_date_range = ['2026-01-01', '2026-01-31']
|
||||||
|
store.cascadeValue.company_id = 1
|
||||||
|
store.cascadeValue.platform_id = 2
|
||||||
|
store.cascadeValue.store_id = 3
|
||||||
|
store.pagination.page = 5
|
||||||
|
|
||||||
|
store.resetFilters()
|
||||||
|
|
||||||
|
expect(store.filters.refund_id).toBeUndefined()
|
||||||
|
expect(store.filters.refund_status_id).toBeUndefined()
|
||||||
|
expect(store.filters.refund_type_id).toBeUndefined()
|
||||||
|
expect(store.filters.platform_refund_id).toBe('')
|
||||||
|
expect(store.filters.platform_order_id).toBe('')
|
||||||
|
expect(store.filters.platform_product_id).toBe('')
|
||||||
|
expect(store.filters.created_date_range).toBeNull()
|
||||||
|
expect(store.cascadeValue.company_id).toBeUndefined()
|
||||||
|
expect(store.cascadeValue.platform_id).toBeUndefined()
|
||||||
|
expect(store.cascadeValue.store_id).toBeUndefined()
|
||||||
|
expect(store.pagination.page).toBe(1)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('name maps', () => {
|
||||||
|
it('builds lookup maps from loaded data', async () => {
|
||||||
|
const { api } = await import('@/utils/request')
|
||||||
|
vi.mocked(api.get)
|
||||||
|
.mockResolvedValueOnce(mockLookupCompanies)
|
||||||
|
.mockResolvedValueOnce(mockLookupPlatforms)
|
||||||
|
.mockResolvedValueOnce(mockLookupStores)
|
||||||
|
|
||||||
|
const store = useRefundItemStore()
|
||||||
|
await store.loadLookups()
|
||||||
|
|
||||||
|
expect(store.companyMap.get(1)).toBe('公司A')
|
||||||
|
expect(store.platformMap.get(1)).toBe('平台 #1')
|
||||||
|
expect(store.storeMap.get(1)).toBe('店铺1')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('handles loadLookups failure gracefully', async () => {
|
||||||
|
const { api } = await import('@/utils/request')
|
||||||
|
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
||||||
|
vi.mocked(api.get).mockRejectedValueOnce(new Error('Network error'))
|
||||||
|
|
||||||
|
const store = useRefundItemStore()
|
||||||
|
await store.loadLookups()
|
||||||
|
|
||||||
|
expect(warnSpy).toHaveBeenCalledWith('加载查找表数据失败', expect.any(Error))
|
||||||
|
expect(store.companyMap.size).toBe(0)
|
||||||
|
warnSpy.mockRestore()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// ─── Page Component Tests ──────────────────────────────
|
||||||
|
|
||||||
|
describe('RefundItemsPage', () => {
|
||||||
|
let wrapper: ReturnType<typeof mount>
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
setActivePinia(createPinia())
|
||||||
|
vi.restoreAllMocks()
|
||||||
|
document.body.innerHTML = ''
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
wrapper?.unmount()
|
||||||
|
document.body.innerHTML = ''
|
||||||
|
})
|
||||||
|
|
||||||
|
async function mountPage() {
|
||||||
|
const { api } = await import('@/utils/request')
|
||||||
|
vi.mocked(api.get).mockImplementation((url: string) => {
|
||||||
|
if (url === '/api/v1/refund-items') return Promise.resolve(mockPaginatedResponse) as never
|
||||||
|
if (url === '/api/v1/companies') return Promise.resolve(mockLookupCompanies) as never
|
||||||
|
if (url === '/api/v1/platforms') return Promise.resolve(mockLookupPlatforms) as never
|
||||||
|
if (url === '/api/v1/stores') return Promise.resolve(mockLookupStores) as never
|
||||||
|
return Promise.resolve([]) as never
|
||||||
|
})
|
||||||
|
|
||||||
|
const { default: RefundItemsPage } = await import('../index.vue')
|
||||||
|
|
||||||
|
wrapper = mount(RefundItemsPage, {
|
||||||
|
attachTo: document.body,
|
||||||
|
global: {
|
||||||
|
stubs: {
|
||||||
|
SearchOutlined: { template: '<span />' },
|
||||||
|
ReloadOutlined: { template: '<span />' },
|
||||||
|
EyeOutlined: { template: '<span />' },
|
||||||
|
CopyOutlined: { template: '<span class="copy-icon-stub" />' },
|
||||||
|
CascadeFilter: { template: '<div class="cascade-filter-stub" />' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
await flushPromises()
|
||||||
|
await nextTick()
|
||||||
|
return { wrapper, api }
|
||||||
|
}
|
||||||
|
|
||||||
|
it('renders page title and action buttons', async () => {
|
||||||
|
await mountPage()
|
||||||
|
|
||||||
|
expect(wrapper.text()).toContain('退款项管理')
|
||||||
|
const buttons = wrapper.findAll('.ant-btn')
|
||||||
|
const buttonTexts = buttons.map((b) => b.text())
|
||||||
|
expect(buttonTexts.some((t) => t.includes('搜索'))).toBe(true)
|
||||||
|
expect(buttonTexts.some((t) => t.includes('重置'))).toBe(true)
|
||||||
|
}, 15000)
|
||||||
|
|
||||||
|
it('calls fetchRefundItems and loadLookups on mount', async () => {
|
||||||
|
const { api } = await mountPage()
|
||||||
|
|
||||||
|
expect(api.get).toHaveBeenCalledWith(
|
||||||
|
'/api/v1/refund-items',
|
||||||
|
expect.objectContaining({
|
||||||
|
page: 1,
|
||||||
|
per_page: 15,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
expect(api.get).toHaveBeenCalledWith('/api/v1/companies')
|
||||||
|
expect(api.get).toHaveBeenCalledWith('/api/v1/platforms')
|
||||||
|
expect(api.get).toHaveBeenCalledWith('/api/v1/stores')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders table with refund item data', async () => {
|
||||||
|
await mountPage()
|
||||||
|
|
||||||
|
const html = wrapper.html()
|
||||||
|
expect(html).toContain('RF-20260101-001')
|
||||||
|
expect(html).toContain('ORD-20260101-001')
|
||||||
|
expect(html).toContain('99.99')
|
||||||
|
expect(html).toContain('PROD-001')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders refund status tags correctly', async () => {
|
||||||
|
await mountPage()
|
||||||
|
|
||||||
|
const html = wrapper.html()
|
||||||
|
expect(html).toContain('退款申请中')
|
||||||
|
expect(html).toContain('退款成功')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders refund type tags correctly', async () => {
|
||||||
|
await mountPage()
|
||||||
|
|
||||||
|
const html = wrapper.html()
|
||||||
|
expect(html).toContain('未发货前退款')
|
||||||
|
expect(html).toContain('退货退款')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders company/platform/store names from lookup maps', async () => {
|
||||||
|
await mountPage()
|
||||||
|
|
||||||
|
const html = wrapper.html()
|
||||||
|
expect(html).toContain('公司A')
|
||||||
|
expect(html).toContain('店铺1')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders refund_id as clickable link', async () => {
|
||||||
|
await mountPage()
|
||||||
|
|
||||||
|
const html = wrapper.html()
|
||||||
|
// refund_id 201 should appear as a link
|
||||||
|
expect(html).toContain('201')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders CascadeFilter component', async () => {
|
||||||
|
await mountPage()
|
||||||
|
|
||||||
|
expect(wrapper.find('.cascade-filter-stub').exists()).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('search button resets page to 1 and fetches', async () => {
|
||||||
|
const { api } = await mountPage()
|
||||||
|
|
||||||
|
const store = useRefundItemStore()
|
||||||
|
store.pagination.page = 3
|
||||||
|
store.filters.platform_refund_id = 'RF-001'
|
||||||
|
vi.mocked(api.get).mockClear()
|
||||||
|
vi.mocked(api.get).mockResolvedValue(mockPaginatedResponse)
|
||||||
|
|
||||||
|
const buttons = Array.from(document.body.querySelectorAll('.ant-btn'))
|
||||||
|
const searchBtn = buttons.find((b) => b.textContent?.trim().includes('搜索'))
|
||||||
|
searchBtn?.dispatchEvent(new MouseEvent('click', { bubbles: true }))
|
||||||
|
await flushPromises()
|
||||||
|
await nextTick()
|
||||||
|
|
||||||
|
expect(store.pagination.page).toBe(1)
|
||||||
|
expect(api.get).toHaveBeenCalledWith(
|
||||||
|
'/api/v1/refund-items',
|
||||||
|
expect.objectContaining({
|
||||||
|
page: 1,
|
||||||
|
platform_refund_id: 'RF-001',
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('resets filters when clicking reset button', async () => {
|
||||||
|
await mountPage()
|
||||||
|
|
||||||
|
const store = useRefundItemStore()
|
||||||
|
store.filters.refund_id = 201
|
||||||
|
store.filters.refund_status_id = 1
|
||||||
|
store.filters.refund_type_id = 2
|
||||||
|
store.filters.platform_refund_id = 'RF-001'
|
||||||
|
store.filters.platform_order_id = 'ORD-001'
|
||||||
|
store.filters.platform_product_id = 'PROD-001'
|
||||||
|
store.filters.created_date_range = ['2026-01-01', '2026-01-31']
|
||||||
|
|
||||||
|
const buttons = Array.from(document.body.querySelectorAll('.ant-btn'))
|
||||||
|
const resetBtn = buttons.find((b) => b.textContent?.trim().includes('重置'))
|
||||||
|
expect(resetBtn).toBeDefined()
|
||||||
|
|
||||||
|
resetBtn?.dispatchEvent(new MouseEvent('click', { bubbles: true }))
|
||||||
|
await flushPromises()
|
||||||
|
await nextTick()
|
||||||
|
|
||||||
|
expect(store.filters.refund_id).toBeUndefined()
|
||||||
|
expect(store.filters.refund_status_id).toBeUndefined()
|
||||||
|
expect(store.filters.refund_type_id).toBeUndefined()
|
||||||
|
expect(store.filters.platform_refund_id).toBe('')
|
||||||
|
expect(store.filters.platform_order_id).toBe('')
|
||||||
|
expect(store.filters.platform_product_id).toBe('')
|
||||||
|
expect(store.filters.created_date_range).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('opens detail drawer when clicking view button', async () => {
|
||||||
|
const { api } = await mountPage()
|
||||||
|
vi.mocked(api.get).mockResolvedValueOnce(mockRefundItemDetail)
|
||||||
|
|
||||||
|
const buttons = Array.from(document.body.querySelectorAll('.ant-btn'))
|
||||||
|
const viewBtn = buttons.find((b) => b.textContent?.trim() === '查看')
|
||||||
|
expect(viewBtn).toBeDefined()
|
||||||
|
|
||||||
|
viewBtn?.dispatchEvent(new MouseEvent('click', { bubbles: true }))
|
||||||
|
await flushPromises()
|
||||||
|
await nextTick()
|
||||||
|
|
||||||
|
const drawer = document.body.querySelector('.ant-drawer')
|
||||||
|
expect(drawer).toBeTruthy()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('displays refund item detail info in drawer', async () => {
|
||||||
|
const { api } = await mountPage()
|
||||||
|
vi.mocked(api.get).mockResolvedValueOnce(mockRefundItemDetail)
|
||||||
|
|
||||||
|
const buttons = Array.from(document.body.querySelectorAll('.ant-btn'))
|
||||||
|
const viewBtn = buttons.find((b) => b.textContent?.trim() === '查看')
|
||||||
|
viewBtn?.dispatchEvent(new MouseEvent('click', { bubbles: true }))
|
||||||
|
await flushPromises()
|
||||||
|
await nextTick()
|
||||||
|
|
||||||
|
const drawerHtml = document.body.querySelector('.ant-drawer')?.innerHTML || ''
|
||||||
|
expect(drawerHtml).toContain('退款项详情')
|
||||||
|
expect(drawerHtml).toContain('RF-20260101-001')
|
||||||
|
expect(drawerHtml).toContain('商品质量问题')
|
||||||
|
expect(drawerHtml).toContain('buyer-001')
|
||||||
|
expect(drawerHtml).toContain('退款申请中')
|
||||||
|
expect(drawerHtml).toContain('RF-PARENT-001')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('displays ext JSON in drawer when available', async () => {
|
||||||
|
const { api } = await mountPage()
|
||||||
|
const detailWithExt = {
|
||||||
|
...mockRefundItemDetail,
|
||||||
|
ext: { return_logistics: 'SF1234567890', warehouse: 'WH-01' },
|
||||||
|
}
|
||||||
|
vi.mocked(api.get).mockResolvedValueOnce(detailWithExt)
|
||||||
|
|
||||||
|
const buttons = Array.from(document.body.querySelectorAll('.ant-btn'))
|
||||||
|
const viewBtn = buttons.find((b) => b.textContent?.trim() === '查看')
|
||||||
|
viewBtn?.dispatchEvent(new MouseEvent('click', { bubbles: true }))
|
||||||
|
await flushPromises()
|
||||||
|
await nextTick()
|
||||||
|
|
||||||
|
const drawerHtml = document.body.querySelector('.ant-drawer')?.innerHTML || ''
|
||||||
|
expect(drawerHtml).toContain('return_logistics')
|
||||||
|
expect(drawerHtml).toContain('SF1234567890')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows placeholder when ext is null', async () => {
|
||||||
|
const { api } = await mountPage()
|
||||||
|
vi.mocked(api.get).mockResolvedValueOnce({ ...mockRefundItemDetail, ext: null })
|
||||||
|
|
||||||
|
const buttons = Array.from(document.body.querySelectorAll('.ant-btn'))
|
||||||
|
const viewBtn = buttons.find((b) => b.textContent?.trim() === '查看')
|
||||||
|
viewBtn?.dispatchEvent(new MouseEvent('click', { bubbles: true }))
|
||||||
|
await flushPromises()
|
||||||
|
await nextTick()
|
||||||
|
|
||||||
|
const drawerHtml = document.body.querySelector('.ant-drawer')?.innerHTML || ''
|
||||||
|
expect(drawerHtml).toContain('暂无数据')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows error message when detail request fails', async () => {
|
||||||
|
const { api } = await mountPage()
|
||||||
|
const { message } = await import('ant-design-vue')
|
||||||
|
const errorSpy = vi.spyOn(message, 'error')
|
||||||
|
vi.mocked(api.get).mockRejectedValueOnce(new Error('Not found'))
|
||||||
|
|
||||||
|
const buttons = Array.from(document.body.querySelectorAll('.ant-btn'))
|
||||||
|
const viewBtn = buttons.find((b) => b.textContent?.trim() === '查看')
|
||||||
|
viewBtn?.dispatchEvent(new MouseEvent('click', { bubbles: true }))
|
||||||
|
await flushPromises()
|
||||||
|
await nextTick()
|
||||||
|
|
||||||
|
expect(errorSpy).toHaveBeenCalledWith('获取退款项详情失败')
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,460 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
import { api } from '@/utils/request'
|
||||||
|
import { useRefundItemStore, type RefundItemRecord } from '@/stores/refund-item'
|
||||||
|
import type { RefundItemDetail } from '@/types/api'
|
||||||
|
import CascadeFilter from '@/components/CascadeFilter.vue'
|
||||||
|
import {
|
||||||
|
SearchOutlined,
|
||||||
|
ReloadOutlined,
|
||||||
|
EyeOutlined,
|
||||||
|
CopyOutlined,
|
||||||
|
} from '@ant-design/icons-vue'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const store = useRefundItemStore()
|
||||||
|
|
||||||
|
// Detail drawer
|
||||||
|
const drawerVisible = ref(false)
|
||||||
|
const drawerLoading = ref(false)
|
||||||
|
const refundItemDetail = ref<RefundItemDetail | null>(null)
|
||||||
|
let detailRequestId = 0
|
||||||
|
|
||||||
|
const refundStatusMap: Record<number, { text: string; color: string }> = {
|
||||||
|
1: { text: '退款申请中', color: 'processing' },
|
||||||
|
2: { text: '卖家已同意', color: 'blue' },
|
||||||
|
3: { text: '卖家举证', color: 'volcano' },
|
||||||
|
4: { text: '卖家拒绝', color: 'red' },
|
||||||
|
5: { text: '退款成功', color: 'green' },
|
||||||
|
6: { text: '退款关闭', color: 'default' },
|
||||||
|
}
|
||||||
|
|
||||||
|
const refundTypeMap: Record<number, { text: string; color: string }> = {
|
||||||
|
1: { text: '未发货前退款', color: 'blue' },
|
||||||
|
2: { text: '退货退款', color: 'orange' },
|
||||||
|
3: { text: '退货后部分退款', color: 'gold' },
|
||||||
|
4: { text: '无须退货退款', color: 'cyan' },
|
||||||
|
5: { text: '闪电退款', color: 'purple' },
|
||||||
|
}
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{ title: 'ID', dataIndex: 'id', width: 80, fixed: 'left' as const },
|
||||||
|
{ title: '公司', key: 'company', width: 100 },
|
||||||
|
{ title: '平台', key: 'platform', width: 100 },
|
||||||
|
{ title: '店铺', key: 'store', width: 120 },
|
||||||
|
{ title: '退款单ID', key: 'refund_id', width: 100 },
|
||||||
|
{ title: '平台退款ID', key: 'platform_refund_id', width: 160, ellipsis: true },
|
||||||
|
{ title: '关联订单ID', key: 'platform_order_id', width: 160, ellipsis: true },
|
||||||
|
{ title: '子订单ID', dataIndex: 'platform_sub_order_id', width: 140, ellipsis: true },
|
||||||
|
{ title: '平台商品ID', dataIndex: 'platform_product_id', width: 140, ellipsis: true },
|
||||||
|
{ title: '退款状态', key: 'refund_status', width: 110 },
|
||||||
|
{ title: '退款类型', key: 'refund_type', width: 130 },
|
||||||
|
{ title: '数量', dataIndex: 'quantity', width: 80 },
|
||||||
|
{ title: '退款金额', key: 'refund_amount', width: 120 },
|
||||||
|
{ title: '币种', dataIndex: 'currency', width: 80 },
|
||||||
|
{ title: '创建时间', key: 'created_date', width: 140 },
|
||||||
|
{ title: '操作', key: 'action', width: 100, fixed: 'right' as const },
|
||||||
|
]
|
||||||
|
|
||||||
|
// RangePicker dayjs 桥接
|
||||||
|
const createdDateRange = computed({
|
||||||
|
get: () => {
|
||||||
|
if (!store.filters.created_date_range) return undefined
|
||||||
|
return store.filters.created_date_range.map((d) => dayjs(d)) as [
|
||||||
|
dayjs.Dayjs,
|
||||||
|
dayjs.Dayjs,
|
||||||
|
]
|
||||||
|
},
|
||||||
|
set: (val) => {
|
||||||
|
store.filters.created_date_range = val
|
||||||
|
? [val[0].format('YYYY-MM-DD'), val[1].format('YYYY-MM-DD')]
|
||||||
|
: null
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
store.loadLookups()
|
||||||
|
store.fetchRefundItems()
|
||||||
|
})
|
||||||
|
|
||||||
|
function handleSearch() {
|
||||||
|
store.pagination.page = 1
|
||||||
|
store.fetchRefundItems()
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleReset() {
|
||||||
|
store.resetFilters()
|
||||||
|
store.fetchRefundItems()
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePageChange(page: number, pageSize: number) {
|
||||||
|
store.pagination.page = page
|
||||||
|
store.pagination.per_page = pageSize
|
||||||
|
store.fetchRefundItems()
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatAmount(val: string | null | undefined) {
|
||||||
|
if (!val || val === '0' || val === '0.00') return '-'
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTime(time: string | null) {
|
||||||
|
if (!time) return '-'
|
||||||
|
return time.replace('T', ' ').substring(0, 16)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleCopyId(text: string, label: string) {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(text)
|
||||||
|
message.success(`已复制${label}`)
|
||||||
|
} catch {
|
||||||
|
message.error('复制失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleGoToOrder(platformOrderId: string) {
|
||||||
|
router.push({ path: '/orders', query: { platform_order_id: platformOrderId } })
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleGoToRefund(refundId: number) {
|
||||||
|
router.push({ path: '/refunds', query: { id: String(refundId) } })
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleViewDetail(record: { id: number }) {
|
||||||
|
const currentRequestId = ++detailRequestId
|
||||||
|
drawerVisible.value = true
|
||||||
|
drawerLoading.value = true
|
||||||
|
refundItemDetail.value = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
const detail = await api.get<RefundItemDetail>(`/api/v1/refund-items/${record.id}`)
|
||||||
|
if (currentRequestId !== detailRequestId) return
|
||||||
|
refundItemDetail.value = detail
|
||||||
|
} catch {
|
||||||
|
if (currentRequestId !== detailRequestId) return
|
||||||
|
message.error('获取退款项详情失败')
|
||||||
|
} finally {
|
||||||
|
if (currentRequestId === detailRequestId) {
|
||||||
|
drawerLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<h2 class="text-xl font-semibold mb-4">退款项管理</h2>
|
||||||
|
|
||||||
|
<!-- Filter area -->
|
||||||
|
<a-card class="mb-4">
|
||||||
|
<a-form layout="inline" @submit.prevent="handleSearch">
|
||||||
|
<a-form-item>
|
||||||
|
<CascadeFilter v-model="store.cascadeValue" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="退款单ID">
|
||||||
|
<a-input-number
|
||||||
|
v-model:value="store.filters.refund_id"
|
||||||
|
placeholder="精确搜索"
|
||||||
|
:controls="false"
|
||||||
|
style="width: 120px"
|
||||||
|
@press-enter="handleSearch"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="退款状态">
|
||||||
|
<a-select
|
||||||
|
v-model:value="store.filters.refund_status_id"
|
||||||
|
placeholder="全部状态"
|
||||||
|
allow-clear
|
||||||
|
style="width: 140px"
|
||||||
|
>
|
||||||
|
<a-select-option
|
||||||
|
v-for="(item, key) in refundStatusMap"
|
||||||
|
:key="key"
|
||||||
|
:value="Number(key)"
|
||||||
|
>
|
||||||
|
{{ item.text }}
|
||||||
|
</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="退款类型">
|
||||||
|
<a-select
|
||||||
|
v-model:value="store.filters.refund_type_id"
|
||||||
|
placeholder="全部类型"
|
||||||
|
allow-clear
|
||||||
|
style="width: 150px"
|
||||||
|
>
|
||||||
|
<a-select-option
|
||||||
|
v-for="(item, key) in refundTypeMap"
|
||||||
|
:key="key"
|
||||||
|
:value="Number(key)"
|
||||||
|
>
|
||||||
|
{{ item.text }}
|
||||||
|
</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="平台退款ID">
|
||||||
|
<a-input
|
||||||
|
v-model:value="store.filters.platform_refund_id"
|
||||||
|
placeholder="精确搜索"
|
||||||
|
allow-clear
|
||||||
|
@press-enter="handleSearch"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="平台订单ID">
|
||||||
|
<a-input
|
||||||
|
v-model:value="store.filters.platform_order_id"
|
||||||
|
placeholder="精确搜索"
|
||||||
|
allow-clear
|
||||||
|
@press-enter="handleSearch"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="平台商品ID">
|
||||||
|
<a-input
|
||||||
|
v-model:value="store.filters.platform_product_id"
|
||||||
|
placeholder="精确搜索"
|
||||||
|
allow-clear
|
||||||
|
@press-enter="handleSearch"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="创建时间">
|
||||||
|
<a-range-picker
|
||||||
|
v-model:value="createdDateRange"
|
||||||
|
format="YYYY-MM-DD"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item>
|
||||||
|
<a-space>
|
||||||
|
<a-button type="primary" html-type="submit">
|
||||||
|
<template #icon><SearchOutlined /></template>
|
||||||
|
搜索
|
||||||
|
</a-button>
|
||||||
|
<a-button @click="handleReset">
|
||||||
|
<template #icon><ReloadOutlined /></template>
|
||||||
|
重置
|
||||||
|
</a-button>
|
||||||
|
</a-space>
|
||||||
|
</a-form-item>
|
||||||
|
</a-form>
|
||||||
|
</a-card>
|
||||||
|
|
||||||
|
<!-- Table -->
|
||||||
|
<a-card>
|
||||||
|
<a-table
|
||||||
|
:columns="columns"
|
||||||
|
:data-source="store.refundItems"
|
||||||
|
:loading="store.loading"
|
||||||
|
:pagination="false"
|
||||||
|
:scroll="{ x: 2000 }"
|
||||||
|
row-key="id"
|
||||||
|
>
|
||||||
|
<template #bodyCell="{ column, record }">
|
||||||
|
<template v-if="column.key === 'company'">
|
||||||
|
{{ store.companyMap.get(record.company_id) || record.company_id }}
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.key === 'platform'">
|
||||||
|
{{ store.platformMap.get(record.platform_id) || record.platform_id }}
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.key === 'store'">
|
||||||
|
{{ store.storeMap.get(record.store_id) || record.store_id }}
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.key === 'refund_id'">
|
||||||
|
<a
|
||||||
|
class="text-blue-500 cursor-pointer"
|
||||||
|
@click="handleGoToRefund(record.refund_id)"
|
||||||
|
>
|
||||||
|
{{ record.refund_id }}
|
||||||
|
</a>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.key === 'platform_refund_id'">
|
||||||
|
{{ record.platform_refund_id }}
|
||||||
|
<CopyOutlined
|
||||||
|
class="ml-1 cursor-pointer text-gray-400 hover:text-blue-500"
|
||||||
|
@click.stop="handleCopyId(record.platform_refund_id, '平台退款ID')"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.key === 'platform_order_id'">
|
||||||
|
<a
|
||||||
|
class="text-blue-500 cursor-pointer"
|
||||||
|
@click="handleGoToOrder(record.platform_order_id)"
|
||||||
|
>
|
||||||
|
{{ record.platform_order_id }}
|
||||||
|
</a>
|
||||||
|
<CopyOutlined
|
||||||
|
class="ml-1 cursor-pointer text-gray-400 hover:text-blue-500"
|
||||||
|
@click.stop="handleCopyId(record.platform_order_id, '平台订单ID')"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.key === 'refund_status'">
|
||||||
|
<a-tag
|
||||||
|
:color="refundStatusMap[record.refund_status_id]?.color || 'default'"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
refundStatusMap[record.refund_status_id]?.text
|
||||||
|
|| `状态 ${record.refund_status_id}`
|
||||||
|
}}
|
||||||
|
</a-tag>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.key === 'refund_type'">
|
||||||
|
<a-tag
|
||||||
|
:color="refundTypeMap[record.refund_type_id]?.color || 'default'"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
refundTypeMap[record.refund_type_id]?.text
|
||||||
|
|| `类型 ${record.refund_type_id}`
|
||||||
|
}}
|
||||||
|
</a-tag>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.key === 'refund_amount'">
|
||||||
|
{{ formatAmount(record.refund_amount) }}
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.key === 'created_date'">
|
||||||
|
{{ formatTime(record.created_date) }}
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.key === 'action'">
|
||||||
|
<a-button
|
||||||
|
type="link"
|
||||||
|
size="small"
|
||||||
|
@click="handleViewDetail(record as RefundItemRecord)"
|
||||||
|
>
|
||||||
|
<template #icon><EyeOutlined /></template>
|
||||||
|
查看
|
||||||
|
</a-button>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</a-table>
|
||||||
|
|
||||||
|
<div class="mt-4 flex justify-end">
|
||||||
|
<a-pagination
|
||||||
|
:current="store.pagination.page"
|
||||||
|
:page-size="store.pagination.per_page"
|
||||||
|
:total="store.pagination.total"
|
||||||
|
show-size-changer
|
||||||
|
show-quick-jumper
|
||||||
|
:show-total="(total: number) => `共 ${total} 条`"
|
||||||
|
@change="handlePageChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</a-card>
|
||||||
|
|
||||||
|
<!-- Detail drawer -->
|
||||||
|
<a-drawer
|
||||||
|
title="退款项详情"
|
||||||
|
:open="drawerVisible"
|
||||||
|
:width="720"
|
||||||
|
@close="drawerVisible = false"
|
||||||
|
>
|
||||||
|
<a-spin :spinning="drawerLoading">
|
||||||
|
<template v-if="refundItemDetail">
|
||||||
|
<!-- 基本信息 -->
|
||||||
|
<a-descriptions title="基本信息" :column="2" bordered class="mb-4">
|
||||||
|
<a-descriptions-item label="ID">{{ refundItemDetail.id }}</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="退款单ID">
|
||||||
|
<a
|
||||||
|
class="text-blue-500 cursor-pointer"
|
||||||
|
@click="handleGoToRefund(refundItemDetail.refund_id)"
|
||||||
|
>
|
||||||
|
{{ refundItemDetail.refund_id }}
|
||||||
|
</a>
|
||||||
|
</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="平台退款ID">
|
||||||
|
{{ refundItemDetail.platform_refund_id }}
|
||||||
|
<CopyOutlined
|
||||||
|
class="ml-2 cursor-pointer text-blue-500"
|
||||||
|
@click="handleCopyId(refundItemDetail.platform_refund_id, '平台退款ID')"
|
||||||
|
/>
|
||||||
|
</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="父退款ID">
|
||||||
|
{{ refundItemDetail.platform_parent_refund_id || '-' }}
|
||||||
|
</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="关联订单ID">
|
||||||
|
<a
|
||||||
|
class="text-blue-500 cursor-pointer"
|
||||||
|
@click="handleGoToOrder(refundItemDetail.platform_order_id)"
|
||||||
|
>
|
||||||
|
{{ refundItemDetail.platform_order_id }}
|
||||||
|
</a>
|
||||||
|
<CopyOutlined
|
||||||
|
class="ml-2 cursor-pointer text-blue-500"
|
||||||
|
@click="handleCopyId(refundItemDetail.platform_order_id, '平台订单ID')"
|
||||||
|
/>
|
||||||
|
</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="子订单ID">
|
||||||
|
{{ refundItemDetail.platform_sub_order_id || '-' }}
|
||||||
|
</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="平台商品ID">
|
||||||
|
{{ refundItemDetail.platform_product_id }}
|
||||||
|
</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="买家ID">
|
||||||
|
{{ refundItemDetail.buyer_user_id || '-' }}
|
||||||
|
</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="退款状态">
|
||||||
|
<a-tag
|
||||||
|
:color="refundStatusMap[refundItemDetail.refund_status_id]?.color || 'default'"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
refundStatusMap[refundItemDetail.refund_status_id]?.text
|
||||||
|
|| `状态 ${refundItemDetail.refund_status_id}`
|
||||||
|
}}
|
||||||
|
</a-tag>
|
||||||
|
</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="退款类型">
|
||||||
|
<a-tag
|
||||||
|
:color="refundTypeMap[refundItemDetail.refund_type_id]?.color || 'default'"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
refundTypeMap[refundItemDetail.refund_type_id]?.text
|
||||||
|
|| `类型 ${refundItemDetail.refund_type_id}`
|
||||||
|
}}
|
||||||
|
</a-tag>
|
||||||
|
</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="退款原因" :span="2">
|
||||||
|
{{ refundItemDetail.reason || '-' }}
|
||||||
|
</a-descriptions-item>
|
||||||
|
</a-descriptions>
|
||||||
|
|
||||||
|
<!-- 金额信息 -->
|
||||||
|
<a-descriptions title="金额信息" :column="3" bordered class="mb-4">
|
||||||
|
<a-descriptions-item label="数量">
|
||||||
|
{{ refundItemDetail.quantity }}
|
||||||
|
</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="退款金额">
|
||||||
|
{{ formatAmount(refundItemDetail.refund_amount) }}
|
||||||
|
</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="币种">
|
||||||
|
{{ refundItemDetail.currency }}
|
||||||
|
</a-descriptions-item>
|
||||||
|
</a-descriptions>
|
||||||
|
|
||||||
|
<!-- 时间信息 -->
|
||||||
|
<a-descriptions title="时间信息" :column="2" bordered class="mb-4">
|
||||||
|
<a-descriptions-item label="退款创建时间">
|
||||||
|
{{ formatTime(refundItemDetail.created_date) }}
|
||||||
|
</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="退款完成时间">
|
||||||
|
{{ formatTime(refundItemDetail.completed_date) }}
|
||||||
|
</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="订单创建时间">
|
||||||
|
{{ formatTime(refundItemDetail.order_created_date) }}
|
||||||
|
</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="订单付款时间">
|
||||||
|
{{ formatTime(refundItemDetail.order_paid_date) }}
|
||||||
|
</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="最后更新时间">
|
||||||
|
{{ formatTime(refundItemDetail.updated_date) }}
|
||||||
|
</a-descriptions-item>
|
||||||
|
</a-descriptions>
|
||||||
|
|
||||||
|
<!-- 扩展数据 -->
|
||||||
|
<a-descriptions title="扩展数据 (ext)" :column="1" bordered>
|
||||||
|
<a-descriptions-item>
|
||||||
|
<pre v-if="refundItemDetail.ext" class="m-0 text-xs max-h-80 overflow-auto">{{
|
||||||
|
JSON.stringify(refundItemDetail.ext, null, 2)
|
||||||
|
}}</pre>
|
||||||
|
<span v-else class="text-gray-400">暂无数据</span>
|
||||||
|
</a-descriptions-item>
|
||||||
|
</a-descriptions>
|
||||||
|
</template>
|
||||||
|
</a-spin>
|
||||||
|
</a-drawer>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
Reference in New Issue
Block a user