alipay-emulator/pages/bill/bill-list/bill-list.vue

824 lines
18 KiB
Vue

<template>
<view style="overflow: hidden;height: 100vh;;">
<navBar isRightIcon :bgColor="data.navBar.bgColor" :buttonGroup="data.navBar.buttonGroup"
@button-click="clickTitlePopupButton">
<template v-slot:center>
<view class="nav-bar-search flex-align-center flex-1">
<image class="search-icon" src="/static/image/bill/bill-list/search-black.png" mode=""></image>
<input type="text" class="search-input flex-1" placeholder="请输入搜索内容" />
<view class="line h100"></view>
<view class="search-button">搜索</view>
</view>
</template>
</navBar>
<view class="filter-box">
<view class="filter-line">
<view class="filter-item w100" :class="{ 'active-text': item.value == currentFilterType }"
v-for="item in filterType" :key="item.value">
{{ item.name }}
</view>
<view style="width: 100rpx; flex-shrink: 0;"></view>
</view>
<view class="filter-button blur"></view>
<view class="filter-button">
筛选
<image class="filter-icon" src="/static/image/bill/bill-list/down-black.png" mode=""></image>
</view>
</view>
<view style="height: 48px;"></view>
<scroll-view :scroll-y="true" class="bill-list-container"
:style="{ height: `calc(100vh - ${92 + data.statusBarHeight}px)` }" @scroll="scrollList">
<view class="sticky-conatianer" v-if="data.currentScrollTop > 100">
<view class="month">
<view v-if="currentMonthData.year == new Date().getFullYear()"><text class="font-w500">{{
currentMonthData.month
}}</text>月
</view>
<view v-else>
<text class="font-w500">{{ currentMonthData.year }}-{{ currentMonthData.month }}</text>
</view>
<image class="down" src="/static/image/bill/bill-list/down-black.png" mode=""></image>
</view>
<view class="income-ande-outCome flex-between">
<view class="flex">
<view class="item"><text>支出¥</text><text class="money wx-font-regular">{{
Number(currentMonthData.outCome).toFixed(2) }}</text></view>
<view class="item"><text>收入¥</text><text class="money wx-font-regular">{{
Number(currentMonthData.inCome).toFixed(2) }}</text></view>
</view>
<view class="">
<text>收支分析</text>
<uni-icons type="right" color="#555555" size="14"></uni-icons>
</view>
</view>
</view>
<view class="bill-list-card" v-for="(item, index) in billList" :key="index"
:ref="el => cardRefs[index] = el">
<view class="list-title-card"
:class="{ 'current-month': item.month == new Date().getMonth() + 1 && item.year == new Date().getFullYear() }">
<view v-if="item.month == new Date().getMonth() + 1 && item.year == new Date().getFullYear()"
class="bg-right-text">
<text>贴纸</text>
<image class="right-icon" src="/static/image/common/right-blue.png"></image>
</view>
<view class="list-title">
<view>
<text class="month alipay-font">{{ item.month }}</text>
<text>月</text>
<image class="filter-icon down " src="/static/image/bill/bill-list/down-black.png" mode="">
</image>
</view>
</view>
<view class="flex-between analysis-box">
<view class="income-ande-outCome">
<view class="outCome item">
<text class="title">支出</text>
<text class="amount alipay-font"><text class="font-11 wx-font-regular">¥</text>{{
Number(item.outCome).toFixed(2)
}}</text>
</view>
<view class="income item">
<text class="title">收入</text>
<text class="amount alipay-font"><text class="font-11 wx-font-regular">¥</text>{{
Number(item.inCome).toFixed(2)
}}</text>
</view>
</view>
<view v-if="!(item.month == new Date().getMonth() + 1 && item.year == new Date().getFullYear())"
class="analysis-button">
收支分析
</view>
</view>
<view v-if="item.month == new Date().getMonth() + 1 && item.year == new Date().getFullYear()"
class="income-ande-outCome-analysis">
<view class="left-box">
<text class="text">设置支出预算</text>
<image class="right-icon" src="/static/image/common/right-black.png" mode=""></image>
</view>
<view class="analysis-button">
收支分析
</view>
</view>
</view>
<view class="bill-list">
<BalanceList :isBalance="false" :list="item.list" @longPress="onLongPress" @onBill="billClick" />
</view>
</view>
</scroll-view>
</view>
<!-- Custom Context Menu -->
<view class="context-menu-mask" v-if="contextMenu.visible" @click="closeContextMenu" @touchmove.stop.prevent></view>
<view class="context-menu" v-if="contextMenu.visible"
:style="{ top: contextMenu.y + 'px', left: contextMenu.x + 'px' }">
<view class="menu-item border-bottom" @click="handleEdit">编辑</view>
<view class="menu-item" @click="handleDelete">删除</view>
</view>
</template>
<script setup>
import {
dateUtil,
util
} from '@/utils/common.js'
import navBar from '@/components/nav-bar/nav-bar.vue'
import BalanceList from '@/components/balance-list/balance-list.vue'
import {
reactive,
toRefs,
onMounted,
ref,
computed
} from 'vue'
import {
useStore
} from '@/store/index.js'
import {
onShow,
} from '@dcloudio/uni-app'
// 存储卡片的 ref
const cardRefs = ref([])
/**
* 筛选类型
*/
const filterType = [{
name: '全部',
value: 'all'
}, {
name: '支出',
value: 'income'
}, {
name: '转账',
value: 'transfer'
}, {
name: '退款',
value: 'refund'
}, {
name: '订单',
value: 'order'
}, {
name: '还款',
value: 'repayment'
}, {
name: '线下消费',
value: 'offline-consumption'
}, {
name: '充值缴费',
value: 'recharge'
}, {
name: '网购',
value: 'online-shopping'
}, {
name: '二维码收款',
value: 'qr-code-receiving'
}]
const buttonGroup = [{
name: "新增账单",
click: () => {
uni.navigateTo({
url: '/pages/bill/add-bill/add-bill'
})
console.log("新增账单")
}
}
]
const data = reactive({
navBar: {
bgColor: '#F5F5F5',
buttonGroup: buttonGroup
},
statusBarHeight: 0,
currentScrollTop: 0,
currentFilterType: 'all', // 当前筛选类型
currentMonthData: {
month: '',
year: '',
inCome: 0,
outCome: 0,
}, // 当前滚动到的月份
cardPositions: [], // 存储每个卡片距离 scroll-view 顶部的距离
billList: []
})
let {
billList,
currentMonthData,
currentFilterType
} = toRefs(data)
const {
getBillList,
deleteBill
} = useStore()
const contextMenu = reactive({
visible: false,
x: 0,
y: 0,
item: null
})
onShow(() => {
// 获取最新账单列表
getBillDataList()
// Close menu if open
closeContextMenu()
// 初始获取状态栏高度和屏幕高度
updateStatusBarHeight()
// #ifdef APP-PLUS
util.setAndroidSystemBarColor('#F5F5F5')
uni.setNavigationBarColor({
animation: { // 动画效果
duration: 100,
timingFunc: 'easeIn'
}
})
plus.navigator.setStatusBarStyle("dark");
// #endif
})
/**
* 获取账单列表
*/
const getBillDataList = () => {
const allBillList = getBillList()
// 按月份分组
const groupList = []
allBillList.forEach(item => {
// 查找itemInfoList中的createTime
const createTimeItem = item.itemInfoList.find(info => info.key == 'createTime')
const createTime = createTimeItem ? createTimeItem.value : new Date()
const date = new Date(createTime)
const year = date.getFullYear() + ''
const month = (date.getMonth() + 1) + ''
// 查找该月份是否已存在
let monthGroup = groupList.find(g => g.year == year && g.month == month)
if (!monthGroup) {
monthGroup = {
year,
month,
inCome: 0,
outCome: 0,
list: []
}
groupList.push(monthGroup)
}
// 处理单个账单数据格式
const billItem = {
id: item.id,
orderId: item.itemInfoList.find(info => info.key == 'orderNumber')?.value || '',
imgUrl: item.imgUrl,
name: item.name,
amount: item.money,
classification: item.merchantOption.billClassify || '',
isRefund: item.merchantOption.refund,
isAdd: item.isAdd,
money: item.money,
time: dateUtil.smart(createTime),
timestamp: createTime,
merchantOption: item.merchantOption
}
monthGroup.list.push(billItem)
// 计算收支
const money = parseFloat(item.money || 0)
if (item.isAdd) {
monthGroup.inCome += money
} else {
if (item.orderStatus.includes('退款')) {
// 退款算收入还是支出减少?通常退款在列表显示为绿色(收入),这里简单处理为收入
// 如果是支出类型的单子的退款,实际上是钱回流
monthGroup.inCome += money
} else {
monthGroup.outCome += money
}
}
})
// 排序:年份倒序,月份倒序
groupList.sort((a, b) => {
if (a.year != b.year) return b.year - a.year
return b.month - a.month
})
//每个月内的账单按时间倒序
groupList.forEach(group => {
group.list.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp))
})
data.billList = groupList
}
onMounted(() => {
// 在组件挂载后获取卡片位置
getCardPositions()
})
// 更新状态栏高度
const updateStatusBarHeight = () => {
uni.getSystemInfo({
success: (res) => {
data.statusBarHeight = res.statusBarHeight || 0
console.log('直接获取状态栏高度:', data.statusBarHeight)
}
})
}
const billClick = (item) => {
uni.navigateTo({
url: '/pages/bill/bill-detail/bill-detail?id=' + item.id
})
}
/**
* 获取所有卡片相对于 scroll-view 顶部的距离
*/
const getCardPositions = () => {
uni.createSelectorQuery().select('.bill-list-container').boundingClientRect(scrollViewRect => {
uni.createSelectorQuery().selectAll('.bill-list-card').boundingClientRect(cardRects => {
if (cardRects && cardRects.length > 0 && scrollViewRect) {
// 计算每个卡片相对于 scroll-view 顶部的距离
data.cardPositions = cardRects.map(rect => rect.top - scrollViewRect.top)
console.log('卡片位置数据:', data.cardPositions)
}
}).exec()
}).exec()
}
/**
* 滚动列表
*/
const scrollList = (e) => {
data.currentScrollTop = e.detail.scrollTop
console.log("滚动高度", data.currentScrollTop)
// 使用卡片位置数据
if (data.cardPositions && data.cardPositions.length > 0) {
// 找出当前可见的卡片(示例)
const visibleCards = data.cardPositions.map((position, index) => ({
index,
position,
isVisible: position <= data.currentScrollTop + data.statusBarHeight + 100
}))
console.log("可见卡片:", visibleCards)
// 找出当前最顶部的卡片索引
const currentCardIndex = data.cardPositions.findIndex(position =>
position > data.currentScrollTop
)
console.log("当前最顶部卡片索引:", currentCardIndex)
// 获取当前最顶部卡片的月份
let currentMonthIndex = currentCardIndex
if (currentCardIndex === -1) {
// 所有卡片都在视口上方,取最后一个卡片
currentMonthIndex = data.billList.length - 1
} else if (currentCardIndex > 0) {
// 当前视口最顶部的卡片是currentCardIndex - 1
currentMonthIndex = currentCardIndex - 1
}
// 设置当前滚动到的月份
if (currentMonthIndex >= 0 && currentMonthIndex < data.billList.length) {
data.currentMonthData.month = parseInt(data.billList[currentMonthIndex].month)
data.currentMonthData.year = data.billList[currentMonthIndex].year
data.currentMonthData.inCome = data.billList[currentMonthIndex].inCome
data.currentMonthData.outCome = data.billList[currentMonthIndex].outCome
console.log("当前滚动到的月份:", data.currentMonthData)
}
}
}
/**
* 点击标题弹出按钮
* @param e
*/
const clickTitlePopupButton = (button) => {
button.click()
}
/**
* 长按编辑/删除 - Custom Menu
*/
const onLongPress = ({ event, item }) => {
console.log('Long press', event)
// Calculate position
// Use clientX/Y from touches if available
let x = 0
let y = 0
if (event.touches && event.touches.length > 0) {
x = event.touches[0].clientX
y = event.touches[0].clientY
} else if (event.detail && event.detail.x) {
x = event.detail.x
y = event.detail.y
}
// Adjust position to not go off screen (simple heuristic)
const screenWidth = uni.getSystemInfoSync().windowWidth
const screenHeight = uni.getSystemInfoSync().windowHeight
if (x + 100 > screenWidth) x = screenWidth - 110
if (y + 100 > screenHeight) y = y - 100 // show above if too low
contextMenu.x = x
contextMenu.y = y
contextMenu.item = item
contextMenu.visible = true
}
const closeContextMenu = () => {
contextMenu.visible = false
contextMenu.item = null
}
const handleEdit = () => {
if (contextMenu.item) {
uni.navigateTo({
url: `/pages/bill/add-bill/add-bill?id=${contextMenu.item.id}&isEdit=${true}`
})
}
closeContextMenu()
}
const handleDelete = () => {
if (contextMenu.item) {
const id = contextMenu.item.id
uni.showModal({
title: '提示',
content: '确定要删除该账单吗?',
success: function (res) {
if (res.confirm) {
deleteBill(id)
getBillDataList()
uni.showToast({
title: '删除成功',
icon: 'none'
})
}
}
})
}
closeContextMenu()
}
</script>
<style>
@import "@/common/main.css";
page {
background-color: #F5F5F5;
}
</style>
<style>
.context-menu-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 998;
background-color: rgba(0, 0, 0, 0);
}
.context-menu {
position: fixed;
z-index: 999;
background-color: #4c4c4c;
border-radius: 4px;
padding: 0;
min-width: 80px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
.menu-item {
padding: 10px 15px;
font-size: 14px;
color: #ffffff;
text-align: center;
white-space: nowrap;
}
.border-bottom {
border-bottom: 1rpx solid #5d5d5d;
}
</style>
<style lang="less" scoped>
.nav-bar-search {
background-color: #ffffff;
border-radius: 4px;
height: 32px;
padding: 4px 0;
.search-icon {
width: 32rpx;
height: 32rpx;
margin-left: 4px;
margin-right: 6px;
}
.search-input {
font-size: 14px;
}
::v-deep .input-placeholder {
color: var(--text-secondary);
}
.line {
width: 1px;
background-color: var(--border-color);
}
.search-button {
font-size: 14px;
padding: 0 12px;
color: var(--text-secondary);
font-weight: 500;
}
}
::v-deep .uni-navbar__header-btns-right {
width: auto !important;
}
::v-deep .uni-navbar__header-btns-left {
width: auto !important;
}
.filter-box {
position: fixed;
height: 48px;
width: 100%;
z-index: 1;
background-color: #F5F5F5;
.filter-button {
display: flex;
align-items: center;
justify-content: center;
width: 120rpx;
height: 28px;
position: absolute;
top: 10px;
right: 0;
}
.blur {
background-color: #F5F5F5;
filter: blur(5px);
}
}
.filter-icon {
width: 24rpx;
height: 24rpx;
}
.filter-line {
display: flex;
flex-wrap: nowrap;
margin: 0 12px;
margin-top: 10px;
overflow: hidden;
overflow-x: scroll;
.filter-item {
background-color: #ffffff;
color: var(--text-primary);
font-size: 14px;
padding: 4px 16px;
width: auto;
border-radius: 14px;
margin-right: 8px;
white-space: nowrap;
flex-shrink: 0;
}
.active-text {
color: #1B71F8;
}
}
.bill-list-container {
position: relative;
// padding: 0 12px;
flex: 1;
padding-bottom: 12px;
.sticky-conatianer {
position: fixed;
width: 100%;
background-color: #F5F5F5;
padding: 12px;
z-index: 1;
.month {
display: flex;
align-items: center;
font-size: 13px;
.down {
width: 16px;
height: 16px;
margin-left: 4px;
}
}
.income-ande-outCome {
display: flex;
font-size: 13px;
.item {
margin-right: 16px;
.money {
font-size: 12px;
margin-left: 2px;
}
}
}
}
}
.bill-list-card {
border-radius: 8px;
margin: 0 12px;
margin-bottom: 12px;
// background-color: #FFFFFF;
.list-title-card {
background-color: #FFFFFF;
border-radius: 8px 8px 0 0;
padding: 10px 12px;
.month {
font-size: 32px;
color: var(--text-primary);
font-weight: 500;
}
.down {
margin-left: 3px;
}
.income-ande-outCome {
display: flex;
.item {
display: flex;
flex-direction: column;
min-width: 80px;
max-width: 120px;
margin-right: 10px;
.font-11 {
font-size: 12px;
margin-right: 4px;
}
}
.title {
font-size: 12px;
color: var(--text-secondary);
}
.amount {
font-size: 16px;
color: var(--text-primary);
font-weight: 500;
}
}
.analysis-box {
align-items: flex-end;
.analysis-button {
padding: 8px 12px;
background: linear-gradient(90deg, #187AFF 0%, #3295FC 100%);
border-radius: 16px 16px 16px 16px;
color: #FFFFFF;
font-size: 12px;
line-height: 16px;
height: 32px;
flex-shrink: 0;
}
}
}
.list-title {
display: flex;
align-items: center;
}
.current-month {
position: relative;
padding: 10px 12px 18px;
background: url('/static/image/bill/bill-list/current-month-bill-bg.png') no-repeat center center;
background-size: 100% 100%;
background-color: transparent;
.income-ande-outCome {
margin-top: 8rpx;
display: flex;
.item {
display: flex;
flex-direction: column;
min-width: 80px;
max-width: 120px;
margin-right: 10px;
.font-11 {
font-size: 12px;
margin-right: 4px;
}
}
.title {
font-size: 12px;
color: var(--text-secondary);
}
.amount {
font-size: 16px;
color: var(--text-primary);
font-weight: 500;
}
}
.income-ande-outCome-analysis {
display: flex;
justify-content: space-between;
.left-box {
display: flex;
align-items: flex-end;
margin-bottom: 3px;
.text {
font-size: 13px;
line-height: 14px;
color: var(--text-primary);
}
}
.right-icon {
width: 14px;
height: 14px;
margin-left: 5px;
}
.analysis-button {
padding: 8px 12px;
background: linear-gradient(90deg, #187AFF 0%, #3295FC 100%);
border-radius: 16px 16px 16px 16px;
color: #FFFFFF;
font-size: 12px;
line-height: 16px;
}
}
}
.bg-right-text {
display: flex;
align-items: center;
color: #2B3841;
font-size: 22rpx;
position: absolute;
right: 18rpx;
top: 32rpx;
.right-icon {
width: 16rpx;
height: 16rpx;
margin-left: 2rpx;
}
}
.bill-list {
background-color: #ffffff;
}
}
</style>