Files
yunzerwebsiteallinone/platform/src/views/system/payment/commission/index.vue
T
2026-09-16 14:28:07 +08:00

404 lines
11 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>