404 lines
11 KiB
Vue
404 lines
11 KiB
Vue
<template>
|
||
<div class="container-box">
|
||
<div class="header-bar">
|
||
<div>
|
||
<h2>推广佣金台账</h2>
|
||
<p class="sub-title">
|
||
推广佣金为平台自有资金支出,与租户收款解耦;佣金按规则在业务订单支付成功后生成,独立发放。
|
||
</p>
|
||
</div>
|
||
<div class="header-actions">
|
||
<el-button @click="goRules">
|
||
<el-icon><Setting /></el-icon>
|
||
佣金规则
|
||
</el-button>
|
||
<el-button :loading="loading" @click="refresh">
|
||
<el-icon><Refresh /></el-icon>
|
||
刷新
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
<el-divider></el-divider>
|
||
|
||
<!-- 统计卡片 -->
|
||
<div class="stat-grid">
|
||
<el-card shadow="never" class="stat-card">
|
||
<div class="stat-label">应付总额</div>
|
||
<div class="stat-value warning">¥{{ fenToYuan(stats.payable_amount) }}</div>
|
||
<div class="stat-tip">已生成佣金、尚未发放的金额</div>
|
||
</el-card>
|
||
<el-card shadow="never" class="stat-card">
|
||
<div class="stat-label">已付总额</div>
|
||
<div class="stat-value success">¥{{ fenToYuan(stats.paid_amount) }}</div>
|
||
<div class="stat-tip">已完成发放登记的金额</div>
|
||
</el-card>
|
||
<el-card shadow="never" class="stat-card">
|
||
<div class="stat-label">待处理笔数</div>
|
||
<div class="stat-value danger">{{ stats.pending_count }} 笔</div>
|
||
<div class="stat-tip">含应付与税务处理中的佣金单</div>
|
||
</el-card>
|
||
</div>
|
||
|
||
<!-- 筛选区 -->
|
||
<el-card shadow="never" class="filter-card">
|
||
<el-form :inline="true" :model="filters" class="filter-form">
|
||
<el-form-item label="推广方">
|
||
<el-input
|
||
v-model="filters.promoter"
|
||
placeholder="推广方名称 / 编号"
|
||
clearable
|
||
style="width: 220px"
|
||
@keyup.enter="handleSearch"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="状态">
|
||
<el-select v-model="filters.status" placeholder="全部状态" clearable style="width: 160px">
|
||
<el-option
|
||
v-for="item in statusOptions"
|
||
:key="item.value"
|
||
:label="item.label"
|
||
:value="item.value"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="下单时间">
|
||
<el-date-picker
|
||
v-model="filters.dateRange"
|
||
type="daterange"
|
||
unlink-panels
|
||
range-separator="至"
|
||
start-placeholder="开始日期"
|
||
end-placeholder="结束日期"
|
||
value-format="YYYY-MM-DD"
|
||
style="width: 260px"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item>
|
||
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||
<el-button @click="handleReset">重置</el-button>
|
||
</el-form-item>
|
||
</el-form>
|
||
</el-card>
|
||
|
||
<!-- 台账列表 -->
|
||
<el-card shadow="never">
|
||
<el-table :data="tableData" stripe v-loading="loading" style="width: 100%">
|
||
<el-table-column prop="commission_no" label="佣金单号" min-width="180" show-overflow-tooltip />
|
||
<el-table-column prop="out_trade_no" label="关联订单" min-width="180">
|
||
<template #default="{ row }">
|
||
<el-button size="small" type="primary" link @click="goOrder(row)">{{ row.out_trade_no }}</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="promoter_name" label="推广方" min-width="180" show-overflow-tooltip />
|
||
<el-table-column label="佣金金额" width="130" align="right">
|
||
<template #default="{ row }">
|
||
<span class="amount">¥{{ fenToYuan(row.amount) }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="状态" width="130" align="center">
|
||
<template #default="{ row }">
|
||
<StatusTag :status="row.status" dict="commission" />
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="发放方式" width="130" align="center">
|
||
<template #default="{ row }">{{ payMethodLabel(row.pay_method) }}</template>
|
||
</el-table-column>
|
||
<el-table-column prop="create_time" label="生成时间" width="180" align="center" />
|
||
<el-table-column prop="paid_at" label="发放时间" width="180" align="center">
|
||
<template #default="{ row }">{{ row.paid_at || '-' }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="操作" width="120" fixed="right" align="center">
|
||
<template #default="{ row }">
|
||
<el-button
|
||
v-if="row.status === 'payable'"
|
||
size="small"
|
||
type="primary"
|
||
link
|
||
@click="handleMarkPaid(row)"
|
||
>
|
||
标记已付
|
||
</el-button>
|
||
<span v-else class="text-muted">-</span>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
|
||
<div class="pagination-bar">
|
||
<el-pagination
|
||
v-model:current-page="page.current"
|
||
v-model:page-size="page.size"
|
||
:total="page.total"
|
||
:page-sizes="[10, 20, 50, 100]"
|
||
layout="total, sizes, prev, pager, next, jumper"
|
||
@size-change="fetchList"
|
||
@current-change="fetchList"
|
||
/>
|
||
</div>
|
||
</el-card>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { reactive, ref, onMounted } from 'vue'
|
||
import { useRouter } from 'vue-router'
|
||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||
import { Refresh, Setting } from '@element-plus/icons-vue'
|
||
import StatusTag from '../components/StatusTag.vue'
|
||
import { getCommissionList, getCommissionStats, markCommissionPaid } from '@/api/payment'
|
||
|
||
const router = useRouter()
|
||
const loading = ref(false)
|
||
|
||
/** TODO: 路由 path 需与你配置的菜单 path 保持一致 */
|
||
const RULES_PATH = '/system/payment/commission/rules'
|
||
const ORDER_DETAIL_PATH = '/system/payment/orders/detail'
|
||
|
||
const filters = reactive<{
|
||
promoter: string
|
||
status: string
|
||
dateRange: [string, string] | []
|
||
}>({
|
||
promoter: '',
|
||
status: '',
|
||
dateRange: []
|
||
})
|
||
|
||
const page = reactive({ current: 1, size: 10, total: 0 })
|
||
|
||
const statusOptions = [
|
||
{ label: '应付', value: 'payable' },
|
||
{ label: '已付', value: 'settled' },
|
||
{ label: '税务处理中', value: 'tax_processing' },
|
||
{ label: '已作废', value: 'canceled' }
|
||
]
|
||
|
||
const payMethodMap: Record<string, string> = {
|
||
balance: '余额抵扣',
|
||
bank: '银行转账',
|
||
manual: '人工发放',
|
||
offset: '下期抵扣'
|
||
}
|
||
const payMethodLabel = (method: string) => payMethodMap[method] || method || '-'
|
||
|
||
/** 统计卡片数据。 */
|
||
const stats = reactive({
|
||
payable_amount: 0,
|
||
paid_amount: 0,
|
||
pending_count: 0
|
||
})
|
||
|
||
/* ---------------- 列表数据 ---------------- */
|
||
|
||
interface CommissionRow {
|
||
id: number
|
||
commission_no: string
|
||
pay_no: string
|
||
out_trade_no: string
|
||
promoter_name: string
|
||
amount: number
|
||
status: string
|
||
pay_method: string
|
||
create_time: string
|
||
paid_at: string
|
||
}
|
||
|
||
const tableData = ref<CommissionRow[]>([])
|
||
|
||
async function fetchStats() {
|
||
try {
|
||
const res: any = await getCommissionStats()
|
||
if (res?.code === 200 && res.data) Object.assign(stats, res.data)
|
||
} catch (error: any) {
|
||
ElMessage.error(error?.response?.data?.msg || error?.message || '获取佣金统计失败')
|
||
}
|
||
}
|
||
|
||
async function fetchList() {
|
||
loading.value = true
|
||
try {
|
||
const res: any = await getCommissionList({
|
||
page: page.current,
|
||
pageSize: page.size,
|
||
promoter: filters.promoter,
|
||
status: filters.status,
|
||
start_time: filters.dateRange?.[0],
|
||
end_time: filters.dateRange?.[1]
|
||
})
|
||
if (res?.code === 200) {
|
||
tableData.value = res.data?.list || []
|
||
page.total = Number(res.data?.total || 0)
|
||
} else {
|
||
ElMessage.error(res?.msg || '获取佣金台账失败')
|
||
}
|
||
} catch (error: any) {
|
||
ElMessage.error(error?.response?.data?.msg || error?.message || '获取佣金台账失败')
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
function handleSearch() {
|
||
page.current = 1
|
||
fetchList()
|
||
}
|
||
|
||
function handleReset() {
|
||
filters.promoter = ''
|
||
filters.status = ''
|
||
filters.dateRange = []
|
||
page.current = 1
|
||
fetchList()
|
||
}
|
||
|
||
async function refresh() {
|
||
await fetchList()
|
||
ElMessage.success('刷新成功')
|
||
}
|
||
|
||
async function handleMarkPaid(row: CommissionRow) {
|
||
try {
|
||
const { value } = await ElMessageBox.prompt(
|
||
`确认将佣金单 ${row.commission_no}(¥${fenToYuan(row.amount)})标记为已发放?`,
|
||
'标记已付',
|
||
{
|
||
type: 'warning',
|
||
inputPlaceholder: '请输入发放方式说明 / 流水号',
|
||
inputValidator: (val: string) => (val && val.trim() ? true : '请填写发放说明'),
|
||
confirmButtonText: '确认发放'
|
||
}
|
||
)
|
||
const res: any = await markCommissionPaid(row.id, {
|
||
pay_method: row.pay_method || 'manual',
|
||
remark: value
|
||
})
|
||
if (res?.code !== 200) {
|
||
ElMessage.error(res?.msg || '标记已付失败')
|
||
return
|
||
}
|
||
ElMessage.success('已标记为已付')
|
||
await Promise.all([fetchList(), fetchStats()])
|
||
} catch {
|
||
// 取消
|
||
}
|
||
}
|
||
|
||
function goRules() {
|
||
router.push(RULES_PATH)
|
||
}
|
||
|
||
function goOrder(row: CommissionRow) {
|
||
if (!row.pay_no) {
|
||
ElMessage.warning('该佣金单未关联支付单')
|
||
return
|
||
}
|
||
router.push({ path: ORDER_DETAIL_PATH, query: { payNo: row.pay_no } })
|
||
}
|
||
|
||
/** 金额:分 -> 元 */
|
||
function fenToYuan(fen: number | string | null | undefined): string {
|
||
const n = Number(fen ?? 0)
|
||
if (!Number.isFinite(n)) return '0.00'
|
||
return (n / 100).toFixed(2)
|
||
}
|
||
|
||
onMounted(() => {
|
||
Promise.all([fetchList(), fetchStats()])
|
||
})
|
||
</script>
|
||
|
||
<style scoped lang="less">
|
||
.container-box {
|
||
padding: 20px;
|
||
}
|
||
|
||
.header-bar {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: flex-start;
|
||
margin-bottom: 20px;
|
||
|
||
h2 {
|
||
margin: 0;
|
||
font-size: 20px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.sub-title {
|
||
margin: 8px 0 0;
|
||
font-size: 13px;
|
||
line-height: 1.6;
|
||
color: #909399;
|
||
}
|
||
|
||
.header-actions {
|
||
display: flex;
|
||
gap: 8px;
|
||
flex-shrink: 0;
|
||
}
|
||
}
|
||
|
||
.stat-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||
gap: 16px;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.stat-card {
|
||
.stat-label {
|
||
font-size: 13px;
|
||
color: #909399;
|
||
}
|
||
|
||
.stat-value {
|
||
margin-top: 8px;
|
||
font-size: 24px;
|
||
font-weight: 600;
|
||
|
||
&.warning {
|
||
color: #e6a23c;
|
||
}
|
||
|
||
&.success {
|
||
color: #67c23a;
|
||
}
|
||
|
||
&.danger {
|
||
color: #f56c6c;
|
||
}
|
||
}
|
||
|
||
.stat-tip {
|
||
margin-top: 6px;
|
||
font-size: 12px;
|
||
color: #909399;
|
||
}
|
||
}
|
||
|
||
.filter-card {
|
||
margin-bottom: 20px;
|
||
|
||
.filter-form {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
align-items: center;
|
||
margin-bottom: -18px;
|
||
}
|
||
}
|
||
|
||
.amount {
|
||
font-weight: 600;
|
||
color: #f56c6c;
|
||
}
|
||
|
||
.text-muted {
|
||
color: #909399;
|
||
}
|
||
|
||
.pagination-bar {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
margin-top: 20px;
|
||
}
|
||
</style>
|