Files
datahub/frontend/src/pages/refund-items/index.vue
T

488 lines
17 KiB
Vue
Raw Normal View History

2026-03-20 09:52:55 +08:00
<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 },
2026-04-03 10:18:05 +08:00
{ title: '子订单ID', key: 'platform_sub_order_id', width: 140, ellipsis: true },
{ title: '平台商品ID', key: 'platform_product_id', width: 140, ellipsis: true },
2026-03-20 09:52:55 +08:00
{ 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)
}
2026-04-03 10:18:05 +08:00
async function handleCopy(text: string, label: string) {
2026-03-20 09:52:55 +08:00
try {
await navigator.clipboard.writeText(text)
message.success(`已复制${label}`)
} catch {
message.error('复制失败')
}
}
function handleGoToOrder(platformOrderId: string) {
2026-04-03 10:18:05 +08:00
router.push({
path: '/orders',
query: { platform_order_id: platformOrderId, auto_submit: '1' },
})
2026-03-20 09:52:55 +08:00
}
2026-04-03 10:18:05 +08:00
function handleGoToRefund(platformRefundId: string) {
router.push({
path: '/refunds',
query: { platform_refund_id: platformRefundId, auto_submit: '1' },
})
2026-03-20 09:52:55 +08:00
}
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">
2026-04-03 10:18:05 +08:00
<a-form layout="inline" class="filter-form" @submit.prevent="handleSearch">
2026-03-20 09:52:55 +08:00
<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"
2026-04-03 10:18:05 +08:00
@click="handleGoToRefund(record.platform_refund_id)"
2026-03-20 09:52:55 +08:00
>
{{ record.refund_id }}
</a>
</template>
<template v-else-if="column.key === 'platform_refund_id'">
2026-04-03 10:18:05 +08:00
<span class="inline-flex items-center gap-1">
<CopyOutlined
class="flex-shrink-0 cursor-pointer text-gray-400 hover:text-blue-500"
@click.stop="handleCopy(record.platform_refund_id, '平台退款ID')"
/>
<span class="truncate">{{ record.platform_refund_id }}</span>
</span>
2026-03-20 09:52:55 +08:00
</template>
<template v-else-if="column.key === 'platform_order_id'">
2026-04-03 10:18:05 +08:00
<span class="inline-flex items-center gap-1">
<CopyOutlined
class="flex-shrink-0 cursor-pointer text-gray-400 hover:text-blue-500"
@click.stop="handleCopy(record.platform_order_id, '平台订单ID')"
/>
<a class="truncate text-blue-500 cursor-pointer"
@click="handleGoToOrder(record.platform_order_id)">
{{ record.platform_order_id }}
</a>
</span>
</template>
<template v-else-if="column.key === 'platform_sub_order_id'">
<span v-if="record.platform_sub_order_id" class="inline-flex items-center gap-1">
<CopyOutlined
class="flex-shrink-0 cursor-pointer text-gray-400 hover:text-blue-500"
@click.stop="handleCopy(record.platform_sub_order_id, '子订单ID')"
/>
<span class="truncate">{{ record.platform_sub_order_id }}</span>
</span>
<span v-else>-</span>
</template>
<template v-else-if="column.key === 'platform_product_id'">
<span class="inline-flex items-center gap-1">
<CopyOutlined
class="flex-shrink-0 cursor-pointer text-gray-400 hover:text-blue-500"
@click.stop="handleCopy(record.platform_product_id, '平台商品ID')"
/>
<span class="truncate">{{ record.platform_product_id }}</span>
</span>
2026-03-20 09:52:55 +08:00
</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"
2026-04-03 10:18:05 +08:00
@click="handleGoToRefund(refundItemDetail.platform_refund_id)"
2026-03-20 09:52:55 +08:00
>
{{ 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"
2026-04-03 10:18:05 +08:00
@click="handleCopy(refundItemDetail.platform_refund_id, '平台退款ID')"
2026-03-20 09:52:55 +08:00
/>
</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"
2026-04-03 10:18:05 +08:00
@click="handleCopy(refundItemDetail.platform_order_id, '平台订单ID')"
2026-03-20 09:52:55 +08:00
/>
</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>