账单详情页展示,完成添加标签和备注字段

This commit is contained in:
tangxinyue 2026-01-13 16:59:11 +08:00
parent dee2f1c3b7
commit 4b97902e91
45 changed files with 2431 additions and 740 deletions

View File

@ -12,6 +12,6 @@ export default {
} }
</script> </script>
<style lang="less"> <style>
@import './common/main.css'; @import "./common/color.css";
// @import './common/layouts.less';</style> </style>

33
common/color.css Normal file
View File

@ -0,0 +1,33 @@
/* 常用颜色变量 */
:root {
--text-color: #1a1a1a;
--primary-color: #007aff;
--success-color: #4cd964;
--warning-color: #ff9500;
--error-color: #ff3b30;
--text-primary: #333;
--text-secondary: #969696;
--text-tertiary: #999;
--bg-primary: #ffffff;
--bg-secondary: #f5f5f5;
--border-color: #D8D8D8;
--page-bg-color: #f0f3f8;
--footer-text-color: #CBCED3;
}
/* 重置样式 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
/* 基础样式 */
page {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
"Helvetica Neue", Arial, sans-serif;
font-size: 14px;
line-height: 1.5;
color: var(--text-color);
background-color: var(--page-bg-color);
}

View File

@ -1,22 +1,5 @@
/* 公共CSS文件 */ /* 公共CSS文件 */
@import "./color.css";
/* 重置样式 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
/* color: var(--text-color); */
}
/* 基础样式 */
body {
/* font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
"Helvetica Neue", Arial, sans-serif; */
font-size: 14px;
line-height: 1.5;
color: var(--text-color);
background-color: var(--page-bg-color);
}
.w100 { .w100 {
width: 100% width: 100%
@ -26,23 +9,6 @@ body {
height: 100%; height: 100%;
} }
/* 常用颜色变量 */
:root {
--text-color: #1a1a1a;
--primary-color: #007aff;
--success-color: #4cd964;
--warning-color: #ff9500;
--error-color: #ff3b30;
--text-primary: #333;
--text-secondary: #969696;
--text-tertiary: #999;
--bg-primary: #ffffff;
--bg-secondary: #f5f5f5;
--border-color: #D8D8D8;
--page-bg-color: #f0f3f8;
--footer-text-color: #CBCED3;
}
/* 文本样式 */ /* 文本样式 */
.text-center { .text-center {
text-align: center; text-align: center;
@ -371,6 +337,16 @@ body {
overflow: hidden; overflow: hidden;
} }
.over-scroll {
overflow: scroll;
&::-webkit-scrollbar {
display: none;
width: 0;
height: 0;
}
}
/* 文本截断 */ /* 文本截断 */
.text-ellipsis { .text-ellipsis {
overflow: hidden; overflow: hidden;
@ -400,6 +376,11 @@ body {
src: url("/static/font/WeChatSansStd-Medium.otf"); src: url("/static/font/WeChatSansStd-Medium.otf");
} }
@font-face {
font-family: "wxNumberLight";
src: url("/static/font/WeChatSansStd-Light.otf");
}
.alipay-font { .alipay-font {
font-family: "alipayNumber"; font-family: "alipayNumber";
} }
@ -410,4 +391,8 @@ body {
.wx-font-medium { .wx-font-medium {
font-family: "wxNumberMedium"; font-family: "wxNumberMedium";
}
.wx-font-light {
font-family: "wxNumberLight";
} }

View File

@ -19,12 +19,18 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
margin-bottom: 50rpx; margin-bottom: 32rpx;
.title { .title {
font-size: 32rpx; font-size: 32rpx;
font-weight: 500; font-weight: 500;
color: var(--font-color); color: var(--font-color);
.text {
color: #AAAAAA;
font-size: 24rpx;
font-weight: 400;
}
} }
.close-image { .close-image {

View File

@ -2,30 +2,27 @@
<view class="balance-list-container"> <view class="balance-list-container">
<view> <view>
<view v-for="item in props.list" :key="item.name" class="balance-item" <view v-for="item in props.list" :key="item.name" class="balance-item"
:class="{ 'flex-align-center': isBalance }"> :class="{ 'flex-align-center': isBalance }" @click="onClick(item)"
@touchstart="handleTouchStart($event, item)" @touchmove="handleTouchMove" @touchend="handleTouchEnd">
<image class="img" :src="item.imgUrl" mode="aspectFill"></image> <image class="img" :src="item.imgUrl" mode="aspectFill"></image>
<view class="balance-item-text-container"> <view class="balance-item-text-container">
<view class="balance-item-text"> <view class="balance-item-text">
<view> <view class="name" :class="{ 'line-height-51rpx': isBalance }">
<text class="name">{{ item.name }}</text> {{ item.name }}
</view> </view>
<view v-if="item.classification"> <text v-if="item.classification" class="time secondary">{{ item.classification }}</text>
<text class="time secondary">{{ item.classification }}</text> <text class="time secondary">{{ item.time }}</text>
</view>
<view>
<text class="time secondary">{{ item.time }}</text>
</view>
</view> </view>
<view class="balance-item-text text-right" :class="{ 'flex-between': isBalance }"> <view class="balance-item-text text-right" :class="{ 'flex-between': isBalance }">
<view class="money alipay-font" <view class="money alipay-font"
:class="item.isAdd ? (isBalance ? 'add-color' : 'red-add-color') : 'minus-color'"> :class="item.isAdd ? (isBalance ? 'add-color' : 'red-add-color') : 'minus-color', { 'line-height-51rpx': isBalance }">
{{ item.isAdd ? '+' : '-' }}{{ Number(item.money).toFixed(2) }} {{ item.isAdd ? '+' : '-' }}{{ Number(item.money).toFixed(2) }}
</view> </view>
<view v-if="item.isRefund" class="refund">已全额退款</view> <view v-if="item.isRefund" class="refund">已全额退款</view>
<view v-if="isBalance" class="balance secondary">余额 <text class="balance-text">{{ <view v-if="isBalance" class="balance secondary">余额 <text
Number(item.balance).toFixed(2) class="balance-text wx-font-regular">{{
}}</text></view> Number(item.balance).toFixed(2)
}}</text></view>
</view> </view>
</view> </view>
</view> </view>
@ -53,15 +50,59 @@ const props = defineProps({
}) })
// //
const emit = defineEmits(['onBill']) const emit = defineEmits(['onBill', 'longPress'])
const data = reactive({}) const data = reactive({})
let timer = null
let isLongPressTriggered = false
let touchStartX = 0
let touchStartY = 0
const handleTouchStart = (e, item) => {
isLongPressTriggered = false
if (e.touches && e.touches.length > 0) {
touchStartX = e.touches[0].clientX
touchStartY = e.touches[0].clientY
}
timer = setTimeout(() => {
isLongPressTriggered = true
emit('longPress', {
event: e,
item
})
}, 1500)
}
const handleTouchMove = (e) => {
if (e.touches && e.touches.length > 0) {
const moveX = e.touches[0].clientX
const moveY = e.touches[0].clientY
// If moved significantly, cancel long press
if (Math.abs(moveX - touchStartX) > 10 || Math.abs(moveY - touchStartY) > 10) {
clearTimeout(timer)
}
} else {
clearTimeout(timer)
}
}
const handleTouchEnd = () => {
clearTimeout(timer)
}
const onClick = (item) => {
if (isLongPressTriggered) return
emit('onBill', item)
}
onMounted(() => { }) onMounted(() => { })
</script> </script>
<style> <style>
/* @import '../../common/main.css'; */ @import '../../common/main.css';
</style> </style>
<style lang="less"> <style lang="less">
@ -80,8 +121,20 @@ onMounted(() => { })
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
margin-left: 12px; margin-left: 12px;
box-shadow: 0 0.3px 0 0 #F0F0EE; position: relative;
padding: 12px 12px 12px 0; padding: 12px 12px 12px 0;
&::after {
content: '';
position: absolute;
left: 0;
bottom: 0;
width: 100%;
height: 3px;
background-color: #F0F0EE;
transform: scaleY(0.1);
transform-origin: 0 100%;
}
} }
.img { .img {
@ -98,17 +151,26 @@ onMounted(() => { })
// justify-content: space-between; // justify-content: space-between;
.name { .name {
font-size: 28rpx;
color: #343434; color: #343434;
line-height: 20px;
}
.line-height-51rpx {
line-height: 51rpx;
} }
.secondary { .secondary {
color: var(--text-secondary); color: var(--text-secondary);
font-size: 12px; font-size: 24rpx;
margin-top: 14rpx;
} }
.money { .money {
font-size: 17px; font-size: 17px;
font-weight: 500; font-weight: 500;
line-height: 20px;
} }
.add-color { .add-color {
@ -132,6 +194,7 @@ onMounted(() => { })
.refund { .refund {
color: #EA6B48; color: #EA6B48;
margin-top: 14rpx;
font-size: 12px; font-size: 12px;
} }

View File

@ -120,6 +120,8 @@ const buttonClick = (button) => {
</script> </script>
<style scoped> <style scoped>
@import "/common/main.css";
.nav-bar-container { .nav-bar-container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -201,5 +203,6 @@ const buttonClick = (button) => {
border-radius: 8px; border-radius: 8px;
height: 42px; height: 42px;
line-height: 42px; line-height: 42px;
font-size: 28rpx;
} }
</style> </style>

View File

@ -1,6 +1,6 @@
{ {
"name" : "alipay-emulator", "name" : "alipay-emulator",
"appid" : "__UNI__B05EDBF", "appid" : "__UNI__D535736",
"description" : "", "description" : "",
"versionName" : "1.0.0", "versionName" : "1.0.0",
"versionCode" : "100", "versionCode" : "100",
@ -17,7 +17,9 @@
"delay" : 0 "delay" : 0
}, },
/* */ /* */
"modules" : {}, "modules" : {
"Camera" : {}
},
/* */ /* */
"distribute" : { "distribute" : {
/* android */ /* android */
@ -46,7 +48,8 @@
}, },
/* SDK */ /* SDK */
"sdkConfigs" : {} "sdkConfigs" : {}
} },
"nvueLaunchMode" : ""
}, },
/* */ /* */
"quickapp" : {}, "quickapp" : {},

View File

@ -5,7 +5,8 @@
"path": "pages/balance/index", "path": "pages/balance/index",
"style": { "style": {
"navigationBarTitleText": "余额页面", "navigationBarTitleText": "余额页面",
"navigationStyle": "custom" "navigationStyle": "custom",
"navigationBarTextStyle": "white"
} }
}, },
{ {
@ -35,6 +36,20 @@
"navigationBarTitleText": "热门图标", "navigationBarTitleText": "热门图标",
"navigationStyle": "custom" "navigationStyle": "custom"
} }
},
{
"path": "pages/balance/fast-entrance-management/fast-entrance-management",
"style": {
"navigationBarTitleText": "快速入口页面",
"navigationStyle": "custom"
}
},
{
"path": "pages/bill/bill-detail/bill-detail",
"style": {
"navigationBarTitleText": "账单详情页",
"navigationStyle": "custom"
}
} }
], ],
"globalStyle": { "globalStyle": {

View File

@ -0,0 +1,271 @@
<template>
<view class="container">
<NavBar class="nav-bar" title="快捷入口" :bgColor="data.navBar.bgColor" isRightButton @right-click="onRightClick">
</NavBar>
<view class="content">
<view class="grid-card">
<view class="grid-list">
<!-- 遍历显示网格项绑定触摸事件以支持拖拽 -->
<view class="grid-item" v-for="(item, index) in data.list" :key="item.id"
:class="{ 'dragging-placeholder': draggingItem && draggingItem.id === item.id }"
@touchstart="handleTouchStart($event, item, index)" @touchmove.stop.prevent="handleTouchMove"
@touchend="handleTouchEnd">
<view class="icon-box">
<image class="icon" :src="`/static/image/balance/menu-icon/${item.label}.png`"
mode="aspectFit"></image>
</view>
<text class="name">{{ item.name }}</text>
</view>
</view>
<view class="footer-tip">*长按可以排序哟</view>
</view>
</view>
<!-- 拖拽时显示的浮动项跟随手指 -->
<view v-if="draggingItem" class="grid-item floating-item" :style="dragStyle">
<view class="icon-box">
<image class="icon" :src="`/static/image/balance/menu-icon/${draggingItem.label}.png`" mode="aspectFit">
</image>
</view>
<text class="name">{{ draggingItem.name }}</text>
</view>
</view>
</template>
<script setup>
import NavBar from '@/components/nav-bar/nav-bar.vue'
import { fastEntranceList } from '@/static/json/initial.json'
import { onShow } from '@dcloudio/uni-app'
import { storage } from '@/utils/storage'
import {
reactive,
toRefs,
ref,
nextTick
} from 'vue'
const data = reactive({
navBar: {
bgColor: "#F5F5F5"
},
list: []
})
let { list } = toRefs(data)
//
const draggingItem = ref(null) //
const dragStyle = ref({ top: '0px', left: '0px' }) //
let startX = 0 // X
let startY = 0 // Y
let longPressTimer = null //
let itemRects = [] //
let containerTop = 0 //
onShow(() => {
//
const cachedList = storage.get('fastEntranceList')
if (cachedList && cachedList.length > 0) {
data.list = cachedList
} else {
// json
data.list = JSON.parse(JSON.stringify(fastEntranceList))
}
//
nextTick(() => {
measureItems()
})
})
/**
* 测量所有网格项的位置用于后续碰撞检测
*/
const measureItems = () => {
uni.createSelectorQuery().select('.grid-list').boundingClientRect(res => {
if (res) {
containerTop = res.top
}
}).exec()
uni.createSelectorQuery().selectAll('.grid-item').boundingClientRect(rects => {
itemRects = rects
}).exec()
}
/**
* 触摸开始启动长按定时器
*/
const handleTouchStart = (e, item, index) => {
if (e.touches.length !== 1) return
startX = e.touches[0].clientX
startY = e.touches[0].clientY
longPressTimer = setTimeout(() => {
//
draggingItem.value = item
// 使
updateDragPosition(startX, startY)
//
uni.vibrateShort()
}, 500) // 500ms
}
/**
* 触摸移动更新拖拽位置并检测排序
*/
const handleTouchMove = (e) => {
if (!draggingItem.value) {
//
const moveX = e.touches[0].clientX
const moveY = e.touches[0].clientY
if (Math.abs(moveX - startX) > 10 || Math.abs(moveY - startY) > 10) {
clearTimeout(longPressTimer)
}
return
}
const x = e.touches[0].clientX
const y = e.touches[0].clientY
updateDragPosition(x, y)
checkReorder(x, y)
}
/**
* 触摸结束重置状态
*/
const handleTouchEnd = () => {
clearTimeout(longPressTimer)
draggingItem.value = null
}
/**
* 更新拖拽浮层的位置
*/
const updateDragPosition = (x, y) => {
//
// 20% (75px) -> 37px
//
dragStyle.value = {
top: (y - 40) + 'px',
left: (x - 37) + 'px',
position: 'fixed',
zIndex: 999,
opacity: 0.8,
width: itemRects[0] ? itemRects[0].width + 'px' : '20%' //
}
}
/**
* 检测是否需要重新排序
*/
const checkReorder = (x, y) => {
//
const targetIndex = itemRects.findIndex(rect =>
x >= rect.left && x <= rect.right &&
y >= rect.top && y <= rect.bottom
)
if (targetIndex !== -1) {
const draggedIndex = data.list.findIndex(item => item.id === draggingItem.value.id)
if (draggedIndex !== -1 && draggedIndex !== targetIndex) {
//
const item = data.list.splice(draggedIndex, 1)[0]
data.list.splice(targetIndex, 0, item)
}
}
}
const onRightClick = () => {
//
storage.set('fastEntranceList', data.list)
uni.showToast({
title: '保存成功',
icon: 'success',
duration: 500
})
setTimeout(() => {
uni.navigateBack()
}, 500)
}
</script>
<style>
page {
background-color: #F5F5F5;
}
</style>
<style lang="less" scoped>
.container {
min-height: 100vh;
}
.content {
padding: 12px;
}
.grid-card {
background-color: #FFFFFF;
border-radius: 12px;
padding: 20px 0;
padding-bottom: 12px;
.footer-tip {
text-align: right;
font-size: 20rpx;
color: #767676;
padding: 0 16rpx;
}
}
.grid-list {
display: flex;
flex-wrap: wrap;
}
.grid-item {
width: 20%;
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 24px;
}
.icon-box {
width: 36px;
height: 36px;
border: 1px dashed #cccccc;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 8px;
}
.icon {
width: 20px;
/* Adjusted based on visual ratio in screenshot inside the box */
height: 20px;
}
.name {
font-size: 12px;
color: #333333;
}
.dragging-placeholder {
opacity: 0;
}
.floating-item {
pointer-events: none;
/* Let touches pass through to underlying elements for detection */
}
</style>

View File

@ -30,7 +30,7 @@
<text>可用余额()</text> <text>可用余额()</text>
<image class="img" src="/static/image/balance/eye.png" mode=""></image> <image class="img" src="/static/image/balance/eye.png" mode=""></image>
</view> </view>
<view class="balance flex-cneter"> <view class="balance flex-cneter" @click="editBalance">
<text class="text alipay-font">{{ Number(balance).toFixed(2) }}</text> <text class="text alipay-font">{{ Number(balance).toFixed(2) }}</text>
</view> </view>
<view class="button-group"> <view class="button-group">
@ -48,9 +48,8 @@
</view> </view>
</view> </view>
<view class="menu-box"> <view class="menu-box">
<view class="item" v-for="item in menuList"> <view class="item menu-icon-box" v-for="item in menuList">
<image class="menu-icon" :src="`/static/image/balance/menu-icon/${item.imgLabel}.png`" <image class="menu-icon" :src="`/static/image/balance/menu-icon/${item.label}.png`" mode="">
mode="">
</image> </image>
<text class="icon-name">{{ item.name }}</text> <text class="icon-name">{{ item.name }}</text>
</view> </view>
@ -64,7 +63,7 @@
</view> </view>
</view> </view>
<view class="item"> <view class="item">
<BalanceList :list="changeDetailList" /> <BalanceList :list="changeDetailList" @longPress="onLongPress" />
</view> </view>
</view> </view>
</view> </view>
@ -74,6 +73,15 @@
</view> </view>
</view> </view>
</scroll-view> </scroll-view>
<!-- Custom Context Menu (Positioned at touch point) -->
<view v-if="contextMenu.visible" class="context-menu-mask" @touchstart="closeContextMenu">
<view class="context-menu" :style="{ top: contextMenu.y + 'px', left: contextMenu.x + 'px' }"
@touchstart.stop>
<view class="menu-item" @click="handleEdit">编辑</view>
<view class="menu-item delete" @click="handleDelete">删除</view>
</view>
</view>
</view> </view>
<!-- 输入框示例 --> <!-- 输入框示例 -->
<uni-popup ref="inputDialog" type="dialog"> <uni-popup ref="inputDialog" type="dialog">
@ -87,6 +95,7 @@
<script setup> <script setup>
import NavBar from '@/components/nav-bar/nav-bar' import NavBar from '@/components/nav-bar/nav-bar'
import BalanceList from '@/components/balance-list/balance-list.vue' import BalanceList from '@/components/balance-list/balance-list.vue'
import { fastEntranceList } from '@/static/json/initial.json'
import { import {
util, util,
@ -114,55 +123,42 @@ import {
useStore, useStore,
} from '@/store' } from '@/store'
import {
dateUtil
} from '@/utils/common.js'
// store // store
const { const {
store store,
getBillList,
deleteBill
} = useStore() } = useStore()
const inputDialog = ref(null) const inputDialog = ref(null)
const menuList = [{ const buttonGroup = [
imgLabel: "zhuanzhang", {
name: "转账" name: "修改余额",
}, { click: () => {
imgLabel: "yinhangka", editBalance()
name: "银行卡" }
}, { },
imgLabel: "qinqingka", {
name: "亲情卡" name: "快捷入口管理",
}, { click: () => {
imgLabel: "xiaohebao", util.goPage('/pages/balance/fast-entrance-management/fast-entrance-management')
name: "小荷包" }
}, { }, {
imgLabel: "zhuanyongjin", name: "添加余额变动明细",
name: "专用金" click: () => {
}] util.goPage(`/pages/bill/add-bill/add-bill?type=${1}&payMethod=余额`)
}
const buttonGroup = [{ }, {
name: "编辑模式", name: "账单列表",
click: () => { click: () => {
console.log("编辑模式") util.goPage("/pages/bill/bill-list/bill-list")
}
} }
},
{
name: "修改余额",
click: () => {
console.log("修改余额")
data.balance = Number(data.balance).toFixed(2)
inputDialog.value.open()
}
},
{
name: "快捷入口管理",
click: () => {
console.log("快捷入口管理")
}
}, {
name: "账单列表",
click: () => {
util.goPage("/pages/bill/bill-list/bill-list")
}
}
] ]
const data = reactive({ const data = reactive({
@ -172,36 +168,14 @@ const data = reactive({
statusBarHeight: 0, statusBarHeight: 0,
balance: 0, balance: 0,
windowHeight: 0, windowHeight: 0,
changeDetailList: [{ changeDetailList: [],
time: "2023-08-15 10:00:00", menuList: []
type: "充值",
money: "1000.00",
isAdd: true,
name: "充值",
balance: 1000.00,
imgUrl: "https://picsum.photos/200/200?random=1"
}, {
time: "2023-08-15 10:00:00",
type: "充值",
money: "1000.00",
isAdd: false,
name: "充值",
balance: 1000.00,
imgUrl: "https://picsum.photos/200/200?random=1"
}, {
time: "2023-08-15 10:00:00",
type: "充值",
money: "1000.00",
isAdd: true,
name: "充值",
balance: 1000.00,
imgUrl: "https://picsum.photos/200/200?random=1"
}]
}) })
let { let {
balance, balance,
changeDetailList changeDetailList,
menuList
} = toRefs(data) } = toRefs(data)
onLoad(async () => { onLoad(async () => {
@ -216,11 +190,56 @@ onLoad(async () => {
}) })
onShow(() => { onShow(() => {
// #ifdef APP-PLUS
util.setAndroidSystemBarColor('#F0F3F8') util.setAndroidSystemBarColor('#F0F3F8')
// #endif
//
data.menuList = fastEntranceData()
getRecentBills()
}) })
const fastEntranceData = () => {
//
const cachedList = storage.get('fastEntranceList')
if (cachedList && cachedList.length > 0) {
return cachedList
} else {
return JSON.parse(JSON.stringify(fastEntranceList))
}
}
//
const getRecentBills = () => {
const allBillList = getBillList()
//
const sortedList = [...allBillList].sort((a, b) => {
const timeA = a.itemInfoList.find(info => info.key == 'createTime')?.value || 0
const timeB = b.itemInfoList.find(info => info.key == 'createTime')?.value || 0
return new Date(timeB) - new Date(timeA)
})
// 3
const recentBills = sortedList.filter(item => item.payMethod == '余额').slice(0, 3)
//
data.changeDetailList = recentBills.map(item => {
const createTime = item.itemInfoList.find(info => info.key == 'createTime')?.value || new Date()
return {
time: dateUtil.format(createTime),
type: item.itemInfoList.find(info => info.key == 'billCategory')?.value || '消费',
money: parseFloat(item.money).toFixed(2),
isAdd: item.isAdd,
name: item.name,
balance: item.balance,
imgUrl: item.imgUrl,
id: item.id
}
})
}
onMounted(() => { onMounted(() => {
//
updateStatusBarHeight() updateStatusBarHeight()
}) })
@ -250,9 +269,13 @@ const clickTitlePopupButton = (button) => {
button.click() button.click()
} }
/** //
* 输入框确认事件 const editBalance = () => {
*/ data.balance = Number(data.balance).toFixed(2)
inputDialog.value.open()
}
//
const dialogInputConfirm = () => { const dialogInputConfirm = () => {
if (data.balance > 999999999) { if (data.balance > 999999999) {
uni.showToast({ uni.showToast({
@ -272,14 +295,110 @@ const dialogInputCancle = () => {
data.balance = storage.get('balance') data.balance = storage.get('balance')
inputDialog.value.close() inputDialog.value.close()
} }
const contextMenu = reactive({
visible: false,
x: 0,
y: 0,
item: null
})
const onLongPress = ({
event,
item
}) => {
//
let touchX = 0
let touchY = 0
if (event.touches && event.touches.length > 0) {
touchX = event.touches[0].clientX
touchY = event.touches[0].clientY
} else if (event.changedTouches && event.changedTouches.length > 0) {
touchX = event.changedTouches[0].clientX
touchY = event.changedTouches[0].clientY
} else {
// 使
touchX = 100
touchY = 100
}
//
const screenWidth = data.windowWidth || 375
const screenHeight = data.windowHeight || 667
if (touchX + 100 > screenWidth) {
touchX = screenWidth - 110
}
if (touchY + 90 > screenHeight) {
touchY = screenHeight - 100
}
contextMenu.x = touchX
contextMenu.y = touchY
contextMenu.item = item
contextMenu.visible = true
//
uni.vibrateShort()
}
/**
* 关闭上下文菜单
*/
const closeContextMenu = () => {
contextMenu.visible = false
contextMenu.item = null
}
/**
* 修改明细
*/
const handleEdit = () => {
if (contextMenu.item) {
util.goPage(`/pages/bill/add-bill/add-bill?id=${contextMenu.item.id}&type=${1}&isEdit=${true}`)
}
closeContextMenu()
}
/**
* 删除明细
*/
const handleDelete = () => {
const itemToDelete = contextMenu.item
if (itemToDelete) {
uni.showModal({
title: '提示',
content: '确定要删除这条账单吗?',
success: (res) => {
if (res.confirm) {
console.log('Deleting item:', itemToDelete)
const success = deleteBill(itemToDelete.id)
if (success) {
uni.showToast({
title: '删除成功',
icon: 'success'
})
getRecentBills()
} else {
uni.showToast({
title: '删除失败',
icon: 'none'
})
}
}
}
})
}
closeContextMenu()
}
</script> </script>
<style> <style lang="less" scoped>
/* 直接在页面导入公共样式 */ /* 直接在页面导入公共样式 */
@import '../../common/main.css'; @import '/common/main.css';
</style>
<style scoped>
.container { .container {
background-color: #F0F3F8; background-color: #F0F3F8;
overflow: hidden; overflow: hidden;
@ -451,9 +570,22 @@ const dialogInputCancle = () => {
margin-top: 8px; margin-top: 8px;
border-radius: 12px; border-radius: 12px;
background-color: #FFFFFF; background-color: #FFFFFF;
padding: 20px; padding: 20px 0;
margin-left: 12px; margin-left: 12px;
margin-right: 12px; margin-right: 12px;
overflow: hidden;
overflow-x: scroll;
.menu-icon-box {
width: 20%;
flex-shrink: 0;
}
&::-webkit-scrollbar {
display: none;
width: 0;
height: 0;
}
} }
.item { .item {
@ -526,4 +658,46 @@ const dialogInputCancle = () => {
color: var(--footer-text-color); color: var(--footer-text-color);
margin-top: 18px; margin-top: 18px;
} }
/* Context Menu Styles */
.context-menu-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 999;
/* transparent mask to catch clicks */
}
.context-menu {
position: fixed;
background-color: rgba(255, 255, 255, 0.95);
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
padding: 4px 0;
min-width: 100px;
z-index: 1000;
backdrop-filter: blur(10px);
}
.menu-item {
padding: 10px 16px;
font-size: 14px;
color: #333;
text-align: center;
border-bottom: 0.5px solid rgba(0, 0, 0, 0.05);
}
.menu-item:last-child {
border-bottom: none;
}
.menu-item:active {
background-color: rgba(0, 0, 0, 0.05);
}
.menu-item.delete {
color: #ff3b30;
}
</style> </style>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,730 @@
<template>
<view style="overflow: hidden;overflow-y: scroll; height: 100vh;">
<navBar :title="data.navBar.title" :bgColor="data.navBar.bgColor" :buttonGroup="data.navBar.buttonGroup"
@button-click="util.clickTitlePopupButton"></navBar>
<!-- 账单信息容器 -->
<view class=" add-bill-container">
<!-- 头像 -->
<view class="avatar-box">
<image class="avatar-image" :src="billData.imgUrl || defaultImage" @click="changeAvatar"></image>
</view>
<!-- 主要信息 -->
<view class="main-info flex-align-center flex-column">
<!-- 名称 -->
<view class="name info-item-input">
<!-- 隐藏的text用于测量宽度 -->
<text class="text-measure">{{ billData.name }}</text>
</view>
<!-- 金额 -->
<view class="money-box flex-align-center">
<view class="add money alipay-font">{{ billData.isAdd ? '+' : '-' }}</view>
<!-- 金额 -->
<view class=" money info-item-input alipay-font" style="height: 77rpx;">
<!-- 隐藏的text用于测量宽度 -->
<text class="text-measure font-w500" style="font-size: 64rpx;">{{ billData.money }}</text>
</view>
</view>
<!-- 订单状态 -->
<view class="order-status-info" :class="{ isRefund: billData.merchantOption.refund }">
{{ billData.orderStatus }}
</view>
</view>
<!-- 详情信息列表 -->
<view class="detail-info-container">
<template v-for="item in billData.itemInfoList" :key="item.id">
<view class="info-item-box" :class="{ 'bottom-border': item.key == 'relatedRecord' }"
v-if="item.key != 'paymentReward' || (item.key == 'paymentReward' && billData.merchantOption.payReward)">
<view class="item-label">
{{ item.label }}
</view>
<view v-if="item.type != 'link' && item.key != 'paymentReward'" class="info-item-input">
<!-- 隐藏的text用于测量宽度 -->
<text class="text-measure">{{ item.value }}</text>
</view>
<view v-if="item.key == 'paymentReward'" class="payment-reward-box">
<view class="payment-reward flex-align-center">
<image class="payment-reward-image"
src="/static/image/bill/add-bill/payment-reward.png"></image>
<text>立即领取{{ item.value }}积分</text>
</view>
</view>
</view>
<view v-if="item.type == 'link'" class="info-item-link">
<view class="img-box">
<image class="img w100 h100" :src="item.value.imgUrl || defaultImage"></image>
</view>
<view class="textarea-box flex-1">
{{ item.value.text }}
</view>
<view class="quantity">
{{ item.value.quantity }}
</view>
</view>
</template>
<!-- 服务详情 -->
<view v-if="billData.merchantOption.serviceDetail || billData.selectId == 8">
<view class="info-item-box">
<view class="item-label">
服务详情
</view>
</view>
<view class="info-item-box w100">
<view v-if="billData.merchantOption.serviceDetail"
class="service-detail-box w100 flex-align-center flex-between">
<view class="info flex-align-center">
<image class="service-detail-image"
:src="billData.merchantOption.serviceDetailInfo.imgUrl">
</image>
<text class="text">{{ billData.merchantOption.serviceDetailInfo.text }}</text>
</view>
<view class="right-box flex-align-center">
<text class="text-right">{{
getServiceDetailRightText(billData.merchantOption.serviceDetailInfo.rightText
) }}</text>
<image class="icon" src="/static/image/common/right-grey.png"></image>
</view>
</view>
<!-- 商家码收款服务详情 -->
<view v-if="billData.selectId == 8"
class="service-detail-box select-id-8 w100 flex-align-center">
<image class="alipay-merchant-img"
src="/static/image/bill/bill-detail/alipay-merchant-img.png"></image>
<view class="center-box flex-1">
<view class="title">支付宝商家服务-收款记录</view>
<view class="flex-align-center">
<text class="text-option-item">收款明细</text>
<view class="line"></view>
<text class="text-option-item">顾客画像</text>
<view class="line"></view>
<text class="text-option-item">有奖励</text>
</view>
</view>
<view class="right-btn">查看详情</view>
</view>
</view>
</view>
<!-- 推荐服务 -->
<view v-if="billData.merchantOption.recommendService">
<view class="info-item-box">
<view class="item-label">
推荐服务
</view>
</view>
<view class="info-item-box w100">
<view class="service-detail-box recommend-service-box w100 flex-align-center flex-between">
<view class="info flex-align-center">
<text class="text">{{ billData.merchantOption.recommendServiceInfo.text }}</text>
</view>
<view class="right-box flex-align-center">
<text class="text-right">去看看</text>
<image class="icon" src="/static/image/common/right-blue.png"></image>
</view>
</view>
</view>
</view>
<!-- 服务推荐 -->
<view v-if="billData.merchantOption.serviceRecommend">
<view class="info-item-box">
<view class="item-label">
服务推荐
</view>
</view>
<view class="info-item-box w100">
<view class="service-detail-box service-recommend-box w100 flex-align-center flex-between">
<text class="text">{{ billData.merchantOption.serviceRecommendInfo.text }}</text>
<image class="icon" src="/static/image/common/right-blue.png"></image>
</view>
</view>
</view>
</view>
<view class="more-box">
<text>更多</text>
<image class="icon" src="/static/image/common/down-grey.png"></image>
</view>
</view>
<view class="bill-management">
<view class="title">账单管理</view>
<view class="top-box" style="background-color: #FBF8F1;">
<text style="font-size: 24rpx;color: #B7971B;">本笔登上月收入榜看看分析吧</text>
<uni-icons type="right" size="10" color="#B7971B"></uni-icons>
</view>
<view class="bill-classification bill-management-item">
<text class="label">账单分类</text>
<view class="right-box flex-align-center">
<text>{{ billData.merchantOption.billClassify }}</text>
<uni-icons style="margin-left: 8rpx;" type="right" size="12" color="#969696"></uni-icons>
</view>
</view>
<view class="bill-management-item">
<text v-if="billData.merchantOption.note.text || billData.merchantOption.note.isImage"
class="label">标签</text>
<text v-else class="label">标签和备注</text>
<view class="tag-box right-box flex-1 flex-align-center">
<text v-if="billData.merchantOption.tag.length == 0 || !billData.merchantOption.tag">添加</text>
<view v-else v-for="(tag, index) in billData.merchantOption.tag.slice(0, 3)" :key="tag"
class="tag-item">
<text>{{ index === 2 ? (tag.substring(0, 1) + (tag.length > 1 ? '...' : '')) :
tag }}</text>
</view>
</view>
<view class="right-box flex-align-center">
<uni-icons style="margin-left: 8rpx;" type="right" size="12" color="#969696"></uni-icons>
</view>
</view>
<view class="bill-management-item"
v-if="billData.merchantOption.note.text || billData.merchantOption.note.isImage">
<text class="label">备注</text>
<view class="tag-box right-box flex-1 flex-align-center">
<view class="remark-box flex-align-center text-align-right flex-1">
<image v-if="billData.merchantOption.note.isImage" class="remark-img"
src="/static/image/bill/bill-detail/bill-remark-img.png">
</image>
<view class="text">
{{ billData.merchantOption.note.text }}
</view>
</view>
</view>
<view class="right-box flex-align-center">
<uni-icons style="margin-left: 8rpx;" type="right" size="12" color="#969696"></uni-icons>
</view>
</view>
<view class="bill-management-item" style="margin-top: 32rpx;">
<text class="label">计入收支</text>
<view class="right-box flex-align-center" style="transform:scale(0.6); width: 100rpx;">
<switch color="#1676FE" :checked="billData.merchantOption.countInAndOut" />
</view>
</view>
<view class="bottom-icon-box">
<view class="line"></view>
<view class="icon-list flex flex-wrap">
<template v-for="(item, index) in billBottomIconList" :key="index">
<view v-if="billData.bottomIcons.includes(item.id)" class="icon-item flex-align-center">
<image class="bottom-icon" :src="`/static/image/bill/blue-icon/${item.icon}.png`"></image>
<text>{{ item.name }}</text>
</view>
</template>
</view>
</view>
</view>
</view>
</template>
<script setup>
import navBar from '@/components/nav-bar/nav-bar.vue'
import {
billBottomIconList
} from '@/static/json/add-bill.json'
import {
util
} from '@/utils/common.js'
import {
reactive,
toRefs,
ref,
nextTick
} from 'vue'
import {
onLoad,
onShow,
} from '@dcloudio/uni-app'
import {
useStore
} from '@/store/index.js'
const {
getBillList,
updateBill
} = useStore()
const buttonGroup = [{
name: "编辑账单",
click: () => {
uni.navigateTo({
url: `/pages/bill/add-bill/add-bill?id=${billData.value.id}&isEdit=${true}`
})
}
}]
const defaultImage = "/static/image/bill/add-bill/add-avatar.png"
const data = reactive({
navBar: {
title: '账单详情',
bgColor: '#F5F5F5',
buttonGroup: buttonGroup
},
billId: "",
//
billData: {
id: "",
selectId: -1,
imgUrl: "",
name: '',
isAdd: false,
money: "",
balance: 0,
payMethod: "",
orderStatus: '交易成功',
itemInfoList: [],
bottomIcons: [0, 1, 2, 3],
merchantOption: {
serviceDetail: false,
serviceDetailInfo: {
imgUrl: '',
rightText: 0,
text: '收钱吧'
},
recommendService: false,
recommendServiceInfo: {
text: '你有一个免费取现机会'
},
serviceRecommend: false,
serviceRecommendInfo: {
text: '恭喜你有6元转账保障福利待领取'
},
refund: false,
countInAndOut: false,
payReward: false
}
}
})
let {
billData
} = toRefs(data)
onShow(() => {
getBillData(data.billId)
})
onLoad((option) => {
console.log(option)
if (option.id) {
data.billId = option.id
}
})
function getBillData(id) {
// id
const existingBill = getBillList().find(b => b.id === id)
if (existingBill) {
//
billData.value.id = existingBill.id
billData.value.selectId = existingBill.selectId
billData.value.name = existingBill.name
billData.value.money = existingBill.money
billData.value.imgUrl = existingBill.imgUrl
billData.value.isAdd = existingBill.isAdd
billData.value.orderStatus = existingBill.orderStatus
billData.value.balance = existingBill.balance
billData.value.payMethod = existingBill.payMethod
billData.value.bottomIcons = existingBill.bottomIcons
billData.value.merchantOption = JSON.parse(JSON.stringify(existingBill.merchantOption))
// selectIditemInfoList
nextTick(() => {
billData.value.itemInfoList = JSON.parse(JSON.stringify(existingBill.itemInfoList))
})
}
}
//
const getServiceDetailRightText = (value) => {
//
const serviceDetailRightTextList = [
{
"value": 0,
"text": "进入小程序"
},
{
"value": 1,
"text": "查看详情"
}
]
const item = serviceDetailRightTextList.find(item => item.value === Number(value))
return item?.text || ""
}
</script>
<style lang="less">
@import "@/common/main.css";
page {
background-color: #F5F5F5;
}
</style>
<style lang="less" scoped>
.add-bill-container {
background-color: #ffffff;
margin: 24rpx;
margin-top: 34rpx;
padding: 24rpx;
border-radius: 16rpx 16rpx 16rpx 16rpx;
.random-dice {
width: 100%;
text-align: right;
.random-dice-image {
width: 64rpx;
height: 64rpx;
}
}
}
.avatar-box {
margin-top: 12rpx;
text-align: center;
margin-bottom: 26rpx;
.avatar-image {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
}
}
.main-info {
color: #1a1a1a;
font-size: 64rpx;
.order-status-info {
font-size: 28rpx;
margin-top: 12rpx;
}
.isRefund {
color: #EA6B48;
}
}
.detail-info-container {
padding-top: 36rpx;
.info-item-input {
font-size: 26rpx;
.text-measure {
font-size: 26rpx;
}
.text-input {
font-size: 26rpx;
}
}
.payment-reward-box {
background-color: #FFEFDB;
border-radius: 26rpx 26rpx 26rpx 26rpx;
padding: 10rpx 16rpx;
line-height: 32rpx;
color: #EC670C;
font-size: 26rpx;
.payment-reward-image {
width: 32rpx;
height: 32rpx;
margin-right: 10rpx;
}
}
.info-item-link {
display: flex;
align-items: center;
padding: 12rpx 16rpx;
background-color: #F6F7FB;
border-radius: 20rpx 20rpx 20rpx 20rpx;
.img-box {
width: 88rpx;
height: 88rpx;
border-radius: 8rpx;
margin-right: 22rpx;
background-color: #E8EDF2;
.img {
border-radius: 8rpx 8rpx 8rpx 8rpx;
}
}
.textarea-box {
height: 40px;
font-size: 26rpx;
color: #1a1a1a;
font-weight: 500;
margin-right: 10rpx;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
line-clamp: 2;
-webkit-box-orient: vertical;
}
.quantity {
font-size: 22rpx;
color: #969696;
text-align: right;
min-width: 100rpx;
}
}
.service-detail-box {
padding: 22rpx 24rpx;
border-radius: 16rpx 16rpx 16rpx 16rpx;
background-color: #F6F7FB;
.service-detail-image {
width: 44rpx;
height: 44rpx;
border-radius: 50%;
margin-right: 8rpx;
}
.text {
font-size: 22rpx;
line-height: 22rpx;
color: #333333;
}
.text-right {
font-size: 22rpx;
color: #969696;
line-height: 22rpx;
text-align: right;
}
.icon {
width: 24rpx;
height: 24rpx;
margin-left: 8rpx;
}
}
.select-id-8 {
padding: 24rpx 16rpx;
.alipay-merchant-img {
width: 88rpx;
height: 88rpx;
margin-right: 30rpx;
}
.center-box {
.title {
font-size: 26rpx;
line-height: 32rpx;
color: #1A1A1A;
font-weight: 500;
margin-bottom: 16rpx;
}
.line {
width: 1rpx;
background-color: #969696;
margin: 0 10rpx;
height: 18rpx;
}
.text-option-item {
font-size: 22rpx;
color: #969696;
}
}
.right-btn {
border-radius: 25rpx;
border: 1rpx solid #043E77;
color: #043E77;
font-size: 22rpx;
padding: 10rpx 24rpx;
}
}
.recommend-service-box {
padding: 22rpx 16rpx;
background-color: #FFF5F6;
.text {
font-size: 26rpx;
line-height: 32rpx;
color: #9E2036;
font-weight: 500;
}
.text-right {
font-size: 24rpx;
line-height: 32rpx;
color: #9E2036;
}
}
.service-recommend-box {
background-color: #F6F7FB;
font-weight: 500;
.text {
color: #123D72;
font-size: 26rpx;
line-height: 32rpx;
}
}
}
.info-item-box {
display: flex;
align-items: center;
margin: 20rpx 0;
.item-label {
min-width: 160rpx;
margin-right: 40rpx;
font-size: 26rpx;
color: var(--text-secondary);
}
}
.bottom-border {
padding-bottom: 12px;
border-bottom: 0.5px solid #EDEDED;
}
.info-item-input {
display: inline-flex;
align-items: center;
position: relative;
min-width: 20px;
max-width: 100%;
width: auto;
overflow: hidden;
min-height: 20px;
/* 隐藏的测量元素 */
.text-measure {
white-space: nowrap;
color: var(--text-color);
font-family: inherit;
font-size: 28rpx;
width: auto;
min-width: 20px;
height: auto;
}
}
.more-box {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
font-size: 26rpx;
color: #969696;
.icon {
width: 24rpx;
height: 24rpx;
margin-left: 8rpx;
}
margin: 28rpx 0;
}
.bill-management {
margin: 16rpx 24rpx;
background-color: #ffffff;
padding: 24rpx;
border-radius: 24rpx;
.title {
font-weight: bold;
margin-bottom: 30rpx;
}
.bill-management-item {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 40rpx;
font-size: 26rpx;
color: #1a1a1a;
.tag-box {
justify-content: flex-end;
.tag-item {
background-color: #E6F2FF;
color: #2788D1;
font-size: 24rpx;
padding: 0 16rpx;
border-radius: 4rpx;
height: 52rpx;
line-height: 52rpx;
margin-right: 8rpx;
}
}
.remark-box {
justify-content: flex-end;
color: #969696;
font-size: 26rpx;
.text {
max-width: 280rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.remark-img {
width: 40rpx;
height: 40rpx;
margin-right: 8rpx;
}
}
.right-box {
color: #969696;
}
}
.bottom-icon-box {
margin-top: 8rpx;
.icon-item {
margin-top: 36rpx;
width: 50%;
font-size: 26rpx;
color: #12447A;
margin-bottom: 14rpx;
}
.bottom-icon {
width: 28rpx;
height: 28rpx;
margin-right: 18rpx;
}
}
.line {
height: 1px;
width: 100%;
background-color: #EDEDED;
transform: scaleY(0.5);
}
}
.top-box {
display: flex;
align-items: center;
justify-content: space-between;
border-radius: 16rpx;
padding: 22rpx 24rpx;
}
</style>

View File

@ -88,12 +88,20 @@
</view> </view>
</view> </view>
<view class="bill-list"> <view class="bill-list">
<BalanceList :isBalance="false" :list="item.list" /> <BalanceList :isBalance="false" :list="item.list" @longPress="onLongPress" @onBill="billClick" />
</view> </view>
</view> </view>
</scroll-view> </scroll-view>
</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> </template>
<script setup> <script setup>
@ -103,9 +111,13 @@ import BalanceList from '@/components/balance-list/balance-list.vue'
import { import {
reactive, reactive,
toRefs, toRefs,
onMounted,
ref, ref,
onMounted computed
} from 'vue' } from 'vue'
import {
useStore
} from '@/store/index.js'
import { import {
onShow, onShow,
} from '@dcloudio/uni-app' } from '@dcloudio/uni-app'
@ -174,151 +186,7 @@ const data = reactive({
outCome: 0, outCome: 0,
}, // }, //
cardPositions: [], // scroll-view cardPositions: [], // scroll-view
billList: [{ billList: []
year: '2025',
month: '12',
inCome: 999999999.00,
outCome: 999999999.00,
list: [{
orderId: '1234567890',
imgUrl: 'https://picsum.photos/200/200?random=1',
name: '测试',
amount: 999999999.00,
classification: '日荣百货',
isRefund: false,
isAdd: false,
money: "200",
time: '2025-12-27 18:18:18'
}, {
orderId: '1234567890',
imgUrl: 'https://picsum.photos/200/200?random=1',
name: '测试',
amount: 999999999.00,
classification: '日荣百货',
isRefund: true,
isAdd: false,
money: "200",
time: '2025-12-27 18:18:18'
}, {
orderId: '1234567890',
imgUrl: 'https://picsum.photos/200/200?random=1',
name: '测试',
amount: 999999999.00,
classification: '日荣百货',
isRefund: false,
isAdd: true,
money: "200",
time: '2025-12-27 18:18:18'
}]
}, {
year: '2025',
month: '11',
inCome: 999999999.00,
outCome: 999999999.00,
list: [{
orderId: '1234567890',
imgUrl: 'https://picsum.photos/200/200?random=1',
name: '测试',
amount: 999999999.00,
classification: '日荣百货',
isRefund: false,
isAdd: false,
money: "200",
time: '2025-12-27 18:18:18'
}, {
orderId: '1234567890',
imgUrl: 'https://picsum.photos/200/200?random=1',
name: '测试',
amount: 999999999.00,
classification: '日荣百货',
isRefund: true,
isAdd: false,
money: "200",
time: '2025-12-27 18:18:18'
}, {
orderId: '1234567890',
imgUrl: 'https://picsum.photos/200/200?random=1',
name: '测试',
amount: 999999999.00,
classification: '日荣百货',
isRefund: false,
isAdd: true,
money: "200",
time: '2025-12-27 18:18:18'
}]
}, {
year: '2025',
month: '10',
inCome: 999999999.00,
outCome: 999999999.00,
list: [{
orderId: '1234567890',
imgUrl: 'https://picsum.photos/200/200?random=1',
name: '测试',
amount: 999999999.00,
classification: '日荣百货',
isRefund: false,
isAdd: false,
money: "200",
time: '2025-12-27 18:18:18'
}, {
orderId: '1234567890',
imgUrl: 'https://picsum.photos/200/200?random=1',
name: '测试',
amount: 999999999.00,
classification: '日荣百货',
isRefund: true,
isAdd: false,
money: "200",
time: '2025-12-27 18:18:18'
}, {
orderId: '1234567890',
imgUrl: 'https://picsum.photos/200/200?random=1',
name: '测试',
amount: 999999999.00,
classification: '日荣百货',
isRefund: false,
isAdd: true,
money: "200",
time: '2025-12-27 18:18:18'
}]
}, {
year: '2025',
month: '9',
inCome: 999999999.00,
outCome: 999999999.00,
list: [{
orderId: '1234567890',
imgUrl: 'https://picsum.photos/200/200?random=1',
name: '测试',
amount: 999999999.00,
classification: '日荣百货',
isRefund: false,
isAdd: false,
money: "200",
time: '2025-12-27 18:18:18'
}, {
orderId: '1234567890',
imgUrl: 'https://picsum.photos/200/200?random=1',
name: '测试',
amount: 999999999.00,
classification: '日荣百货',
isRefund: true,
isAdd: false,
money: "200",
time: '2025-12-27 18:18:18'
}, {
orderId: '1234567890',
imgUrl: 'https://picsum.photos/200/200?random=1',
name: '测试',
amount: 999999999.00,
classification: '日荣百货',
isRefund: false,
isAdd: true,
money: "200",
time: '2025-12-27 18:18:18'
}]
}]
}) })
let { let {
@ -327,11 +195,101 @@ let {
currentFilterType currentFilterType
} = toRefs(data) } = toRefs(data)
const {
getBillList,
deleteBill
} = useStore()
const contextMenu = reactive({
visible: false,
x: 0,
y: 0,
item: null
})
onShow(() => { onShow(() => {
// //
getBillDataList()
// Close menu if open
closeContextMenu()
//
updateStatusBarHeight() updateStatusBarHeight()
}) })
/**
* 获取账单列表
*/
const getBillDataList = () => {
const allBillList = getBillList()
//
const groupList = []
allBillList.forEach(item => {
// itemInfoListcreateTime
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: createTime
}
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.time) - new Date(a.time))
})
data.billList = groupList
}
onMounted(() => { onMounted(() => {
// //
getCardPositions() getCardPositions()
@ -347,6 +305,12 @@ const updateStatusBarHeight = () => {
}) })
} }
const billClick = (item) => {
uni.navigateTo({
url: '/pages/bill/bill-detail/bill-detail?id=' + item.id
})
}
/** /**
* 获取所有卡片相对于 scroll-view 顶部的距离 * 获取所有卡片相对于 scroll-view 顶部的距离
*/ */
@ -415,6 +379,71 @@ const scrollList = (e) => {
const clickTitlePopupButton = (button) => { const clickTitlePopupButton = (button) => {
button.click() 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> </script>
<style> <style>
@ -424,6 +453,39 @@ page {
background-color: #F5F5F5; background-color: #F5F5F5;
} }
</style> </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> <style lang="less" scoped>
.nav-bar-search { .nav-bar-search {
background-color: #ffffff; background-color: #ffffff;

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 788 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 881 B

After

Width:  |  Height:  |  Size: 788 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1009 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 287 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 286 B

View File

@ -32,6 +32,14 @@
"key": "productDescription", "key": "productDescription",
"required": true "required": true
}, },
{
"label": "支付奖励",
"value": 10,
"type": "number",
"focus": false,
"key": "paymentReward",
"required": true
},
{ {
"label": "收单机构", "label": "收单机构",
"value": "支付宝支付科技有限公司", "value": "支付宝支付科技有限公司",
@ -47,22 +55,6 @@
"focus": false, "focus": false,
"key": "receiverFullName", "key": "receiverFullName",
"required": true "required": true
},
{
"label": "账单分类",
"value": "日用百货",
"type": "select",
"focus": false,
"key": "billCategory",
"required": true
},
{
"label": "标签和备注",
"value": "",
"type": "text",
"focus": false,
"key": "tagAndNote",
"required": false
} }
], ],
"defaultBottomIcons": [ "defaultBottomIcons": [
@ -96,6 +88,14 @@
"key": "payMethod", "key": "payMethod",
"required": true "required": true
}, },
{
"label": "支付奖励",
"value": 10,
"type": "number",
"focus": false,
"key": "paymentReward",
"required": true
},
{ {
"label": "商品说明", "label": "商品说明",
"value": "", "value": "",
@ -111,22 +111,6 @@
"focus": false, "focus": false,
"key": "receiverFullName", "key": "receiverFullName",
"required": true "required": true
},
{
"label": "账单分类",
"value": "日用百货",
"type": "select",
"focus": false,
"key": "billCategory",
"required": true
},
{
"label": "标签和备注",
"value": "",
"type": "text",
"focus": false,
"key": "tagAndNote",
"required": false
} }
], ],
"defaultBottomIcons": [ "defaultBottomIcons": [
@ -169,6 +153,14 @@
"key": "productDescription", "key": "productDescription",
"required": true "required": true
}, },
{
"label": "支付奖励",
"value": 10,
"type": "number",
"focus": false,
"key": "paymentReward",
"required": true
},
{ {
"label": "收单机构", "label": "收单机构",
"value": "支付宝支付科技有限公司", "value": "支付宝支付科技有限公司",
@ -184,22 +176,6 @@
"focus": false, "focus": false,
"key": "clearingOrganization", "key": "clearingOrganization",
"required": true "required": true
},
{
"label": "账单分类",
"value": "餐饮美食",
"type": "select",
"focus": false,
"key": "billCategory",
"required": true
},
{
"label": "标签和备注",
"value": "",
"type": "text",
"focus": false,
"key": "tagAndNote",
"required": false
} }
], ],
"defaultBottomIcons": [ "defaultBottomIcons": [
@ -241,20 +217,12 @@
"required": true "required": true
}, },
{ {
"label": "账单分类", "label": "支付奖励",
"value": "信用借还", "value": 10,
"type": "select", "type": "number",
"focus": false, "focus": false,
"key": "billCategory", "key": "paymentReward",
"required": true "required": true
},
{
"label": "标签和备注",
"value": "",
"type": "text",
"focus": false,
"key": "tagAndNote",
"required": false
} }
], ],
"defaultBottomIcons": [ "defaultBottomIcons": [
@ -279,7 +247,7 @@
}, },
{ {
"label": "转账备注", "label": "转账备注",
"value": "", "value": "报销",
"type": "text", "type": "text",
"focus": false, "focus": false,
"key": "transferNote", "key": "transferNote",
@ -300,22 +268,6 @@
"focus": false, "focus": false,
"key": "orderNumber", "key": "orderNumber",
"required": true "required": true
},
{
"label": "账单分类",
"value": "转账红包",
"type": "select",
"focus": false,
"key": "billCategory",
"required": true
},
{
"label": "标签和备注",
"value": "",
"type": "text",
"focus": false,
"key": "tagAndNote",
"required": false
} }
], ],
"defaultBottomIcons": [ "defaultBottomIcons": [
@ -342,7 +294,7 @@
}, },
{ {
"label": "转账备注", "label": "转账备注",
"value": "", "value": "转账",
"type": "text", "type": "text",
"focus": false, "focus": false,
"key": "transferNote", "key": "transferNote",
@ -355,22 +307,6 @@
"focus": false, "focus": false,
"key": "counterAccount", "key": "counterAccount",
"required": true "required": true
},
{
"label": "账单分类",
"value": "转账红包",
"type": "select",
"focus": false,
"key": "billCategory",
"required": true
},
{
"label": "标签和备注",
"value": "",
"type": "text",
"focus": false,
"key": "tagAndNote",
"required": false
} }
], ],
"defaultBottomIcons": [ "defaultBottomIcons": [
@ -411,22 +347,6 @@
"focus": false, "focus": false,
"key": "counterAccount", "key": "counterAccount",
"required": true "required": true
},
{
"label": "账单分类",
"value": "转账红包",
"type": "select",
"focus": false,
"key": "billCategory",
"required": true
},
{
"label": "标签和备注",
"value": "",
"type": "text",
"focus": false,
"key": "tagAndNote",
"required": false
} }
], ],
"defaultBottomIcons": [ "defaultBottomIcons": [
@ -469,22 +389,6 @@
"focus": false, "focus": false,
"key": "productDescription", "key": "productDescription",
"required": true "required": true
},
{
"label": "账单分类",
"value": "收入",
"type": "select",
"focus": false,
"key": "billCategory",
"required": true
},
{
"label": "标签和备注",
"value": "",
"type": "text",
"focus": false,
"key": "tagAndNote",
"required": false
} }
], ],
"defaultBottomIcons": [ "defaultBottomIcons": [
@ -525,7 +429,7 @@
"value": "", "value": "",
"type": "text", "type": "text",
"focus": false, "focus": false,
"key": "refundMethod", "key": "payMethod",
"required": true "required": true
}, },
{ {
@ -535,22 +439,6 @@
"focus": false, "focus": false,
"key": "productDescription", "key": "productDescription",
"required": true "required": true
},
{
"label": "账单分类",
"value": "退款",
"type": "select",
"focus": false,
"key": "billCategory",
"required": true
},
{
"label": "标签和备注",
"value": "",
"type": "text",
"focus": false,
"key": "tagAndNote",
"required": false
} }
], ],
"defaultBottomIcons": [ "defaultBottomIcons": [
@ -599,22 +487,6 @@
"focus": false, "focus": false,
"key": "receiverFullName", "key": "receiverFullName",
"required": true "required": true
},
{
"label": "账单分类",
"value": "充值缴费",
"type": "select",
"focus": false,
"key": "billCategory",
"required": true
},
{
"label": "标签和备注",
"value": "",
"type": "text",
"focus": false,
"key": "tagAndNote",
"required": false
} }
], ],
"defaultBottomIcons": [ "defaultBottomIcons": [
@ -673,22 +545,6 @@
"focus": false, "focus": false,
"key": "orderNumber", "key": "orderNumber",
"required": true "required": true
},
{
"label": "账单分类",
"value": "充值缴费",
"type": "select",
"focus": false,
"key": "billCategory",
"required": true
},
{
"label": "标签和备注",
"value": "",
"type": "text",
"focus": false,
"key": "tagAndNote",
"required": false
} }
], ],
"defaultBottomIcons": [ "defaultBottomIcons": [
@ -729,22 +585,6 @@
"key": "payMethod", "key": "payMethod",
"required": true "required": true
}, },
{
"label": "账单分类",
"value": "日用百货",
"type": "select",
"focus": false,
"key": "billCategory",
"required": true
},
{
"label": "标签和备注",
"value": "",
"type": "text",
"focus": false,
"key": "tagAndNote",
"required": false
},
{ {
"label": "交易详情", "label": "交易详情",
"value": { "value": {
@ -797,6 +637,14 @@
"key": "productDescription", "key": "productDescription",
"required": true "required": true
}, },
{
"label": "支付奖励",
"value": 10,
"type": "number",
"focus": false,
"key": "paymentReward",
"required": true
},
{ {
"label": "收款方全称", "label": "收款方全称",
"value": "", "value": "",
@ -804,22 +652,6 @@
"focus": false, "focus": false,
"key": "receiverFullName", "key": "receiverFullName",
"required": true "required": true
},
{
"label": "账单分类",
"value": "日用百货",
"type": "select",
"focus": false,
"key": "billCategory",
"required": true
},
{
"label": "标签和备注",
"value": "",
"type": "text",
"focus": false,
"key": "tagAndNote",
"required": false
} }
], ],
"defaultBottomIcons": [ "defaultBottomIcons": [
@ -959,11 +791,33 @@
} }
], ],
"billBottomIconList": [ "billBottomIconList": [
{
"id": 4,
"icon": "lianxishoukuanfang",
"name": "联系收款方",
"hideIncloudId": 8
},
{
"id": 8,
"icon": "lianxishangjia",
"name": "联系商家",
"hideIncloudId": 4
},
{
"id": 7,
"icon": "chakanjiaofeijindu",
"name": "查看缴费进度"
},
{ {
"id": 0, "id": 0,
"icon": "chakanwanglaijilu", "icon": "chakanwanglaijilu",
"name": "查看往来记录" "name": "查看往来记录"
}, },
{
"id": 5,
"icon": "AAshoukuan",
"name": "AA收款"
},
{ {
"id": 1, "id": 1,
"icon": "wanglailiushuizhengming", "icon": "wanglailiushuizhengming",
@ -974,40 +828,20 @@
"icon": "shenqingdianzihuidan", "icon": "shenqingdianzihuidan",
"name": "申请电子回单" "name": "申请电子回单"
}, },
{
"id": 3,
"icon": "duidingdanyouyiwen",
"name": "对订单有疑问"
},
{
"id": 7,
"icon": "chakanjiaofeijindu",
"name": "查看缴费进度"
},
{
"id": 4,
"icon": "lianxishoukuanfang",
"name": "联系收款方"
},
{ {
"id": 9, "id": 9,
"icon": "lianxifukuanfang", "icon": "lianxifukuanfang",
"name": "联系付款方" "name": "联系付款方"
}, },
{
"id": 3,
"icon": "duidingdanyouyiwen",
"name": "对订单有疑问"
},
{ {
"id": 6, "id": 6,
"icon": "zhuanzhangpingzheng", "icon": "zhuanzhangpingzheng",
"name": "转账凭证" "name": "转账凭证"
},
{
"id": 8,
"icon": "lianxishangjia",
"name": "联系商家"
},
{
"id": 5,
"icon": "AAshoukuan",
"name": "AA收款"
} }
] ]
} }

