feat: add avatar payments and finance management
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
<template>
|
||||
<div class="page-container finance-page">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<div class="page-title">财务管理</div>
|
||||
<div class="subtitle">数字分身积分订单、退款与发票处理</div>
|
||||
</div>
|
||||
<el-button :loading="loading" @click="loadAll"><el-icon><Refresh /></el-icon>刷新</el-button>
|
||||
</div>
|
||||
|
||||
<div class="summary-grid">
|
||||
<div class="stat-card"><div class="stat-label">实收金额</div><div class="stat-value">¥{{ money(summary.paid_revenue) }}</div><small>{{ summary.paid_orders }} 笔已支付</small></div>
|
||||
<div class="stat-card"><div class="stat-label">待支付订单</div><div class="stat-value warning">{{ summary.pending_orders }}</div><small>可关闭或标记失败</small></div>
|
||||
<div class="stat-card"><div class="stat-label">处理中退款</div><div class="stat-value danger">{{ summary.processing_refunds }}</div><small>等待支付渠道确认</small></div>
|
||||
<div class="stat-card"><div class="stat-label">待开发票</div><div class="stat-value success">{{ summary.pending_invoices }}</div><small>等待财务处理</small></div>
|
||||
</div>
|
||||
|
||||
<el-tabs v-model="activeTab" class="finance-tabs" @tab-change="loadActive">
|
||||
<el-tab-pane label="支付订单" name="orders">
|
||||
<div class="filter-bar">
|
||||
<el-input v-model="filters.keyword" placeholder="订单号、昵称或手机号" clearable style="width:240px" @keyup.enter="loadOrders" />
|
||||
<el-select v-model="filters.status" placeholder="订单状态" clearable style="width:140px" @change="loadOrders">
|
||||
<el-option v-for="item in orderStatuses" :key="item.value" :label="item.label" :value="item.value" />
|
||||
</el-select>
|
||||
<el-select v-model="filters.provider" placeholder="支付渠道" clearable style="width:150px" @change="loadOrders">
|
||||
<el-option label="会会支付" value="huihui" /><el-option label="微信虚拟支付" value="wechat_virtual" />
|
||||
</el-select>
|
||||
<el-button type="primary" @click="loadOrders">查询</el-button>
|
||||
</div>
|
||||
<el-table :data="orders" v-loading="loading" class="data-table">
|
||||
<el-table-column label="订单 / 用户" min-width="230">
|
||||
<template #default="{ row }"><b>{{ row.order_no }}</b><div class="muted">{{ row.user_nickname || '会会用户' }} · {{ row.user_phone || '--' }}</div></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="套餐积分" width="130"><template #default="{ row }">{{ Number(row.points_amount || 0).toLocaleString() }}</template></el-table-column>
|
||||
<el-table-column label="金额" width="100"><template #default="{ row }">¥{{ money(row.price) }}</template></el-table-column>
|
||||
<el-table-column label="渠道" width="130"><template #default="{ row }">{{ providerLabel[row.provider] || row.provider }}</template></el-table-column>
|
||||
<el-table-column label="支付状态" width="110"><template #default="{ row }"><el-tag :type="tagType(row.status)">{{ statusLabel[row.status] || row.status }}</el-tag></template></el-table-column>
|
||||
<el-table-column label="退款" width="100"><template #default="{ row }"><el-tag v-if="row.refund_status && row.refund_status !== 'none'" :type="tagType(row.refund_status)">{{ statusLabel[row.refund_status] || row.refund_status }}</el-tag><span v-else>--</span></template></el-table-column>
|
||||
<el-table-column label="下单时间" min-width="165"><template #default="{ row }">{{ dateTime(row.created_at) }}</template></el-table-column>
|
||||
<el-table-column label="操作" width="165" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button v-if="row.status === 'paid' && ['none','',null].includes(row.refund_status)" link type="danger" @click="refund(row)">退款</el-button>
|
||||
<el-dropdown v-if="row.status === 'pending'" @command="command => closeOrder(row, command)">
|
||||
<el-button link type="warning">状态处理<el-icon><ArrowDown /></el-icon></el-button>
|
||||
<template #dropdown><el-dropdown-menu><el-dropdown-item command="closed">关闭订单</el-dropdown-item><el-dropdown-item command="failed">标记失败</el-dropdown-item></el-dropdown-menu></template>
|
||||
</el-dropdown>
|
||||
<span v-if="row.status !== 'pending' && !(row.status === 'paid' && ['none','',null].includes(row.refund_status))" class="muted">已处理</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination v-model:current-page="orderPage" :page-size="20" :total="orderTotal" layout="total, prev, pager, next" @change="loadOrders" />
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="退款管理" name="refunds">
|
||||
<el-table :data="refunds" v-loading="loading" class="data-table">
|
||||
<el-table-column label="退款单号" min-width="220" prop="refund_no" />
|
||||
<el-table-column label="原订单" min-width="210" prop="order_no" />
|
||||
<el-table-column label="用户" width="150"><template #default="{ row }">{{ row.user_nickname || row.user_phone || '--' }}</template></el-table-column>
|
||||
<el-table-column label="金额" width="100"><template #default="{ row }">¥{{ money(row.amount) }}</template></el-table-column>
|
||||
<el-table-column label="状态" width="110"><template #default="{ row }"><el-tag :type="tagType(row.status)">{{ statusLabel[row.status] || row.status }}</el-tag></template></el-table-column>
|
||||
<el-table-column label="原因" min-width="160" prop="reason" show-overflow-tooltip />
|
||||
<el-table-column label="创建时间" min-width="165"><template #default="{ row }">{{ dateTime(row.created_at) }}</template></el-table-column>
|
||||
<el-table-column label="对账确认" width="155" fixed="right">
|
||||
<template #default="{ row }"><template v-if="['pending','processing'].includes(row.status)"><el-button link type="success" @click="confirmRefund(row, 'succeeded')">已退款</el-button><el-button link type="danger" @click="confirmRefund(row, 'failed')">失败</el-button></template><span v-else class="muted">{{ row.failure_reason || '已完成' }}</span></template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="发票管理" name="invoices">
|
||||
<el-table :data="invoices" v-loading="loading" class="data-table">
|
||||
<el-table-column label="订单号" min-width="210" prop="order_no" />
|
||||
<el-table-column label="抬头" min-width="180" prop="title" />
|
||||
<el-table-column label="类型 / 税号" min-width="190"><template #default="{ row }">{{ row.invoice_type === 'company' ? '企业' : '个人' }}<div class="muted">{{ row.tax_number || '--' }}</div></template></el-table-column>
|
||||
<el-table-column label="金额" width="100"><template #default="{ row }">¥{{ money(row.amount) }}</template></el-table-column>
|
||||
<el-table-column label="邮箱" min-width="175" prop="email" />
|
||||
<el-table-column label="状态" width="100"><template #default="{ row }"><el-tag :type="tagType(row.status)">{{ invoiceStatusLabel[row.status] || row.status }}</el-tag></template></el-table-column>
|
||||
<el-table-column label="操作" width="145" fixed="right"><template #default="{ row }"><template v-if="row.status === 'pending'"><el-button link type="primary" @click="openInvoice(row)">开具</el-button><el-button link type="danger" @click="rejectInvoice(row)">驳回</el-button></template><a v-else-if="row.invoice_url" :href="row.invoice_url" target="_blank">查看发票</a><span v-else class="muted">{{ row.invoice_no || row.remark || '已处理' }}</span></template></el-table-column>
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
|
||||
<el-dialog v-model="invoiceDialog" title="登记已开发票" width="480px">
|
||||
<el-form label-width="92px">
|
||||
<el-form-item label="发票号码"><el-input v-model="invoiceForm.invoiceNo" maxlength="120" /></el-form-item>
|
||||
<el-form-item label="发票地址"><el-input v-model="invoiceForm.invoiceUrl" placeholder="电子发票下载地址(选填)" /></el-form-item>
|
||||
<el-form-item label="备注"><el-input v-model="invoiceForm.remark" type="textarea" /></el-form-item>
|
||||
</el-form>
|
||||
<template #footer><el-button @click="invoiceDialog=false">取消</el-button><el-button type="primary" :loading="saving" @click="issueInvoice">确认开具</el-button></template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { confirmFinanceRefund, getFinanceInvoices, getFinanceOrders, getFinanceRefunds, getFinanceSummary, requestFinanceRefund, updateFinanceInvoice, updateFinanceOrderStatus } from '@/api'
|
||||
|
||||
const activeTab = ref('orders')
|
||||
const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
const summary = ref({ paid_revenue: 0, paid_orders: 0, pending_orders: 0, processing_refunds: 0, pending_invoices: 0 })
|
||||
const orders = ref([]), refunds = ref([]), invoices = ref([])
|
||||
const orderPage = ref(1), orderTotal = ref(0)
|
||||
const filters = ref({ keyword: '', status: '', provider: '' })
|
||||
const invoiceDialog = ref(false), currentInvoice = ref(null)
|
||||
const invoiceForm = ref({ invoiceNo: '', invoiceUrl: '', remark: '' })
|
||||
const orderStatuses = [{ label: '待支付', value: 'pending' }, { label: '已支付', value: 'paid' }, { label: '失败', value: 'failed' }, { label: '已关闭', value: 'closed' }, { label: '已退款', value: 'refunded' }]
|
||||
const providerLabel = { huihui: '会会支付', wechat_virtual: '微信虚拟支付' }
|
||||
const statusLabel = { pending: '待处理', processing: '处理中', paid: '已支付', succeeded: '已成功', failed: '失败', closed: '已关闭', refunded: '已退款' }
|
||||
const invoiceStatusLabel = { pending: '待开具', issued: '已开具', rejected: '已驳回' }
|
||||
const money = value => Number(value || 0).toFixed(2)
|
||||
const dateTime = value => value ? new Date(value).toLocaleString('zh-CN', { hour12: false }) : '--'
|
||||
const tagType = value => ({ paid: 'success', succeeded: 'success', issued: 'success', pending: 'warning', processing: 'warning', failed: 'danger', rejected: 'danger', refunded: 'info', closed: 'info' }[value] || 'info')
|
||||
|
||||
async function loadSummary() { const res = await getFinanceSummary(); summary.value = res.data || summary.value }
|
||||
async function loadOrders() { const res = await getFinanceOrders({ page: orderPage.value, page_size: 20, ...filters.value }); orders.value = res.data?.items || []; orderTotal.value = res.data?.total || 0 }
|
||||
async function loadRefunds() { const res = await getFinanceRefunds({ page: 1, page_size: 100 }); refunds.value = res.data?.items || [] }
|
||||
async function loadInvoices() { const res = await getFinanceInvoices({ page: 1, page_size: 100 }); invoices.value = res.data?.items || [] }
|
||||
async function loadActive() { loading.value = true; try { if (activeTab.value === 'orders') await loadOrders(); if (activeTab.value === 'refunds') await loadRefunds(); if (activeTab.value === 'invoices') await loadInvoices() } finally { loading.value = false } }
|
||||
async function loadAll() { loading.value = true; try { await Promise.all([loadSummary(), loadOrders(), loadRefunds(), loadInvoices()]) } finally { loading.value = false } }
|
||||
|
||||
async function refund(row) {
|
||||
try { const { value } = await ElMessageBox.prompt('退款成功后会扣回本订单发放的全部积分。', `退款 ¥${money(row.price)}`, { inputPlaceholder: '请输入退款原因', inputValidator: value => value?.trim() ? true : '退款原因不能为空', confirmButtonText: '提交退款', cancelButtonText: '取消' }); await requestFinanceRefund(row.order_no, { reason: value, operator: '后台管理员' }); ElMessage.success('退款已提交渠道处理'); await loadAll() } catch (error) { if (error !== 'cancel' && error !== 'close') console.error(error) }
|
||||
}
|
||||
async function closeOrder(row, status) { try { await ElMessageBox.confirm(`确认将订单标记为“${status === 'closed' ? '已关闭' : '失败'}”?`, '订单状态处理', { type: 'warning' }); await updateFinanceOrderStatus(row.order_no, { status, reason: '后台管理员处理' }); ElMessage.success('订单状态已更新'); await loadAll() } catch {} }
|
||||
async function confirmRefund(row, status) { try { const message = status === 'succeeded' ? '仅在支付渠道后台已确认退款到账时操作。' : '确认供应商退款失败?'; await ElMessageBox.confirm(message, '退款对账确认', { type: 'warning' }); await confirmFinanceRefund(row.refund_no, { status, failureReason: status === 'failed' ? '供应商退款失败' : '' }); ElMessage.success('退款结果已登记'); await loadAll() } catch {} }
|
||||
function openInvoice(row) { currentInvoice.value = row; invoiceForm.value = { invoiceNo: '', invoiceUrl: '', remark: '' }; invoiceDialog.value = true }
|
||||
async function issueInvoice() { if (!invoiceForm.value.invoiceNo.trim()) return ElMessage.warning('请填写发票号码'); saving.value = true; try { await updateFinanceInvoice(currentInvoice.value.id, { status: 'issued', ...invoiceForm.value }); ElMessage.success('发票已登记'); invoiceDialog.value = false; await loadAll() } finally { saving.value = false } }
|
||||
async function rejectInvoice(row) { try { const { value } = await ElMessageBox.prompt('请输入驳回原因', '驳回发票申请', { inputValidator: value => value?.trim() ? true : '驳回原因不能为空' }); await updateFinanceInvoice(row.id, { status: 'rejected', remark: value }); ElMessage.success('发票申请已驳回'); await loadAll() } catch {} }
|
||||
onMounted(loadAll)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.finance-page { overflow-y: auto; }
|
||||
.subtitle, .muted, small { color: var(--color-text-muted); font-size: 12px; margin-top: 5px; }
|
||||
.summary-grid { display:grid; grid-template-columns:repeat(4,minmax(0,1fr)); gap:16px; margin-bottom:20px; }
|
||||
.stat-value { margin:8px 0 4px; }.stat-value.warning{color:var(--color-accent-orange)}.stat-value.danger{color:var(--color-accent-red)}.stat-value.success{color:var(--color-accent-green)}
|
||||
.finance-tabs { background:#fff; border:1px solid var(--color-border); border-radius:12px; padding:8px 16px 16px; box-shadow:var(--shadow-sm); }
|
||||
.filter-bar { display:flex; gap:10px; margin:8px 0 16px; }.el-pagination{justify-content:flex-end;margin-top:16px}
|
||||
a { color:var(--color-accent); text-decoration:none; }
|
||||
@media (max-width: 1100px) { .summary-grid{grid-template-columns:repeat(2,1fr)} }
|
||||
</style>
|
||||
Reference in New Issue
Block a user