rentease-app/pages/stats/stats.vue

1198 lines
31 KiB
Vue
Raw Normal View History

2026-04-20 06:23:11 +00:00
<template>
<view class="stats-page">
<!-- 自定义导航栏 -->
<view class="custom-nav safe-area-top">
<view class="nav-content">
<text class="nav-title">收支统计</text>
<view class="nav-actions">
<view class="nav-btn" @click="showDatePicker">
<text class="date-text">{{currentPeriod}}</text>
<uni-icons type="down" size="14" color="#64748B"></uni-icons>
</view>
</view>
</view>
</view>
<scroll-view scroll-y class="page-content" @scrolltolower="loadMore">
<!-- 统计概览卡片 - 对应Web端统计概览 -->
<view class="overview-section">
<view class="overview-row">
<view class="overview-card income">
<view class="card-icon">
<uni-icons type="wallet-filled" size="28" color="#FFFFFF"></uni-icons>
</view>
<view class="card-content">
<text class="card-value">¥{{formatAmount(statistics.totalReceivable)}}</text>
<text class="card-label">应收总额</text>
</view>
</view>
<view class="overview-card received">
<view class="card-icon">
<uni-icons type="checkmarkempty" size="28" color="#FFFFFF"></uni-icons>
</view>
<view class="card-content">
<text class="card-value">¥{{formatAmount(statistics.totalReceived)}}</text>
<text class="card-label">实收总额</text>
</view>
</view>
</view>
<view class="overview-row">
<view class="overview-card pending">
<view class="card-icon">
2026-04-22 06:47:04 +00:00
<uni-icons type="refresh-filled" size="28" color="#FFFFFF"></uni-icons>
2026-04-20 06:23:11 +00:00
</view>
<view class="card-content">
<text class="card-value">¥{{formatAmount(statistics.totalUnreceived)}}</text>
<text class="card-label">待收金额</text>
</view>
</view>
<view class="overview-card expense">
<view class="card-icon">
<uni-icons type="cart-filled" size="28" color="#FFFFFF"></uni-icons>
</view>
<view class="card-content">
<text class="card-value">¥{{formatAmount(statistics.totalExpense)}}</text>
<text class="card-label">总支出</text>
</view>
</view>
</view>
</view>
<!-- 收支趋势图 - 对应Web端收支趋势 -->
<view class="chart-section">
<view class="section-header">
<text class="section-title">收支趋势</text>
</view>
<view class="chart-card">
<view class="trend-chart">
<view
v-for="(item, index) in trendData"
:key="index"
class="trend-bar-group"
>
<view class="trend-bars">
<view class="bar-wrapper">
<view
class="trend-bar income"
:style="{ height: item.incomeHeight + 'rpx' }"
></view>
</view>
<view class="bar-wrapper">
<view
class="trend-bar expense"
:style="{ height: item.expenseHeight + 'rpx' }"
></view>
</view>
</view>
<text class="trend-label">{{item.label}}</text>
</view>
</view>
<view class="chart-legend">
<view class="legend-item">
<view class="legend-dot income"></view>
<text>收入</text>
</view>
<view class="legend-item">
<view class="legend-dot expense"></view>
<text>支出</text>
</view>
</view>
</view>
</view>
<!-- 收入构成 - 对应Web端收入构成饼图 -->
<view class="composition-section">
<view class="section-header">
<text class="section-title">收入构成</text>
</view>
<view class="composition-card">
<view class="composition-list">
<view
v-for="(item, index) in incomeByCategory"
:key="index"
class="composition-item"
>
<view class="item-left">
<view class="item-icon" :style="{ background: getCategoryColor(item.category) }">
<text class="icon-text">{{getCategoryIcon(item.category)}}</text>
</view>
<view class="item-info">
<text class="item-name">{{getCategoryText(item.category)}}</text>
<text class="item-percent">{{item.percent}}%</text>
</view>
</view>
<view class="item-right">
<text class="item-amount">¥{{formatAmount(item.amount)}}</text>
<view class="progress-bar">
<view
class="progress-fill"
:style="{ width: item.percent + '%', background: getCategoryColor(item.category) }"
></view>
</view>
</view>
</view>
</view>
<view v-if="incomeByCategory.length === 0" class="empty-tip">
<text>暂无收入数据</text>
</view>
</view>
</view>
<!-- 支出构成 - 对应Web端支出构成饼图 -->
<view class="composition-section">
<view class="section-header">
<text class="section-title">支出构成</text>
</view>
<view class="composition-card">
<view class="composition-list">
<view
v-for="(item, index) in expenseByCategory"
:key="index"
class="composition-item"
>
<view class="item-left">
<view class="item-icon" :style="{ background: getCategoryColor(item.category) }">
<text class="icon-text">{{getCategoryIcon(item.category)}}</text>
</view>
<view class="item-info">
<text class="item-name">{{getCategoryText(item.category)}}</text>
<text class="item-percent">{{item.percent}}%</text>
</view>
</view>
<view class="item-right">
<text class="item-amount expense">-¥{{formatAmount(item.amount)}}</text>
<view class="progress-bar">
<view
class="progress-fill"
:style="{ width: item.percent + '%', background: getCategoryColor(item.category) }"
></view>
</view>
</view>
</view>
</view>
<view v-if="expenseByCategory.length === 0" class="empty-tip">
<text>暂无支出数据</text>
</view>
</view>
</view>
<!-- 收支明细 - 对应Web端详细数据表格 -->
<view class="detail-section">
<view class="section-header">
<text class="section-title">收支明细</text>
</view>
<view class="detail-tabs">
<view
v-for="(tab, index) in detailTabs"
:key="index"
class="detail-tab"
:class="{ active: activeTab === tab.value }"
@click="switchTab(tab.value)"
>
<text>{{tab.label}}</text>
</view>
</view>
<view class="detail-list">
<view
v-for="(item, index) in currentDetailList"
:key="index"
class="detail-item"
@click="viewDetail(item)"
>
<view class="detail-left">
<view class="detail-icon" :class="item.type">
<uni-icons
:type="item.type === 'income' ? 'wallet-filled' : 'cart-filled'"
size="20"
color="#FFFFFF"
></uni-icons>
</view>
<view class="detail-info">
<text class="detail-title">{{getCategoryText(item.category)}}</text>
<text class="detail-subtitle">{{item.roomNumber}} · {{item.renterName || '-'}}</text>
<text class="detail-date">{{item.billDate}}</text>
</view>
</view>
<view class="detail-right">
<text class="detail-amount" :class="item.type">
{{item.type === 'income' ? '+' : '-'}}¥{{formatAmount(item.receivableAmount)}}
</text>
<view class="detail-status" :class="item.status">
<text>{{getStatusText(item.status)}}</text>
</view>
</view>
</view>
<view v-if="currentDetailList.length === 0" class="empty-tip">
<text>暂无明细数据</text>
</view>
</view>
</view>
<view class="safe-area-bottom" style="height: 40rpx;"></view>
</scroll-view>
2026-04-22 06:47:04 +00:00
<!-- 自定义日期选择弹窗 -->
<view class="custom-date-mask" v-if="showCustomDatePicker" @click="cancelCustomDate">
<view class="date-picker-popup" @click.stop>
<view class="popup-header">
<text class="popup-title">选择日期范围</text>
<view class="popup-close" @click="cancelCustomDate">
<uni-icons type="close" size="20" color="#64748B"></uni-icons>
</view>
</view>
<view class="popup-content">
<uni-datetime-picker
v-model="tempDateRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
:border="false"
:clear-icon="false"
/>
</view>
<view class="popup-footer">
<view class="btn btn-cancel" @click="cancelCustomDate">取消</view>
<view class="btn btn-confirm" @click="confirmCustomDate">确定</view>
</view>
</view>
</view>
2026-04-20 06:23:11 +00:00
</view>
</template>
<script>
import { billApi, settingApi } from '../../api/index.js'
export default {
data() {
return {
currentPeriod: '本月',
dateRange: [],
statistics: {
totalReceivable: 0,
totalReceived: 0,
totalUnreceived: 0,
totalExpense: 0
},
trendData: [],
incomeByCategory: [],
expenseByCategory: [],
incomeList: [],
expenseList: [],
activeTab: 'income',
detailTabs: [
{ label: '收入明细', value: 'income' },
{ label: '支出明细', value: 'expense' }
],
categoryMap: {},
isLoading: false,
2026-04-22 06:47:04 +00:00
// 自定义日期选择
showCustomDatePicker: false,
tempDateRange: []
2026-04-20 06:23:11 +00:00
}
},
computed: {
currentDetailList() {
return this.activeTab === 'income' ? this.incomeList : this.expenseList
}
},
onLoad() {
this.initDateRange()
this.loadCategories()
this.loadData()
},
onPullDownRefresh() {
this.loadData().finally(() => {
uni.stopPullDownRefresh()
})
},
methods: {
// 初始化日期范围为当月
initDateRange() {
const now = new Date()
const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1)
const endOfMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0)
this.dateRange = [
this.formatDate(startOfMonth),
this.formatDate(endOfMonth)
]
},
// 加载类目
async loadCategories() {
try {
const response = await settingApi.getCategories()
const categories = response.data || []
const map = {}
categories.forEach(cat => {
map[cat.code] = cat.name
})
this.categoryMap = map
} catch (error) {
console.error('加载类目列表失败:', error)
}
},
// 加载数据
async loadData() {
if (this.isLoading) return
this.isLoading = true
try {
2026-04-22 06:47:04 +00:00
console.log('开始加载数据,日期范围:', this.dateRange)
2026-04-20 06:23:11 +00:00
const billRes = await billApi.list({
startDate: this.dateRange[0],
2026-04-22 06:47:04 +00:00
endDate: this.dateRange[1]
2026-04-20 06:23:11 +00:00
})
2026-04-22 06:47:04 +00:00
console.log('API返回结果:', billRes)
if (billRes.code === 200) {
this.processData(billRes.data || [])
} else {
uni.showToast({
title: billRes.message || '加载数据失败',
icon: 'none'
})
}
2026-04-20 06:23:11 +00:00
} catch (error) {
2026-04-22 06:47:04 +00:00
console.error('加载数据失败:', error)
2026-04-20 06:23:11 +00:00
uni.showToast({
title: '加载数据失败',
icon: 'none'
})
} finally {
this.isLoading = false
}
},
// 处理数据
processData(bills) {
let totalReceivable = 0
let totalReceived = 0
let totalExpense = 0
const incomeList = []
const expenseList = []
const incomeByCategory = {}
const expenseByCategory = {}
const trendData = {}
bills.forEach(bill => {
const receivable = parseFloat(bill.receivableAmount) || 0
const received = parseFloat(bill.receivedAmount) || 0
const billDate = bill.billDate || (bill.createTime && bill.createTime.split(' ')[0])
if (bill.type === 'income') {
totalReceivable += receivable
totalReceived += received
// 按类别统计收入
const category = bill.category || 'other'
if (!incomeByCategory[category]) {
incomeByCategory[category] = 0
}
incomeByCategory[category] += receivable
// 按日期统计趋势
if (billDate) {
if (!trendData[billDate]) {
trendData[billDate] = { income: 0, expense: 0 }
}
trendData[billDate].income += receivable
}
incomeList.push({
...bill,
type: 'income',
roomNumber: bill.roomNumber || (bill.Room && bill.Room.roomNumber) || '-',
renterName: bill.renterName || (bill.Renter && bill.Renter.name) || '-'
})
} else if (bill.type === 'expense') {
totalExpense += receivable
// 按类别统计支出
const category = bill.category || 'other'
if (!expenseByCategory[category]) {
expenseByCategory[category] = 0
}
expenseByCategory[category] += receivable
// 按日期统计趋势
if (billDate) {
if (!trendData[billDate]) {
trendData[billDate] = { income: 0, expense: 0 }
}
trendData[billDate].expense += receivable
}
expenseList.push({
...bill,
type: 'expense',
roomNumber: bill.roomNumber || (bill.Room && bill.Room.roomNumber) || '-'
})
}
})
// 按日期排序
incomeList.sort((a, b) => new Date(b.billDate) - new Date(a.billDate))
expenseList.sort((a, b) => new Date(b.billDate) - new Date(a.billDate))
this.statistics = {
totalReceivable,
totalReceived,
totalUnreceived: totalReceivable - totalReceived,
totalExpense
}
// 处理收入分类数据
const incomeTotal = Object.values(incomeByCategory).reduce((sum, val) => sum + val, 0)
this.incomeByCategory = Object.keys(incomeByCategory).map(category => ({
category,
amount: incomeByCategory[category],
percent: incomeTotal > 0 ? ((incomeByCategory[category] / incomeTotal) * 100).toFixed(1) : 0
})).sort((a, b) => b.amount - a.amount)
// 处理支出分类数据
const expenseTotal = Object.values(expenseByCategory).reduce((sum, val) => sum + val, 0)
this.expenseByCategory = Object.keys(expenseByCategory).map(category => ({
category,
amount: expenseByCategory[category],
percent: expenseTotal > 0 ? ((expenseByCategory[category] / expenseTotal) * 100).toFixed(1) : 0
})).sort((a, b) => b.amount - a.amount)
// 处理趋势数据
this.processTrendData(trendData)
2026-04-22 06:47:04 +00:00
this.incomeList = incomeList
this.expenseList = expenseList
2026-04-20 06:23:11 +00:00
},
// 处理趋势数据
processTrendData(trendData) {
const dates = Object.keys(trendData).sort()
const maxIncome = Math.max(...dates.map(d => trendData[d].income), 1)
const maxExpense = Math.max(...dates.map(d => trendData[d].expense), 1)
const maxValue = Math.max(maxIncome, maxExpense)
// 只取最近7天的数据
const recentDates = dates.slice(-7)
this.trendData = recentDates.map(date => {
const data = trendData[date]
return {
label: date.slice(5), // 显示 MM-DD
incomeHeight: maxValue > 0 ? (data.income / maxValue) * 200 : 0,
expenseHeight: maxValue > 0 ? (data.expense / maxValue) * 200 : 0
}
})
},
// 显示日期选择
showDatePicker() {
uni.showActionSheet({
itemList: ['本周', '本月', '本季度', '本年', '自定义'],
success: (res) => {
2026-04-22 06:47:04 +00:00
if (res.tapIndex === 4) {
// 自定义日期
this.showCustomDatePicker = true
this.tempDateRange = this.dateRange
} else {
const periods = ['本周', '本月', '本季度', '本年', '自定义']
this.currentPeriod = periods[res.tapIndex]
this.updateDateRange(res.tapIndex)
}
2026-04-20 06:23:11 +00:00
}
})
},
// 更新日期范围
updateDateRange(index) {
const now = new Date()
let start, end
switch(index) {
case 0: // 本周
const dayOfWeek = now.getDay() || 7
start = new Date(now.getFullYear(), now.getMonth(), now.getDate() - dayOfWeek + 1)
end = new Date(now.getFullYear(), now.getMonth(), now.getDate())
break
case 1: // 本月
start = new Date(now.getFullYear(), now.getMonth(), 1)
end = new Date(now.getFullYear(), now.getMonth() + 1, 0)
break
case 2: // 本季度
const quarter = Math.floor(now.getMonth() / 3)
start = new Date(now.getFullYear(), quarter * 3, 1)
end = new Date(now.getFullYear(), quarter * 3 + 3, 0)
break
case 3: // 本年
start = new Date(now.getFullYear(), 0, 1)
end = new Date(now.getFullYear(), 11, 31)
break
default:
return
}
this.dateRange = [this.formatDate(start), this.formatDate(end)]
this.loadData()
},
2026-04-22 06:47:04 +00:00
// 确认自定义日期
confirmCustomDate() {
if (this.tempDateRange && this.tempDateRange.length === 2) {
this.dateRange = this.tempDateRange
this.currentPeriod = `${this.tempDateRange[0]}${this.tempDateRange[1]}`
this.showCustomDatePicker = false
this.loadData()
} else {
uni.showToast({
title: '请选择开始和结束日期',
icon: 'none'
})
}
},
// 取消自定义日期选择
cancelCustomDate() {
this.showCustomDatePicker = false
this.tempDateRange = []
},
2026-04-20 06:23:11 +00:00
// 切换明细标签
switchTab(value) {
this.activeTab = value
},
2026-04-22 06:47:04 +00:00
// 加载更多(已移除分页,此方法保留但为空)
2026-04-20 06:23:11 +00:00
loadMore() {
2026-04-22 06:47:04 +00:00
// 不再使用分页加载
2026-04-20 06:23:11 +00:00
},
// 查看详情
viewDetail(item) {
uni.navigateTo({
url: `/pages/bill-detail/bill-detail?id=${item.id}`
})
},
// 格式化日期
formatDate(date) {
if (!date) return '-'
const d = new Date(date)
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
},
// 格式化金额
formatAmount(amount) {
if (amount === null || amount === undefined) return '0.00'
return parseFloat(amount).toFixed(2)
},
// 获取类目文本
getCategoryText(category) {
const texts = {
'rent': '租金',
'deposit': '押金',
'water': '水费',
'electricity': '电费',
'maintenance': '维修费',
'property': '物业费',
'other': '其他'
}
return this.categoryMap[category] || texts[category] || category || '其他'
},
// 获取类目图标
getCategoryIcon(category) {
const icons = {
'rent': '租',
'deposit': '押',
'water': '水',
'electricity': '电',
'maintenance': '修',
'property': '物',
'other': '其'
}
return icons[category] || '账'
},
// 获取类目颜色
getCategoryColor(category) {
const colors = {
'rent': '#667eea',
'deposit': '#f093fb',
'water': '#4facfe',
'electricity': '#f5576c',
'maintenance': '#fa709a',
'property': '#30cfd0',
'other': '#a8edea'
}
return colors[category] || '#667eea'
},
// 获取状态文本
getStatusText(status) {
const texts = {
'paid': '已付清',
'partial': '部分付款',
'unpaid': '未付款'
}
return texts[status] || status
}
}
}
</script>
<style scoped>
.stats-page {
min-height: 100vh;
background: #F8FAFC;
display: flex;
flex-direction: column;
width: 100%;
max-width: 100vw;
overflow-x: hidden;
}
/* 自定义导航栏 */
.custom-nav {
background: #FFFFFF;
border-bottom: 2rpx solid #F1F5F9;
width: 100%;
}
.nav-content {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20rpx 32rpx;
width: 100%;
box-sizing: border-box;
}
.nav-title {
font-size: 36rpx;
font-weight: 700;
color: #1E293B;
}
.nav-actions {
display: flex;
align-items: center;
}
.nav-btn {
display: flex;
align-items: center;
gap: 8rpx;
padding: 12rpx 24rpx;
background: #F8FAFC;
border-radius: 12rpx;
}
.date-text {
font-size: 26rpx;
color: #64748B;
}
/* 页面内容 */
.page-content {
flex: 1;
padding: 24rpx 32rpx;
width: 100%;
box-sizing: border-box;
}
/* 统计概览 */
.overview-section {
margin-bottom: 32rpx;
}
.overview-row {
display: flex;
gap: 24rpx;
margin-bottom: 24rpx;
}
.overview-row:last-child {
margin-bottom: 0;
}
.overview-card {
flex: 1;
display: flex;
align-items: center;
padding: 28rpx;
border-radius: 20rpx;
color: #FFFFFF;
}
.overview-card.income {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.overview-card.received {
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
}
.overview-card.pending {
background: linear-gradient(135deg, #fa709a 0%, #fee140 100%);
}
.overview-card.expense {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
}
.card-icon {
width: 64rpx;
height: 64rpx;
border-radius: 16rpx;
background: rgba(255, 255, 255, 0.2);
display: flex;
align-items: center;
justify-content: center;
margin-right: 20rpx;
}
.card-content {
flex: 1;
}
.card-value {
display: block;
font-size: 32rpx;
font-weight: 700;
margin-bottom: 8rpx;
}
.card-label {
font-size: 24rpx;
opacity: 0.9;
}
/* 区域标题 */
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24rpx;
}
.section-title {
font-size: 32rpx;
font-weight: 700;
color: #1E293B;
}
/* 图表区域 */
.chart-section {
margin-bottom: 32rpx;
}
.chart-card {
background: #FFFFFF;
border-radius: 24rpx;
padding: 32rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.04);
}
.trend-chart {
display: flex;
justify-content: space-around;
align-items: flex-end;
height: 280rpx;
padding-bottom: 40rpx;
border-bottom: 2rpx solid #F1F5F9;
margin-bottom: 24rpx;
}
.trend-bar-group {
display: flex;
flex-direction: column;
align-items: center;
gap: 16rpx;
}
.trend-bars {
display: flex;
gap: 8rpx;
align-items: flex-end;
height: 220rpx;
}
.bar-wrapper {
display: flex;
align-items: flex-end;
height: 220rpx;
}
.trend-bar {
width: 28rpx;
border-radius: 6rpx;
transition: height 0.3s ease;
}
.trend-bar.income {
background: linear-gradient(180deg, #667eea 0%, #764ba2 100%);
}
.trend-bar.expense {
background: linear-gradient(180deg, #f093fb 0%, #f5576c 100%);
}
.trend-label {
font-size: 22rpx;
color: #64748B;
}
.chart-legend {
display: flex;
justify-content: center;
gap: 48rpx;
}
.legend-item {
display: flex;
align-items: center;
gap: 12rpx;
font-size: 26rpx;
color: #64748B;
}
.legend-dot {
width: 16rpx;
height: 16rpx;
border-radius: 4rpx;
}
.legend-dot.income {
background: linear-gradient(180deg, #667eea 0%, #764ba2 100%);
}
.legend-dot.expense {
background: linear-gradient(180deg, #f093fb 0%, #f5576c 100%);
}
/* 构成区域 */
.composition-section {
margin-bottom: 32rpx;
}
.composition-card {
background: #FFFFFF;
border-radius: 24rpx;
padding: 24rpx 32rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.04);
}
.composition-list {
display: flex;
flex-direction: column;
}
.composition-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20rpx 0;
border-bottom: 2rpx solid #F1F5F9;
}
.composition-item:last-child {
border-bottom: none;
}
.item-left {
display: flex;
align-items: center;
gap: 20rpx;
}
.item-icon {
width: 56rpx;
height: 56rpx;
border-radius: 14rpx;
display: flex;
align-items: center;
justify-content: center;
}
.icon-text {
font-size: 24rpx;
color: #FFFFFF;
font-weight: 600;
}
.item-info {
display: flex;
flex-direction: column;
gap: 4rpx;
}
.item-name {
font-size: 28rpx;
color: #1E293B;
font-weight: 500;
}
.item-percent {
font-size: 22rpx;
color: #94A3B8;
}
.item-right {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 8rpx;
}
.item-amount {
font-size: 28rpx;
font-weight: 600;
color: #1E293B;
}
.item-amount.expense {
color: #f5576c;
}
.progress-bar {
width: 120rpx;
height: 6rpx;
background: #F1F5F9;
border-radius: 3rpx;
overflow: hidden;
}
.progress-fill {
height: 100%;
border-radius: 3rpx;
transition: width 0.3s ease;
}
/* 明细区域 */
.detail-section {
margin-bottom: 32rpx;
}
.detail-tabs {
display: flex;
background: #FFFFFF;
border-radius: 16rpx;
padding: 8rpx;
margin-bottom: 24rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.04);
}
.detail-tab {
flex: 1;
text-align: center;
padding: 20rpx 0;
font-size: 28rpx;
color: #64748B;
border-radius: 12rpx;
}
.detail-tab.active {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #FFFFFF;
font-weight: 500;
}
.detail-list {
background: #FFFFFF;
border-radius: 24rpx;
padding: 0 32rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.04);
}
.detail-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 28rpx 0;
border-bottom: 2rpx solid #F1F5F9;
}
.detail-item:last-child {
border-bottom: none;
}
.detail-left {
display: flex;
align-items: center;
gap: 20rpx;
}
.detail-icon {
width: 72rpx;
height: 72rpx;
border-radius: 18rpx;
display: flex;
align-items: center;
justify-content: center;
}
.detail-icon.income {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.detail-icon.expense {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
}
.detail-info {
display: flex;
flex-direction: column;
gap: 6rpx;
}
.detail-title {
font-size: 30rpx;
font-weight: 600;
color: #1E293B;
}
.detail-subtitle {
font-size: 24rpx;
color: #64748B;
}
.detail-date {
font-size: 22rpx;
color: #94A3B8;
}
.detail-right {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 8rpx;
}
.detail-amount {
font-size: 32rpx;
font-weight: 700;
}
.detail-amount.income {
color: #667eea;
}
.detail-amount.expense {
color: #f5576c;
}
.detail-status {
padding: 6rpx 16rpx;
border-radius: 8rpx;
font-size: 22rpx;
}
.detail-status.paid {
background: #DCFCE7;
color: #16A34A;
}
.detail-status.partial {
background: #FEF3C7;
color: #D97706;
}
.detail-status.unpaid {
background: #FEE2E2;
color: #DC2626;
}
/* 空提示 */
.empty-tip {
text-align: center;
padding: 60rpx 0;
color: #94A3B8;
font-size: 28rpx;
}
/* 加载更多 */
.load-more {
text-align: center;
padding: 32rpx;
color: #64748B;
font-size: 26rpx;
}
2026-04-22 06:47:04 +00:00
/* 日期选择弹窗 */
.custom-date-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 999;
}
.date-picker-popup {
background: #FFFFFF;
border-radius: 24rpx;
width: 600rpx;
overflow: hidden;
margin: 0 40rpx;
}
.popup-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 32rpx;
border-bottom: 2rpx solid #F1F5F9;
}
.popup-title {
font-size: 32rpx;
font-weight: 600;
color: #1E293B;
}
.popup-close {
width: 48rpx;
height: 48rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 24rpx;
background: #F8FAFC;
}
.popup-content {
padding: 48rpx 32rpx;
}
.popup-footer {
display: flex;
padding: 24rpx 32rpx;
gap: 24rpx;
border-top: 2rpx solid #F1F5F9;
}
.btn {
flex: 1;
text-align: center;
padding: 24rpx 0;
border-radius: 12rpx;
font-size: 28rpx;
}
.btn-cancel {
background: #F1F5F9;
color: #64748B;
}
.btn-confirm {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #FFFFFF;
}
2026-04-20 06:23:11 +00:00
</style>