add order list and detail page
Implement P6.1: order management UI with list table, multi-dimension filters (cascade + status + date ranges), detail drawer with order items nested table, and 20 test cases. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,519 @@
|
||||
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 { useOrderStore, type OrderRecord } from '@/stores/order'
|
||||
|
||||
// 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 mockOrders: OrderRecord[] = [
|
||||
{
|
||||
id: 1,
|
||||
company_id: 1,
|
||||
platform_id: 1,
|
||||
store_id: 1,
|
||||
order_status_id: 3,
|
||||
platform_order_id: 'ORD-20260101-001',
|
||||
total_amount: '199.99',
|
||||
total_paid: '189.99',
|
||||
total_discount: '10.00',
|
||||
created_date: '2026-01-01T10:00:00Z',
|
||||
paid_date: '2026-01-01T10:05:00Z',
|
||||
shipping_date: '2026-01-02T08:00:00Z',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
company_id: 2,
|
||||
platform_id: 1,
|
||||
store_id: 2,
|
||||
order_status_id: 1,
|
||||
platform_order_id: 'ORD-20260201-002',
|
||||
total_amount: '59.00',
|
||||
total_paid: '0',
|
||||
total_discount: '0',
|
||||
created_date: '2026-02-01T14:00:00Z',
|
||||
paid_date: null,
|
||||
shipping_date: null,
|
||||
},
|
||||
]
|
||||
|
||||
const mockPaginatedResponse = {
|
||||
items: mockOrders,
|
||||
total: 2,
|
||||
page: 1,
|
||||
per_page: 15,
|
||||
}
|
||||
|
||||
const mockOrderDetail = {
|
||||
...mockOrders[0],
|
||||
buyer_user_id: 'buyer_123',
|
||||
payment_method_id: 1,
|
||||
presale: false,
|
||||
total_received: '189.99',
|
||||
freight_fee: '0.00',
|
||||
tax_fee: '0.00',
|
||||
discount_fee: '10.00',
|
||||
commission_fee: '5.00',
|
||||
coupon_amount: '0.00',
|
||||
voucher_amount: '0.00',
|
||||
order_type_id: 1,
|
||||
updated_date: '2026-01-02T08:00:00Z',
|
||||
zipcode: '310000',
|
||||
city: '杭州',
|
||||
province: '浙江',
|
||||
country: '中国',
|
||||
ext: { remark: '加急发货', tags: ['vip'] },
|
||||
created_at: '2026-01-01T10:00:00Z',
|
||||
updated_at: '2026-01-02T08:00:00Z',
|
||||
order_items: [
|
||||
{
|
||||
id: 101,
|
||||
company_id: 1,
|
||||
platform_id: 1,
|
||||
store_id: 1,
|
||||
order_id: 1,
|
||||
platform_order_id: 'ORD-20260101-001',
|
||||
sub_order_id: 'SUB-001',
|
||||
sub_order_type_id: 1,
|
||||
product_id: 500,
|
||||
platform_product_id: 'PROD-001',
|
||||
product_sku: 'SKU-ABC-001',
|
||||
product_barcode: '6901234567890',
|
||||
unit_price: '99.99',
|
||||
quantity: 2,
|
||||
discount: '10.00',
|
||||
total: '189.98',
|
||||
created_date: '2026-01-01T10:00:00Z',
|
||||
ext: null,
|
||||
created_at: '2026-01-01T10:00:00Z',
|
||||
updated_at: '2026-01-01T10:00:00Z',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const mockRawDetail = {
|
||||
id: 1,
|
||||
platform_order_id: 'ORD-20260101-001',
|
||||
store_id: 1,
|
||||
company_id: 1,
|
||||
platform_id: 1,
|
||||
order_status_id: 3,
|
||||
raw: { tid: 'tmall_001', status: 'TRADE_FINISHED', orders: [{ oid: 'sub_1' }] },
|
||||
hash: 'abc123',
|
||||
ext: null,
|
||||
created_date: '2026-01-01T10: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('useOrderStore', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
describe('initial state', () => {
|
||||
it('starts with empty orders and default pagination', () => {
|
||||
const store = useOrderStore()
|
||||
expect(store.orders).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.order_status_id).toBeUndefined()
|
||||
expect(store.filters.platform_order_id).toBe('')
|
||||
expect(store.filters.created_date_range).toBeNull()
|
||||
expect(store.filters.paid_date_range).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('fetchOrders', () => {
|
||||
it('loads data and updates state', async () => {
|
||||
const { api } = await import('@/utils/request')
|
||||
vi.mocked(api.get).mockResolvedValueOnce(mockPaginatedResponse)
|
||||
|
||||
const store = useOrderStore()
|
||||
await store.fetchOrders()
|
||||
|
||||
expect(api.get).toHaveBeenCalledWith(
|
||||
'/api/v1/orders',
|
||||
expect.objectContaining({
|
||||
page: 1,
|
||||
per_page: 15,
|
||||
order_status_id: undefined,
|
||||
platform_order_id: undefined,
|
||||
created_date_from: undefined,
|
||||
created_date_to: undefined,
|
||||
paid_date_from: undefined,
|
||||
paid_date_to: undefined,
|
||||
}),
|
||||
)
|
||||
expect(store.orders).toEqual(mockOrders)
|
||||
expect(store.pagination.total).toBe(2)
|
||||
})
|
||||
|
||||
it('passes date range params correctly', async () => {
|
||||
const { api } = await import('@/utils/request')
|
||||
vi.mocked(api.get).mockResolvedValueOnce(mockPaginatedResponse)
|
||||
|
||||
const store = useOrderStore()
|
||||
store.filters.order_status_id = 3
|
||||
store.filters.platform_order_id = 'ORD-001'
|
||||
store.filters.created_date_range = ['2026-01-01', '2026-01-31']
|
||||
store.filters.paid_date_range = ['2026-02-01', '2026-02-28']
|
||||
store.cascadeValue.company_id = 1
|
||||
store.cascadeValue.platform_id = 2
|
||||
store.cascadeValue.store_id = 3
|
||||
|
||||
await store.fetchOrders()
|
||||
|
||||
expect(api.get).toHaveBeenCalledWith(
|
||||
'/api/v1/orders',
|
||||
expect.objectContaining({
|
||||
order_status_id: 3,
|
||||
platform_order_id: 'ORD-001',
|
||||
created_date_from: '2026-01-01',
|
||||
created_date_to: '2026-01-31',
|
||||
paid_date_from: '2026-02-01',
|
||||
paid_date_to: '2026-02-28',
|
||||
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 = useOrderStore()
|
||||
await store.fetchOrders()
|
||||
|
||||
expect(errorSpy).toHaveBeenCalledWith('Network error')
|
||||
expect(store.orders).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('resetFilters', () => {
|
||||
it('clears all filters including date ranges and resets page to 1', () => {
|
||||
const store = useOrderStore()
|
||||
store.filters.order_status_id = 3
|
||||
store.filters.platform_order_id = 'ORD-001'
|
||||
store.filters.created_date_range = ['2026-01-01', '2026-01-31']
|
||||
store.filters.paid_date_range = ['2026-02-01', '2026-02-28']
|
||||
store.cascadeValue.company_id = 1
|
||||
store.cascadeValue.platform_id = 2
|
||||
store.cascadeValue.store_id = 3
|
||||
store.pagination.page = 5
|
||||
|
||||
store.resetFilters()
|
||||
|
||||
expect(store.filters.order_status_id).toBeUndefined()
|
||||
expect(store.filters.platform_order_id).toBe('')
|
||||
expect(store.filters.created_date_range).toBeNull()
|
||||
expect(store.filters.paid_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 = useOrderStore()
|
||||
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 = useOrderStore()
|
||||
await store.loadLookups()
|
||||
|
||||
expect(warnSpy).toHaveBeenCalledWith('加载查找表数据失败', expect.any(Error))
|
||||
expect(store.companyMap.size).toBe(0)
|
||||
warnSpy.mockRestore()
|
||||
})
|
||||
})
|
||||
|
||||
describe('fetchOrders error handling', () => {
|
||||
it('clears orders on fetch failure', async () => {
|
||||
const { api } = await import('@/utils/request')
|
||||
const { message } = await import('ant-design-vue')
|
||||
vi.spyOn(message, 'error')
|
||||
|
||||
const store = useOrderStore()
|
||||
vi.mocked(api.get).mockResolvedValueOnce(mockPaginatedResponse)
|
||||
await store.fetchOrders()
|
||||
expect(store.orders.length).toBe(2)
|
||||
|
||||
vi.mocked(api.get).mockRejectedValueOnce(new Error('Server error'))
|
||||
await store.fetchOrders()
|
||||
|
||||
expect(store.orders).toEqual([])
|
||||
expect(store.pagination.total).toBe(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// ─── Page Component Tests ──────────────────────────────
|
||||
|
||||
describe('OrdersPage', () => {
|
||||
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/orders') 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: OrdersPage } = await import('../index.vue')
|
||||
|
||||
wrapper = mount(OrdersPage, {
|
||||
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 fetchOrders on mount', async () => {
|
||||
const { api } = await mountPage()
|
||||
|
||||
expect(api.get).toHaveBeenCalledWith(
|
||||
'/api/v1/orders',
|
||||
expect.objectContaining({
|
||||
page: 1,
|
||||
per_page: 15,
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('renders table with order data', async () => {
|
||||
await mountPage()
|
||||
|
||||
const html = wrapper.html()
|
||||
expect(html).toContain('ORD-20260101-001')
|
||||
expect(html).toContain('199.99')
|
||||
expect(html).toContain('189.99')
|
||||
})
|
||||
|
||||
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 status tags with correct text', async () => {
|
||||
await mountPage()
|
||||
|
||||
const tags = wrapper.findAll('.ant-tag')
|
||||
const tagTexts = tags.map((t) => t.text())
|
||||
expect(tagTexts).toContain('已支付')
|
||||
expect(tagTexts).toContain('待付款')
|
||||
})
|
||||
|
||||
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 = useOrderStore()
|
||||
store.pagination.page = 3
|
||||
store.filters.order_status_id = 3
|
||||
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/orders',
|
||||
expect.objectContaining({
|
||||
page: 1,
|
||||
order_status_id: 3,
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('resets filters when clicking reset button', async () => {
|
||||
await mountPage()
|
||||
|
||||
const store = useOrderStore()
|
||||
store.filters.order_status_id = 3
|
||||
store.filters.platform_order_id = 'ORD-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.order_status_id).toBeUndefined()
|
||||
expect(store.filters.platform_order_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(mockOrderDetail)
|
||||
.mockResolvedValueOnce(mockRawDetail)
|
||||
|
||||
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 ext JSON in drawer when available', async () => {
|
||||
const { api } = await mountPage()
|
||||
vi.mocked(api.get)
|
||||
.mockResolvedValueOnce(mockOrderDetail)
|
||||
.mockResolvedValueOnce(mockRawDetail)
|
||||
|
||||
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('remark')
|
||||
expect(drawerHtml).toContain('加急发货')
|
||||
})
|
||||
|
||||
it('displays raw JSON in drawer when available', async () => {
|
||||
const { api } = await mountPage()
|
||||
vi.mocked(api.get)
|
||||
.mockResolvedValueOnce(mockOrderDetail)
|
||||
.mockResolvedValueOnce(mockRawDetail)
|
||||
|
||||
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('tmall_001')
|
||||
expect(drawerHtml).toContain('TRADE_FINISHED')
|
||||
})
|
||||
|
||||
it('shows placeholder when ext/raw is null', async () => {
|
||||
const { api } = await mountPage()
|
||||
vi.mocked(api.get)
|
||||
.mockResolvedValueOnce({ ...mockOrderDetail, ext: null, order_items: [] })
|
||||
.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()
|
||||
|
||||
const drawerHtml = document.body.querySelector('.ant-drawer')?.innerHTML || ''
|
||||
expect(drawerHtml).toContain('暂无数据')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,460 @@
|
||||
<script setup lang="ts">
|
||||
import dayjs from 'dayjs'
|
||||
import { api } from '@/utils/request'
|
||||
import {
|
||||
useOrderStore,
|
||||
type OrderRecord,
|
||||
type OrderDetail,
|
||||
type RawOrderDetail,
|
||||
} from '@/stores/order'
|
||||
import CascadeFilter from '@/components/CascadeFilter.vue'
|
||||
import {
|
||||
SearchOutlined,
|
||||
ReloadOutlined,
|
||||
EyeOutlined,
|
||||
CopyOutlined,
|
||||
} from '@ant-design/icons-vue'
|
||||
|
||||
const store = useOrderStore()
|
||||
|
||||
// Detail drawer
|
||||
const drawerVisible = ref(false)
|
||||
const drawerLoading = ref(false)
|
||||
const orderDetail = ref<OrderDetail | null>(null)
|
||||
const rawDetail = ref<RawOrderDetail | null>(null)
|
||||
let detailRequestId = 0
|
||||
|
||||
const orderStatusMap: Record<number, { text: string; color: string }> = {
|
||||
1: { text: '待付款', color: 'orange' },
|
||||
2: { text: '支付失败', color: 'red' },
|
||||
3: { text: '已支付', color: 'blue' },
|
||||
4: { text: '待发货', color: 'cyan' },
|
||||
5: { text: '已发货', color: 'geekblue' },
|
||||
6: { text: '取消申请中', color: 'volcano' },
|
||||
7: { text: '已取消', color: 'default' },
|
||||
8: { text: '已完成', color: 'green' },
|
||||
9: { text: '发货前取消', color: 'default' },
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{ title: 'ID', dataIndex: 'id', width: 80, fixed: 'left' as const },
|
||||
{ title: '公司', key: 'company', width: 120 },
|
||||
{ title: '平台', key: 'platform', width: 100 },
|
||||
{ title: '店铺', key: 'store', width: 140 },
|
||||
{ title: '平台订单ID', key: 'platform_order_id', width: 180, ellipsis: true },
|
||||
{ title: '订单金额', key: 'total_amount', width: 120 },
|
||||
{ title: '已支付', key: 'total_paid', width: 120 },
|
||||
{ title: '总折扣', key: 'total_discount', width: 120 },
|
||||
{ title: '状态', key: 'status', width: 110 },
|
||||
{ title: '创建时间', key: 'created_date', width: 160 },
|
||||
{ title: '付款时间', key: 'paid_date', width: 160 },
|
||||
{ title: '操作', key: 'action', width: 100, fixed: 'right' as const },
|
||||
]
|
||||
|
||||
const orderItemColumns = [
|
||||
{ title: 'SKU', dataIndex: 'product_sku', width: 140, ellipsis: true },
|
||||
{ title: '平台商品ID', dataIndex: 'platform_product_id', width: 160, ellipsis: true },
|
||||
{ title: '数量', dataIndex: 'quantity', width: 70 },
|
||||
{ title: '单价', key: 'unit_price', width: 100 },
|
||||
{ title: '优惠', key: 'item_discount', width: 100 },
|
||||
{ title: '小计', key: 'item_total', width: 100 },
|
||||
]
|
||||
|
||||
// RangePicker dayjs 桥接(undefined 替代 null,匹配 antdv 类型要求)
|
||||
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
|
||||
},
|
||||
})
|
||||
|
||||
const paidDateRange = computed({
|
||||
get: () => {
|
||||
if (!store.filters.paid_date_range) return undefined
|
||||
return store.filters.paid_date_range.map((d) => dayjs(d)) as [
|
||||
dayjs.Dayjs,
|
||||
dayjs.Dayjs,
|
||||
]
|
||||
},
|
||||
set: (val) => {
|
||||
store.filters.paid_date_range = val
|
||||
? [val[0].format('YYYY-MM-DD'), val[1].format('YYYY-MM-DD')]
|
||||
: null
|
||||
},
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
store.loadLookups()
|
||||
store.fetchOrders()
|
||||
})
|
||||
|
||||
function handleSearch() {
|
||||
store.pagination.page = 1
|
||||
store.fetchOrders()
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
store.resetFilters()
|
||||
store.fetchOrders()
|
||||
}
|
||||
|
||||
function handlePageChange(page: number, pageSize: number) {
|
||||
store.pagination.page = page
|
||||
store.pagination.per_page = pageSize
|
||||
store.fetchOrders()
|
||||
}
|
||||
|
||||
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 handleCopyOrderId(platformOrderId: string) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(platformOrderId)
|
||||
message.success('已复制平台订单ID')
|
||||
} catch {
|
||||
message.error('复制失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function handleViewDetail(record: { id: number }) {
|
||||
const currentRequestId = ++detailRequestId
|
||||
drawerVisible.value = true
|
||||
drawerLoading.value = true
|
||||
orderDetail.value = null
|
||||
rawDetail.value = null
|
||||
|
||||
const [normalResult, rawResult] = await Promise.allSettled([
|
||||
api.get<OrderDetail>(`/api/v1/orders/${record.id}`),
|
||||
api.get<RawOrderDetail>(`/api/v1/raw/orders/${record.id}`),
|
||||
])
|
||||
|
||||
// 竞态保护:若用户已切换到另一订单详情,丢弃过期响应
|
||||
if (currentRequestId !== detailRequestId) return
|
||||
|
||||
if (normalResult.status === 'fulfilled') {
|
||||
orderDetail.value = normalResult.value
|
||||
} else {
|
||||
message.error('获取订单详情失败')
|
||||
}
|
||||
|
||||
if (rawResult.status === 'fulfilled') {
|
||||
rawDetail.value = rawResult.value
|
||||
} else {
|
||||
console.warn('加载原始数据(raw)失败', rawResult.reason)
|
||||
}
|
||||
|
||||
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="订单状态">
|
||||
<a-select
|
||||
v-model:value="store.filters.order_status_id"
|
||||
placeholder="全部"
|
||||
allow-clear
|
||||
style="width: 130px"
|
||||
>
|
||||
<a-select-option
|
||||
v-for="(item, key) in orderStatusMap"
|
||||
: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_order_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 label="付款时间">
|
||||
<a-range-picker
|
||||
v-model:value="paidDateRange"
|
||||
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.orders"
|
||||
:loading="store.loading"
|
||||
:pagination="false"
|
||||
:scroll="{ x: 1360 }"
|
||||
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 === 'platform_order_id'">
|
||||
{{ record.platform_order_id }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'total_amount'">
|
||||
{{ formatAmount(record.total_amount) }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'total_paid'">
|
||||
{{ formatAmount(record.total_paid) }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'total_discount'">
|
||||
{{ formatAmount(record.total_discount) }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'status'">
|
||||
<a-tag :color="orderStatusMap[record.order_status_id]?.color || 'default'">
|
||||
{{ orderStatusMap[record.order_status_id]?.text || `状态 ${record.order_status_id}` }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'created_date'">
|
||||
{{ formatTime(record.created_date) }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'paid_date'">
|
||||
{{ formatTime(record.paid_date) }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<a-button
|
||||
type="link"
|
||||
size="small"
|
||||
@click="handleViewDetail(record as OrderRecord)"
|
||||
>
|
||||
<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="orderDetail">
|
||||
<!-- 基本信息 -->
|
||||
<a-descriptions title="基本信息" :column="2" bordered class="mb-4">
|
||||
<a-descriptions-item label="ID">{{ orderDetail.id }}</a-descriptions-item>
|
||||
<a-descriptions-item label="状态">
|
||||
<a-tag
|
||||
:color="orderStatusMap[orderDetail.order_status_id]?.color || 'default'"
|
||||
>
|
||||
{{
|
||||
orderStatusMap[orderDetail.order_status_id]?.text
|
||||
|| `状态 ${orderDetail.order_status_id}`
|
||||
}}
|
||||
</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="平台订单ID" :span="2">
|
||||
{{ orderDetail.platform_order_id }}
|
||||
<CopyOutlined
|
||||
class="ml-2 cursor-pointer text-blue-500"
|
||||
@click="handleCopyOrderId(orderDetail.platform_order_id)"
|
||||
/>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="公司">
|
||||
{{
|
||||
store.companyMap.get(orderDetail.company_id) || orderDetail.company_id
|
||||
}}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="平台">
|
||||
{{
|
||||
store.platformMap.get(orderDetail.platform_id)
|
||||
|| orderDetail.platform_id
|
||||
}}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="店铺">
|
||||
{{
|
||||
store.storeMap.get(orderDetail.store_id) || orderDetail.store_id
|
||||
}}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="买家ID">
|
||||
{{ orderDetail.buyer_user_id || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="支付方式">
|
||||
{{ orderDetail.payment_method_id }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="预售">
|
||||
{{ orderDetail.presale ? '是' : '否' }}
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
|
||||
<!-- 金额信息 -->
|
||||
<a-descriptions title="金额信息" :column="2" bordered class="mb-4">
|
||||
<a-descriptions-item label="订单总金额">
|
||||
{{ formatAmount(orderDetail.total_amount) }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="已支付金额">
|
||||
{{ formatAmount(orderDetail.total_paid) }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="总折扣">
|
||||
{{ formatAmount(orderDetail.total_discount) }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="实收金额">
|
||||
{{ formatAmount(orderDetail.total_received) }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="运费">
|
||||
{{ formatAmount(orderDetail.freight_fee) }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="税费">
|
||||
{{ formatAmount(orderDetail.tax_fee) }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="优惠金额">
|
||||
{{ formatAmount(orderDetail.discount_fee) }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="佣金">
|
||||
{{ formatAmount(orderDetail.commission_fee) }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="优惠券金额">
|
||||
{{ formatAmount(orderDetail.coupon_amount) }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="代金券金额">
|
||||
{{ formatAmount(orderDetail.voucher_amount) }}
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
|
||||
<!-- 时间与地址 -->
|
||||
<a-descriptions title="时间与地址" :column="2" bordered class="mb-4">
|
||||
<a-descriptions-item label="创建时间">
|
||||
{{ formatTime(orderDetail.created_date) }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="更新时间">
|
||||
{{ formatTime(orderDetail.updated_date) }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="付款时间">
|
||||
{{ formatTime(orderDetail.paid_date) }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="发货时间">
|
||||
{{ formatTime(orderDetail.shipping_date) }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="国家">
|
||||
{{ orderDetail.country || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="省份">
|
||||
{{ orderDetail.province || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="城市">
|
||||
{{ orderDetail.city || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="邮编">
|
||||
{{ orderDetail.zipcode || '-' }}
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
|
||||
<!-- 订单子项 -->
|
||||
<h4 class="text-base font-medium mb-2">订单子项</h4>
|
||||
<a-table
|
||||
:columns="orderItemColumns"
|
||||
:data-source="orderDetail.order_items"
|
||||
:pagination="false"
|
||||
size="small"
|
||||
row-key="id"
|
||||
class="mb-4"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'unit_price'">
|
||||
{{ formatAmount(record.unit_price) }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'item_discount'">
|
||||
{{ formatAmount(record.discount) }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'item_total'">
|
||||
{{ formatAmount(record.total) }}
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
|
||||
<!-- ext JSON -->
|
||||
<a-descriptions title="扩展数据 (ext)" :column="1" bordered class="mb-4">
|
||||
<a-descriptions-item>
|
||||
<pre v-if="orderDetail.ext" class="m-0 text-xs max-h-80 overflow-auto">{{
|
||||
JSON.stringify(orderDetail.ext, null, 2)
|
||||
}}</pre>
|
||||
<span v-else class="text-gray-400">暂无数据</span>
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
|
||||
<!-- raw JSON -->
|
||||
<a-descriptions title="原始数据 (raw)" :column="1" bordered>
|
||||
<a-descriptions-item>
|
||||
<pre
|
||||
v-if="rawDetail?.raw"
|
||||
class="m-0 text-xs max-h-80 overflow-auto"
|
||||
>{{ JSON.stringify(rawDetail.raw, null, 2) }}</pre>
|
||||
<span v-else class="text-gray-400">暂无数据</span>
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</template>
|
||||
</a-spin>
|
||||
</a-drawer>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,216 @@
|
||||
import { api } from '@/utils/request'
|
||||
import type { PaginatedData } from '@/types/api'
|
||||
|
||||
/** 订单列表记录(12 字段,与 getListFields 对应) */
|
||||
export interface OrderRecord {
|
||||
id: number
|
||||
company_id: number
|
||||
platform_id: number
|
||||
store_id: number
|
||||
order_status_id: number
|
||||
platform_order_id: string
|
||||
total_amount: string
|
||||
total_paid: string
|
||||
total_discount: string
|
||||
created_date: string | null
|
||||
paid_date: string | null
|
||||
shipping_date: string | null
|
||||
}
|
||||
|
||||
/** 订单子项记录 */
|
||||
export interface OrderItemRecord {
|
||||
id: number
|
||||
company_id: number
|
||||
platform_id: number
|
||||
store_id: number
|
||||
order_id: number
|
||||
platform_order_id: string
|
||||
sub_order_id: string | null
|
||||
sub_order_type_id: number
|
||||
product_id: number
|
||||
platform_product_id: string
|
||||
product_sku: string | null
|
||||
product_barcode: string | null
|
||||
unit_price: string
|
||||
quantity: number
|
||||
discount: string
|
||||
total: string
|
||||
created_date: string | null
|
||||
ext: Record<string, unknown> | null
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
/** 订单详情(含 order_items) */
|
||||
export interface OrderDetail {
|
||||
id: number
|
||||
company_id: number
|
||||
platform_id: number
|
||||
store_id: number
|
||||
order_status_id: number
|
||||
platform_order_id: string
|
||||
buyer_user_id: string | null
|
||||
payment_method_id: number
|
||||
presale: boolean
|
||||
total_amount: string
|
||||
total_paid: string
|
||||
total_discount: string
|
||||
total_received: string
|
||||
freight_fee: string
|
||||
tax_fee: string
|
||||
discount_fee: string
|
||||
commission_fee: string
|
||||
coupon_amount: string
|
||||
voucher_amount: string
|
||||
order_type_id: number
|
||||
created_date: string | null
|
||||
updated_date: string | null
|
||||
paid_date: string | null
|
||||
shipping_date: string | null
|
||||
zipcode: string
|
||||
city: string
|
||||
province: string
|
||||
country: string
|
||||
ext: Record<string, unknown> | null
|
||||
created_at: string
|
||||
updated_at: string
|
||||
order_items: OrderItemRecord[]
|
||||
}
|
||||
|
||||
/** Raw 订单详情 */
|
||||
export interface RawOrderDetail {
|
||||
id: number
|
||||
platform_order_id: string
|
||||
store_id: number
|
||||
company_id: number
|
||||
platform_id: number
|
||||
order_status_id: number
|
||||
raw: Record<string, unknown> | null
|
||||
hash: string
|
||||
ext: Record<string, unknown> | null
|
||||
created_date: string | null
|
||||
}
|
||||
|
||||
export interface OrderFilters {
|
||||
order_status_id: number | undefined
|
||||
platform_order_id: string
|
||||
created_date_range: [string, string] | null
|
||||
paid_date_range: [string, string] | null
|
||||
}
|
||||
|
||||
/** 名称映射用的查找表 */
|
||||
interface LookupItem {
|
||||
id: number
|
||||
name: string
|
||||
label?: string
|
||||
}
|
||||
|
||||
export const useOrderStore = defineStore('order', () => {
|
||||
const orders = ref<OrderRecord[]>([])
|
||||
const loading = ref(false)
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
per_page: 15,
|
||||
total: 0,
|
||||
})
|
||||
const cascadeValue = reactive({
|
||||
company_id: undefined as number | undefined,
|
||||
platform_id: undefined as number | undefined,
|
||||
store_id: undefined as number | undefined,
|
||||
})
|
||||
const filters = reactive<OrderFilters>({
|
||||
order_status_id: undefined,
|
||||
platform_order_id: '',
|
||||
created_date_range: null,
|
||||
paid_date_range: null,
|
||||
})
|
||||
|
||||
// 名称映射数据
|
||||
const companies = ref<LookupItem[]>([])
|
||||
const platforms = ref<{ id: number; developer_id: number }[]>([])
|
||||
const stores = ref<(LookupItem & { company_id: number; platform_id: number })[]>([])
|
||||
|
||||
const companyMap = computed(
|
||||
() => new Map(companies.value.map((c) => [c.id, c.label || c.name])),
|
||||
)
|
||||
const platformMap = computed(
|
||||
() => new Map(platforms.value.map((p) => [p.id, `平台 #${p.id}`])),
|
||||
)
|
||||
const storeMap = computed(
|
||||
() => new Map(stores.value.map((s) => [s.id, s.label || s.name])),
|
||||
)
|
||||
|
||||
async function loadLookups() {
|
||||
try {
|
||||
const [c, p, s] = await Promise.all([
|
||||
api.get<LookupItem[]>('/api/v1/companies'),
|
||||
api.get<{ id: number; developer_id: number }[]>('/api/v1/platforms'),
|
||||
api.get<(LookupItem & { company_id: number; platform_id: number })[]>(
|
||||
'/api/v1/stores',
|
||||
),
|
||||
])
|
||||
companies.value = c
|
||||
platforms.value = p
|
||||
stores.value = s
|
||||
} catch (err: unknown) {
|
||||
console.warn('加载查找表数据失败', err)
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchOrders() {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await api.get<PaginatedData<OrderRecord>>('/api/v1/orders', {
|
||||
page: pagination.page,
|
||||
per_page: pagination.per_page,
|
||||
company_id: cascadeValue.company_id,
|
||||
platform_id: cascadeValue.platform_id,
|
||||
store_id: cascadeValue.store_id,
|
||||
order_status_id: filters.order_status_id,
|
||||
platform_order_id: filters.platform_order_id || undefined,
|
||||
created_date_from: filters.created_date_range?.[0] || undefined,
|
||||
created_date_to: filters.created_date_range?.[1] || undefined,
|
||||
paid_date_from: filters.paid_date_range?.[0] || undefined,
|
||||
paid_date_to: filters.paid_date_range?.[1] || undefined,
|
||||
})
|
||||
orders.value = data.items
|
||||
pagination.total = data.total
|
||||
pagination.page = data.page
|
||||
} catch (err: unknown) {
|
||||
orders.value = []
|
||||
pagination.total = 0
|
||||
const msg = err instanceof Error ? err.message : '获取订单列表失败'
|
||||
message.error(msg)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function resetFilters() {
|
||||
filters.order_status_id = undefined
|
||||
filters.platform_order_id = ''
|
||||
filters.created_date_range = null
|
||||
filters.paid_date_range = null
|
||||
cascadeValue.company_id = undefined
|
||||
cascadeValue.platform_id = undefined
|
||||
cascadeValue.store_id = undefined
|
||||
pagination.page = 1
|
||||
}
|
||||
|
||||
return {
|
||||
orders,
|
||||
loading,
|
||||
pagination,
|
||||
cascadeValue,
|
||||
filters,
|
||||
companies,
|
||||
platforms,
|
||||
stores,
|
||||
companyMap,
|
||||
platformMap,
|
||||
storeMap,
|
||||
loadLookups,
|
||||
fetchOrders,
|
||||
resetFilters,
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user