69
static/json/initial.json Normal file
View File

@ -0,0 +1,69 @@
{
"fastEntranceList": [
{
"id": 1,
"name": "转账",
"label": "zhuanzhang"
},
{
"id": 2,
"name": "银行卡",
"label": "yinhangka"
},
{
"id": 3,
"name": "亲情卡",
"label": "qinqingka"
},
{
"id": 4,
"name": "搭搭哒",
"label": "dadada"
},
{
"id": 5,
"name": "小荷包",
"label": "xiaohebao"
},
{
"id": 6,
"name": "专用金",
"label": "zhuanyongjin"
},
{
"id": 7,
"name": "定时充值",
"label": "dingshichongzhi"
},
{
"id": 8,
"name": "红包",
"label": "hongbao"
},
{
"id": 9,
"name": "零花钱",
"label": "linghuaqian"
},
{
"id": 10,
"name": "免费额度",
"label": "mianfeiedu"
},
{
"id": 11,
"name": "专用资金",
"label": "zhuanyongzijin"
},
{
"id": 12,
"name": "备用余额",
"label": "beiyongyue"
},
{
"id": 13,
"name": "信用卡还款",
"label": "xinyongkahuankuan"
}
]
}

View File

@ -22,12 +22,8 @@ export const store = reactive({
windowWidth: 0 windowWidth: 0
}, },
// 示例数据:待办事项 // 账单列表
todos: storage.get('todos') || [], billList: storage.get('bill_list') || [],
// 示例数据:商品列表
products: storage.get('products') || [],
}); });
@ -47,66 +43,33 @@ export const useStore = () => {
store.settings = { ...store.settings, ...settings }; store.settings = { ...store.settings, ...settings };
}; };
// 待办事项相关操作 // 账单相关操作
const addTodo = (todo) => { const addBill = (bill) => {
const newTodo = { store.billList.unshift(bill);
id: Date.now(), return bill;
...todo,
completed: false,
createdAt: new Date().toISOString()
};
store.todos.push(newTodo);
return newTodo;
}; };
const updateTodo = (id, updates) => { const updateBill = (id, updates) => {
const index = store.todos.findIndex(todo => todo.id === id); const index = store.billList.findIndex(bill => bill.id === id);
if (index !== -1) { if (index !== -1) {
store.todos[index] = { ...store.todos[index], ...updates }; store.billList[index] = { ...store.billList[index], ...updates };
return store.todos[index]; return store.billList[index];
} }
return null; return null;
}; };
const deleteTodo = (id) => { const deleteBill = (id) => {
const index = store.todos.findIndex(todo => todo.id === id); const index = store.billList.findIndex(bill => bill.id === id);
if (index !== -1) { if (index !== -1) {
store.todos.splice(index, 1); store.billList.splice(index, 1);
return true; return true;
} }
return false; return false;
}; };
// 商品相关操作 const getBillList = () => {
const addProduct = (product) => { return store.billList
const newProduct = { }
id: Date.now(),
...product,
createdAt: new Date().toISOString()
};
store.products.push(newProduct);
return newProduct;
};
const updateProduct = (id, updates) => {
const index = store.products.findIndex(product => product.id === id);
if (index !== -1) {
store.products[index] = { ...store.products[index], ...updates };
return store.products[index];
}
return null;
};
const deleteProduct = (id) => {
const index = store.products.findIndex(product => product.id === id);
if (index !== -1) {
store.products.splice(index, 1);
return true;
}
return false;
};
// 数据持久化 // 数据持久化
// 监听store变化自动保存到本地存储 // 监听store变化自动保存到本地存储
@ -123,19 +86,11 @@ export const useStore = () => {
); );
watch( watch(
() => store.todos, () => store.billList,
(newValue) => storage.set('todos', newValue), (newValue) => storage.set('bill_list', newValue),
{ deep: true } { deep: true }
); );
watch(
() => store.products,
(newValue) => storage.set('products', newValue),
{ deep: true }
);
// 获取系统信息 // 获取系统信息
const getSystemInfo = () => { const getSystemInfo = () => {
uni.getSystemInfo({ uni.getSystemInfo({
@ -155,12 +110,10 @@ export const useStore = () => {
setUserInfo, setUserInfo,
clearUserInfo, clearUserInfo,
updateSettings, updateSettings,
addTodo, addBill,
updateTodo, updateBill,
deleteTodo, deleteBill,
addProduct, getBillList,
updateProduct,
deleteProduct,
getSystemInfo getSystemInfo
}; };
}; };

View File

@ -378,6 +378,15 @@ export const util = {
uni.navigateTo({ uni.navigateTo({
url url
}); });
},
/**
* 点击标题弹出按钮
* @param e
*/
clickTitlePopupButton(button) {
button.click()
} }
}; };

View File

@ -52,32 +52,5 @@ export const storage = {
console.error('清空数据失败:', error); console.error('清空数据失败:', error);
return false; return false;
} }
},
// 批量保存
setMultiple(dataObj) {
try {
Object.keys(dataObj).forEach(key => {
this.set(key, dataObj[key]);
});
return true;
} catch (error) {
console.error('批量存储失败:', error);
return false;
}
},
// 批量获取
getMultiple(keys) {
try {
const result = {};
keys.forEach(key => {
result[key] = this.get(key);
});
return result;
} catch (error) {
console.error('批量获取失败:', error);
return {};
}
} }
}; };