312 lines
13 KiB
Vue
312 lines
13 KiB
Vue
<template>
|
|
<view class="bills-page">
|
|
<view class="custom-nav safe-area-top">
|
|
<view class="nav-content">
|
|
<view class="nav-back" @click="goBack">
|
|
<uni-icons type="left" size="20" color="#1E293B"></uni-icons>
|
|
</view>
|
|
<text class="nav-title">账单管理</text>
|
|
<view class="nav-actions">
|
|
<view class="nav-btn" @click="addBill">
|
|
<uni-icons type="plus" size="20" color="#2563EB"></uni-icons>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<scroll-view scroll-y class="page-content" @scrolltolower="loadMore">
|
|
<!-- 筛选栏 -->
|
|
<view class="filter-section">
|
|
<view class="filter-row">
|
|
<view class="filter-label">类型:</view>
|
|
<view class="filter-options">
|
|
<view v-for="(tab, index) in typeTabs" :key="index" class="filter-option" :class="{ active: searchForm.type === tab.value }" @click="switchTypeFilter(tab.value)">
|
|
<text>{{tab.label}}</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
<view class="filter-row">
|
|
<view class="filter-label">状态:</view>
|
|
<view class="filter-options">
|
|
<view v-for="(tab, index) in statusTabs" :key="index" class="filter-option" :class="{ active: searchForm.status === tab.value }" @click="switchStatusFilter(tab.value)">
|
|
<text>{{tab.label}}</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
<view class="filter-row">
|
|
<view class="filter-label">日期:</view>
|
|
<view class="date-picker-group">
|
|
<picker mode="date" :value="searchForm.startDate" @change="onStartDateChange">
|
|
<view class="date-picker">{{searchForm.startDate || '开始日期'}}</view>
|
|
</picker>
|
|
<text class="date-separator">至</text>
|
|
<picker mode="date" :value="searchForm.endDate" @change="onEndDateChange">
|
|
<view class="date-picker">{{searchForm.endDate || '结束日期'}}</view>
|
|
</picker>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- 账单列表 -->
|
|
<view class="bill-list">
|
|
<view v-for="(item, index) in billList" :key="index" class="bill-card" @click="viewDetail(item)">
|
|
<view class="bill-header">
|
|
<view class="bill-info">
|
|
<text class="bill-no">{{item.billNo || '-'}}</text>
|
|
<text class="bill-date">{{item.billDate}}</text>
|
|
</view>
|
|
<view class="bill-status" :class="item.status">
|
|
<text>{{getStatusText(item.status)}}</text>
|
|
</view>
|
|
</view>
|
|
<view class="bill-content">
|
|
<view class="bill-type">
|
|
<view class="type-icon" :class="item.type">
|
|
<uni-icons :type="item.type === 'income' ? 'wallet-filled' : 'cart-filled'" size="20" color="#FFFFFF"></uni-icons>
|
|
</view>
|
|
<view class="type-info">
|
|
<text class="type-name">{{getCategoryText(item.category)}}</text>
|
|
<text class="type-tag" :class="item.type">{{item.type === 'income' ? '收入' : '支出'}}</text>
|
|
</view>
|
|
</view>
|
|
<view class="bill-amount">
|
|
<text class="amount" :class="item.type">{{item.type === 'income' ? '+' : '-'}}¥{{item.receivableAmount}}</text>
|
|
</view>
|
|
</view>
|
|
<view class="bill-remark" v-if="item.remark">
|
|
<text class="remark-label">备注:</text>
|
|
<text class="remark-content">{{item.remark}}</text>
|
|
</view>
|
|
<view class="bill-footer">
|
|
<text class="create-time">创建时间: {{item.createTime}}</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<view v-if="billList.length === 0 && !isLoading" class="empty-state">
|
|
<text class="empty-text">暂无账单数据</text>
|
|
<button class="add-btn" @click="addBill">记一笔</button>
|
|
</view>
|
|
|
|
<view class="safe-area-bottom" style="height: 40rpx;"></view>
|
|
</scroll-view>
|
|
</view>
|
|
</template>
|
|
|
|
<script>
|
|
import { billApi, settingApi } from '../../api/index.js'
|
|
|
|
export default {
|
|
data() {
|
|
return {
|
|
typeTabs: [
|
|
{ label: '全部', value: '' },
|
|
{ label: '收入', value: 'income' },
|
|
{ label: '支出', value: 'expense' }
|
|
],
|
|
statusTabs: [
|
|
{ label: '全部', value: '' },
|
|
{ label: '未收', value: 'unpaid' },
|
|
{ label: '部分收款', value: 'partial' },
|
|
{ label: '已收清', value: 'paid' },
|
|
{ label: '已取消', value: 'cancelled' }
|
|
],
|
|
searchForm: {
|
|
type: '',
|
|
status: '',
|
|
startDate: '',
|
|
endDate: ''
|
|
},
|
|
billList: [],
|
|
isLoading: false,
|
|
page: 1,
|
|
pageSize: 10,
|
|
hasMore: true,
|
|
total: 0,
|
|
stats: {
|
|
totalReceivable: '0.00',
|
|
totalReceived: '0.00',
|
|
totalPending: '0.00'
|
|
},
|
|
categoryMap: {}
|
|
}
|
|
},
|
|
onLoad() {
|
|
this.loadCategories()
|
|
this.loadData()
|
|
},
|
|
onShow() {
|
|
this.loadData()
|
|
},
|
|
methods: {
|
|
async loadData() {
|
|
if (this.isLoading) return
|
|
this.isLoading = true
|
|
try {
|
|
const params = {
|
|
page: this.page,
|
|
pageSize: this.pageSize,
|
|
type: this.searchForm.type,
|
|
status: this.searchForm.status,
|
|
startDate: this.searchForm.startDate,
|
|
endDate: this.searchForm.endDate
|
|
}
|
|
const res = await billApi.getAll(params)
|
|
// 适配分页数据格式: { data: { list: [], total: n, page: n, pageSize: n } }
|
|
const list = res.data?.list || []
|
|
this.total = res.data?.total || 0
|
|
if (this.page === 1) {
|
|
this.billList = list
|
|
} else {
|
|
this.billList = [...this.billList, ...list]
|
|
}
|
|
this.hasMore = this.billList.length < this.total
|
|
this.updateStats()
|
|
} catch (error) {
|
|
uni.showToast({ title: '加载失败', icon: 'none' })
|
|
} finally {
|
|
this.isLoading = false
|
|
}
|
|
},
|
|
updateStats() {
|
|
let totalReceivable = 0, totalReceived = 0, totalPending = 0
|
|
this.billList.forEach(item => {
|
|
if (item.type === 'income') {
|
|
totalReceivable += parseFloat(item.receivableAmount || 0)
|
|
totalReceived += parseFloat(item.receivedAmount || 0)
|
|
totalPending += parseFloat(item.receivableAmount || 0) - parseFloat(item.receivedAmount || 0)
|
|
}
|
|
})
|
|
this.stats.totalReceivable = totalReceivable.toFixed(2)
|
|
this.stats.totalReceived = totalReceived.toFixed(2)
|
|
this.stats.totalPending = totalPending.toFixed(2)
|
|
},
|
|
loadMore() {
|
|
if (this.hasMore && !this.isLoading) {
|
|
this.page++
|
|
this.loadData()
|
|
}
|
|
},
|
|
switchTypeFilter(value) {
|
|
this.searchForm.type = value
|
|
this.page = 1
|
|
this.loadData()
|
|
},
|
|
switchStatusFilter(value) {
|
|
this.searchForm.status = value
|
|
this.page = 1
|
|
this.loadData()
|
|
},
|
|
onStartDateChange(e) {
|
|
this.searchForm.startDate = e.detail.value
|
|
this.page = 1
|
|
this.loadData()
|
|
},
|
|
onEndDateChange(e) {
|
|
this.searchForm.endDate = e.detail.value
|
|
this.page = 1
|
|
this.loadData()
|
|
},
|
|
resetSearch() {
|
|
this.searchForm = {
|
|
type: '',
|
|
status: '',
|
|
startDate: '',
|
|
endDate: ''
|
|
}
|
|
this.page = 1
|
|
this.loadData()
|
|
},
|
|
addBill() {
|
|
uni.navigateTo({ url: '/pages/bill-add/bill-add' })
|
|
},
|
|
viewDetail(item) {
|
|
uni.navigateTo({ url: `/pages/bill-detail/bill-detail?id=${item.id}` })
|
|
},
|
|
goBack() { uni.navigateBack() },
|
|
getStatusText(status) {
|
|
const texts = { 'paid': '已收清', 'partial': '部分收款', 'unpaid': '未收', 'cancelled': '已取消' }
|
|
return texts[status] || status
|
|
},
|
|
// 加载类目列表
|
|
async loadCategories() {
|
|
try {
|
|
const res = await settingApi.getCategories()
|
|
const categories = res.data || []
|
|
// 构建 code -> name 的映射
|
|
const map = {}
|
|
categories.forEach(cat => {
|
|
map[cat.code] = cat.name
|
|
})
|
|
this.categoryMap = map
|
|
} catch (error) {
|
|
console.error('加载类目列表失败:', error)
|
|
}
|
|
},
|
|
getCategoryText(category) {
|
|
return this.categoryMap[category] || category || '其他'
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.bills-page { min-height: 100vh; background: #F8FAFC; display: flex; flex-direction: column; }
|
|
.custom-nav { background: #FFFFFF; border-bottom: 2rpx solid #F1F5F9; }
|
|
.nav-content { display: flex; align-items: center; justify-content: space-between; padding: 20rpx 32rpx; }
|
|
.nav-back { width: 60rpx; height: 60rpx; display: flex; align-items: center; justify-content: center; }
|
|
.nav-title { font-size: 36rpx; font-weight: 700; color: #1E293B; }
|
|
.nav-actions { width: 60rpx; height: 60rpx; display: flex; align-items: center; justify-content: center; }
|
|
.page-content { flex: 1; padding: 24rpx 32rpx; }
|
|
.filter-section { margin-bottom: 24rpx; background: #FFFFFF; border-radius: 24rpx; padding: 24rpx; }
|
|
.filter-row { display: flex; align-items: center; margin-bottom: 20rpx; }
|
|
.filter-row:last-child { margin-bottom: 0; }
|
|
.filter-label { font-size: 26rpx; color: #64748B; min-width: 80rpx; }
|
|
.filter-options { display: flex; flex-wrap: wrap; gap: 12rpx; flex: 1; }
|
|
.filter-option { padding: 12rpx 24rpx; background: #F1F5F9; border-radius: 8rpx; font-size: 24rpx; color: #64748B; }
|
|
.filter-option.active { background: #2563EB; color: #FFFFFF; }
|
|
.date-picker-group { display: flex; align-items: center; gap: 16rpx; flex: 1; }
|
|
.date-picker { padding: 12rpx 20rpx; background: #F1F5F9; border-radius: 8rpx; font-size: 24rpx; color: #64748B; min-width: 160rpx; text-align: center; }
|
|
.date-separator { font-size: 24rpx; color: #94A3B8; }
|
|
.stats-section { margin-bottom: 24rpx; }
|
|
.stats-card { background: #FFFFFF; border-radius: 24rpx; padding: 32rpx; display: flex; align-items: center; justify-content: space-around; box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.04); }
|
|
.stat-item { display: flex; flex-direction: column; align-items: center; gap: 8rpx; }
|
|
.stat-label { font-size: 24rpx; color: #64748B; }
|
|
.stat-value { font-size: 32rpx; font-weight: 700; color: #1E293B; }
|
|
.stat-value.received { color: #10B981; }
|
|
.stat-value.pending { color: #EF4444; }
|
|
.stat-divider { width: 2rpx; height: 60rpx; background: #E2E8F0; }
|
|
.bill-list { display: flex; flex-direction: column; gap: 24rpx; }
|
|
.bill-card { background: #FFFFFF; border-radius: 24rpx; padding: 32rpx; box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.04); }
|
|
.bill-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 24rpx; }
|
|
.bill-info { display: flex; flex-direction: column; gap: 8rpx; }
|
|
.bill-no { font-size: 28rpx; font-weight: 600; color: #1E293B; }
|
|
.bill-date { font-size: 24rpx; color: #94A3B8; }
|
|
.bill-status { padding: 6rpx 16rpx; border-radius: 8rpx; font-size: 22rpx; }
|
|
.bill-status.paid { background: #DCFCE7; color: #16A34A; }
|
|
.bill-status.partial { background: #FEF3C7; color: #D97706; }
|
|
.bill-status.unpaid { background: #FEE2E2; color: #DC2626; }
|
|
.bill-content { display: flex; justify-content: space-between; align-items: center; }
|
|
.bill-type { display: flex; align-items: center; gap: 20rpx; }
|
|
.type-icon { width: 72rpx; height: 72rpx; border-radius: 18rpx; display: flex; align-items: center; justify-content: center; }
|
|
.type-icon.income { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); }
|
|
.type-icon.expense { background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); }
|
|
.type-info { display: flex; flex-direction: column; gap: 8rpx; }
|
|
.type-name { font-size: 30rpx; font-weight: 600; color: #1E293B; }
|
|
.type-tag { font-size: 22rpx; padding: 4rpx 12rpx; border-radius: 8rpx; margin-top: 4rpx; }
|
|
.type-tag.income { background: #DCFCE7; color: #16A34A; }
|
|
.type-tag.expense { background: #FEE2E2; color: #DC2626; }
|
|
.bill-amount { display: flex; flex-direction: column; align-items: flex-end; gap: 8rpx; }
|
|
.amount { font-size: 36rpx; font-weight: 700; }
|
|
.amount.income { color: #16A34A; }
|
|
.amount.expense { color: #DC2626; }
|
|
.bill-remark { margin-top: 20rpx; padding-top: 20rpx; border-top: 2rpx solid #F1F5F9; display: flex; gap: 8rpx; }
|
|
.remark-label { font-size: 24rpx; color: #64748B; flex-shrink: 0; }
|
|
.remark-content { font-size: 24rpx; color: #1E293B; flex: 1; word-break: break-all; }
|
|
.bill-footer { margin-top: 16rpx; }
|
|
.create-time { font-size: 22rpx; color: #94A3B8; }
|
|
.empty-state { display: flex; flex-direction: column; align-items: center; padding: 120rpx 0; }
|
|
.empty-text { font-size: 28rpx; color: #94A3B8; margin-bottom: 32rpx; }
|
|
.add-btn { background: linear-gradient(135deg, #2563EB 0%, #1D4ED8 100%); color: #FFFFFF; font-size: 26rpx; padding: 16rpx 40rpx; border-radius: 12rpx; border: none; }
|
|
</style>
|