interpreter-admin/pages/finance.vue
Mars Developer 51f8d95bf9 first commit
2025-06-26 11:24:11 +08:00

479 lines
17 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>