完成理财

This commit is contained in:
tangxinyue 2026-01-27 18:31:45 +08:00
parent 2147da46ac
commit 4da4c4de84
25 changed files with 1098 additions and 248 deletions

View File

@ -6,7 +6,8 @@
"type" : "uni-app:app-ios"
},
{
"playground" : "custom",
"customPlaygroundType" : "device",
"playground" : "standard",
"type" : "uni-app:app-android"
}
]

View File

@ -1,95 +1,111 @@
{
"name" : "alipay-emulator",
"appid" : "__UNI__D535736",
"description" : "",
"versionName" : "1.0.0",
"versionCode" : "100",
"transformPx" : false,
/* 5+App特有相关 */
"app-plus" : {
"darkmode" : false,
"usingComponents" : true,
"nvueStyleCompiler" : "uni-app",
"compilerVersion" : 3,
"splashscreen" : {
"alwaysShowBeforeRender" : true,
"waiting" : true,
"autoclose" : true,
"delay" : 0
},
"optimization" : {
"subPackages" : true
},
"runmode" : "liberate", // 开启分包优化后,必须配置资源释放模式
/* 模块配置 */
"modules" : {
"Camera" : {},
"Payment" : {}
},
/* 应用发布信息 */
"distribute" : {
/* android打包配置 */
"android" : {
"permissions" : [
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
"<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
"<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
"<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
"<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
"<uses-feature android:name=\"android.hardware.camera\"/>",
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
]
},
/* ios打包配置 */
"ios" : {
"dSYMs" : false
},
/* SDK配置 */
"sdkConfigs" : {
"payment" : {
"weixin" : {
"__platform__" : [ "ios", "android" ],
"appid" : "123456",
"UniversalLinks" : "https://hhhhh.com/apple-app-site-association/"
},
"alipay" : {
"__platform__" : [ "ios", "android" ]
}
}
}
},
"nvueLaunchMode" : ""
},
/* 快应用特有相关 */
"quickapp" : {},
/* 小程序特有相关 */
"mp-weixin" : {
"appid" : "",
"setting" : {
"urlCheck" : false
},
"usingComponents" : true
},
"mp-alipay" : {
"usingComponents" : true
},
"mp-baidu" : {
"usingComponents" : true
},
"mp-toutiao" : {
"usingComponents" : true
},
"uniStatistics" : {
"enable" : false
},
"vueVersion" : "3"
"name": "alipay-emulator",
"appid": "__UNI__D535736",
"description": "",
"versionName": "1.0.0",
"versionCode": "100",
"transformPx": false,
/* 5+App特有相关 */
"app-plus": {
"confusion": {
"resources": {
"app-service.js": {}
}
},
"encryption": {
"wgt": true,
"files": [
"app-service.js"
]
},
"darkmode": false,
"usingComponents": true,
"nvueStyleCompiler": "uni-app",
"compilerVersion": 3,
"splashscreen": {
"alwaysShowBeforeRender": true,
"waiting": true,
"autoclose": true,
"delay": 0
},
"optimization": {
"subPackages": false
},
"runmode": "liberate", // 开启分包优化后,必须配置资源释放模式
/* 模块配置 */
"modules": {
"Camera": {},
"Payment": {}
},
/* 应用发布信息 */
"distribute": {
/* android打包配置 */
"android": {
"permissions": [
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
"<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
"<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
"<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
"<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
"<uses-feature android:name=\"android.hardware.camera\"/>",
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
]
},
/* ios打包配置 */
"ios": {
"dSYMs": false
},
/* SDK配置 */
"sdkConfigs": {
"payment": {
"weixin": {
"__platform__": [
"ios",
"android"
],
"appid": "123456",
"UniversalLinks": "https://hhhhh.com/apple-app-site-association/"
},
"alipay": {
"__platform__": [
"ios",
"android"
]
}
}
}
},
"nvueLaunchMode": ""
},
/* 快应用特有相关 */
"quickapp": {},
/* 小程序特有相关 */
"mp-weixin": {
"appid": "",
"setting": {
"urlCheck": false
},
"usingComponents": true
},
"mp-alipay": {
"usingComponents": true
},
"mp-baidu": {
"usingComponents": true
},
"mp-toutiao": {
"usingComponents": true
},
"uniStatistics": {
"enable": false
},
"vueVersion": "3"
}

View File

@ -73,12 +73,27 @@
}, {
"root": "pages/finance-management",
"pages": [{
"path": "index",
"style": {
"navigationBarTitleText": "理财首页",
"navigationStyle": "custom"
"path": "index",
"style": {
"navigationBarTitleText": "理财首页",
"navigationStyle": "custom"
}
},
{
"path": "select-image/select-image",
"style": {
"navigationBarTitleText": "选择理财图片",
"navigationStyle": "custom"
}
},
{
"path": "edit/edit",
"style": {
"navigationBarTitleText": "编辑理财数据",
"navigationStyle": "custom"
}
}
}]
]
},
{
"root": "pages/common",
@ -119,7 +134,9 @@
"packages": [
"pages/balance",
"pages/bill",
"pages/common"
"pages/common",
"pages/finance-management",
"pages/ant-credit-pay"
]
}
},

View File

