完成账单列表,余额页面交互

This commit is contained in:
tangxinyue 2025-12-29 18:32:10 +08:00
parent a9b52f0d5c
commit ebcde6511d
25 changed files with 2142 additions and 1611 deletions

View File

@ -13,10 +13,5 @@
</script>
<style lang="less">
/*每个页面公共css */
/* #ifdef VUE */
/* 只在vue页面生效的样式nvue页面不使用 */
@import './common/main.css';
@import './common/layouts.less';
/* #endif */
</style>
// @import './common/layouts.less';</style>

View File

@ -1,108 +0,0 @@
// layouts.less 使用示例
// 这个文件展示了如何在页面中使用 common/layouts.less 中的布局方法
// 示例1: 使用Flex布局
.example-flex {
.flex-between; // 使用两端对齐布局
padding: 20px;
background-color: #f5f5f5;
.item {
.flex-center; // 使用居中布局
width: 100px;
height: 100px;
background-color: #007aff;
color: white;
border-radius: 8px;
}
}
// 示例2: 使用Grid布局
.example-grid {
.grid-responsive(4, 16px); // 使用响应式网格4列间距16px
padding: 20px;
.item {
height: 120px;
background-color: #ff9500;
border-radius: 8px;
&:nth-child(1) {
.grid-col(2); // 占据2列
}
}
}
// 示例3: 使用卡片布局
.example-card {
max-width: 400px;
margin: 20px auto;
.card(); // 使用基础卡片样式
.card-header {
font-size: 18px;
font-weight: bold;
}
.card-body {
.flex-column;
gap: 16px;
}
.card-footer {
.flex-between;
}
}
// 示例4: 使用响应式布局
.example-responsive {
display: flex;
gap: 20px;
padding: 20px;
.desktop-only {
.hidden-sm; // 在小屏幕上隐藏
width: 200px;
height: 200px;
background-color: #4cd964;
}
.mobile-only {
.visible-sm; // 只在小屏幕上显示
width: 100%;
height: 100px;
background-color: #ff3b30;
}
}
// 示例5: 使用间距工具类
.example-spacing {
.m-4; // margin: 16px
.p-6; // padding: 24px
background-color: #e9e9eb;
.child {
.mt-3; // margin-top: 12px
.mb-3; // margin-bottom: 12px
.px-4; // padding-left: 16px; padding-right: 16px
background-color: #ffffff;
}
}
// 示例6: 使用页面布局
.example-page {
.page-with-header-footer;
.page-header {
.navbar-top;
}
.page-content {
.container-responsive;
}
.page-footer {
.navbar-bottom;
}
}

View File

