317 lines
8.3 KiB
Vue
317 lines
8.3 KiB
Vue
<template>
|
|
<div class="aksrt-page-container">
|
|
<PageHeader title="返佣管理" desc="按状态、类型和时间筛选返佣记录,自定义列展示并在详情中查看处理历史。" />
|
|
|
|
<div class="aksrt-rebate-page__stats">
|
|
<n-card v-for="card in statCards" :key="card.key" size="small">
|
|
<n-statistic :label="card.label" :value="stats[card.key] || 0" />
|
|
</n-card>
|
|
</div>
|
|
|
|
<n-card size="small" title="筛选与列控制">
|
|
<n-space vertical :size="12">
|
|
<n-grid cols="1 s:2 m:5" :x-gap="12" :y-gap="12" responsive="screen">
|
|
<n-grid-item>
|
|
<n-input v-model:value="filters.keyword" clearable placeholder="搜索订单号、用户、备注" />
|
|
</n-grid-item>
|
|
<n-grid-item>
|
|
<n-select v-model:value="filters.status" :options="statusOptions" placeholder="状态" />
|
|
</n-grid-item>
|
|
<n-grid-item>
|
|
<n-select v-model:value="filters.type" :options="typeOptions" placeholder="类型" />
|
|
</n-grid-item>
|
|
<n-grid-item>
|
|
<n-date-picker v-model:value="filters.dateRange" type="daterange" clearable style="width: 100%;" />
|
|
</n-grid-item>
|
|
<n-grid-item>
|
|
<n-space>
|
|
<n-button type="primary" @click="handleSearch">筛选</n-button>
|
|
<n-button @click="resetFilters">重置</n-button>
|
|
</n-space>
|
|
</n-grid-item>
|
|
</n-grid>
|
|
|
|
<n-checkbox-group v-model:value="visibleColumnKeys">
|
|
<n-space wrap>
|
|
<n-checkbox v-for="column in columnChoices" :key="column.value" :value="column.value">
|
|
{{ column.label }}
|
|
</n-checkbox>
|
|
</n-space>
|
|
</n-checkbox-group>
|
|
</n-space>
|
|
</n-card>
|
|
|
|
<n-card class="aksrt-rebate-page__card" size="small" title="返佣台账">
|
|
<n-data-table
|
|
:columns="tableColumns"
|
|
:data="rows"
|
|
:loading="loading"
|
|
:bordered="false"
|
|
:pagination="pagination"
|
|
:row-key="(row) => row.id"
|
|
/>
|
|
</n-card>
|
|
|
|
<RebateDetailDrawer
|
|
v-model:show="drawerVisible"
|
|
:rebate-id="currentRebateId"
|
|
@updated="loadData"
|
|
/>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { computed, h, onMounted, reactive, ref } from 'vue'
|
|
import { NButton, NSpace, NTag, useMessage } from 'naive-ui'
|
|
import api from '@/api'
|
|
import PageHeader from '@/components/PageHeader.vue'
|
|
import RebateDetailDrawer from './components/RebateDetailDrawer.vue'
|
|
|
|
const message = useMessage()
|
|
const loading = ref(false)
|
|
const rows = ref([])
|
|
const drawerVisible = ref(false)
|
|
const currentRebateId = ref(null)
|
|
|
|
const stats = reactive({
|
|
total: 0,
|
|
pending: 0,
|
|
settled: 0,
|
|
total_amount: 0,
|
|
settled_amount: 0,
|
|
})
|
|
|
|
const filters = reactive({
|
|
keyword: '',
|
|
status: '',
|
|
type: '',
|
|
dateRange: null,
|
|
})
|
|
|
|
const pager = reactive({
|
|
page: 1,
|
|
pageSize: 20,
|
|
itemCount: 0,
|
|
})
|
|
|
|
const optionsState = reactive({
|
|
statuses: [],
|
|
types: [],
|
|
})
|
|
|
|
const statCards = [
|
|
{ key: 'total', label: '总记录' },
|
|
{ key: 'pending', label: '待结算' },
|
|
{ key: 'settled', label: '已结算' },
|
|
{ key: 'total_amount', label: '总返佣' },
|
|
{ key: 'settled_amount', label: '已结算金额' },
|
|
]
|
|
|
|
const statusOptions = computed(() => optionsState.statuses.length
|
|
? optionsState.statuses
|
|
: [
|
|
{ label: '全部状态', value: '' },
|
|
{ label: '待结算', value: 'pending' },
|
|
{ label: '已结算', value: 'settled' },
|
|
{ label: '已取消', value: 'cancelled' },
|
|
])
|
|
|
|
const typeOptions = computed(() => optionsState.types.length
|
|
? optionsState.types
|
|
: [
|
|
{ label: '全部类型', value: '' },
|
|
{ label: '订单返佣', value: 'order' },
|
|
{ label: '邀请返佣', value: 'invite' },
|
|
])
|
|
|
|
const columnChoices = [
|
|
{ label: '来源用户', value: 'source_user' },
|
|
{ label: '创建时间', value: 'created_at' },
|
|
{ label: '结算时间', value: 'settled_at' },
|
|
{ label: '备注', value: 'note' },
|
|
{ label: '处理记录数', value: 'history_count' },
|
|
]
|
|
|
|
const visibleColumnKeys = ref(['source_user', 'created_at', 'settled_at', 'history_count'])
|
|
|
|
const baseColumns = computed(() => {
|
|
const columns = [
|
|
{ title: 'ID', key: 'id', width: 70 },
|
|
{ title: '订单号', key: 'order_number', minWidth: 180, ellipsis: { tooltip: true } },
|
|
{
|
|
title: '返佣对象',
|
|
key: 'user',
|
|
width: 160,
|
|
render: (row) => row.user?.display_name || '-',
|
|
},
|
|
{
|
|
title: '返佣金额',
|
|
key: 'amount',
|
|
width: 100,
|
|
},
|
|
{
|
|
title: '状态',
|
|
key: 'status',
|
|
width: 100,
|
|
render: (row) => {
|
|
const typeMap = { pending: 'warning', settled: 'success', cancelled: 'default' }
|
|
return h(NTag, { size: 'small', round: true, type: typeMap[row.status] || 'default' }, () => row.status_label)
|
|
},
|
|
},
|
|
{
|
|
title: '类型',
|
|
key: 'type',
|
|
width: 100,
|
|
render: (row) => row.type_label || row.type,
|
|
},
|
|
]
|
|
|
|
if (visibleColumnKeys.value.includes('source_user')) {
|
|
columns.push({
|
|
title: '来源用户',
|
|
key: 'source_user',
|
|
width: 160,
|
|
render: (row) => row.source_user?.display_name || '-',
|
|
})
|
|
}
|
|
|
|
if (visibleColumnKeys.value.includes('created_at')) {
|
|
columns.push({ title: '创建时间', key: 'created_at', width: 170 })
|
|
}
|
|
|
|
if (visibleColumnKeys.value.includes('settled_at')) {
|
|
columns.push({
|
|
title: '结算时间',
|
|
key: 'settled_at',
|
|
width: 170,
|
|
render: (row) => row.settled_at || '-',
|
|
})
|
|
}
|
|
|
|
if (visibleColumnKeys.value.includes('note')) {
|
|
columns.push({
|
|
title: '备注',
|
|
key: 'note',
|
|
minWidth: 160,
|
|
ellipsis: { tooltip: true },
|
|
render: (row) => row.note || '-',
|
|
})
|
|
}
|
|
|
|
if (visibleColumnKeys.value.includes('history_count')) {
|
|
columns.push({ title: '处理记录数', key: 'history_count', width: 100 })
|
|
}
|
|
|
|
columns.push({
|
|
title: '操作',
|
|
key: 'actions',
|
|
width: 120,
|
|
render: (row) => h(NSpace, { size: 'small' }, () => [
|
|
h(NButton, {
|
|
size: 'tiny',
|
|
type: 'primary',
|
|
quaternary: true,
|
|
onClick: () => openDrawer(row.id),
|
|
}, () => '详情'),
|
|
]),
|
|
})
|
|
|
|
return columns
|
|
})
|
|
|
|
const tableColumns = computed(() => baseColumns.value)
|
|
|
|
const pagination = computed(() => ({
|
|
page: pager.page,
|
|
pageSize: pager.pageSize,
|
|
itemCount: pager.itemCount,
|
|
showSizePicker: true,
|
|
pageSizes: [10, 20, 50, 100],
|
|
onUpdatePage: (page) => {
|
|
pager.page = page
|
|
loadData()
|
|
},
|
|
onUpdatePageSize: (pageSize) => {
|
|
pager.pageSize = pageSize
|
|
pager.page = 1
|
|
loadData()
|
|
},
|
|
}))
|
|
|
|
onMounted(() => {
|
|
loadData()
|
|
})
|
|
|
|
function formatDateValue(value) {
|
|
const date = new Date(value)
|
|
const year = date.getFullYear()
|
|
const month = `${date.getMonth() + 1}`.padStart(2, '0')
|
|
const day = `${date.getDate()}`.padStart(2, '0')
|
|
return `${year}-${month}-${day}`
|
|
}
|
|
|
|
function buildParams() {
|
|
const params = {
|
|
page: pager.page,
|
|
per_page: pager.pageSize,
|
|
}
|
|
if (filters.keyword) params.keyword = filters.keyword
|
|
if (filters.status) params.status = filters.status
|
|
if (filters.type) params.type = filters.type
|
|
if (Array.isArray(filters.dateRange) && filters.dateRange.length === 2) {
|
|
params.date_from = formatDateValue(filters.dateRange[0])
|
|
params.date_to = formatDateValue(filters.dateRange[1])
|
|
}
|
|
return params
|
|
}
|
|
|
|
async function loadData() {
|
|
loading.value = true
|
|
try {
|
|
const res = await api.request('/payment/rebate/log', { params: buildParams() })
|
|
const data = res?.data || {}
|
|
rows.value = data.items || []
|
|
pager.itemCount = data.total || 0
|
|
Object.assign(stats, data.stats || {})
|
|
optionsState.statuses = data.options?.statuses || []
|
|
optionsState.types = data.options?.types || []
|
|
} catch (error) {
|
|
message.error(error?.message || '加载返佣台账失败')
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
function handleSearch() {
|
|
pager.page = 1
|
|
loadData()
|
|
}
|
|
|
|
function resetFilters() {
|
|
filters.keyword = ''
|
|
filters.status = ''
|
|
filters.type = ''
|
|
filters.dateRange = null
|
|
pager.page = 1
|
|
loadData()
|
|
}
|
|
|
|
function openDrawer(id) {
|
|
currentRebateId.value = id
|
|
drawerVisible.value = true
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.aksrt-rebate-page__stats {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
|
gap: 12px;
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
.aksrt-rebate-page__card {
|
|
margin-top: 16px;
|
|
}
|
|
</style>
|