增加支付功能
This commit is contained in:
@@ -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>
|
||||
Reference in New Issue
Block a user