@ -507,13 +507,25 @@ const chooseImage = () => {
// 长按事件定时器
let longPressTimer = null
const handleTouchStart = () => {
const handleTouchStart = (e) => {
// 兼容iOS上滑HOME条,如果有底部安全区且触摸位置在底部安全区内,则不触发
const systemInfo = uni.getSystemInfoSync()
if (systemInfo.platform === 'ios' && systemInfo.safeAreaInsets?.bottom) {
const clientY = e.touches[0].clientY
const windowHeight = systemInfo.windowHeight
// 如果触摸点在底部安全区范围内(通常是34px),则忽略
if (clientY > windowHeight - systemInfo.safeAreaInsets.bottom) {
return
}
}
longPressTimer = setTimeout(() => {
uni.vibrateShort()
chooseImage()
}, 1200) // 长按时间大于1s
}
const handleTouchEnd = () => {
if (longPressTimer) {
clearTimeout(longPressTimer)

View File

@ -38,7 +38,7 @@
<image class="img" src="/static/image/balance/eye.png" mode=""></image>
</view>
<view class="balance flex-cneter" @click="editBalance">
<text class="text alipay-font">{{ Number(balance).toFixed(2) }}</text>
<text class="text alipay-font">{{ numberUtil.formatMoneyWithThousand(balance) }}</text>
</view>
<view class="button-group">
<view class="flex-1 btn-box">
@ -116,7 +116,8 @@ import BalanceList from '@/components/balance-list/balance-list.vue'
import { fastEntranceList } from '@/static/json/initial.json'
import {
util,
deviceUtil
deviceUtil,
numberUtil
} from '@/utils/common';
import {
storage
@ -212,7 +213,9 @@ onLoad(async () => {
onShow(() => {
// #ifdef APP-PLUS
util.setAndroidSystemBarColor('#F0F3F8')
plus.navigator.setStatusBarStyle("light");
setTimeout(() => {
plus.navigator.setStatusBarStyle("light");
}, 500)
// #endif
// 获取快速入口配置

View File

@ -0,0 +1,222 @@
<template>
<view class="container">
<NavBar title="修改理财数据" bgColor="#F5F5F5" isRightButton @right-click="handleRightButtonClick"></NavBar>
<scroll-view scroll-y class="form-content">
<!-- 基础信息 -->
<view class="section-title">基础信息</view>
<view class="card">
<view class="form-item">
<text class="label">持卡人姓名</text>
<input class="input" v-model="financeInfo.cardHolderName" />
</view>
<picker mode="selector" :range="vipLevels" @change="onVipLevelChange">
<view class="form-item">
<text class="label">财富等级</text>
<view class="picker-value">{{ financeInfo.vipLevel }} </view>
</view>
</picker>
<view class="form-item">
<text class="label">权益文案</text>
<input class="input" v-model="financeInfo.benefitsText" />
</view>
</view>
<!-- 财富收益 -->
<view class="section-title">财富收益</view>
<view class="card" v-for="(item, key) in financeInfo.fortune" :key="key">
<view class="card-header">{{ getFortuneName(key) }}</view>
<view class="form-item">
<text class="label">总金额</text>
<input class="input" type="digit" v-model="item.labelSub" @input="onTotalAmountInput(item)" />
</view>
<view class="form-item">
<text class="label">昨日收益</text>
<input class="input" type="digit" v-model="item.value" />
</view>
</view>
<!-- 资产信息 -->
<view class="section-title">资产信息</view>
<view class="card">
<view class="form-item">
<text class="label">保险信息</text>
<input class="input" v-model="financeInfo.myAssets.baoxian.labelSub" />
</view>
</view>
<!-- 额度信息 -->
<view class="section-title">额度信息</view>
<view class="card">
<view class="form-item" v-for="(item, key) in financeInfo.myQuota" :key="key">
<text class="label">{{ getQuotaName(key) }}</text>
<input class="input" v-model="item.labelSub" />
</view>
</view>
<view class="placeholder"></view>
</scroll-view>
</view>
</template>
<script setup>
import NavBar from '@/components/nav-bar/nav-bar.vue'
import { ref, reactive, toRefs, onMounted } from 'vue';
const defualtData = {
benefitsText: '尊享礼遇18000元/年',
cardHolderName: 'XiaoMing',
vipLevel: "v3",
navigationMenuStyle: "style-1",
bgImage: "",
fortune: {
yuebao: { value: 3.53, labelSub: "100000" },
dingqi: { value: 50, labelSub: "100000" },
jijin: { value: 5000, labelSub: "100000" },
huangjin: { value: -300, labelSub: "100000" },
yulingbao: { value: 20, labelSub: "100000" }
},
myAssets: {
baoxian: { labelSub: "3份保单保障中", type: "text" }
},
myQuota: {
huabei: { labelSub: "可用100000", type: "text" },
jubei: { labelSub: "可用100000", type: "text" },
beizhi: { labelSub: "可用100000", type: "text" },
wangshangdai: { labelSub: "预计可借100000", type: "text" }
}
}
const data = reactive({
financeInfo: JSON.parse(JSON.stringify(defualtData))
})
const { financeInfo } = toRefs(data)
const fortuneMap = {
yuebao: '余额宝',
dingqi: '定期',
jijin: '基金',
huangjin: '黄金',
yulingbao: '余利宝'
}
const quotaMap = {
huabei: '花呗',
jubei: '借呗',
beizhi: '备用金',
wangshangdai: '网商贷'
}
const getFortuneName = (key) => fortuneMap[key] || key
const getQuotaName = (key) => quotaMap[key] || key
const vipLevels = ['v1', 'v2', 'v3']
const onVipLevelChange = (e) => {
financeInfo.value.vipLevel = vipLevels[e.detail.value]
}
const onTotalAmountInput = (item) => {
// 使用 setTimeout 确保 v-model 更新后再处理,或者直接处理 value
// 简单的非负数过滤
if (item.labelSub && item.labelSub.toString().includes('-')) {
item.labelSub = item.labelSub.replace('-', '')
}
}
onMounted(() => {
const stored = uni.getStorageSync('financeInfo')
if (stored) {
// Deep merge to ensure structure integrity
Object.assign(data.financeInfo, stored)
}
})
const handleRightButtonClick = () => {
uni.setStorageSync('financeInfo', data.financeInfo)
uni.navigateBack()
}
</script>
<style>
@import "@/common/main.css";
page {
background-color: #F8F8F8;
height: 100vh;
overflow: hidden;
}
</style>
<style lang="less" scoped>
.container {
display: flex;
flex-direction: column;
height: 100vh;
}
.form-content {
flex: 1;
height: 0;
padding: 24rpx;
box-sizing: border-box;
}
.section-title {
font-size: 28rpx;
color: #666;
margin: 24rpx 0 16rpx 12rpx;
font-weight: 500;
}
.card {
background-color: #fff;
border-radius: 16rpx;
padding: 0 24rpx;
margin-bottom: 24rpx;
.card-header {
padding: 24rpx 0 12rpx;
font-size: 30rpx;
font-weight: bold;
color: #333;
border-bottom: 1rpx solid #f5f5f5;
margin-bottom: 12rpx;
}
}
.form-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 24rpx 0;
border-bottom: 1rpx solid #F5F5F5;
&:last-child {
border-bottom: none;
}
.label {
font-size: 30rpx;
color: #333;
width: 240rpx;
}
.input {
flex: 1;
font-size: 30rpx;
color: #333;
text-align: right;
}
.picker-value {
flex: 1;
font-size: 30rpx;
color: #333;
text-align: right;
}
}
.placeholder {
height: 60rpx;
}
</style>

View File

@ -8,10 +8,12 @@
</view>
<view class="container flex flex-column" @touchstart="handleTouchStart" @touchmove.stop.prevent="handleTouchMove"
@touchend="handleTouchEnd">
<!-- 下层信息盒子 -->
<view class="main-info-container" :class="{ 'open': isOpen }"
:style="{ 'padding-bottom': `${cardArrowHeight}px` }">
<!-- 导航栏 placeholder -->
<NavBar bgColor="transparent">
<NavBar v-if="!selectedImage" bgColor="transparent" :buttonGroup="buttonGroup"
@button-click="clickTitlePopupButton">
<view class="nav-bar flex-align-center h100">
<view class="left flex-align-center">
<image src="/static/image/finance-management/search/search-left.png"></image>
@ -27,8 +29,12 @@
</view>
</view>
</NavBar>
<NavBar v-else title="拼图" bgColor="#EFEFEF" noBack @back="closeImage" isRightButton
@right-click="confirmImage">
</NavBar>
<view class="total-money-box flex-align-center flex-justify-between">
<!-- 总资产信息 -->
<view class="revenue-box flex-1">
<view class="total-money flex-align-center">
<view class="total-money-title flex-align-center">
@ -65,12 +71,14 @@
</view>
</view>
</view>
<!-- 财富信息 -->
<view class="data-box">
<view v-for="item in myAssets" :key="item.title">
<view v-if="item.title" class="title second-color">{{ item.title }}</view>
<view class="group-data flex-justify-between">
<template v-for="subItem in item.items" :key="subItem.label">
<view v-if="financeInfo[item.key][subItem.key].labelSub"
<view
v-if="financeInfo[item.key][subItem.key].labelSub && (financeInfo[item.key][subItem.key].labelSub > 0 || financeInfo[item.key][subItem.key].type == 'text')"
class="item flex-align-center flex-justify-between">
<view class="flex-align-center">
<text class="label primary-color">{{ subItem.label }}</text>
@ -102,8 +110,10 @@
</view>
</view>
</view>
<!-- 图片盒子 -->
<view class="image-service-box flex-column"
:style="{ top: topHeight + 'px', 'min-height': `calc(100vh - ${imageCardBoxtopHeight}px)` }">
:style="{ top: topHeight + 'px', 'height': `calc(100vh - ${imageCardBoxtopHeight}px)`, transition: isDragging || !isReady ? 'none' : 'top 0.3s ease-out' }">
<view class="card-arrow-box">
<view class="arrow-box">
<image @click="data.isOpen = !data.isOpen" class="arrow-icon"
@ -153,18 +163,35 @@
</view>
</view>
<view class="content flex-1 flex-align-center flex-justify-center">
<view class="flex-align-center flex-column">
<view v-if="!selectedImage" class="content flex-1 flex-column flex" @touchstart="clickHandleTouchStart"
@touchend="clickHandleTouchEnd">
<view v-if="financeInfo.bgImage" class="w100 h100" style="margin-top: -1px;">
<image class="w100 h100" :src="financeInfo.bgImage" mode="widthFix">
</image>
</view>
<view v-else class="flex-align-center flex-justify-center flex-column flex-1 w100 h100">
<image style="width:92rpx; height: 92rpx;margin-top: 16rpx;"
src="/static/image/common/upload-screenshot.png">
</image>
<text style="font-size: 36rpx;color: #1777FF;">长按替换真实截图</text>
</view>
</view>
<view v-else class="scroll-image-box flex-1">
<scroll-view class="image-box h100" style="width: 100%;" scroll-y :show-scrollbar="false"
@scroll="onImageScroll">
<image class="crop-image-target" style="width:100%;" :src="selectedImage" mode="widthFix"></image>
</scroll-view>
<view class="dashed-line-box">
<view class="dashed-line-text">我是分割线</view>
</view>
</view>
<canvas canvas-id="crop-canvas"
style="position: fixed; left: -9999px; width: 750rpx; height: 100vh; pointer-events: none;"></canvas>
</view>
<!-- 底部导航栏 -->
<view class="bottom-box"
:class="`navigation-menu-${financeInfo.navigationMenuStyle}`, { 'ios-bottom-box': $system == 'iOS' }">
<view class="bottom-item" v-for="item in navigationMenu" :key="item.name">
@ -175,33 +202,142 @@
</view>
</view>
</view>
<!-- 样式选择弹窗 -->
<uni-popup ref="stylePopup" type="center">
<view class="popup-content">
<view class="popup-title">选择底部导航样式</view>
<view class="style-list">
<view class="style-item" v-for="(item, index) in styleList" :key="index"
@click="confirmStyleDialog(item.value)">
<text>{{ item.label }}</text>
<uni-icons v-if="financeInfo.navigationMenuStyle == item.value" type="checkmarkempty" size="20"
color="#1777FF"></uni-icons>
</view>
</view>
<view class="popup-btns">
<view class="btn cancel" @click="closeStyleDialog">取消</view>
</view>
</view>
</uni-popup>
<!-- 蒙层 -->
<view v-if="showMask" class="mask" @click="closeMask">
<image class="mask-icon" src="/static/image/common/mask-icon.png" mode="widthFix">
</image>
</view>
</template>
<script setup>
import NavBar from '@/components/nav-bar/nav-bar.vue'
import { ref, toRefs, reactive, onMounted, watch, computed } from 'vue'
import { onLoad, onReady } from '@dcloudio/uni-app'
import { serviceIcons, navigationMenu, myAssets } from '@/static/json/fortune.json'
import { numberUtil } from '@/utils/common.js'
import {
ref,
toRefs,
reactive,
onMounted,
watch,
computed,
getCurrentInstance
} from 'vue'
import {
onLoad,
onReady,
onShow
} from '@dcloudio/uni-app'
import {
serviceIcons,
navigationMenu,
myAssets
} from '@/static/json/fortune.json'
import {
numberUtil,
util
} from '@/utils/common.js'
const instance = getCurrentInstance();
const { proxy } = instance
const buttonGroup = [{
name: "编辑理财数据",
click: () => {
util.goPage('/pages/finance-management/edit/edit')
}
}, {
name: "切换导航栏样式",
click: () => {
openStyleDialog()
}
}, {
name: "选择理财图片样式",
click: () => {
util.goPage('/pages/finance-management/select-image/select-image')
}
}, {
name: "删除当前理财图片",
click: () => {
financeInfo.value.bgImage = ""
uni.setStorageSync('financeInfo', financeInfo.value)
}
}]
const defualtData = {
benefitsText: '尊享礼遇18000元/年',
cardHolderName: 'XiaoMing',
vipLevel: "v3",
navigationMenuStyle: "style-1",
bgImage: "",
fortune: {
yuebao: { value: 3.53, labelSub: "100000" },
dingqi: { value: 50, labelSub: "100000" },
jijin: { value: 5000, labelSub: "100000" },
huangjin: { value: -300, labelSub: "100000" },
yulingbao: { value: 20, labelSub: "100000" }
},
myAssets: {
baoxian: { labelSub: "3份保单保障中", type: "text" }
},
myQuota: {
huabei: { labelSub: "可用100000", type: "text" },
jubei: { labelSub: "可用100000", type: "text" },
beizhi: { labelSub: "可用100000", type: "text" },
wangshangdai: { labelSub: "预计可借100000", type: "text" }
}
}
onLoad(() => {
// 理财页面埋点
// proxy.$apiUserEvent('all', {
// type: 'click',
// key: 'fortune',
// value: "理财"
// })
proxy.$apiUserEvent('all', {
type: 'click',
key: 'fortune',
value: "理财"
})
})
onReady(() => {
statusBarHeight.value = uni.getSystemInfoSync().statusBarHeight;
uni.createSelectorQuery().select('.total-money-box').boundingClientRect((data) => {
console.log("total-money-box height: " + (data ? data.height : "null"));
topHeight.value = data ? data.height + 44 + statusBarHeight.value : 44 + statusBarHeight.value;
imageCardBoxtopHeight.value = data ? data.height + 44 + statusBarHeight.value : 44 + statusBarHeight.value;
const initialH = data ? data.height + 44 + statusBarHeight.value : 44 + statusBarHeight.value;
topHeight.value = initialH;
minTop = initialH;
imageCardBoxtopHeight.value = initialH;
setTimeout(() => {
isReady.value = true
}, 200)
}).exec()
})
onShow(() => {
financeInfo.value = uni.getStorageSync('financeInfo') || defualtData
console.log("financeInfo.value", financeInfo.value)
// #ifdef APP-PLUS
util.setAndroidSystemBarColor(financeInfo.value.navigationMenuStyle == 'style-1' ? '#ffffff' : '#F8F8F8')
setTimeout(() => {
plus.navigator.setStatusBarStyle("light");
}, 500)
// #endif
})
const updateTopHeight = () => {
setTimeout(() => {
uni.createSelectorQuery().select('.card-arrow-box').boundingClientRect((data) => {
@ -215,7 +351,7 @@ const updateTopHeight = () => {
const totalMoney = computed(() => {
let totalMoney = 0;
for (const key in financeInfo.value.fortune) {
if (!Object.hasOwn(financeInfo.value.fortune, key)) continue;
if (!Object.prototype.hasOwnProperty.call(financeInfo.value.fortune, key)) continue;
const element = financeInfo.value.fortune[key];
totalMoney += Number(element.labelSub)
}
@ -226,7 +362,7 @@ const totalMoney = computed(() => {
const yesterdayIncome = computed(() => {
let yesterdayIncome = 0;
for (const key in financeInfo.value.fortune) {
if (!Object.hasOwn(financeInfo.value.fortune, key)) continue;
if (!Object.prototype.hasOwnProperty.call(financeInfo.value.fortune, key)) continue;
const element = financeInfo.value.fortune[key];
yesterdayIncome += Number(element.value)
}
@ -238,68 +374,38 @@ const familyProtection = computed(() => {
return parseInt(financeInfo.value.myAssets.baoxian.labelSub)
})
const scrollTop = ref(0)
const onImageScroll = (e) => {
scrollTop.value = e.detail.scrollTop
}
const data = reactive({
financeInfo: {
benefitsText: '尊享礼遇18000元/年',
cardHolderName: 'LiXing',
vipLevel: "v3",
navigationMenuStyle: "style-1",
fortune: {
yuebao: {
value: 3.53,
labelSub: "100000",
},
dingqi: {
value: 50,
labelSub: "100000",
},
jijin: {
value: 5000,
labelSub: "100000",
},
huangjin: {
value: -300,
labelSub: "100000",
},
yulingbao: {
value: 20,
labelSub: "100000",
}
},
myAssets: {
baoxian: {
labelSub: "3份保单保障中",
type: "text"
}
},
myQuota: {
huabei: {
labelSub: "可用100000",
type: "text"
},
jubei: {
labelSub: "可用100000",
type: "text"
},
beizhi: {
labelSub: "可用100000",
type: "text"
},
wangshangdai: {
labelSub: "预计可借100000",
type: "text"
}
}
},
financeInfo: { ...defualtData },
topHeight: 0,
statusBarHeight: 0,
cardArrowHeight: 0,
imageCardBoxtopHeight: 0,
isOpen: false
isOpen: false,
isDragging: false,
isReady: false,
selectedImage: '',
showMask: false
})
let { financeInfo, topHeight, statusBarHeight, cardArrowHeight, imageCardBoxtopHeight, isOpen } = toRefs(data)
let {
financeInfo,
topHeight,
statusBarHeight,
cardArrowHeight,
imageCardBoxtopHeight,
isOpen,
isDragging,
isReady,
selectedImage,
showMask
} = toRefs(data)
const formatTextWithNumber = (str) => {
@ -320,6 +426,8 @@ watch(() => isOpen.value, () => {
})
const handleTouchStart = (e) => {
if (selectedImage.value) return
isDragging.value = true;
startY = e.touches[0].clientY;
startTop = topHeight.value;
@ -337,37 +445,12 @@ const handleTouchStart = (e) => {
// 最小高度(关闭状态):total-money-box 高度 + 导航栏 + 状态栏 - 卡片箭头偏移量
minTop = totalMoneyBox.height + 44 + statusBar;
}
if (mainInfoContainer) {
// 最大高度(打开状态):main-info-container 高度 - 卡片箭头高度
// 注意:isOpen 会切换内边距/布局,因此这可能需要根据当前状态进行调整。
// 为简单起见,我们假设我们在初始化/切换期间捕获了正确的值或对其进行了估算。
// 更稳健的方法:
// 关闭状态顶部:~160px(示例)
// 打开状态顶部:~400px(示例)
// 我们可以从之前使用的 'isOpen=true' 计算逻辑推断出 maxTop:
// data.height (main-info-container 的高度)。
// 如果当前已关闭,main-info-container 可能很小。
// 但我们知道 'isOpen=true' 会将 topHeight 设置为 main-info-container 的高度(打开时)。
// 让我们依赖之前记录/使用的值。
// 如果当前已关闭:minTop 是当前 topHeight。
// 如果当前已打开:maxTop 是当前 topHeight。
// 我们需要 *另一个* 值。
// 让我们在 onReady/updateTopHeight 中动态重新测量以确信,
// 但在这里我们可以尝试估算或依赖之前的逻辑。
// 更好的方法:根据 DOM 预先计算两者或使用固定逻辑。
// minTop 大约是 44 + statusBar + totalMoneyBoxHeight
// maxTop 是 mainContainerHeight(展开时)
}
// 目前,让我们使用 handleTouchEnd 中的逻辑来设置这些动态限制
// 我们将在 handleTouchEnd 或 init 中更新它们。
});
}
}
const handleTouchMove = (e) => {
if (selectedImage.value) return
let currentY = e.touches[0].clientY;
let delta = currentY - startY;
let newTop = startTop + delta;
@ -377,10 +460,8 @@ const handleTouchMove = (e) => {
if (maxTop === 0 && isOpen.value) maxTop = startTop;
// 如果有值,则进行简单钳位
if (minTop && maxTop) {
if (newTop < minTop) newTop = minTop;
if (newTop > maxTop) newTop = maxTop;
}
if (minTop && newTop < minTop) newTop = minTop;
if (maxTop && newTop > maxTop) newTop = maxTop;
// 应用
topHeight.value = newTop;
@ -409,7 +490,10 @@ const updateHeightByState = () => {
}
}
const handleTouchEnd = (e) => {
if (selectedImage.value) return
isDragging.value = false;
let endY = e.changedTouches[0].clientY;
let diff = endY - startY;
@ -432,7 +516,212 @@ const handleTouchEnd = (e) => {
}
// 长按事件定时器
let longPressTimer = null
const clickHandleTouchStart = (e) => {
// 兼容iOS上滑HOME条,如果有底部安全区且触摸位置在底部安全区内,则不触发
const systemInfo = uni.getSystemInfoSync()
if (systemInfo.platform === 'ios' && systemInfo.safeAreaInsets?.bottom) {
const clientY = e.touches[0].clientY
const windowHeight = systemInfo.windowHeight
// 如果触摸点在底部安全区范围内(通常是34px),则忽略
if (clientY > windowHeight - systemInfo.safeAreaInsets.bottom) {
return
}
}
longPressTimer = setTimeout(() => {
uni.vibrateShort()
chooseImage()
}, 1200) // 长按时间大于1s
}
// 选择图片
const chooseImage = () => {
if (selectedImage.value) return
uni.chooseImage({
count: 1,
sourceType: ['album'],
success: (res) => {
selectedImage.value = res.tempFilePaths[0]
data.showMask = true
setTimeout(() => {
plus.navigator.setStatusBarStyle("dark");
}, 500)
}
})
}
// 确认图片裁剪
const confirmImage = () => {
uni.showLoading({
title: '处理中...'
})
const query = uni.createSelectorQuery().in(instance)
// 获取容器和图片信息
query.select('.image-box').boundingClientRect()
query.select('.crop-image-target').boundingClientRect()
query.exec(res => {
if (!res[0] || !res[1]) {
uni.hideLoading()
return
}
console.log('rects', res)
const container = res[0] // 容器
const image = res[1] // 图片实际渲染尺寸
// 计算缩放比例 (渲染宽度 / 实际宽度 不准确,应该反过来用 图片原始宽/渲染宽?)
// 这里更简单的方法是:canvas设为容器大小,把图片画进去
// canvas drawImage 参数: img, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight
// 获取图片原始尺寸
uni.getImageInfo({
src: selectedImage.value,
success: (imgInfo) => {
const scale = imgInfo.width / image.width // 图片 原始宽 / 渲染宽
const sTop = scrollTop.value * scale // 原始图上的裁切起始Y
const sHeight = container.height * scale // 原始图上的裁切高度
// 因为是 widthFix,宽度就是原始图宽度(或裁切全宽)
const sWidth = imgInfo.width
// 设置画布尺寸 (使用像素值)
// 注意:canvasContext绘制使用的是逻辑像素还是物理像素?通常需要考虑到 pixelRatio,
// 但 uni-app canvas-id 方式通常对应逻辑像素(px)
// 我们把 canvas 大小设为和容器显示一致
const canvasW = container.width
const canvasH = container.height
const ctx = uni.createCanvasContext('crop-canvas', instance)
// 清除画布
ctx.clearRect(0, 0, canvasW, canvasH)
// 绘制
// drawImage(imageResource, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight)
ctx.drawImage(
imgInfo.path,
0, sTop, sWidth, sHeight, // 源图裁剪区域
0, 0, canvasW, canvasH // 画布绘制区域
)
ctx.draw(false, () => {
uni.canvasToTempFilePath({
canvasId: 'crop-canvas',
width: canvasW,
height: canvasH,
destWidth: sWidth, // 使用原图实际宽度,保持原图清晰度
destHeight: sHeight, // 使用原图实际高度,保持原图清晰度
success: (res) => {
console.log('crop success (temp)', res
.tempFilePath)
// 将临时路径保存为永久路径
uni.saveFile({
tempFilePath: res.tempFilePath,
success: (saveRes) => {
console.log('save success (saved)', saveRes.savedFilePath)
financeInfo.value.bgImage = saveRes.savedFilePath
selectedImage.value = '' // 隐藏编辑模式
setTimeout(() => {
plus.navigator.setStatusBarStyle("light");
}, 200)
// 保存到缓存
uni.setStorageSync("financeInfo", financeInfo.value)
uni.hideLoading()
},
fail: (err) => {
console.error('saveFile fail', err)
uni.hideLoading()
uni.showToast({
title: '保存失败',
icon: 'none'
})
}
})
},
fail: (err) => {
console.error(err)
uni.hideLoading()
uni.showToast({
title: '裁剪失败',
icon: 'none'
})
}
}, instance)
})
},
fail: () => {
uni.hideLoading()
uni.showToast({
title: '图片加载失败',
icon: 'none'
})
}
})
})
}
const clickHandleTouchEnd = () => {
if (longPressTimer) {
clearTimeout(longPressTimer)
longPressTimer = null
}
}
const closeImage = () => {
selectedImage.value = ''
data.showMask = false
plus.navigator.setStatusBarStyle("light");
return false
}
const closeMask = () => {
data.showMask = false
}
const stylePopup = ref(null)
const styleList = [{
label: '样式 1 (默认)',
value: 'style-1'
},
{
label: '样式 2',
value: 'style-2'
}
]
// 打开样式选择弹窗
const openStyleDialog = () => {
stylePopup.value.open()
}
// 关闭样式选择弹窗
const closeStyleDialog = () => {
stylePopup.value.close()
}
// 确认样式选择
const confirmStyleDialog = (type) => {
financeInfo.value.navigationMenuStyle = type
uni.setStorageSync('financeInfo', financeInfo.value)
// #ifdef APP-PLUS
util.setAndroidSystemBarColor(financeInfo.value.navigationMenuStyle == 'style-1' ? '#ffffff' : '#F8F8F8')
// #endif
stylePopup.value.close()
}
const clickTitlePopupButton = (button) => {
button.click()
}
</script>
<style>
@ -592,6 +881,10 @@ const handleTouchEnd = (e) => {
margin-right: 18rpx;
flex-shrink: 0;
}
.label-sub {
white-space: nowrap;
}
}
}
@ -610,6 +903,8 @@ const handleTouchEnd = (e) => {
background: linear-gradient(146deg, #0B1028 49%, #08112E 71.26%, #18336C 83.18%, #18336C 96%)
}
.image-service-box {
position: absolute;
display: flex;
@ -701,6 +996,7 @@ const handleTouchEnd = (e) => {
margin-top: -1px;
background: linear-gradient(180deg, #FFFFFF 56.37%, #F0F4F9 100%);
padding: 20rpx;
padding-bottom: 12rpx;
.service-icons {
display: flex;
@ -730,11 +1026,70 @@ const handleTouchEnd = (e) => {
}
.image-box {
width: 100%;
overflow: hidden; // scroll-view 需要
}
.scroll-image-box {
background-color: #ffffff;
width: 100%;
min-height: 0; // 修复 flex一出问题
position: relative;
}
.dashed-line-box {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
border: 4rpx dashed #ffffff;
pointer-events: none;
.dashed-line-text {
height: 44rpx;
line-height: 44rpx;
width: 180rpx;
padding: 0 20rpx;
border-radius: 8rpx;
color: #1777FF;
font-size: 24rpx;
font-weight: 500;
background-color: #fff;
position: absolute;
top: 0;
left: 50%;
transform: translate(-50%, -50%);
}
}
}
.mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.8);
z-index: 999;
.mask-icon {
position: absolute;
top: 50%;
right: 52rpx;
transform: translateY(-25%);
width: 360rpx;
height: 360rpx;
}
}
.content {
background-color: #ffffff;
padding: 20rpx;
// padding: 20rpx;
margin-top: -1px;
}
@ -742,7 +1097,7 @@ const handleTouchEnd = (e) => {
position: fixed;
bottom: 0;
width: 100%;
height: 100rpx;
// height: 100rpx;
background-color: #ffffff;
display: flex;
justify-content: space-around;
@ -768,11 +1123,7 @@ const handleTouchEnd = (e) => {
}
}
.ios-bottom-box {
padding-bottom: constant(safe-area-inset-bottom); // 兼容 IOS<11.2
padding-bottom: env(safe-area-inset-bottom); // 兼容 IOS>11.2
}
.navigation-menu-style-1 {
background-color: #ffffff;
@ -795,4 +1146,64 @@ const handleTouchEnd = (e) => {
}
}
}
.ios-bottom-box {
padding-bottom: constant(safe-area-inset-bottom); // 兼容 IOS<11.2
padding-bottom: env(safe-area-inset-bottom); // 兼容 IOS>11.2
}
</style>
<style scoped>
.popup-content {
background-color: #fff;
border-radius: 12px;
width: 600rpx;
padding: 20px;
}
.popup-title {
text-align: center;
font-size: 18px;
font-weight: 500;
margin-bottom: 20px;
color: #333;
}
.style-list {
margin-bottom: 20px;
}
.style-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px 10px;
border-bottom: 1px solid #f5f5f5;
font-size: 16px;
color: #333;
}
.style-item:active {
background-color: #f9f9f9;
border-radius: 8px;
}
.popup-btns {
display: flex;
justify-content: center;
}
.btn {
width: 100%;
height: 44px;
line-height: 44px;
text-align: center;
border-radius: 22px;
font-size: 16px;
}
.btn.cancel {
background-color: #f5f5f5;
color: #666;
}
</style>

View File

@ -0,0 +1,162 @@
<template>
<view>
<NavBar title="选择理财图片样式" bgColor="#F5F5F5" isRightButton @right-click="handleRightButtonClick"></NavBar>
<view class="conatiner flex flex-justify-between">
<view v-for="index in 6" :key="index" class="cover-item flex flex-column flex-align-center"
@click="data.selectedImage = index">
<view class="image-box" :class="{ 'active': index == data.selectedImage }">
<image class="w100 h100" :src="`/static/image/finance-management/covers/cover-${index}.png`"
mode="widthFix"></image>
</view>
<view class="text">样式{{ index }}</view>
</view>
</view>
</view>
</template>
<script setup>
import NavBar from '@/components/nav-bar/nav-bar.vue'
import { ref, reactive, toRefs, onMounted } from 'vue';
const defualtData = {
benefitsText: '尊享礼遇18000元/年',
cardHolderName: 'XiaoMing',
vipLevel: "v3",
navigationMenuStyle: "style-1",
bgImage: "",
fortune: {
yuebao: {
value: 3.53,
labelSub: "100000",
},
dingqi: {
value: 50,
labelSub: "100000",
},
jijin: {
value: 5000,
labelSub: "100000",
},
huangjin: {
value: -300,
labelSub: "100000",
},
yulingbao: {
value: 20,
labelSub: "100000",
}
},
myAssets: {
baoxian: {
labelSub: "3份保单保障中",
type: "text"
}
},
myQuota: {
huabei: {
labelSub: "可用100000",
type: "text"
},
jubei: {
labelSub: "可用100000",
type: "text"
},
beizhi: {
labelSub: "可用100000",
type: "text"
},
wangshangdai: {
labelSub: "预计可借100000",
type: "text"
}
}
}
const data = reactive({
financeInfo: {
...defualtData
},
bgImage: "",
selectedImage: 0
})
const { bgImage, selectedImage, financeInfo } = toRefs(data)
onMounted(() => {
financeInfo.value = uni.getStorageSync('financeInfo') || defualtData
})
const handleRightButtonClick = () => {
const financeInfoData = {
...financeInfo.value
}
financeInfoData.bgImage = `/static/image/finance-management/bg-style/style-${data.selectedImage}.png`
uni.setStorageSync('financeInfo', financeInfoData)
uni.navigateBack()
}
</script>
<style>
@import "@/common/main.css";
page {
background-color: #F5F5F5;
}
</style>
<style lang="less" scoped>
.conatiner {
background-color: #ffffff;
margin: 16rpx 24rpx;
border-radius: 16rpx;
padding: 36rpx;
flex-wrap: wrap;
.image-box {
width: 100%;
overflow: hidden;
border-radius: 24rpx;
}
.cover-item {
width: calc(50% - 13rpx);
flex-shrink: 0;
.text {
font-size: 28rpx;
color: #767676;
margin-top: 8rpx;
margin-bottom: 24rpx;
}
}
.active {
position: relative;
border: 6rpx solid #1A7CFF;
border-radius: 24rpx;
}
.active::before {
content: '';
position: absolute;
top: 0px;
left: 0px;
width: 100%;
height: 100%;
border-radius: 24rpx;
background-color: #1A7CFF;
opacity: 0.1;
z-index: 1;
}
.active::after {
content: '';
position: absolute;
bottom: -1px;
right: -1px;
width: 52rpx;
height: 44rpx;
background-image: url(/static/image/finance-management/active.png);
background-size: 100% 100%;
z-index: 2;
}
}
</style>

View File

@ -42,7 +42,7 @@
</view>
</view>
<view class="notice-box" @click="clickNotice">
<view v-if="noticeInfo.enable" class="notice-box" @click="clickNotice">
<view class="sound-box">
<uni-icons type="sound" size="18" color="#D8D8D8"></uni-icons>
</view>
@ -306,13 +306,16 @@ const setUserData = () => {
// 配置信息 - 安全访问
const configData = storage.get("config")
if (configData && configData.config) {
console.log("公告牌配置信息", configData.config['client.uniapp.notice'])
data.noticeInfo = configData.config['client.uniapp.notice'] || {
text: '加载中...',
url: ''
url: '',
enable: false
}
data.noticeInfo = configData.config['client.uniapp.notice'] || {
text: '加载中...',
url: ''
url: '',
enable: false
}
// 启动走马灯
startMarquee();
@ -320,7 +323,8 @@ const setUserData = () => {
} else {
data.noticeInfo = {
text: '加载中...',
url: ''
url: '',
enable: false
}
data.videoHelpList = []
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 634 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 280 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 335 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 292 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 307 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 327 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 692 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

View File

@ -70,34 +70,6 @@ export const useStore = () => {
return JSON.parse(JSON.stringify(store.billList))
}
// 优化后的监听:使用防抖减少存储频率
const debouncedSaveSettings = debounce((newValue) => {
storage.set('settings', newValue)
}, 500)
watch(
() => store.userInfo,
(newValue) => storage.set('userInfo', newValue),
{ deep: true }
);
watch(
() => store.settings,
(newValue) => debouncedSaveSettings(newValue),
{ deep: true }
);
// 监听billList改变自动保存
watch(
() => store.billList,
(newValue) => {
console.log("store.billList")
storage.set('bill_list', newValue)
},
{ deep: true }
);
return {
store,
addBill,
@ -106,3 +78,31 @@ export const useStore = () => {
getBillList
};
};
// 优化后的监听:使用防抖减少存储频率
const debouncedSaveSettings = debounce((newValue) => {
storage.set('settings', newValue)
}, 500)
watch(
() => store.userInfo,
(newValue) => storage.set('userInfo', newValue),
{ deep: true }
);
watch(
() => store.settings,
(newValue) => debouncedSaveSettings(newValue),
{ deep: true }
);
// 监听billList改变自动保存
watch(
() => store.billList,
(newValue) => {
console.log("store.billList changed, saving...")
storage.set('bill_list', newValue)
},
{ deep: true }
);

View File

@ -393,9 +393,11 @@ export const util = {
* @returns {void}
*/
setAndroidSystemBarColor(backgroundColor, frontColor = "#000000") {
if (deviceUtil.isAndroid()) {
// 使用同步方式判断或直接通过编译宏/plus判断
const isAndroid = uni.getSystemInfoSync().platform === 'android';
if (isAndroid) {
try {
// #ifndef APP-IOS
// #ifdef APP-PLUS
if (plus.os.name === 'Android') {
let color = plus.android.newObject("android.graphics.Color");
let activity = plus.android.runtimeMainActivity();