Files
tyapi-frontend/src/pages/finance/Transactions.vue
2026-04-25 20:48:08 +08:00

649 lines
16 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>
<ListPageLayout title="消费记录" subtitle="查看您的钱包消费历史记录">
<template #filters>
<FilterSection>
<FilterItem label="交易ID">
<el-input
v-model="filters.transaction_id"
placeholder="输入交易ID"
clearable
@input="handleFilterChange"
class="w-full"
/>
</FilterItem>
<FilterItem label="产品名称">
<el-input
v-model="filters.product_name"
placeholder="输入产品名称"
clearable
@input="handleFilterChange"
class="w-full"
/>
</FilterItem>
<FilterItem label="金额范围">
<div class="flex gap-2">
<el-input
v-model="filters.min_amount"
placeholder="最小金额"
clearable
@input="handleFilterChange"
class="flex-1"
/>
<span class="text-gray-400 self-center">-</span>
<el-input
v-model="filters.max_amount"
placeholder="最大金额"
clearable
@input="handleFilterChange"
class="flex-1"
/>
</div>
</FilterItem>
<FilterItem label="时间范围" class="col-span-1">
<el-date-picker
v-model="dateRange"
type="datetimerange"
range-separator=""
start-placeholder="开始时间"
end-placeholder="结束时间"
format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DD HH:mm:ss"
@change="handleDateRangeChange"
class="w-full"
:size="isMobile ? 'small' : 'default'"
/>
</FilterItem>
<template #stats> 共找到 {{ total }} 条消费记录 </template>
<template #buttons>
<el-button @click="resetFilters">重置筛选</el-button>
<el-button type="primary" @click="loadTransactions">应用筛选</el-button>
</template>
</FilterSection>
</template>
<template #table>
<div v-if="loading" class="flex justify-center items-center py-12">
<el-loading size="large" />
</div>
<div v-else-if="transactions.length === 0" class="text-center py-12">
<el-empty description="暂无消费记录">
<el-button v-if="!isSubordinate" type="primary" @click="$router.push('/finance/wallet')">
前往钱包充值
</el-button>
</el-empty>
</div>
<div v-else class="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden">
<div class="table-container">
<el-table
:data="transactions"
style="width: 100%"
:header-cell-style="{
background: '#f8fafc',
color: '#475569',
fontWeight: '600',
fontSize: '14px',
}"
:cell-style="{
fontSize: '14px',
color: '#1e293b',
}"
>
<el-table-column prop="transaction_id" label="交易ID" min-width="180">
<template #default="{ row }">
<span class="font-mono text-sm text-gray-700">{{ row.transaction_id }}</span>
</template>
</el-table-column>
<el-table-column prop="product_name" label="产品名称" min-width="150">
<template #default="{ row }">
<span class="text-sm text-blue-600">{{ row.product_name || '-' }}</span>
</template>
</el-table-column>
<el-table-column prop="amount" label="消费金额" width="120">
<template #default="{ row }">
<span class="font-semibold text-red-600">¥{{ formatPrice(row.amount) }}</span>
</template>
</el-table-column>
<el-table-column prop="created_at" label="消费时间" width="160">
<template #default="{ row }">
<div class="text-sm">
<div class="text-gray-900">{{ formatDate(row.created_at) }}</div>
<div class="text-gray-500">{{ formatTime(row.created_at) }}</div>
</div>
</template>
</el-table-column>
</el-table>
</div>
</div>
</template>
<template #pagination>
<div class="pagination-wrapper">
<el-pagination
v-if="total > 0"
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</template>
<template #extra>
<!-- 交易详情弹窗 -->
<el-dialog v-model="detailDialogVisible" title="消费详情" width="600px" class="detail-dialog">
<div v-if="selectedTransaction" class="space-y-6">
<div class="grid grid-cols-2 gap-6">
<div class="detail-item">
<label class="detail-label">记录ID</label>
<span class="detail-value">{{ selectedTransaction.id }}</span>
</div>
<div class="detail-item">
<label class="detail-label">交易ID</label>
<span class="detail-value font-mono text-blue-600">{{
selectedTransaction.transaction_id
}}</span>
</div>
<div class="detail-item">
<label class="detail-label">产品名称</label>
<span class="detail-value">{{ selectedTransaction.product_name || '-' }}</span>
</div>
<div class="detail-item">
<label class="detail-label">用户ID</label>
<span class="detail-value">{{ selectedTransaction.user_id }}</span>
</div>
<div class="detail-item">
<label class="detail-label">消费金额</label>
<span class="detail-value">
<span class="text-red-600 font-semibold"
>¥{{ formatPrice(selectedTransaction.amount) }}</span
>
</span>
</div>
<div class="detail-item">
<label class="detail-label">创建时间</label>
<span class="detail-value">{{ formatDateTime(selectedTransaction.created_at) }}</span>
</div>
<div class="detail-item">
<label class="detail-label">更新时间</label>
<span class="detail-value">{{ formatDateTime(selectedTransaction.updated_at) }}</span>
</div>
</div>
<div class="transaction-info">
<h4 class="info-title">交易说明</h4>
<div class="info-content">
<p class="text-gray-700">
此交易记录记录了API调用产生的费用扣除每次API调用成功后系统会根据接口定价自动从您的钱包余额中扣除相应费用
</p>
</div>
</div>
</div>
</el-dialog>
</template>
</ListPageLayout>
</template>
<script setup>
import { financeApi } from '@/api'
import FilterItem from '@/components/common/FilterItem.vue'
import FilterSection from '@/components/common/FilterSection.vue'
import ListPageLayout from '@/components/common/ListPageLayout.vue'
import { useMobileTable } from '@/composables/useMobileTable'
import { ElMessage } from 'element-plus'
import { useUserStore } from '@/stores/user'
// 移动端检测
const { isMobile } = useMobileTable()
const userStore = useUserStore()
const isSubordinate = computed(() => userStore.accountKind === 'subordinate')
// 响应式数据
const loading = ref(false)
const transactions = ref([])
const total = ref(0)
const currentPage = ref(1)
const pageSize = ref(10)
const detailDialogVisible = ref(false)
const selectedTransaction = ref(null)
const dateRange = ref([])
// 统计数据
const stats = ref({
total_transactions: 0,
total_amount: 0,
})
// 筛选条件
const filters = reactive({
min_amount: '',
max_amount: '',
start_time: '',
end_time: '',
transaction_id: '',
product_name: '',
})
// 搜索防抖
let searchTimer = null
// 初始化
onMounted(() => {
loadTransactions()
loadStats()
})
// 加载钱包交易记录
const loadTransactions = async () => {
loading.value = true
try {
const params = {
page: currentPage.value,
page_size: pageSize.value,
...filters,
}
const response = await financeApi.getUserWalletTransactions(params)
transactions.value = response.data?.items || []
total.value = response.data?.total || 0
} catch (error) {
console.error('加载钱包交易记录失败:', error)
ElMessage.error('加载钱包交易记录失败')
} finally {
loading.value = false
}
}
// 加载统计数据
const loadStats = async () => {
try {
// 计算统计数据
const totalAmount = transactions.value.reduce((sum, transaction) => {
return sum + Number(transaction.amount || 0)
}, 0)
stats.value = {
total_transactions: total.value,
total_amount: totalAmount,
}
} catch (error) {
console.error('加载统计数据失败:', error)
}
}
// 格式化价格
const formatPrice = (price) => {
if (!price) return '0.00'
return Number(price).toFixed(2)
}
// 格式化日期
const formatDate = (date) => {
if (!date) return '-'
return new Date(date).toLocaleDateString('zh-CN')
}
// 格式化时间
const formatTime = (date) => {
if (!date) return '-'
return new Date(date).toLocaleTimeString('zh-CN', {
hour: '2-digit',
minute: '2-digit',
})
}
// 格式化日期时间
const formatDateTime = (date) => {
if (!date) return '-'
return new Date(date).toLocaleString('zh-CN')
}
// 处理筛选变化
const handleFilterChange = () => {
currentPage.value = 1
loadTransactions()
}
// 处理时间范围变化
const handleDateRangeChange = (range) => {
if (range && range.length === 2) {
filters.start_time = range[0]
filters.end_time = range[1]
} else {
filters.start_time = ''
filters.end_time = ''
}
currentPage.value = 1
loadTransactions()
}
// 重置筛选
const resetFilters = () => {
Object.keys(filters).forEach((key) => {
filters[key] = ''
})
dateRange.value = []
currentPage.value = 1
loadTransactions()
}
// 处理分页大小变化
const handleSizeChange = (size) => {
pageSize.value = size
currentPage.value = 1
loadTransactions()
}
// 处理当前页变化
const handleCurrentChange = (page) => {
currentPage.value = page
loadTransactions()
}
// 查看详情
const handleViewDetail = (transaction) => {
selectedTransaction.value = transaction
detailDialogVisible.value = true
}
</script>
<style scoped>
/* 统计项样式 */
.stat-item {
display: flex;
flex-direction: column;
align-items: center;
padding: 16px 24px;
background: rgba(255, 255, 255, 0.95);
border: 1px solid rgba(226, 232, 240, 0.6);
border-radius: 12px;
min-width: 120px;
}
.stat-value {
font-size: 24px;
font-weight: 700;
color: #1e293b;
line-height: 1;
margin-bottom: 4px;
}
.stat-label {
font-size: 13px;
color: #64748b;
font-weight: 500;
}
/* 详情弹窗样式 */
.detail-dialog :deep(.el-dialog) {
border-radius: 16px;
overflow: hidden;
}
.detail-dialog :deep(.el-dialog__header) {
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
border-bottom: 1px solid rgba(226, 232, 240, 0.6);
padding: 20px 24px;
}
.detail-dialog :deep(.el-dialog__title) {
font-size: 18px;
font-weight: 600;
color: #1e293b;
}
.detail-dialog :deep(.el-dialog__body) {
padding: 24px;
}
.detail-dialog :deep(.el-dialog__footer) {
background: rgba(248, 250, 252, 0.5);
border-top: 1px solid rgba(226, 232, 240, 0.4);
padding: 16px 24px;
}
/* 详情项样式 */
.detail-item {
display: flex;
flex-direction: column;
gap: 4px;
}
.detail-label {
font-size: 14px;
font-weight: 500;
color: #64748b;
}
.detail-value {
font-size: 16px;
color: #1e293b;
font-weight: 500;
}
/* 交易信息样式 */
.transaction-info {
background: rgba(248, 250, 252, 0.8);
border: 1px solid rgba(226, 232, 240, 0.6);
border-radius: 8px;
padding: 16px;
}
.info-title {
font-size: 16px;
font-weight: 600;
color: #1e293b;
margin-bottom: 12px;
}
.info-content {
background: rgba(255, 255, 255, 0.8);
border: 1px solid rgba(226, 232, 240, 0.4);
border-radius: 6px;
padding: 12px;
}
/* 表格样式优化 */
:deep(.el-table) {
border-radius: 8px;
overflow: hidden;
}
:deep(.el-table th) {
background: #f8fafc !important;
border-bottom: 1px solid #e2e8f0;
}
:deep(.el-table td) {
border-bottom: 1px solid #f1f5f9;
}
:deep(.el-table tr:hover > td) {
background: #f8fafc !important;
}
/* 表格容器 */
.table-container {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
/* 分页器包装器 */
.pagination-wrapper {
padding: 16px 0;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
/* 响应式设计 */
@media (max-width: 768px) {
.stat-item {
padding: 12px 16px;
min-width: 100px;
}
.stat-value {
font-size: 20px;
}
.stat-label {
font-size: 12px;
}
.detail-item {
margin-bottom: 16px;
}
.detail-label {
font-size: 13px;
}
.detail-value {
font-size: 14px;
}
/* 表格在移动端优化 */
.table-container {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
:deep(.el-table) {
font-size: 12px;
min-width: 800px;
}
:deep(.el-table th),
:deep(.el-table td) {
padding: 8px 4px;
}
:deep(.el-table .cell) {
padding: 0 4px;
word-break: break-word;
line-height: 1.4;
}
/* 分页组件在移动端优化 */
.pagination-wrapper {
padding: 12px 0;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
.pagination-wrapper :deep(.el-pagination) {
flex-wrap: nowrap;
min-width: fit-content;
justify-content: flex-start;
}
.pagination-wrapper :deep(.el-pagination__sizes) {
display: none;
}
.pagination-wrapper :deep(.el-pagination__total) {
font-size: 12px;
margin-right: 8px;
white-space: nowrap;
}
.pagination-wrapper :deep(.el-pagination__jump) {
display: none;
}
.pagination-wrapper :deep(.el-pager li) {
min-width: 32px;
height: 32px;
line-height: 32px;
margin: 0 2px;
font-size: 12px;
}
.pagination-wrapper :deep(.btn-prev),
.pagination-wrapper :deep(.btn-next) {
min-width: 32px;
height: 32px;
line-height: 32px;
font-size: 12px;
}
/* 对话框在移动端优化 */
.detail-dialog :deep(.el-dialog) {
margin: 20px;
width: calc(100% - 40px) !important;
max-width: none;
}
.detail-dialog :deep(.el-dialog__body) {
padding: 16px;
max-height: 80vh;
overflow-y: auto;
}
.detail-dialog :deep(.grid-cols-2) {
grid-template-columns: 1fr;
gap: 16px;
}
/* 金额范围输入框在移动端优化 */
.flex.gap-2 {
flex-wrap: wrap;
}
}
/* 超小屏幕进一步优化 */
@media (max-width: 480px) {
.pagination-wrapper {
padding: 8px 0;
}
.pagination-wrapper :deep(.el-pagination__total) {
display: none;
}
.pagination-wrapper :deep(.el-pagination) {
font-size: 11px;
}
.pagination-wrapper :deep(.el-pager li) {
min-width: 28px;
height: 28px;
line-height: 28px;
font-size: 11px;
}
.pagination-wrapper :deep(.btn-prev),
.pagination-wrapper :deep(.btn-next) {
min-width: 28px;
height: 28px;
line-height: 28px;
font-size: 11px;
}
.stat-item {
padding: 10px 12px;
min-width: 80px;
}
.stat-value {
font-size: 18px;
}
.stat-label {
font-size: 11px;
}
}
</style>