479 lines
17 KiB
Vue
479 lines
17 KiB
Vue
<template>
|
||
<div class="space-y-6">
|
||
<!-- 页面头部 -->
|
||
<div class="flex items-center justify-between">
|
||
<div>
|
||
<h1 class="text-2xl font-semibold text-gray-900">财务管理</h1>
|
||
<p class="mt-1 text-sm text-gray-500">管理平台财务数据和交易记录</p>
|
||
</div>
|
||
<div class="flex space-x-3">
|
||
<button
|
||
class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50"
|
||
>
|
||
导出报表
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 财务统计卡片 -->
|
||
<div class="grid grid-cols-1 md:grid-cols-4 gap-6">
|
||
<div class="bg-white overflow-hidden shadow rounded-lg">
|
||
<div class="p-5">
|
||
<div class="flex items-center">
|
||
<div class="flex-shrink-0">
|
||
<div class="w-8 h-8 bg-green-500 rounded-full flex items-center justify-center">
|
||
<svg class="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1"/>
|
||
</svg>
|
||
</div>
|
||
</div>
|
||
<div class="ml-5 w-0 flex-1">
|
||
<dl>
|
||
<dt class="text-sm font-medium text-gray-500 truncate">总收入</dt>
|
||
<dd class="text-lg font-medium text-gray-900">¥{{ stats.totalRevenue.toLocaleString() }}</dd>
|
||
</dl>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="bg-white overflow-hidden shadow rounded-lg">
|
||
<div class="p-5">
|
||
<div class="flex items-center">
|
||
<div class="flex-shrink-0">
|
||
<div class="w-8 h-8 bg-blue-500 rounded-full flex items-center justify-center">
|
||
<svg class="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"/>
|
||
</svg>
|
||
</div>
|
||
</div>
|
||
<div class="ml-5 w-0 flex-1">
|
||
<dl>
|
||
<dt class="text-sm font-medium text-gray-500 truncate">本月收入</dt>
|
||
<dd class="text-lg font-medium text-gray-900">¥{{ stats.monthlyRevenue.toLocaleString() }}</dd>
|
||
</dl>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="bg-white overflow-hidden shadow rounded-lg">
|
||
<div class="p-5">
|
||
<div class="flex items-center">
|
||
<div class="flex-shrink-0">
|
||
<div class="w-8 h-8 bg-yellow-500 rounded-full flex items-center justify-center">
|
||
<svg class="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||
</svg>
|
||
</div>
|
||
</div>
|
||
<div class="ml-5 w-0 flex-1">
|
||
<dl>
|
||
<dt class="text-sm font-medium text-gray-500 truncate">待结算</dt>
|
||
<dd class="text-lg font-medium text-gray-900">¥{{ stats.pendingAmount.toLocaleString() }}</dd>
|
||
</dl>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="bg-white overflow-hidden shadow rounded-lg">
|
||
<div class="p-5">
|
||
<div class="flex items-center">
|
||
<div class="flex-shrink-0">
|
||
<div class="w-8 h-8 bg-red-500 rounded-full flex items-center justify-center">
|
||
<svg class="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 14l-7 7m0 0l-7-7m7 7V3"/>
|
||
</svg>
|
||
</div>
|
||
</div>
|
||
<div class="ml-5 w-0 flex-1">
|
||
<dl>
|
||
<dt class="text-sm font-medium text-gray-500 truncate">总支出</dt>
|
||
<dd class="text-lg font-medium text-gray-900">¥{{ stats.totalExpenses.toLocaleString() }}</dd>
|
||
</dl>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 筛选和搜索 -->
|
||
<div class="bg-white shadow rounded-lg">
|
||
<div class="p-6">
|
||
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||
<div>
|
||
<label for="search" class="block text-sm font-medium text-gray-700 mb-1">搜索交易</label>
|
||
<input
|
||
id="search"
|
||
v-model="searchQuery"
|
||
type="text"
|
||
placeholder="订单号、用户名、备注"
|
||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label for="type-filter" class="block text-sm font-medium text-gray-700 mb-1">交易类型</label>
|
||
<select
|
||
id="type-filter"
|
||
v-model="typeFilter"
|
||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||
>
|
||
<option value="">全部类型</option>
|
||
<option value="income">收入</option>
|
||
<option value="expense">支出</option>
|
||
<option value="refund">退款</option>
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label for="status-filter" class="block text-sm font-medium text-gray-700 mb-1">交易状态</label>
|
||
<select
|
||
id="status-filter"
|
||
v-model="statusFilter"
|
||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||
>
|
||
<option value="">全部状态</option>
|
||
<option value="completed">已完成</option>
|
||
<option value="pending">待处理</option>
|
||
<option value="failed">失败</option>
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label for="date-range" class="block text-sm font-medium text-gray-700 mb-1">时间范围</label>
|
||
<input
|
||
id="date-range"
|
||
v-model="dateRange"
|
||
type="date"
|
||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 交易记录列表 -->
|
||
<div class="bg-white shadow rounded-lg overflow-hidden">
|
||
<div class="px-6 py-4 border-b border-gray-200">
|
||
<h3 class="text-lg font-medium text-gray-900">交易记录</h3>
|
||
</div>
|
||
<div class="overflow-x-auto">
|
||
<table class="min-w-full divide-y divide-gray-200">
|
||
<thead class="bg-gray-50">
|
||
<tr>
|
||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">交易ID</th>
|
||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">类型</th>
|
||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">用户</th>
|
||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">金额</th>
|
||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">状态</th>
|
||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">时间</th>
|
||
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">操作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody class="bg-white divide-y divide-gray-200">
|
||
<tr v-for="transaction in filteredTransactions" :key="transaction.id" class="hover:bg-gray-50">
|
||
<td class="px-6 py-4 whitespace-nowrap">
|
||
<div class="text-sm font-medium text-gray-900">{{ transaction.id }}</div>
|
||
</td>
|
||
<td class="px-6 py-4 whitespace-nowrap">
|
||
<span :class="getTypeClass(transaction.type)" class="inline-flex px-2 py-1 text-xs font-semibold rounded-full">
|
||
{{ getTypeName(transaction.type) }}
|
||
</span>
|
||
</td>
|
||
<td class="px-6 py-4 whitespace-nowrap">
|
||
<div>
|
||
<div class="text-sm font-medium text-gray-900">{{ transaction.userName }}</div>
|
||
<div class="text-sm text-gray-500">{{ transaction.userEmail }}</div>
|
||
</div>
|
||
</td>
|
||
<td class="px-6 py-4 whitespace-nowrap">
|
||
<div :class="transaction.type === 'expense' ? 'text-red-600' : 'text-green-600'" class="text-sm font-medium">
|
||
{{ transaction.type === 'expense' ? '-' : '+' }}¥{{ transaction.amount.toLocaleString() }}
|
||
</div>
|
||
</td>
|
||
<td class="px-6 py-4 whitespace-nowrap">
|
||
<span :class="getStatusClass(transaction.status)" class="inline-flex px-2 py-1 text-xs font-semibold rounded-full">
|
||
{{ getStatusName(transaction.status) }}
|
||
</span>
|
||
</td>
|
||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||
{{ formatDate(transaction.createdAt) }}
|
||
</td>
|
||
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||
<div class="flex justify-end space-x-2">
|
||
<button
|
||
@click="viewTransaction(transaction)"
|
||
class="text-blue-600 hover:text-blue-900"
|
||
>
|
||
查看
|
||
</button>
|
||
<button
|
||
v-if="transaction.status === 'pending'"
|
||
@click="processTransaction(transaction)"
|
||
class="text-green-600 hover:text-green-900"
|
||
>
|
||
处理
|
||
</button>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<!-- 空状态 -->
|
||
<div v-if="filteredTransactions.length === 0" class="text-center py-12">
|
||
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1"/>
|
||
</svg>
|
||
<h3 class="mt-2 text-sm font-medium text-gray-900">暂无交易记录</h3>
|
||
<p class="mt-1 text-sm text-gray-500">还没有任何财务交易记录</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
// 页面元数据
|
||
definePageMeta({
|
||
middleware: 'auth',
|
||
layout: 'default' // 明确指定使用默认布局
|
||
})
|
||
|
||
// 页面标题
|
||
useHead({
|
||
title: '财务管理 - 翻译管理系统'
|
||
})
|
||
|
||
// 导入Supabase数据操作
|
||
const { getPayments } = useSupabaseData()
|
||
|
||
// 路由
|
||
const router = useRouter()
|
||
|
||
// 搜索和筛选
|
||
const searchQuery = ref('')
|
||
const typeFilter = ref('')
|
||
const statusFilter = ref('')
|
||
const dateRange = ref('')
|
||
|
||
// 加载状态
|
||
const loading = ref(false)
|
||
const error = ref(null)
|
||
|
||
// 统计数据
|
||
const stats = ref({
|
||
totalRevenue: 0,
|
||
monthlyRevenue: 0,
|
||
pendingAmount: 0,
|
||
totalExpenses: 0
|
||
})
|
||
|
||
// 交易列表
|
||
const transactions = ref([])
|
||
const allTransactions = ref([])
|
||
|
||
// 计算属性:过滤后的交易
|
||
const filteredTransactions = computed(() => {
|
||
let filtered = transactions.value
|
||
|
||
// 搜索过滤
|
||
if (searchQuery.value) {
|
||
const query = searchQuery.value.toLowerCase()
|
||
filtered = filtered.filter(transaction =>
|
||
transaction.id.toLowerCase().includes(query) ||
|
||
transaction.userName.toLowerCase().includes(query) ||
|
||
transaction.userEmail.toLowerCase().includes(query) ||
|
||
transaction.description.toLowerCase().includes(query)
|
||
)
|
||
}
|
||
|
||
// 类型过滤
|
||
if (typeFilter.value) {
|
||
filtered = filtered.filter(transaction => transaction.type === typeFilter.value)
|
||
}
|
||
|
||
// 状态过滤
|
||
if (statusFilter.value) {
|
||
filtered = filtered.filter(transaction => transaction.status === statusFilter.value)
|
||
}
|
||
|
||
// 日期过滤
|
||
if (dateRange.value) {
|
||
const filterDate = new Date(dateRange.value)
|
||
filtered = filtered.filter(transaction => {
|
||
const transactionDate = new Date(transaction.createdAt)
|
||
return transactionDate.toDateString() === filterDate.toDateString()
|
||
})
|
||
}
|
||
|
||
return filtered
|
||
})
|
||
|
||
// 获取交易类型样式
|
||
const getTypeClass = (type) => {
|
||
const classes = {
|
||
income: 'bg-green-100 text-green-800',
|
||
expense: 'bg-red-100 text-red-800',
|
||
refund: 'bg-yellow-100 text-yellow-800'
|
||
}
|
||
return classes[type] || 'bg-gray-100 text-gray-800'
|
||
}
|
||
|
||
// 获取交易类型名称
|
||
const getTypeName = (type) => {
|
||
const names = {
|
||
income: '收入',
|
||
expense: '支出',
|
||
refund: '退款'
|
||
}
|
||
return names[type] || type
|
||
}
|
||
|
||
// 获取状态样式
|
||
const getStatusClass = (status) => {
|
||
switch (status) {
|
||
case 'completed':
|
||
return 'bg-green-100 text-green-800'
|
||
case 'pending':
|
||
return 'bg-yellow-100 text-yellow-800'
|
||
case 'failed':
|
||
return 'bg-red-100 text-red-800'
|
||
case 'refunded':
|
||
return 'bg-gray-100 text-gray-800'
|
||
default:
|
||
return 'bg-gray-100 text-gray-800'
|
||
}
|
||
}
|
||
|
||
// 获取状态名称
|
||
const getStatusName = (status) => {
|
||
const statusMap = {
|
||
completed: '已完成',
|
||
pending: '待处理',
|
||
failed: '失败',
|
||
refunded: '已退款'
|
||
}
|
||
return statusMap[status] || status
|
||
}
|
||
|
||
// 格式化日期
|
||
const formatDate = (dateString) => {
|
||
if (!dateString) return '未知时间'
|
||
const date = new Date(dateString)
|
||
return date.toLocaleDateString('zh-CN', {
|
||
year: 'numeric',
|
||
month: '2-digit',
|
||
day: '2-digit',
|
||
hour: '2-digit',
|
||
minute: '2-digit'
|
||
})
|
||
}
|
||
|
||
// 查看交易详情
|
||
const viewTransaction = (transaction) => {
|
||
alert(`交易ID: ${transaction.id}`)
|
||
}
|
||
|
||
// 处理交易
|
||
const processTransaction = async (transaction) => {
|
||
if (confirm(`确定要处理交易 ${transaction.id} 吗?`)) {
|
||
try {
|
||
// 注意:这里需要实现Supabase的更新操作
|
||
// 暂时模拟处理操作
|
||
transaction.status = 'completed'
|
||
updateStats()
|
||
alert('交易处理成功')
|
||
} catch (error) {
|
||
alert('处理失败,请重试')
|
||
}
|
||
}
|
||
}
|
||
|
||
// 更新统计数据
|
||
const updateStats = () => {
|
||
const now = new Date()
|
||
const currentMonth = now.getMonth()
|
||
const currentYear = now.getFullYear()
|
||
|
||
// 计算本月收入
|
||
const monthlyIncome = allTransactions.value.filter(t => {
|
||
const date = new Date(t.createdAt)
|
||
return date.getMonth() === currentMonth &&
|
||
date.getFullYear() === currentYear &&
|
||
t.status === 'completed' &&
|
||
t.amount > 0
|
||
})
|
||
|
||
// 计算待处理金额
|
||
const pending = allTransactions.value.filter(t => t.status === 'pending')
|
||
|
||
// 计算总费用(退款和其他支出)
|
||
const expenses = allTransactions.value.filter(t =>
|
||
t.status === 'completed' && (t.currency === 'refund' || t.amount < 0)
|
||
)
|
||
|
||
stats.value = {
|
||
totalRevenue: allTransactions.value.filter(t => t.status === 'completed' && t.amount > 0)
|
||
.reduce((sum, t) => sum + t.amount, 0),
|
||
monthlyRevenue: monthlyIncome.reduce((sum, t) => sum + t.amount, 0),
|
||
pendingAmount: pending.filter(t => t.amount > 0).reduce((sum, t) => sum + t.amount, 0),
|
||
totalExpenses: Math.abs(expenses.reduce((sum, t) => sum + Math.abs(t.amount), 0))
|
||
}
|
||
}
|
||
|
||
// 加载交易数据
|
||
const loadTransactions = async () => {
|
||
loading.value = true
|
||
error.value = null
|
||
|
||
try {
|
||
// 从Supabase获取支付记录
|
||
const paymentsData = await getPayments()
|
||
|
||
// 转换数据格式以匹配财务显示
|
||
allTransactions.value = paymentsData.map(payment => ({
|
||
id: payment.id || `payment_${Date.now()}`,
|
||
type: payment.amount > 0 ? 'income' : 'expense',
|
||
amount: Math.abs(payment.amount),
|
||
userName: payment.profiles?.full_name || '未知用户',
|
||
userEmail: payment.profiles?.email || '未提供',
|
||
status: payment.status || 'pending',
|
||
description: getPaymentDescription(payment),
|
||
createdAt: payment.created_at,
|
||
currency: payment.currency || 'CNY',
|
||
stripePaymentId: payment.stripe_payment_id
|
||
}))
|
||
|
||
transactions.value = [...allTransactions.value]
|
||
updateStats()
|
||
|
||
console.log('财务数据加载成功:', allTransactions.value.length, '笔交易')
|
||
|
||
} catch (err) {
|
||
console.error('加载财务数据失败:', err)
|
||
error.value = '加载数据失败,请刷新页面重试'
|
||
allTransactions.value = []
|
||
transactions.value = []
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
// 根据支付信息生成描述
|
||
const getPaymentDescription = (payment) => {
|
||
if (payment.stripe_payment_id) {
|
||
return `在线支付 - ${payment.currency || 'CNY'}`
|
||
}
|
||
if (payment.status === 'refunded') {
|
||
return '订单退款'
|
||
}
|
||
return `支付交易 - ${payment.amount > 0 ? '收入' : '支出'}`
|
||
}
|
||
|
||
// 页面挂载时加载数据
|
||
onMounted(() => {
|
||
loadTransactions()
|
||
})
|
||
</script>
|
||
|