增加支付功能

This commit is contained in:
2026-09-15 12:46:29 +08:00
parent 943c3708b0
commit a82b300b1a
46 changed files with 11675 additions and 21 deletions
@@ -0,0 +1,421 @@
<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'
// TODO: 接口联调时打开
// 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 || '-'
/**
* 统计卡片数据:TODO 换成 getCommissionStats()
* 后端聚合自 yz_platform_commission_order:
* 应付总额 = sum(amount) where status='payable'
* 已付总额 = sum(amount) where status='settled'
* 待处理笔数 = count(*) where status in ('payable','tax_processing')
*/
const stats = reactive({
payable_amount: 1288000,
paid_amount: 4560500,
pending_count: 23
})
/* ---------------- Mock 数据(本地筛选 + 分页) ---------------- */
interface CommissionRow {
id: number
commission_no: string
out_trade_no: string
promoter_name: string
amount: number
status: string
pay_method: string
create_time: string
paid_at: string
}
const ALL_COMMISSIONS: CommissionRow[] = Array.from({ length: 24 }).map((_, index) => {
const promoters = ['云泽渠道-张明', '渠道伙伴-李雷', '服务商-杭州微光', '渠道伙伴-王芳', '服务商-深圳智联']
const statuses = ['payable', 'payable', 'settled', 'settled', 'tax_processing']
const methods = ['bank', 'balance', 'manual', 'offset']
const day = String(15 - Math.floor(index / 8)).padStart(2, '0')
const status = statuses[index % statuses.length]
return {
id: index + 1,
commission_no: `CM202609${day}${String(3000 + index)}`,
out_trade_no: `ORD202609${day}${String(2000 + index)}`,
promoter_name: promoters[index % promoters.length],
amount: [9990, 19990, 29990, 49990][index % 4],
status,
pay_method: methods[index % methods.length],
create_time: `2026-09-${day} 11:${String((index * 5) % 60).padStart(2, '0')}:00`,
paid_at: status === 'settled' ? `2026-09-${day} 16:20:00` : ''
}
})
const tableData = ref<CommissionRow[]>([])
async function fetchList() {
loading.value = true
try {
// TODO: 接口联通后替换为真实请求
// const res = await getCommissionList({
// page: page.current,
// pageSize: page.size,
// promoter: filters.promoter,
// status: filters.status,
// start_time: filters.dateRange?.[0],
// end_time: filters.dateRange?.[1]
// })
// tableData.value = res.data.list || []
// page.total = res.data.total || 0
const [start, end] = filters.dateRange || []
const filtered = ALL_COMMISSIONS.filter((item) => {
if (filters.promoter && !item.promoter_name.includes(filters.promoter)) return false
if (filters.status && item.status !== filters.status) return false
const date = item.create_time.slice(0, 10)
if (start && date < start) return false
if (end && date > end) return false
return true
})
page.total = filtered.length
const from = (page.current - 1) * page.size
tableData.value = filtered.slice(from, from + page.size)
} catch (error: any) {
ElMessage.error(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: '确认发放'
}
)
// TODO: 接口联通后替换为 markCommissionPaid(row.id, { pay_method: row.pay_method, remark: value })
row.status = 'settled'
row.paid_at = new Date().toLocaleString('zh-CN')
ElMessage.success('已标记为已付')
await fetchList()
} catch {
// 取消
}
}
function goRules() {
router.push(RULES_PATH)
}
function goOrder(row: CommissionRow) {
router.push({ path: ORDER_DETAIL_PATH, query: { outTradeNo: row.out_trade_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(() => {
fetchList()
})
</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>