608 lines
12 KiB
Plaintext
608 lines
12 KiB
Plaintext
<!-- 财务管理页面 - 基于CRMEB设计 -->
|
|
<template>
|
|
<view class="finance-management">
|
|
<!-- 财务概览 -->
|
|
<view class="finance-overview">
|
|
<view class="overview-grid">
|
|
<view class="overview-card">
|
|
<text class="card-title">今日收入</text>
|
|
<text class="card-amount">¥{{ overview.today_income }}</text>
|
|
<text class="card-change positive">+{{ overview.income_growth }}%</text>
|
|
</view>
|
|
<view class="overview-card">
|
|
<text class="card-title">本月收入</text>
|
|
<text class="card-amount">¥{{ overview.month_income }}</text>
|
|
<text class="card-change positive">+{{ overview.month_growth }}%</text>
|
|
</view>
|
|
<view class="overview-card">
|
|
<text class="card-title">账户余额</text>
|
|
<text class="card-amount">¥{{ overview.account_balance }}</text>
|
|
<text class="card-change neutral">--</text>
|
|
</view>
|
|
<view class="overview-card">
|
|
<text class="card-title">待结算</text>
|
|
<text class="card-amount">¥{{ overview.pending_settlement }}</text>
|
|
<text class="card-change neutral">--</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- 财务明细 -->
|
|
<view class="finance-details">
|
|
<view class="section-header">
|
|
<text class="section-title">财务明细</text>
|
|
<view class="header-actions">
|
|
<button class="btn btn-primary" @click="exportRecords">导出记录</button>
|
|
</view>
|
|
</view>
|
|
|
|
<view class="filter-bar">
|
|
<view class="filter-item">
|
|
<text class="label">交易类型:</text>
|
|
<picker mode="selector" :range="transactionTypes" range-key="label" @change="onTypeChange">
|
|
<view class="picker-text">{{ selectedType || '全部' }}</view>
|
|
</picker>
|
|
</view>
|
|
<view class="filter-item">
|
|
<text class="label">时间范围:</text>
|
|
<view class="date-range">
|
|
<picker mode="date" @change="onStartDateChange">
|
|
<view class="date-picker">{{ startDate || '开始日期' }}</view>
|
|
</picker>
|
|
<text class="separator">至</text>
|
|
<picker mode="date" @change="onEndDateChange">
|
|
<view class="date-picker">{{ endDate || '结束日期' }}</view>
|
|
</picker>
|
|
</view>
|
|
</view>
|
|
<view class="filter-actions">
|
|
<button class="btn btn-default" @click="resetFilters">重置</button>
|
|
<button class="btn btn-primary" @click="applyFilters">筛选</button>
|
|
</view>
|
|
</view>
|
|
|
|
<view class="records-table">
|
|
<view class="table-header">
|
|
<view class="table-row">
|
|
<view class="table-cell">交易时间</view>
|
|
<view class="table-cell">交易类型</view>
|
|
<view class="table-cell">订单号</view>
|
|
<view class="table-cell">金额</view>
|
|
<view class="table-cell">余额</view>
|
|
<view class="table-cell">备注</view>
|
|
</view>
|
|
</view>
|
|
<view class="table-body">
|
|
<view v-for="record in records" :key="record.id" class="table-row data-row">
|
|
<view class="table-cell">{{ formatDateTime(record.created_at) }}</view>
|
|
<view class="table-cell">
|
|
<text class="type-tag" :class="'type-' + record.type">{{ getTypeText(record.type) }}</text>
|
|
</view>
|
|
<view class="table-cell">{{ record.order_sn || '--' }}</view>
|
|
<view class="table-cell">
|
|
<text :class="record.amount > 0 ? 'income' : 'expense'">
|
|
{{ record.amount > 0 ? '+' : '' }}¥{{ record.amount }}
|
|
</text>
|
|
</view>
|
|
<view class="table-cell">¥{{ record.balance_after }}</view>
|
|
<view class="table-cell">{{ record.remark || '--' }}</view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- 分页 -->
|
|
<view class="pagination">
|
|
<view class="page-info">共 {{ totalRecords }} 条记录,第 {{ currentPage }}/{{ totalPages }} 页</view>
|
|
<view class="page-buttons">
|
|
<button class="page-btn" :disabled="currentPage === 1" @click="goToPage(currentPage - 1)">上一页</button>
|
|
<view class="page-numbers">
|
|
<button
|
|
v-for="page in visiblePages"
|
|
:key="page"
|
|
class="page-number"
|
|
:class="{ active: page === currentPage }"
|
|
@click="goToPage(page)"
|
|
>{{ page }}</button>
|
|
</view>
|
|
<button class="page-btn" :disabled="currentPage === totalPages" @click="goToPage(currentPage + 1)">下一页</button>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</template>
|
|
|
|
<script setup lang="uts">
|
|
import { ref, onMounted, computed } from 'vue'
|
|
import supa from '@/components/supadb/aksupainstance.uts'
|
|
|
|
// 响应式数据
|
|
const overview = ref({
|
|
today_income: '0.00',
|
|
income_growth: '0.00',
|
|
month_income: '0.00',
|
|
month_growth: '0.00',
|
|
account_balance: '0.00',
|
|
pending_settlement: '0.00'
|
|
})
|
|
|
|
const records = ref([])
|
|
const currentPage = ref(1)
|
|
const pageSize = ref(20)
|
|
const totalRecords = ref(0)
|
|
const totalPages = ref(0)
|
|
|
|
const transactionTypes = ref([
|
|
{ value: '', label: '全部' },
|
|
{ value: '1', label: '订单收入' },
|
|
{ value: '2', label: '退款支出' },
|
|
{ value: '3', label: '提现支出' },
|
|
{ value: '4', label: '充值收入' },
|
|
{ value: '5', label: '系统调整' }
|
|
])
|
|
|
|
const selectedType = ref('')
|
|
const startDate = ref('')
|
|
const endDate = ref('')
|
|
|
|
// 计算属性
|
|
const visiblePages = computed(() => {
|
|
const pages = []
|
|
const start = Math.max(1, currentPage.value - 2)
|
|
const end = Math.min(totalPages.value, currentPage.value + 2)
|
|
|
|
for (let i = start; i <= end; i++) {
|
|
pages.push(i)
|
|
}
|
|
return pages
|
|
})
|
|
|
|
// 方法
|
|
const onTypeChange = (e: any) => {
|
|
selectedType.value = transactionTypes.value[e.detail.value].label
|
|
}
|
|
|
|
const onStartDateChange = (e: any) => {
|
|
startDate.value = e.detail.value
|
|
}
|
|
|
|
const onEndDateChange = (e: any) => {
|
|
endDate.value = e.detail.value
|
|
}
|
|
|
|
const applyFilters = () => {
|
|
loadRecords()
|
|
}
|
|
|
|
const resetFilters = () => {
|
|
selectedType.value = ''
|
|
startDate.value = ''
|
|
endDate.value = ''
|
|
loadRecords()
|
|
}
|
|
|
|
const exportRecords = () => {
|
|
uni.showToast({
|
|
title: '导出功能开发中',
|
|
icon: 'none'
|
|
})
|
|
}
|
|
|
|
const goToPage = (page: number) => {
|
|
if (page >= 1 && page <= totalPages.value) {
|
|
currentPage.value = page
|
|
loadRecords()
|
|
}
|
|
}
|
|
|
|
const getTypeText = (type: number) => {
|
|
const typeMap = {
|
|
1: '订单收入',
|
|
2: '退款支出',
|
|
3: '提现支出',
|
|
4: '充值收入',
|
|
5: '系统调整'
|
|
}
|
|
return typeMap[type] || '未知'
|
|
}
|
|
|
|
const formatDateTime = (dateStr: string) => {
|
|
if (!dateStr) return ''
|
|
const date = new Date(dateStr)
|
|
return date.toLocaleString()
|
|
}
|
|
|
|
// 数据加载方法
|
|
const loadOverview = async () => {
|
|
try {
|
|
const { data } = await supa.rpc('get_finance_overview')
|
|
if (data) {
|
|
overview.value = data
|
|
}
|
|
} catch (error) {
|
|
console.error('加载财务概览失败:', error)
|
|
}
|
|
}
|
|
|
|
const loadRecords = async () => {
|
|
try {
|
|
let query = supa.from('finance_records').select('*').order('created_at', { ascending: false })
|
|
|
|
// 筛选条件
|
|
if (selectedType.value && selectedType.value !== '全部') {
|
|
const typeObj = transactionTypes.value.find((t: any) => t.label === selectedType.value)
|
|
if (typeObj) {
|
|
query = query.eq('type', parseInt(typeObj.value))
|
|
}
|
|
}
|
|
|
|
if (startDate.value && endDate.value) {
|
|
query = query.gte('created_at', startDate.value + ' 00:00:00')
|
|
.lte('created_at', endDate.value + ' 23:59:59')
|
|
}
|
|
|
|
// 分页
|
|
const from = (currentPage.value - 1) * pageSize.value
|
|
const to = from + pageSize.value - 1
|
|
|
|
const { data, count } = await query.range(from, to)
|
|
records.value = data || []
|
|
totalRecords.value = count || 0
|
|
totalPages.value = Math.ceil(totalRecords.value / pageSize.value)
|
|
|
|
} catch (error) {
|
|
console.error('加载财务记录失败:', error)
|
|
uni.showToast({
|
|
title: '加载失败',
|
|
icon: 'error'
|
|
})
|
|
}
|
|
}
|
|
|
|
// 页面初始化
|
|
onMounted(async () => {
|
|
await Promise.all([
|
|
loadOverview(),
|
|
loadRecords()
|
|
])
|
|
})
|
|
</script>
|
|
|
|
<style lang="scss">
|
|
.finance-management {
|
|
padding: 30rpx;
|
|
background-color: #f5f5f5;
|
|
min-height: 100vh;
|
|
}
|
|
|
|
// 财务概览样式
|
|
.finance-overview {
|
|
background-color: #fff;
|
|
border-radius: 12rpx;
|
|
padding: 30rpx;
|
|
margin-bottom: 30rpx;
|
|
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
|
|
|
|
.overview-grid {
|
|
display: flex;
|
|
gap: 20rpx;
|
|
}
|
|
|
|
.overview-card {
|
|
flex: 1;
|
|
padding: 20rpx;
|
|
background-color: #f8f9fa;
|
|
border-radius: 8rpx;
|
|
text-align: center;
|
|
|
|
.card-title {
|
|
font-size: 24rpx;
|
|
color: #6c757d;
|
|
margin-bottom: 10rpx;
|
|
display: block;
|
|
}
|
|
|
|
.card-amount {
|
|
font-size: 32rpx;
|
|
font-weight: bold;
|
|
color: #28a745;
|
|
margin-bottom: 5rpx;
|
|
display: block;
|
|
}
|
|
|
|
.card-change {
|
|
font-size: 20rpx;
|
|
|
|
&.positive {
|
|
color: #28a745;
|
|
}
|
|
|
|
&.negative {
|
|
color: #dc3545;
|
|
}
|
|
|
|
&.neutral {
|
|
color: #6c757d;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 财务明细样式
|
|
.finance-details {
|
|
background-color: #fff;
|
|
border-radius: 12rpx;
|
|
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
|
|
|
|
.section-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 30rpx 30rpx 0;
|
|
|
|
.section-title {
|
|
font-size: 32rpx;
|
|
font-weight: bold;
|
|
color: #212529;
|
|
}
|
|
|
|
.btn {
|
|
padding: 12rpx 24rpx;
|
|
border-radius: 6rpx;
|
|
font-size: 26rpx;
|
|
border: none;
|
|
background-color: #007bff;
|
|
color: white;
|
|
cursor: pointer;
|
|
}
|
|
}
|
|
}
|
|
|
|
.filter-bar {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 20rpx;
|
|
padding: 30rpx;
|
|
border-bottom: 1rpx solid #e9ecef;
|
|
|
|
.filter-item {
|
|
display: flex;
|
|
align-items: center;
|
|
|
|
.label {
|
|
font-size: 26rpx;
|
|
color: #666;
|
|
margin-right: 10rpx;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.picker-text {
|
|
padding: 0 20rpx;
|
|
height: 60rpx;
|
|
line-height: 60rpx;
|
|
border: 1rpx solid #ddd;
|
|
border-radius: 6rpx;
|
|
font-size: 26rpx;
|
|
color: #333;
|
|
min-width: 150rpx;
|
|
}
|
|
|
|
.date-range {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10rpx;
|
|
|
|
.date-picker {
|
|
padding: 0 20rpx;
|
|
height: 60rpx;
|
|
line-height: 60rpx;
|
|
border: 1rpx solid #ddd;
|
|
border-radius: 6rpx;
|
|
font-size: 26rpx;
|
|
color: #333;
|
|
flex: 1;
|
|
}
|
|
|
|
.separator {
|
|
color: #666;
|
|
}
|
|
}
|
|
}
|
|
|
|
.filter-actions {
|
|
display: flex;
|
|
gap: 20rpx;
|
|
margin-left: auto;
|
|
|
|
.btn {
|
|
padding: 12rpx 24rpx;
|
|
border-radius: 6rpx;
|
|
font-size: 26rpx;
|
|
border: none;
|
|
cursor: pointer;
|
|
|
|
&.btn-primary {
|
|
background-color: #007bff;
|
|
color: white;
|
|
}
|
|
|
|
&.btn-default {
|
|
background-color: #f5f5f5;
|
|
color: #333;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
.records-table {
|
|
.table-header {
|
|
background-color: #f8f9fa;
|
|
border-bottom: 1rpx solid #e9ecef;
|
|
|
|
.table-row {
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 20rpx 30rpx;
|
|
font-weight: bold;
|
|
font-size: 26rpx;
|
|
color: #495057;
|
|
}
|
|
}
|
|
|
|
.table-body {
|
|
.data-row {
|
|
border-bottom: 1rpx solid #e9ecef;
|
|
|
|
&:last-child {
|
|
border-bottom: none;
|
|
}
|
|
}
|
|
}
|
|
|
|
.table-row {
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 20rpx 30rpx;
|
|
min-height: 80rpx;
|
|
|
|
.table-cell {
|
|
flex: 1;
|
|
font-size: 26rpx;
|
|
color: #495057;
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
}
|
|
}
|
|
|
|
.type-tag {
|
|
padding: 4rpx 12rpx;
|
|
border-radius: 12rpx;
|
|
font-size: 24rpx;
|
|
font-weight: bold;
|
|
|
|
&.type-1 {
|
|
background-color: #d4edda;
|
|
color: #155724;
|
|
}
|
|
|
|
&.type-2,
|
|
&.type-3 {
|
|
background-color: #f8d7da;
|
|
color: #721c24;
|
|
}
|
|
|
|
&.type-4 {
|
|
background-color: #cce5ff;
|
|
color: #0066cc;
|
|
}
|
|
|
|
&.type-5 {
|
|
background-color: #fff3cd;
|
|
color: #856404;
|
|
}
|
|
}
|
|
|
|
.income {
|
|
color: #28a745;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.expense {
|
|
color: #dc3545;
|
|
font-weight: bold;
|
|
}
|
|
|
|
// 分页样式
|
|
.pagination {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 30rpx;
|
|
border-top: 1rpx solid #e9ecef;
|
|
|
|
.page-info {
|
|
font-size: 26rpx;
|
|
color: #666;
|
|
}
|
|
|
|
.page-buttons {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10rpx;
|
|
}
|
|
|
|
.page-btn,
|
|
.page-number {
|
|
padding: 12rpx 20rpx;
|
|
border: 1rpx solid #ddd;
|
|
background-color: #fff;
|
|
color: #333;
|
|
border-radius: 6rpx;
|
|
font-size: 26rpx;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
|
|
&:disabled {
|
|
background-color: #f5f5f5;
|
|
color: #999;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
&:hover:not(:disabled) {
|
|
background-color: #007bff;
|
|
color: white;
|
|
border-color: #007bff;
|
|
}
|
|
|
|
&.active {
|
|
background-color: #007bff;
|
|
color: white;
|
|
border-color: #007bff;
|
|
}
|
|
}
|
|
}
|
|
|
|
// 响应式设计
|
|
@media (max-width: 750rpx) {
|
|
.overview-grid {
|
|
flex-direction: column;
|
|
gap: 15rpx;
|
|
}
|
|
|
|
.filter-bar {
|
|
flex-direction: column;
|
|
align-items: stretch;
|
|
}
|
|
|
|
.filter-item {
|
|
margin-bottom: 15rpx;
|
|
}
|
|
|
|
.filter-actions {
|
|
margin-left: 0;
|
|
margin-top: 15rpx;
|
|
justify-content: center;
|
|
}
|
|
|
|
.table-row {
|
|
flex-wrap: wrap;
|
|
padding: 15rpx;
|
|
|
|
.table-cell {
|
|
min-width: 150rpx;
|
|
margin-bottom: 10rpx;
|
|
}
|
|
}
|
|
|
|
.pagination {
|
|
flex-direction: column;
|
|
gap: 20rpx;
|
|
align-items: center;
|
|
|
|
.page-info {
|
|
order: 2;
|
|
}
|
|
}
|
|
}
|
|
</style>
|