@ -1,446 +0,0 @@
// 布局专用LESS文件
// 包含常用的布局混合宏和样式
// ------------------------------
// 容器布局
// ------------------------------
// 全屏容器
.full-screen {
width: 100%;
height: 100vh;
}
// 固定宽度容器
.container-fixed(@width: 1200px) {
width: @width;
margin: 0 auto;
padding: 0 20px;
}
// 响应式容器
.container-responsive {
width: 100%;
margin: 0 auto;
padding: 0 20px;
max-width: 1200px;
}
// ------------------------------
// Flex布局混合宏
// ------------------------------
// 基础Flex布局
.flex(@direction: row; @wrap: nowrap; @justify: flex-start; @align: stretch) {
display: flex;
flex-direction: @direction;
flex-wrap: @wrap;
justify-content: @justify;
align-items: @align;
}
// 居中布局
.flex-center {
.flex(row, nowrap, center, center);
}
// 两端对齐
.flex-between {
.flex(row, nowrap, space-between, center);
}
// 顶部对齐
.flex-start {
.flex(row, nowrap, flex-start, flex-start);
}
// 底部对齐
.flex-end {
.flex(row, nowrap, flex-end, flex-end);
}
// 垂直居中
.flex-middle {
.flex(row, nowrap, flex-start, center);
}
// 垂直布局
.flex-column {
.flex(column, nowrap, flex-start, stretch);
}
// 垂直居中布局
.flex-column-center {
.flex(column, nowrap, center, center);
}
// 垂直两端对齐
.flex-column-between {
.flex(column, nowrap, space-between, stretch);
}
// ------------------------------
// 网格布局
// ------------------------------
// 基础网格容器
.grid(@columns: 12; @gap: 20px) {
display: grid;
grid-template-columns: repeat(@columns, 1fr);
grid-gap: @gap;
gap: @gap;
}
// 响应式网格
.grid-responsive(@columns: 12; @gap: 20px) {
.grid(@columns, @gap);
@media (max-width: 768px) {
grid-template-columns: repeat(6, 1fr);
}
@media (max-width: 480px) {
grid-template-columns: repeat(3, 1fr);
}
}
// 网格列宽
.grid-col(@span: 1) {
grid-column: span @span;
}
// 网格行高
.grid-row(@span: 1) {
grid-row: span @span;
}
// ------------------------------
// 定位布局
// ------------------------------
// 绝对定位
.position-absolute(@top: auto; @right: auto; @bottom: auto; @left: auto) {
position: absolute;
top: @top;
right: @right;
bottom: @bottom;
left: @left;
}
// 固定定位
.position-fixed(@top: auto; @right: auto; @bottom: auto; @left: auto) {
position: fixed;
top: @top;
right: @right;
bottom: @bottom;
left: @left;
}
// 粘性定位
.position-sticky(@top: 0; @z-index: 10) {
position: sticky;
top: @top;
z-index: @z-index;
}
// 居中定位(基于父容器)
.position-center {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
// 水平居中定位
.position-center-x {
position: absolute;
left: 50%;
transform: translateX(-50%);
}
// 垂直居中定位
.position-center-y {
position: absolute;
top: 50%;
transform: translateY(-50%);
}
// ------------------------------
// 间距布局
// ------------------------------
// 外边距
.margin(@top: 0; @right: @top; @bottom: @top; @left: @right) {
margin: @top @right @bottom @left;
}
// 内边距
.padding(@top: 0; @right: @top; @bottom: @top; @left: @right) {
padding: @top @right @bottom @left;
}
// 间距工具类基于4px网格系统
.spacing-utilities(@prefix: m; @property: margin) {
.loop(@i) when (@i <= 12) {
.@{prefix}-@{i} {
@{property}: @i * 4px;
}
.@{prefix}t-@{i} {
@{property}-top: @i * 4px;
}
.@{prefix}r-@{i} {
@{property}-right: @i * 4px;
}
.@{prefix}b-@{i} {
@{property}-bottom: @i * 4px;
}
.@{prefix}l-@{i} {
@{property}-left: @i * 4px;
}
.loop(@i + 1);
}
.loop(0);
}
// 生成外边距工具类
.spacing-utilities(m, margin);
// 生成内边距工具类
.spacing-utilities(p, padding);
// ------------------------------
// 卡片布局
// ------------------------------
// 基础卡片
.card(@radius: 8px; @shadow: 0 2px 8px rgba(0, 0, 0, 0.1); @bg: #fff) {
background-color: @bg;
border-radius: @radius;
box-shadow: @shadow;
overflow: hidden;
}
// 带边框的卡片
.card-bordered(@radius: 8px; @border-color: #e5e5e5; @bg: #fff) {
.card(@radius, none, @bg);
border: 1px solid @border-color;
}
// 卡片头部
.card-header {
padding: 16px 20px;
border-bottom: 1px solid #e5e5e5;
}
// 卡片主体
.card-body {
padding: 20px;
}
// 卡片底部
.card-footer {
padding: 16px 20px;
border-top: 1px solid #e5e5e5;
background-color: #fafafa;
}
// ------------------------------
// 导航栏布局
// ------------------------------
// 基础导航栏
.navbar(@height: 60px; @bg-color: #fff; @text-color: #333) {
height: @height;
background-color: @bg-color;
color: @text-color;
display: flex;
align-items: center;
padding: 0 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
// 顶部导航栏
.navbar-top {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 1000;
.navbar();
}
// 底部导航栏
.navbar-bottom {
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 1000;
.navbar();
border-top: 1px solid #e5e5e5;
}
// ------------------------------
// 页面布局
// ------------------------------
// 基础页面布局
.page-layout {
min-height: 100vh;
display: flex;
flex-direction: column;
}
// 带头部和底部的页面布局
.page-with-header-footer {
.page-layout();
.page-header {
flex-shrink: 0;
}
.page-content {
flex: 1;
padding: 20px;
overflow-y: auto;
}
.page-footer {
flex-shrink: 0;
}
}
// ------------------------------
// 响应式布局
// ------------------------------
// 断点定义
@breakpoint-sm: 480px;
@breakpoint-md: 768px;
@breakpoint-lg: 1024px;
@breakpoint-xl: 1200px;
// 媒体查询混合宏
.media-sm(@rules) {
@media (max-width: @breakpoint-sm) {
@rules();
}
}
.media-md(@rules) {
@media (max-width: @breakpoint-md) {
@rules();
}
}
.media-lg(@rules) {
@media (max-width: @breakpoint-lg) {
@rules();
}
}
.media-xl(@rules) {
@media (max-width: @breakpoint-xl) {
@rules();
}
}
// 响应式显示/隐藏
.hidden-sm {
.media-sm({
display: none;
});
}
.visible-sm {
display: none;
.media-sm({
display: block;
});
}
.hidden-md {
.media-md({
display: none;
});
}
.visible-md {
display: none;
.media-md({
display: block;
});
}
// ------------------------------
// 其他布局工具
// ------------------------------
// 清除浮动
.clearfix {
&::after {
content: '';
display: table;
clear: both;
}
}
// 相对定位
.relative {
position: relative;
}
// 绝对定位
.absolute {
position: absolute;
}
// 固定定位
.fixed {
position: fixed;
}
// 粘性定位
.sticky {
position: sticky;
}
// 隐藏溢出
.overflow-hidden {
overflow: hidden;
}
// 自动溢出
.overflow-auto {
overflow: auto;
}
// 水平滚动
.overflow-x-auto {
overflow-x: auto;
overflow-y: hidden;
}
// 垂直滚动
.overflow-y-auto {
overflow-y: auto;
overflow-x: hidden;
}
// 全屏高度
.full-height {
height: 100%;
min-height: 100vh;
}
// 全屏宽度
.full-width {
width: 100%;
max-width: 100%;
}

View File

@ -10,11 +10,20 @@
/* 基础样式 */
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
/* font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
"Helvetica Neue", Arial, sans-serif; */
font-size: 14px;
line-height: 1.5;
color: #333;
background-color: #f5f5f5;
color: var(--text-color);
background-color: var(--page-bg-color);
}
.w100 {
width: 100%
}
.h100 {
height: 100%;
}
/* 常用颜色变量 */
@ -25,11 +34,13 @@ body {
--warning-color: #ff9500;
--error-color: #ff3b30;
--text-primary: #333;
--text-secondary: #666;
--text-secondary: #969696;
--text-tertiary: #999;
--bg-primary: #fff;
--bg-primary: #ffffff;
--bg-secondary: #f5f5f5;
--border-color: #e5e5e5;
--border-color: #D8D8D8;
--page-bg-color: #f0f3f8;
--footer-text-color: #CBCED3;
}
/* 文本样式 */
@ -74,65 +85,205 @@ body {
}
/* 间距工具类 */
.m-0 { margin: 0; }
.m-1 { margin: 4px; }
.m-2 { margin: 8px; }
.m-3 { margin: 16px; }
.m-4 { margin: 24px; }
.m-0 {
margin: 0;
}
.mt-0 { margin-top: 0; }
.mt-1 { margin-top: 4px; }
.mt-2 { margin-top: 8px; }
.mt-3 { margin-top: 16px; }
.mt-4 { margin-top: 24px; }
.m-1 {
margin: 4px;
}
.mb-0 { margin-bottom: 0; }
.mb-1 { margin-bottom: 4px; }
.mb-2 { margin-bottom: 8px; }
.mb-3 { margin-bottom: 16px; }
.mb-4 { margin-bottom: 24px; }
.m-2 {
margin: 8px;
}
.ml-0 { margin-left: 0; }
.ml-1 { margin-left: 4px; }
.ml-2 { margin-left: 8px; }
.ml-3 { margin-left: 16px; }
.ml-4 { margin-left: 24px; }
.m-3 {
margin: 16px;
}
.mr-0 { margin-right: 0; }
.mr-1 { margin-right: 4px; }
.mr-2 { margin-right: 8px; }
.mr-3 { margin-right: 16px; }
.mr-4 { margin-right: 24px; }
.m-4 {
margin: 24px;
}
.p-0 { padding: 0; }
.p-1 { padding: 4px; }
.p-2 { padding: 8px; }
.p-3 { padding: 16px; }
.p-4 { padding: 24px; }
.mt-0 {
margin-top: 0;
}
.pt-0 { padding-top: 0; }
.pt-1 { padding-top: 4px; }
.pt-2 { padding-top: 8px; }
.pt-3 { padding-top: 16px; }
.pt-4 { padding-top: 24px; }
.mt-1 {
margin-top: 4px;
}
.pb-0 { padding-bottom: 0; }
.pb-1 { padding-bottom: 4px; }
.pb-2 { padding-bottom: 8px; }
.pb-3 { padding-bottom: 16px; }
.pb-4 { padding-bottom: 24px; }
.mt-2 {
margin-top: 8px;
}
.pl-0 { padding-left: 0; }
.pl-1 { padding-left: 4px; }
.pl-2 { padding-left: 8px; }
.pl-3 { padding-left: 16px; }
.pl-4 { padding-left: 24px; }
.mt-3 {
margin-top: 16px;
}
.pr-0 { padding-right: 0; }
.pr-1 { padding-right: 4px; }
.pr-2 { padding-right: 8px; }
.pr-3 { padding-right: 16px; }
.pr-4 { padding-right: 24px; }
.mt-4 {
margin-top: 24px;
}
.mb-0 {
margin-bottom: 0;
}
.mb-1 {
margin-bottom: 4px;
}
.mb-2 {
margin-bottom: 8px;
}
.mb-3 {
margin-bottom: 16px;
}
.mb-4 {
margin-bottom: 24px;
}
.ml-0 {
margin-left: 0;
}
.ml-1 {
margin-left: 4px;
}
.ml-2 {
margin-left: 8px;
}
.ml-3 {
margin-left: 16px;
}
.ml-4 {
margin-left: 24px;
}
.mr-0 {
margin-right: 0;
}
.mr-1 {
margin-right: 4px;
}
.mr-2 {
margin-right: 8px;
}
.mr-3 {
margin-right: 16px;
}
.mr-4 {
margin-right: 24px;
}
.p-0 {
padding: 0;
}
.p-1 {
padding: 4px;
}
.p-2 {
padding: 8px;
}
.p-3 {
padding: 16px;
}
.p-4 {
padding: 24px;
}
.pt-0 {
padding-top: 0;
}
.pt-1 {
padding-top: 4px;
}
.pt-2 {
padding-top: 8px;
}
.pt-3 {
padding-top: 16px;
}
.pt-4 {
padding-top: 24px;
}
.pb-0 {
padding-bottom: 0;
}
.pb-1 {
padding-bottom: 4px;
}
.pb-2 {
padding-bottom: 8px;
}
.pb-3 {
padding-bottom: 16px;
}
.pb-4 {
padding-bottom: 24px;
}
.pl-0 {
padding-left: 0;
}
.pl-1 {
padding-left: 4px;
}
.pl-2 {
padding-left: 8px;
}
.pl-3 {
padding-left: 16px;
}
.pl-4 {
padding-left: 24px;
}
.pr-0 {
padding-right: 0;
}
.pr-1 {
padding-right: 4px;
}
.pr-2 {
padding-right: 8px;
}
.pr-3 {
padding-right: 16px;
}
.pr-4 {
padding-right: 24px;
}
/* 布局工具类 */
.flex {
@ -169,35 +320,13 @@ body {
flex-wrap: wrap;
}
.flex-1 {
flex: 1;
}
/* 边框样式 */
.border {
border: 1px solid var(--border-color);
}
.border-top {
border-top: 1px solid var(--border-color);
}
.border-bottom {
border-bottom: 1px solid var(--border-color);
}
.border-left {
border-left: 1px solid var(--border-color);
}
.border-right {
border-right: 1px solid var(--border-color);
}
.rounded {
border-radius: 4px;
}
.rounded-lg {
border-radius: 8px;
}
/* 圆角 */
.rounded-full {
border-radius: 50%;
}
@ -227,22 +356,6 @@ body {
background-color: var(--error-color);
}
/* 尺寸工具类 */
.w-full {
width: 100%;
}
.h-full {
height: 100%;
}
.w-auto {
width: auto;
}
.h-auto {
height: auto;
}
/* 隐藏元素 */
.hidden {
@ -254,15 +367,6 @@ body {
display: block;
}
/* 溢出处理 */
.overflow-hidden {
overflow: hidden;
}
.overflow-auto {
overflow: auto;
}
/* 文本截断 */
.text-ellipsis {
overflow: hidden;
@ -270,8 +374,36 @@ body {
white-space: nowrap;
}
/* 透明度 */
.opacity-100 { opacity: 1; }
.opacity-75 { opacity: 0.75; }
.opacity-50 { opacity: 0.5; }
.opacity-25 { opacity: 0.25; }
/* 字体 */
.font-w500 {
font-weight: 500;
}
@font-face {
font-family: "alipayNumber";
src: url("/static/font/AlipayNumber.ttf") format("truetype");
}
@font-face {
font-family: "wxNumberRegular";
src: url("/static/font/WeChatSansStd-Regular.otf");
}
@font-face {
font-family: "wxNumberMedium";
src: url("/static/font/WeChatSansStd-Medium.otf");
}
.alipay-font {
font-family: "alipayNumber";
}
.wx-font-regular {
font-family: "wxNumberRegular";
}
.wx-font-medium {
font-family: "wxNumberMedium";
}

View File

@ -0,0 +1,144 @@
<template>
<view class="balance-list-container">
<view>
<view v-for="item in props.list" :key="item.name" class="balance-item"
:class="{ 'flex-align-center': isBalance }">
<image class="img" :src="item.imgUrl" mode="aspectFill"></image>
<view class="balance-item-text-container">
<view class="balance-item-text">
<view>
<text class="name">{{ item.name }}</text>
</view>
<view v-if="item.classification">
<text class="time secondary">{{ item.classification }}</text>
</view>
<view>
<text class="time secondary">{{ item.time }}</text>
</view>
</view>
<view class="balance-item-text text-right" :class="{ 'flex-between': isBalance }">
<view class="money alipay-font"
:class="item.isAdd ? (isBalance ? 'add-color' : 'red-add-color') : 'minus-color'">
{{ item.isAdd ? '+' : '-' }}{{ Number(item.money).toFixed(2) }}
</view>
<view v-if="item.isRefund" class="refund">已全额退款</view>
<view v-if="isBalance" class="balance secondary">余额 <text class="balance-text">{{
Number(item.balance).toFixed(2)
}}</text></view>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup>
import {
defineProps,
defineEmits,
onMounted,
reactive
} from 'vue'
//
const props = defineProps({
list: {
type: Array,
default: () => []
},
isBalance: {
type: Boolean,
default: true
}
})
//
const emit = defineEmits(['onBill'])
const data = reactive({})
onMounted(() => {})
</script>
<style>
/* @import '../../common/main.css'; */
</style>
<style lang="less">
.balance-list-container {
width: 100%;
}
.balance-item {
display: flex;
width: 100%;
flex-direction: row;
padding: 0;
.balance-item-text-container {
flex: 1;
display: flex;
justify-content: space-between;
margin-left: 12px;
box-shadow: 0 0.3px 0 0 #F0F0EE;
padding: 12px 12px 12px 0;
}
.img {
margin: 12px 0;
width: 30px;
height: 30px;
border-radius: 50%;
margin-left: 12px;
}
.balance-item-text {
display: flex;
flex-direction: column;
// justify-content: space-between;
.name {
color: #343434;
}
.secondary {
color: var(--text-secondary);
font-size: 12px;
}
.money {
font-size: 17px;
font-weight: 500;
}
.add-color {
color: #F6610F;
}
.red-add-color {
color: #F53646;
}
.minus-color {
color: #333333;
}
.balance-text {
margin-right: 2px;
}
}
.refund {
color: #EA6B48;
font-size: 12px;
}
.text-right {
align-items: flex-end;
}
}
</style>

View File

@ -1,28 +1,90 @@
<template>
<view class="nav-bar-container">
<view style="width: 100%;" :style="{ height: `${data.statusBarHeight + 44}px` }"></view>
<view class="nav-bar-container" :style="{ backgroundColor: bgColor }">
<view class="status-placeholder" :style="{ height: `${data.statusBarHeight}px` }"></view>
<uni-nav-bar class="nav-bar" :border="false" :title="title" v-bind="$attrs" v-on="$attrs">
<uni-nav-bar backgroundColor="#00000000" class="nav-bar" :border="false" :title="title" v-bind="$attrs"
v-on="$attrs">
<template v-slot:left>
<view class="nav-bar-left">
<slot name="left">
<view class="left-icon" @click="onBack">
<image class="nav-icon-back" src="/static/image/nav-bar/back-black.png" mode=""></image>
</view>
</slot>
</view>
</template>
<view class="nav-bar-title w100 h100 flex-1" @click="openPopup">
<slot name="center">
{{ title }}
</slot>
</view>
<template v-slot:right>
<view class="nav-bar-right" @click="onRightClick">
<slot name="right">
<view v-if="isRightIcon" class="right-icon">
<image class="nav-icon-more" src="/static/image/nav-bar/more-black.png" mode=""></image>
</view>
<view v-if="isRightButton" class="right-button">
{{ rightButtonText }}
</view>
</slot>
</view>
</template>
</uni-nav-bar>
<popup ref="topPopup">
<view class="button-group w100 flex-between flex-wrap">
<view class="button-box" v-for="button in buttonGroup" @click="buttonClick(button)">
<view class="button">
{{ button.name }}
</view>
</view>
</view>
</popup>
</view>
</template>
<script setup>
import popup from '../popup/popup.vue'
import {
defineProps,
defineEmits,
onMounted,
reactive
reactive,
ref
} from 'vue'
const topPopup = ref()
//
const props = defineProps({})
const props = defineProps({
bgColor: {
type: String,
default: '#fff'
},
title: {
type: String,
default: ''
},
buttonGroup: {
type: Array,
default: () => []
},
isRightIcon: {
type: Boolean,
default: false
},
isRightButton: {
type: Boolean,
default: false
},
rightButtonText: {
type: String,
default: '确定'
}
})
//
const emit = defineEmits(['back', 'right-click'])
const emit = defineEmits(['back', 'right-click', 'button-click'])
const data = reactive({
statusBarHeight: 0
@ -34,6 +96,12 @@
data.statusBarHeight = systemInfo.statusBarHeight || 0;
})
const openPopup = () => {
if (props.buttonGroup.length > 0) {
topPopup.value.open()
}
}
//
const onBack = () => {
emit('back')
@ -45,20 +113,32 @@
const onRightClick = () => {
emit('right-click')
}
const buttonClick = (button) => {
topPopup.value.close()
emit('button-click', button)
}
</script>
<style scoped>
.nav-bar-container {
display: flex;
flex-direction: column;
position: fixed !important;
top: 0;
left: 0;
right: 0;
z-index: 999;
}
.nav-bar {
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
position: relative;
z-index: 100;
z-index: 1;
}
.status-placeholder {
@ -68,4 +148,60 @@
::v-deep .uni-navbar__content {
width: 100%;
}
.nav-bar-left {
display: flex;
align-items: center;
justify-content: flex-start;
}
.nav-icon-back {
width: 24px;
height: 24px;
}
.nav-icon-more {
width: 26px;
height: 26px;
}
.nav-bar-title {
flex: 1;
margin: auto;
display: flex;
align-items: center;
justify-content: center;
font-size: 17px;
font-weight: 500;
}
.nav-bar-right {
display: flex;
align-items: center;
justify-content: flex-end;
}
.right-button {
font-size: 12px;
border-radius: 16px;
color: #fff;
text-align: center;
line-height: 30px;
height: 30px;
min-width: 60px;
background: linear-gradient(90deg, #187AFF 0%, #3295FC 100%);
}
.button-box {
width: calc(50% - 8rpx);
text-align: center;
margin-top: 16rpx;
}
.button {
border: 1px solid #E4E4E4;
border-radius: 8px;
height: 42px;
line-height: 42px;
}
</style>

View File

@ -0,0 +1,96 @@
<template>
<view>
<!-- 弹窗 -->
<uni-popup ref="popup" type="center" border-radius="10px" background-color="rgba(0,0,0,0)">
<view class="topBtnBox">
<slot></slot>
</view>
<view class="topBtnBoxbottom" @click="close()">
<uni-icons type="clear" color="#AAA" size='40'></uni-icons>
</view>
</uni-popup>
<!-- 底部弹窗 -->
<uni-popup class="popup-bottom" ref="popupBottom" type="bottom" border-radius="10px"
background-color="rgba(0,0,0,0)">
<view class="popup-bottom-box">
<slot>
</slot>
</view>
</uni-popup>
</view>
</template>
<script>
import {
ref
} from 'vue';
export default {
name: "popup",
data() {
return {
};
},
methods: {
/**
* @param {Object} e
* bottom top lef right
* 打开弹窗
*/
open(e) {
switch (e) {
case "bottom":
this.$refs.popupBottom.open(e)
break;
default:
this.$refs.popup.open(e)
}
},
/**
* 关闭弹窗
*/
close() {
this.$refs.popup.close()
}
}
}
</script>
<style lang="less">
.popup-box {
padding: 32rpx;
background-color: #ffffff;
border-radius: 32rpx;
}
.topBtnBox {
background-color: #fff;
width: 80vw;
padding: 32rpx;
border-radius: 10px;
overflow: hidden;
display: flex;
flex-wrap: wrap;
}
.topBtnBoxbottom {
display: flex;
justify-content: center;
margin-top: 36rpx;
}
.popup-bottom {
.popup-bottom-box {
width: 100%;
border-radius: 32rpx 32rpx 0 0;
background-color: #EDEDED;
height: 100rpx;
}
}
</style>

View File

@ -14,13 +14,32 @@
"navigationBarTitleText": "uni-app",
"navigationStyle": "custom"
}
},
{
"path": "pages/bill/bill-list/bill-list",
"style": {
"navigationBarTitleText": "账单列表页面",
"navigationStyle": "custom"
}
},
{
"path": "pages/bill/add-bill/add-bill",
"style": {
"navigationBarTitleText": "新增账单",
"navigationStyle": "custom"
}
}
],
"globalStyle": {
"navigationBarTextStyle": "black",
"navigationBarTitleText": "uni-app",
"navigationBarBackgroundColor": "#F8F8F8",
"backgroundColor": "#F8F8F8"
"backgroundColor": "#F8F8F8",
"androidNavigationBar": {
"navigationBarBackgroundColor": "#00000000", //
"navigationBarTextStyle": "white", //
"backgroundColor": "#00000000" //
}
},
"uniIdRouter": {}
}

View File

@ -1,316 +0,0 @@
<template>
<view class="container" :style="{ height: data.windowHeight + 'px' }">
<NavBar class="nav-bar" title="" :backgroundColor="data.navBar.bgColor">
<template v-slot:left>
<view class="nav-bar-left">
<image class="nav-icon" src="/static/image/nav-bar/back-white.png" mode=""></image>
<text class="nav-text">余额</text>
</view>
</template>
</NavBar>
<view class="background-container" :style="{ 'padding-top': `${data.statusBarHeight * 2 + 124}rpx` }">
<view class="balance-box">
<view class="top-box">
<image class="img" src="/static/image/balance/safe-icon-blue.png" mode=""></image>
<text class="text">资金安全有保障</text>
<image class="img" src="/static/image/balance/right-blue.png" mode=""></image>
</view>
<view class="balance-title flex-cneter">
<text>可用余额(元)</text>
<image class="img" src="/static/image/balance/eye.png" mode=""></image>
</view>
<view class="balance flex-cneter">
<text class="text">{{ Number(balance).toFixed(2) }}</text>
</view>
<view class="button-group">
<view class="flex-1 btn-box">
<view class="left btn flex-cneter">
<text class="text">提现</text>
</view>
</view>
<view class="flex-1 btn-box">
<view class="right btn flex-cneter">
<text class="text">充值</text>
</view>
</view>
</view>
</view>
</view>
<view class="menu-box">
<view class="item" v-for="item in menuList">
<image class="menu-icon" :src="`/static/image/balance/menu-icon/${item.imgLabel}.png`" mode=""></image>
<text class="icon-name">{{item.name}}</text>
</view>
</view>
</view>
</template>
<script setup>
import NavBar from '@/components/nav-bar/nav-bar'
import {
deviceUtil
} from '@/utils/common';
import {
reactive,
ref,
onMounted,
watch,
toRefs
} from 'vue'
import {
onLoad
} from '@dcloudio/uni-app'
// 导入状态管理
import {
useStore
} from '@/store'
// 获取store
const {
store
} = useStore()
const menuList = [{
imgLabel: "zhuanzhang",
name: "转账"
}, {
imgLabel: "yinhangka",
name: "银行卡"
}, {
imgLabel: "qinqingka",
name: "亲情卡"
}, {
imgLabel: "xiaohebao",
name: "小荷包"
}, {
imgLabel: "zhuanyongjin",
name: "专用金"
}]
const data = reactive({
navBar: {
bgColor: "#00000000"
},
statusBarHeight: 0,
balance: 0,
windowHeight: 0
})
let {
balance
} = toRefs(data)
onLoad(async () => {
// 初始获取状态栏高度和屏幕高度
updateStatusBarHeight()
data.windowHeight = await deviceUtil.getScreenHeight()
})
onMounted(() => {
// 组件挂载后再次获取,确保信息已更新
updateStatusBarHeight()
})
// 更新状态栏高度
const updateStatusBarHeight = () => {
uni.getSystemInfo({
success: (res) => {
data.statusBarHeight = res.statusBarHeight || 0
console.log('直接获取状态栏高度:', data.statusBarHeight)
}
})
}
// 监听store中系统信息的变化
watch(() => store.systemInfo, (newVal) => {
if (newVal && newVal.statusBarHeight !== undefined) {
data.statusBarHeight = newVal.statusBarHeight
console.log('监听状态管理变化,更新状态栏高度:', data.statusBarHeight)
}
}, {
deep: true
})
</script>
<style scoped>
.container {
background-color: #F0F3F8;
}
.flex-cneter {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
}
.flex-1 {
flex: 1;
}
.nav-bar {
position: absolute;
width: 100%;
}
.nav-bar-left {
display: flex;
flex-direction: row;
align-items: center;
}
.nav-icon {
width: 24px;
height: 24px;
}
.nav-text {
font-size: 18px;
margin-right: 4px;
color: #FFFFFF;
height: 24px;
line-height: 24px;
}
::v-deep .uni-navbar__header-btns-left {
flex: 1;
}
::v-deep .uni-navbar__header-btns-right {
flex: 1;
}
.background-container {
background: linear-gradient(181deg, #1E76FE 0%, rgba(30, 118, 254, 0.74) 39%, rgba(240, 243, 248, 0.84) 90%, #F0F3F8 100%);
padding: 12px;
padding-bottom: 0;
padding-top: 62px;
}
.balance-box {
background-color: #FFFFFF;
border-radius: 12px;
padding-bottom: 20px;
}
.top-box {
background-color: #E3EFFF;
border-radius: 12px 12px 0 0;
text-align: center;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
padding: 6px;
}
.top-box>.text {
color: #2977E6;
font-size: 12px;
}
.top-box>.img {
width: 14px;
height: 14px;
}
.balance-title {
margin-top: 20px;
}
.balance-title>.img {
width: 16px;
height: 16px;
margin-left: 4px;
}
.balance-title>.text {
font-size: 14px;
color: #1A1A1A;
}
.balance {
margin-top: 21px;
}
.balance>.text {
color: #1A1A1A;
font-size: 40px;
font-weight: 500;
line-height: 32px;
}
.button-group {
margin: 0 12px;
margin-top: 60px;
display: flex;
flex-direction: row;
align-items: center;
/* box-sizing: border-box; */
}
.btn-box {
display: flex;
align-items: center;
justify-content: center;
}
.btn {
border-radius: 24px;
height: 49px;
width: 150px;
font-size: 18px;
}
.btn>.text {
color: #1A1A1A;
}
.left {
border: 1px solid #E2E2E2;
color: #1A1A1A;
}
.right {
color: #fff;
background-color: #1777FF;
}
.right>.text {
color: #fff;
}
.menu-box {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
margin-top: 8px;
border-radius: 12px;
background-color: #FFFFFF;
padding: 20px;
margin-left: 12px;
margin-right: 12px;
}
.item{
display: flex;
flex-direction: column;
align-items: center;
}
.menu-icon{
width: 48rpx;
height: 48rpx;
}
.icon-name{
font-size: 24rpx;
color: var(--text-color);
margin-top: 8px;
}
</style>

529
pages/balance/index.vue Normal file
View File

@ -0,0 +1,529 @@
<template>
<view class="container" :style="{ height: data.windowHeight + 'px' }">
<view class="bg-container"></view>
<NavBar class="nav-bar" isRightIcon title="" :bgColor="data.navBar.bgColor" :buttonGroup="buttonGroup"
@button-click="clickTitlePopupButton">
<template v-slot:left>
<view class="nav-bar-left">
<image class="nav-icon" src="/static/image/nav-bar/back-white.png" mode=""></image>
<text class="nav-text">余额</text>
</view>
</template>
<template v-slot:right>
<view class="nav-bar-right mr-1">
<image class="nav-icon" src="/static/image/nav-bar/more-white.png" mode=""></image>
</view>
</template>
</NavBar>
<scroll-view class="scroll-view" :style="{ height: (data.windowHeight - 44 - data.statusBarHeight) + 'px' }"
scroll-y="true">
<view class="h100 w100 flex-between" style="flex-direction: column;">
<view class="main-container w100 flex-1">
<view class="background-container" :style="{ 'padding-top': '36rpx' }">
<view class="balance-box">
<view class="top-box">
<image class="img" src="/static/image/balance/safe-icon-blue.png" mode=""></image>
<text class="text">资金安全有保障</text>
<image class="img" src="/static/image/balance/right-blue.png" mode=""></image>
</view>
<view class="balance-title flex-cneter">
<text>可用余额()</text>
<image class="img" src="/static/image/balance/eye.png" mode=""></image>
</view>
<view class="balance flex-cneter">
<text class="text alipay-font">{{ Number(balance).toFixed(2) }}</text>
</view>
<view class="button-group">
<view class="flex-1 btn-box">
<view class="left btn flex-cneter">
<text class="text">提现</text>
</view>
</view>
<view class="flex-1 btn-box">
<view class="right btn flex-cneter">
<text class="text">充值</text>
</view>
</view>
</view>
</view>
</view>
<view class="menu-box">
<view class="item" v-for="item in menuList">
<image class="menu-icon" :src="`/static/image/balance/menu-icon/${item.imgLabel}.png`"
mode="">
</image>
<text class="icon-name">{{ item.name }}</text>
</view>
</view>
<view class="balance-change-detail-list">
<view class="title-box">
<view class="text font-w500" style="font-weight: 500;">余额变动明细</view>
<view class="title-right">
<text class="text">全部</text>
<image class="right-icon" src="/static/image/common/right-grey.png" mode=""></image>
</view>
</view>
<view class="item">
<BalanceList :list="changeDetailList" />
</view>
</view>
</view>
<view class="footer-box">
<view class="blue-text">我的客服</view>
<view class="footer-text">余额升级服务由支付宝和网商银行提供</view>
</view>
</view>
</scroll-view>
</view>
<!-- 输入框示例 -->
<uni-popup ref="inputDialog" type="dialog">
<uni-popup-dialog before-close mode="input" title="修改余额" @confirm="dialogInputConfirm"
@close="dialogInputCancle">
<uni-easyinput type="digit" v-model="data.balance" focus placeholder="请输入余额"></uni-easyinput>
</uni-popup-dialog>
</uni-popup>
</template>
<script setup>
import NavBar from '@/components/nav-bar/nav-bar'
import BalanceList from '@/components/balance-list/balance-list.vue'
import {
util,
deviceUtil
} from '@/utils/common';
import {
storage
} from '@/utils/storage';
import {
reactive,
ref,
onMounted,
watch,
toRefs
} from 'vue'
import {
onLoad,
onShow
} from '@dcloudio/uni-app'
//
import {
useStore,
} from '@/store'
// store
const {
store
} = useStore()
const inputDialog = ref(null)
const menuList = [{
imgLabel: "zhuanzhang",
name: "转账"
}, {
imgLabel: "yinhangka",
name: "银行卡"
}, {
imgLabel: "qinqingka",
name: "亲情卡"
}, {
imgLabel: "xiaohebao",
name: "小荷包"
}, {
imgLabel: "zhuanyongjin",
name: "专用金"
}]
const buttonGroup = [{
name: "编辑模式",
click: () => {
console.log("编辑模式")
}
},
{
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({
navBar: {
bgColor: "#00000000"
},
statusBarHeight: 0,
balance: 0,
windowHeight: 0,
changeDetailList: [{
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"
}, {
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 {
balance,
changeDetailList
} = toRefs(data)
onLoad(async () => {
//
updateStatusBarHeight()
data.windowHeight = await deviceUtil.getWindowHeight()
//
const cachedBalance = storage.get('balance')
if (cachedBalance !== null) {
data.balance = cachedBalance
}
})
onShow(() => {
util.setAndroidSystemBarColor('#F0F3F8')
})
onMounted(() => {
//
updateStatusBarHeight()
})
//
const updateStatusBarHeight = () => {
uni.getSystemInfo({
success: (res) => {
data.statusBarHeight = res.statusBarHeight || 0
console.log('直接获取状态栏高度:', data.statusBarHeight)
}
})
}
// store
watch(() => store.systemInfo, (newVal) => {
if (newVal && newVal.statusBarHeight !== undefined) {
data.statusBarHeight = newVal.statusBarHeight
console.log('监听状态管理变化,更新状态栏高度:', data.statusBarHeight)
}
}, {
deep: true
})
//
const clickTitlePopupButton = (button) => {
button.click()
}
/**
* 输入框确认事件
*/
const dialogInputConfirm = () => {
if (data.balance > 999999999) {
uni.showToast({
title: '余额不能超过999999999',
icon: 'none'
})
data.balance = 999999999.00
}
storage.set('balance', data.balance)
inputDialog.value.close()
}
/**
* 点击关闭输入框弹窗
*/
const dialogInputCancle = () => {
data.balance = storage.get('balance')
inputDialog.value.close()
}
</script>
<style>
/* 直接在页面导入公共样式 */
@import '../../common/main.css';
</style>
<style scoped>
.container {
background-color: #F0F3F8;
overflow: hidden;
}
.scroll-view {
display: flex;
flex-direction: column;
justify-content: space-between;
background-color: transparent;
}
.bg-container {
position: absolute;
/* z-index: -1; */
width: 100%;
height: 600rpx;
background: linear-gradient(181deg, #1E76FE 0%, rgba(30, 118, 254, 0.74) 39%, rgba(240, 243, 248, 0.84) 90%, #F0F3F8 100%);
}
.flex-cneter {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
}
.flex-1 {
flex: 1;
}
.nav-bar {
position: absolute;
width: 100%;
}
.nav-bar-left {
display: flex;
flex-direction: row;
align-items: center;
}
.nav-icon {
width: 24px;
height: 24px;
}
.nav-text {
font-size: 18px;
margin-right: 4px;
color: #FFFFFF;
height: 24px;
line-height: 24px;
}
::v-deep .uni-navbar__header-btns-left {
flex: 1;
}
::v-deep .uni-navbar__header-btns-right {
flex: 1;
}
.background-container {
padding: 12px;
padding-bottom: 0;
padding-top: 62px;
}
.balance-box {
background-color: #FFFFFF;
border-radius: 12px;
padding-bottom: 20px;
}
.top-box {
background-color: #E3EFFF;
border-radius: 12px 12px 0 0;
text-align: center;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
padding: 6px;
}
.top-box>.text {
color: #2977E6;
font-size: 12px;
}
.top-box>.img {
width: 14px;
height: 14px;
}
.balance-title {
margin-top: 20px;
}
.balance-title>.img {
width: 16px;
height: 16px;
margin-left: 4px;
}
.balance-title>.text {
font-size: 14px;
color: #1A1A1A;
}
.balance {
margin-top: 21px;
}
.balance>.text {
color: #1A1A1A;
font-size: 40px;
font-weight: 500;
line-height: 32px;
}
.button-group {
margin: 0 12px;
margin-top: 60px;
display: flex;
flex-direction: row;
align-items: center;
/* box-sizing: border-box; */
}
.btn-box {
display: flex;
align-items: center;
justify-content: center;
}
.btn {
border-radius: 24px;
height: 49px;
width: 150px;
font-size: 18px;
}
.btn>.text {
color: #1A1A1A;
}
.left {
border: 1px solid #E2E2E2;
color: #1A1A1A;
}
.right {
color: #fff;
background-color: #1777FF;
}
.right>.text {
color: #fff;
}
.menu-box {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
margin-top: 8px;
border-radius: 12px;
background-color: #FFFFFF;
padding: 20px;
margin-left: 12px;
margin-right: 12px;
}
.item {
display: flex;
flex-direction: column;
align-items: center;
}
.menu-icon {
width: 48rpx;
height: 48rpx;
}
.icon-name {
font-size: 24rpx;
color: var(--text-color);
margin-top: 8px;
}
.balance-change-detail-list {
border-radius: 12px;
margin-top: 8px;
margin-left: 12px;
margin-right: 12px;
background-color: #FFFFFF;
}
.title-box {
display: flex;
flex-direction: row;
padding: 13px 12px;
justify-content: space-between;
box-shadow: 0 0.3px 0 0 #F0F0EE;
}
.title-box>.text {
font-size: 14px;
color: var(--text-color);
}
.title-right {
display: flex;
align-items: center;
flex-direction: row;
}
.title-right>.text {
font-size: 14px;
color: var(--text-color);
}
.right-icon {
width: 10px;
height: 10px;
margin-left: 5px;
}
.footer-box {
margin-top: 18px;
text-align: center;
margin-bottom: 17px;
}
.blue-text {
color: #507295;
}
.footer-text {
font-size: 13px;
color: var(--footer-text-color);
margin-top: 18px;
}
</style>

View File

@ -0,0 +1,48 @@
<template>
<view style="overflow: hidden;height: 100vh;">
<navBar isRightButton :title="data.navBar.title" :bgColor="data.navBar.bgColor" @right-click="onRightClick">
</navBar>
</view>
</template>
<script setup>
import navBar from '@/components/nav-bar/nav-bar.vue'
import {
reactive,
toRefs,
ref,
onMounted
} from 'vue'
import {
onShow,
} from '@dcloudio/uni-app'
const data = reactive({
navBar: {
title: '新增账单',
bgColor: '#F5F5F5',
},
})
let {
} = toRefs(data)
onShow(() => {
})
onMounted(() => {
})
</script>
<style>
@import "@/common/main.css";
page {
background-color: #F5F5F5;
}
</style>
<style lang="less" scoped></style>

View File

@ -0,0 +1,704 @@
<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">
<text class="font-w500">{{ currentMonthData.month }}</text>
<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 class="list-title">
<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 class="flex-between analysis-box">
<view class="income-ande-outCome">
<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 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>
<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" />
</view>
</view>
</scroll-view>
</view>
</template>
<script setup>
import navBar from '@/components/nav-bar/nav-bar.vue'
import BalanceList from '@/components/balance-list/balance-list.vue'
import {
reactive,
toRefs,
ref,
onMounted
} from 'vue'
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: [{
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 {
billList,
currentMonthData,
currentFilterType
} = toRefs(data)
onShow(() => {
//
updateStatusBarHeight()
})
onMounted(() => {
//
getCardPositions()
})
//
const updateStatusBarHeight = () => {
uni.getSystemInfo({
success: (res) => {
data.statusBarHeight = res.statusBarHeight || 0
console.log('直接获取状态栏高度:', data.statusBarHeight)
}
})
}
/**
* 获取所有卡片相对于 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()
}
</script>
<style>
@import "@/common/main.css";
page {
background-color: #F5F5F5;
}
</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;
margin-top: 6px;
.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 {
margin-top: 8px;
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;
}
}
}
.current-month {
padding: 10px 12px 18px;
background: url('/static/image/bill/bill-list/current-month-bill-bg.png') no-repeat center center;
background-size: 100% 100%;
.income-ande-outCome {
margin-top: 8px;
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 {
margin-top: 3px;
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;
}
}
}
}
</style>

View File

@ -1,321 +1,3 @@
<template>
<view class="container">
<view class="header">
<text class="title">公共方法使用示例</text>
</view>
<view class="content">
<!-- 日期处理示例 -->
<view class="section">
<text class="section-title">日期处理</text>
<view class="example-item">
<text class="label">当前日期</text>
<text class="value">{{ currentDate }}</text>
</view>
<view class="example-item">
<text class="label">格式化日期</text>
<text class="value">{{ formattedDate }}</text>
</view>
<view class="example-item">
<text class="label">相对时间</text>
<text class="value">{{ relativeDate }}</text>
</view>
</view>
<!-- 数字处理示例 -->
<view class="section">
<text class="section-title">数字处理</text>
<view class="example-item">
<text class="label">千分位格式化</text>
<text class="value">{{ formattedNumber }}</text>
</view>
<view class="example-item">
<text class="label">金额格式化</text>
<text class="value">{{ formattedMoney }}</text>
</view>
<view class="example-item">
<text class="label">随机数</text>
<text class="value">{{ randomNumber }}</text>
</view>
</view>
<!-- 字符串处理示例 -->
<view class="section">
<text class="section-title">字符串处理</text>
<view class="example-item">
<text class="label">UUID</text>
<text class="value">{{ uuid }}</text>
</view>
<view class="example-item">
<text class="label">手机号脱敏</text>
<text class="value">{{ maskedPhone }}</text>
</view>
<view class="example-item">
<text class="label">字符串截断</text>
<text class="value">{{ truncatedString }}</text>
</view>
</view>
<!-- UI工具示例 -->
<view class="section">
<text class="section-title">UI工具</text>
<view class="button-group">
<button class="button success" @click="showSuccess">成功提示</button>
<button class="button error" @click="showError">错误提示</button>
<button class="button loading" @click="showLoading">加载提示</button>
<button class="button confirm" @click="showConfirm">确认对话框</button>
</view>
</view>
<!-- 设备信息示例 -->
<view class="section">
<text class="section-title">设备信息</text>
<view class="example-item">
<text class="label">状态栏高度</text>
<text class="value">{{ statusBarHeight }}</text>
</view>
<view class="example-item">
<text class="label">设备平台</text>
<text class="value">{{ devicePlatform }}</text>
</view>
<view class="example-item">
<text class="label">网络状态</text>
<text class="value">{{ networkStatus }}</text>
</view>
</view>
<!-- 屏幕尺寸示例 -->
<view class="section">
<text class="section-title">屏幕尺寸</text>
<view class="example-item">
<text class="label">屏幕宽度</text>
<text class="value">{{ screenWidth }}</text>
</view>
<view class="example-item">
<text class="label">屏幕高度</text>
<text class="value">{{ screenHeight }}</text>
</view>
<view class="example-item">
<text class="label">窗口宽度</text>
<text class="value">{{ windowWidth }}</text>
</view>
<view class="example-item">
<text class="label">窗口高度</text>
<text class="value">{{ windowHeight }}</text>
</view>
<view class="button-group">
<button class="button loading" @click="loadScreenSize">获取屏幕尺寸</button>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import { dateUtil, numberUtil, stringUtil, deviceUtil, networkUtil, uiUtil } from '@/utils/common';
//
const currentDate = ref('');
const formattedDate = ref('');
const relativeDate = ref('');
//
const formattedNumber = ref('');
const formattedMoney = ref('');
const randomNumber = ref(0);
//
const uuid = ref('');
const maskedPhone = ref('');
const truncatedString = ref('');
//
const statusBarHeight = ref(0);
const devicePlatform = ref('');
const networkStatus = ref('');
//
const screenWidth = ref(0);
const screenHeight = ref(0);
const windowWidth = ref(0);
const windowHeight = ref(0);
//
onMounted(async () => {
//
currentDate.value = dateUtil.now();
const testDate = new Date('2025-12-25 10:30:00');
formattedDate.value = dateUtil.format(testDate, 'YYYY年MM月DD日 HH时mm分ss秒');
relativeDate.value = dateUtil.relative(testDate);
//
formattedNumber.value = numberUtil.format(1234567.89);
formattedMoney.value = numberUtil.formatMoney(98765.4321);
randomNumber.value = numberUtil.random(1, 100);
//
uuid.value = stringUtil.uuid();
maskedPhone.value = stringUtil.maskPhone('13812345678');
truncatedString.value = stringUtil.truncate('这是一个很长的字符串,用于测试字符串截断功能', 20);
//
await loadDeviceInfo();
//
await loadNetworkStatus();
});
//
async function loadDeviceInfo() {
try {
const systemInfo = await deviceUtil.getSystemInfo();
statusBarHeight.value = systemInfo.statusBarHeight || 0;
devicePlatform.value = systemInfo.platform || '未知';
} catch (error) {
console.error('获取设备信息失败:', error);
}
}
//
async function loadNetworkStatus() {
try {
const isConnected = await networkUtil.isConnected();
networkStatus.value = isConnected ? '已连接' : '未连接';
} catch (error) {
console.error('获取网络状态失败:', error);
}
}
// UI
function showSuccess() {
uiUtil.showSuccess('操作成功');
}
function showError() {
uiUtil.showError('操作失败,请重试');
}
function showLoading() {
uiUtil.showLoading('加载中...');
//
setTimeout(() => {
uiUtil.hideLoading();
}, 2000);
}
function showConfirm() {
uiUtil.showConfirm('确认操作', '您确定要执行此操作吗?', () => {
uiUtil.showSuccess('确认成功');
}, () => {
uiUtil.showError('已取消操作');
});
}
//
async function loadScreenSize() {
try {
const screenSize = await deviceUtil.getScreenSize();
screenWidth.value = screenSize.width;
screenHeight.value = screenSize.height;
windowWidth.value = screenSize.windowWidth;
windowHeight.value = screenSize.windowHeight;
} catch (error) {
console.error('获取屏幕尺寸失败:', error);
}
}
</script>
<style lang="less">
.container {
min-height: 100vh;
background-color: #f5f5f5;
padding-bottom: 20px;
}
.header {
padding: 20px;
background-color: #007aff;
text-align: center;
}
.title {
font-size: 20px;
font-weight: bold;
color: #fff;
}
.content {
padding: 15px;
}
.section {
background-color: #fff;
border-radius: 8px;
padding: 15px;
margin-bottom: 15px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.section-title {
font-size: 18px;
font-weight: bold;
margin-bottom: 15px;
color: #333;
}
.example-item {
margin-bottom: 10px;
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 0;
}
.label {
font-size: 14px;
color: #666;
}
.value {
font-size: 14px;
color: #333;
font-weight: 500;
}
.button-group {
display: flex;
flex-direction: column;
gap: 10px;
}
.button {
padding: 12px;
border-radius: 6px;
font-size: 16px;
color: #fff;
border: none;
cursor: pointer;
transition: opacity 0.2s;
&.success {
background-color: #4cd964;
}
&.error {
background-color: #ff3b30;
}
&.loading {
background-color: #007aff;
}
&.confirm {
background-color: #5856d6;
}
&:active {
opacity: 0.8;
}
}
</style>
<template></template>
<script setup></script>
<style></style>

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 396 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 305 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 213 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 805 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -228,34 +228,6 @@ export const deviceUtil = {
}
},
/**
* 获取屏幕宽度
* @returns {Promise<number>} 屏幕宽度px
*/
async getScreenWidth() {
try {
const systemInfo = await this.getSystemInfo();
return systemInfo.screenWidth || 0;
} catch (error) {
console.error('获取屏幕宽度失败:', error);
return 0;
}
},
/**
* 获取屏幕高度
* @returns {Promise<number>} 屏幕高度px
*/
async getScreenHeight() {
try {
const systemInfo = await this.getSystemInfo();
return systemInfo.screenHeight || 0;
} catch (error) {
console.error('获取屏幕高度失败:', error);
return 0;
}
},
/**
* 获取窗口宽度
* @returns {Promise<number>} 窗口宽度px
@ -285,37 +257,6 @@ export const deviceUtil = {
}
};
/**
* 网络请求相关方法
*/
export const networkUtil = {
/**
* 检查网络状态
* @returns {Promise<Object>} 网络状态对象
*/
checkNetwork() {
return new Promise((resolve, reject) => {
uni.getNetworkType({
success: resolve,
fail: reject
});
});
},
/**
* 判断是否有网络连接
* @returns {Promise<boolean>} 是否有网络连接
*/
async isConnected() {
try {
const networkType = await this.checkNetwork();
return networkType.networkType !== 'none';
} catch (error) {
console.error('检查网络状态失败:', error);
return false;
}
}
};
/**
* UI相关方法
@ -342,20 +283,6 @@ export const uiUtil = {
uni.hideLoading();
},
/**
* 显示成功提示
* @param {string} title - 提示文本
* @param {number} duration - 显示时长
* @returns {void}
*/
showSuccess(title, duration = 1500) {
uni.showToast({
title,
icon: 'success',
duration
});
},
/**
* 显示错误提示
* @param {string} title - 提示文本
@ -397,65 +324,55 @@ export const uiUtil = {
* 其他工具方法
*/
export const util = {
/**
* 深拷贝对象
* @param {Object} obj - 原始对象
* @returns {Object} 拷贝后的对象
*/
deepClone(obj) {
if (obj === null || typeof obj !== 'object') return obj;
if (obj instanceof Date) return new Date(obj.getTime());
if (obj instanceof Array) return obj.map(item => this.deepClone(item));
const clonedObj = {};
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
clonedObj[key] = this.deepClone(obj[key]);
setAndroidSystemBarColor(backgroundColor) {
if (deviceUtil.isAndroid()) {
try {
// #ifndef APP-IOS
if (plus.os.name === 'Android') {
console.log("1212333");
let color = plus.android.newObject("android.graphics.Color");
let activity = plus.android.runtimeMainActivity();
let colorInt = plus.android.invoke(color, "parseColor", backgroundColor);
let window = plus.android.invoke(activity, "getWindow");
plus.android.invoke(window, "setNavigationBarColor", colorInt);
uni.setNavigationBarColor({
animation: { // 动画效果
duration: 100,
timingFunc: 'easeIn'
}
})
console.log("状态栏设置完毕!");
setTimeout(function () {
uni.setNavigationBarColor({
backgroundColor: backgroundColor,
animation: { // 动画效果
duration: 100,
timingFunc: 'easeIn'
}
})
}, 200);
}
// #endif
} catch (err) {
console.log("状态栏修改失败", err);
uni.setNavigationBarColor({
animation: { // 动画效果
duration: 100,
timingFunc: 'easeIn'
}
})
}
}
return clonedObj;
},
/**
* 防抖函数
* @param {Function} func - 要执行的函数
* @param {number} wait - 等待时间
* @returns {Function} 防抖后的函数
* 页面跳转
* @param {*} url
*/
debounce(func, wait = 300) {
let timeout;
return function(...args) {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), wait);
};
},
/**
* 节流函数
* @param {Function} func - 要执行的函数
* @param {number} limit - 限制时间
* @returns {Function} 节流后的函数
*/
throttle(func, limit = 300) {
let inThrottle;
return function(...args) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
},
/**
* 延迟执行
* @param {number} ms - 延迟时间毫秒
* @returns {Promise} Promise对象
*/
delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
goPage(url) {
uni.navigateTo({
url
});
}
};
@ -465,7 +382,6 @@ export default {
...numberUtil,
...stringUtil,
...deviceUtil,
...networkUtil,
...uiUtil,
...util
};