|
|
@@ -0,0 +1,431 @@
|
|
|
+<template>
|
|
|
+ <div class="dan-settlement admin-page">
|
|
|
+ <!-- 统计卡片 -->
|
|
|
+ <el-row :gutter="20" style="margin-bottom: 20px;">
|
|
|
+ <el-col :span="8">
|
|
|
+ <el-card shadow="hover">
|
|
|
+ <div class="stat-card">
|
|
|
+ <div class="stat-icon-wrap total">
|
|
|
+ <i class="el-icon-money"></i>
|
|
|
+ </div>
|
|
|
+ <div class="stat-info">
|
|
|
+ <div class="stat-label">总结算金额</div>
|
|
|
+ <div class="stat-value">¥{{ (stats.totalAmount / 100).toFixed(2) }}</div>
|
|
|
+ <div class="stat-desc">共 {{ stats.totalCount }} 笔</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-card>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="8">
|
|
|
+ <el-card shadow="hover">
|
|
|
+ <div class="stat-card">
|
|
|
+ <div class="stat-icon-wrap pending">
|
|
|
+ <i class="el-icon-time"></i>
|
|
|
+ </div>
|
|
|
+ <div class="stat-info">
|
|
|
+ <div class="stat-label">待结算金额</div>
|
|
|
+ <div class="stat-value pending">¥{{ (stats.pendingAmount / 100).toFixed(2) }}</div>
|
|
|
+ <div class="stat-desc">共 {{ stats.pendingCount }} 笔</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-card>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="8">
|
|
|
+ <el-card shadow="hover">
|
|
|
+ <div class="stat-card">
|
|
|
+ <div class="stat-icon-wrap settled">
|
|
|
+ <i class="el-icon-circle-check"></i>
|
|
|
+ </div>
|
|
|
+ <div class="stat-info">
|
|
|
+ <div class="stat-label">已结算金额</div>
|
|
|
+ <div class="stat-value settled">¥{{ (stats.settledAmount / 100).toFixed(2) }}</div>
|
|
|
+ <div class="stat-desc">共 {{ stats.settledCount }} 笔</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-card>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+
|
|
|
+ <!-- 结算列表 -->
|
|
|
+ <el-card>
|
|
|
+ <div slot="header" class="admin-page-header">
|
|
|
+ <span class="admin-page-title">结算记录管理</span>
|
|
|
+ <div class="filter-bar">
|
|
|
+ <el-select v-model="statusFilter" placeholder="结算状态" clearable @change="handleSearch" style="width: 140px; margin-right: 10px;">
|
|
|
+ <el-option label="全部状态" value="" />
|
|
|
+ <el-option label="待结算" value="pending" />
|
|
|
+ <el-option label="已结算" value="settled" />
|
|
|
+ <el-option label="结算失败" value="failed" />
|
|
|
+ </el-select>
|
|
|
+ <el-select v-model="providerTypeFilter" placeholder="服务类型" clearable @change="handleSearch" style="width: 140px; margin-right: 10px;">
|
|
|
+ <el-option label="全部类型" value="" />
|
|
|
+ <el-option label="评估师" value="assessor" />
|
|
|
+ <el-option label="规划师" value="planner" />
|
|
|
+ </el-select>
|
|
|
+ <el-button type="primary" icon="el-icon-search" @click="handleSearch">搜索</el-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <el-table :data="list" v-loading="loading" border stripe empty-text="暂无结算记录">
|
|
|
+ <el-table-column label="ID" prop="id" width="80" />
|
|
|
+ <el-table-column label="执行ID" prop="executionId" width="100" />
|
|
|
+ <el-table-column label="订单ID" prop="assessmentOrderId" width="100" />
|
|
|
+ <el-table-column label="服务方ID" prop="providerId" width="100" />
|
|
|
+ <el-table-column label="服务类型" width="110">
|
|
|
+ <template slot-scope="{ row }">
|
|
|
+ <div class="status-cell">
|
|
|
+ <i :class="row.providerType === 'assessor' ? 'el-icon-user' : 'el-icon-s-custom'"
|
|
|
+ :style="{ color: row.providerType === 'assessor' ? '#409EFF' : '#67c23a' }"></i>
|
|
|
+ <el-tag :type="row.providerType === 'assessor' ? '' : 'success'" size="mini">
|
|
|
+ {{ row.providerType === 'assessor' ? '评估师' : '规划师' }}
|
|
|
+ </el-tag>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="服务费" width="100">
|
|
|
+ <template slot-scope="{ row }">
|
|
|
+ ¥{{ (row.serviceFee / 100).toFixed(2) }}
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="佣金比例" width="100">
|
|
|
+ <template slot-scope="{ row }">
|
|
|
+ {{ row.commissionRate }}%
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="结算金额" width="120">
|
|
|
+ <template slot-scope="{ row }">
|
|
|
+ <span style="font-weight: bold; color: #409EFF;">
|
|
|
+ ¥{{ (row.commissionAmount / 100).toFixed(2) }}
|
|
|
+ </span>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="结算状态" width="130">
|
|
|
+ <template slot-scope="{ row }">
|
|
|
+ <div class="status-cell">
|
|
|
+ <i :class="statusIconClass(row.status)" :style="{ color: statusIconColor(row.status) }"></i>
|
|
|
+ <el-tag :type="statusType(row.status)" size="mini">
|
|
|
+ {{ statusLabel(row.status) }}
|
|
|
+ </el-tag>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="结算时间" width="160">
|
|
|
+ <template slot-scope="{ row }">{{ formatTime(row.settledAt) }}</template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="交易ID" prop="transactionId" min-width="150" show-overflow-tooltip />
|
|
|
+ <el-table-column label="创建时间" width="160">
|
|
|
+ <template slot-scope="{ row }">{{ formatTime(row.createdAt) }}</template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="操作" width="150" fixed="right">
|
|
|
+ <template slot-scope="{ row }">
|
|
|
+ <el-button
|
|
|
+ v-if="row.status === 'pending'"
|
|
|
+ size="mini"
|
|
|
+ type="success"
|
|
|
+ icon="el-icon-check"
|
|
|
+ plain
|
|
|
+ @click="confirmSettlement(row)"
|
|
|
+ >确认结算</el-button>
|
|
|
+ <el-button size="mini" type="primary" icon="el-icon-view" plain @click="viewDetail(row)">详情</el-button>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table>
|
|
|
+
|
|
|
+ <el-pagination
|
|
|
+ @size-change="handleSizeChange"
|
|
|
+ @current-change="handleCurrentChange"
|
|
|
+ :current-page="page"
|
|
|
+ :page-sizes="[10, 20, 50]"
|
|
|
+ :page-size="size"
|
|
|
+ :total="total"
|
|
|
+ layout="total, sizes, prev, pager, next, jumper"
|
|
|
+ class="pagination-wrap"
|
|
|
+ />
|
|
|
+ </el-card>
|
|
|
+
|
|
|
+ <!-- 详情 Dialog -->
|
|
|
+ <el-dialog title="结算详情" :visible.sync="detailDialogVisible" width="600px">
|
|
|
+ <div v-if="currentRecord" v-loading="detailLoading">
|
|
|
+ <el-descriptions :column="2" border size="small">
|
|
|
+ <el-descriptions-item label="结算ID">{{ currentRecord.id }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="执行ID">{{ currentRecord.executionId }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="订单ID">{{ currentRecord.assessmentOrderId }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="服务方ID">{{ currentRecord.providerId }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="服务类型">
|
|
|
+ <div class="status-cell">
|
|
|
+ <i :class="currentRecord.providerType === 'assessor' ? 'el-icon-user' : 'el-icon-s-custom'"
|
|
|
+ :style="{ color: currentRecord.providerType === 'assessor' ? '#409EFF' : '#67c23a' }"></i>
|
|
|
+ <el-tag :type="currentRecord.providerType === 'assessor' ? '' : 'success'" size="mini">
|
|
|
+ {{ currentRecord.providerType === 'assessor' ? '评估师' : '规划师' }}
|
|
|
+ </el-tag>
|
|
|
+ </div>
|
|
|
+ </el-descriptions-item>
|
|
|
+ <el-descriptions-item label="服务费">¥{{ (currentRecord.serviceFee / 100).toFixed(2) }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="佣金比例">{{ currentRecord.commissionRate }}%</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="结算金额">
|
|
|
+ <span style="font-weight: bold; color: #409EFF;">
|
|
|
+ ¥{{ (currentRecord.commissionAmount / 100).toFixed(2) }}
|
|
|
+ </span>
|
|
|
+ </el-descriptions-item>
|
|
|
+ <el-descriptions-item label="结算状态">
|
|
|
+ <div class="status-cell">
|
|
|
+ <i :class="statusIconClass(currentRecord.status)" :style="{ color: statusIconColor(currentRecord.status) }"></i>
|
|
|
+ <el-tag :type="statusType(currentRecord.status)" size="mini">
|
|
|
+ {{ statusLabel(currentRecord.status) }}
|
|
|
+ </el-tag>
|
|
|
+ </div>
|
|
|
+ </el-descriptions-item>
|
|
|
+ <el-descriptions-item label="结算时间">{{ formatTime(currentRecord.settledAt) }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="交易ID">{{ currentRecord.transactionId || '-' }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="创建时间">{{ formatTime(currentRecord.createdAt) }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="更新时间">{{ formatTime(currentRecord.updatedAt) }}</el-descriptions-item>
|
|
|
+ </el-descriptions>
|
|
|
+ </div>
|
|
|
+ </el-dialog>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script>
|
|
|
+export default {
|
|
|
+ name: 'DanSettlement',
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ list: [],
|
|
|
+ loading: false,
|
|
|
+ page: 1,
|
|
|
+ size: 10,
|
|
|
+ total: 0,
|
|
|
+ statusFilter: '',
|
|
|
+ providerTypeFilter: '',
|
|
|
+ stats: {
|
|
|
+ totalAmount: 0,
|
|
|
+ pendingAmount: 0,
|
|
|
+ settledAmount: 0,
|
|
|
+ totalCount: 0,
|
|
|
+ pendingCount: 0,
|
|
|
+ settledCount: 0
|
|
|
+ },
|
|
|
+ detailDialogVisible: false,
|
|
|
+ detailLoading: false,
|
|
|
+ currentRecord: null
|
|
|
+ }
|
|
|
+ },
|
|
|
+ created() {
|
|
|
+ this.loadData()
|
|
|
+ this.loadStats()
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ async loadData() {
|
|
|
+ this.loading = true
|
|
|
+ try {
|
|
|
+ const res = await this.$axios.post('/api/admin/dan-settlement/list', {
|
|
|
+ status: this.statusFilter,
|
|
|
+ providerType: this.providerTypeFilter
|
|
|
+ })
|
|
|
+ if (res.data.code === 200) {
|
|
|
+ this.list = res.data.data || []
|
|
|
+ this.total = this.list.length
|
|
|
+ } else {
|
|
|
+ this.$message.error(res.data.message || '加载失败')
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ console.error('加载结算记录失败', e)
|
|
|
+ this.$message.error('加载失败')
|
|
|
+ } finally {
|
|
|
+ this.loading = false
|
|
|
+ }
|
|
|
+ },
|
|
|
+ async loadStats() {
|
|
|
+ try {
|
|
|
+ const res = await this.$axios.post('/api/admin/dan-settlement/stats')
|
|
|
+ if (res.data.code === 200) {
|
|
|
+ this.stats = res.data.data || this.stats
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ console.error('加载统计数据失败', e)
|
|
|
+ }
|
|
|
+ },
|
|
|
+ async viewDetail(row) {
|
|
|
+ this.detailDialogVisible = true
|
|
|
+ this.detailLoading = true
|
|
|
+ try {
|
|
|
+ const res = await this.$axios.get(`/api/admin/dan-settlement/detail/${row.id}`)
|
|
|
+ if (res.data.code === 200) {
|
|
|
+ this.currentRecord = res.data.data
|
|
|
+ } else {
|
|
|
+ this.$message.error(res.data.message || '加载失败')
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ console.error('加载详情失败', e)
|
|
|
+ this.$message.error('加载失败')
|
|
|
+ } finally {
|
|
|
+ this.detailLoading = false
|
|
|
+ }
|
|
|
+ },
|
|
|
+ async confirmSettlement(row) {
|
|
|
+ try {
|
|
|
+ await this.$confirm('确认将此结算记录标记为已结算?', '提示', {
|
|
|
+ confirmButtonText: '确定',
|
|
|
+ cancelButtonText: '取消',
|
|
|
+ type: 'warning'
|
|
|
+ })
|
|
|
+
|
|
|
+ const res = await this.$axios.post(`/api/admin/dan-settlement/confirm/${row.id}`)
|
|
|
+ if (res.data.code === 200) {
|
|
|
+ this.$message.success('确认成功')
|
|
|
+ this.loadData()
|
|
|
+ this.loadStats()
|
|
|
+ } else {
|
|
|
+ this.$message.error(res.data.message || '操作失败')
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ if (e !== 'cancel') {
|
|
|
+ console.error('确认结算失败', e)
|
|
|
+ this.$message.error('操作失败')
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ handleSearch() {
|
|
|
+ this.page = 1
|
|
|
+ this.loadData()
|
|
|
+ },
|
|
|
+ handleSizeChange(val) {
|
|
|
+ this.size = val
|
|
|
+ this.loadData()
|
|
|
+ },
|
|
|
+ handleCurrentChange(val) {
|
|
|
+ this.page = val
|
|
|
+ this.loadData()
|
|
|
+ },
|
|
|
+ formatTime(time) {
|
|
|
+ if (!time) return '-'
|
|
|
+ return new Date(time).toLocaleString('zh-CN')
|
|
|
+ },
|
|
|
+ statusType(status) {
|
|
|
+ if (status === 'settled') return 'success'
|
|
|
+ if (status === 'pending') return 'warning'
|
|
|
+ if (status === 'failed') return 'danger'
|
|
|
+ return 'info'
|
|
|
+ },
|
|
|
+ statusLabel(status) {
|
|
|
+ if (status === 'settled') return '已结算'
|
|
|
+ if (status === 'pending') return '待结算'
|
|
|
+ if (status === 'failed') return '失败'
|
|
|
+ return status || '-'
|
|
|
+ },
|
|
|
+ statusIconClass(status) {
|
|
|
+ if (status === 'settled') return 'el-icon-circle-check'
|
|
|
+ if (status === 'pending') return 'el-icon-time'
|
|
|
+ if (status === 'failed') return 'el-icon-circle-close'
|
|
|
+ return 'el-icon-info'
|
|
|
+ },
|
|
|
+ statusIconColor(status) {
|
|
|
+ if (status === 'settled') return '#67c23a'
|
|
|
+ if (status === 'pending') return '#e6a23c'
|
|
|
+ if (status === 'failed') return '#f56c6c'
|
|
|
+ return '#909399'
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.admin-page {
|
|
|
+ padding: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.admin-page-header {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+}
|
|
|
+
|
|
|
+.admin-page-title {
|
|
|
+ font-size: 18px;
|
|
|
+ font-weight: bold;
|
|
|
+}
|
|
|
+
|
|
|
+.filter-bar {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+}
|
|
|
+
|
|
|
+.stat-card {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ padding: 10px 0;
|
|
|
+}
|
|
|
+
|
|
|
+.stat-icon-wrap {
|
|
|
+ width: 56px;
|
|
|
+ height: 56px;
|
|
|
+ border-radius: 14px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ margin-right: 16px;
|
|
|
+ flex-shrink: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.stat-icon-wrap i {
|
|
|
+ font-size: 28px;
|
|
|
+ color: #fff;
|
|
|
+}
|
|
|
+
|
|
|
+.stat-icon-wrap.total {
|
|
|
+ background: linear-gradient(135deg, #409EFF, #2d8cf0);
|
|
|
+}
|
|
|
+
|
|
|
+.stat-icon-wrap.pending {
|
|
|
+ background: linear-gradient(135deg, #e6a23c, #d4941c);
|
|
|
+}
|
|
|
+
|
|
|
+.stat-icon-wrap.settled {
|
|
|
+ background: linear-gradient(135deg, #67c23a, #4fa62c);
|
|
|
+}
|
|
|
+
|
|
|
+.stat-info {
|
|
|
+ flex: 1;
|
|
|
+}
|
|
|
+
|
|
|
+.stat-label {
|
|
|
+ font-size: 13px;
|
|
|
+ color: #909399;
|
|
|
+ margin-bottom: 6px;
|
|
|
+}
|
|
|
+
|
|
|
+.stat-value {
|
|
|
+ font-size: 24px;
|
|
|
+ font-weight: bold;
|
|
|
+ color: #303133;
|
|
|
+ margin-bottom: 4px;
|
|
|
+}
|
|
|
+
|
|
|
+.stat-value.pending {
|
|
|
+ color: #e6a23c;
|
|
|
+}
|
|
|
+
|
|
|
+.stat-value.settled {
|
|
|
+ color: #67c23a;
|
|
|
+}
|
|
|
+
|
|
|
+.stat-desc {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #909399;
|
|
|
+}
|
|
|
+
|
|
|
+.status-cell {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 6px;
|
|
|
+}
|
|
|
+
|
|
|
+.status-cell i {
|
|
|
+ font-size: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+.pagination-wrap {
|
|
|
+ margin-top: 20px;
|
|
|
+ text-align: right;
|
|
|
+}
|
|
|
+</style>
|